patchrelay 0.38.2 → 0.40.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 +27 -1
- package/dist/cli/data.js +22 -0
- package/dist/cli/formatters/text.js +35 -0
- package/dist/cli/help.js +3 -0
- package/dist/cli/index.js +3 -0
- package/dist/codex-session-source.js +85 -0
- package/dist/github-worktree-auth.js +18 -0
- package/dist/linear-progress-facts.js +170 -0
- package/dist/linear-progress-reporter.js +34 -7
- package/dist/linear-workflow-state-sync.js +37 -18
- package/dist/merged-linear-completion-reconciler.js +102 -22
- package/dist/no-pr-completion-check.js +52 -0
- package/dist/run-launcher.js +6 -6
- package/dist/trusted-no-pr-completion.js +7 -0
- 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, formatWorktree } from "../formatters/text.js";
|
|
5
|
+
import { 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 "transcript-source":
|
|
40
|
+
return await handleTranscriptSourceCommand(nested);
|
|
39
41
|
case "retry":
|
|
40
42
|
return await handleRetryCommand(nested);
|
|
41
43
|
case "close":
|
|
@@ -116,6 +118,30 @@ export async function handleOpenCommand(params) {
|
|
|
116
118
|
const openCommand = buildOpenCommand(params.config, result.worktreePath, result.resumeThreadId);
|
|
117
119
|
return await params.runInteractive(openCommand.command, openCommand.args);
|
|
118
120
|
}
|
|
121
|
+
export async function handleTranscriptSourceCommand(params) {
|
|
122
|
+
const issueKey = params.commandArgs[0];
|
|
123
|
+
if (!issueKey) {
|
|
124
|
+
throw new Error("transcript-source requires <issueKey>.");
|
|
125
|
+
}
|
|
126
|
+
const runFlag = params.parsed.flags.get("run");
|
|
127
|
+
let runId;
|
|
128
|
+
if (typeof runFlag === "string") {
|
|
129
|
+
const parsedRunId = Number(runFlag);
|
|
130
|
+
if (!Number.isSafeInteger(parsedRunId) || parsedRunId <= 0) {
|
|
131
|
+
throw new Error("--run must be a positive integer.");
|
|
132
|
+
}
|
|
133
|
+
runId = parsedRunId;
|
|
134
|
+
}
|
|
135
|
+
const result = params.data.transcriptSource(issueKey, runId);
|
|
136
|
+
if (!result) {
|
|
137
|
+
throw new Error(`Issue not found: ${issueKey}`);
|
|
138
|
+
}
|
|
139
|
+
if (runId !== undefined && result.runId !== runId) {
|
|
140
|
+
throw new Error(`Run not found for ${issueKey}: ${runId}`);
|
|
141
|
+
}
|
|
142
|
+
writeOutput(params.stdout, params.json ? formatJson(result) : formatTranscriptSource(result));
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
119
145
|
export async function handleSessionsCommand(params) {
|
|
120
146
|
const issueKey = params.commandArgs[0];
|
|
121
147
|
if (!issueKey) {
|
package/dist/cli/data.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import pino from "pino";
|
|
3
3
|
import { CodexAppServerClient } from "../codex-app-server.js";
|
|
4
|
+
import { resolveCodexSessionSource } from "../codex-session-source.js";
|
|
4
5
|
import { extractCompletionCheck } from "../completion-check.js";
|
|
5
6
|
import { getThreadTurns } from "../codex-thread-utils.js";
|
|
6
7
|
import { PatchRelayDatabase } from "../db.js";
|
|
@@ -272,6 +273,25 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
272
273
|
...(run ? { releasedRunId: run.id } : {}),
|
|
273
274
|
};
|
|
274
275
|
}
|
|
276
|
+
transcriptSource(issueKey, runId) {
|
|
277
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
278
|
+
if (!issue)
|
|
279
|
+
return undefined;
|
|
280
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
281
|
+
const runs = this.db.runs.listRunsForIssue(issue.projectId, issue.linearIssueId);
|
|
282
|
+
const selectedRun = runId !== undefined
|
|
283
|
+
? runs.find((run) => run.id === runId)
|
|
284
|
+
: runs.slice().reverse().find((run) => run.threadId);
|
|
285
|
+
const threadId = selectedRun?.threadId ?? dbIssue.threadId;
|
|
286
|
+
return {
|
|
287
|
+
issue,
|
|
288
|
+
...(selectedRun ? { runId: selectedRun.id, runType: selectedRun.runType, runStatus: selectedRun.status } : {}),
|
|
289
|
+
...(threadId ? { threadId } : {}),
|
|
290
|
+
...(selectedRun?.turnId ? { turnId: selectedRun.turnId } : {}),
|
|
291
|
+
...(dbIssue.worktreePath ? { worktreePath: dbIssue.worktreePath } : {}),
|
|
292
|
+
...(threadId ? { sessionSource: resolveCodexSessionSource(threadId) } : {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
275
295
|
sessions(issueKey) {
|
|
276
296
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
277
297
|
if (!issue)
|
|
@@ -284,6 +304,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
284
304
|
.map((run) => {
|
|
285
305
|
const summary = summarizeRun(run);
|
|
286
306
|
const eventCount = this.db.runs.listThreadEvents(run.id).length;
|
|
307
|
+
const sessionSource = run.threadId ? resolveCodexSessionSource(run.threadId) : undefined;
|
|
287
308
|
return {
|
|
288
309
|
runId: run.id,
|
|
289
310
|
runType: run.runType,
|
|
@@ -293,6 +314,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
293
314
|
...(run.parentThreadId ? { parentThreadId: run.parentThreadId } : {}),
|
|
294
315
|
...(summary ? { summary } : {}),
|
|
295
316
|
...(run.failureReason ? { failureReason: run.failureReason } : {}),
|
|
317
|
+
...(sessionSource ? { sessionSource } : {}),
|
|
296
318
|
eventCount,
|
|
297
319
|
eventCountAvailable: this.config.runner.codex.persistExtendedHistory || eventCount > 0,
|
|
298
320
|
startedAt: run.startedAt,
|
|
@@ -97,6 +97,29 @@ export function formatClose(result) {
|
|
|
97
97
|
function formatTimestampRange(startedAt, endedAt) {
|
|
98
98
|
return endedAt ? `${startedAt} -> ${endedAt}` : `${startedAt} -> running`;
|
|
99
99
|
}
|
|
100
|
+
function formatSessionSource(sessionSource) {
|
|
101
|
+
if (!sessionSource)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (sessionSource.exists)
|
|
104
|
+
return sessionSource.path;
|
|
105
|
+
return sessionSource.error ?? "not found";
|
|
106
|
+
}
|
|
107
|
+
export function formatTranscriptSource(result) {
|
|
108
|
+
const lines = [
|
|
109
|
+
value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
|
|
110
|
+
value("Worktree", result.worktreePath),
|
|
111
|
+
value("Run", result.runId !== undefined
|
|
112
|
+
? `#${result.runId}${result.runType ? ` ${result.runType}` : ""}${result.runStatus ? ` (${result.runStatus})` : ""}`
|
|
113
|
+
: undefined),
|
|
114
|
+
value("Thread", result.threadId),
|
|
115
|
+
value("Turn", result.turnId),
|
|
116
|
+
value("Session source", formatSessionSource(result.sessionSource)),
|
|
117
|
+
result.sessionSource?.startedAt ? value("Started", result.sessionSource.startedAt) : undefined,
|
|
118
|
+
result.sessionSource?.originator ? value("Originator", result.sessionSource.originator) : undefined,
|
|
119
|
+
result.sessionSource?.cwd ? value("Working directory", result.sessionSource.cwd) : undefined,
|
|
120
|
+
].filter(Boolean);
|
|
121
|
+
return `${lines.join("\n")}\n`;
|
|
122
|
+
}
|
|
100
123
|
export function formatSessionHistory(result, buildOpenForThread) {
|
|
101
124
|
const lines = [
|
|
102
125
|
`${result.issue.issueKey ?? result.issue.linearIssueId}${result.issue.currentLinearState ? ` ${result.issue.currentLinearState}` : ""}`,
|
|
@@ -134,6 +157,18 @@ export function formatSessionHistory(result, buildOpenForThread) {
|
|
|
134
157
|
else if (session.failureReason) {
|
|
135
158
|
lines.push(value("Failure", truncateLine(session.failureReason)));
|
|
136
159
|
}
|
|
160
|
+
if (session.sessionSource) {
|
|
161
|
+
lines.push(value("Session source", formatSessionSource(session.sessionSource)));
|
|
162
|
+
if (session.sessionSource.startedAt) {
|
|
163
|
+
lines.push(value("Started", session.sessionSource.startedAt));
|
|
164
|
+
}
|
|
165
|
+
if (session.sessionSource.originator) {
|
|
166
|
+
lines.push(value("Originator", session.sessionSource.originator));
|
|
167
|
+
}
|
|
168
|
+
if (session.sessionSource.cwd) {
|
|
169
|
+
lines.push(value("Working directory", session.sessionSource.cwd));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
137
172
|
if (session.threadId && result.worktreePath && buildOpenForThread) {
|
|
138
173
|
const command = buildOpenForThread(session.threadId);
|
|
139
174
|
lines.push(value("Open", formatCommand(command.command, command.args)));
|
package/dist/cli/help.js
CHANGED
|
@@ -37,6 +37,7 @@ export function rootHelpText() {
|
|
|
37
37
|
" issue watch <issueKey> [--json] Follow the active run until it settles",
|
|
38
38
|
" issue open <issueKey> [--print] [--json] Open Codex in the issue worktree",
|
|
39
39
|
" issue sessions <issueKey> [--json] Show recorded Codex app-server sessions for one issue",
|
|
40
|
+
" issue transcript-source <issueKey> [--run <id>] [--json] Show the raw Codex session file for one issue run",
|
|
40
41
|
" issue close <issueKey> [--failed] [--reason <text>] [--json]",
|
|
41
42
|
" Force-close one issue and release any active run",
|
|
42
43
|
" service status [--json] Show systemd state and local health",
|
|
@@ -150,6 +151,7 @@ export function issueHelpText() {
|
|
|
150
151
|
" path <issueKey> Print the issue worktree path",
|
|
151
152
|
" open <issueKey> Open Codex in the issue worktree",
|
|
152
153
|
" sessions <issueKey> Show recorded Codex app-server sessions",
|
|
154
|
+
" transcript-source <issueKey> Show the raw Codex session file for one issue run",
|
|
153
155
|
" retry <issueKey> Requeue a run",
|
|
154
156
|
" close <issueKey> Force-close a stuck issue",
|
|
155
157
|
"",
|
|
@@ -158,6 +160,7 @@ export function issueHelpText() {
|
|
|
158
160
|
" patchrelay issue show USE-54",
|
|
159
161
|
" patchrelay issue watch USE-54",
|
|
160
162
|
" patchrelay issue sessions USE-54",
|
|
163
|
+
" patchrelay issue transcript-source USE-54",
|
|
161
164
|
" patchrelay close USE-54 --reason \"already handled manually\"",
|
|
162
165
|
].join("\n");
|
|
163
166
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -64,6 +64,9 @@ function validateFlags(command, commandArgs, parsed) {
|
|
|
64
64
|
case "sessions":
|
|
65
65
|
assertKnownFlags(parsed, "issue", ["json"]);
|
|
66
66
|
return;
|
|
67
|
+
case "transcript-source":
|
|
68
|
+
assertKnownFlags(parsed, "issue", ["run", "json"]);
|
|
69
|
+
return;
|
|
67
70
|
case "retry":
|
|
68
71
|
assertKnownFlags(parsed, "issue", ["run-type", "reason", "json"]);
|
|
69
72
|
return;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function resolveCodexHome(explicit) {
|
|
5
|
+
return explicit
|
|
6
|
+
?? process.env.CODEX_HOME
|
|
7
|
+
?? path.join(homedir(), ".codex");
|
|
8
|
+
}
|
|
9
|
+
function walkSessionFiles(directory, threadId, matches) {
|
|
10
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const fullPath = path.join(directory, entry.name);
|
|
13
|
+
if (entry.isDirectory()) {
|
|
14
|
+
walkSessionFiles(fullPath, threadId, matches);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (entry.name.includes(threadId)) {
|
|
21
|
+
matches.push(fullPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function readSessionMeta(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const firstLine = readFileSync(filePath, "utf8").split(/\r?\n/, 1)[0];
|
|
28
|
+
if (!firstLine) {
|
|
29
|
+
return { error: "session file is empty" };
|
|
30
|
+
}
|
|
31
|
+
const parsed = JSON.parse(firstLine);
|
|
32
|
+
if (parsed.type !== "session_meta" || !parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload)) {
|
|
33
|
+
return { error: "session file does not start with session_meta" };
|
|
34
|
+
}
|
|
35
|
+
return { payload: parsed.payload };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
error: error instanceof Error ? error.message : String(error),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function resolveCodexSessionSource(threadId, options) {
|
|
44
|
+
const codexHome = resolveCodexHome(options?.codexHome);
|
|
45
|
+
const sessionsDir = path.join(codexHome, "sessions");
|
|
46
|
+
if (!existsSync(sessionsDir)) {
|
|
47
|
+
return {
|
|
48
|
+
threadId,
|
|
49
|
+
codexHome,
|
|
50
|
+
sessionsDir,
|
|
51
|
+
exists: false,
|
|
52
|
+
error: `sessions directory not found: ${sessionsDir}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const matches = [];
|
|
56
|
+
walkSessionFiles(sessionsDir, threadId, matches);
|
|
57
|
+
matches.sort().reverse();
|
|
58
|
+
let firstError;
|
|
59
|
+
for (const candidate of matches) {
|
|
60
|
+
const { payload, error } = readSessionMeta(candidate);
|
|
61
|
+
if (payload && payload.id === threadId) {
|
|
62
|
+
return {
|
|
63
|
+
threadId,
|
|
64
|
+
codexHome,
|
|
65
|
+
sessionsDir,
|
|
66
|
+
exists: true,
|
|
67
|
+
path: candidate,
|
|
68
|
+
...(typeof payload.id === "string" ? { sessionId: payload.id } : {}),
|
|
69
|
+
...(typeof payload.timestamp === "string" ? { startedAt: payload.timestamp } : {}),
|
|
70
|
+
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
71
|
+
...(typeof payload.originator === "string" ? { originator: payload.originator } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (!firstError && error) {
|
|
75
|
+
firstError = `${candidate}: ${error}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
threadId,
|
|
80
|
+
codexHome,
|
|
81
|
+
sessionsDir,
|
|
82
|
+
exists: false,
|
|
83
|
+
...(firstError ? { error: firstError } : { error: `session file not found for thread ${threadId}` }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execCommand } from "./utils.js";
|
|
2
|
+
function shellSingleQuote(value) {
|
|
3
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
4
|
+
}
|
|
5
|
+
export function buildGitHubBotCredentialHelper(tokenFile) {
|
|
6
|
+
const quotedTokenFile = shellSingleQuote(tokenFile);
|
|
7
|
+
return `!f() { [ "$1" = get ] || exit 0; echo "username=x-access-token"; echo "password=$(cat ${quotedTokenFile})"; }; f`;
|
|
8
|
+
}
|
|
9
|
+
export async function configureGitHubBotAuthForWorktree(params) {
|
|
10
|
+
const helper = buildGitHubBotCredentialHelper(params.botIdentity.tokenFile);
|
|
11
|
+
const gitArgs = ["-C", params.worktreePath, "config"];
|
|
12
|
+
await execCommand(params.gitBin, [...gitArgs, "user.name", params.botIdentity.name], { timeoutMs: 5_000 });
|
|
13
|
+
await execCommand(params.gitBin, [...gitArgs, "user.email", params.botIdentity.email], { timeoutMs: 5_000 });
|
|
14
|
+
// Clear inherited GitHub-specific helpers such as `gh auth git-credential`
|
|
15
|
+
// so git HTTPS operations use the same bot token as the wrapped `gh` CLI.
|
|
16
|
+
await execCommand(params.gitBin, [...gitArgs, "--replace-all", "credential.https://github.com.helper", ""], { timeoutMs: 5_000 });
|
|
17
|
+
await execCommand(params.gitBin, [...gitArgs, "--add", "credential.https://github.com.helper", helper], { timeoutMs: 5_000 });
|
|
18
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
2
|
+
export function deriveLinearProgressFact(notification, issue) {
|
|
3
|
+
switch (notification.method) {
|
|
4
|
+
case "item/completed":
|
|
5
|
+
return deriveProgressFactFromCompletedItem(notification.params.item, issue);
|
|
6
|
+
case "turn/plan/updated":
|
|
7
|
+
return deriveProgressFactFromPlan(notification.params.plan, issue);
|
|
8
|
+
default:
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function deriveProgressFactFromCompletedItem(rawItem, issue) {
|
|
13
|
+
void issue;
|
|
14
|
+
if (!rawItem || typeof rawItem !== "object") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const item = rawItem;
|
|
18
|
+
if (item.type !== "agentMessage" || typeof item.text !== "string") {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const body = compactOperatorSentence(item.text);
|
|
22
|
+
if (!body) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (looksLikeVerification(body)) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "verification_started",
|
|
28
|
+
meaningKey: `verification:${normalizeMeaningKey(body)}`,
|
|
29
|
+
content: { type: "thought", body },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (looksLikePublishing(body)) {
|
|
33
|
+
return {
|
|
34
|
+
kind: "publishing_started",
|
|
35
|
+
meaningKey: `publishing:${normalizeMeaningKey(body)}`,
|
|
36
|
+
content: { type: "thought", body },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (looksLikeRootCause(body)) {
|
|
40
|
+
return {
|
|
41
|
+
kind: "root_cause_found",
|
|
42
|
+
meaningKey: `finding:${normalizeMeaningKey(body)}`,
|
|
43
|
+
content: { type: "thought", body },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function deriveProgressFactFromPlan(rawPlan, issue) {
|
|
49
|
+
if (!Array.isArray(rawPlan)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const activeStep = rawPlan
|
|
53
|
+
.map((entry) => normalizePlanEntry(entry))
|
|
54
|
+
.find((entry) => entry && entry.status === "in_progress");
|
|
55
|
+
if (!activeStep) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (looksLikeVerification(activeStep.step)) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "verification_started",
|
|
61
|
+
meaningKey: `verification:${normalizeMeaningKey(activeStep.step)}`,
|
|
62
|
+
content: {
|
|
63
|
+
type: "action",
|
|
64
|
+
action: "Verifying",
|
|
65
|
+
parameter: summarizePlanStep(activeStep.step, "latest changes before publishing"),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (looksLikePublishing(activeStep.step)) {
|
|
70
|
+
const parameter = summarizePlanStep(activeStep.step, issue?.prNumber !== undefined ? `changes to PR #${issue.prNumber}` : "latest changes");
|
|
71
|
+
return {
|
|
72
|
+
kind: "publishing_started",
|
|
73
|
+
meaningKey: `publishing:${normalizeMeaningKey(activeStep.step)}`,
|
|
74
|
+
content: {
|
|
75
|
+
type: "action",
|
|
76
|
+
action: "Publishing",
|
|
77
|
+
parameter,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function normalizePlanEntry(rawEntry) {
|
|
84
|
+
if (!rawEntry || typeof rawEntry !== "object") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const entry = rawEntry;
|
|
88
|
+
const rawStep = entry.step;
|
|
89
|
+
if (typeof rawStep !== "string" || !rawStep.trim()) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const rawStatus = typeof entry.status === "string" ? entry.status : "pending";
|
|
93
|
+
return {
|
|
94
|
+
step: rawStep.trim(),
|
|
95
|
+
status: rawStatus === "inProgress" ? "in_progress"
|
|
96
|
+
: rawStatus === "completed" ? "completed"
|
|
97
|
+
: rawStatus === "pending" ? "pending"
|
|
98
|
+
: rawStatus === "in_progress" ? "in_progress"
|
|
99
|
+
: "pending",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function looksLikeRootCause(text) {
|
|
103
|
+
const normalized = text.toLowerCase();
|
|
104
|
+
return /\b(narrowed|isolated|root cause)\b/.test(normalized)
|
|
105
|
+
|| normalized.startsWith("found that ")
|
|
106
|
+
|| normalized.startsWith("the failure is isolated")
|
|
107
|
+
|| normalized.startsWith("the issue is isolated");
|
|
108
|
+
}
|
|
109
|
+
function looksLikeVerification(text) {
|
|
110
|
+
const normalized = text.toLowerCase();
|
|
111
|
+
return /\b(verifying|verification|targeted verification|smoke)\b/.test(normalized);
|
|
112
|
+
}
|
|
113
|
+
function looksLikePublishing(text) {
|
|
114
|
+
const normalized = text.toLowerCase();
|
|
115
|
+
return /\b(publish|publishing|push|pushing)\b/.test(normalized)
|
|
116
|
+
|| normalized.includes("opening pr")
|
|
117
|
+
|| normalized.includes("opening the pr")
|
|
118
|
+
|| normalized.includes("opening pull request");
|
|
119
|
+
}
|
|
120
|
+
function compactOperatorSentence(text, maxLength = 160) {
|
|
121
|
+
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
122
|
+
if (!sanitized) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (sanitized.length <= maxLength) {
|
|
126
|
+
return sanitized;
|
|
127
|
+
}
|
|
128
|
+
const punctuated = lastBoundaryWithinLimit(sanitized, maxLength, /[.;!?]/g);
|
|
129
|
+
if (punctuated !== undefined) {
|
|
130
|
+
return sanitized.slice(0, punctuated + 1).trim();
|
|
131
|
+
}
|
|
132
|
+
const spaced = sanitized.lastIndexOf(" ", maxLength);
|
|
133
|
+
if (spaced > 0) {
|
|
134
|
+
return `${sanitized.slice(0, spaced).trimEnd()}...`;
|
|
135
|
+
}
|
|
136
|
+
return `${sanitized.slice(0, maxLength).trimEnd()}...`;
|
|
137
|
+
}
|
|
138
|
+
function summarizePlanStep(step, fallback) {
|
|
139
|
+
const sanitized = sanitizeOperatorFacingText(step)?.replace(/\s+/g, " ").trim();
|
|
140
|
+
if (!sanitized) {
|
|
141
|
+
return fallback;
|
|
142
|
+
}
|
|
143
|
+
const stripped = sanitized
|
|
144
|
+
.replace(/^(run|running|start|starting)\s+/i, "")
|
|
145
|
+
.replace(/^(verify|verifying|verification of)\s+/i, "")
|
|
146
|
+
.replace(/^(publish|publishing|push|pushing|open|opening)\s+/i, "")
|
|
147
|
+
.trim()
|
|
148
|
+
.replace(/[.]+$/, "");
|
|
149
|
+
return stripped || fallback;
|
|
150
|
+
}
|
|
151
|
+
function normalizeMeaningKey(text) {
|
|
152
|
+
return text
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/\s+/g, " ")
|
|
155
|
+
.trim();
|
|
156
|
+
}
|
|
157
|
+
function lastBoundaryWithinLimit(text, maxLength, pattern) {
|
|
158
|
+
let last = -1;
|
|
159
|
+
for (;;) {
|
|
160
|
+
const match = pattern.exec(text);
|
|
161
|
+
if (!match) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (match.index >= maxLength) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
last = match.index;
|
|
168
|
+
}
|
|
169
|
+
return last >= 0 ? last : undefined;
|
|
170
|
+
}
|
|
@@ -1,11 +1,38 @@
|
|
|
1
|
+
import { deriveLinearProgressFact } from "./linear-progress-facts.js";
|
|
1
2
|
export class LinearProgressReporter {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
db;
|
|
4
|
+
emitActivity;
|
|
5
|
+
publicationsByRun = new Map();
|
|
6
|
+
constructor(db, emitActivity) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
this.emitActivity = emitActivity;
|
|
7
9
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
maybeEmitProgress(notification, run) {
|
|
11
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
12
|
+
if (!issue) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const fact = deriveLinearProgressFact(notification, issue);
|
|
16
|
+
if (!fact) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const previous = this.publicationsByRun.get(run.id);
|
|
20
|
+
if (previous?.meaningKey === fact.meaningKey) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const publication = {
|
|
24
|
+
meaningKey: fact.meaningKey,
|
|
25
|
+
publishedAtMs: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
this.publicationsByRun.set(run.id, publication);
|
|
28
|
+
void this.emitActivity(issue, fact.content, { ephemeral: true }).catch(() => {
|
|
29
|
+
const current = this.publicationsByRun.get(run.id);
|
|
30
|
+
if (current?.publishedAtMs === publication.publishedAtMs && current.meaningKey === publication.meaningKey) {
|
|
31
|
+
this.publicationsByRun.delete(run.id);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
clearProgress(runId) {
|
|
36
|
+
this.publicationsByRun.delete(runId);
|
|
10
37
|
}
|
|
11
38
|
}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
1
|
+
import { resolvePreferredCompletedLinearState, resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
2
|
+
import { isCompletedLinearState } from "./pr-state.js";
|
|
3
|
+
import { hasTrustedNoPrCompletion } from "./trusted-no-pr-completion.js";
|
|
2
4
|
export async function syncActiveWorkflowState(params) {
|
|
3
5
|
const { db, issue, linear, trackedIssue, options } = params;
|
|
4
|
-
if (!shouldAutoAdvanceLinearState(issue)) {
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
6
|
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
8
7
|
if (!liveIssue)
|
|
9
8
|
return;
|
|
9
|
+
const latestRun = db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
10
|
+
if (hasTrustedNoPrCompletion(issue, latestRun)) {
|
|
11
|
+
await syncCompletedLinearState({ db, issue, linear, liveIssue });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!shouldAutoAdvanceLinearState(issue)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
10
17
|
if (!shouldAutoAdvanceLinearState({
|
|
11
18
|
currentLinearState: liveIssue.stateName,
|
|
12
19
|
currentLinearStateType: liveIssue.stateType,
|
|
13
20
|
})) {
|
|
14
|
-
db.
|
|
15
|
-
projectId: issue.projectId,
|
|
16
|
-
linearIssueId: issue.linearIssueId,
|
|
17
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
18
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
19
|
-
});
|
|
21
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
22
24
|
const targetState = resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue);
|
|
@@ -24,20 +26,37 @@ export async function syncActiveWorkflowState(params) {
|
|
|
24
26
|
return;
|
|
25
27
|
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
26
28
|
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
27
|
-
db.
|
|
28
|
-
projectId: issue.projectId,
|
|
29
|
-
linearIssueId: issue.linearIssueId,
|
|
30
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
31
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
32
|
-
});
|
|
29
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
33
30
|
return;
|
|
34
31
|
}
|
|
35
32
|
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
33
|
+
refreshCachedLinearState(db, issue, updated.stateName, updated.stateType);
|
|
34
|
+
}
|
|
35
|
+
async function syncCompletedLinearState(params) {
|
|
36
|
+
const { db, issue, linear, liveIssue } = params;
|
|
37
|
+
if (isCompletedLinearState(liveIssue.stateType, liveIssue.stateName)) {
|
|
38
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const targetState = resolvePreferredCompletedLinearState(liveIssue);
|
|
42
|
+
if (!targetState) {
|
|
43
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
47
|
+
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
48
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
52
|
+
refreshCachedLinearState(db, issue, updated.stateName, updated.stateType);
|
|
53
|
+
}
|
|
54
|
+
function refreshCachedLinearState(db, issue, stateName, stateType) {
|
|
36
55
|
db.issues.upsertIssue({
|
|
37
56
|
projectId: issue.projectId,
|
|
38
57
|
linearIssueId: issue.linearIssueId,
|
|
39
|
-
...(
|
|
40
|
-
...(
|
|
58
|
+
...(stateName ? { currentLinearState: stateName } : {}),
|
|
59
|
+
...(stateType ? { currentLinearStateType: stateType } : {}),
|
|
41
60
|
});
|
|
42
61
|
}
|
|
43
62
|
function shouldAutoAdvanceLinearState(issue) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
1
2
|
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
3
|
+
import { isCompletedLinearState } from "./pr-state.js";
|
|
4
|
+
import { hasTrustedNoPrCompletion } from "./trusted-no-pr-completion.js";
|
|
2
5
|
export class MergedLinearCompletionReconciler {
|
|
3
6
|
db;
|
|
4
7
|
linearProvider;
|
|
@@ -10,39 +13,116 @@ export class MergedLinearCompletionReconciler {
|
|
|
10
13
|
}
|
|
11
14
|
async reconcile() {
|
|
12
15
|
for (const issue of this.db.issues.listIssues()) {
|
|
13
|
-
if (issue.prState !== "merged")
|
|
14
|
-
continue;
|
|
15
|
-
if (issue.currentLinearStateType?.trim().toLowerCase() === "completed")
|
|
16
|
+
if (issue.factoryState !== "done" && issue.prState !== "merged") {
|
|
16
17
|
continue;
|
|
18
|
+
}
|
|
17
19
|
const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
18
|
-
if (!linear)
|
|
20
|
+
if (!linear) {
|
|
19
21
|
continue;
|
|
22
|
+
}
|
|
20
23
|
try {
|
|
21
24
|
const liveIssue = await linear.getIssue(issue.linearIssueId);
|
|
22
|
-
|
|
23
|
-
if (!targetState)
|
|
24
|
-
continue;
|
|
25
|
-
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
26
|
-
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
27
|
-
this.db.issues.upsertIssue({
|
|
28
|
-
projectId: issue.projectId,
|
|
29
|
-
linearIssueId: issue.linearIssueId,
|
|
30
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
31
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
32
|
-
});
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
36
|
-
this.db.issues.upsertIssue({
|
|
25
|
+
this.db.issues.replaceIssueDependencies({
|
|
37
26
|
projectId: issue.projectId,
|
|
38
27
|
linearIssueId: issue.linearIssueId,
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
blockers: liveIssue.blockedBy.map((blocker) => ({
|
|
29
|
+
blockerLinearIssueId: blocker.id,
|
|
30
|
+
...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
|
|
31
|
+
...(blocker.title ? { blockerTitle: blocker.title } : {}),
|
|
32
|
+
...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
|
|
33
|
+
...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
|
|
34
|
+
})),
|
|
41
35
|
});
|
|
36
|
+
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
37
|
+
const trustedNoPrDone = hasTrustedNoPrCompletion(issue, latestRun);
|
|
38
|
+
if (issue.prState === "merged" || trustedNoPrDone) {
|
|
39
|
+
await this.reconcileCompletedLinearState(issue, liveIssue, linear);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (issue.factoryState === "done" && !isCompletedLinearState(liveIssue.stateType, liveIssue.stateName)) {
|
|
43
|
+
this.reopenStaleLocalDoneIssue(issue, liveIssue);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.refreshCachedLinearState(issue, liveIssue);
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
catch (error) {
|
|
44
|
-
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to reconcile merged
|
|
50
|
+
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to reconcile merged or stale completed issue state");
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
}
|
|
54
|
+
async reconcileCompletedLinearState(issue, liveIssue, linear) {
|
|
55
|
+
if (isCompletedLinearState(liveIssue.stateType, liveIssue.stateName)) {
|
|
56
|
+
this.refreshCachedLinearState(issue, liveIssue);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const targetState = resolvePreferredCompletedLinearState(liveIssue);
|
|
60
|
+
if (!targetState) {
|
|
61
|
+
this.refreshCachedLinearState(issue, liveIssue);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
65
|
+
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
66
|
+
this.refreshCachedLinearState(issue, liveIssue);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
70
|
+
this.db.issues.upsertIssue({
|
|
71
|
+
projectId: issue.projectId,
|
|
72
|
+
linearIssueId: issue.linearIssueId,
|
|
73
|
+
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
74
|
+
...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
reopenStaleLocalDoneIssue(issue, liveIssue) {
|
|
78
|
+
const restored = resolveOpenWorkflowState(issue);
|
|
79
|
+
this.db.issues.upsertIssue({
|
|
80
|
+
projectId: issue.projectId,
|
|
81
|
+
linearIssueId: issue.linearIssueId,
|
|
82
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
83
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
84
|
+
...(restored ? { factoryState: restored.factoryState } : {}),
|
|
85
|
+
...(restored ? { pendingRunType: restored.pendingRunType } : {}),
|
|
86
|
+
});
|
|
87
|
+
this.logger.info({
|
|
88
|
+
issueKey: issue.issueKey,
|
|
89
|
+
previousFactoryState: issue.factoryState,
|
|
90
|
+
restoredFactoryState: restored?.factoryState,
|
|
91
|
+
liveLinearState: liveIssue.stateName,
|
|
92
|
+
}, "Reopened stale local done state from live Linear workflow");
|
|
93
|
+
}
|
|
94
|
+
refreshCachedLinearState(issue, liveIssue) {
|
|
95
|
+
this.db.issues.upsertIssue({
|
|
96
|
+
projectId: issue.projectId,
|
|
97
|
+
linearIssueId: issue.linearIssueId,
|
|
98
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
99
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function resolveOpenWorkflowState(issue) {
|
|
104
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
105
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
106
|
+
prNumber: issue.prNumber,
|
|
107
|
+
prState: issue.prState,
|
|
108
|
+
prReviewState: issue.prReviewState,
|
|
109
|
+
prCheckStatus: issue.prCheckStatus,
|
|
110
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
111
|
+
});
|
|
112
|
+
if (reactiveIntent) {
|
|
113
|
+
return {
|
|
114
|
+
factoryState: reactiveIntent.compatibilityFactoryState,
|
|
115
|
+
pendingRunType: reactiveIntent.runType,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (issue.prNumber !== undefined && (issue.prState === undefined || issue.prState === "open")) {
|
|
119
|
+
if (issue.prReviewState === "approved" && (issue.prCheckStatus === "success" || issue.prCheckStatus === "passed")) {
|
|
120
|
+
return { factoryState: "awaiting_queue", pendingRunType: null };
|
|
121
|
+
}
|
|
122
|
+
return { factoryState: "pr_open", pendingRunType: null };
|
|
123
|
+
}
|
|
124
|
+
if (issue.delegatedToPatchRelay) {
|
|
125
|
+
return { factoryState: "delegated", pendingRunType: null };
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
48
128
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { buildCompletionCheckActivity } from "./linear-session-reporting.js";
|
|
2
|
+
function shouldContinueForUnpublishedLocalChanges(message) {
|
|
3
|
+
const normalized = message.trim().toLowerCase();
|
|
4
|
+
if (!normalized)
|
|
5
|
+
return false;
|
|
6
|
+
return normalized.includes("worktree still has")
|
|
7
|
+
|| (normalized.includes("local commit") && normalized.includes("ahead of origin/"));
|
|
8
|
+
}
|
|
2
9
|
export async function handleNoPrCompletionCheck(params) {
|
|
3
10
|
const completedRunUpdate = buildCompletedRunUpdate({
|
|
4
11
|
threadId: params.threadId,
|
|
@@ -115,6 +122,51 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
115
122
|
return;
|
|
116
123
|
}
|
|
117
124
|
if (completionCheck.outcome === "done") {
|
|
125
|
+
if (shouldContinueForUnpublishedLocalChanges(params.publishedOutcomeError)) {
|
|
126
|
+
const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
127
|
+
params.db.runs.finishRun(params.run.id, completedRunUpdate);
|
|
128
|
+
params.db.runs.saveCompletionCheck(params.run.id, {
|
|
129
|
+
...completionCheck,
|
|
130
|
+
outcome: "continue",
|
|
131
|
+
summary: "PatchRelay changed files locally but has not published them yet; continuing automatically to finish publication.",
|
|
132
|
+
why: params.publishedOutcomeError,
|
|
133
|
+
});
|
|
134
|
+
params.db.issues.upsertIssue({
|
|
135
|
+
projectId: params.run.projectId,
|
|
136
|
+
linearIssueId: params.run.linearIssueId,
|
|
137
|
+
activeRunId: null,
|
|
138
|
+
factoryState: "delegated",
|
|
139
|
+
pendingRunType: null,
|
|
140
|
+
pendingRunContextJson: null,
|
|
141
|
+
});
|
|
142
|
+
return Boolean(params.db.issueSessions.appendIssueSessionEventWithLease(lease, {
|
|
143
|
+
projectId: params.run.projectId,
|
|
144
|
+
linearIssueId: params.run.linearIssueId,
|
|
145
|
+
eventType: "completion_check_continue",
|
|
146
|
+
eventJson: JSON.stringify({
|
|
147
|
+
runType: params.run.runType,
|
|
148
|
+
summary: params.publishedOutcomeError,
|
|
149
|
+
}),
|
|
150
|
+
dedupeKey: `completion_check_continue:${params.run.id}`,
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
if (!continued) {
|
|
154
|
+
params.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping completion-check continue writes after losing issue-session lease");
|
|
155
|
+
params.clearProgressAndRelease(params.run);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
params.syncCompletionCheckOutcome({
|
|
159
|
+
run: params.run,
|
|
160
|
+
fallbackIssue: params.issue,
|
|
161
|
+
level: "info",
|
|
162
|
+
status: "completion_check_continue",
|
|
163
|
+
summary: "No PR found; continuing automatically to finish publication",
|
|
164
|
+
detail: params.publishedOutcomeError,
|
|
165
|
+
activity: buildCompletionCheckActivity("continue"),
|
|
166
|
+
enqueue: true,
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
118
170
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
119
171
|
params.db.runs.finishRun(params.run.id, completedRunUpdate);
|
|
120
172
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
package/dist/run-launcher.js
CHANGED
|
@@ -2,7 +2,7 @@ import { buildHookEnv, runProjectHook } from "./hook-runner.js";
|
|
|
2
2
|
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
3
|
import { loadPatchRelayRepoPrompting } from "./patchrelay-customization.js";
|
|
4
4
|
import { buildRunPrompt as buildPatchRelayRunPrompt, findDisallowedPatchRelayPromptSectionIds, findUnknownPatchRelayPromptSectionIds, mergePromptCustomizationLayers, resolvePromptLayers, } from "./prompting/patchrelay.js";
|
|
5
|
-
import {
|
|
5
|
+
import { configureGitHubBotAuthForWorktree } from "./github-worktree-auth.js";
|
|
6
6
|
function slugify(value) {
|
|
7
7
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
8
8
|
}
|
|
@@ -116,11 +116,11 @@ export class RunLauncher {
|
|
|
116
116
|
try {
|
|
117
117
|
await this.worktreeManager.ensureIssueWorktree(params.project.repoPath, params.project.worktreeRoot, params.worktreePath, params.branchName, { allowExistingOutsideRoot: params.issue.branchName !== undefined });
|
|
118
118
|
if (params.botIdentity) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
await configureGitHubBotAuthForWorktree({
|
|
120
|
+
gitBin: this.config.runner.gitBin,
|
|
121
|
+
worktreePath: params.worktreePath,
|
|
122
|
+
botIdentity: params.botIdentity,
|
|
123
|
+
});
|
|
124
124
|
}
|
|
125
125
|
await this.worktreeManager.resetWorktreeToTrackedBranch(params.worktreePath, params.branchName, params.issue, this.logger);
|
|
126
126
|
if (params.runType !== "queue_repair") {
|