patchrelay 0.2.0 → 0.4.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/args.js +73 -0
- package/dist/cli/command-types.js +1 -0
- package/dist/cli/commands/connect.js +28 -0
- package/dist/cli/commands/issues.js +155 -0
- package/dist/cli/commands/project.js +140 -0
- package/dist/cli/commands/setup.js +140 -0
- package/dist/cli/connect-flow.js +52 -0
- package/dist/cli/data.js +124 -67
- package/dist/cli/index.js +59 -615
- package/dist/cli/interactive.js +48 -0
- package/dist/cli/output.js +13 -0
- package/dist/cli/service-commands.js +31 -0
- package/dist/db/authoritative-ledger-store.js +95 -0
- package/dist/db/issue-projection-store.js +54 -0
- package/dist/db/issue-workflow-coordinator.js +309 -0
- package/dist/db/issue-workflow-store.js +53 -550
- package/dist/db/migrations.js +19 -0
- package/dist/db/run-report-store.js +33 -0
- package/dist/db.js +22 -1
- package/dist/index.js +13 -4
- package/dist/install.js +4 -3
- package/dist/linear-oauth.js +8 -7
- package/dist/service-stage-finalizer.js +2 -2
- package/dist/service-stage-runner.js +4 -4
- package/dist/service.js +1 -0
- package/dist/stage-failure.js +3 -17
- package/dist/stage-lifecycle-publisher.js +5 -28
- package/dist/webhook-desired-stage-recorder.js +4 -35
- package/infra/patchrelay.path +2 -0
- package/infra/patchrelay.service +2 -0
- package/package.json +1 -1
package/dist/cli/data.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import pino from "pino";
|
|
2
3
|
import { CodexAppServerClient } from "../codex-app-server.js";
|
|
3
4
|
import { PatchRelayDatabase } from "../db.js";
|
|
5
|
+
import { WorktreeManager } from "../worktree-manager.js";
|
|
4
6
|
import { resolveWorkflowStage } from "../workflow-policy.js";
|
|
5
7
|
function safeJsonParse(value) {
|
|
6
8
|
if (!value) {
|
|
@@ -167,16 +169,43 @@ export class CliDataAccess {
|
|
|
167
169
|
if (!worktree) {
|
|
168
170
|
return undefined;
|
|
169
171
|
}
|
|
170
|
-
const
|
|
171
|
-
const resumeThreadId = (ledger.issueControl?.activeRunLeaseId ? ledger.runLease?.threadId : undefined) ??
|
|
172
|
-
worktree.workspace.lastThreadId ??
|
|
173
|
-
worktree.issue.latestThreadId ??
|
|
174
|
-
ledger.runLease?.threadId;
|
|
172
|
+
const resumeThreadId = this.getStoredOpenThreadId(worktree);
|
|
175
173
|
return {
|
|
176
174
|
...worktree,
|
|
177
175
|
...(resumeThreadId ? { resumeThreadId } : {}),
|
|
178
176
|
};
|
|
179
177
|
}
|
|
178
|
+
async prepareOpen(issueKey) {
|
|
179
|
+
const worktree = this.worktree(issueKey);
|
|
180
|
+
if (!worktree) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
await this.ensureOpenWorktree(worktree);
|
|
184
|
+
const existingThreadId = await this.resolveStoredOpenThreadId(worktree);
|
|
185
|
+
if (existingThreadId) {
|
|
186
|
+
return {
|
|
187
|
+
...worktree,
|
|
188
|
+
resumeThreadId: existingThreadId,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const codex = await this.getCodex();
|
|
192
|
+
const thread = await codex.startThread({
|
|
193
|
+
cwd: worktree.workspace.worktreePath,
|
|
194
|
+
});
|
|
195
|
+
this.db.issueSessions.upsertIssueSession({
|
|
196
|
+
projectId: worktree.issue.projectId,
|
|
197
|
+
linearIssueId: worktree.issue.linearIssueId,
|
|
198
|
+
workspaceOwnershipId: worktree.workspace.id,
|
|
199
|
+
threadId: thread.id,
|
|
200
|
+
source: "operator_open",
|
|
201
|
+
...(worktree.issue.activeAgentSessionId ? { linkedAgentSessionId: worktree.issue.activeAgentSessionId } : {}),
|
|
202
|
+
});
|
|
203
|
+
this.db.issueSessions.touchIssueSession(thread.id);
|
|
204
|
+
return {
|
|
205
|
+
...worktree,
|
|
206
|
+
resumeThreadId: thread.id,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
180
209
|
retry(issueKey, options) {
|
|
181
210
|
const issue = this.db.issueWorkflows.getTrackedIssueByKey(issueKey);
|
|
182
211
|
if (!issue) {
|
|
@@ -200,14 +229,10 @@ export class CliDataAccess {
|
|
|
200
229
|
projectId: issue.projectId,
|
|
201
230
|
linearIssueId: issue.linearIssueId,
|
|
202
231
|
});
|
|
203
|
-
this.db.
|
|
204
|
-
projectId: issue.projectId,
|
|
205
|
-
linearIssueId: issue.linearIssueId,
|
|
206
|
-
desiredStage: stage,
|
|
232
|
+
this.db.workflowCoordinator.setIssueDesiredStage(issue.projectId, issue.linearIssueId, stage, {
|
|
207
233
|
desiredReceiptId: receipt.id,
|
|
208
234
|
lifecycleStatus: "queued",
|
|
209
235
|
});
|
|
210
|
-
this.db.issueWorkflows.setIssueDesiredStage(issue.projectId, issue.linearIssueId, stage, webhookId);
|
|
211
236
|
const updated = this.db.issueWorkflows.getTrackedIssue(issue.projectId, issue.linearIssueId);
|
|
212
237
|
return {
|
|
213
238
|
issue: updated,
|
|
@@ -278,13 +303,19 @@ export class CliDataAccess {
|
|
|
278
303
|
: {}),
|
|
279
304
|
...(row.latest_stage !== null
|
|
280
305
|
? { latestStage: row.latest_stage }
|
|
281
|
-
: ledger?.
|
|
282
|
-
? { latestStage: ledger.
|
|
306
|
+
: ledger?.runLease
|
|
307
|
+
? { latestStage: ledger.runLease.stage }
|
|
283
308
|
: {}),
|
|
284
309
|
...(row.latest_stage_status !== null
|
|
285
310
|
? { latestStageStatus: String(row.latest_stage_status) }
|
|
286
|
-
: ledger?.
|
|
287
|
-
? {
|
|
311
|
+
: ledger?.runLease
|
|
312
|
+
? {
|
|
313
|
+
latestStageStatus: ledger.runLease.status === "failed"
|
|
314
|
+
? "failed"
|
|
315
|
+
: ledger.runLease.status === "completed" || ledger.runLease.status === "released" || ledger.runLease.status === "paused"
|
|
316
|
+
? "completed"
|
|
317
|
+
: "running",
|
|
318
|
+
}
|
|
288
319
|
: {}),
|
|
289
320
|
updatedAt: String(row.updated_at),
|
|
290
321
|
};
|
|
@@ -302,20 +333,16 @@ export class CliDataAccess {
|
|
|
302
333
|
getLedgerIssueContext(projectId, linearIssueId) {
|
|
303
334
|
const issueControl = this.db.issueControl.getIssueControl(projectId, linearIssueId);
|
|
304
335
|
const runLease = issueControl?.activeRunLeaseId ? this.db.runLeases.getRunLease(issueControl.activeRunLeaseId) : undefined;
|
|
305
|
-
const workspaceOwnership = issueControl?.activeWorkspaceOwnershipId
|
|
306
|
-
? this.db.workspaceOwnership.getWorkspaceOwnership(issueControl.activeWorkspaceOwnershipId)
|
|
307
|
-
: undefined;
|
|
308
|
-
const mirroredStageRun = issueControl?.activeRunLeaseId ? this.db.issueWorkflows.getStageRun(issueControl.activeRunLeaseId) : undefined;
|
|
309
336
|
return {
|
|
310
337
|
...(issueControl ? { issueControl } : {}),
|
|
311
338
|
...(runLease ? { runLease } : {}),
|
|
312
|
-
...(workspaceOwnership ? { workspaceOwnership } : {}),
|
|
313
|
-
...(mirroredStageRun ? { mirroredStageRun } : {}),
|
|
314
339
|
};
|
|
315
340
|
}
|
|
316
341
|
getActiveStageRunForIssue(issue, ledger) {
|
|
317
342
|
const context = ledger ?? this.getLedgerIssueContext(issue.projectId, issue.linearIssueId);
|
|
318
|
-
const activeStageRun = context.
|
|
343
|
+
const activeStageRun = context.issueControl?.activeRunLeaseId
|
|
344
|
+
? this.db.issueWorkflows.getStageRun(context.issueControl.activeRunLeaseId)
|
|
345
|
+
: undefined;
|
|
319
346
|
if (!activeStageRun) {
|
|
320
347
|
return undefined;
|
|
321
348
|
}
|
|
@@ -323,59 +350,89 @@ export class CliDataAccess {
|
|
|
323
350
|
? activeStageRun
|
|
324
351
|
: undefined;
|
|
325
352
|
}
|
|
326
|
-
synthesizeStageRunFromLease(ledger) {
|
|
327
|
-
if (!ledger.runLease) {
|
|
328
|
-
return undefined;
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
id: -ledger.runLease.id,
|
|
332
|
-
pipelineRunId: 0,
|
|
333
|
-
projectId: ledger.runLease.projectId,
|
|
334
|
-
linearIssueId: ledger.runLease.linearIssueId,
|
|
335
|
-
workspaceId: 0,
|
|
336
|
-
stage: ledger.runLease.stage,
|
|
337
|
-
status: ledger.runLease.status === "failed"
|
|
338
|
-
? "failed"
|
|
339
|
-
: ledger.runLease.status === "completed" || ledger.runLease.status === "released" || ledger.runLease.status === "paused"
|
|
340
|
-
? "completed"
|
|
341
|
-
: "running",
|
|
342
|
-
triggerWebhookId: "ledger-active-run",
|
|
343
|
-
workflowFile: ledger.runLease.workflowFile,
|
|
344
|
-
promptText: ledger.runLease.promptText,
|
|
345
|
-
...(ledger.runLease.threadId ? { threadId: ledger.runLease.threadId } : {}),
|
|
346
|
-
...(ledger.runLease.parentThreadId ? { parentThreadId: ledger.runLease.parentThreadId } : {}),
|
|
347
|
-
...(ledger.runLease.turnId ? { turnId: ledger.runLease.turnId } : {}),
|
|
348
|
-
startedAt: ledger.runLease.startedAt,
|
|
349
|
-
...(ledger.runLease.endedAt ? { endedAt: ledger.runLease.endedAt } : {}),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
353
|
getWorkspaceForIssue(issue, ledger) {
|
|
353
354
|
const context = ledger ?? this.getLedgerIssueContext(issue.projectId, issue.linearIssueId);
|
|
354
|
-
if (
|
|
355
|
-
const activeWorkspace = this.db.issueWorkflows.
|
|
355
|
+
if (context.issueControl?.activeWorkspaceOwnershipId !== undefined) {
|
|
356
|
+
const activeWorkspace = this.db.issueWorkflows.getWorkspace(context.issueControl.activeWorkspaceOwnershipId);
|
|
356
357
|
if (activeWorkspace) {
|
|
357
358
|
return activeWorkspace;
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
return this.db.issueWorkflows.getActiveWorkspaceForIssue(issue.projectId, issue.linearIssueId);
|
|
362
|
+
}
|
|
363
|
+
getStoredOpenThreadId(worktree) {
|
|
364
|
+
return this.listOpenCandidateThreadIds(worktree).at(0);
|
|
365
|
+
}
|
|
366
|
+
async resolveStoredOpenThreadId(worktree) {
|
|
367
|
+
for (const threadId of this.listOpenCandidateThreadIds(worktree)) {
|
|
368
|
+
if (!(await this.canReadThread(threadId))) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
this.recordOpenThreadForIssue(worktree, threadId);
|
|
372
|
+
return threadId;
|
|
363
373
|
}
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
listOpenCandidateThreadIds(worktree) {
|
|
377
|
+
const ledger = this.getLedgerIssueContext(worktree.issue.projectId, worktree.issue.linearIssueId);
|
|
378
|
+
const sessions = this.db.issueSessions.listIssueSessionsForIssue(worktree.issue.projectId, worktree.issue.linearIssueId);
|
|
379
|
+
const candidates = [
|
|
380
|
+
ledger.issueControl?.activeRunLeaseId ? ledger.runLease?.threadId : undefined,
|
|
381
|
+
...sessions.map((session) => session.threadId),
|
|
382
|
+
worktree.workspace.lastThreadId,
|
|
383
|
+
worktree.issue.latestThreadId,
|
|
384
|
+
ledger.runLease?.threadId,
|
|
385
|
+
];
|
|
386
|
+
const seen = new Set();
|
|
387
|
+
const ordered = [];
|
|
388
|
+
for (const candidate of candidates) {
|
|
389
|
+
if (!candidate || seen.has(candidate)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
seen.add(candidate);
|
|
393
|
+
ordered.push(candidate);
|
|
394
|
+
}
|
|
395
|
+
return ordered;
|
|
396
|
+
}
|
|
397
|
+
recordOpenThreadForIssue(worktree, threadId) {
|
|
398
|
+
const existing = this.db.issueSessions.getIssueSessionByThreadId(threadId);
|
|
399
|
+
if (existing) {
|
|
400
|
+
this.db.issueSessions.touchIssueSession(threadId);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const runLease = this.db.runLeases.getRunLeaseByThreadId(threadId);
|
|
404
|
+
this.db.issueSessions.upsertIssueSession({
|
|
405
|
+
projectId: worktree.issue.projectId,
|
|
406
|
+
linearIssueId: worktree.issue.linearIssueId,
|
|
407
|
+
workspaceOwnershipId: runLease?.workspaceOwnershipId ?? worktree.workspace.id,
|
|
408
|
+
threadId,
|
|
409
|
+
source: runLease ? "stage_run" : "operator_open",
|
|
410
|
+
...(runLease?.id !== undefined ? { runLeaseId: runLease.id } : {}),
|
|
411
|
+
...(runLease?.parentThreadId ? { parentThreadId: runLease.parentThreadId } : {}),
|
|
412
|
+
...(worktree.issue.activeAgentSessionId ? { linkedAgentSessionId: worktree.issue.activeAgentSessionId } : {}),
|
|
413
|
+
});
|
|
414
|
+
this.db.issueSessions.touchIssueSession(threadId);
|
|
415
|
+
}
|
|
416
|
+
async canReadThread(threadId) {
|
|
417
|
+
try {
|
|
418
|
+
const codex = await this.getCodex();
|
|
419
|
+
await codex.readThread(threadId, false);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async ensureOpenWorktree(worktree) {
|
|
427
|
+
if (existsSync(worktree.workspace.worktreePath)) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const project = this.config.projects.find((entry) => entry.id === worktree.repoId);
|
|
431
|
+
if (!project) {
|
|
432
|
+
throw new Error(`Project not found for ${worktree.repoId}`);
|
|
433
|
+
}
|
|
434
|
+
const worktreeManager = new WorktreeManager(this.config);
|
|
435
|
+
await worktreeManager.ensureIssueWorktree(project.repoPath, project.worktreeRoot, worktree.workspace.worktreePath, worktree.workspace.branchName);
|
|
379
436
|
}
|
|
380
437
|
async connect(projectId) {
|
|
381
438
|
return await this.requestJson("/api/oauth/linear/start", {
|