patchrelay 0.8.9 → 0.9.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.
Files changed (60) hide show
  1. package/README.md +64 -62
  2. package/dist/agent-session-plan.js +17 -17
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli/args.js +1 -1
  5. package/dist/cli/commands/issues.js +18 -18
  6. package/dist/cli/data.js +109 -298
  7. package/dist/cli/formatters/text.js +22 -28
  8. package/dist/cli/help.js +7 -7
  9. package/dist/cli/index.js +3 -3
  10. package/dist/config.js +13 -166
  11. package/dist/db/migrations.js +46 -154
  12. package/dist/db.js +369 -45
  13. package/dist/factory-state.js +55 -0
  14. package/dist/github-webhook-handler.js +199 -0
  15. package/dist/github-webhooks.js +166 -0
  16. package/dist/hook-runner.js +28 -0
  17. package/dist/http.js +48 -22
  18. package/dist/issue-query-service.js +33 -38
  19. package/dist/linear-workflow.js +5 -118
  20. package/dist/preflight.js +1 -6
  21. package/dist/project-resolution.js +12 -1
  22. package/dist/run-orchestrator.js +446 -0
  23. package/dist/{stage-reporting.js → run-reporting.js} +11 -13
  24. package/dist/service-runtime.js +12 -61
  25. package/dist/service-webhooks.js +7 -52
  26. package/dist/service.js +39 -61
  27. package/dist/webhook-handler.js +387 -0
  28. package/dist/webhook-installation-handler.js +3 -8
  29. package/package.json +2 -1
  30. package/dist/db/authoritative-ledger-store.js +0 -536
  31. package/dist/db/issue-projection-store.js +0 -54
  32. package/dist/db/issue-workflow-coordinator.js +0 -320
  33. package/dist/db/issue-workflow-store.js +0 -194
  34. package/dist/db/run-report-store.js +0 -33
  35. package/dist/db/stage-event-store.js +0 -33
  36. package/dist/db/webhook-event-store.js +0 -59
  37. package/dist/db-ports.js +0 -5
  38. package/dist/ledger-ports.js +0 -1
  39. package/dist/reconciliation-action-applier.js +0 -68
  40. package/dist/reconciliation-actions.js +0 -1
  41. package/dist/reconciliation-engine.js +0 -350
  42. package/dist/reconciliation-snapshot-builder.js +0 -135
  43. package/dist/reconciliation-types.js +0 -1
  44. package/dist/service-stage-finalizer.js +0 -753
  45. package/dist/service-stage-runner.js +0 -336
  46. package/dist/service-webhook-processor.js +0 -411
  47. package/dist/stage-agent-activity-publisher.js +0 -59
  48. package/dist/stage-event-ports.js +0 -1
  49. package/dist/stage-failure.js +0 -92
  50. package/dist/stage-handoff.js +0 -107
  51. package/dist/stage-launch.js +0 -84
  52. package/dist/stage-lifecycle-publisher.js +0 -284
  53. package/dist/stage-turn-input-dispatcher.js +0 -104
  54. package/dist/webhook-agent-session-handler.js +0 -228
  55. package/dist/webhook-comment-handler.js +0 -141
  56. package/dist/webhook-desired-stage-recorder.js +0 -122
  57. package/dist/webhook-event-ports.js +0 -1
  58. package/dist/workflow-policy.js +0 -149
  59. package/dist/workflow-ports.js +0 -1
  60. /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, stageRunId) {
34
- const events = db.stageEvents.listThreadEvents(stageRunId);
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.issueWorkflows.getTrackedIssueByKey(issueKey);
64
- if (!issue) {
53
+ const issue = this.db.getTrackedIssueByKey(issueKey);
54
+ if (!issue)
65
55
  return undefined;
66
- }
67
- const ledger = this.getLedgerIssueContext(issue.projectId, issue.linearIssueId);
68
- const workspace = this.getWorkspaceForIssue(issue, ledger);
69
- const activeStageRun = this.getActiveStageRunForIssue(issue, ledger);
70
- const latestStageRun = this.db.issueWorkflows.getLatestStageRunForIssue(issue.projectId, issue.linearIssueId);
71
- const latestReport = latestStageRun?.reportJson ? JSON.parse(latestStageRun.reportJson) : undefined;
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
- (latestStageRun?.status === "failed" ? "Latest stage failed." : undefined) ??
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
- ...(workspace ? { workspace } : {}),
84
- ...(activeStageRun ? { activeStageRun } : {}),
85
- ...(latestStageRun ? { latestStageRun } : {}),
67
+ ...(activeRun ? { activeRun } : {}),
68
+ ...(!activeRun && latestRun ? { latestRun } : {}),
86
69
  ...(latestReport ? { latestReport } : {}),
87
70
  ...(latestSummary ? { latestSummary } : {}),
88
- ...(live ? { live } : {}),
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.issueWorkflows.getTrackedIssueByKey(issueKey);
94
- if (!issue) {
77
+ const issue = this.db.getTrackedIssueByKey(issueKey);
78
+ if (!issue)
95
79
  return undefined;
96
- }
97
- const stageRun = this.getActiveStageRunForIssue(issue);
98
- if (!stageRun) {
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
- const live = stageRun.threadId &&
102
- (await this.readLiveSummary(stageRun.threadId, latestEventTimestamp(this.db, stageRun.id)).catch(() => undefined));
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.issueWorkflows.getTrackedIssueByKey(issueKey);
111
- if (!issue) {
89
+ const issue = this.db.getTrackedIssueByKey(issueKey);
90
+ if (!issue)
112
91
  return undefined;
113
- }
114
- const stages = this.db
115
- .issueWorkflows.listStageRunsForIssue(issue.projectId, issue.linearIssueId)
116
- .filter((stageRun) => {
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((stageRun) => ({
127
- stageRun,
128
- ...(stageRun.reportJson ? { report: JSON.parse(stageRun.reportJson) } : {}),
129
- ...(safeJsonParse(stageRun.summaryJson) ? { summary: safeJsonParse(stageRun.summaryJson) } : {}),
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, stages };
107
+ return { issue, runs };
132
108
  }
133
109
  events(issueKey, options) {
134
- const issue = this.db.issueWorkflows.getTrackedIssueByKey(issueKey);
135
- if (!issue) {
110
+ const issue = this.db.getTrackedIssueByKey(issueKey);
111
+ if (!issue)
136
112
  return undefined;
137
- }
138
- const stageRun = (options?.stageRunId !== undefined ? this.db.issueWorkflows.getStageRun(options.stageRunId) : undefined) ??
139
- this.getActiveStageRunForIssue(issue) ??
140
- this.db.issueWorkflows.getLatestStageRunForIssue(issue.projectId, issue.linearIssueId);
141
- if (!stageRun || stageRun.projectId !== issue.projectId || stageRun.linearIssueId !== issue.linearIssueId) {
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
- .stageEvents.listThreadEvents(stageRun.id)
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, stageRun, events };
127
+ return { issue, run, events };
153
128
  }
154
129
  worktree(issueKey) {
155
- const issue = this.db.issueWorkflows.getTrackedIssueByKey(issueKey);
156
- if (!issue) {
130
+ const issue = this.db.getTrackedIssueByKey(issueKey);
131
+ if (!issue)
157
132
  return undefined;
158
- }
159
- const workspace = this.getWorkspaceForIssue(issue);
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 = this.getStoredOpenThreadId(worktree);
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 existingThreadId = await this.resolveStoredOpenThreadId(worktree);
189
- if (existingThreadId) {
190
- return {
191
- ...worktree,
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
- cwd: worktree.workspace.worktreePath,
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
- this.db.issueSessions.touchIssueSession(thread.id);
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.issueWorkflows.getTrackedIssueByKey(issueKey);
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 ledger = this.getLedgerIssueContext(issue.projectId, issue.linearIssueId);
231
- if (ledger.issueControl?.activeRunLeaseId !== undefined) {
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.workflowCoordinator.setIssueDesiredStage(issue.projectId, issue.linearIssueId, stage, {
249
- desiredReceiptId: receipt.id,
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("ai.project_id = ?");
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
- ai.project_id,
276
- ai.linear_issue_id,
277
- ip.issue_key,
278
- ip.title,
279
- ip.current_linear_state,
280
- COALESCE(ic.lifecycle_status, 'idle') AS lifecycle_status,
281
- COALESCE(ic.updated_at, ip.updated_at) AS updated_at,
282
- active_run.stage AS active_stage,
283
- latest_run.stage AS latest_stage,
284
- latest_run.status AS latest_stage_status
285
- FROM all_issues ai
286
- LEFT JOIN issue_projection ip
287
- ON ip.project_id = ai.project_id AND ip.linear_issue_id = ai.linear_issue_id
288
- LEFT JOIN issue_control ic
289
- ON ic.project_id = ai.project_id AND ic.linear_issue_id = ai.linear_issue_id
290
- LEFT JOIN run_leases active_run
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 COALESCE(ic.updated_at, ip.updated_at) DESC, ip.issue_key ASC, ai.linear_issue_id ASC
223
+ ORDER BY i.updated_at DESC, i.issue_key ASC
301
224
  `)
302
225
  .all(...values);
303
- const items = rows.map((row) => {
304
- const projectId = String(row.project_id);
305
- const linearIssueId = String(row.linear_issue_id);
306
- const issueKey = row.issue_key === null ? undefined : String(row.issue_key);
307
- const issue = this.db.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
308
- const ledger = issue ? this.getLedgerIssueContext(issue.projectId, issue.linearIssueId) : undefined;
309
- return {
310
- ...(issueKey ? { issueKey } : {}),
311
- ...(row.title === null ? {} : { title: String(row.title) }),
312
- projectId,
313
- ...(row.current_linear_state === null ? {} : { currentLinearState: String(row.current_linear_state) }),
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.activeStage) {
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.workspace.worktreePath)) {
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.workspace.worktreePath, worktree.workspace.branchName);
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("Lifecycle", result.issue?.lifecycleStatus),
19
- value("Active stage", result.activeStageRun?.stage),
20
- value("Latest stage", result.latestStageRun?.stage),
21
- value("Latest result", result.latestStageRun?.status),
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("Stage", result.stageRun.stage),
36
- value("Thread", result.stageRun.threadId),
37
- value("Turn", result.live?.latestTurnId ?? result.stageRun.turnId),
38
- value("Turn status", result.live?.latestTurnStatus ?? result.live?.threadStatus ?? result.stageRun.status),
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.stages.map(({ stageRun, report, summary }) => {
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
- `${stageRun.stage} #${stageRun.id} ${stageRun.status}`,
54
- value("Started", stageRun.startedAt),
55
- value("Ended", stageRun.endedAt),
56
- value("Thread", stageRun.threadId),
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("Stage run", result.stageRun.id)}\n${value("Stage", result.stageRun.stage)}\n\n${sections.join("\n\n")}\n`;
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.workspace.worktreePath}\n`;
73
+ return `${result.worktreePath}\n`;
80
74
  }
81
75
  return `${[
82
76
  value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
83
- value("Worktree", result.workspace.worktreePath),
84
- value("Branch", result.workspace.branchName),
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.workspace.worktreePath}`,
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.stage),
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.lifecycleStatus,
121
- item.activeStage ?? "-",
122
- item.latestStage ? `${item.latestStage}:${item.latestStageStatus ?? "-"}` : "-",
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`;