patchrelay 0.12.8 → 0.13.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.
@@ -1,5 +1,6 @@
1
- import { buildPreparingSessionPlan, buildRunningSessionPlan, } from "./agent-session-plan.js";
1
+ import { buildAgentSessionPlanForIssue, } from "./agent-session-plan.js";
2
2
  import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
3
+ import { buildAlreadyRunningThought, buildDelegationThought, buildPromptDeliveredThought, } from "./linear-session-reporting.js";
3
4
  import { resolveProject, triggerEventAllowed, trustedActorAllowed } from "./project-resolution.js";
4
5
  import { normalizeWebhook } from "./webhooks.js";
5
6
  import { InstallationWebhookHandler } from "./webhook-installation-handler.js";
@@ -138,10 +139,10 @@ export class WebhookHandler {
138
139
  const activeRun = existingIssue?.activeRunId ? this.db.getRun(existingIssue.activeRunId) : undefined;
139
140
  const delegated = this.isDelegatedToPatchRelay(project, normalized);
140
141
  const triggerAllowed = triggerEventAllowed(project, normalized.triggerEvent);
141
- // In the factory model, delegation → queue an implementation run.
142
- // agentSessionCreated is itself a delegation signal (session exists because issue was delegated).
142
+ const pendingRunContextJson = mergePendingImplementationContext(existingIssue?.pendingRunContextJson, normalized);
143
+ // In the factory model, only a true delegation queues implementation work.
143
144
  let pendingRunType;
144
- const isDelegationSignal = delegated || normalized.triggerEvent === "agentSessionCreated";
145
+ const isDelegationSignal = delegated;
145
146
  if (isDelegationSignal && triggerAllowed && !activeRun && !existingIssue?.pendingRunType) {
146
147
  pendingRunType = "implementation";
147
148
  }
@@ -157,6 +158,9 @@ export class WebhookHandler {
157
158
  ...(normalizedIssue.url ? { url: normalizedIssue.url } : {}),
158
159
  ...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
159
160
  ...(pendingRunType ? { pendingRunType, factoryState: "delegated" } : {}),
161
+ ...((pendingRunType || existingIssue?.pendingRunType === "implementation") && pendingRunContextJson
162
+ ? { pendingRunContextJson }
163
+ : {}),
160
164
  ...(agentSessionId !== undefined ? { agentSessionId } : {}),
161
165
  });
162
166
  return {
@@ -189,19 +193,15 @@ export class WebhookHandler {
189
193
  return;
190
194
  }
191
195
  if (desiredStage) {
192
- await this.updateAgentSessionPlan(linear, project, normalized.agentSession.id, trackedIssue, buildPreparingSessionPlan(desiredStage));
193
- await this.publishAgentActivity(linear, normalized.agentSession.id, {
194
- type: "response",
195
- body: `PatchRelay started working on the ${desiredStage} workflow.`,
196
- });
196
+ const latestIssue = this.db.getIssue(project.id, normalized.issue.id);
197
+ await this.syncAgentSession(linear, normalized.agentSession.id, latestIssue ?? trackedIssue, { pendingRunType: desiredStage });
198
+ await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(desiredStage));
197
199
  return;
198
200
  }
199
201
  if (activeRun) {
200
- await this.updateAgentSessionPlan(linear, project, normalized.agentSession.id, trackedIssue, buildRunningSessionPlan(activeRun.runType));
201
- await this.publishAgentActivity(linear, normalized.agentSession.id, {
202
- type: "response",
203
- body: `PatchRelay is already running the ${activeRun.runType} workflow for this issue.`,
204
- });
202
+ const latestIssue = this.db.getIssue(project.id, normalized.issue.id);
203
+ await this.syncAgentSession(linear, normalized.agentSession.id, latestIssue ?? trackedIssue, { activeRunType: activeRun.runType });
204
+ await this.publishAgentActivity(linear, normalized.agentSession.id, buildAlreadyRunningThought(activeRun.runType));
205
205
  return;
206
206
  }
207
207
  await this.publishAgentActivity(linear, normalized.agentSession.id, {
@@ -242,18 +242,13 @@ export class WebhookHandler {
242
242
  summary: `Could not deliver follow-up prompt to active ${activeRun.runType} workflow`,
243
243
  });
244
244
  }
245
- await this.publishAgentActivity(linear, normalized.agentSession.id, {
246
- type: "thought",
247
- body: `PatchRelay routed your follow-up instructions into the active ${activeRun.runType} workflow.`,
248
- });
245
+ await this.publishAgentActivity(linear, normalized.agentSession.id, buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
249
246
  return;
250
247
  }
251
248
  if (desiredStage) {
252
- await this.updateAgentSessionPlan(linear, project, normalized.agentSession.id, trackedIssue, buildPreparingSessionPlan(desiredStage));
253
- await this.publishAgentActivity(linear, normalized.agentSession.id, {
254
- type: "response",
255
- body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
256
- });
249
+ const latestIssue = this.db.getIssue(project.id, normalized.issue.id);
250
+ await this.syncAgentSession(linear, normalized.agentSession.id, latestIssue ?? trackedIssue, { pendingRunType: desiredStage });
251
+ await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(desiredStage, "prompt"), { ephemeral: true });
257
252
  }
258
253
  }
259
254
  // ─── Comment handling (inlined) ───────────────────────────────────
@@ -303,27 +298,40 @@ export class WebhookHandler {
303
298
  }
304
299
  }
305
300
  // ─── Helpers ──────────────────────────────────────────────────────
306
- async publishAgentActivity(linear, agentSessionId, content) {
301
+ async publishAgentActivity(linear, agentSessionId, content, options) {
307
302
  try {
308
303
  await linear.createAgentActivity({
309
304
  agentSessionId,
310
305
  content,
311
- ephemeral: content.type === "thought",
306
+ ephemeral: options?.ephemeral ?? content.type === "thought",
312
307
  });
313
308
  }
314
309
  catch (error) {
315
310
  this.logger.warn({ agentSessionId, error: error instanceof Error ? error.message : String(error) }, "Failed to publish Linear agent activity");
316
311
  }
317
312
  }
318
- async updateAgentSessionPlan(linear, project, agentSessionId, issue, plan) {
313
+ async syncAgentSession(linear, agentSessionId, issue, options) {
319
314
  if (!linear.updateAgentSession)
320
315
  return;
321
316
  try {
322
- const externalUrls = buildAgentSessionExternalUrls(this.config, issue?.issueKey);
317
+ const prUrl = issue && "prUrl" in issue ? issue.prUrl : undefined;
318
+ const externalUrls = buildAgentSessionExternalUrls(this.config, {
319
+ ...(issue?.issueKey ? { issueKey: issue.issueKey } : {}),
320
+ ...(prUrl ? { prUrl } : {}),
321
+ });
323
322
  await linear.updateAgentSession({
324
323
  agentSessionId,
325
324
  ...(externalUrls ? { externalUrls } : {}),
326
- plan,
325
+ ...(issue
326
+ ? {
327
+ plan: buildAgentSessionPlanForIssue({
328
+ factoryState: issue.factoryState,
329
+ pendingRunType: options?.pendingRunType ?? ("pendingRunType" in issue ? issue.pendingRunType : undefined),
330
+ ciRepairAttempts: "ciRepairAttempts" in issue ? issue.ciRepairAttempts : 0,
331
+ queueRepairAttempts: "queueRepairAttempts" in issue ? issue.queueRepairAttempts : 0,
332
+ }, options?.activeRunType ? { activeRunType: options.activeRunType } : undefined),
333
+ }
334
+ : {}),
327
335
  });
328
336
  }
329
337
  catch (error) {
@@ -372,6 +380,19 @@ export class WebhookHandler {
372
380
  function hasCompleteIssueContext(issue) {
373
381
  return Boolean(issue.stateName && issue.delegateId && issue.teamId && issue.teamKey);
374
382
  }
383
+ function mergePendingImplementationContext(existingJson, normalized) {
384
+ const existing = existingJson ? safeJsonParse(existingJson) ?? {} : {};
385
+ const next = { ...existing };
386
+ const promptContext = normalized.agentSession?.promptContext?.trim();
387
+ const promptBody = normalized.agentSession?.promptBody?.trim();
388
+ if (promptContext) {
389
+ next.promptContext = promptContext;
390
+ }
391
+ if (promptBody) {
392
+ next.promptBody = promptBody;
393
+ }
394
+ return Object.keys(next).length > 0 ? JSON.stringify(next) : undefined;
395
+ }
375
396
  function mergeIssueMetadata(issue, liveIssue) {
376
397
  return {
377
398
  ...issue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.12.8",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -43,13 +43,16 @@
43
43
  "dependencies": {
44
44
  "fastify": "^5.8.2",
45
45
  "fastify-raw-body": "^5.0.0",
46
+ "ink": "^6.8.0",
46
47
  "pino": "^10.3.1",
47
48
  "pino-logfmt": "^1.1.3",
49
+ "react": "^19.2.4",
48
50
  "zod": "^4.3.6"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@eslint/js": "^10.0.1",
52
54
  "@types/node": "^24.12.0",
55
+ "@types/react": "^19.2.14",
53
56
  "eslint": "^10.0.3",
54
57
  "typescript": "^5.9.3",
55
58
  "typescript-eslint": "^8.57.0"