patchrelay 0.50.1 → 0.50.2
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 +2 -2
- package/config/patchrelay.example.json +2 -1
- package/dist/build-info.json +3 -3
- package/dist/completion-check.js +6 -0
- package/dist/config.js +23 -3
- package/dist/prompting/patchrelay.js +102 -164
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,12 +61,12 @@ patchrelay service status
|
|
|
61
61
|
patchrelay dashboard
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Each repo needs two workflow files
|
|
64
|
+
Each repo needs two workflow files for repo-specific run behavior:
|
|
65
65
|
|
|
66
66
|
- `IMPLEMENTATION_WORKFLOW.md` — implementation, CI repair, queue repair runs
|
|
67
67
|
- `REVIEW_WORKFLOW.md` — review fix runs
|
|
68
68
|
|
|
69
|
-
Keep them short, action-oriented, human-authored. See [prompting.md](./docs/prompting.md) for how the built-in scaffold composes them
|
|
69
|
+
Keep them short, action-oriented, human-authored. Durable machine-level policy belongs in Codex `developer_instructions`; workflow files are for repo-local behavior and validation. See [prompting.md](./docs/prompting.md) for how the built-in scaffold composes them.
|
|
70
70
|
|
|
71
71
|
Full install, ingress, and GitHub/Linear app setup: [self-hosting.md](./docs/self-hosting.md). Daily ops and CLI cheatsheet: [operator-guide.md](./docs/operator-guide.md).
|
|
72
72
|
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"git_bin": "git",
|
|
7
7
|
"codex": {
|
|
8
8
|
"model": "gpt-5.3-codex-spark",
|
|
9
|
-
"reasoning_effort": "high"
|
|
9
|
+
"reasoning_effort": "high",
|
|
10
|
+
"developer_instructions": "Keep the public API stable for this installation unless the task explicitly requires a breaking change."
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
}
|
package/dist/build-info.json
CHANGED
package/dist/completion-check.js
CHANGED
|
@@ -101,6 +101,12 @@ function buildCompletionCheckPrompt(params) {
|
|
|
101
101
|
'- "done" if the task was successfully completed without a PR.',
|
|
102
102
|
'- "failed" if the run stopped incorrectly and PatchRelay should not auto-continue.',
|
|
103
103
|
"",
|
|
104
|
+
"Bias rules:",
|
|
105
|
+
'- Prefer "continue" when the task looks like normal code-delivery work and the run simply ended before publishing a branch or PR.',
|
|
106
|
+
'- Use "done" only when the issue explicitly permits a no-PR outcome or the deliverable is clearly non-repo work such as planning, orchestration, or follow-up issue creation.',
|
|
107
|
+
'- Use "needs_input" only when one specific missing human decision blocks the very next concrete action. Do not use it just because the branch might need more work.',
|
|
108
|
+
'- If the run appears unfinished but still actionable by PatchRelay, return "continue", not "done".',
|
|
109
|
+
"",
|
|
104
110
|
"Facts:",
|
|
105
111
|
`- Issue: ${params.issue.issueKey ?? params.issue.linearIssueId}`,
|
|
106
112
|
...(params.issue.title ? [`- Title: ${params.issue.title}`] : []),
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,21 @@ import { ensureAbsolutePath } from "./utils.js";
|
|
|
9
9
|
const LINEAR_OAUTH_CALLBACK_PATH = "/oauth/linear/callback";
|
|
10
10
|
const REPO_SETTINGS_DIRNAME = ".patchrelay";
|
|
11
11
|
const REPO_SETTINGS_FILENAME = "project.json";
|
|
12
|
+
const DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS = [
|
|
13
|
+
"You are PatchRelay's coding agent.",
|
|
14
|
+
"",
|
|
15
|
+
"Core rules:",
|
|
16
|
+
"- Complete the delegated task in the current repository and worktree.",
|
|
17
|
+
"- Stay inside the issue's scope. Do not widen into unrelated cleanup or polish.",
|
|
18
|
+
"- Use repository docs and workflow files as the source of truth for local conventions.",
|
|
19
|
+
"- Prefer concrete fixes over speculative defenses.",
|
|
20
|
+
"- For code-delivery work, publish before stopping.",
|
|
21
|
+
"- If you change files for an implementation run, commit, push the issue branch, and open or update the PR.",
|
|
22
|
+
"- For repair runs, work on the existing PR branch and do not open a new PR.",
|
|
23
|
+
"- A requested-changes repair is only complete after a newer PR head is pushed, unless a genuine external blocker prevents correct publication.",
|
|
24
|
+
"- If a broader inconsistency is not required to make this task correct, mention it briefly instead of expanding scope.",
|
|
25
|
+
"- Before publishing, do one brief reviewer-minded pass on the current head and fix likely in-scope blockers.",
|
|
26
|
+
].join("\n");
|
|
12
27
|
const trustedActorsSchema = z
|
|
13
28
|
.object({
|
|
14
29
|
ids: z.array(z.string().min(1)).default([]),
|
|
@@ -238,6 +253,13 @@ function loadPromptLayer(configDir, layer) {
|
|
|
238
253
|
replaceSections: Object.fromEntries(Object.entries(layer.replace_sections).map(([sectionId, fragmentPath]) => [sectionId, readPromptFile(configDir, fragmentPath)])),
|
|
239
254
|
};
|
|
240
255
|
}
|
|
256
|
+
function mergeDeveloperInstructions(custom) {
|
|
257
|
+
const normalized = custom?.trim();
|
|
258
|
+
if (!normalized) {
|
|
259
|
+
return DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS;
|
|
260
|
+
}
|
|
261
|
+
return `${DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS}\n\n## Local Developer Instructions\n\n${normalized}`;
|
|
262
|
+
}
|
|
241
263
|
function expandEnv(value, env) {
|
|
242
264
|
if (typeof value === "string") {
|
|
243
265
|
return value.replace(/\$\{([A-Z0-9_]+)(?::-(.*?))?\}/g, (_match, name, fallback) => {
|
|
@@ -548,9 +570,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
548
570
|
...(parsed.runner.codex.reasoning_effort ? { reasoningEffort: parsed.runner.codex.reasoning_effort } : {}),
|
|
549
571
|
...(parsed.runner.codex.service_name ? { serviceName: parsed.runner.codex.service_name } : {}),
|
|
550
572
|
...(parsed.runner.codex.base_instructions ? { baseInstructions: parsed.runner.codex.base_instructions } : {}),
|
|
551
|
-
|
|
552
|
-
? { developerInstructions: parsed.runner.codex.developer_instructions }
|
|
553
|
-
: {}),
|
|
573
|
+
developerInstructions: mergeDeveloperInstructions(parsed.runner.codex.developer_instructions),
|
|
554
574
|
approvalPolicy: parsed.runner.codex.approval_policy,
|
|
555
575
|
sandboxMode: parsed.runner.codex.sandbox_mode,
|
|
556
576
|
persistExtendedHistory: parsed.runner.codex.persist_extended_history,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const WORKFLOW_FILES = {
|
|
4
4
|
implementation: "IMPLEMENTATION_WORKFLOW.md",
|
|
@@ -22,12 +22,10 @@ export const PATCHRELAY_REPLACEABLE_SECTION_IDS = [
|
|
|
22
22
|
"workflow-guidance",
|
|
23
23
|
"publication-contract",
|
|
24
24
|
];
|
|
25
|
-
function
|
|
25
|
+
function hasWorkflowFile(repoPath, runType) {
|
|
26
26
|
const filename = WORKFLOW_FILES[runType];
|
|
27
27
|
const filePath = path.join(repoPath, filename);
|
|
28
|
-
|
|
29
|
-
return undefined;
|
|
30
|
-
return readFileSync(filePath, "utf8").trim();
|
|
28
|
+
return existsSync(filePath);
|
|
31
29
|
}
|
|
32
30
|
function buildPromptHeader(issue) {
|
|
33
31
|
return [
|
|
@@ -102,7 +100,7 @@ function summarizeRelationEntries(entries, options) {
|
|
|
102
100
|
}
|
|
103
101
|
return lines;
|
|
104
102
|
}
|
|
105
|
-
function
|
|
103
|
+
function buildIssueTopology(context) {
|
|
106
104
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
107
105
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
108
106
|
: [];
|
|
@@ -111,18 +109,10 @@ function buildCoordinationGuidance(context) {
|
|
|
111
109
|
: Array.isArray(context?.trackedDependents)
|
|
112
110
|
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
113
111
|
: [];
|
|
114
|
-
const lines = [
|
|
115
|
-
"### Coordination / Issue Topology",
|
|
116
|
-
"",
|
|
117
|
-
"First decide whether this issue should publish code itself or mainly coordinate other issues.",
|
|
118
|
-
"If this issue is a parent tracker, umbrella, migration program, or convergence container and the concrete implementation belongs in child issues, do not create a duplicate umbrella PR.",
|
|
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.",
|
|
120
|
-
"Prefer one PR per concrete implementation issue over a broad parent branch that restates overlapping child work.",
|
|
121
|
-
];
|
|
122
112
|
if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
|
|
123
|
-
return
|
|
113
|
+
return [];
|
|
124
114
|
}
|
|
125
|
-
lines
|
|
115
|
+
const lines = ["### Issue Topology", ""];
|
|
126
116
|
if (unresolvedBlockers.length > 0) {
|
|
127
117
|
lines.push("Unresolved blockers:");
|
|
128
118
|
lines.push(...summarizeRelationEntries(unresolvedBlockers));
|
|
@@ -136,35 +126,25 @@ function buildCoordinationGuidance(context) {
|
|
|
136
126
|
}
|
|
137
127
|
return lines;
|
|
138
128
|
}
|
|
139
|
-
function
|
|
129
|
+
function buildConstraints(issue, context) {
|
|
140
130
|
const description = issue.description?.trim();
|
|
141
131
|
const scope = extractIssueSection(description, "Scope");
|
|
142
132
|
const acceptance = extractIssueSection(description, "Acceptance criteria")
|
|
143
133
|
?? extractIssueSection(description, "Success criteria");
|
|
144
134
|
const relevantCode = extractIssueSection(description, "Relevant code");
|
|
135
|
+
const topology = buildIssueTopology(context);
|
|
145
136
|
return [
|
|
146
|
-
"##
|
|
137
|
+
"## Constraints",
|
|
147
138
|
"",
|
|
148
|
-
"Stay inside the delegated task.",
|
|
149
|
-
"Finish the issue completely enough to satisfy its stated scope and acceptance criteria, but do not widen it into unrelated product polish or follow-up cleanup.",
|
|
150
|
-
"Only broaden to adjacent routes, copy, or supporting surfaces when the issue text or repository guidance explicitly says they are the same user flow.",
|
|
151
|
-
"Your implementation goal is to leave the branch review-ready, not merely locally working: look for likely regressions or invariant breaks in the touched flow before you stop.",
|
|
152
|
-
"If a narrow patch fixes the immediate symptom but leaves the same underlying risk elsewhere in the changed flow, keep going until the branch is likely to survive strict review or clearly explain the blocker.",
|
|
153
|
-
"If you notice a worthwhile broader inconsistency that is not required to make this task correct, mention it in your summary as follow-up context instead of expanding the implementation.",
|
|
139
|
+
"Stay inside the delegated task. Do not widen scope into unrelated cleanup or optional polish.",
|
|
154
140
|
"",
|
|
155
141
|
...(scope ? ["### In Scope", "", scope, ""] : []),
|
|
156
142
|
...(acceptance ? ["### Acceptance / Done", "", acceptance, ""] : []),
|
|
157
143
|
...(relevantCode ? ["### Relevant Code", "", relevantCode, ""] : []),
|
|
158
|
-
...
|
|
159
|
-
"",
|
|
160
|
-
"### Likely Review Invariants",
|
|
161
|
-
"",
|
|
162
|
-
"- Check the surfaces explicitly named in the task before stopping.",
|
|
163
|
-
"- If repository guidance says certain changed surfaces are one flow, verify that shared flow, but do not treat unrelated surrounding cleanup as part of this task.",
|
|
164
|
-
"- A review repair should fix the concrete concern on the current head, not silently expand the Linear issue into a broader rewrite.",
|
|
144
|
+
...topology,
|
|
165
145
|
].join("\n");
|
|
166
146
|
}
|
|
167
|
-
function
|
|
147
|
+
function buildOrchestrationConstraints(context) {
|
|
168
148
|
const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
|
|
169
149
|
? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
170
150
|
: [];
|
|
@@ -174,23 +154,11 @@ function buildOrchestrationScopeDiscipline(context) {
|
|
|
174
154
|
? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
175
155
|
: [];
|
|
176
156
|
return [
|
|
177
|
-
"##
|
|
157
|
+
"## Constraints",
|
|
178
158
|
"",
|
|
179
|
-
"This issue is orchestration work.",
|
|
180
|
-
"
|
|
181
|
-
"
|
|
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
|
-
"When you split required implementation work out of the parent, make it a child issue of this umbrella rather than making the parent block the child.",
|
|
186
|
-
"If sequencing matters, let the parent wait on the child; do not model the child as blocked by the parent umbrella it is supposed to satisfy.",
|
|
187
|
-
"If child work is still in motion, babysit the plan, record useful observations, and return to waiting.",
|
|
188
|
-
"If child work looks delivered, audit whether the original parent goal is actually satisfied.",
|
|
189
|
-
"Create blocking follow-up work only when it is necessary to satisfy the original parent goal.",
|
|
190
|
-
"Prefer non-blocking follow-up issues over keeping the umbrella open for optional polish or adjacent expansion.",
|
|
191
|
-
"New child issues should stay in Backlog and undelegated by default.",
|
|
192
|
-
"Only delegate or move a new child to Start when it is immediately actionable, unblocked, and you intend for PatchRelay to begin it right away.",
|
|
193
|
-
"If you create multiple new child issues, keep later-wave or dependency-bound children queued rather than waking them all at once.",
|
|
159
|
+
"This issue is orchestration work. Coordinate convergence instead of duplicating child implementation.",
|
|
160
|
+
"Do not open an overlapping umbrella PR unless this parent owns unique direct work.",
|
|
161
|
+
"Leave later-wave child issues queued unless they are immediately actionable.",
|
|
194
162
|
"",
|
|
195
163
|
"### Child Issue Summaries",
|
|
196
164
|
"",
|
|
@@ -204,29 +172,28 @@ function buildOrchestrationScopeDiscipline(context) {
|
|
|
204
172
|
"### Convergence Rule",
|
|
205
173
|
"",
|
|
206
174
|
"- Close the umbrella when the original parent goal is satisfied.",
|
|
207
|
-
"-
|
|
208
|
-
"- Do not invent optional expansion without explicit human approval.",
|
|
175
|
+
"- Create blocking follow-up work only when it is required to satisfy that goal.",
|
|
209
176
|
].join("\n");
|
|
210
177
|
}
|
|
211
|
-
function
|
|
178
|
+
function buildHumanContextLines(context) {
|
|
212
179
|
const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
|
|
213
180
|
const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
|
|
214
181
|
const operatorPrompt = typeof context?.operatorPrompt === "string" ? context.operatorPrompt.trim() : "";
|
|
215
182
|
const userComment = typeof context?.userComment === "string" ? context.userComment.trim() : "";
|
|
216
183
|
const lines = [];
|
|
217
184
|
if (promptContext) {
|
|
218
|
-
lines.push("
|
|
185
|
+
lines.push("Linear session context:", promptContext, "");
|
|
219
186
|
}
|
|
220
187
|
if (latestPrompt) {
|
|
221
|
-
lines.push("
|
|
188
|
+
lines.push("Latest human instruction:", latestPrompt, "");
|
|
222
189
|
}
|
|
223
190
|
if (operatorPrompt) {
|
|
224
|
-
lines.push("
|
|
191
|
+
lines.push("Operator prompt:", operatorPrompt, "");
|
|
225
192
|
}
|
|
226
193
|
if (userComment) {
|
|
227
|
-
lines.push("
|
|
194
|
+
lines.push("Human follow-up comment:", userComment, "");
|
|
228
195
|
}
|
|
229
|
-
return lines
|
|
196
|
+
return lines;
|
|
230
197
|
}
|
|
231
198
|
function resolveRequestedChangesMode(runType, context) {
|
|
232
199
|
if (runType === "branch_upkeep") {
|
|
@@ -303,12 +270,12 @@ function buildRequestedChangesContext(runType, context) {
|
|
|
303
270
|
const mode = resolveRequestedChangesMode(runType, context);
|
|
304
271
|
const lines = [];
|
|
305
272
|
if (mode === "branch_upkeep") {
|
|
306
|
-
lines.push("
|
|
273
|
+
lines.push("Branch upkeep is required on the existing PR branch.", "Goal: restore merge readiness on the current branch and push a newer head without regressing review or CI readiness.");
|
|
307
274
|
}
|
|
308
275
|
else {
|
|
309
276
|
const reviewer = typeof context?.reviewerName === "string" ? context.reviewerName : undefined;
|
|
310
277
|
const reviewBody = typeof context?.reviewBody === "string" ? context.reviewBody.trim() : "";
|
|
311
|
-
lines.push("
|
|
278
|
+
lines.push("Requested changes on the existing PR branch.", "Goal: restore review readiness and push a newer head on the current PR branch.", "Address the real concern behind the feedback and verify nearby invariants in the touched flow before you publish.", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "");
|
|
312
279
|
appendStructuredReviewContext(lines, context);
|
|
313
280
|
}
|
|
314
281
|
return lines.join("\n").trim();
|
|
@@ -318,10 +285,8 @@ function buildCiRepairContext(context) {
|
|
|
318
285
|
? context.ciSnapshot
|
|
319
286
|
: undefined;
|
|
320
287
|
return [
|
|
321
|
-
"
|
|
322
|
-
"",
|
|
323
|
-
"Goal: restore CI readiness on the existing PR branch so the next full CI run is likely to pass.",
|
|
324
|
-
"A full CI iteration has settled failed on your PR. Start from the specific failing check/job/step below on the latest remote PR branch tip, but do not stop at a narrow patch if the same root cause is likely to fail other checks in the suite.",
|
|
288
|
+
"Settled CI failure on the existing PR branch.",
|
|
289
|
+
"Goal: restore CI readiness and push a branch that is likely to pass the next full CI run.",
|
|
325
290
|
snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "",
|
|
326
291
|
snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "",
|
|
327
292
|
snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "",
|
|
@@ -337,14 +302,6 @@ function buildCiRepairContext(context) {
|
|
|
337
302
|
Array.isArray(context?.annotations) && context.annotations.length > 0
|
|
338
303
|
? `Annotations:\n${context.annotations.map((entry) => `- ${String(entry)}`).join("\n")}`
|
|
339
304
|
: "",
|
|
340
|
-
"",
|
|
341
|
-
"Fetch the latest remote branch state first. If the branch moved since this failure, restart from the new tip instead of pushing older work.",
|
|
342
|
-
"Read the latest logs for the named failing check, identify the root cause, and check whether that same cause is likely to affect other jobs or checks.",
|
|
343
|
-
"Fix the root cause, not just the first visible symptom.",
|
|
344
|
-
"Do not change workflows, dependency installation, or unrelated tests unless the failing logs clearly point there.",
|
|
345
|
-
"Run the narrowest local verification that gives real confidence for the suite, then commit and push.",
|
|
346
|
-
"Do not open a new PR. Keep working on the existing branch until the branch is likely to pass CI again or the situation is clearly stuck.",
|
|
347
|
-
"Do not change test expectations unless the test is genuinely wrong.",
|
|
348
305
|
].filter(Boolean).join("\n");
|
|
349
306
|
}
|
|
350
307
|
function appendQueueRepairContext(lines, context) {
|
|
@@ -388,67 +345,57 @@ function appendQueueRepairContext(lines, context) {
|
|
|
388
345
|
function buildQueueRepairContext(context) {
|
|
389
346
|
const lines = [];
|
|
390
347
|
appendQueueRepairContext(lines, context);
|
|
391
|
-
lines.push("
|
|
348
|
+
lines.push("Merge queue rejection on the existing PR branch.", "Goal: restore a mergeable branch, verify the queue-blocking fix, and push the existing PR branch.", context?.failureReason ? `Failure reason: ${String(context.failureReason)}` : "");
|
|
392
349
|
return lines.filter(Boolean).join("\n");
|
|
393
350
|
}
|
|
394
|
-
function
|
|
351
|
+
function buildFollowUpContextLines(issue, runType, context) {
|
|
395
352
|
const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
|
|
396
353
|
const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
|
|
397
354
|
const followUpLines = followUps
|
|
398
355
|
.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
399
356
|
.map((entry) => `${String(entry.type ?? "follow_up")} from ${String(entry.author ?? "unknown")}: ${String(entry.text ?? "").trim()}`.trim())
|
|
400
357
|
.filter((line) => !line.endsWith(":"));
|
|
401
|
-
const lines = [
|
|
402
|
-
|
|
403
|
-
""
|
|
404
|
-
wakeReason === "
|
|
405
|
-
? "
|
|
406
|
-
: wakeReason === "
|
|
407
|
-
? "
|
|
408
|
-
: wakeReason === "
|
|
409
|
-
? "
|
|
410
|
-
: wakeReason === "
|
|
411
|
-
? "
|
|
412
|
-
: wakeReason === "
|
|
413
|
-
? "
|
|
414
|
-
: wakeReason === "
|
|
415
|
-
? "
|
|
416
|
-
: wakeReason === "
|
|
417
|
-
? "
|
|
418
|
-
: wakeReason === "
|
|
419
|
-
? "
|
|
420
|
-
:
|
|
421
|
-
|
|
422
|
-
: `Why this turn exists: Continue the existing ${runType} run from the latest issue state.`,
|
|
423
|
-
wakeReason === "direct_reply"
|
|
424
|
-
? "Required action now: Apply the latest human answer, continue from the current branch/session context, and publish the next concrete result."
|
|
425
|
-
: wakeReason === "initial_delegate"
|
|
426
|
-
? "Required action now: Inspect the umbrella goal, review the child set, and record the next orchestration step."
|
|
427
|
-
: wakeReason === "completion_check_continue"
|
|
428
|
-
? "Required action now: Continue from the current branch and thread context, finish the task, and publish the next concrete result."
|
|
429
|
-
: "Required action now: Continue from the latest branch state, refresh any stale assumptions, and publish the next concrete result.",
|
|
430
|
-
"",
|
|
431
|
-
];
|
|
358
|
+
const lines = [];
|
|
359
|
+
const turnReason = wakeReason === "direct_reply"
|
|
360
|
+
? "Human reply to the previous question."
|
|
361
|
+
: wakeReason === "initial_delegate"
|
|
362
|
+
? "Initial orchestration turn after delegation."
|
|
363
|
+
: wakeReason === "child_delivered"
|
|
364
|
+
? "A child issue was delivered."
|
|
365
|
+
: wakeReason === "child_changed"
|
|
366
|
+
? "A child issue changed state."
|
|
367
|
+
: wakeReason === "child_regressed"
|
|
368
|
+
? "A child issue regressed."
|
|
369
|
+
: wakeReason === "human_instruction"
|
|
370
|
+
? "A human added new orchestration guidance."
|
|
371
|
+
: wakeReason === "completion_check_continue"
|
|
372
|
+
? "The previous turn ended without a PR and PatchRelay chose to continue automatically."
|
|
373
|
+
: wakeReason === "branch_upkeep"
|
|
374
|
+
? "GitHub still shows the PR branch as needing upkeep."
|
|
375
|
+
: wakeReason === "followup_comment"
|
|
376
|
+
? "A human follow-up comment arrived after the previous turn."
|
|
377
|
+
: `Continue the existing ${runType} run from the latest issue state.`;
|
|
378
|
+
lines.push(`Turn reason: ${turnReason}`);
|
|
432
379
|
if (wakeReason === "completion_check_continue" && typeof context?.completionCheckSummary === "string" && context.completionCheckSummary.trim()) {
|
|
433
|
-
lines.push(`Completion check summary: ${context.completionCheckSummary.trim()}
|
|
380
|
+
lines.push(`Completion check summary: ${context.completionCheckSummary.trim()}`);
|
|
434
381
|
}
|
|
435
382
|
if (followUpLines.length > 0) {
|
|
436
|
-
lines.push("Recent updates:");
|
|
383
|
+
lines.push("", "Recent updates:");
|
|
437
384
|
followUpLines.forEach((line) => lines.push(`- ${line}`));
|
|
438
|
-
lines.push("");
|
|
439
385
|
}
|
|
440
386
|
if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
|
|
441
|
-
lines.push("
|
|
387
|
+
lines.push("", "Current PR facts:", `Fact freshness: ${context?.githubFactsFresh === true
|
|
442
388
|
? "refreshed immediately before this turn was created."
|
|
443
389
|
: "may now be stale; refresh before making irreversible decisions."}`, issue.prNumber ? `Current PR: #${issue.prNumber}` : "", issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
|
|
444
390
|
}
|
|
445
|
-
return lines.filter(Boolean)
|
|
391
|
+
return lines.filter(Boolean);
|
|
446
392
|
}
|
|
447
|
-
function
|
|
393
|
+
function buildCurrentContext(runType, issue, context, followUp = false) {
|
|
448
394
|
const lines = [];
|
|
449
395
|
if (followUp) {
|
|
450
|
-
lines.push(
|
|
396
|
+
lines.push(...buildFollowUpContextLines(issue, runType, context), "");
|
|
451
397
|
}
|
|
398
|
+
lines.push(...buildHumanContextLines(context));
|
|
452
399
|
switch (runType) {
|
|
453
400
|
case "ci_repair":
|
|
454
401
|
lines.push(buildCiRepairContext(context));
|
|
@@ -464,74 +411,72 @@ function buildReactiveContext(runType, issue, context, followUp = false) {
|
|
|
464
411
|
break;
|
|
465
412
|
}
|
|
466
413
|
const content = lines.map((line) => line.trimEnd()).join("\n").trim();
|
|
467
|
-
|
|
414
|
+
if (!content.length)
|
|
415
|
+
return undefined;
|
|
416
|
+
return ["## Current Context", "", content].join("\n");
|
|
468
417
|
}
|
|
469
418
|
function buildWorkflowGuidance(repoPath, runType) {
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
return
|
|
473
|
-
|
|
474
|
-
|
|
419
|
+
const filename = WORKFLOW_FILES[runType];
|
|
420
|
+
if (hasWorkflowFile(repoPath, runType)) {
|
|
421
|
+
return [
|
|
422
|
+
"## Workflow",
|
|
423
|
+
"",
|
|
424
|
+
`Read and follow \`${filename}\` in the repository for task-specific behavior before making irreversible changes.`,
|
|
425
|
+
].join("\n");
|
|
475
426
|
}
|
|
476
|
-
return
|
|
427
|
+
return [
|
|
428
|
+
"## Workflow",
|
|
429
|
+
"",
|
|
430
|
+
"Use repository docs and local guidance as the source of truth for task-specific behavior.",
|
|
431
|
+
].join("\n");
|
|
477
432
|
}
|
|
478
433
|
function buildOrchestrationWorkflowGuidance() {
|
|
479
434
|
return [
|
|
480
|
-
"## Workflow
|
|
435
|
+
"## Workflow",
|
|
481
436
|
"",
|
|
482
|
-
"Use the wake reason and
|
|
483
|
-
"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.",
|
|
484
|
-
"When creating follow-up work, prefer one immediately runnable child at a time and keep the rest queued until their prerequisites are genuinely ready.",
|
|
437
|
+
"Use the wake reason and child issue summaries to decide the next orchestration step.",
|
|
485
438
|
"Keep outputs concise and observable in Linear.",
|
|
486
439
|
].join("\n");
|
|
487
440
|
}
|
|
441
|
+
function buildPrePushSelfReviewSection(target) {
|
|
442
|
+
const publishTarget = target === "new_pr"
|
|
443
|
+
? "open or update the PR"
|
|
444
|
+
: "push the existing PR branch";
|
|
445
|
+
return [
|
|
446
|
+
"## Final Self-Review Before Push",
|
|
447
|
+
"",
|
|
448
|
+
`Before you ${publishTarget}, do one brief reviewer-minded pass on the current head.`,
|
|
449
|
+
"Fix any likely in-scope blocker you can see now: missing edge-case handling, broken adjacent invariant in the touched flow, mismatch between the PR explanation and the code, or an obviously unreviewable half-finished branch.",
|
|
450
|
+
"Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.",
|
|
451
|
+
];
|
|
452
|
+
}
|
|
488
453
|
function buildPublicationContract(runType, issueClass) {
|
|
489
454
|
if (issueClass === "orchestration") {
|
|
490
455
|
return [
|
|
491
|
-
"##
|
|
456
|
+
"## Publish",
|
|
492
457
|
"",
|
|
493
|
-
"
|
|
494
|
-
"
|
|
495
|
-
"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.",
|
|
496
|
-
"If you create new blocking follow-up work, justify it against the original parent goal rather than optional polish.",
|
|
497
|
-
"Blocking follow-up children must block the parent goal they satisfy, not the other way around.",
|
|
498
|
-
"If you create new child issues, leave them in Backlog unless they are truly ready to start now.",
|
|
458
|
+
"Publish the orchestration outcome clearly: observation, follow-up issues, rollout update, closeout, or a small parent-owned cleanup PR.",
|
|
459
|
+
"Do not open an overlapping umbrella PR unless this parent owns unique direct work.",
|
|
499
460
|
].join("\n");
|
|
500
461
|
}
|
|
501
462
|
if (runType === "implementation") {
|
|
502
463
|
return [
|
|
503
|
-
"##
|
|
464
|
+
"## Publish",
|
|
504
465
|
"",
|
|
505
|
-
"
|
|
506
|
-
"If the
|
|
507
|
-
"If the worktree already contains relevant changes for this issue, verify them and publish them.",
|
|
508
|
-
"If you changed files for this issue, commit them, push the issue branch, and open or update the PR before stopping.",
|
|
509
|
-
"Do not stop with only local commits or uncommitted changes.",
|
|
466
|
+
"If this is code-delivery work, publish before stopping: commit, push the issue branch, and open or update the PR.",
|
|
467
|
+
"If the issue explicitly allows a non-PR outcome, complete that outcome clearly instead of inventing a PR.",
|
|
510
468
|
"",
|
|
511
|
-
"
|
|
512
|
-
"",
|
|
513
|
-
"When you open or update a PR, shape the body so a strict reviewer can decide in one pass.",
|
|
514
|
-
"",
|
|
515
|
-
"Title: imperative, ≤72 chars. Do not prefix with the issue key — the branch carries it.",
|
|
516
|
-
"",
|
|
517
|
-
"Body sections, in this order. Omit any that do not apply but keep the order:",
|
|
518
|
-
"",
|
|
519
|
-
" ## Why — 1-3 sentences on the problem and motivation.",
|
|
520
|
-
" ## What — ≤5 bullets naming the files or surfaces that change.",
|
|
521
|
-
" ## Tradeoffs — one explicit tradeoff taken, or the single word \"None\".",
|
|
522
|
-
" ## Risks — 1-3 things a strict reviewer would ask about. For each, either fix it before committing or explain why it is acceptable. This section is load-bearing; a strict reviewer reads it first.",
|
|
523
|
-
"",
|
|
524
|
-
"Do not restate the diff in prose. Quote the ambiguous fragment directly if the reader needs to see it.",
|
|
525
|
-
"Do not add a \"Verification\" or \"I ran these commands\" section; CI owns pass/fail and posts check runs the reviewer already sees.",
|
|
469
|
+
...buildPrePushSelfReviewSection("new_pr"),
|
|
526
470
|
].join("\n");
|
|
527
471
|
}
|
|
528
472
|
return [
|
|
529
|
-
"##
|
|
473
|
+
"## Publish",
|
|
530
474
|
"",
|
|
531
|
-
"
|
|
532
|
-
"If you changed files for this repair, commit them and push the same branch before stopping.",
|
|
475
|
+
"Restore and publish on the existing PR branch: commit and push the same branch.",
|
|
533
476
|
"Do not open a new PR.",
|
|
534
|
-
"
|
|
477
|
+
"A PR-less stop is not a successful outcome for a repair run unless a genuine external blocker prevents any correct push.",
|
|
478
|
+
"",
|
|
479
|
+
...buildPrePushSelfReviewSection("existing_pr"),
|
|
535
480
|
].join("\n");
|
|
536
481
|
}
|
|
537
482
|
function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
@@ -539,20 +484,13 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
|
539
484
|
const sections = [
|
|
540
485
|
{ id: "header", content: buildPromptHeader(issue) },
|
|
541
486
|
];
|
|
542
|
-
const
|
|
543
|
-
if (followUp && reactiveContext) {
|
|
544
|
-
sections.push({ id: "follow-up-turn", content: reactiveContext });
|
|
545
|
-
}
|
|
487
|
+
const currentContext = buildCurrentContext(runType, issue, context, followUp);
|
|
546
488
|
sections.push({ id: "task-objective", content: buildTaskObjective(issue) }, {
|
|
547
489
|
id: "scope-discipline",
|
|
548
|
-
content: issueClass === "orchestration" ?
|
|
490
|
+
content: issueClass === "orchestration" ? buildOrchestrationConstraints(context) : buildConstraints(issue, context),
|
|
549
491
|
});
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
sections.push({ id: "human-context", content: humanContext });
|
|
553
|
-
}
|
|
554
|
-
if (!followUp && reactiveContext) {
|
|
555
|
-
sections.push({ id: "reactive-context", content: reactiveContext });
|
|
492
|
+
if (currentContext) {
|
|
493
|
+
sections.push({ id: "reactive-context", content: currentContext });
|
|
556
494
|
}
|
|
557
495
|
const workflow = issueClass === "orchestration"
|
|
558
496
|
? buildOrchestrationWorkflowGuidance()
|