patchrelay 0.40.0 → 0.41.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/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +15 -1
- package/dist/cli/data.js +45 -0
- package/dist/cli/formatters/text.js +20 -0
- package/dist/cli/help.js +2 -0
- package/dist/cli/watch/state-visualization.js +1 -1
- package/dist/db/issue-session-store.js +3 -8
- package/dist/db/issue-store.js +8 -2
- package/dist/db/migrations.js +5 -0
- package/dist/delegation-audit.js +39 -0
- package/dist/delegation-linked-pr.js +104 -0
- package/dist/issue-overview-query.js +2 -1
- package/dist/issue-session-events.js +11 -3
- package/dist/issue-session.js +2 -0
- package/dist/linear-client.js +17 -0
- package/dist/linear-linked-pr-reconciliation.js +44 -0
- package/dist/merged-linear-completion-reconciler.js +84 -2
- package/dist/remote-pr-state.js +1 -1
- package/dist/run-reconciler.js +131 -22
- package/dist/service-runtime.js +1 -2
- package/dist/service-startup-recovery.js +19 -0
- package/dist/service.js +4 -1
- package/dist/waiting-reason.js +1 -1
- package/dist/webhooks/decision-helpers.js +8 -0
- package/dist/webhooks/desired-stage-recorder.js +149 -35
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
2
2
|
import { getRunTypeFlag } from "../args.js";
|
|
3
3
|
import { CliUsageError } from "../errors.js";
|
|
4
4
|
import { formatJson } from "../formatters/json.js";
|
|
5
|
-
import { formatClose, formatInspect, formatList, formatLive, formatOpen, formatRetry, formatSessionHistory, formatTranscriptSource, formatWorktree } from "../formatters/text.js";
|
|
5
|
+
import { formatAudit, formatClose, formatInspect, formatList, formatLive, formatOpen, formatRetry, formatSessionHistory, formatTranscriptSource, formatWorktree } from "../formatters/text.js";
|
|
6
6
|
import { buildOpenCommand } from "../interactive.js";
|
|
7
7
|
import { writeOutput } from "../output.js";
|
|
8
8
|
export async function handleIssueCommand(params) {
|
|
@@ -36,6 +36,8 @@ export async function handleIssueCommand(params) {
|
|
|
36
36
|
return await handleOpenCommand(nested);
|
|
37
37
|
case "sessions":
|
|
38
38
|
return await handleSessionsCommand(nested);
|
|
39
|
+
case "audit":
|
|
40
|
+
return await handleAuditCommand(nested);
|
|
39
41
|
case "transcript-source":
|
|
40
42
|
return await handleTranscriptSourceCommand(nested);
|
|
41
43
|
case "retry":
|
|
@@ -156,6 +158,18 @@ export async function handleSessionsCommand(params) {
|
|
|
156
158
|
: formatSessionHistory(result, (threadId) => buildOpenCommand(params.config, result.worktreePath ?? "", threadId)));
|
|
157
159
|
return 0;
|
|
158
160
|
}
|
|
161
|
+
export async function handleAuditCommand(params) {
|
|
162
|
+
const issueKey = params.commandArgs[0];
|
|
163
|
+
if (!issueKey) {
|
|
164
|
+
throw new Error("audit requires <issueKey>.");
|
|
165
|
+
}
|
|
166
|
+
const result = params.data.audit(issueKey);
|
|
167
|
+
if (!result) {
|
|
168
|
+
throw new Error(`Issue not found: ${issueKey}`);
|
|
169
|
+
}
|
|
170
|
+
writeOutput(params.stdout, params.json ? formatJson(result) : formatAudit(result));
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
159
173
|
export async function handleRetryCommand(params) {
|
|
160
174
|
const issueKey = params.commandArgs[0];
|
|
161
175
|
if (!issueKey) {
|
package/dist/cli/data.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getThreadTurns } from "../codex-thread-utils.js";
|
|
|
7
7
|
import { PatchRelayDatabase } from "../db.js";
|
|
8
8
|
import { buildManualRetryAttemptReset, resolveRetryTarget } from "../manual-issue-actions.js";
|
|
9
9
|
import { WorktreeManager } from "../worktree-manager.js";
|
|
10
|
+
import { parseDelegationObservedPayload, parseRunReleasedAuthorityPayload } from "../delegation-audit.js";
|
|
10
11
|
import { CliOperatorApiClient } from "./operator-client.js";
|
|
11
12
|
function safeJsonParse(value) {
|
|
12
13
|
if (!value)
|
|
@@ -273,6 +274,50 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
273
274
|
...(run ? { releasedRunId: run.id } : {}),
|
|
274
275
|
};
|
|
275
276
|
}
|
|
277
|
+
audit(issueKey) {
|
|
278
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
279
|
+
if (!issue)
|
|
280
|
+
return undefined;
|
|
281
|
+
const events = this.db.issueSessions
|
|
282
|
+
.listIssueSessionEvents(issue.projectId, issue.linearIssueId)
|
|
283
|
+
.flatMap((event) => {
|
|
284
|
+
const delegationObserved = parseDelegationObservedPayload(event);
|
|
285
|
+
if (delegationObserved) {
|
|
286
|
+
return [{
|
|
287
|
+
createdAt: event.createdAt,
|
|
288
|
+
eventType: event.eventType,
|
|
289
|
+
summary: [
|
|
290
|
+
delegationObserved.source,
|
|
291
|
+
`observed=${delegationObserved.observedDelegatedToPatchRelay ? "delegated" : "undelegated"}`,
|
|
292
|
+
`applied=${delegationObserved.appliedDelegatedToPatchRelay ? "delegated" : "undelegated"}`,
|
|
293
|
+
`hydration=${delegationObserved.hydration}`,
|
|
294
|
+
delegationObserved.reason ? `reason=${delegationObserved.reason}` : undefined,
|
|
295
|
+
].filter(Boolean).join(" "),
|
|
296
|
+
details: delegationObserved,
|
|
297
|
+
}];
|
|
298
|
+
}
|
|
299
|
+
const authorityRelease = parseRunReleasedAuthorityPayload(event);
|
|
300
|
+
if (authorityRelease) {
|
|
301
|
+
return [{
|
|
302
|
+
createdAt: event.createdAt,
|
|
303
|
+
eventType: event.eventType,
|
|
304
|
+
summary: `released run #${authorityRelease.runId} (${authorityRelease.runType}) via ${authorityRelease.source}: ${authorityRelease.reason}`,
|
|
305
|
+
details: authorityRelease,
|
|
306
|
+
}];
|
|
307
|
+
}
|
|
308
|
+
if (event.eventType === "delegated" || event.eventType === "undelegated") {
|
|
309
|
+
return [{
|
|
310
|
+
createdAt: event.createdAt,
|
|
311
|
+
eventType: event.eventType,
|
|
312
|
+
summary: event.eventType === "delegated"
|
|
313
|
+
? "PatchRelay accepted delegation"
|
|
314
|
+
: "PatchRelay recorded undelegation",
|
|
315
|
+
}];
|
|
316
|
+
}
|
|
317
|
+
return [];
|
|
318
|
+
});
|
|
319
|
+
return { issue, events };
|
|
320
|
+
}
|
|
276
321
|
transcriptSource(issueKey, runId) {
|
|
277
322
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
278
323
|
if (!issue)
|
|
@@ -84,6 +84,26 @@ export function formatRetry(result) {
|
|
|
84
84
|
.filter(Boolean)
|
|
85
85
|
.join("\n")}\n`;
|
|
86
86
|
}
|
|
87
|
+
export function formatAudit(result) {
|
|
88
|
+
const lines = [
|
|
89
|
+
`${result.issue.issueKey ?? result.issue.linearIssueId}${result.issue.currentLinearState ? ` ${result.issue.currentLinearState}` : ""}`,
|
|
90
|
+
];
|
|
91
|
+
if (result.events.length === 0) {
|
|
92
|
+
lines.push("No delegation audit events recorded.");
|
|
93
|
+
return `${lines.join("\n")}\n`;
|
|
94
|
+
}
|
|
95
|
+
for (const event of result.events) {
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push([event.createdAt, event.eventType].join(" "));
|
|
98
|
+
lines.push(event.summary);
|
|
99
|
+
if (event.details && Object.keys(event.details).length > 0) {
|
|
100
|
+
lines.push(Object.entries(event.details)
|
|
101
|
+
.map(([key, value]) => `${key}=${value === undefined ? "-" : typeof value === "string" ? value : JSON.stringify(value)}`)
|
|
102
|
+
.join(" "));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return `${lines.join("\n")}\n`;
|
|
106
|
+
}
|
|
87
107
|
export function formatClose(result) {
|
|
88
108
|
return `${[
|
|
89
109
|
value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
|
package/dist/cli/help.js
CHANGED
|
@@ -34,6 +34,7 @@ export function rootHelpText() {
|
|
|
34
34
|
" issue list [--active] [--failed] [--repo <id>] [--json]",
|
|
35
35
|
" List tracked issues",
|
|
36
36
|
" issue show <issueKey> [--json] Show the latest known issue state",
|
|
37
|
+
" issue audit <issueKey> [--json] Show delegation/release audit events for one issue",
|
|
37
38
|
" issue watch <issueKey> [--json] Follow the active run until it settles",
|
|
38
39
|
" issue open <issueKey> [--print] [--json] Open Codex in the issue worktree",
|
|
39
40
|
" issue sessions <issueKey> [--json] Show recorded Codex app-server sessions for one issue",
|
|
@@ -146,6 +147,7 @@ export function issueHelpText() {
|
|
|
146
147
|
"",
|
|
147
148
|
"Commands:",
|
|
148
149
|
" show <issueKey> Show the latest known issue state",
|
|
150
|
+
" audit <issueKey> Show delegation/release audit events",
|
|
149
151
|
" list List tracked issues",
|
|
150
152
|
" watch <issueKey> Follow issue activity until it settles",
|
|
151
153
|
" path <issueKey> Print the issue worktree path",
|
|
@@ -127,7 +127,7 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
127
127
|
case "awaiting_queue":
|
|
128
128
|
observations.push({
|
|
129
129
|
tone: "info",
|
|
130
|
-
text: "PatchRelay has finished active work
|
|
130
|
+
text: "PatchRelay has finished active work. Delivery now depends on downstream review and merge automation.",
|
|
131
131
|
});
|
|
132
132
|
break;
|
|
133
133
|
case "repairing_queue":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deriveSessionWakePlan } from "../issue-session-events.js";
|
|
1
|
+
import { deriveSessionWakePlan, isActionableIssueSessionEventType } from "../issue-session-events.js";
|
|
2
2
|
import { isoNow } from "./shared.js";
|
|
3
3
|
export class IssueSessionStore {
|
|
4
4
|
connection;
|
|
@@ -90,13 +90,8 @@ export class IssueSessionStore {
|
|
|
90
90
|
`).run(isoNow(), projectId, linearIssueId);
|
|
91
91
|
}
|
|
92
92
|
hasPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
FROM issue_session_events
|
|
96
|
-
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
97
|
-
LIMIT 1
|
|
98
|
-
`).get(projectId, linearIssueId);
|
|
99
|
-
return row !== undefined;
|
|
93
|
+
return this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true })
|
|
94
|
+
.some((event) => isActionableIssueSessionEventType(event.eventType));
|
|
100
95
|
}
|
|
101
96
|
peekIssueSessionWake(projectId, linearIssueId) {
|
|
102
97
|
const issue = this.issues.getIssue(projectId, linearIssueId);
|
package/dist/db/issue-store.js
CHANGED
|
@@ -104,6 +104,10 @@ export class IssueStore {
|
|
|
104
104
|
sets.push("pr_state = @prState");
|
|
105
105
|
values.prState = params.prState;
|
|
106
106
|
}
|
|
107
|
+
if (params.prIsDraft !== undefined) {
|
|
108
|
+
sets.push("pr_is_draft = @prIsDraft");
|
|
109
|
+
values.prIsDraft = params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0;
|
|
110
|
+
}
|
|
107
111
|
if (params.prHeadSha !== undefined) {
|
|
108
112
|
sets.push("pr_head_sha = @prHeadSha");
|
|
109
113
|
values.prHeadSha = params.prHeadSha;
|
|
@@ -218,7 +222,7 @@ export class IssueStore {
|
|
|
218
222
|
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
219
223
|
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
220
224
|
agent_session_id, last_linear_activity_key,
|
|
221
|
-
pr_number, pr_url, pr_state, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
|
|
225
|
+
pr_number, pr_url, pr_state, pr_is_draft, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
|
|
222
226
|
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at,
|
|
223
227
|
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,
|
|
224
228
|
last_queue_signal_at, last_queue_incident_json,
|
|
@@ -231,7 +235,7 @@ export class IssueStore {
|
|
|
231
235
|
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
232
236
|
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
233
237
|
@agentSessionId, @lastLinearActivityKey,
|
|
234
|
-
@prNumber, @prUrl, @prState, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
|
|
238
|
+
@prNumber, @prUrl, @prState, @prIsDraft, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
|
|
235
239
|
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
236
240
|
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
237
241
|
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
@@ -264,6 +268,7 @@ export class IssueStore {
|
|
|
264
268
|
prNumber: params.prNumber ?? null,
|
|
265
269
|
prUrl: params.prUrl ?? null,
|
|
266
270
|
prState: params.prState ?? null,
|
|
271
|
+
prIsDraft: params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0,
|
|
267
272
|
prHeadSha: params.prHeadSha ?? null,
|
|
268
273
|
prAuthorLogin: params.prAuthorLogin ?? null,
|
|
269
274
|
prReviewState: params.prReviewState ?? null,
|
|
@@ -495,6 +500,7 @@ export function mapIssueRow(row) {
|
|
|
495
500
|
...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
|
|
496
501
|
...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
|
|
497
502
|
...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
|
|
503
|
+
...(row.pr_is_draft !== null && row.pr_is_draft !== undefined ? { prIsDraft: Boolean(row.pr_is_draft) } : {}),
|
|
498
504
|
...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
499
505
|
...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
|
|
500
506
|
...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
|
package/dist/db/migrations.js
CHANGED
|
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
22
22
|
pr_number INTEGER,
|
|
23
23
|
pr_url TEXT,
|
|
24
24
|
pr_state TEXT,
|
|
25
|
+
pr_is_draft INTEGER,
|
|
25
26
|
pr_head_sha TEXT,
|
|
26
27
|
pr_author_login TEXT,
|
|
27
28
|
pr_review_state TEXT,
|
|
@@ -275,6 +276,7 @@ export function runPatchRelayMigrations(connection) {
|
|
|
275
276
|
// branch CI failures from merge-queue evictions after webhook delivery.
|
|
276
277
|
addColumnIfMissing(connection, "issues", "pr_head_sha", "TEXT");
|
|
277
278
|
addColumnIfMissing(connection, "issues", "pr_author_login", "TEXT");
|
|
279
|
+
addColumnIfMissing(connection, "issues", "pr_is_draft", "INTEGER");
|
|
278
280
|
addColumnIfMissing(connection, "issues", "last_github_failure_source", "TEXT");
|
|
279
281
|
addColumnIfMissing(connection, "issues", "last_github_failure_head_sha", "TEXT");
|
|
280
282
|
addColumnIfMissing(connection, "issues", "last_github_failure_signature", "TEXT");
|
|
@@ -335,6 +337,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
335
337
|
pr_number INTEGER,
|
|
336
338
|
pr_url TEXT,
|
|
337
339
|
pr_state TEXT,
|
|
340
|
+
pr_is_draft INTEGER,
|
|
338
341
|
pr_head_sha TEXT,
|
|
339
342
|
pr_author_login TEXT,
|
|
340
343
|
pr_review_state TEXT,
|
|
@@ -391,6 +394,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
391
394
|
pr_number,
|
|
392
395
|
pr_url,
|
|
393
396
|
pr_state,
|
|
397
|
+
pr_is_draft,
|
|
394
398
|
pr_head_sha,
|
|
395
399
|
pr_author_login,
|
|
396
400
|
pr_review_state,
|
|
@@ -445,6 +449,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
|
|
|
445
449
|
pr_number,
|
|
446
450
|
pr_url,
|
|
447
451
|
pr_state,
|
|
452
|
+
pr_is_draft,
|
|
448
453
|
pr_head_sha,
|
|
449
454
|
pr_author_login,
|
|
450
455
|
pr_review_state,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function appendDelegationObservedEvent(db, params) {
|
|
2
|
+
db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.projectId, params.linearIssueId, {
|
|
3
|
+
projectId: params.projectId,
|
|
4
|
+
linearIssueId: params.linearIssueId,
|
|
5
|
+
eventType: "delegation_observed",
|
|
6
|
+
eventJson: JSON.stringify(params.payload),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export function appendRunReleasedAuthorityEvent(db, params) {
|
|
10
|
+
db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.projectId, params.linearIssueId, {
|
|
11
|
+
projectId: params.projectId,
|
|
12
|
+
linearIssueId: params.linearIssueId,
|
|
13
|
+
eventType: "run_released_authority",
|
|
14
|
+
eventJson: JSON.stringify(params.payload),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function parseDelegationObservedPayload(event) {
|
|
18
|
+
if (event.eventType !== "delegation_observed" || !event.eventJson) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return parseObject(event.eventJson);
|
|
22
|
+
}
|
|
23
|
+
export function parseRunReleasedAuthorityPayload(event) {
|
|
24
|
+
if (event.eventType !== "run_released_authority" || !event.eventJson) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return parseObject(event.eventJson);
|
|
28
|
+
}
|
|
29
|
+
function parseObject(raw) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
33
|
+
? parsed
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
2
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
3
|
+
import { buildClosedPrCleanupFields } from "./pr-state.js";
|
|
4
|
+
import { buildReviewFixBranchUpkeepContext, normalizeRemotePrState, normalizeRemoteReviewDecision, } from "./reactive-pr-state.js";
|
|
5
|
+
export function deriveLinkedPrAdoptionOutcome(project, prNumber, remote) {
|
|
6
|
+
const prState = normalizeRemotePrState(remote.state);
|
|
7
|
+
const reviewState = normalizeRemoteReviewDecision(remote.reviewDecision);
|
|
8
|
+
const configuredGateChecks = (project.gateChecks ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
9
|
+
const gateCheckNames = configuredGateChecks.length > 0 ? configuredGateChecks : ["verify"];
|
|
10
|
+
const primaryGateCheck = gateCheckNames[0];
|
|
11
|
+
const gateCheckStatus = deriveGateCheckStatusFromRollup(remote.statusCheckRollup, gateCheckNames);
|
|
12
|
+
const mergeConflictDetected = remote.mergeable === "CONFLICTING" || remote.mergeStateStatus === "DIRTY";
|
|
13
|
+
const downstreamOwned = reviewState === "approved";
|
|
14
|
+
const issueUpdates = {
|
|
15
|
+
prNumber,
|
|
16
|
+
...(remote.url ? { prUrl: remote.url } : {}),
|
|
17
|
+
...(prState ? { prState } : {}),
|
|
18
|
+
...(typeof remote.isDraft === "boolean" ? { prIsDraft: remote.isDraft } : {}),
|
|
19
|
+
...(remote.headRefName ? { branchName: remote.headRefName } : {}),
|
|
20
|
+
...(remote.headRefOid ? { prHeadSha: remote.headRefOid } : {}),
|
|
21
|
+
...(remote.author?.login ? { prAuthorLogin: remote.author.login } : {}),
|
|
22
|
+
...(reviewState ? { prReviewState: reviewState } : {}),
|
|
23
|
+
...(gateCheckStatus ? { prCheckStatus: gateCheckStatus } : {}),
|
|
24
|
+
...(reviewState === "changes_requested"
|
|
25
|
+
? { lastBlockingReviewHeadSha: remote.headRefOid ?? null }
|
|
26
|
+
: { lastBlockingReviewHeadSha: null }),
|
|
27
|
+
...(remote.headRefOid && gateCheckStatus
|
|
28
|
+
? {
|
|
29
|
+
lastGitHubCiSnapshotHeadSha: remote.headRefOid,
|
|
30
|
+
lastGitHubCiSnapshotGateCheckName: primaryGateCheck,
|
|
31
|
+
lastGitHubCiSnapshotGateCheckStatus: gateCheckStatus,
|
|
32
|
+
lastGitHubCiSnapshotSettledAt: gateCheckStatus === "pending" ? null : new Date().toISOString(),
|
|
33
|
+
}
|
|
34
|
+
: {}),
|
|
35
|
+
};
|
|
36
|
+
if (prState === "merged") {
|
|
37
|
+
return {
|
|
38
|
+
factoryState: "done",
|
|
39
|
+
pendingRunType: null,
|
|
40
|
+
issueUpdates: {
|
|
41
|
+
...issueUpdates,
|
|
42
|
+
prIsDraft: false,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (prState === "closed") {
|
|
47
|
+
return {
|
|
48
|
+
factoryState: "awaiting_input",
|
|
49
|
+
pendingRunType: null,
|
|
50
|
+
issueUpdates: {
|
|
51
|
+
...issueUpdates,
|
|
52
|
+
prIsDraft: false,
|
|
53
|
+
...buildClosedPrCleanupFields(),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (remote.isCrossRepository) {
|
|
58
|
+
return {
|
|
59
|
+
factoryState: "awaiting_input",
|
|
60
|
+
pendingRunType: null,
|
|
61
|
+
issueUpdates,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (remote.isDraft) {
|
|
65
|
+
return {
|
|
66
|
+
factoryState: "delegated",
|
|
67
|
+
pendingRunType: "implementation",
|
|
68
|
+
issueUpdates,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
72
|
+
delegatedToPatchRelay: true,
|
|
73
|
+
prNumber,
|
|
74
|
+
prState,
|
|
75
|
+
prReviewState: reviewState,
|
|
76
|
+
prCheckStatus: gateCheckStatus,
|
|
77
|
+
mergeConflictDetected,
|
|
78
|
+
downstreamOwned,
|
|
79
|
+
});
|
|
80
|
+
if (reactiveIntent) {
|
|
81
|
+
return {
|
|
82
|
+
factoryState: reactiveIntent.compatibilityFactoryState,
|
|
83
|
+
pendingRunType: reactiveIntent.runType,
|
|
84
|
+
...(reactiveIntent.runType === "branch_upkeep"
|
|
85
|
+
? {
|
|
86
|
+
pendingRunContext: buildReviewFixBranchUpkeepContext(prNumber, project.github?.baseBranch ?? "main", remote),
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
issueUpdates,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (reviewState === "approved") {
|
|
93
|
+
return {
|
|
94
|
+
factoryState: "awaiting_queue",
|
|
95
|
+
pendingRunType: null,
|
|
96
|
+
issueUpdates,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
factoryState: "pr_open",
|
|
101
|
+
pendingRunType: null,
|
|
102
|
+
issueUpdates,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -88,7 +88,7 @@ export class IssueOverviewQuery {
|
|
|
88
88
|
const runCount = runs.length;
|
|
89
89
|
const liveThread = await this.readLiveThread(activeRun);
|
|
90
90
|
const failureContext = parseGitHubFailureContext(issueRecord?.lastGitHubFailureContextJson);
|
|
91
|
-
const
|
|
91
|
+
const derivedWaitingReason = derivePatchRelayWaitingReason({
|
|
92
92
|
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
93
93
|
...(activeRun ? { activeRunType: activeRun.runType } : {}),
|
|
94
94
|
blockedByKeys,
|
|
@@ -102,6 +102,7 @@ export class IssueOverviewQuery {
|
|
|
102
102
|
lastBlockingReviewHeadSha: issueRecord?.lastBlockingReviewHeadSha,
|
|
103
103
|
latestFailureCheckName: issueRecord?.lastGitHubFailureCheckName,
|
|
104
104
|
});
|
|
105
|
+
const waitingReason = derivedWaitingReason ?? session.waitingReason;
|
|
105
106
|
const issue = {
|
|
106
107
|
id: issueRecord?.id ?? session.id,
|
|
107
108
|
projectId: session.projectId,
|
|
@@ -7,14 +7,19 @@ const TERMINAL_SESSION_EVENTS = new Set([
|
|
|
7
7
|
"pr_closed",
|
|
8
8
|
"pr_merged",
|
|
9
9
|
]);
|
|
10
|
+
const NON_ACTIONABLE_SESSION_EVENTS = new Set([
|
|
11
|
+
"delegation_observed",
|
|
12
|
+
"run_released_authority",
|
|
13
|
+
]);
|
|
10
14
|
const RUN_TYPES = new Set(["implementation", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
|
|
11
15
|
function parseRunType(value) {
|
|
12
16
|
return typeof value === "string" && RUN_TYPES.has(value) ? value : undefined;
|
|
13
17
|
}
|
|
14
18
|
export function deriveSessionWakePlan(issue, events) {
|
|
15
|
-
|
|
19
|
+
const actionableEvents = events.filter((event) => !NON_ACTIONABLE_SESSION_EVENTS.has(event.eventType));
|
|
20
|
+
if (actionableEvents.length === 0)
|
|
16
21
|
return undefined;
|
|
17
|
-
if (
|
|
22
|
+
if (actionableEvents.some((event) => TERMINAL_SESSION_EVENTS.has(event.eventType))) {
|
|
18
23
|
return undefined;
|
|
19
24
|
}
|
|
20
25
|
const context = {};
|
|
@@ -22,7 +27,7 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
22
27
|
let wakeReason;
|
|
23
28
|
let runType;
|
|
24
29
|
let resumeThread = false;
|
|
25
|
-
for (const event of
|
|
30
|
+
for (const event of actionableEvents) {
|
|
26
31
|
const payload = parseEventJson(event.eventJson);
|
|
27
32
|
switch (event.eventType) {
|
|
28
33
|
case "merge_steward_incident":
|
|
@@ -128,6 +133,9 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
128
133
|
}
|
|
129
134
|
return { runType, wakeReason, resumeThread, context };
|
|
130
135
|
}
|
|
136
|
+
export function isActionableIssueSessionEventType(eventType) {
|
|
137
|
+
return !NON_ACTIONABLE_SESSION_EVENTS.has(eventType);
|
|
138
|
+
}
|
|
131
139
|
export function extractLatestAssistantSummary(run) {
|
|
132
140
|
if (!run)
|
|
133
141
|
return undefined;
|
package/dist/issue-session.js
CHANGED
|
@@ -49,6 +49,8 @@ export function deriveIssueSessionReactiveIntent(params) {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
if (params.prState && params.prState !== "open")
|
|
51
51
|
return undefined;
|
|
52
|
+
if (params.prIsDraft)
|
|
53
|
+
return undefined;
|
|
52
54
|
if (params.latestFailureSource === "queue_eviction" || (params.mergeConflictDetected && params.downstreamOwned)) {
|
|
53
55
|
return {
|
|
54
56
|
runType: "queue_repair",
|
package/dist/linear-client.js
CHANGED
|
@@ -6,6 +6,14 @@ const LINEAR_ISSUE_SELECTION = `
|
|
|
6
6
|
title
|
|
7
7
|
description
|
|
8
8
|
url
|
|
9
|
+
attachments {
|
|
10
|
+
nodes {
|
|
11
|
+
id
|
|
12
|
+
title
|
|
13
|
+
subtitle
|
|
14
|
+
url
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
priority
|
|
10
18
|
estimate
|
|
11
19
|
delegate {
|
|
@@ -316,12 +324,21 @@ export class LinearGraphqlClient {
|
|
|
316
324
|
const teamLabels = (issue.team?.labels?.nodes ?? []).map((label) => ({ id: label.id, name: label.name }));
|
|
317
325
|
const blocksRelations = (issue.relations?.nodes ?? []).filter((relation) => relation.type?.trim().toLowerCase() === "blocks");
|
|
318
326
|
const blockedByRelations = (issue.inverseRelations?.nodes ?? []).filter((relation) => relation.type?.trim().toLowerCase() === "blocks");
|
|
327
|
+
const attachments = (issue.attachments?.nodes ?? [])
|
|
328
|
+
.filter((attachment) => Boolean(attachment?.url))
|
|
329
|
+
.map((attachment) => ({
|
|
330
|
+
id: attachment.id,
|
|
331
|
+
...(attachment.title ? { title: attachment.title } : {}),
|
|
332
|
+
...(attachment.subtitle ? { subtitle: attachment.subtitle } : {}),
|
|
333
|
+
url: attachment.url,
|
|
334
|
+
}));
|
|
319
335
|
return {
|
|
320
336
|
id: issue.id,
|
|
321
337
|
...(issue.identifier ? { identifier: issue.identifier } : {}),
|
|
322
338
|
...(issue.title ? { title: issue.title } : {}),
|
|
323
339
|
...(issue.description ? { description: issue.description } : {}),
|
|
324
340
|
...(issue.url ? { url: issue.url } : {}),
|
|
341
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
325
342
|
...(issue.priority != null ? { priority: issue.priority } : {}),
|
|
326
343
|
...(issue.estimate != null ? { estimate: issue.estimate } : {}),
|
|
327
344
|
...(issue.state?.id ? { stateId: issue.state.id } : {}),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const GITHUB_PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#].*)?$/i;
|
|
2
|
+
export function resolveLinkedPullRequest(attachments, repoFullName) {
|
|
3
|
+
if (!repoFullName || !attachments || attachments.length === 0) {
|
|
4
|
+
return { kind: "none" };
|
|
5
|
+
}
|
|
6
|
+
const matches = attachments
|
|
7
|
+
.map((attachment) => parseGitHubPullRequestUrl(attachment.url))
|
|
8
|
+
.filter((reference) => Boolean(reference))
|
|
9
|
+
.filter((reference) => reference.repoFullName.toLowerCase() === repoFullName.toLowerCase());
|
|
10
|
+
const unique = dedupeReferences(matches);
|
|
11
|
+
if (unique.length === 0) {
|
|
12
|
+
return { kind: "none" };
|
|
13
|
+
}
|
|
14
|
+
if (unique.length === 1) {
|
|
15
|
+
return { kind: "matched", reference: unique[0] };
|
|
16
|
+
}
|
|
17
|
+
return { kind: "ambiguous", references: unique };
|
|
18
|
+
}
|
|
19
|
+
function parseGitHubPullRequestUrl(url) {
|
|
20
|
+
const match = url.trim().match(GITHUB_PR_URL_PATTERN);
|
|
21
|
+
if (!match)
|
|
22
|
+
return undefined;
|
|
23
|
+
const [, owner, repo, prNumberRaw] = match;
|
|
24
|
+
const prNumber = Number(prNumberRaw);
|
|
25
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
return {
|
|
28
|
+
repoFullName: `${owner}/${repo}`,
|
|
29
|
+
prNumber,
|
|
30
|
+
url,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function dedupeReferences(references) {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const unique = [];
|
|
36
|
+
for (const reference of references) {
|
|
37
|
+
const key = `${reference.repoFullName.toLowerCase()}#${reference.prNumber}`;
|
|
38
|
+
if (seen.has(key))
|
|
39
|
+
continue;
|
|
40
|
+
seen.add(key);
|
|
41
|
+
unique.push(reference);
|
|
42
|
+
}
|
|
43
|
+
return unique;
|
|
44
|
+
}
|