patchrelay 0.38.1 → 0.39.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 (40) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli/args.js +4 -0
  3. package/dist/cli/commands/issues.js +20 -1
  4. package/dist/cli/data.js +54 -7
  5. package/dist/cli/formatters/text.js +10 -0
  6. package/dist/cli/help.js +4 -0
  7. package/dist/cli/index.js +3 -0
  8. package/dist/config.js +26 -0
  9. package/dist/db/issue-store.js +10 -2
  10. package/dist/db/migrations.js +5 -0
  11. package/dist/factory-state.js +1 -0
  12. package/dist/github-webhook-handler.js +12 -0
  13. package/dist/github-webhook-late-publication-guard.js +94 -0
  14. package/dist/github-webhook-state-projector.js +15 -1
  15. package/dist/github-webhooks.js +39 -4
  16. package/dist/github-worktree-auth.js +18 -0
  17. package/dist/http.js +17 -0
  18. package/dist/idle-reconciliation.js +4 -2
  19. package/dist/issue-session-events.js +1 -0
  20. package/dist/linear-activity-key.js +11 -0
  21. package/dist/linear-agent-session-client.js +14 -1
  22. package/dist/linear-progress-facts.js +170 -0
  23. package/dist/linear-progress-reporter.js +21 -168
  24. package/dist/linear-status-comment-sync.js +3 -19
  25. package/dist/linear-workflow-state-sync.js +37 -18
  26. package/dist/manual-issue-actions.js +37 -0
  27. package/dist/merged-linear-completion-reconciler.js +102 -22
  28. package/dist/no-pr-completion-check.js +52 -0
  29. package/dist/presentation-text.js +11 -1
  30. package/dist/prompting/patchrelay.js +8 -6
  31. package/dist/run-budgets.js +12 -0
  32. package/dist/run-launcher.js +6 -6
  33. package/dist/run-notification-handler.js +4 -0
  34. package/dist/run-orchestrator.js +7 -1
  35. package/dist/run-wake-planner.js +11 -10
  36. package/dist/service-issue-actions.js +80 -27
  37. package/dist/service.js +3 -0
  38. package/dist/trusted-no-pr-completion.js +7 -0
  39. package/dist/webhooks/desired-stage-recorder.js +34 -10
  40. package/package.json +1 -1
@@ -19,16 +19,22 @@ export class DesiredStageRecorder {
19
19
  const existingIssue = this.db.issues.getIssue(params.project.id, normalizedIssue.id);
20
20
  const activeRun = existingIssue?.activeRunId ? this.db.runs.getRunById(existingIssue.activeRunId) : undefined;
21
21
  const latestRun = existingIssue ? this.db.runs.getLatestRunForIssue(params.project.id, normalizedIssue.id) : undefined;
22
- const delegated = this.isDelegatedToPatchRelay(params.project, params.normalized);
23
22
  const triggerAllowed = triggerEventAllowed(params.project, params.normalized.triggerEvent);
24
23
  const incomingAgentSessionId = params.normalized.agentSession?.id;
25
24
  const hasPendingWake = this.db.issueSessions.peekIssueSessionWake(params.project.id, normalizedIssue.id) !== undefined;
26
- if (!existingIssue && !delegated && !incomingAgentSessionId) {
27
- return { issue: undefined, wakeRunType: undefined, delegated };
25
+ if (!existingIssue && !this.isDelegatedToPatchRelay(params.project, normalizedIssue) && !incomingAgentSessionId) {
26
+ return { issue: undefined, wakeRunType: undefined, delegated: false };
28
27
  }
29
28
  const hydratedIssue = await this.syncIssueDependencies(params.project.id, normalizedIssue);
29
+ const delegated = this.isDelegatedToPatchRelay(params.project, hydratedIssue);
30
30
  const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(params.project.id, normalizedIssue.id);
31
31
  const terminal = isTerminalDelegationState(existingIssue, hydratedIssue);
32
+ const openPrExists = existingIssue?.prNumber !== undefined
33
+ && existingIssue.prState !== "closed"
34
+ && existingIssue.prState !== "merged";
35
+ const blockerPausedImplementation = unresolvedBlockers > 0
36
+ && activeRun?.runType === "implementation"
37
+ && !openPrExists;
32
38
  const desiredStage = decideRunIntent({
33
39
  delegated,
34
40
  triggerAllowed,
@@ -45,6 +51,9 @@ export class DesiredStageRecorder {
45
51
  triggerEvent: params.normalized.triggerEvent,
46
52
  delegated,
47
53
  });
54
+ const effectiveRunRelease = blockerPausedImplementation
55
+ ? { release: true, reason: "Issue became blocked during implementation" }
56
+ : runRelease;
48
57
  const undelegation = decideUnDelegation({
49
58
  triggerEvent: params.normalized.triggerEvent,
50
59
  delegated,
@@ -96,11 +105,12 @@ export class DesiredStageRecorder {
96
105
  ...(!reDelegationResume.factoryState && desiredStage ? { pendingRunType: null, pendingRunContextJson: null, factoryState: "delegated" } : {}),
97
106
  ...(clearPending ? { pendingRunType: null, pendingRunContextJson: null } : {}),
98
107
  ...(agentSessionId !== undefined ? { agentSessionId } : {}),
99
- ...(runRelease.release ? { activeRunId: null } : {}),
108
+ ...(effectiveRunRelease.release ? { activeRunId: null } : {}),
109
+ ...(blockerPausedImplementation ? { factoryState: "delegated" } : {}),
100
110
  ...(undelegation.factoryState ? { factoryState: undelegation.factoryState } : {}),
101
111
  });
102
- if (runRelease.release && activeRun && runRelease.reason) {
103
- this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: runRelease.reason });
112
+ if (effectiveRunRelease.release && activeRun && effectiveRunRelease.reason) {
113
+ this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: effectiveRunRelease.reason });
104
114
  }
105
115
  return record;
106
116
  };
@@ -136,6 +146,22 @@ export class DesiredStageRecorder {
136
146
  : `Issue un-delegated from PatchRelay; ${issue.factoryState} is now paused`,
137
147
  });
138
148
  }
149
+ else if (blockerPausedImplementation) {
150
+ if (activeRun?.threadId && activeRun.turnId) {
151
+ await params.stopActiveRun(activeRun, "STOP: The issue is now blocked by another task. Stop working immediately and exit without publishing.");
152
+ }
153
+ this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(params.project.id, normalizedIssue.id);
154
+ this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(params.project.id, normalizedIssue.id);
155
+ this.feed?.publish({
156
+ level: "warn",
157
+ kind: "stage",
158
+ issueKey: issue.issueKey,
159
+ projectId: params.project.id,
160
+ stage: issue.factoryState,
161
+ status: "blocked",
162
+ summary: `Implementation paused because ${issue.issueKey ?? normalizedIssue.id} is now blocked`,
163
+ });
164
+ }
139
165
  else if (reDelegationResume.pendingRunType) {
140
166
  this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.project.id, normalizedIssue.id, {
141
167
  projectId: params.project.id,
@@ -168,13 +194,11 @@ export class DesiredStageRecorder {
168
194
  delegated,
169
195
  };
170
196
  }
171
- isDelegatedToPatchRelay(project, normalized) {
172
- if (!normalized.issue)
173
- return false;
197
+ isDelegatedToPatchRelay(project, issue) {
174
198
  const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
175
199
  if (!installation?.actorId)
176
200
  return false;
177
- return normalized.issue.delegateId === installation.actorId;
201
+ return issue.delegateId === installation.actorId;
178
202
  }
179
203
  async syncIssueDependencies(projectId, issue) {
180
204
  let source = issue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.38.1",
3
+ "version": "0.39.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {