pr-shepherd 0.14.1 → 0.15.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/bin/checks/startup-failures.mjs +22 -0
- package/bin/checks/triage.mjs +47 -1
- package/bin/cli/fix-formatter.mjs +1 -2
- package/bin/cli/handlers.mjs +0 -1
- package/bin/cli/iterate-formatter.mjs +5 -7
- package/bin/cli/iterate-instructions.mjs +15 -16
- package/bin/cli/iterate-lean.mjs +4 -6
- package/bin/commands/check.mjs +5 -2
- package/bin/commands/iterate/check-instructions.mjs +24 -0
- package/bin/commands/iterate/render.mjs +2 -18
- package/bin/commands/monitor.mjs +21 -9
- package/bin/reporters/agent.mjs +1 -1
- package/bin/reporters/check-instructions.mjs +3 -1
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ Some other workflow improvements:
|
|
|
126
126
|
|
|
127
127
|
Recommendations:
|
|
128
128
|
|
|
129
|
-
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. In Claude Code, `/pr-shepherd:monitor` uses `/loop` and continues working when your rate limit window is reset. In Codex, keep an active goal cycling the reusable command
|
|
129
|
+
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. In Claude Code, `/pr-shepherd:monitor` uses `/loop` and continues working when your rate limit window is reset. In Codex, keep an active goal cycling the reusable command with a fresh 1-4 minute sleep before each rerun until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
|
|
130
130
|
- Instruct your agents to write comments in a single review (comment, changes requested, or approved). This allows the review's comments/threads to be minimized or resolved together, keeping your pull request history clean. If you write inline comments outside of a review, each comment would still show up in the pull request history and take up space.
|
|
131
131
|
- Avoid sticky comments as they will continue to be hidden. Instead, just make a new comment, especially on reviews. If you really want sticky comments, instruct your agent to unhide/unminimize them when updating them.
|
|
132
132
|
- Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
|
|
@@ -279,7 +279,7 @@ Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until
|
|
|
279
279
|
<runner> pr-shepherd 42
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
For an active Codex goal, rerun that command
|
|
282
|
+
For an active Codex goal, rerun that command after picking a fresh 1-4 minute sleep until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows. There is no background `/loop` scheduler in Codex.
|
|
283
283
|
|
|
284
284
|
### Without the plugin
|
|
285
285
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function mergeStartupFailureChecks(checks, startupFailureChecks) {
|
|
2
|
+
const byRunId = new Map();
|
|
3
|
+
checks.forEach((check, index) => {
|
|
4
|
+
if (check.runId === null)
|
|
5
|
+
return;
|
|
6
|
+
const indices = byRunId.get(check.runId) ?? [];
|
|
7
|
+
indices.push(index);
|
|
8
|
+
byRunId.set(check.runId, indices);
|
|
9
|
+
});
|
|
10
|
+
const merged = [...checks];
|
|
11
|
+
const removed = new Set();
|
|
12
|
+
for (const startupFailure of startupFailureChecks) {
|
|
13
|
+
const indices = startupFailure.runId === null ? undefined : byRunId.get(startupFailure.runId);
|
|
14
|
+
if (indices === undefined) {
|
|
15
|
+
merged.push(startupFailure);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
merged[indices[0]] = startupFailure;
|
|
19
|
+
indices.slice(1).forEach((index) => removed.add(index));
|
|
20
|
+
}
|
|
21
|
+
return merged.filter((_, index) => !removed.has(index));
|
|
22
|
+
}
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { rest } from "../github/http.mjs";
|
|
2
|
+
const STARTUP_FAILURE_STATUS = "startup_failure";
|
|
2
3
|
export function triageFailingChecks(failingChecks, repo) {
|
|
3
4
|
const jobsCache = new Map();
|
|
4
5
|
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
|
|
5
6
|
}
|
|
6
7
|
async function triageCheck(check, repo, jobsCache) {
|
|
7
|
-
if (check.runId === null ||
|
|
8
|
+
if (check.runId === null ||
|
|
9
|
+
check.conclusion === "CANCELLED" ||
|
|
10
|
+
check.conclusion === "STARTUP_FAILURE") {
|
|
8
11
|
return { ...check };
|
|
9
12
|
}
|
|
10
13
|
const jobs = await fetchJobs(check.runId, repo, jobsCache);
|
|
@@ -16,6 +19,49 @@ async function triageCheck(check, repo, jobsCache) {
|
|
|
16
19
|
...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
|
|
17
20
|
};
|
|
18
21
|
}
|
|
22
|
+
export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
|
|
23
|
+
try {
|
|
24
|
+
return await fetchStartupFailureChecksUncached(repo, headSha, prNumber);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28
|
+
process.stderr.write(`pr-shepherd: startup-failure run fetch failed for PR #${prNumber} at ${headSha} (ignored): ${msg}\n`);
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function fetchStartupFailureChecksUncached(repo, headSha, prNumber) {
|
|
33
|
+
const { owner, name } = repo;
|
|
34
|
+
const perPage = 100;
|
|
35
|
+
const MAX_RUN_PAGES = 10;
|
|
36
|
+
const checks = [];
|
|
37
|
+
for (let page = 1; page <= MAX_RUN_PAGES; page++) {
|
|
38
|
+
const data = await rest("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`);
|
|
39
|
+
checks.push(...data.workflow_runs
|
|
40
|
+
.filter((run) => runBelongsToPr(run, prNumber, headSha))
|
|
41
|
+
.map(workflowRunToCheckRun));
|
|
42
|
+
if (data.workflow_runs.length < perPage)
|
|
43
|
+
break;
|
|
44
|
+
if (page === MAX_RUN_PAGES) {
|
|
45
|
+
process.stderr.write(`pr-shepherd: startup-failure run pagination cap (${MAX_RUN_PAGES * perPage} runs) reached for ${headSha} — startup-failure detection may be incomplete\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return checks;
|
|
49
|
+
}
|
|
50
|
+
function workflowRunToCheckRun(run) {
|
|
51
|
+
const summary = run.display_title?.trim() || undefined;
|
|
52
|
+
return {
|
|
53
|
+
name: run.name?.trim() || `workflow run ${run.id}`,
|
|
54
|
+
status: "COMPLETED",
|
|
55
|
+
conclusion: "STARTUP_FAILURE",
|
|
56
|
+
detailsUrl: run.html_url,
|
|
57
|
+
event: run.event,
|
|
58
|
+
runId: String(run.id),
|
|
59
|
+
...(summary !== undefined && { summary }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function runBelongsToPr(run, prNumber, headSha) {
|
|
63
|
+
return (run.pull_requests ?? []).some((pr) => pr.number === prNumber && (pr.head?.sha ?? headSha) === headSha);
|
|
64
|
+
}
|
|
19
65
|
function fetchJobs(runId, repo, cache) {
|
|
20
66
|
const cached = cache.get(runId);
|
|
21
67
|
if (cached)
|
|
@@ -6,7 +6,6 @@ import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instruct
|
|
|
6
6
|
export function formatFixCodeResult(header, result, opts) {
|
|
7
7
|
const runtime = opts?.runtime ?? "claude";
|
|
8
8
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
9
|
-
const retryInterval = opts?.retryInterval;
|
|
10
9
|
const runner = opts?.runner;
|
|
11
10
|
const sections = [header];
|
|
12
11
|
if (result.fix.threads.length > 0) {
|
|
@@ -118,7 +117,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
118
117
|
}
|
|
119
118
|
sections.push(postFixLines.join("\n"));
|
|
120
119
|
sections.push("## Instructions");
|
|
121
|
-
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix,
|
|
120
|
+
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, runner)));
|
|
122
121
|
return joinSections(sections);
|
|
123
122
|
}
|
|
124
123
|
function blockquote(body) {
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -20,7 +20,6 @@ export function formatIterateResult(result, opts) {
|
|
|
20
20
|
const verbose = opts?.verbose ?? false;
|
|
21
21
|
const runtime = opts?.runtime ?? "claude";
|
|
22
22
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
23
|
-
const retryInterval = opts?.retryInterval;
|
|
24
23
|
const runner = opts?.runner;
|
|
25
24
|
const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
|
|
26
25
|
const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
|
|
@@ -57,37 +56,36 @@ export function formatIterateResult(result, opts) {
|
|
|
57
56
|
return joinSections([
|
|
58
57
|
verbose ? header : heading,
|
|
59
58
|
adaptIterateLog(result.log, runtime),
|
|
60
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix,
|
|
59
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner))}`,
|
|
61
60
|
]);
|
|
62
61
|
case "wait":
|
|
63
62
|
return joinSections([
|
|
64
63
|
header,
|
|
65
64
|
adaptIterateLog(result.log, runtime),
|
|
66
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix,
|
|
65
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner))}`,
|
|
67
66
|
]);
|
|
68
67
|
case "mark_ready":
|
|
69
68
|
return joinSections([
|
|
70
69
|
header,
|
|
71
70
|
adaptIterateLog(result.log, runtime),
|
|
72
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix,
|
|
71
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner))}`,
|
|
73
72
|
]);
|
|
74
73
|
case "cancel":
|
|
75
74
|
return joinSections([
|
|
76
75
|
[`${heading} — ${result.reason}`, "", baseLine, summaryLine].join("\n"),
|
|
77
76
|
adaptIterateLog(result.log, runtime),
|
|
78
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix,
|
|
77
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner))}`,
|
|
79
78
|
]);
|
|
80
79
|
case "escalate":
|
|
81
80
|
return joinSections([
|
|
82
81
|
header,
|
|
83
82
|
result.escalate.humanMessage,
|
|
84
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix,
|
|
83
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner))}`,
|
|
85
84
|
]);
|
|
86
85
|
case "fix_code":
|
|
87
86
|
return formatFixCodeResult(header, result, {
|
|
88
87
|
runtime,
|
|
89
88
|
readyDelaySuffix,
|
|
90
|
-
retryInterval,
|
|
91
89
|
runner,
|
|
92
90
|
});
|
|
93
91
|
}
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import { FIX_INSTRUCTION_END_ITERATION, FIX_INSTRUCTION_STOP_AFTER_PUSH, FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK, } from "../commands/iterate/render.mjs";
|
|
2
2
|
import { buildPrShepherdCommand } from "./runner.mjs";
|
|
3
|
-
const DEFAULT_CODEX_RETRY_INTERVAL = "4m";
|
|
4
|
-
const VALID_RETRY_INTERVAL = /^\d+[smhd]$/;
|
|
5
3
|
const CLAUDE_DYNAMIC_WAKEUP = "Schedule the next dynamic wakeup with `ScheduleWakeup` using `delaySeconds` between 60 and 240 and the same loop prompt (skip if running under a fixed-interval cron loop — the next scheduled fire handles it), then end this iteration.";
|
|
6
|
-
|
|
4
|
+
const CODEX_DYNAMIC_WAIT = "pick a fresh sleep/timeout between 1 and 4 minutes, wait that long";
|
|
5
|
+
function capitalizeFirst(value) {
|
|
6
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
7
|
+
}
|
|
8
|
+
export function buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner) {
|
|
7
9
|
const rerunCommand = buildCodexIterateCommand(result.pr, readyDelaySuffix, runner);
|
|
8
|
-
const retryDelay = buildCodexRetryDelay(
|
|
10
|
+
const retryDelay = buildCodexRetryDelay();
|
|
9
11
|
switch (result.action) {
|
|
10
12
|
case "cooldown":
|
|
11
13
|
return [
|
|
12
14
|
runtime === "codex"
|
|
13
|
-
? `Continue the active Codex goal —
|
|
15
|
+
? `Continue the active Codex goal — ${retryDelay}, then rerun \`${rerunCommand}\` after CI starts reporting.`
|
|
14
16
|
: `CI still needs time to start reporting. ${CLAUDE_DYNAMIC_WAKEUP}`,
|
|
15
17
|
];
|
|
16
18
|
case "wait":
|
|
17
19
|
return [
|
|
18
20
|
runtime === "codex"
|
|
19
|
-
? `Continue the active Codex goal —
|
|
21
|
+
? `Continue the active Codex goal — ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`
|
|
20
22
|
: CLAUDE_DYNAMIC_WAKEUP,
|
|
21
23
|
];
|
|
22
24
|
case "mark_ready":
|
|
23
25
|
return [
|
|
24
26
|
runtime === "codex"
|
|
25
|
-
? `The CLI already marked the PR ready for review. Continue the active Codex goal until the ready-delay completes —
|
|
27
|
+
? `The CLI already marked the PR ready for review. Continue the active Codex goal until the ready-delay completes — ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`
|
|
26
28
|
: `The CLI already marked the PR ready for review. ${CLAUDE_DYNAMIC_WAKEUP}`,
|
|
27
29
|
];
|
|
28
30
|
case "cancel":
|
|
@@ -41,18 +43,18 @@ export function buildSimpleIterateInstructions(result, runtime, readyDelaySuffix
|
|
|
41
43
|
];
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
export function adaptFixCodeInstructions(instructions, pr, runtime, readyDelaySuffix,
|
|
46
|
+
export function adaptFixCodeInstructions(instructions, pr, runtime, readyDelaySuffix, runner) {
|
|
45
47
|
if (runtime !== "codex")
|
|
46
48
|
return instructions;
|
|
47
49
|
const rerunCommand = buildCodexIterateCommand(pr, readyDelaySuffix, runner);
|
|
48
|
-
const retryDelay = buildCodexRetryDelay(
|
|
50
|
+
const retryDelay = buildCodexRetryDelay();
|
|
49
51
|
return instructions.map((instruction) => {
|
|
50
52
|
if (instruction === FIX_INSTRUCTION_STOP_AFTER_PUSH) {
|
|
51
|
-
return `Continue the active Codex goal — CI needs time to run on the new push.
|
|
53
|
+
return `Continue the active Codex goal — CI needs time to run on the new push. ${capitalizeFirst(retryDelay)}, then rerun \`${rerunCommand}\` to recheck.`;
|
|
52
54
|
}
|
|
53
55
|
if (instruction === FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK ||
|
|
54
56
|
instruction === FIX_INSTRUCTION_END_ITERATION) {
|
|
55
|
-
return `Continue the active Codex goal —
|
|
57
|
+
return `Continue the active Codex goal — ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`;
|
|
56
58
|
}
|
|
57
59
|
return instruction;
|
|
58
60
|
});
|
|
@@ -68,11 +70,8 @@ export function buildCodexIterateCommand(pr, readyDelaySuffix, runner) {
|
|
|
68
70
|
runner,
|
|
69
71
|
}).text;
|
|
70
72
|
}
|
|
71
|
-
export function buildCodexRetryDelay(
|
|
72
|
-
|
|
73
|
-
return VALID_RETRY_INTERVAL.test(interval)
|
|
74
|
-
? `the configured interval (${interval})`
|
|
75
|
-
: `the configured interval (default ${DEFAULT_CODEX_RETRY_INTERVAL})`;
|
|
73
|
+
export function buildCodexRetryDelay() {
|
|
74
|
+
return CODEX_DYNAMIC_WAIT;
|
|
76
75
|
}
|
|
77
76
|
export function numberInstructions(instructions) {
|
|
78
77
|
return instructions.map((instruction, i) => `${i + 1}. ${instruction}`).join("\n");
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -7,9 +7,8 @@ import { adaptIterateLog, adaptFixCodeInstructions, buildSimpleIterateInstructio
|
|
|
7
7
|
export function projectIterateLean(result, opts) {
|
|
8
8
|
const runtime = opts?.runtime ?? "claude";
|
|
9
9
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
10
|
-
const retryInterval = opts?.retryInterval;
|
|
11
10
|
const runner = opts?.runner;
|
|
12
|
-
const simpleInstructions = (r) => buildSimpleIterateInstructions(r, runtime, readyDelaySuffix,
|
|
11
|
+
const simpleInstructions = (r) => buildSimpleIterateInstructions(r, runtime, readyDelaySuffix, runner);
|
|
13
12
|
const base = {
|
|
14
13
|
action: result.action,
|
|
15
14
|
pr: result.pr,
|
|
@@ -102,7 +101,7 @@ export function projectIterateLean(result, opts) {
|
|
|
102
101
|
}),
|
|
103
102
|
resolveCommand: result.fix.resolveCommand,
|
|
104
103
|
...(result.fix.instructions.length > 0 && {
|
|
105
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix,
|
|
104
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, runner),
|
|
106
105
|
}),
|
|
107
106
|
},
|
|
108
107
|
};
|
|
@@ -134,14 +133,13 @@ export function projectIterateLean(result, opts) {
|
|
|
134
133
|
export function projectIterateVerbose(result, opts) {
|
|
135
134
|
const runtime = opts?.runtime ?? "claude";
|
|
136
135
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
137
|
-
const retryInterval = opts?.retryInterval;
|
|
138
136
|
const runner = opts?.runner;
|
|
139
137
|
if (result.action === "fix_code") {
|
|
140
138
|
return {
|
|
141
139
|
...result,
|
|
142
140
|
fix: {
|
|
143
141
|
...result.fix,
|
|
144
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix,
|
|
142
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, runner),
|
|
145
143
|
},
|
|
146
144
|
};
|
|
147
145
|
}
|
|
@@ -151,6 +149,6 @@ export function projectIterateVerbose(result, opts) {
|
|
|
151
149
|
return {
|
|
152
150
|
...result,
|
|
153
151
|
...log,
|
|
154
|
-
instructions: buildSimpleIterateInstructions(result, runtime, readyDelaySuffix,
|
|
152
|
+
instructions: buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner),
|
|
155
153
|
};
|
|
156
154
|
}
|
package/bin/commands/check.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
2
2
|
import { getRepoInfo, getCurrentPrNumber, getMergeableState } from "../github/client.mjs";
|
|
3
3
|
import { classifyChecks, getCiVerdict } from "../checks/classify.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
5
|
+
import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
|
|
5
6
|
import { getOutdatedThreads } from "../comments/outdated.mjs";
|
|
6
7
|
import { autoResolveOutdated } from "../comments/resolve.mjs";
|
|
7
8
|
import { deriveMergeStatus } from "../merge-status/derive.mjs";
|
|
@@ -28,7 +29,9 @@ export async function runCheck(opts) {
|
|
|
28
29
|
mergeStateStatus: restState.mergeStateStatus ?? batchData.mergeStateStatus,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
|
-
const
|
|
32
|
+
const startupFailureChecks = await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
|
|
33
|
+
const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
|
|
34
|
+
const classifiedChecks = classifyChecks(allChecks);
|
|
32
35
|
const verdict = getCiVerdict(classifiedChecks);
|
|
33
36
|
const passing = classifiedChecks.filter((c) => c.category === "passed");
|
|
34
37
|
const failing = classifiedChecks.filter((c) => c.category === "failing");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function buildFailingCheckInstructions(checks) {
|
|
2
|
+
const instructions = [];
|
|
3
|
+
const failedRunIdChecks = checks.filter((c) => c.runId && c.conclusion !== "CANCELLED" && c.conclusion !== "STARTUP_FAILURE");
|
|
4
|
+
const cancelledRunIdChecks = checks.filter((c) => c.runId && c.conclusion === "CANCELLED");
|
|
5
|
+
const startupFailureRunIdChecks = checks.filter((c) => c.runId && c.conclusion === "STARTUP_FAILURE");
|
|
6
|
+
const externalChecks = checks.filter((c) => !c.runId && c.detailsUrl);
|
|
7
|
+
const bareChecks = checks.filter((c) => !c.runId && !c.detailsUrl);
|
|
8
|
+
if (failedRunIdChecks.length > 0) {
|
|
9
|
+
instructions.push(`For each failing check under \`## Failing checks\` with a run ID and no \`[conclusion: CANCELLED]\` or \`[conclusion: STARTUP_FAILURE]\` tag: run \`gh run view <runId> --log-failed\` to fetch the failing job's log.`, `If the log shows a transient infrastructure failure (network timeout, runner setup crash, OOM kill): run \`gh run rerun <runId> --failed\`.`, `If the log shows a real test/build failure: apply a code fix.`);
|
|
10
|
+
}
|
|
11
|
+
if (cancelledRunIdChecks.length > 0) {
|
|
12
|
+
instructions.push(`For each \`[conclusion: CANCELLED]\` bullet under \`## Failing checks\`: the run was cancelled outside Shepherd's control (manual cancel, newer push, concurrency-group eviction). Run \`gh run rerun <runId>\` only if the cancellation looks unintended; otherwise treat it as resolved by the superseding run. Do NOT confuse these with IDs under \`## Cancelled runs\` — those were cancelled by Shepherd itself.`);
|
|
13
|
+
}
|
|
14
|
+
if (startupFailureRunIdChecks.length > 0) {
|
|
15
|
+
instructions.push(`For each \`[conclusion: STARTUP_FAILURE]\` bullet under \`## Failing checks\`: the workflow failed before jobs/logs were created. Run \`gh run view <runId>\` to inspect the run metadata, then run \`gh run rerun <runId>\` if the workflow should be attempted again.`);
|
|
16
|
+
}
|
|
17
|
+
if (externalChecks.length > 0) {
|
|
18
|
+
instructions.push(`For each bullet in \`## Failing checks\` starting with \`external\` (external status check): open the linked URL in a browser to inspect the failure — log tails are not available for external checks.`);
|
|
19
|
+
}
|
|
20
|
+
if (bareChecks.length > 0) {
|
|
21
|
+
instructions.push(`For each bullet in \`## Failing checks\` starting with \`(no runId)\`: there is no run or details URL to inspect. Escalate these to a human — they require manual investigation outside the pr-shepherd flow.`);
|
|
22
|
+
}
|
|
23
|
+
return instructions;
|
|
24
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildPrShepherdCommand, renderShellCommand } from "../../cli/runner.mjs";
|
|
2
|
+
import { buildFailingCheckInstructions } from "./check-instructions.mjs";
|
|
2
3
|
export const FIX_INSTRUCTION_STOP_AFTER_PUSH = "Stop this iteration — CI needs time to run on the new push before the next tick.";
|
|
3
4
|
export const FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK = "Stop this iteration before the next tick.";
|
|
4
5
|
export const FIX_INSTRUCTION_END_ITERATION = "End this iteration.";
|
|
@@ -41,24 +42,7 @@ export function buildFixInstructions(threads, actionableComments, checks, review
|
|
|
41
42
|
if (resolutionOnlyThreads.length > 0) {
|
|
42
43
|
instructions.push(`Resolve the threads under \`## Review threads to resolve\` with the \`resolve:\` command shown below. These threads are already outdated or minimized, so no code edit is required for them unless their body reveals separate work you choose to do.`);
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
const failedRunIdChecks = checks.filter((c) => c.runId && c.conclusion !== "CANCELLED");
|
|
46
|
-
const externalChecks = checks.filter((c) => !c.runId && c.detailsUrl);
|
|
47
|
-
const bareChecks = checks.filter((c) => !c.runId && !c.detailsUrl);
|
|
48
|
-
if (failedRunIdChecks.length > 0) {
|
|
49
|
-
instructions.push(`For each failing check under \`## Failing checks\` with a run ID and no \`[conclusion: CANCELLED]\` tag: run \`gh run view <runId> --log-failed\` to fetch the failing job's log.`);
|
|
50
|
-
instructions.push(`If the log shows a transient infrastructure failure (network timeout, runner setup crash, OOM kill): run \`gh run rerun <runId> --failed\`.`);
|
|
51
|
-
instructions.push(`If the log shows a real test/build failure: apply a code fix.`);
|
|
52
|
-
}
|
|
53
|
-
if (cancelledRunIdChecks.length > 0) {
|
|
54
|
-
instructions.push(`For each \`[conclusion: CANCELLED]\` bullet under \`## Failing checks\`: the run was cancelled outside Shepherd's control (manual cancel, newer push, concurrency-group eviction). Run \`gh run rerun <runId>\` only if the cancellation looks unintended; otherwise treat it as resolved by the superseding run. Do NOT confuse these with IDs under \`## Cancelled runs\` — those were cancelled by Shepherd itself.`);
|
|
55
|
-
}
|
|
56
|
-
if (externalChecks.length > 0) {
|
|
57
|
-
instructions.push(`For each bullet in \`## Failing checks\` starting with \`external\` (external status check): open the linked URL in a browser to inspect the failure — log tails are not available for external checks.`);
|
|
58
|
-
}
|
|
59
|
-
if (bareChecks.length > 0) {
|
|
60
|
-
instructions.push(`For each bullet in \`## Failing checks\` starting with \`(no runId)\`: there is no run or details URL to inspect. Escalate these to a human — they require manual investigation outside the pr-shepherd flow.`);
|
|
61
|
-
}
|
|
45
|
+
instructions.push(...buildFailingCheckInstructions(checks));
|
|
62
46
|
if (reviews.length > 0) {
|
|
63
47
|
instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes.`);
|
|
64
48
|
}
|
package/bin/commands/monitor.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export async function runMonitor(opts) {
|
|
|
20
20
|
const loopTag = `#pr-shepherd-loop:pr=${prNumber}:`;
|
|
21
21
|
const loopArgs = interval;
|
|
22
22
|
const reusableCommand = buildIterateCommand(prNumber, opts.readyDelaySuffix, config.cli?.runner);
|
|
23
|
-
const loopPrompt = buildLoopPrompt(prNumber, loopTag, reusableCommand,
|
|
23
|
+
const loopPrompt = buildLoopPrompt(prNumber, loopTag, reusableCommand, opts.runtime ?? "claude", config.cli?.runner);
|
|
24
24
|
return {
|
|
25
25
|
prNumber,
|
|
26
26
|
loopTag,
|
|
@@ -40,8 +40,10 @@ export function formatMonitorResult(result, opts) {
|
|
|
40
40
|
`# PR #${prNumber} [MONITOR]`,
|
|
41
41
|
"",
|
|
42
42
|
`Loop tag: \`${loopTag}\``,
|
|
43
|
-
`Loop args: \`${loopArgs}\``,
|
|
44
|
-
]
|
|
43
|
+
runtime === "codex" ? null : `Loop args: \`${loopArgs}\``,
|
|
44
|
+
]
|
|
45
|
+
.filter((line) => line !== null)
|
|
46
|
+
.join("\n"),
|
|
45
47
|
runtime === "codex" ? `Reusable command: \`${result.reusableCommand}\`` : null,
|
|
46
48
|
"## Loop prompt",
|
|
47
49
|
loopPrompt,
|
|
@@ -54,10 +56,20 @@ export function formatMonitorResult(result, opts) {
|
|
|
54
56
|
}
|
|
55
57
|
export function formatMonitorJson(result, opts) {
|
|
56
58
|
const runtime = opts?.runtime ?? "claude";
|
|
57
|
-
|
|
59
|
+
if (runtime === "codex") {
|
|
60
|
+
return {
|
|
61
|
+
prNumber: result.prNumber,
|
|
62
|
+
loopTag: result.loopTag,
|
|
63
|
+
loopPrompt: result.loopPrompt,
|
|
64
|
+
reusableCommand: result.reusableCommand,
|
|
65
|
+
instructions: buildMonitorInstructions(result, runtime),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
58
68
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
prNumber: result.prNumber,
|
|
70
|
+
loopTag: result.loopTag,
|
|
71
|
+
loopArgs: result.loopArgs,
|
|
72
|
+
loopPrompt: result.loopPrompt,
|
|
61
73
|
instructions: buildMonitorInstructions(result, runtime),
|
|
62
74
|
};
|
|
63
75
|
}
|
|
@@ -77,14 +89,14 @@ function buildIterateCommand(prNumber, readyDelaySuffix, runner) {
|
|
|
77
89
|
const validatedDelay = validateReadyDelaySuffix(readyDelaySuffix);
|
|
78
90
|
return buildPrShepherdCommand([String(prNumber), ...(validatedDelay ? ["--ready-delay", validatedDelay] : [])], { runner }).text;
|
|
79
91
|
}
|
|
80
|
-
function buildLoopPrompt(prNumber, loopTag, iterateCmd,
|
|
92
|
+
function buildLoopPrompt(prNumber, loopTag, iterateCmd, runtime = "claude", runner) {
|
|
81
93
|
if (runtime === "codex") {
|
|
82
94
|
return [
|
|
83
95
|
loopTag,
|
|
84
96
|
"",
|
|
85
97
|
"**IMPORTANT — Codex recurrence rules:**",
|
|
86
98
|
"- Run the command below once and follow its `## Instructions` exactly.",
|
|
87
|
-
|
|
99
|
+
"- If the output tells you to continue the active Codex goal, pick a fresh sleep/timeout between 1 and 4 minutes, wait that long, and rerun the reusable command from the monitor output.",
|
|
88
100
|
"- Stop only when Shepherd emits `[CANCEL]` because the ready-delay completed or the PR was merged/closed, or when Shepherd emits `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).",
|
|
89
101
|
`- Do not call \`/loop\`, \`ScheduleWakeup\`, \`CronCreate\`, or \`${buildPrShepherdCommand(["monitor", String(prNumber)], { runner }).text}\`; Codex recurrence is explicit \`iterate\` command cycles.`,
|
|
90
102
|
"",
|
|
@@ -117,7 +129,7 @@ function buildMonitorInstructions(result, runtime) {
|
|
|
117
129
|
if (runtime === "codex") {
|
|
118
130
|
return [
|
|
119
131
|
"Run the `## Loop prompt` body once inline now.",
|
|
120
|
-
`For an active Codex goal, keep cycling with \`${result.reusableCommand}\`
|
|
132
|
+
`For an active Codex goal, keep cycling with \`${result.reusableCommand}\` by picking a fresh sleep/timeout between 1 and 4 minutes before each rerun until a terminal condition is reached. Codex does not create a \`/loop\` monitor.`,
|
|
121
133
|
];
|
|
122
134
|
}
|
|
123
135
|
return [
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* These strip fields that are always-false by the time items reach iterate
|
|
5
5
|
* (isResolved, isOutdated, isMinimized, createdAtUnix) and check metadata the
|
|
6
6
|
* monitor prompt never reads (event, status, category).
|
|
7
|
-
* conclusion is preserved on AgentCheck so the formatter can branch on
|
|
7
|
+
* conclusion is preserved on AgentCheck so the formatter can branch on run-level conclusions.
|
|
8
8
|
* detailsUrl is preserved in AgentCheck as a fallback for external status checks.
|
|
9
9
|
* The original domain types are preserved for check command output.
|
|
10
10
|
*/
|
|
@@ -35,7 +35,9 @@ export function buildCheckInstructions(report, opts) {
|
|
|
35
35
|
const diagnosisHint = c.runId
|
|
36
36
|
? c.conclusion === "CANCELLED"
|
|
37
37
|
? `cancelled — if unintended, rerun with \`gh run rerun ${c.runId}\``
|
|
38
|
-
:
|
|
38
|
+
: c.conclusion === "STARTUP_FAILURE"
|
|
39
|
+
? `startup failure — run \`gh run view ${c.runId}\` to inspect the workflow run; rerun with \`gh run rerun ${c.runId}\` if appropriate`
|
|
40
|
+
: `run \`gh run view ${c.runId} --log-failed\`${stepHint} to diagnose — if transient, rerun with \`gh run rerun ${c.runId} --failed\`; otherwise apply a fix`
|
|
39
41
|
: c.detailsUrl
|
|
40
42
|
? `open the check details (${c.detailsUrl}) to diagnose the failure`
|
|
41
43
|
: `no run or details URL available — escalate to a human`;
|
package/package.json
CHANGED