patchrelay 0.36.19 → 0.37.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/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +25 -22
- package/dist/cli/data.js +1 -0
- package/dist/cli/formatters/text.js +5 -1
- package/dist/cli/watch/App.js +226 -27
- package/dist/cli/watch/HelpBar.js +18 -9
- package/dist/cli/watch/IssueDetailView.js +32 -14
- package/dist/cli/watch/IssueRow.js +4 -3
- package/dist/cli/watch/StatusBar.js +2 -1
- package/dist/cli/watch/detail-rows.js +5 -25
- package/dist/cli/watch/detail-status.js +38 -0
- package/dist/cli/watch/layout-measure.js +7 -0
- package/dist/cli/watch/pr-status.js +2 -1
- package/dist/cli/watch/prompt-layout.js +14 -0
- package/dist/cli/watch/state-visualization.js +5 -1
- package/dist/cli/watch/timeline-builder.js +169 -18
- package/dist/cli/watch/timeline-presentation.js +21 -1
- package/dist/cli/watch/transient-status.js +28 -0
- package/dist/cli/watch/watch-actions.js +76 -0
- package/dist/cli/watch/watch-state.js +2 -12
- package/dist/factory-state.js +1 -1
- package/dist/github-webhook-handler.js +26 -4
- package/dist/idle-reconciliation.js +19 -2
- package/dist/implementation-outcome-policy.js +3 -1
- package/dist/issue-overview-query.js +5 -0
- package/dist/linear-session-reporting.js +15 -6
- package/dist/linear-status-comment-sync.js +13 -1
- package/dist/pr-state.js +49 -0
- package/dist/service-issue-actions.js +5 -4
- package/dist/tracked-issue-list-query.js +3 -1
- package/dist/tracked-issue-projector.js +5 -0
- package/dist/waiting-reason.js +3 -2
- package/package.json +1 -1
- package/dist/cli/watch/ItemLine.js +0 -80
- package/dist/cli/watch/Timeline.js +0 -22
- package/dist/cli/watch/TimelineRow.js +0 -77
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveFactoryStateFromGitHub
|
|
1
|
+
import { resolveFactoryStateFromGitHub } from "./factory-state.js";
|
|
2
2
|
import { createGitHubCiSnapshotResolver, createGitHubFailureContextResolver, summarizeGitHubFailureContext, } from "./github-failure-context.js";
|
|
3
3
|
import { normalizeGitHubWebhook, verifyGitHubWebhookSignature } from "./github-webhooks.js";
|
|
4
4
|
import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
|
|
@@ -7,6 +7,7 @@ import { buildGitHubStateActivity } from "./linear-session-reporting.js";
|
|
|
7
7
|
import { resolveMergeQueueProtocol, } from "./merge-queue-protocol.js";
|
|
8
8
|
import { buildQueueRepairContextFromEvent } from "./merge-queue-incident.js";
|
|
9
9
|
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
10
|
+
import { buildClosedPrCleanupFields, isIssueTerminal, resolveClosedPrFactoryState, resolveClosedPrDisposition, } from "./pr-state.js";
|
|
10
11
|
import { resolveSecret } from "./resolve-secret.js";
|
|
11
12
|
import { safeJsonParse } from "./utils.js";
|
|
12
13
|
/**
|
|
@@ -153,6 +154,9 @@ export class GitHubWebhookHandler {
|
|
|
153
154
|
: event.reviewState === "approved"
|
|
154
155
|
? { lastBlockingReviewHeadSha: null }
|
|
155
156
|
: {}),
|
|
157
|
+
...(event.triggerEvent === "pr_closed"
|
|
158
|
+
? buildClosedPrCleanupFields()
|
|
159
|
+
: {}),
|
|
156
160
|
});
|
|
157
161
|
await this.updateCiSnapshot(issue, event, project);
|
|
158
162
|
await this.updateFailureProvenance(issue, event, project);
|
|
@@ -227,11 +231,14 @@ export class GitHubWebhookHandler {
|
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
233
|
resolveFactoryStateForEvent(issue, event, project) {
|
|
234
|
+
if (event.triggerEvent === "pr_closed") {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
230
237
|
if (event.triggerEvent === "check_failed"
|
|
231
238
|
&& this.isQueueEvictionFailure(issue, event, project)
|
|
232
239
|
&& issue.prState === "open"
|
|
233
240
|
&& issue.activeRunId === undefined
|
|
234
|
-
&& !
|
|
241
|
+
&& !isIssueTerminal(issue)) {
|
|
235
242
|
return "repairing_queue";
|
|
236
243
|
}
|
|
237
244
|
return resolveFactoryStateFromGitHub(event.triggerEvent, issue.factoryState, {
|
|
@@ -318,7 +325,7 @@ export class GitHubWebhookHandler {
|
|
|
318
325
|
return;
|
|
319
326
|
// Don't trigger on terminal issues — late-arriving webhooks (e.g.
|
|
320
327
|
// merge_group_failed after pr_merged) must not resurrect done issues.
|
|
321
|
-
if (
|
|
328
|
+
if (isIssueTerminal(issue))
|
|
322
329
|
return;
|
|
323
330
|
if (!this.isPatchRelayOwnedPr(issue)) {
|
|
324
331
|
this.feed?.publish({
|
|
@@ -521,11 +528,14 @@ export class GitHubWebhookHandler {
|
|
|
521
528
|
: "Pull request closed during active run",
|
|
522
529
|
});
|
|
523
530
|
}
|
|
531
|
+
const terminalFactoryState = event.triggerEvent === "pr_merged"
|
|
532
|
+
? "done"
|
|
533
|
+
: resolveClosedPrFactoryState(issue);
|
|
524
534
|
this.db.issues.upsertIssue({
|
|
525
535
|
projectId: issue.projectId,
|
|
526
536
|
linearIssueId: issue.linearIssueId,
|
|
527
537
|
activeRunId: null,
|
|
528
|
-
factoryState:
|
|
538
|
+
factoryState: terminalFactoryState,
|
|
529
539
|
});
|
|
530
540
|
};
|
|
531
541
|
const activeLease = this.db.issueSessions.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
@@ -537,6 +547,18 @@ export class GitHubWebhookHandler {
|
|
|
537
547
|
}
|
|
538
548
|
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
539
549
|
const updatedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
550
|
+
if (event.triggerEvent === "pr_closed" && resolveClosedPrDisposition(issue) === "redelegate") {
|
|
551
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
552
|
+
projectId: issue.projectId,
|
|
553
|
+
linearIssueId: issue.linearIssueId,
|
|
554
|
+
eventType: "delegated",
|
|
555
|
+
dedupeKey: `github_pr_closed:implementation:${issue.linearIssueId}`,
|
|
556
|
+
});
|
|
557
|
+
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
558
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
559
|
+
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
540
562
|
if (event.triggerEvent === "pr_merged") {
|
|
541
563
|
await this.completeLinearIssueAfterMerge(updatedIssue);
|
|
542
564
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import {} from "./factory-state.js";
|
|
1
2
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
2
3
|
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
3
4
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
4
5
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
5
6
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
7
|
+
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
6
8
|
import { execCommand } from "./utils.js";
|
|
7
9
|
const DEFAULT_REVIEW_FIX_BUDGET = 12;
|
|
8
10
|
function isFailingCheckStatus(status) {
|
|
@@ -439,8 +441,23 @@ export class IdleIssueReconciler {
|
|
|
439
441
|
return;
|
|
440
442
|
}
|
|
441
443
|
if (pr.state === "CLOSED") {
|
|
442
|
-
|
|
443
|
-
this.db.issues.upsertIssue({
|
|
444
|
+
const closedPrDisposition = resolveClosedPrDisposition(issue);
|
|
445
|
+
this.db.issues.upsertIssue({
|
|
446
|
+
projectId: issue.projectId,
|
|
447
|
+
linearIssueId: issue.linearIssueId,
|
|
448
|
+
prState: "closed",
|
|
449
|
+
...buildClosedPrCleanupFields(),
|
|
450
|
+
});
|
|
451
|
+
if (closedPrDisposition === "done") {
|
|
452
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed for an already completed issue; preserving done state");
|
|
453
|
+
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (closedPrDisposition === "terminal") {
|
|
457
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, factoryState: issue.factoryState }, "Reconciliation: PR was closed on a terminal issue; preserving terminal state");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed on unfinished work, re-delegating for implementation");
|
|
444
461
|
this.advanceIdleIssue(issue, "delegated", {
|
|
445
462
|
pendingRunType: "implementation",
|
|
446
463
|
clearFailureProvenance: true,
|
|
@@ -46,7 +46,9 @@ export class ImplementationOutcomePolicy {
|
|
|
46
46
|
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
47
47
|
...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
|
|
48
48
|
}, "published PR verification refresh");
|
|
49
|
-
|
|
49
|
+
if (pr.state?.toLowerCase() !== "closed") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -143,6 +143,7 @@ export class IssueOverviewQuery {
|
|
|
143
143
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
144
144
|
pendingRunType: issueRecord?.pendingRunType,
|
|
145
145
|
prNumber: session.prNumber,
|
|
146
|
+
prState: issueRecord?.prState,
|
|
146
147
|
prHeadSha: issueRecord?.prHeadSha ?? session.prHeadSha,
|
|
147
148
|
prReviewState: issueRecord?.prReviewState,
|
|
148
149
|
prCheckStatus: issueRecord?.prCheckStatus,
|
|
@@ -159,6 +160,10 @@ export class IssueOverviewQuery {
|
|
|
159
160
|
...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
|
|
160
161
|
sessionState: session.sessionState,
|
|
161
162
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
163
|
+
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
|
164
|
+
...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
|
|
165
|
+
...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
|
|
166
|
+
...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
|
|
162
167
|
blockedByCount: unresolvedBlockedBy.length,
|
|
163
168
|
blockedByKeys,
|
|
164
169
|
readyForExecution: isIssueSessionReadyForExecution({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatRunTypeLabel } from "./agent-session-plan.js";
|
|
2
2
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
3
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
3
4
|
function lowerRunTypeLabel(runType) {
|
|
4
5
|
return formatRunTypeLabel(runType).toLowerCase();
|
|
5
6
|
}
|
|
@@ -188,15 +189,19 @@ export function buildMergePrepEscalationActivity(attempts) {
|
|
|
188
189
|
export function summarizeIssueStateForLinear(issue) {
|
|
189
190
|
switch (issue.sessionState) {
|
|
190
191
|
case "waiting_input":
|
|
191
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
192
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
192
193
|
case "running":
|
|
193
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
194
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
194
195
|
case "idle":
|
|
195
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is idle.` : "Idle.");
|
|
196
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is idle.` : "Idle.");
|
|
196
197
|
case "done":
|
|
197
|
-
|
|
198
|
+
if (issue.prNumber && issue.prState === "merged")
|
|
199
|
+
return `PR #${issue.prNumber} has merged.`;
|
|
200
|
+
if (issue.prNumber && isClosedPrState(issue.prState))
|
|
201
|
+
return `Completed without merging PR #${issue.prNumber}.`;
|
|
202
|
+
return issue.prNumber ? `Completed with PR #${issue.prNumber}.` : "Completed.";
|
|
198
203
|
case "failed":
|
|
199
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
204
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
200
205
|
}
|
|
201
206
|
switch (issue.factoryState) {
|
|
202
207
|
case "pr_open":
|
|
@@ -204,7 +209,11 @@ export function summarizeIssueStateForLinear(issue) {
|
|
|
204
209
|
case "awaiting_queue":
|
|
205
210
|
return issue.prNumber ? `PR #${issue.prNumber} is approved and awaiting merge.` : "Approved and awaiting merge.";
|
|
206
211
|
case "done":
|
|
207
|
-
|
|
212
|
+
if (issue.prNumber && issue.prState === "merged")
|
|
213
|
+
return `PR #${issue.prNumber} has merged.`;
|
|
214
|
+
if (issue.prNumber && isClosedPrState(issue.prState))
|
|
215
|
+
return `Completed without merging PR #${issue.prNumber}.`;
|
|
216
|
+
return issue.prNumber ? `Completed with PR #${issue.prNumber}.` : "Completed.";
|
|
208
217
|
default:
|
|
209
218
|
return undefined;
|
|
210
219
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
2
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
3
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
4
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
4
5
|
export async function syncVisibleStatusComment(params) {
|
|
5
6
|
const { db, issue, linear, logger, trackedIssue, options } = params;
|
|
6
7
|
try {
|
|
@@ -31,7 +32,9 @@ export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
|
|
|
31
32
|
|| issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
32
33
|
return true;
|
|
33
34
|
}
|
|
34
|
-
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
35
|
+
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
36
|
+
&& ((issue.prNumber === undefined && !issue.prUrl)
|
|
37
|
+
|| isClosedPrState(issue.prState))) {
|
|
35
38
|
return true;
|
|
36
39
|
}
|
|
37
40
|
return false;
|
|
@@ -49,6 +52,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
49
52
|
factoryState: issue.factoryState,
|
|
50
53
|
pendingRunType: issue.pendingRunType,
|
|
51
54
|
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
55
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
52
56
|
prHeadSha: issue.prHeadSha,
|
|
53
57
|
prReviewState: issue.prReviewState,
|
|
54
58
|
prCheckStatus: issue.prCheckStatus,
|
|
@@ -110,6 +114,10 @@ function statusHeadline(issue, activeRunType) {
|
|
|
110
114
|
case "running":
|
|
111
115
|
return issue.prNumber !== undefined ? `PR #${issue.prNumber} is actively running` : "Actively running";
|
|
112
116
|
case "done":
|
|
117
|
+
if (issue.prNumber !== undefined && issue.prState === "merged")
|
|
118
|
+
return `Completed with merged PR #${issue.prNumber}`;
|
|
119
|
+
if (issue.prNumber !== undefined && isClosedPrState(issue.prState))
|
|
120
|
+
return `Completed without merging PR #${issue.prNumber}`;
|
|
113
121
|
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
114
122
|
case "failed":
|
|
115
123
|
return "Needs operator intervention";
|
|
@@ -138,6 +146,10 @@ function statusHeadline(issue, activeRunType) {
|
|
|
138
146
|
case "escalated":
|
|
139
147
|
return "Needs operator intervention";
|
|
140
148
|
case "done":
|
|
149
|
+
if (issue.prNumber !== undefined && issue.prState === "merged")
|
|
150
|
+
return `Completed with merged PR #${issue.prNumber}`;
|
|
151
|
+
if (issue.prNumber !== undefined && isClosedPrState(issue.prState))
|
|
152
|
+
return `Completed without merging PR #${issue.prNumber}`;
|
|
141
153
|
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
142
154
|
default:
|
|
143
155
|
return humanize(issue.factoryState);
|
package/dist/pr-state.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TERMINAL_STATES } from "./factory-state.js";
|
|
2
|
+
export function isOpenPrState(prState) {
|
|
3
|
+
return prState === undefined || prState === "open";
|
|
4
|
+
}
|
|
5
|
+
export function hasOpenPr(prNumber, prState) {
|
|
6
|
+
// Transitional compatibility: older rows may still have a tracked PR number
|
|
7
|
+
// before webhook/reconciliation has populated pr_state.
|
|
8
|
+
return prNumber !== undefined && isOpenPrState(prState);
|
|
9
|
+
}
|
|
10
|
+
export function isClosedPrState(prState) {
|
|
11
|
+
return prState === "closed";
|
|
12
|
+
}
|
|
13
|
+
export function isCompletedLinearState(currentLinearStateType, currentLinearState) {
|
|
14
|
+
return currentLinearStateType === "completed"
|
|
15
|
+
|| currentLinearState?.trim().toLowerCase() === "done";
|
|
16
|
+
}
|
|
17
|
+
export function isIssueCompleted(issue) {
|
|
18
|
+
return issue.factoryState === "done" || isCompletedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
19
|
+
}
|
|
20
|
+
export function isIssueTerminal(issue) {
|
|
21
|
+
return issue.factoryState !== undefined && TERMINAL_STATES.has(issue.factoryState);
|
|
22
|
+
}
|
|
23
|
+
export function resolveClosedPrDisposition(issue) {
|
|
24
|
+
if (isIssueCompleted(issue))
|
|
25
|
+
return "done";
|
|
26
|
+
if (isIssueTerminal(issue))
|
|
27
|
+
return "terminal";
|
|
28
|
+
return "redelegate";
|
|
29
|
+
}
|
|
30
|
+
export function resolveClosedPrFactoryState(issue) {
|
|
31
|
+
const disposition = resolveClosedPrDisposition(issue);
|
|
32
|
+
if (disposition === "done")
|
|
33
|
+
return "done";
|
|
34
|
+
if (disposition === "terminal")
|
|
35
|
+
return issue.factoryState;
|
|
36
|
+
return "delegated";
|
|
37
|
+
}
|
|
38
|
+
export function buildClosedPrCleanupFields() {
|
|
39
|
+
return {
|
|
40
|
+
prReviewState: null,
|
|
41
|
+
prCheckStatus: null,
|
|
42
|
+
lastBlockingReviewHeadSha: null,
|
|
43
|
+
lastGitHubCiSnapshotHeadSha: null,
|
|
44
|
+
lastGitHubCiSnapshotGateCheckName: null,
|
|
45
|
+
lastGitHubCiSnapshotGateCheckStatus: null,
|
|
46
|
+
lastGitHubCiSnapshotJson: null,
|
|
47
|
+
lastGitHubCiSnapshotSettledAt: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildOperatorRetryEvent } from "./operator-retry-event.js";
|
|
2
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
2
3
|
export class ServiceIssueActions {
|
|
3
4
|
db;
|
|
4
5
|
codex;
|
|
@@ -107,21 +108,21 @@ export class ServiceIssueActions {
|
|
|
107
108
|
}
|
|
108
109
|
let runType = "implementation";
|
|
109
110
|
let factoryState = "delegated";
|
|
110
|
-
if (issue.prNumber && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
111
|
+
if (hasOpenPr(issue.prNumber, issue.prState) && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
111
112
|
runType = "queue_repair";
|
|
112
113
|
factoryState = "repairing_queue";
|
|
113
114
|
}
|
|
114
|
-
else if (issue.prNumber && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
115
|
+
else if (hasOpenPr(issue.prNumber, issue.prState) && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
115
116
|
runType = "ci_repair";
|
|
116
117
|
factoryState = "repairing_ci";
|
|
117
118
|
}
|
|
118
|
-
else if (issue.prNumber && issue.prReviewState === "changes_requested") {
|
|
119
|
+
else if (hasOpenPr(issue.prNumber, issue.prState) && issue.prReviewState === "changes_requested") {
|
|
119
120
|
runType = issue.pendingRunType === "branch_upkeep" || issueSession?.lastRunType === "branch_upkeep"
|
|
120
121
|
? "branch_upkeep"
|
|
121
122
|
: "review_fix";
|
|
122
123
|
factoryState = "changes_requested";
|
|
123
124
|
}
|
|
124
|
-
else if (issue.prNumber) {
|
|
125
|
+
else if (hasOpenPr(issue.prNumber, issue.prState)) {
|
|
125
126
|
runType = "implementation";
|
|
126
127
|
factoryState = "implementing";
|
|
127
128
|
}
|
|
@@ -82,7 +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, s.session_state, s.waiting_reason, s.summary_text, s.updated_at,
|
|
84
84
|
i.pending_run_type,
|
|
85
|
-
i.pr_number, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
85
|
+
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
86
|
i.last_github_ci_snapshot_json,
|
|
87
87
|
i.last_github_failure_source,
|
|
88
88
|
i.last_github_failure_head_sha,
|
|
@@ -180,6 +180,7 @@ export class TrackedIssueListQuery {
|
|
|
180
180
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
181
181
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
182
182
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
183
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
183
184
|
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
184
185
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
185
186
|
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
@@ -242,6 +243,7 @@ export class TrackedIssueListQuery {
|
|
|
242
243
|
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
243
244
|
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
244
245
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
246
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
245
247
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
246
248
|
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
247
249
|
...(prChecksSummary ? { prChecksSummary } : {}),
|
|
@@ -15,6 +15,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
15
15
|
factoryState: params.issue.factoryState,
|
|
16
16
|
pendingRunType: params.issue.pendingRunType,
|
|
17
17
|
prNumber: params.issue.prNumber,
|
|
18
|
+
prState: params.issue.prState,
|
|
18
19
|
prHeadSha: params.issue.prHeadSha,
|
|
19
20
|
prReviewState: params.issue.prReviewState,
|
|
20
21
|
prCheckStatus: params.issue.prCheckStatus,
|
|
@@ -46,6 +47,10 @@ export function buildTrackedIssueRecord(params) {
|
|
|
46
47
|
...(params.issue.currentLinearState ? { currentLinearState: params.issue.currentLinearState } : {}),
|
|
47
48
|
...(params.session?.sessionState ? { sessionState: params.session.sessionState } : {}),
|
|
48
49
|
factoryState: params.issue.factoryState,
|
|
50
|
+
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
51
|
+
...(params.issue.prState ? { prState: params.issue.prState } : {}),
|
|
52
|
+
...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
|
|
53
|
+
...(params.issue.prCheckStatus ? { prCheckStatus: params.issue.prCheckStatus } : {}),
|
|
49
54
|
blockedByCount: unresolvedBlockedBy.length,
|
|
50
55
|
blockedByKeys,
|
|
51
56
|
readyForExecution: isIssueSessionReadyForExecution({
|
package/dist/waiting-reason.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
1
2
|
export const PATCHRELAY_WAITING_REASONS = {
|
|
2
3
|
activeWork: "PatchRelay is actively working",
|
|
3
4
|
finalizingPublishedPr: "PatchRelay is finalizing a published PR",
|
|
@@ -14,7 +15,7 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
14
15
|
};
|
|
15
16
|
export function derivePatchRelayWaitingReason(params) {
|
|
16
17
|
if (params.activeRunType) {
|
|
17
|
-
if (params.prNumber
|
|
18
|
+
if (hasOpenPr(params.prNumber, params.prState) && (params.factoryState === "pr_open" || params.factoryState === "awaiting_queue")) {
|
|
18
19
|
return PATCHRELAY_WAITING_REASONS.finalizingPublishedPr;
|
|
19
20
|
}
|
|
20
21
|
if (params.factoryState === "done") {
|
|
@@ -66,7 +67,7 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
66
67
|
if (params.prReviewState === "approved") {
|
|
67
68
|
return PATCHRELAY_WAITING_REASONS.waitingForDownstreamAutomation;
|
|
68
69
|
}
|
|
69
|
-
if (params.prNumber
|
|
70
|
+
if (hasOpenPr(params.prNumber, params.prState)) {
|
|
70
71
|
return PATCHRELAY_WAITING_REASONS.waitingForExternalReview;
|
|
71
72
|
}
|
|
72
73
|
if (params.pendingRunType) {
|
package/package.json
CHANGED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
function cleanCommand(raw) {
|
|
4
|
-
const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
|
|
5
|
-
if (bashMatch?.[1])
|
|
6
|
-
return bashMatch[1];
|
|
7
|
-
const bashMatch2 = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+"(.+?)"$/s);
|
|
8
|
-
if (bashMatch2?.[1])
|
|
9
|
-
return bashMatch2[1];
|
|
10
|
-
return raw;
|
|
11
|
-
}
|
|
12
|
-
function summarizeFileChange(item) {
|
|
13
|
-
const count = item.changes?.length ?? 0;
|
|
14
|
-
return `updated ${count} file${count === 1 ? "" : "s"}`;
|
|
15
|
-
}
|
|
16
|
-
function summarizeToolCall(item) {
|
|
17
|
-
return `used ${item.toolName ?? item.type}`;
|
|
18
|
-
}
|
|
19
|
-
function summarizeText(item) {
|
|
20
|
-
return (item.text ?? "").replace(/\s+/g, " ").trim();
|
|
21
|
-
}
|
|
22
|
-
function itemPrefix(item) {
|
|
23
|
-
if (item.type === "commandExecution")
|
|
24
|
-
return "$ ";
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
function formatItemDuration(ms) {
|
|
28
|
-
if (ms === undefined || ms === null)
|
|
29
|
-
return "";
|
|
30
|
-
const seconds = Math.floor(ms / 1000);
|
|
31
|
-
if (seconds < 1)
|
|
32
|
-
return "";
|
|
33
|
-
if (seconds < 60)
|
|
34
|
-
return ` ${seconds}s`;
|
|
35
|
-
const minutes = Math.floor(seconds / 60);
|
|
36
|
-
return ` ${minutes}m`;
|
|
37
|
-
}
|
|
38
|
-
function itemText(item) {
|
|
39
|
-
switch (item.type) {
|
|
40
|
-
case "agentMessage":
|
|
41
|
-
case "plan":
|
|
42
|
-
case "reasoning":
|
|
43
|
-
return summarizeText(item);
|
|
44
|
-
case "commandExecution": {
|
|
45
|
-
const cmd = cleanCommand(item.command ?? "?");
|
|
46
|
-
const exit = item.exitCode !== undefined && item.exitCode !== null && item.exitCode !== 0
|
|
47
|
-
? ` exit ${item.exitCode}` : "";
|
|
48
|
-
const dur = formatItemDuration(item.durationMs);
|
|
49
|
-
return `${cmd}${exit}${dur}`;
|
|
50
|
-
}
|
|
51
|
-
case "fileChange":
|
|
52
|
-
return summarizeFileChange(item);
|
|
53
|
-
case "mcpToolCall":
|
|
54
|
-
case "dynamicToolCall": {
|
|
55
|
-
const dur = formatItemDuration(item.durationMs);
|
|
56
|
-
return `${summarizeToolCall(item)}${dur}`;
|
|
57
|
-
}
|
|
58
|
-
case "userMessage":
|
|
59
|
-
return `you: ${summarizeText(item)}`;
|
|
60
|
-
default:
|
|
61
|
-
return item.text ? summarizeText(item) : item.type;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function itemColor(item) {
|
|
65
|
-
if (item.status === "failed" || item.status === "declined")
|
|
66
|
-
return "red";
|
|
67
|
-
if (item.status === "inProgress")
|
|
68
|
-
return "yellow";
|
|
69
|
-
if (item.type === "userMessage")
|
|
70
|
-
return "yellow";
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
export function ItemLine({ item }) {
|
|
74
|
-
const text = itemText(item);
|
|
75
|
-
if (!text) {
|
|
76
|
-
return _jsx(_Fragment, {});
|
|
77
|
-
}
|
|
78
|
-
const color = itemColor(item);
|
|
79
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { wrap: "wrap", bold: item.type === "agentMessage", ...(color ? { color } : {}), children: [itemPrefix(item), text] }), item.output && item.status === "inProgress" && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: item.output.split("\n").filter(Boolean).at(-1) ?? "" }) }))] }));
|
|
80
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
import { Box, Static, Text, useStdout } from "ink";
|
|
4
|
-
import { buildTimelineRows } from "./timeline-presentation.js";
|
|
5
|
-
import { TimelineRow } from "./TimelineRow.js";
|
|
6
|
-
const ACTIVE_TAIL = 8;
|
|
7
|
-
export function Timeline({ entries, follow }) {
|
|
8
|
-
const { stdout } = useStdout();
|
|
9
|
-
const rows = stdout?.rows ?? 24;
|
|
10
|
-
const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
|
|
11
|
-
const displayRows = useMemo(() => buildTimelineRows(entries), [entries]);
|
|
12
|
-
// Always cap the rendered entries to prevent OOM/WASM crashes.
|
|
13
|
-
// In follow mode: older entries go to Static (terminal scrollback).
|
|
14
|
-
// Without follow: show last maxActive entries only.
|
|
15
|
-
const splitIndex = Math.max(0, displayRows.length - maxActive);
|
|
16
|
-
const finalized = follow ? displayRows.slice(0, splitIndex) : [];
|
|
17
|
-
const active = displayRows.slice(splitIndex);
|
|
18
|
-
if (displayRows.length === 0) {
|
|
19
|
-
return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
|
|
20
|
-
}
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", children: [finalized.length > 0 && (_jsx(Static, { items: finalized, children: (entry) => _jsx(TimelineRow, { entry: entry }, entry.id) })), active.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
|
|
22
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { ItemLine } from "./ItemLine.js";
|
|
4
|
-
function formatDuration(startedAt, endedAt) {
|
|
5
|
-
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
6
|
-
const seconds = Math.floor(ms / 1000);
|
|
7
|
-
if (seconds < 60)
|
|
8
|
-
return `${seconds}s`;
|
|
9
|
-
const minutes = Math.floor(seconds / 60);
|
|
10
|
-
const s = seconds % 60;
|
|
11
|
-
return `${minutes}m ${String(s).padStart(2, "0")}s`;
|
|
12
|
-
}
|
|
13
|
-
const CHECK_SYMBOLS = { passed: "\u2713", failed: "\u2717", pending: "\u25cf" };
|
|
14
|
-
const CHECK_COLORS = { passed: "green", failed: "red", pending: "yellow" };
|
|
15
|
-
const RUN_LABELS = {
|
|
16
|
-
implementation: "implement",
|
|
17
|
-
ci_repair: "ci fix",
|
|
18
|
-
review_fix: "review fix",
|
|
19
|
-
branch_upkeep: "branch upkeep",
|
|
20
|
-
queue_repair: "merge fix",
|
|
21
|
-
};
|
|
22
|
-
function runDotColor(status) {
|
|
23
|
-
if (status === "completed")
|
|
24
|
-
return "green";
|
|
25
|
-
if (status === "failed")
|
|
26
|
-
return "red";
|
|
27
|
-
if (status === "released")
|
|
28
|
-
return "magenta";
|
|
29
|
-
if (status === "running")
|
|
30
|
-
return "yellow";
|
|
31
|
-
return "white";
|
|
32
|
-
}
|
|
33
|
-
function detailColor(detail) {
|
|
34
|
-
if (detail.tone === "command")
|
|
35
|
-
return "white";
|
|
36
|
-
if (detail.tone === "user")
|
|
37
|
-
return "yellow";
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
function detailPrefix(detail) {
|
|
41
|
-
if (detail.tone === "command")
|
|
42
|
-
return "$ ";
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
function FeedRow({ entry }) {
|
|
46
|
-
const label = entry.feed.status ?? entry.feed.feedKind;
|
|
47
|
-
const repeatSuffix = entry.repeatCount && entry.repeatCount > 1 ? ` \u00d7${entry.repeatCount}` : "";
|
|
48
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u25cf" }), _jsx(Text, { color: "cyan", children: ` ${label}` }), _jsx(Text, { dimColor: true, children: ` ${entry.feed.summary}${repeatSuffix}` })] }));
|
|
49
|
-
}
|
|
50
|
-
function RunRow({ entry, }) {
|
|
51
|
-
const run = entry.run;
|
|
52
|
-
const dotColor = runDotColor(run.status);
|
|
53
|
-
const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : undefined;
|
|
54
|
-
const showItems = entry.items.length > 0;
|
|
55
|
-
const showDetails = !showItems && entry.details.length > 0;
|
|
56
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, children: "\u25cf" }), _jsx(Text, { bold: true, color: "yellow", children: ` ${RUN_LABELS[run.runType] ?? run.runType}` }), _jsx(Text, { bold: true, color: dotColor, children: ` ${run.status}` }), duration ? _jsx(Text, { dimColor: true, children: ` ${duration}` }) : null] }), showItems && entry.items.map((itemEntry, index) => (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: itemEntry.item }) }, `${entry.id}-item-${index}`))), showDetails && entry.details.map((detail, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { wrap: "wrap", ...(detailColor(detail) ? { color: detailColor(detail) } : { dimColor: true }), bold: detail.tone === "message", children: [detailPrefix(detail), detail.text] }) }, `${entry.id}-detail-${index}`)))] }));
|
|
57
|
-
}
|
|
58
|
-
function ItemRow({ entry, }) {
|
|
59
|
-
return (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: entry.item }) }));
|
|
60
|
-
}
|
|
61
|
-
function CIChecksRow({ entry }) {
|
|
62
|
-
const ci = entry.ciChecks;
|
|
63
|
-
const dotColor = CHECK_COLORS[ci.overall] ?? "white";
|
|
64
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: dotColor, children: "\u25cf" }), _jsx(Text, { color: dotColor, bold: true, children: ` checks` }), _jsx(Text, { children: ` ` }), ci.checks.map((check, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { children: ` ` }) : null, _jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsx(Text, { dimColor: true, children: ` ${check.name}` })] }, `c-${i}`)))] }));
|
|
65
|
-
}
|
|
66
|
-
export function TimelineRow({ entry }) {
|
|
67
|
-
switch (entry.kind) {
|
|
68
|
-
case "feed":
|
|
69
|
-
return _jsx(FeedRow, { entry: entry });
|
|
70
|
-
case "run":
|
|
71
|
-
return _jsx(RunRow, { entry: entry });
|
|
72
|
-
case "item":
|
|
73
|
-
return _jsx(ItemRow, { entry: entry });
|
|
74
|
-
case "ci-checks":
|
|
75
|
-
return _jsx(CIChecksRow, { entry: entry });
|
|
76
|
-
}
|
|
77
|
-
}
|