patchrelay 0.47.2 → 0.49.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/README.md +71 -362
- package/dist/agent-session-plan.js +42 -0
- package/dist/build-info.json +3 -3
- package/dist/db/issue-store.js +101 -4
- package/dist/db/migrations.js +44 -0
- package/dist/db.js +12 -0
- package/dist/github-webhook-terminal-handler.js +7 -0
- package/dist/idle-reconciliation.js +19 -0
- package/dist/issue-class.js +35 -0
- package/dist/issue-overview-query.js +2 -0
- package/dist/issue-session-events.js +12 -2
- package/dist/issue-session.js +6 -0
- package/dist/linear-client.js +13 -0
- package/dist/linear-status-comment-sync.js +1 -0
- package/dist/no-pr-completion-check.js +18 -3
- package/dist/orchestration-parent-wake.js +96 -0
- package/dist/prompting/patchrelay.js +99 -22
- package/dist/run-orchestrator.js +22 -9
- package/dist/tracked-issue-list-query.js +3 -0
- package/dist/tracked-issue-projector.js +3 -0
- package/dist/waiting-reason.js +7 -0
- package/dist/webhooks/comment-wake-handler.js +6 -1
- package/dist/webhooks/decision-helpers.js +3 -0
- package/dist/webhooks/desired-stage-recorder.js +86 -1
- package/dist/webhooks.js +7 -0
- package/package.json +1 -1
|
@@ -106,9 +106,11 @@ function buildCoordinationGuidance(context) {
|
|
|
106
106
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
107
107
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
108
108
|
: [];
|
|
109
|
-
const
|
|
110
|
-
? context.
|
|
111
|
-
:
|
|
109
|
+
const childIssues = Array.isArray(context?.childIssues)
|
|
110
|
+
? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
111
|
+
: Array.isArray(context?.trackedDependents)
|
|
112
|
+
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
113
|
+
: [];
|
|
112
114
|
const lines = [
|
|
113
115
|
"### Coordination / Issue Topology",
|
|
114
116
|
"",
|
|
@@ -117,7 +119,7 @@ function buildCoordinationGuidance(context) {
|
|
|
117
119
|
"When child issues already own the concrete code slices, use this issue to coordinate, create or refine follow-up issues, or verify convergence. Only ship code here if this issue still has unique implementation scope that is not already owned elsewhere.",
|
|
118
120
|
"Prefer one PR per concrete implementation issue over a broad parent branch that restates overlapping child work.",
|
|
119
121
|
];
|
|
120
|
-
if (unresolvedBlockers.length === 0 &&
|
|
122
|
+
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
121
123
|
return lines;
|
|
122
124
|
}
|
|
123
125
|
lines.push("", "Known relations from PatchRelay:");
|
|
@@ -125,12 +127,12 @@ function buildCoordinationGuidance(context) {
|
|
|
125
127
|
lines.push("Unresolved blockers:");
|
|
126
128
|
lines.push(...summarizeRelationEntries(unresolvedBlockers));
|
|
127
129
|
}
|
|
128
|
-
if (
|
|
130
|
+
if (childIssues.length > 0) {
|
|
129
131
|
if (unresolvedBlockers.length > 0) {
|
|
130
132
|
lines.push("");
|
|
131
133
|
}
|
|
132
|
-
lines.push("
|
|
133
|
-
lines.push(...summarizeRelationEntries(
|
|
134
|
+
lines.push("Canonical child issues:");
|
|
135
|
+
lines.push(...summarizeRelationEntries(childIssues));
|
|
134
136
|
}
|
|
135
137
|
return lines;
|
|
136
138
|
}
|
|
@@ -162,6 +164,45 @@ function buildScopeDiscipline(issue, context) {
|
|
|
162
164
|
"- A review repair should fix the concrete concern on the current head, not silently expand the Linear issue into a broader rewrite.",
|
|
163
165
|
].join("\n");
|
|
164
166
|
}
|
|
167
|
+
function buildOrchestrationScopeDiscipline(context) {
|
|
168
|
+
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
169
|
+
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
170
|
+
: [];
|
|
171
|
+
const childIssues = Array.isArray(context?.childIssues)
|
|
172
|
+
? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
173
|
+
: Array.isArray(context?.trackedDependents)
|
|
174
|
+
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
175
|
+
: [];
|
|
176
|
+
return [
|
|
177
|
+
"## Scope Discipline",
|
|
178
|
+
"",
|
|
179
|
+
"This issue is orchestration work.",
|
|
180
|
+
"Treat it as the owner of convergence across related issues rather than as a normal code-owning implementation branch.",
|
|
181
|
+
"Inspect why this wake happened before acting.",
|
|
182
|
+
"Adopt already-existing canonical child issues when they cover the intended split.",
|
|
183
|
+
"Do not recreate child issues that already exist under this parent unless a genuinely missing required slice remains.",
|
|
184
|
+
"Do not create an overlapping umbrella PR unless this parent clearly owns unique direct cleanup work that child issues do not already cover.",
|
|
185
|
+
"If child work is still in motion, babysit the plan, record useful observations, and return to waiting.",
|
|
186
|
+
"If child work looks delivered, audit whether the original parent goal is actually satisfied.",
|
|
187
|
+
"Create blocking follow-up work only when it is necessary to satisfy the original parent goal.",
|
|
188
|
+
"Prefer non-blocking follow-up issues over keeping the umbrella open for optional polish or adjacent expansion.",
|
|
189
|
+
"",
|
|
190
|
+
"### Child Issue Summaries",
|
|
191
|
+
"",
|
|
192
|
+
...(childIssues.length > 0
|
|
193
|
+
? summarizeRelationEntries(childIssues, { emptyText: "No child issues are currently tracked." })
|
|
194
|
+
: ["No child issues are currently tracked."]),
|
|
195
|
+
"",
|
|
196
|
+
...(unresolvedBlockers.length > 0
|
|
197
|
+
? ["### Unresolved Blockers", "", ...summarizeRelationEntries(unresolvedBlockers), ""]
|
|
198
|
+
: []),
|
|
199
|
+
"### Convergence Rule",
|
|
200
|
+
"",
|
|
201
|
+
"- Close the umbrella when the original parent goal is satisfied.",
|
|
202
|
+
"- If you discover one missing required slice, you may create a justified blocking follow-up.",
|
|
203
|
+
"- Do not invent optional expansion without explicit human approval.",
|
|
204
|
+
].join("\n");
|
|
205
|
+
}
|
|
165
206
|
function buildHumanContext(context) {
|
|
166
207
|
const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
|
|
167
208
|
const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
|
|
@@ -357,18 +398,30 @@ function buildFollowUpPromptPrelude(issue, runType, context) {
|
|
|
357
398
|
"",
|
|
358
399
|
wakeReason === "direct_reply"
|
|
359
400
|
? "Why this turn exists: A human reply arrived for the outstanding question from the previous turn."
|
|
360
|
-
: wakeReason === "
|
|
361
|
-
? "Why this turn exists:
|
|
362
|
-
: wakeReason === "
|
|
363
|
-
? "Why this turn exists:
|
|
364
|
-
: wakeReason === "
|
|
365
|
-
? "Why this turn exists: A
|
|
366
|
-
:
|
|
401
|
+
: wakeReason === "initial_delegate"
|
|
402
|
+
? "Why this turn exists: This orchestration issue was just delegated and needs an initial plan."
|
|
403
|
+
: wakeReason === "child_delivered"
|
|
404
|
+
? "Why this turn exists: A child issue was delivered and the umbrella needs to review the outcome."
|
|
405
|
+
: wakeReason === "child_changed"
|
|
406
|
+
? "Why this turn exists: A child issue changed state and the umbrella may need to adjust."
|
|
407
|
+
: wakeReason === "child_regressed"
|
|
408
|
+
? "Why this turn exists: A previously progressing child issue regressed and the umbrella needs to reassess."
|
|
409
|
+
: wakeReason === "human_instruction"
|
|
410
|
+
? "Why this turn exists: A human added new guidance for this orchestration issue."
|
|
411
|
+
: wakeReason === "completion_check_continue"
|
|
412
|
+
? "Why this turn exists: The previous turn ended without a PR, and PatchRelay's completion check decided the work should continue automatically."
|
|
413
|
+
: wakeReason === "branch_upkeep"
|
|
414
|
+
? "Why this turn exists: GitHub still shows the PR branch as needing upkeep after the requested code change was addressed."
|
|
415
|
+
: wakeReason === "followup_comment"
|
|
416
|
+
? "Why this turn exists: A human follow-up comment arrived after the previous turn."
|
|
417
|
+
: `Why this turn exists: Continue the existing ${runType} run from the latest issue state.`,
|
|
367
418
|
wakeReason === "direct_reply"
|
|
368
419
|
? "Required action now: Apply the latest human answer, continue from the current branch/session context, and publish the next concrete result."
|
|
369
|
-
: wakeReason === "
|
|
370
|
-
? "Required action now:
|
|
371
|
-
:
|
|
420
|
+
: wakeReason === "initial_delegate"
|
|
421
|
+
? "Required action now: Inspect the umbrella goal, review the child set, and record the next orchestration step."
|
|
422
|
+
: wakeReason === "completion_check_continue"
|
|
423
|
+
? "Required action now: Continue from the current branch and thread context, finish the task, and publish the next concrete result."
|
|
424
|
+
: "Required action now: Continue from the latest branch state, refresh any stale assumptions, and publish the next concrete result.",
|
|
372
425
|
"",
|
|
373
426
|
];
|
|
374
427
|
if (wakeReason === "completion_check_continue" && typeof context?.completionCheckSummary === "string" && context.completionCheckSummary.trim()) {
|
|
@@ -417,14 +470,32 @@ function buildWorkflowGuidance(repoPath, runType) {
|
|
|
417
470
|
}
|
|
418
471
|
return "";
|
|
419
472
|
}
|
|
420
|
-
function
|
|
473
|
+
function buildOrchestrationWorkflowGuidance() {
|
|
474
|
+
return [
|
|
475
|
+
"## Workflow Guidance",
|
|
476
|
+
"",
|
|
477
|
+
"Use the wake reason and current child issue summaries to decide what kind of orchestration work is needed now.",
|
|
478
|
+
"Typical orchestration phases are: initial setup, waiting on child progress, reviewing delivered child work, final audit, creating a justified follow-up, or closing the umbrella.",
|
|
479
|
+
"Keep outputs concise and observable in Linear.",
|
|
480
|
+
].join("\n");
|
|
481
|
+
}
|
|
482
|
+
function buildPublicationContract(runType, issueClass) {
|
|
483
|
+
if (issueClass === "orchestration") {
|
|
484
|
+
return [
|
|
485
|
+
"## Publication Requirements",
|
|
486
|
+
"",
|
|
487
|
+
"Before finishing, publish the orchestration outcome rather than leaving it implicit.",
|
|
488
|
+
"By default, orchestration work should finish without opening an overlapping umbrella PR.",
|
|
489
|
+
"Valid orchestration outcomes include: recording an observation, updating the rollout plan, creating follow-up issues, opening a small cleanup PR that the parent clearly owns, or closing the umbrella.",
|
|
490
|
+
"If you create new blocking follow-up work, justify it against the original parent goal rather than optional polish.",
|
|
491
|
+
].join("\n");
|
|
492
|
+
}
|
|
421
493
|
if (runType === "implementation") {
|
|
422
494
|
return [
|
|
423
495
|
"## Publication Requirements",
|
|
424
496
|
"",
|
|
425
497
|
"Before finishing, publish the result instead of leaving it only in the worktree.",
|
|
426
498
|
"If the task is genuinely complete without a PR, say so clearly in your normal summary instead of inventing one.",
|
|
427
|
-
"If the issue is acting as coordination-only work and the real implementation belongs in child issues, finish without opening an overlapping umbrella PR.",
|
|
428
499
|
"If the worktree already contains relevant changes for this issue, verify them and publish them.",
|
|
429
500
|
"If you changed files for this issue, commit them, push the issue branch, and open or update the PR before stopping.",
|
|
430
501
|
"Do not stop with only local commits or uncommitted changes.",
|
|
@@ -456,6 +527,7 @@ function buildPublicationContract(runType) {
|
|
|
456
527
|
].join("\n");
|
|
457
528
|
}
|
|
458
529
|
function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
530
|
+
const issueClass = issue.issueClass;
|
|
459
531
|
const sections = [
|
|
460
532
|
{ id: "header", content: buildPromptHeader(issue) },
|
|
461
533
|
];
|
|
@@ -463,7 +535,10 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
|
463
535
|
if (followUp && reactiveContext) {
|
|
464
536
|
sections.push({ id: "follow-up-turn", content: reactiveContext });
|
|
465
537
|
}
|
|
466
|
-
sections.push({ id: "task-objective", content: buildTaskObjective(issue) }, {
|
|
538
|
+
sections.push({ id: "task-objective", content: buildTaskObjective(issue) }, {
|
|
539
|
+
id: "scope-discipline",
|
|
540
|
+
content: issueClass === "orchestration" ? buildOrchestrationScopeDiscipline(context) : buildScopeDiscipline(issue, context),
|
|
541
|
+
});
|
|
467
542
|
const humanContext = buildHumanContext(context);
|
|
468
543
|
if (humanContext) {
|
|
469
544
|
sections.push({ id: "human-context", content: humanContext });
|
|
@@ -471,11 +546,13 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
|
471
546
|
if (!followUp && reactiveContext) {
|
|
472
547
|
sections.push({ id: "reactive-context", content: reactiveContext });
|
|
473
548
|
}
|
|
474
|
-
const workflow =
|
|
549
|
+
const workflow = issueClass === "orchestration"
|
|
550
|
+
? buildOrchestrationWorkflowGuidance()
|
|
551
|
+
: buildWorkflowGuidance(repoPath, runType);
|
|
475
552
|
if (workflow) {
|
|
476
553
|
sections.push({ id: "workflow-guidance", content: workflow });
|
|
477
554
|
}
|
|
478
|
-
sections.push({ id: "publication-contract", content: buildPublicationContract(runType) });
|
|
555
|
+
sections.push({ id: "publication-contract", content: buildPublicationContract(runType, issueClass) });
|
|
479
556
|
return sections;
|
|
480
557
|
}
|
|
481
558
|
function filterAllowedReplacements(promptLayer) {
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -16,6 +16,7 @@ import { RunReconciler } from "./run-reconciler.js";
|
|
|
16
16
|
import { RunRecoveryService } from "./run-recovery-service.js";
|
|
17
17
|
import { RunWakePlanner } from "./run-wake-planner.js";
|
|
18
18
|
import { getRemainingZombieRecoveryDelayMs } from "./zombie-recovery.js";
|
|
19
|
+
import { classifyIssue } from "./issue-class.js";
|
|
19
20
|
import { loadConfig } from "./config.js";
|
|
20
21
|
function lowerCaseFirst(value) {
|
|
21
22
|
return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
|
|
@@ -138,7 +139,7 @@ export class RunOrchestrator {
|
|
|
138
139
|
materializeLegacyPendingWake(issue, lease) {
|
|
139
140
|
return this.runWakePlanner.materializeLegacyPendingWake(issue, lease);
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
+
buildRelatedIssueContext(issue) {
|
|
142
143
|
const unresolvedBlockers = this.db.issues
|
|
143
144
|
.listIssueDependencies(issue.projectId, issue.linearIssueId)
|
|
144
145
|
.filter((entry) => !isResolvedDependencyState(entry.blockerCurrentLinearStateType))
|
|
@@ -149,10 +150,8 @@ export class RunOrchestrator {
|
|
|
149
150
|
...(entry.blockerCurrentLinearState ? { stateName: entry.blockerCurrentLinearState } : {}),
|
|
150
151
|
...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
|
|
151
152
|
}));
|
|
152
|
-
const
|
|
153
|
-
.
|
|
154
|
-
.map((entry) => this.db.issues.getIssue(issue.projectId, entry.linearIssueId))
|
|
155
|
-
.filter((entry) => Boolean(entry))
|
|
153
|
+
const childIssues = this.db.issues
|
|
154
|
+
.listChildIssues(issue.projectId, issue.linearIssueId)
|
|
156
155
|
.map((entry) => ({
|
|
157
156
|
linearIssueId: entry.linearIssueId,
|
|
158
157
|
...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
|
|
@@ -162,14 +161,27 @@ export class RunOrchestrator {
|
|
|
162
161
|
delegatedToPatchRelay: entry.delegatedToPatchRelay,
|
|
163
162
|
hasOpenPr: entry.prNumber !== undefined && entry.prState !== "closed" && entry.prState !== "merged",
|
|
164
163
|
}));
|
|
165
|
-
if (unresolvedBlockers.length === 0 &&
|
|
164
|
+
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
166
165
|
return {};
|
|
167
166
|
}
|
|
168
167
|
return {
|
|
169
168
|
...(unresolvedBlockers.length > 0 ? { unresolvedBlockers } : {}),
|
|
170
|
-
...(
|
|
169
|
+
...(childIssues.length > 0 ? { childIssues } : {}),
|
|
171
170
|
};
|
|
172
171
|
}
|
|
172
|
+
classifyTrackedIssue(issue) {
|
|
173
|
+
const childIssueCount = this.db.issues.listChildIssues(issue.projectId, issue.linearIssueId).length;
|
|
174
|
+
const classification = classifyIssue({ issue, childIssueCount });
|
|
175
|
+
if (issue.issueClass === classification.issueClass && issue.issueClassSource === classification.issueClassSource) {
|
|
176
|
+
return issue;
|
|
177
|
+
}
|
|
178
|
+
return this.db.issues.upsertIssue({
|
|
179
|
+
projectId: issue.projectId,
|
|
180
|
+
linearIssueId: issue.linearIssueId,
|
|
181
|
+
issueClass: classification.issueClass,
|
|
182
|
+
issueClassSource: classification.issueClassSource,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
173
185
|
// ─── Run ────────────────────────────────────────────────────────
|
|
174
186
|
async run(item) {
|
|
175
187
|
await this.refreshCodexRuntimeConfig();
|
|
@@ -179,7 +191,8 @@ export class RunOrchestrator {
|
|
|
179
191
|
if (this.leaseService.hasLocalLease(item.projectId, item.issueId)) {
|
|
180
192
|
return;
|
|
181
193
|
}
|
|
182
|
-
const
|
|
194
|
+
const initialIssue = this.db.issues.getIssue(item.projectId, item.issueId);
|
|
195
|
+
const issue = initialIssue ? this.classifyTrackedIssue(initialIssue) : undefined;
|
|
183
196
|
if (!issue || issue.activeRunId !== undefined)
|
|
184
197
|
return;
|
|
185
198
|
const issueSession = this.db.issueSessions.getIssueSession(item.projectId, item.issueId);
|
|
@@ -216,7 +229,7 @@ export class RunOrchestrator {
|
|
|
216
229
|
? await this.runCompletionPolicy.resolveRequestedChangesWakeContext(issue, runType, context)
|
|
217
230
|
: context;
|
|
218
231
|
const coordinationContext = runType === "implementation"
|
|
219
|
-
? this.
|
|
232
|
+
? this.buildRelatedIssueContext(issue)
|
|
220
233
|
: undefined;
|
|
221
234
|
const effectiveContext = coordinationContext
|
|
222
235
|
? { ...coordinationContext, ...(baseContext ?? {}) }
|
|
@@ -82,6 +82,7 @@ export class TrackedIssueListQuery {
|
|
|
82
82
|
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
83
83
|
i.current_linear_state, i.factory_state, i.delegated_to_patchrelay, s.session_state, s.waiting_reason, s.summary_text, s.display_updated_at,
|
|
84
84
|
i.pending_run_type,
|
|
85
|
+
i.orchestration_settle_until,
|
|
85
86
|
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
86
87
|
i.last_github_ci_snapshot_json,
|
|
87
88
|
i.last_github_failure_source,
|
|
@@ -162,6 +163,7 @@ export class TrackedIssueListQuery {
|
|
|
162
163
|
blockedByCount,
|
|
163
164
|
hasPendingWake,
|
|
164
165
|
hasLegacyPendingRun: row.pending_run_type !== null && row.pending_run_type !== undefined,
|
|
166
|
+
...(row.orchestration_settle_until !== null ? { orchestrationSettleUntil: String(row.orchestration_settle_until) } : {}),
|
|
165
167
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
166
168
|
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
167
169
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
@@ -181,6 +183,7 @@ export class TrackedIssueListQuery {
|
|
|
181
183
|
blockedByKeys,
|
|
182
184
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
183
185
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
186
|
+
...(row.orchestration_settle_until !== null ? { orchestrationSettleUntil: String(row.orchestration_settle_until) } : {}),
|
|
184
187
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
185
188
|
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
186
189
|
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
@@ -15,6 +15,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
15
15
|
blockedByKeys,
|
|
16
16
|
factoryState: params.issue.factoryState,
|
|
17
17
|
pendingRunType: params.issue.pendingRunType,
|
|
18
|
+
orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
|
|
18
19
|
prNumber: params.issue.prNumber,
|
|
19
20
|
prState: params.issue.prState,
|
|
20
21
|
prHeadSha: params.issue.prHeadSha,
|
|
@@ -42,6 +43,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
42
43
|
projectId: params.issue.projectId,
|
|
43
44
|
linearIssueId: params.issue.linearIssueId,
|
|
44
45
|
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
46
|
+
...(params.issue.issueClass ? { issueClass: params.issue.issueClass } : {}),
|
|
45
47
|
...(params.issue.issueKey ? { issueKey: params.issue.issueKey } : {}),
|
|
46
48
|
...(params.issue.title ? { title: params.issue.title } : {}),
|
|
47
49
|
...(params.issue.url ? { issueUrl: params.issue.url } : {}),
|
|
@@ -63,6 +65,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
63
65
|
blockedByCount: unresolvedBlockedBy.length,
|
|
64
66
|
hasPendingWake: params.hasPendingWake,
|
|
65
67
|
hasLegacyPendingRun: params.issue.pendingRunType !== undefined,
|
|
68
|
+
orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
|
|
66
69
|
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
67
70
|
...(params.issue.prState ? { prState: params.issue.prState } : {}),
|
|
68
71
|
...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
|
package/dist/waiting-reason.js
CHANGED
|
@@ -11,6 +11,7 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
11
11
|
sameHeadStillBlocked: "Requested changes still block the current head",
|
|
12
12
|
waitingForMergeStewardRepair: "Waiting to repair a merge-steward incident",
|
|
13
13
|
waitingForDownstreamAutomation: "PatchRelay work is done; waiting on downstream review/merge automation",
|
|
14
|
+
waitingForChildSettle: "Waiting briefly for child issues to settle before orchestration starts",
|
|
14
15
|
workComplete: "PatchRelay work is complete",
|
|
15
16
|
waitingForOperatorIntervention: "Waiting on operator intervention",
|
|
16
17
|
waitingForExternalReview: "Waiting on external review",
|
|
@@ -33,6 +34,12 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
33
34
|
if (params.activeRunId !== undefined) {
|
|
34
35
|
return PATCHRELAY_WAITING_REASONS.activeWork;
|
|
35
36
|
}
|
|
37
|
+
if (params.orchestrationSettleUntil) {
|
|
38
|
+
const settleAt = Date.parse(params.orchestrationSettleUntil);
|
|
39
|
+
if (Number.isFinite(settleAt) && settleAt > Date.now()) {
|
|
40
|
+
return PATCHRELAY_WAITING_REASONS.waitingForChildSettle;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
const blockedByKeys = (params.blockedByKeys ?? []).filter((value) => value.trim().length > 0);
|
|
37
44
|
if (blockedByKeys.length > 0) {
|
|
38
45
|
return `Blocked by ${blockedByKeys.join(", ")}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
2
|
import { hasExplicitPatchRelayWakeIntent, isInertPatchRelayComment, isPatchRelayManagedCommentAuthor, } from "./comment-policy.js";
|
|
3
|
+
import { classifyIssue } from "../issue-class.js";
|
|
3
4
|
const ENQUEUEABLE_STATES = new Set(["pr_open", "changes_requested", "implementing", "delegated", "awaiting_input"]);
|
|
4
5
|
export class CommentWakeHandler {
|
|
5
6
|
db;
|
|
@@ -24,6 +25,10 @@ export class CommentWakeHandler {
|
|
|
24
25
|
const issue = this.db.issues.getIssue(project.id, normalized.issue.id);
|
|
25
26
|
if (!issue)
|
|
26
27
|
return;
|
|
28
|
+
const issueClass = classifyIssue({
|
|
29
|
+
issue,
|
|
30
|
+
childIssueCount: this.db.issues.listChildIssues(project.id, normalized.issue.id).length,
|
|
31
|
+
}).issueClass;
|
|
27
32
|
const trimmedBody = normalized.comment.body.trim();
|
|
28
33
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
29
34
|
const selfAuthored = isPatchRelayManagedCommentAuthor(installation, normalized.actor, normalized.comment.userName);
|
|
@@ -55,7 +60,7 @@ export class CommentWakeHandler {
|
|
|
55
60
|
if (!issue.activeRunId) {
|
|
56
61
|
if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
|
|
57
62
|
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|
|
58
|
-
const wakeIntent = directReply || hasExplicitPatchRelayWakeIntent(trimmedBody);
|
|
63
|
+
const wakeIntent = issueClass === "orchestration" || directReply || hasExplicitPatchRelayWakeIntent(trimmedBody);
|
|
59
64
|
if (!wakeIntent) {
|
|
60
65
|
this.feed?.publish({
|
|
61
66
|
level: "info",
|
|
@@ -104,6 +104,9 @@ export function hasCompleteIssueContext(issue) {
|
|
|
104
104
|
export function mergeIssueMetadata(issue, liveIssue) {
|
|
105
105
|
return {
|
|
106
106
|
...issue,
|
|
107
|
+
...(issue.parentId ? {} : liveIssue.parentId ? { parentId: liveIssue.parentId } : {}),
|
|
108
|
+
...(issue.parentIdentifier ? {} : liveIssue.parentIdentifier ? { parentIdentifier: liveIssue.parentIdentifier } : {}),
|
|
109
|
+
...(issue.parentTitle ? {} : liveIssue.parentTitle ? { parentTitle: liveIssue.parentTitle } : {}),
|
|
107
110
|
...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
|
|
108
111
|
...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
|
|
109
112
|
...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { classifyIssue } from "../issue-class.js";
|
|
2
|
+
import { computeOrchestrationSettleUntil, wakeOrchestrationParentsForChildEvent, } from "../orchestration-parent-wake.js";
|
|
1
3
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
4
|
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
3
5
|
import { appendDelegationObservedEvent } from "../delegation-audit.js";
|
|
4
|
-
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
6
|
+
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isResolvedLinearState, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
5
7
|
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
6
8
|
import { resolveLinkedPullRequest } from "../linear-linked-pr-reconciliation.js";
|
|
7
9
|
import { readRemotePrState } from "../remote-pr-state.js";
|
|
@@ -70,6 +72,24 @@ export class DesiredStageRecorder {
|
|
|
70
72
|
terminal,
|
|
71
73
|
currentState: existingIssue?.factoryState,
|
|
72
74
|
});
|
|
75
|
+
const childIssueCount = this.db.issues.listChildIssues(params.project.id, normalizedIssue.id).length;
|
|
76
|
+
const classification = classifyIssue({
|
|
77
|
+
issue: {
|
|
78
|
+
issueClass: existingIssue?.issueClass,
|
|
79
|
+
issueClassSource: existingIssue?.issueClassSource,
|
|
80
|
+
title: hydratedIssue.title ?? existingIssue?.title,
|
|
81
|
+
description: hydratedIssue.description ?? existingIssue?.description,
|
|
82
|
+
parentLinearIssueId: hydratedIssue.parentId ?? existingIssue?.parentLinearIssueId,
|
|
83
|
+
},
|
|
84
|
+
childIssueCount,
|
|
85
|
+
});
|
|
86
|
+
const shouldEnterOrchestrationSettle = Boolean(delegated
|
|
87
|
+
&& desiredStage === "implementation"
|
|
88
|
+
&& classification.issueClass === "orchestration"
|
|
89
|
+
&& childIssueCount === 0
|
|
90
|
+
&& !existingIssue?.threadId
|
|
91
|
+
&& !activeRun
|
|
92
|
+
&& !terminal);
|
|
73
93
|
const runRelease = decideActiveRunRelease({
|
|
74
94
|
hasActiveRun: Boolean(activeRun),
|
|
75
95
|
terminal,
|
|
@@ -126,6 +146,10 @@ export class DesiredStageRecorder {
|
|
|
126
146
|
projectId: params.project.id,
|
|
127
147
|
linearIssueId: normalizedIssue.id,
|
|
128
148
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
149
|
+
...(hydratedIssue.parentId !== undefined ? { parentLinearIssueId: hydratedIssue.parentId ?? null } : {}),
|
|
150
|
+
...(hydratedIssue.parentIdentifier !== undefined ? { parentIssueKey: hydratedIssue.parentIdentifier ?? null } : {}),
|
|
151
|
+
issueClass: classification.issueClass,
|
|
152
|
+
issueClassSource: classification.issueClassSource,
|
|
129
153
|
...(hydratedIssue.title ? { title: hydratedIssue.title } : {}),
|
|
130
154
|
...(hydratedIssue.description ? { description: hydratedIssue.description } : {}),
|
|
131
155
|
...(hydratedIssue.url ? { url: hydratedIssue.url } : {}),
|
|
@@ -152,6 +176,7 @@ export class DesiredStageRecorder {
|
|
|
152
176
|
...(terminalRunRelease ? { factoryState: "done", pendingRunType: null, pendingRunContextJson: null } : {}),
|
|
153
177
|
...(blockerPausedImplementation ? { factoryState: "delegated" } : {}),
|
|
154
178
|
...(undelegation.factoryState ? { factoryState: undelegation.factoryState } : {}),
|
|
179
|
+
...(shouldEnterOrchestrationSettle ? { orchestrationSettleUntil: computeOrchestrationSettleUntil() } : {}),
|
|
155
180
|
});
|
|
156
181
|
if (effectiveRunRelease.release && activeRun && effectiveRunRelease.reason) {
|
|
157
182
|
this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: effectiveRunRelease.reason });
|
|
@@ -166,6 +191,10 @@ export class DesiredStageRecorder {
|
|
|
166
191
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
167
192
|
}))
|
|
168
193
|
: this.db.transaction(commitIssueUpdate);
|
|
194
|
+
const previousParentIssueId = existingIssue?.parentLinearIssueId;
|
|
195
|
+
const currentParentIssueId = issue.parentLinearIssueId;
|
|
196
|
+
const wasResolved = isResolvedLinearState(existingIssue?.currentLinearStateType, existingIssue?.currentLinearState);
|
|
197
|
+
const isResolved = isResolvedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
169
198
|
if (undelegation.factoryState) {
|
|
170
199
|
if (activeRun?.threadId && activeRun.turnId) {
|
|
171
200
|
await params.stopActiveRun(activeRun, "STOP: The issue was un-delegated from PatchRelay. Stop working immediately and exit.");
|
|
@@ -213,6 +242,17 @@ export class DesiredStageRecorder {
|
|
|
213
242
|
...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
|
|
214
243
|
});
|
|
215
244
|
}
|
|
245
|
+
else if (shouldEnterOrchestrationSettle) {
|
|
246
|
+
this.feed?.publish({
|
|
247
|
+
level: "info",
|
|
248
|
+
kind: "stage",
|
|
249
|
+
issueKey: issue.issueKey,
|
|
250
|
+
projectId: params.project.id,
|
|
251
|
+
stage: issue.factoryState,
|
|
252
|
+
status: "settling_children",
|
|
253
|
+
summary: "Waiting briefly for child issues to settle before orchestration starts",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
216
256
|
else if (!startupResume.factoryState
|
|
217
257
|
&& !startupResume.pendingRunType
|
|
218
258
|
&&
|
|
@@ -232,6 +272,46 @@ export class DesiredStageRecorder {
|
|
|
232
272
|
dedupeKey: `delegated:${normalizedIssue.id}`,
|
|
233
273
|
});
|
|
234
274
|
}
|
|
275
|
+
if (previousParentIssueId && previousParentIssueId !== currentParentIssueId) {
|
|
276
|
+
wakeOrchestrationParentsForChildEvent({
|
|
277
|
+
db: this.db,
|
|
278
|
+
child: {
|
|
279
|
+
projectId: issue.projectId,
|
|
280
|
+
linearIssueId: issue.linearIssueId,
|
|
281
|
+
parentLinearIssueId: previousParentIssueId,
|
|
282
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
283
|
+
...(issue.title ? { title: issue.title } : {}),
|
|
284
|
+
factoryState: issue.factoryState,
|
|
285
|
+
...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
|
|
286
|
+
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
287
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
288
|
+
},
|
|
289
|
+
eventType: "child_changed",
|
|
290
|
+
changeKind: "detached",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (currentParentIssueId) {
|
|
294
|
+
const changeKind = previousParentIssueId !== currentParentIssueId
|
|
295
|
+
? "attached"
|
|
296
|
+
: issue.currentLinearState?.trim().toLowerCase() === "duplicate"
|
|
297
|
+
? "duplicate"
|
|
298
|
+
: issue.currentLinearStateType === "canceled"
|
|
299
|
+
? "canceled"
|
|
300
|
+
: "updated";
|
|
301
|
+
const eventType = previousParentIssueId !== currentParentIssueId
|
|
302
|
+
? "child_changed"
|
|
303
|
+
: !wasResolved && isResolved
|
|
304
|
+
? "child_delivered"
|
|
305
|
+
: wasResolved && !isResolved
|
|
306
|
+
? "child_regressed"
|
|
307
|
+
: "child_changed";
|
|
308
|
+
wakeOrchestrationParentsForChildEvent({
|
|
309
|
+
db: this.db,
|
|
310
|
+
child: issue,
|
|
311
|
+
eventType,
|
|
312
|
+
changeKind,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
235
315
|
return {
|
|
236
316
|
issue: this.db.issueToTrackedIssue(issue),
|
|
237
317
|
wakeRunType: params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id),
|
|
@@ -310,6 +390,11 @@ export class DesiredStageRecorder {
|
|
|
310
390
|
})),
|
|
311
391
|
});
|
|
312
392
|
}
|
|
393
|
+
this.db.issues.replaceIssueParentLink({
|
|
394
|
+
projectId,
|
|
395
|
+
childLinearIssueId: source.id,
|
|
396
|
+
parentLinearIssueId: source.parentId ?? null,
|
|
397
|
+
});
|
|
313
398
|
return { issue: source, hydration };
|
|
314
399
|
}
|
|
315
400
|
async resolveLinkedPrAdoption(params) {
|
package/dist/webhooks.js
CHANGED
|
@@ -195,6 +195,7 @@ function extractIssueMetadata(payload) {
|
|
|
195
195
|
return undefined;
|
|
196
196
|
}
|
|
197
197
|
const teamRecord = asRecord(issueRecord.team);
|
|
198
|
+
const parentRecord = asRecord(issueRecord.parent);
|
|
198
199
|
const identifier = getString(issueRecord, "identifier");
|
|
199
200
|
const title = getString(issueRecord, "title");
|
|
200
201
|
const url = getString(issueRecord, "url") ?? payload.url;
|
|
@@ -208,12 +209,18 @@ function extractIssueMetadata(payload) {
|
|
|
208
209
|
const delegateId = getString(issueRecord, "delegateId") ?? getString(delegateRecord ?? {}, "id");
|
|
209
210
|
const delegateName = getString(delegateRecord ?? {}, "name");
|
|
210
211
|
const description = getString(issueRecord, "description");
|
|
212
|
+
const parentId = getString(issueRecord, "parentId") ?? getString(parentRecord ?? {}, "id");
|
|
213
|
+
const parentIdentifier = getString(parentRecord ?? {}, "identifier");
|
|
214
|
+
const parentTitle = getString(parentRecord ?? {}, "title");
|
|
211
215
|
const rawPriority = issueRecord.priority;
|
|
212
216
|
const priority = typeof rawPriority === "number" ? rawPriority : undefined;
|
|
213
217
|
const rawEstimate = issueRecord.estimate;
|
|
214
218
|
const estimate = typeof rawEstimate === "number" ? rawEstimate : undefined;
|
|
215
219
|
return {
|
|
216
220
|
id,
|
|
221
|
+
...(parentId ? { parentId } : {}),
|
|
222
|
+
...(parentIdentifier ? { parentIdentifier } : {}),
|
|
223
|
+
...(parentTitle ? { parentTitle } : {}),
|
|
217
224
|
...(identifier ? { identifier } : {}),
|
|
218
225
|
...(title ? { title } : {}),
|
|
219
226
|
...(description ? { description } : {}),
|