patchrelay 0.8.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +12 -12
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- package/dist/config.js +13 -166
- package/dist/db/migrations.js +46 -154
- package/dist/db.js +369 -45
- package/dist/factory-state.js +55 -0
- package/dist/github-webhook-handler.js +199 -0
- package/dist/github-webhooks.js +166 -0
- package/dist/hook-runner.js +28 -0
- package/dist/http.js +48 -22
- package/dist/issue-query-service.js +33 -38
- package/dist/linear-workflow.js +5 -118
- package/dist/preflight.js +1 -6
- package/dist/project-resolution.js +12 -1
- package/dist/run-orchestrator.js +446 -0
- package/dist/{stage-reporting.js → run-reporting.js} +11 -13
- package/dist/service-runtime.js +12 -61
- package/dist/service-webhooks.js +7 -52
- package/dist/service.js +39 -61
- package/dist/webhook-handler.js +387 -0
- package/dist/webhook-installation-handler.js +3 -8
- package/package.json +2 -1
- package/dist/db/authoritative-ledger-store.js +0 -536
- package/dist/db/issue-projection-store.js +0 -54
- package/dist/db/issue-workflow-coordinator.js +0 -320
- package/dist/db/issue-workflow-store.js +0 -194
- package/dist/db/run-report-store.js +0 -33
- package/dist/db/stage-event-store.js +0 -33
- package/dist/db/webhook-event-store.js +0 -59
- package/dist/db-ports.js +0 -5
- package/dist/ledger-ports.js +0 -1
- package/dist/reconciliation-action-applier.js +0 -68
- package/dist/reconciliation-actions.js +0 -1
- package/dist/reconciliation-engine.js +0 -350
- package/dist/reconciliation-snapshot-builder.js +0 -135
- package/dist/reconciliation-types.js +0 -1
- package/dist/service-stage-finalizer.js +0 -753
- package/dist/service-stage-runner.js +0 -336
- package/dist/service-webhook-processor.js +0 -411
- package/dist/stage-agent-activity-publisher.js +0 -59
- package/dist/stage-event-ports.js +0 -1
- package/dist/stage-failure.js +0 -92
- package/dist/stage-handoff.js +0 -107
- package/dist/stage-launch.js +0 -84
- package/dist/stage-lifecycle-publisher.js +0 -284
- package/dist/stage-turn-input-dispatcher.js +0 -104
- package/dist/webhook-agent-session-handler.js +0 -228
- package/dist/webhook-comment-handler.js +0 -141
- package/dist/webhook-desired-stage-recorder.js +0 -122
- package/dist/webhook-event-ports.js +0 -1
- package/dist/workflow-policy.js +0 -149
- package/dist/workflow-ports.js +0 -1
- /package/dist/{installation-ports.js → github-types.js} +0 -0
package/dist/cli/data.js
CHANGED
|
@@ -4,11 +4,9 @@ import { CodexAppServerClient } from "../codex-app-server.js";
|
|
|
4
4
|
import { PatchRelayDatabase } from "../db.js";
|
|
5
5
|
import { WorktreeManager } from "../worktree-manager.js";
|
|
6
6
|
import { CliOperatorApiClient } from "./operator-client.js";
|
|
7
|
-
import { resolveWorkflowStage } from "../workflow-policy.js";
|
|
8
7
|
function safeJsonParse(value) {
|
|
9
|
-
if (!value)
|
|
8
|
+
if (!value)
|
|
10
9
|
return undefined;
|
|
11
|
-
}
|
|
12
10
|
try {
|
|
13
11
|
const parsed = JSON.parse(value);
|
|
14
12
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
@@ -30,17 +28,10 @@ function summarizeThread(thread, latestTimestampSeen) {
|
|
|
30
28
|
...(latestTimestampSeen ? { latestTimestampSeen } : {}),
|
|
31
29
|
};
|
|
32
30
|
}
|
|
33
|
-
function latestEventTimestamp(db,
|
|
34
|
-
const events = db.
|
|
31
|
+
function latestEventTimestamp(db, runId) {
|
|
32
|
+
const events = db.listThreadEvents(runId);
|
|
35
33
|
return events.at(-1)?.createdAt;
|
|
36
34
|
}
|
|
37
|
-
function resolveStageFromState(config, projectId, stateName) {
|
|
38
|
-
const project = config.projects.find((entry) => entry.id === projectId);
|
|
39
|
-
if (!project) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
return resolveWorkflowStage(project, stateName);
|
|
43
|
-
}
|
|
44
35
|
export class CliDataAccess extends CliOperatorApiClient {
|
|
45
36
|
config;
|
|
46
37
|
db;
|
|
@@ -53,125 +44,103 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
53
44
|
this.codex = options?.codex;
|
|
54
45
|
}
|
|
55
46
|
close() {
|
|
56
|
-
if (!this.codexStarted)
|
|
47
|
+
if (!this.codexStarted)
|
|
57
48
|
return;
|
|
58
|
-
}
|
|
59
49
|
void this.codex?.stop();
|
|
60
50
|
this.codexStarted = false;
|
|
61
51
|
}
|
|
62
52
|
async inspect(issueKey) {
|
|
63
|
-
const issue = this.db.
|
|
64
|
-
if (!issue)
|
|
53
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
54
|
+
if (!issue)
|
|
65
55
|
return undefined;
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const latestSummary = safeJsonParse(latestStageRun?.summaryJson);
|
|
73
|
-
const live = activeStageRun?.threadId &&
|
|
74
|
-
(await this.readLiveSummary(activeStageRun.threadId, latestEventTimestamp(this.db, activeStageRun.id)).catch(() => undefined));
|
|
75
|
-
const statusNote = (live && live.latestAssistantMessage) ??
|
|
76
|
-
latestReport?.assistantMessages.at(-1) ??
|
|
56
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
57
|
+
const activeRun = dbIssue.activeRunId ? this.db.getRun(dbIssue.activeRunId) : undefined;
|
|
58
|
+
const latestRun = this.db.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
59
|
+
const latestReport = latestRun?.reportJson ? JSON.parse(latestRun.reportJson) : undefined;
|
|
60
|
+
const latestSummary = safeJsonParse(latestRun?.summaryJson);
|
|
61
|
+
const statusNote = latestReport?.assistantMessages.at(-1) ??
|
|
77
62
|
(typeof latestSummary?.latestAssistantMessage === "string" ? latestSummary.latestAssistantMessage : undefined) ??
|
|
78
|
-
(
|
|
79
|
-
(issue.desiredStage ? `Queued for ${issue.desiredStage}.` : undefined) ??
|
|
63
|
+
(latestRun?.status === "failed" ? "Latest run failed." : undefined) ??
|
|
80
64
|
undefined;
|
|
81
65
|
return {
|
|
82
66
|
issue,
|
|
83
|
-
...(
|
|
84
|
-
...(
|
|
85
|
-
...(latestStageRun ? { latestStageRun } : {}),
|
|
67
|
+
...(activeRun ? { activeRun } : {}),
|
|
68
|
+
...(!activeRun && latestRun ? { latestRun } : {}),
|
|
86
69
|
...(latestReport ? { latestReport } : {}),
|
|
87
70
|
...(latestSummary ? { latestSummary } : {}),
|
|
88
|
-
...(
|
|
71
|
+
...(dbIssue.prNumber ? { prNumber: dbIssue.prNumber } : {}),
|
|
72
|
+
...(dbIssue.prReviewState ? { prReviewState: dbIssue.prReviewState } : {}),
|
|
89
73
|
...(statusNote ? { statusNote } : {}),
|
|
90
74
|
};
|
|
91
75
|
}
|
|
92
76
|
async live(issueKey) {
|
|
93
|
-
const issue = this.db.
|
|
94
|
-
if (!issue)
|
|
77
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
78
|
+
if (!issue)
|
|
95
79
|
return undefined;
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
if (!
|
|
80
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
81
|
+
const run = dbIssue.activeRunId ? this.db.getRun(dbIssue.activeRunId) : undefined;
|
|
82
|
+
if (!run)
|
|
99
83
|
return undefined;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
issue,
|
|
105
|
-
stageRun,
|
|
106
|
-
...(live ? { live } : {}),
|
|
107
|
-
};
|
|
84
|
+
const live = run.threadId &&
|
|
85
|
+
(await this.readLiveSummary(run.threadId, latestEventTimestamp(this.db, run.id)).catch(() => undefined));
|
|
86
|
+
return { issue, run, ...(live ? { live } : {}) };
|
|
108
87
|
}
|
|
109
88
|
report(issueKey, options) {
|
|
110
|
-
const issue = this.db.
|
|
111
|
-
if (!issue)
|
|
89
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
90
|
+
if (!issue)
|
|
112
91
|
return undefined;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
if (options?.stageRunId !== undefined && stageRun.id !== options.stageRunId) {
|
|
92
|
+
const runs = this.db
|
|
93
|
+
.listRunsForIssue(issue.projectId, issue.linearIssueId)
|
|
94
|
+
.filter((run) => {
|
|
95
|
+
if (options?.runId !== undefined && run.id !== options.runId)
|
|
118
96
|
return false;
|
|
119
|
-
|
|
120
|
-
if (options?.stage !== undefined && stageRun.stage !== options.stage) {
|
|
97
|
+
if (options?.runType !== undefined && run.runType !== options.runType)
|
|
121
98
|
return false;
|
|
122
|
-
}
|
|
123
99
|
return true;
|
|
124
100
|
})
|
|
125
101
|
.reverse()
|
|
126
|
-
.map((
|
|
127
|
-
|
|
128
|
-
...(
|
|
129
|
-
...(safeJsonParse(
|
|
102
|
+
.map((run) => ({
|
|
103
|
+
run,
|
|
104
|
+
...(run.reportJson ? { report: JSON.parse(run.reportJson) } : {}),
|
|
105
|
+
...(safeJsonParse(run.summaryJson) ? { summary: safeJsonParse(run.summaryJson) } : {}),
|
|
130
106
|
}));
|
|
131
|
-
return { issue,
|
|
107
|
+
return { issue, runs };
|
|
132
108
|
}
|
|
133
109
|
events(issueKey, options) {
|
|
134
|
-
const issue = this.db.
|
|
135
|
-
if (!issue)
|
|
110
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
111
|
+
if (!issue)
|
|
136
112
|
return undefined;
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
this.
|
|
140
|
-
this.db.
|
|
141
|
-
if (!
|
|
113
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
114
|
+
const run = (options?.runId !== undefined ? this.db.getRun(options.runId) : undefined) ??
|
|
115
|
+
(dbIssue.activeRunId ? this.db.getRun(dbIssue.activeRunId) : undefined) ??
|
|
116
|
+
this.db.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
117
|
+
if (!run || run.projectId !== issue.projectId || run.linearIssueId !== issue.linearIssueId)
|
|
142
118
|
return undefined;
|
|
143
|
-
}
|
|
144
119
|
const events = this.db
|
|
145
|
-
.
|
|
120
|
+
.listThreadEvents(run.id)
|
|
146
121
|
.filter((event) => (options?.method ? event.method === options.method : true))
|
|
147
122
|
.filter((event) => (options?.afterId !== undefined ? event.id > options.afterId : true))
|
|
148
123
|
.map((event) => ({
|
|
149
124
|
...event,
|
|
150
125
|
...(safeJsonParse(event.eventJson) ? { parsedEvent: safeJsonParse(event.eventJson) } : {}),
|
|
151
126
|
}));
|
|
152
|
-
return { issue,
|
|
127
|
+
return { issue, run, events };
|
|
153
128
|
}
|
|
154
129
|
worktree(issueKey) {
|
|
155
|
-
const issue = this.db.
|
|
156
|
-
if (!issue)
|
|
130
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
131
|
+
if (!issue)
|
|
157
132
|
return undefined;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!workspace) {
|
|
133
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
134
|
+
if (!dbIssue.branchName || !dbIssue.worktreePath)
|
|
161
135
|
return undefined;
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
issue,
|
|
165
|
-
workspace,
|
|
166
|
-
repoId: issue.projectId,
|
|
167
|
-
};
|
|
136
|
+
return { issue, branchName: dbIssue.branchName, worktreePath: dbIssue.worktreePath, repoId: issue.projectId };
|
|
168
137
|
}
|
|
169
138
|
open(issueKey) {
|
|
170
139
|
const worktree = this.worktree(issueKey);
|
|
171
|
-
if (!worktree)
|
|
140
|
+
if (!worktree)
|
|
172
141
|
return undefined;
|
|
173
|
-
|
|
174
|
-
const resumeThreadId =
|
|
142
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
143
|
+
const resumeThreadId = dbIssue.threadId ?? undefined;
|
|
175
144
|
return {
|
|
176
145
|
...worktree,
|
|
177
146
|
...(resumeThreadId ? { resumeThreadId } : {}),
|
|
@@ -179,256 +148,100 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
179
148
|
}
|
|
180
149
|
async resolveOpen(issueKey, options) {
|
|
181
150
|
const worktree = this.worktree(issueKey);
|
|
182
|
-
if (!worktree)
|
|
151
|
+
if (!worktree)
|
|
183
152
|
return undefined;
|
|
184
|
-
}
|
|
185
153
|
if (options?.ensureWorktree) {
|
|
186
154
|
await this.ensureOpenWorktree(worktree);
|
|
187
155
|
}
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
resumeThreadId: existingThreadId,
|
|
193
|
-
};
|
|
156
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
157
|
+
const existingThreadId = dbIssue.threadId;
|
|
158
|
+
if (existingThreadId && (await this.canReadThread(existingThreadId))) {
|
|
159
|
+
return { ...worktree, resumeThreadId: existingThreadId };
|
|
194
160
|
}
|
|
195
161
|
if (!options?.createThreadIfMissing) {
|
|
196
|
-
return {
|
|
197
|
-
...worktree,
|
|
198
|
-
needsNewSession: true,
|
|
199
|
-
};
|
|
162
|
+
return { ...worktree, needsNewSession: true };
|
|
200
163
|
}
|
|
201
164
|
const codex = await this.getCodex();
|
|
202
|
-
const thread = await codex.startThread({
|
|
203
|
-
|
|
204
|
-
});
|
|
205
|
-
this.db.issueSessions.upsertIssueSession({
|
|
165
|
+
const thread = await codex.startThread({ cwd: worktree.worktreePath });
|
|
166
|
+
this.db.upsertIssue({
|
|
206
167
|
projectId: worktree.issue.projectId,
|
|
207
168
|
linearIssueId: worktree.issue.linearIssueId,
|
|
208
|
-
workspaceOwnershipId: worktree.workspace.id,
|
|
209
169
|
threadId: thread.id,
|
|
210
|
-
source: "operator_open",
|
|
211
|
-
...(worktree.issue.activeAgentSessionId ? { linkedAgentSessionId: worktree.issue.activeAgentSessionId } : {}),
|
|
212
170
|
});
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
...worktree,
|
|
216
|
-
resumeThreadId: thread.id,
|
|
217
|
-
};
|
|
171
|
+
return { ...worktree, resumeThreadId: thread.id };
|
|
218
172
|
}
|
|
219
173
|
async prepareOpen(issueKey) {
|
|
220
|
-
return await this.resolveOpen(issueKey, {
|
|
221
|
-
ensureWorktree: true,
|
|
222
|
-
createThreadIfMissing: true,
|
|
223
|
-
});
|
|
174
|
+
return await this.resolveOpen(issueKey, { ensureWorktree: true, createThreadIfMissing: true });
|
|
224
175
|
}
|
|
225
176
|
retry(issueKey, options) {
|
|
226
|
-
const issue = this.db.
|
|
227
|
-
if (!issue)
|
|
177
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
178
|
+
if (!issue)
|
|
228
179
|
return undefined;
|
|
180
|
+
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
181
|
+
if (dbIssue.activeRunId !== undefined) {
|
|
182
|
+
throw new Error(`Issue ${issueKey} already has an active run.`);
|
|
229
183
|
}
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
throw new Error(`Issue ${issueKey} already has an active stage run.`);
|
|
233
|
-
}
|
|
234
|
-
const stage = options?.stage ?? resolveStageFromState(this.config, issue.projectId, issue.currentLinearState);
|
|
235
|
-
if (!stage) {
|
|
236
|
-
throw new Error(`Unable to infer a stage for ${issueKey}; pass --stage.`);
|
|
237
|
-
}
|
|
238
|
-
const webhookId = `cli-retry-${Date.now()}`;
|
|
239
|
-
const receipt = this.db.eventReceipts.insertEventReceipt({
|
|
240
|
-
source: "linear-webhook",
|
|
241
|
-
externalId: webhookId,
|
|
242
|
-
eventType: "cli-retry",
|
|
243
|
-
receivedAt: new Date().toISOString(),
|
|
244
|
-
acceptanceStatus: "accepted",
|
|
184
|
+
const runType = (options?.runType ?? "implementation");
|
|
185
|
+
this.db.upsertIssue({
|
|
245
186
|
projectId: issue.projectId,
|
|
246
187
|
linearIssueId: issue.linearIssueId,
|
|
188
|
+
pendingRunType: runType,
|
|
189
|
+
factoryState: "delegated",
|
|
247
190
|
});
|
|
248
|
-
this.db.
|
|
249
|
-
|
|
250
|
-
lifecycleStatus: "queued",
|
|
251
|
-
});
|
|
252
|
-
const updated = this.db.issueWorkflows.getTrackedIssue(issue.projectId, issue.linearIssueId);
|
|
253
|
-
return {
|
|
254
|
-
issue: updated,
|
|
255
|
-
stage,
|
|
256
|
-
...(options?.reason ? { reason: options.reason } : {}),
|
|
257
|
-
};
|
|
191
|
+
const updated = this.db.getTrackedIssue(issue.projectId, issue.linearIssueId);
|
|
192
|
+
return { issue: updated, runType, ...(options?.reason ? { reason: options.reason } : {}) };
|
|
258
193
|
}
|
|
259
194
|
list(options) {
|
|
260
195
|
const conditions = [];
|
|
261
196
|
const values = [];
|
|
262
197
|
if (options?.project) {
|
|
263
|
-
conditions.push("
|
|
198
|
+
conditions.push("i.project_id = ?");
|
|
264
199
|
values.push(options.project);
|
|
265
200
|
}
|
|
266
201
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
267
202
|
const rows = this.db.connection
|
|
268
203
|
.prepare(`
|
|
269
|
-
WITH all_issues AS (
|
|
270
|
-
SELECT project_id, linear_issue_id FROM issue_projection
|
|
271
|
-
UNION
|
|
272
|
-
SELECT project_id, linear_issue_id FROM issue_control
|
|
273
|
-
)
|
|
274
204
|
SELECT
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
active_run.
|
|
283
|
-
latest_run.
|
|
284
|
-
latest_run.status AS
|
|
285
|
-
FROM
|
|
286
|
-
LEFT JOIN
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
ON active_run.id = ic.active_run_lease_id
|
|
292
|
-
LEFT JOIN run_leases latest_run ON latest_run.id = (
|
|
293
|
-
SELECT rl.id
|
|
294
|
-
FROM run_leases rl
|
|
295
|
-
WHERE rl.project_id = ai.project_id AND rl.linear_issue_id = ai.linear_issue_id
|
|
296
|
-
ORDER BY rl.id DESC
|
|
297
|
-
LIMIT 1
|
|
205
|
+
i.project_id,
|
|
206
|
+
i.linear_issue_id,
|
|
207
|
+
i.issue_key,
|
|
208
|
+
i.title,
|
|
209
|
+
i.current_linear_state,
|
|
210
|
+
i.factory_state,
|
|
211
|
+
i.updated_at,
|
|
212
|
+
active_run.run_type AS active_run_type,
|
|
213
|
+
latest_run.run_type AS latest_run_type,
|
|
214
|
+
latest_run.status AS latest_run_status
|
|
215
|
+
FROM issues i
|
|
216
|
+
LEFT JOIN runs active_run ON active_run.id = i.active_run_id
|
|
217
|
+
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
218
|
+
SELECT r.id FROM runs r
|
|
219
|
+
WHERE r.project_id = i.project_id AND r.linear_issue_id = i.linear_issue_id
|
|
220
|
+
ORDER BY r.id DESC LIMIT 1
|
|
298
221
|
)
|
|
299
222
|
${whereClause}
|
|
300
|
-
ORDER BY
|
|
223
|
+
ORDER BY i.updated_at DESC, i.issue_key ASC
|
|
301
224
|
`)
|
|
302
225
|
.all(...values);
|
|
303
|
-
const items = rows.map((row) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
lifecycleStatus: String(row.lifecycle_status),
|
|
315
|
-
...(ledger?.runLease
|
|
316
|
-
? { activeStage: ledger.runLease.stage }
|
|
317
|
-
: row.active_stage !== null
|
|
318
|
-
? { activeStage: row.active_stage }
|
|
319
|
-
: {}),
|
|
320
|
-
...(row.latest_stage !== null
|
|
321
|
-
? { latestStage: row.latest_stage }
|
|
322
|
-
: ledger?.runLease
|
|
323
|
-
? { latestStage: ledger.runLease.stage }
|
|
324
|
-
: {}),
|
|
325
|
-
...(row.latest_stage_status !== null
|
|
326
|
-
? { latestStageStatus: String(row.latest_stage_status) }
|
|
327
|
-
: ledger?.runLease
|
|
328
|
-
? {
|
|
329
|
-
latestStageStatus: ledger.runLease.status === "failed"
|
|
330
|
-
? "failed"
|
|
331
|
-
: ledger.runLease.status === "completed" || ledger.runLease.status === "released" || ledger.runLease.status === "paused"
|
|
332
|
-
? "completed"
|
|
333
|
-
: "running",
|
|
334
|
-
}
|
|
335
|
-
: {}),
|
|
336
|
-
updatedAt: String(row.updated_at),
|
|
337
|
-
};
|
|
338
|
-
});
|
|
226
|
+
const items = rows.map((row) => ({
|
|
227
|
+
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
228
|
+
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
229
|
+
projectId: String(row.project_id),
|
|
230
|
+
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
231
|
+
factoryState: String(row.factory_state ?? "delegated"),
|
|
232
|
+
...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
|
|
233
|
+
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
234
|
+
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
235
|
+
updatedAt: String(row.updated_at),
|
|
236
|
+
}));
|
|
339
237
|
return items.filter((item) => {
|
|
340
|
-
if (options?.active && !item.
|
|
238
|
+
if (options?.active && !item.activeRunType)
|
|
341
239
|
return false;
|
|
342
|
-
|
|
343
|
-
if (options?.failed && item.latestStageStatus !== "failed") {
|
|
240
|
+
if (options?.failed && item.latestRunStatus !== "failed")
|
|
344
241
|
return false;
|
|
345
|
-
}
|
|
346
242
|
return true;
|
|
347
243
|
});
|
|
348
244
|
}
|
|
349
|
-
getLedgerIssueContext(projectId, linearIssueId) {
|
|
350
|
-
const issueControl = this.db.issueControl.getIssueControl(projectId, linearIssueId);
|
|
351
|
-
const runLease = issueControl?.activeRunLeaseId ? this.db.runLeases.getRunLease(issueControl.activeRunLeaseId) : undefined;
|
|
352
|
-
return {
|
|
353
|
-
...(issueControl ? { issueControl } : {}),
|
|
354
|
-
...(runLease ? { runLease } : {}),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
getActiveStageRunForIssue(issue, ledger) {
|
|
358
|
-
const context = ledger ?? this.getLedgerIssueContext(issue.projectId, issue.linearIssueId);
|
|
359
|
-
const activeStageRun = context.issueControl?.activeRunLeaseId
|
|
360
|
-
? this.db.issueWorkflows.getStageRun(context.issueControl.activeRunLeaseId)
|
|
361
|
-
: undefined;
|
|
362
|
-
if (!activeStageRun) {
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
return activeStageRun.projectId === issue.projectId && activeStageRun.linearIssueId === issue.linearIssueId
|
|
366
|
-
? activeStageRun
|
|
367
|
-
: undefined;
|
|
368
|
-
}
|
|
369
|
-
getWorkspaceForIssue(issue, ledger) {
|
|
370
|
-
const context = ledger ?? this.getLedgerIssueContext(issue.projectId, issue.linearIssueId);
|
|
371
|
-
if (context.issueControl?.activeWorkspaceOwnershipId !== undefined) {
|
|
372
|
-
const activeWorkspace = this.db.issueWorkflows.getWorkspace(context.issueControl.activeWorkspaceOwnershipId);
|
|
373
|
-
if (activeWorkspace) {
|
|
374
|
-
return activeWorkspace;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return this.db.issueWorkflows.getActiveWorkspaceForIssue(issue.projectId, issue.linearIssueId);
|
|
378
|
-
}
|
|
379
|
-
getStoredOpenThreadId(worktree) {
|
|
380
|
-
return this.listOpenCandidateThreadIds(worktree).at(0);
|
|
381
|
-
}
|
|
382
|
-
async resolveStoredOpenThreadId(worktree) {
|
|
383
|
-
for (const threadId of this.listOpenCandidateThreadIds(worktree)) {
|
|
384
|
-
if (!(await this.canReadThread(threadId))) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
this.recordOpenThreadForIssue(worktree, threadId);
|
|
388
|
-
return threadId;
|
|
389
|
-
}
|
|
390
|
-
return undefined;
|
|
391
|
-
}
|
|
392
|
-
listOpenCandidateThreadIds(worktree) {
|
|
393
|
-
const ledger = this.getLedgerIssueContext(worktree.issue.projectId, worktree.issue.linearIssueId);
|
|
394
|
-
const sessions = this.db.issueSessions.listIssueSessionsForIssue(worktree.issue.projectId, worktree.issue.linearIssueId);
|
|
395
|
-
const candidates = [
|
|
396
|
-
ledger.issueControl?.activeRunLeaseId ? ledger.runLease?.threadId : undefined,
|
|
397
|
-
...sessions.map((session) => session.threadId),
|
|
398
|
-
worktree.workspace.lastThreadId,
|
|
399
|
-
worktree.issue.latestThreadId,
|
|
400
|
-
ledger.runLease?.threadId,
|
|
401
|
-
];
|
|
402
|
-
const seen = new Set();
|
|
403
|
-
const ordered = [];
|
|
404
|
-
for (const candidate of candidates) {
|
|
405
|
-
if (!candidate || seen.has(candidate)) {
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
seen.add(candidate);
|
|
409
|
-
ordered.push(candidate);
|
|
410
|
-
}
|
|
411
|
-
return ordered;
|
|
412
|
-
}
|
|
413
|
-
recordOpenThreadForIssue(worktree, threadId) {
|
|
414
|
-
const existing = this.db.issueSessions.getIssueSessionByThreadId(threadId);
|
|
415
|
-
if (existing) {
|
|
416
|
-
this.db.issueSessions.touchIssueSession(threadId);
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const runLease = this.db.runLeases.getRunLeaseByThreadId(threadId);
|
|
420
|
-
this.db.issueSessions.upsertIssueSession({
|
|
421
|
-
projectId: worktree.issue.projectId,
|
|
422
|
-
linearIssueId: worktree.issue.linearIssueId,
|
|
423
|
-
workspaceOwnershipId: runLease?.workspaceOwnershipId ?? worktree.workspace.id,
|
|
424
|
-
threadId,
|
|
425
|
-
source: runLease ? "stage_run" : "operator_open",
|
|
426
|
-
...(runLease?.id !== undefined ? { runLeaseId: runLease.id } : {}),
|
|
427
|
-
...(runLease?.parentThreadId ? { parentThreadId: runLease.parentThreadId } : {}),
|
|
428
|
-
...(worktree.issue.activeAgentSessionId ? { linkedAgentSessionId: worktree.issue.activeAgentSessionId } : {}),
|
|
429
|
-
});
|
|
430
|
-
this.db.issueSessions.touchIssueSession(threadId);
|
|
431
|
-
}
|
|
432
245
|
async canReadThread(threadId) {
|
|
433
246
|
try {
|
|
434
247
|
const codex = await this.getCodex();
|
|
@@ -440,15 +253,13 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
440
253
|
}
|
|
441
254
|
}
|
|
442
255
|
async ensureOpenWorktree(worktree) {
|
|
443
|
-
if (existsSync(worktree.
|
|
256
|
+
if (existsSync(worktree.worktreePath))
|
|
444
257
|
return;
|
|
445
|
-
}
|
|
446
258
|
const project = this.config.projects.find((entry) => entry.id === worktree.repoId);
|
|
447
|
-
if (!project)
|
|
259
|
+
if (!project)
|
|
448
260
|
throw new Error(`Project not found for ${worktree.repoId}`);
|
|
449
|
-
}
|
|
450
261
|
const worktreeManager = new WorktreeManager(this.config);
|
|
451
|
-
await worktreeManager.ensureIssueWorktree(project.repoPath, project.worktreeRoot, worktree.
|
|
262
|
+
await worktreeManager.ensureIssueWorktree(project.repoPath, project.worktreeRoot, worktree.worktreePath, worktree.branchName);
|
|
452
263
|
}
|
|
453
264
|
async readLiveSummary(threadId, latestTimestampSeen) {
|
|
454
265
|
const codex = await this.getCodex();
|
|
@@ -15,34 +15,28 @@ export function formatInspect(result) {
|
|
|
15
15
|
const lines = [
|
|
16
16
|
header,
|
|
17
17
|
value("Title", result.issue?.title),
|
|
18
|
-
value("
|
|
19
|
-
value("Active
|
|
20
|
-
value("Latest
|
|
21
|
-
value("
|
|
22
|
-
value("Workspace", result.workspace?.worktreePath),
|
|
23
|
-
value("Branch", result.workspace?.branchName),
|
|
24
|
-
value("Latest thread", result.activeStageRun?.threadId ?? result.issue?.latestThreadId ?? result.workspace?.lastThreadId),
|
|
25
|
-
value("Latest turn", result.live?.latestTurnId ?? result.activeStageRun?.turnId ?? result.latestStageRun?.turnId),
|
|
18
|
+
value("State", result.issue?.factoryState),
|
|
19
|
+
result.activeRun ? value("Active run", `${result.activeRun.runType} (${result.activeRun.status})`) : undefined,
|
|
20
|
+
result.latestRun && !result.activeRun ? value("Latest run", `${result.latestRun.runType} (${result.latestRun.status})`) : undefined,
|
|
21
|
+
result.prNumber ? value("PR", `#${result.prNumber}${result.prReviewState ? ` [${result.prReviewState}]` : ""}`) : undefined,
|
|
26
22
|
result.statusNote ? value("Status", truncateLine(result.statusNote)) : undefined,
|
|
27
|
-
result.live?.latestTurnStatus ? value("Live turn", result.live.latestTurnStatus) : undefined,
|
|
28
|
-
result.live?.latestAssistantMessage ? `Latest assistant message:\n${truncateLine(result.live.latestAssistantMessage)}` : undefined,
|
|
29
23
|
].filter(Boolean);
|
|
30
24
|
return `${lines.join("\n")}\n`;
|
|
31
25
|
}
|
|
32
26
|
export function formatLive(result) {
|
|
33
27
|
const lines = [
|
|
34
28
|
value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
|
|
35
|
-
value("
|
|
36
|
-
value("Thread", result.
|
|
37
|
-
value("Turn", result.live?.latestTurnId ?? result.
|
|
38
|
-
value("Turn status", result.live?.latestTurnStatus ?? result.live?.threadStatus ?? result.
|
|
29
|
+
value("Run type", result.run.runType),
|
|
30
|
+
value("Thread", result.run.threadId),
|
|
31
|
+
value("Turn", result.live?.latestTurnId ?? result.run.turnId),
|
|
32
|
+
value("Turn status", result.live?.latestTurnStatus ?? result.live?.threadStatus ?? result.run.status),
|
|
39
33
|
value("Latest timestamp", result.live?.latestTimestampSeen),
|
|
40
34
|
result.live?.latestAssistantMessage ? `Latest assistant message:\n${truncateLine(result.live.latestAssistantMessage)}` : undefined,
|
|
41
35
|
].filter(Boolean);
|
|
42
36
|
return `${lines.join("\n")}\n`;
|
|
43
37
|
}
|
|
44
38
|
export function formatReport(result) {
|
|
45
|
-
const sections = result.
|
|
39
|
+
const sections = result.runs.map(({ run, report, summary }) => {
|
|
46
40
|
const changedFiles = report?.fileChanges
|
|
47
41
|
.map((entry) => (typeof entry.path === "string" ? entry.path : undefined))
|
|
48
42
|
.filter(Boolean)
|
|
@@ -50,10 +44,10 @@ export function formatReport(result) {
|
|
|
50
44
|
const commands = report?.commands.map((command) => command.command).join(" | ");
|
|
51
45
|
const tools = report?.toolCalls.map((tool) => `${tool.type}:${tool.name}`).join(", ");
|
|
52
46
|
return [
|
|
53
|
-
`${
|
|
54
|
-
value("Started",
|
|
55
|
-
value("Ended",
|
|
56
|
-
value("Thread",
|
|
47
|
+
`${run.runType} #${run.id} ${run.status}`,
|
|
48
|
+
value("Started", run.startedAt),
|
|
49
|
+
value("Ended", run.endedAt),
|
|
50
|
+
value("Thread", run.threadId),
|
|
57
51
|
summary?.latestAssistantMessage ? value("Summary", truncateLine(String(summary.latestAssistantMessage))) : undefined,
|
|
58
52
|
report?.assistantMessages.at(-1) ? value("Assistant conclusion", truncateLine(report.assistantMessages.at(-1))) : undefined,
|
|
59
53
|
commands ? value("Commands", commands) : undefined,
|
|
@@ -72,16 +66,16 @@ export function formatEvents(result) {
|
|
|
72
66
|
value("Turn", event.turnId),
|
|
73
67
|
event.parsedEvent ? JSON.stringify(event.parsedEvent, null, 2) : event.eventJson,
|
|
74
68
|
].join("\n"));
|
|
75
|
-
return `${value("
|
|
69
|
+
return `${value("Run", result.run.id)}\n${value("Run type", result.run.runType)}\n\n${sections.join("\n\n")}\n`;
|
|
76
70
|
}
|
|
77
71
|
export function formatWorktree(result, cdOnly) {
|
|
78
72
|
if (cdOnly) {
|
|
79
|
-
return `${result.
|
|
73
|
+
return `${result.worktreePath}\n`;
|
|
80
74
|
}
|
|
81
75
|
return `${[
|
|
82
76
|
value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
|
|
83
|
-
value("Worktree", result.
|
|
84
|
-
value("Branch", result.
|
|
77
|
+
value("Worktree", result.worktreePath),
|
|
78
|
+
value("Branch", result.branchName),
|
|
85
79
|
value("Repo", result.repoId),
|
|
86
80
|
].join("\n")}\n`;
|
|
87
81
|
}
|
|
@@ -90,7 +84,7 @@ function formatCommand(command, args) {
|
|
|
90
84
|
}
|
|
91
85
|
export function formatOpen(result, command) {
|
|
92
86
|
const commands = [
|
|
93
|
-
`cd ${result.
|
|
87
|
+
`cd ${result.worktreePath}`,
|
|
94
88
|
"git branch --show-current",
|
|
95
89
|
];
|
|
96
90
|
if (result.needsNewSession) {
|
|
@@ -106,7 +100,7 @@ export function formatOpen(result, command) {
|
|
|
106
100
|
export function formatRetry(result) {
|
|
107
101
|
return `${[
|
|
108
102
|
value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
|
|
109
|
-
value("Queued stage", result.
|
|
103
|
+
value("Queued stage", result.runType),
|
|
110
104
|
result.reason ? value("Reason", result.reason) : undefined,
|
|
111
105
|
]
|
|
112
106
|
.filter(Boolean)
|
|
@@ -117,9 +111,9 @@ export function formatList(items) {
|
|
|
117
111
|
.map((item) => [
|
|
118
112
|
item.issueKey ?? "-",
|
|
119
113
|
item.currentLinearState ?? "-",
|
|
120
|
-
item.
|
|
121
|
-
item.
|
|
122
|
-
item.
|
|
114
|
+
item.factoryState,
|
|
115
|
+
item.activeRunType ?? "-",
|
|
116
|
+
item.latestRunType ? `${item.latestRunType}:${item.latestRunStatus ?? "-"}` : "-",
|
|
123
117
|
item.updatedAt,
|
|
124
118
|
].join("\t"))
|
|
125
119
|
.join("\n")}\n`;
|