patchrelay 0.8.8 → 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.
Files changed (57) 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/commands/issues.js +12 -12
  5. package/dist/cli/data.js +109 -298
  6. package/dist/cli/formatters/text.js +22 -28
  7. package/dist/config.js +13 -166
  8. package/dist/db/migrations.js +46 -154
  9. package/dist/db.js +369 -45
  10. package/dist/factory-state.js +55 -0
  11. package/dist/github-webhook-handler.js +199 -0
  12. package/dist/github-webhooks.js +166 -0
  13. package/dist/hook-runner.js +28 -0
  14. package/dist/http.js +48 -22
  15. package/dist/issue-query-service.js +33 -38
  16. package/dist/linear-workflow.js +5 -118
  17. package/dist/preflight.js +1 -6
  18. package/dist/project-resolution.js +12 -1
  19. package/dist/run-orchestrator.js +446 -0
  20. package/dist/{stage-reporting.js → run-reporting.js} +11 -13
  21. package/dist/service-runtime.js +21 -54
  22. package/dist/service-webhooks.js +7 -52
  23. package/dist/service.js +39 -61
  24. package/dist/webhook-handler.js +387 -0
  25. package/dist/webhook-installation-handler.js +3 -8
  26. package/package.json +2 -1
  27. package/dist/db/authoritative-ledger-store.js +0 -536
  28. package/dist/db/issue-projection-store.js +0 -54
  29. package/dist/db/issue-workflow-coordinator.js +0 -320
  30. package/dist/db/issue-workflow-store.js +0 -194
  31. package/dist/db/run-report-store.js +0 -33
  32. package/dist/db/stage-event-store.js +0 -33
  33. package/dist/db/webhook-event-store.js +0 -59
  34. package/dist/db-ports.js +0 -5
  35. package/dist/ledger-ports.js +0 -1
  36. package/dist/reconciliation-action-applier.js +0 -68
  37. package/dist/reconciliation-actions.js +0 -1
  38. package/dist/reconciliation-engine.js +0 -350
  39. package/dist/reconciliation-snapshot-builder.js +0 -135
  40. package/dist/reconciliation-types.js +0 -1
  41. package/dist/service-stage-finalizer.js +0 -753
  42. package/dist/service-stage-runner.js +0 -336
  43. package/dist/service-webhook-processor.js +0 -411
  44. package/dist/stage-agent-activity-publisher.js +0 -59
  45. package/dist/stage-event-ports.js +0 -1
  46. package/dist/stage-failure.js +0 -92
  47. package/dist/stage-handoff.js +0 -107
  48. package/dist/stage-launch.js +0 -84
  49. package/dist/stage-lifecycle-publisher.js +0 -284
  50. package/dist/stage-turn-input-dispatcher.js +0 -104
  51. package/dist/webhook-agent-session-handler.js +0 -228
  52. package/dist/webhook-comment-handler.js +0 -141
  53. package/dist/webhook-desired-stage-recorder.js +0 -122
  54. package/dist/webhook-event-ports.js +0 -1
  55. package/dist/workflow-policy.js +0 -149
  56. package/dist/workflow-ports.js +0 -1
  57. /package/dist/{installation-ports.js → github-types.js} +0 -0
package/dist/config.js CHANGED
@@ -7,32 +7,6 @@ import { ensureAbsolutePath } from "./utils.js";
7
7
  const LINEAR_OAUTH_CALLBACK_PATH = "/oauth/linear/callback";
8
8
  const REPO_SETTINGS_DIRNAME = ".patchrelay";
9
9
  const REPO_SETTINGS_FILENAME = "project.json";
10
- const workflowSchema = z.object({
11
- id: z.string().min(1),
12
- when_state: z.string().min(1),
13
- active_state: z.string().min(1),
14
- workflow_file: z.string().min(1),
15
- fallback_state: z.string().min(1).nullable().optional(),
16
- });
17
- const workflowDefinitionSchema = z.object({
18
- id: z.string().min(1),
19
- stages: z.array(workflowSchema).min(1),
20
- });
21
- const workflowSelectionSchema = z.object({
22
- default_workflow: z.string().min(1).optional(),
23
- by_label: z
24
- .array(z.object({
25
- label: z.string().min(1),
26
- workflow: z.string().min(1),
27
- }))
28
- .default([]),
29
- });
30
- const workflowLabelsSchema = z
31
- .object({
32
- working: z.string().min(1).optional(),
33
- awaiting_handoff: z.string().min(1).optional(),
34
- })
35
- .optional();
36
10
  const trustedActorsSchema = z
37
11
  .object({
38
12
  ids: z.array(z.string().min(1)).default([]),
@@ -42,11 +16,6 @@ const trustedActorsSchema = z
42
16
  })
43
17
  .optional();
44
18
  const repoSettingsSchema = z.object({
45
- workflows: z.array(workflowSchema).min(1).optional(),
46
- workflow_definitions: z.array(workflowDefinitionSchema).min(1).optional(),
47
- workflow_selection: workflowSelectionSchema.optional(),
48
- workflow_labels: workflowLabelsSchema,
49
- trusted_actors: trustedActorsSchema,
50
19
  trigger_events: z.array(z.string().min(1)).min(1).optional(),
51
20
  branch_prefix: z.string().min(1).optional(),
52
21
  });
@@ -54,16 +23,16 @@ const projectSchema = z.object({
54
23
  id: z.string().min(1),
55
24
  repo_path: z.string().min(1),
56
25
  worktree_root: z.string().min(1).optional(),
57
- workflows: z.array(workflowSchema).min(1).optional(),
58
- workflow_definitions: z.array(workflowDefinitionSchema).min(1).optional(),
59
- workflow_selection: workflowSelectionSchema.optional(),
60
- workflow_labels: workflowLabelsSchema,
61
26
  trusted_actors: trustedActorsSchema,
62
27
  issue_key_prefixes: z.array(z.string().min(1)).default([]),
63
28
  linear_team_ids: z.array(z.string().min(1)).default([]),
64
29
  allow_labels: z.array(z.string().min(1)).default([]),
65
30
  trigger_events: z.array(z.string().min(1)).min(1).optional(),
66
31
  branch_prefix: z.string().min(1).optional(),
32
+ github: z.object({
33
+ webhook_secret: z.string().min(1).optional(),
34
+ repo_full_name: z.string().min(1).optional(),
35
+ }).optional(),
67
36
  });
68
37
  const configSchema = z.object({
69
38
  server: z.object({
@@ -75,6 +44,7 @@ const configSchema = z.object({
75
44
  }),
76
45
  ingress: z.object({
77
46
  linear_webhook_path: z.string().default("/webhooks/linear"),
47
+ github_webhook_path: z.string().default("/webhooks/github"),
78
48
  max_body_bytes: z.number().int().positive().default(262144),
79
49
  max_timestamp_skew_seconds: z.number().int().positive().default(60),
80
50
  }),
@@ -151,42 +121,6 @@ function normalizeTriggerEvents(actor, configured) {
151
121
  });
152
122
  return [...required, ...extras];
153
123
  }
154
- const builtinWorkflows = [
155
- {
156
- id: "development",
157
- when_state: "Start",
158
- active_state: "Implementing",
159
- workflow_file: "IMPLEMENTATION_WORKFLOW.md",
160
- fallback_state: "Human Needed",
161
- },
162
- {
163
- id: "review",
164
- when_state: "Review",
165
- active_state: "Reviewing",
166
- workflow_file: "REVIEW_WORKFLOW.md",
167
- fallback_state: "Human Needed",
168
- },
169
- {
170
- id: "deploy",
171
- when_state: "Deploy",
172
- active_state: "Deploying",
173
- workflow_file: "DEPLOY_WORKFLOW.md",
174
- fallback_state: "Human Needed",
175
- },
176
- {
177
- id: "cleanup",
178
- when_state: "Cleanup",
179
- active_state: "Cleaning Up",
180
- workflow_file: "CLEANUP_WORKFLOW.md",
181
- fallback_state: "Human Needed",
182
- },
183
- ];
184
- const builtinWorkflowDefinitions = [
185
- {
186
- id: "default",
187
- stages: builtinWorkflows,
188
- },
189
- ];
190
124
  function withSectionDefaults(input) {
191
125
  const source = input && typeof input === "object" ? input : {};
192
126
  const { linear: _linear, runner: _runner, ...rest } = source;
@@ -283,9 +217,6 @@ function readEnvFilesForProfile(configPath, profile) {
283
217
  const paths = getEnvFilesForProfile(configPath, profile);
284
218
  return Object.assign({}, ...paths.map((envPath) => readEnvFile(envPath)));
285
219
  }
286
- function resolveWorkflowFilePath(repoPath, workflowFile) {
287
- return path.isAbsolute(workflowFile) ? ensureAbsolutePath(workflowFile) : path.resolve(repoPath, workflowFile);
288
- }
289
220
  function parseJsonFile(filePath, label) {
290
221
  const raw = readFileSync(filePath, "utf8");
291
222
  try {
@@ -309,49 +240,6 @@ function readRepoSettings(repoPath, env) {
309
240
  configPath,
310
241
  };
311
242
  }
312
- function mergeWorkflowStages(repoPath, workflows) {
313
- return workflows.map((workflow) => ({
314
- id: workflow.id,
315
- whenState: workflow.when_state,
316
- activeState: workflow.active_state,
317
- workflowFile: resolveWorkflowFilePath(repoPath, workflow.workflow_file),
318
- ...(workflow.fallback_state ? { fallbackState: workflow.fallback_state } : {}),
319
- }));
320
- }
321
- function mergeWorkflowDefinitions(repoPath, definitions) {
322
- return definitions.map((definition) => ({
323
- id: definition.id,
324
- stages: mergeWorkflowStages(repoPath, definition.stages),
325
- }));
326
- }
327
- function resolveProjectWorkflowConfig(repoPath, project, repoSettings) {
328
- const selectedDefinitions = repoSettings?.workflow_definitions ??
329
- project.workflow_definitions ??
330
- (repoSettings?.workflows
331
- ? [{ id: "default", stages: repoSettings.workflows }]
332
- : project.workflows
333
- ? [{ id: "default", stages: project.workflows }]
334
- : builtinWorkflowDefinitions.map((definition) => ({ id: definition.id, stages: [...definition.stages] })));
335
- const workflowDefinitions = mergeWorkflowDefinitions(repoPath, selectedDefinitions);
336
- const selectionSource = repoSettings?.workflow_selection ?? project.workflow_selection;
337
- const defaultWorkflowId = selectionSource?.default_workflow ?? workflowDefinitions[0]?.id;
338
- const workflows = workflowDefinitions.find((definition) => definition.id === defaultWorkflowId)?.stages ?? workflowDefinitions[0]?.stages ?? [];
339
- return {
340
- workflows,
341
- ...(workflowDefinitions.length > 0 ? { workflowDefinitions } : {}),
342
- ...(defaultWorkflowId || (selectionSource?.by_label?.length ?? 0) > 0
343
- ? {
344
- workflowSelection: {
345
- ...(defaultWorkflowId ? { defaultWorkflowId } : {}),
346
- byLabel: (selectionSource?.by_label ?? []).map((entry) => ({
347
- label: entry.label,
348
- workflowId: entry.workflow,
349
- })),
350
- },
351
- }
352
- : {}),
353
- };
354
- }
355
243
  function defaultWorktreeRoot(projectId) {
356
244
  return path.join(getPatchRelayDataDir(), "worktrees", projectId);
357
245
  }
@@ -427,6 +315,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
427
315
  },
428
316
  ingress: {
429
317
  linearWebhookPath: parsed.ingress.linear_webhook_path,
318
+ githubWebhookPath: parsed.ingress.github_webhook_path,
430
319
  maxBodyBytes: parsed.ingress.max_body_bytes,
431
320
  maxTimestampSkewSeconds: parsed.ingress.max_timestamp_skew_seconds,
432
321
  },
@@ -478,24 +367,11 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
478
367
  projects: parsed.projects.map((project) => {
479
368
  const repoPath = ensureAbsolutePath(project.repo_path);
480
369
  const repoSettings = readRepoSettings(repoPath, env);
481
- const workflowConfig = resolveProjectWorkflowConfig(repoPath, project, repoSettings);
482
- const workflowLabels = repoSettings?.workflow_labels ?? project.workflow_labels;
483
- const trustedActors = repoSettings?.trusted_actors ?? project.trusted_actors;
370
+ const trustedActors = project.trusted_actors;
484
371
  return {
485
372
  id: project.id,
486
373
  repoPath,
487
374
  worktreeRoot: ensureAbsolutePath(project.worktree_root ?? defaultWorktreeRoot(project.id)),
488
- workflows: workflowConfig.workflows,
489
- ...(workflowConfig.workflowDefinitions ? { workflowDefinitions: workflowConfig.workflowDefinitions } : {}),
490
- ...(workflowConfig.workflowSelection ? { workflowSelection: workflowConfig.workflowSelection } : {}),
491
- ...(workflowLabels
492
- ? {
493
- workflowLabels: {
494
- ...(workflowLabels.working ? { working: workflowLabels.working } : {}),
495
- ...(workflowLabels.awaiting_handoff ? { awaitingHandoff: workflowLabels.awaiting_handoff } : {}),
496
- },
497
- }
498
- : {}),
499
375
  ...(trustedActors
500
376
  ? {
501
377
  trustedActors: {
@@ -513,6 +389,12 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
513
389
  project.trigger_events),
514
390
  branchPrefix: repoSettings?.branch_prefix ?? project.branch_prefix ?? defaultBranchPrefix(project.id),
515
391
  ...(repoSettings?.configPath ? { repoSettingsPath: repoSettings.configPath } : {}),
392
+ ...(project.github ? {
393
+ github: {
394
+ ...(project.github.webhook_secret ? { webhookSecret: project.github.webhook_secret } : {}),
395
+ ...(project.github.repo_full_name ? { repoFullName: project.github.repo_full_name } : {}),
396
+ },
397
+ } : {}),
516
398
  };
517
399
  }),
518
400
  };
@@ -578,41 +460,6 @@ function validateConfigSemantics(config, options) {
578
460
  }
579
461
  linearTeamIds.set(teamId, project.id);
580
462
  }
581
- const workflowDefinitions = project.workflowDefinitions ?? [{ id: "default", stages: project.workflows }];
582
- const definitionIds = new Set();
583
- for (const definition of workflowDefinitions) {
584
- const normalizedDefinitionId = definition.id.trim().toLowerCase();
585
- if (definitionIds.has(normalizedDefinitionId)) {
586
- throw new Error(`Workflow definition "${definition.id}" is configured more than once in project ${project.id}`);
587
- }
588
- definitionIds.add(normalizedDefinitionId);
589
- const workflowIds = new Set();
590
- const workflowStates = new Set();
591
- for (const workflow of definition.stages) {
592
- const normalizedWorkflowId = workflow.id.trim().toLowerCase();
593
- if (workflowIds.has(normalizedWorkflowId)) {
594
- throw new Error(`Workflow id "${workflow.id}" is configured more than once in project ${project.id}`);
595
- }
596
- workflowIds.add(normalizedWorkflowId);
597
- const normalizedState = workflow.whenState.trim().toLowerCase();
598
- if (workflowStates.has(normalizedState)) {
599
- throw new Error(`Linear state "${workflow.whenState}" is configured for more than one workflow in project ${project.id}`);
600
- }
601
- workflowStates.add(normalizedState);
602
- }
603
- }
604
- if (project.workflowSelection?.defaultWorkflowId) {
605
- const normalizedDefaultWorkflowId = project.workflowSelection.defaultWorkflowId.trim().toLowerCase();
606
- if (!workflowDefinitions.some((definition) => definition.id.trim().toLowerCase() === normalizedDefaultWorkflowId)) {
607
- throw new Error(`Default workflow "${project.workflowSelection.defaultWorkflowId}" does not exist in project ${project.id}`);
608
- }
609
- }
610
- for (const rule of project.workflowSelection?.byLabel ?? []) {
611
- const normalizedWorkflowId = rule.workflowId.trim().toLowerCase();
612
- if (!workflowDefinitions.some((definition) => definition.id.trim().toLowerCase() === normalizedWorkflowId)) {
613
- throw new Error(`Workflow selection for label "${rule.label}" points to unknown workflow "${rule.workflowId}" in project ${project.id}`);
614
- }
615
- }
616
463
  }
617
464
  if (config.operatorApi.enabled &&
618
465
  config.server.bind !== "127.0.0.1" &&
@@ -1,151 +1,67 @@
1
- const baseMigration = `
2
- CREATE TABLE IF NOT EXISTS webhook_events (
3
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4
- webhook_id TEXT NOT NULL UNIQUE,
5
- received_at TEXT NOT NULL,
6
- event_type TEXT NOT NULL,
7
- issue_id TEXT,
8
- project_id TEXT,
9
- headers_json TEXT NOT NULL,
10
- payload_json TEXT NOT NULL,
11
- signature_valid INTEGER NOT NULL,
12
- dedupe_status TEXT NOT NULL,
13
- processing_status TEXT NOT NULL DEFAULT 'pending'
14
- );
15
-
16
- CREATE TABLE IF NOT EXISTS event_receipts (
17
- id INTEGER PRIMARY KEY AUTOINCREMENT,
18
- source TEXT NOT NULL,
19
- external_id TEXT NOT NULL,
20
- event_type TEXT NOT NULL,
21
- received_at TEXT NOT NULL,
22
- acceptance_status TEXT NOT NULL,
23
- processing_status TEXT NOT NULL DEFAULT 'pending',
24
- project_id TEXT,
25
- linear_issue_id TEXT,
26
- headers_json TEXT,
27
- payload_json TEXT,
28
- UNIQUE(source, external_id)
29
- );
30
-
31
- CREATE TABLE IF NOT EXISTS issue_control (
32
- id INTEGER PRIMARY KEY AUTOINCREMENT,
33
- project_id TEXT NOT NULL,
34
- linear_issue_id TEXT NOT NULL,
35
- selected_workflow_id TEXT,
36
- desired_stage TEXT,
37
- desired_receipt_id INTEGER,
38
- active_run_lease_id INTEGER,
39
- active_workspace_ownership_id INTEGER,
40
- service_owned_comment_id TEXT,
41
- active_agent_session_id TEXT,
42
- lifecycle_status TEXT NOT NULL,
43
- updated_at TEXT NOT NULL,
44
- UNIQUE(project_id, linear_issue_id)
45
- );
46
-
47
- CREATE TABLE IF NOT EXISTS issue_projection (
1
+ const schema = `
2
+ CREATE TABLE IF NOT EXISTS issues (
48
3
  id INTEGER PRIMARY KEY AUTOINCREMENT,
49
4
  project_id TEXT NOT NULL,
50
5
  linear_issue_id TEXT NOT NULL,
51
6
  issue_key TEXT,
52
7
  title TEXT,
53
- issue_url TEXT,
8
+ url TEXT,
54
9
  current_linear_state TEXT,
55
- last_webhook_at TEXT,
56
- updated_at TEXT NOT NULL,
57
- UNIQUE(project_id, linear_issue_id)
58
- );
59
-
60
- CREATE TABLE IF NOT EXISTS workspace_ownership (
61
- id INTEGER PRIMARY KEY AUTOINCREMENT,
62
- project_id TEXT NOT NULL,
63
- linear_issue_id TEXT NOT NULL,
64
- branch_name TEXT NOT NULL,
65
- worktree_path TEXT NOT NULL,
66
- status TEXT NOT NULL,
67
- current_run_lease_id INTEGER,
68
- created_at TEXT NOT NULL,
10
+ factory_state TEXT NOT NULL DEFAULT 'delegated',
11
+ pending_run_type TEXT,
12
+ pending_run_context_json TEXT,
13
+ branch_name TEXT,
14
+ worktree_path TEXT,
15
+ thread_id TEXT,
16
+ active_run_id INTEGER,
17
+ status_comment_id TEXT,
18
+ agent_session_id TEXT,
19
+ pr_number INTEGER,
20
+ pr_url TEXT,
21
+ pr_state TEXT,
22
+ pr_review_state TEXT,
23
+ pr_check_status TEXT,
24
+ ci_repair_attempts INTEGER NOT NULL DEFAULT 0,
25
+ queue_repair_attempts INTEGER NOT NULL DEFAULT 0,
69
26
  updated_at TEXT NOT NULL,
70
27
  UNIQUE(project_id, linear_issue_id)
71
28
  );
72
29
 
73
- CREATE TABLE IF NOT EXISTS run_leases (
30
+ CREATE TABLE IF NOT EXISTS runs (
74
31
  id INTEGER PRIMARY KEY AUTOINCREMENT,
75
- issue_control_id INTEGER NOT NULL,
32
+ issue_id INTEGER NOT NULL REFERENCES issues(id),
76
33
  project_id TEXT NOT NULL,
77
34
  linear_issue_id TEXT NOT NULL,
78
- workspace_ownership_id INTEGER NOT NULL,
79
- stage TEXT NOT NULL,
35
+ run_type TEXT NOT NULL DEFAULT 'implementation',
80
36
  status TEXT NOT NULL,
81
- trigger_receipt_id INTEGER,
82
- workflow_file TEXT NOT NULL DEFAULT '',
83
- prompt_text TEXT NOT NULL DEFAULT '',
37
+ prompt_text TEXT,
84
38
  thread_id TEXT,
85
- parent_thread_id TEXT,
86
39
  turn_id TEXT,
87
- started_at TEXT NOT NULL,
88
- ended_at TEXT,
40
+ parent_thread_id TEXT,
41
+ summary_json TEXT,
42
+ report_json TEXT,
89
43
  failure_reason TEXT,
90
- FOREIGN KEY(issue_control_id) REFERENCES issue_control(id) ON DELETE CASCADE,
91
- FOREIGN KEY(workspace_ownership_id) REFERENCES workspace_ownership(id) ON DELETE CASCADE,
92
- FOREIGN KEY(trigger_receipt_id) REFERENCES event_receipts(id) ON DELETE SET NULL
44
+ started_at TEXT NOT NULL,
45
+ ended_at TEXT
93
46
  );
94
47
 
95
- CREATE TABLE IF NOT EXISTS issue_sessions (
48
+ CREATE TABLE IF NOT EXISTS webhook_events (
96
49
  id INTEGER PRIMARY KEY AUTOINCREMENT,
97
- project_id TEXT NOT NULL,
98
- linear_issue_id TEXT NOT NULL,
99
- workspace_ownership_id INTEGER NOT NULL,
100
- run_lease_id INTEGER,
101
- thread_id TEXT NOT NULL UNIQUE,
102
- parent_thread_id TEXT,
103
- source TEXT NOT NULL,
104
- linked_agent_session_id TEXT,
105
- created_at TEXT NOT NULL,
106
- updated_at TEXT NOT NULL,
107
- last_opened_at TEXT,
108
- FOREIGN KEY(workspace_ownership_id) REFERENCES workspace_ownership(id) ON DELETE CASCADE,
109
- FOREIGN KEY(run_lease_id) REFERENCES run_leases(id) ON DELETE SET NULL
110
- );
111
-
112
- CREATE TABLE IF NOT EXISTS run_reports (
113
- run_lease_id INTEGER PRIMARY KEY,
114
- summary_json TEXT,
115
- report_json TEXT,
116
- created_at TEXT NOT NULL,
117
- updated_at TEXT NOT NULL,
118
- FOREIGN KEY(run_lease_id) REFERENCES run_leases(id) ON DELETE CASCADE
50
+ webhook_id TEXT NOT NULL UNIQUE,
51
+ received_at TEXT NOT NULL,
52
+ project_id TEXT,
53
+ payload_json TEXT,
54
+ processing_status TEXT NOT NULL DEFAULT 'pending'
119
55
  );
120
56
 
121
57
  CREATE TABLE IF NOT EXISTS run_thread_events (
122
58
  id INTEGER PRIMARY KEY AUTOINCREMENT,
123
- run_lease_id INTEGER NOT NULL,
59
+ run_id INTEGER NOT NULL,
124
60
  thread_id TEXT NOT NULL,
125
61
  turn_id TEXT,
126
62
  method TEXT NOT NULL,
127
63
  event_json TEXT NOT NULL,
128
- created_at TEXT NOT NULL,
129
- FOREIGN KEY(run_lease_id) REFERENCES run_leases(id) ON DELETE CASCADE
130
- );
131
-
132
- CREATE TABLE IF NOT EXISTS obligations (
133
- id INTEGER PRIMARY KEY AUTOINCREMENT,
134
- project_id TEXT NOT NULL,
135
- linear_issue_id TEXT NOT NULL,
136
- kind TEXT NOT NULL,
137
- status TEXT NOT NULL,
138
- source TEXT NOT NULL,
139
- payload_json TEXT NOT NULL,
140
- run_lease_id INTEGER,
141
- thread_id TEXT,
142
- turn_id TEXT,
143
- dedupe_key TEXT,
144
- last_error TEXT,
145
- created_at TEXT NOT NULL,
146
- updated_at TEXT NOT NULL,
147
- completed_at TEXT,
148
- FOREIGN KEY(run_lease_id) REFERENCES run_leases(id) ON DELETE SET NULL
64
+ created_at TEXT NOT NULL
149
65
  );
150
66
 
151
67
  CREATE TABLE IF NOT EXISTS linear_installations (
@@ -195,44 +111,20 @@ CREATE TABLE IF NOT EXISTS operator_feed_events (
195
111
  issue_key TEXT,
196
112
  project_id TEXT,
197
113
  stage TEXT,
198
- status TEXT,
199
- workflow_id TEXT,
200
- next_stage TEXT
114
+ status TEXT
201
115
  );
202
116
 
203
- CREATE INDEX IF NOT EXISTS idx_event_receipts_project_issue ON event_receipts(project_id, linear_issue_id);
204
- CREATE INDEX IF NOT EXISTS idx_issue_control_ready ON issue_control(desired_stage, active_run_lease_id);
205
- CREATE INDEX IF NOT EXISTS idx_issue_projection_issue_key ON issue_projection(issue_key);
206
- CREATE INDEX IF NOT EXISTS idx_issue_sessions_issue ON issue_sessions(project_id, linear_issue_id, id DESC);
207
- CREATE INDEX IF NOT EXISTS idx_issue_sessions_last_opened ON issue_sessions(project_id, linear_issue_id, last_opened_at DESC, id DESC);
208
- CREATE INDEX IF NOT EXISTS idx_run_leases_active ON run_leases(status, project_id, linear_issue_id);
209
- CREATE INDEX IF NOT EXISTS idx_run_leases_thread ON run_leases(thread_id);
210
- CREATE INDEX IF NOT EXISTS idx_run_thread_events_run ON run_thread_events(run_lease_id, id);
117
+ CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id, linear_issue_id);
118
+ CREATE INDEX IF NOT EXISTS idx_issues_key ON issues(issue_key);
119
+ CREATE INDEX IF NOT EXISTS idx_issues_ready ON issues(pending_run_type, active_run_id);
120
+ CREATE INDEX IF NOT EXISTS idx_issues_branch ON issues(branch_name);
121
+ CREATE INDEX IF NOT EXISTS idx_runs_issue ON runs(issue_id);
122
+ CREATE INDEX IF NOT EXISTS idx_runs_active ON runs(status, project_id, linear_issue_id);
123
+ CREATE INDEX IF NOT EXISTS idx_runs_thread ON runs(thread_id);
124
+ CREATE INDEX IF NOT EXISTS idx_run_thread_events_run ON run_thread_events(run_id, id);
211
125
  CREATE INDEX IF NOT EXISTS idx_operator_feed_events_issue ON operator_feed_events(issue_key, id);
212
126
  CREATE INDEX IF NOT EXISTS idx_operator_feed_events_project ON operator_feed_events(project_id, id);
213
- CREATE INDEX IF NOT EXISTS idx_obligations_pending ON obligations(status, run_lease_id, kind);
214
- CREATE UNIQUE INDEX IF NOT EXISTS idx_obligations_dedupe
215
- ON obligations(run_lease_id, kind, dedupe_key)
216
- WHERE dedupe_key IS NOT NULL;
217
127
  `;
218
128
  export function runPatchRelayMigrations(connection) {
219
- connection.exec(baseMigration);
220
- try {
221
- connection.exec("ALTER TABLE issue_control ADD COLUMN selected_workflow_id TEXT");
222
- }
223
- catch {
224
- // Column already exists on upgraded installs.
225
- }
226
- try {
227
- connection.exec("ALTER TABLE operator_feed_events ADD COLUMN workflow_id TEXT");
228
- }
229
- catch {
230
- // Column already exists on upgraded installs.
231
- }
232
- try {
233
- connection.exec("ALTER TABLE operator_feed_events ADD COLUMN next_stage TEXT");
234
- }
235
- catch {
236
- // Column already exists on upgraded installs.
237
- }
129
+ connection.exec(schema);
238
130
  }