@valescoagency/runway 0.14.3 → 0.15.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 +8 -1
- package/dist/config.js +22 -0
- package/dist/dashboard/projector.js +90 -0
- package/dist/dashboard/server.js +51 -2
- package/dist/dashboard/storage.js +121 -0
- package/dist/dashboard/views.js +77 -1
- package/dist/github.js +350 -0
- package/dist/orchestrator.js +44 -0
- package/dist/prompts.js +99 -0
- package/dist/shepherd.js +707 -0
- package/package.json +1 -1
- package/prompts/shepherd-ci-fix.md +47 -0
- package/prompts/shepherd-rebase.md +40 -0
- package/prompts/shepherd-review-fix.md +40 -0
- package/prompts/shepherd-review-respond.md +31 -0
package/dist/github.js
CHANGED
|
@@ -18,6 +18,12 @@ export class PrCreateFailed extends Data.TaggedError("PrCreateFailed") {
|
|
|
18
18
|
// be running. We accept that limitation for Step 2.
|
|
19
19
|
export class GithubTimeout extends Data.TaggedError("GithubTimeout") {
|
|
20
20
|
}
|
|
21
|
+
// VA-461: read failures (gh pr view / git merge-tree). Kept distinct
|
|
22
|
+
// from PushFailed / PrCreateFailed because the recovery is different
|
|
23
|
+
// — a read failure on the shepherd's poll is recoverable (try again
|
|
24
|
+
// next tick); a push or pr-create failure interrupts the workflow.
|
|
25
|
+
export class GithubReadFailed extends Data.TaggedError("GithubReadFailed") {
|
|
26
|
+
}
|
|
21
27
|
// VA-357: same jittered exponential shape as the Linear policy.
|
|
22
28
|
export const githubRetrySchedule = Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(5)), Schedule.jittered);
|
|
23
29
|
/**
|
|
@@ -145,5 +151,349 @@ export function createGithubGateway() {
|
|
|
145
151
|
// and a retry would create a duplicate.
|
|
146
152
|
});
|
|
147
153
|
},
|
|
154
|
+
getPullRequest({ repoPath, prNumber }) {
|
|
155
|
+
return applyGithubPolicy(Effect.tryPromise({
|
|
156
|
+
try: async () => {
|
|
157
|
+
const { stdout } = await execa("gh", [
|
|
158
|
+
"pr",
|
|
159
|
+
"view",
|
|
160
|
+
String(prNumber),
|
|
161
|
+
"--json",
|
|
162
|
+
"number,mergeable,mergeStateStatus,merged,headRefOid,headRefName,baseRefName",
|
|
163
|
+
], { cwd: repoPath });
|
|
164
|
+
const parsed = JSON.parse(stdout);
|
|
165
|
+
// `mergeStateStatus` is the v4 enum runway maps to its
|
|
166
|
+
// own `MergeableState`. gh returns it ALL_CAPS; runway
|
|
167
|
+
// canonicalises to lowercase. Anything we don't
|
|
168
|
+
// recognise becomes `unknown` rather than throwing — the
|
|
169
|
+
// shepherd loop treats unknown as quiet, which is the
|
|
170
|
+
// safe fallback.
|
|
171
|
+
const mergeableState = canonicaliseMergeableState(parsed.mergeStateStatus);
|
|
172
|
+
return {
|
|
173
|
+
number: parsed.number,
|
|
174
|
+
mergeableState,
|
|
175
|
+
merged: parsed.merged,
|
|
176
|
+
headSha: parsed.headRefOid,
|
|
177
|
+
headRefName: parsed.headRefName,
|
|
178
|
+
baseRefName: parsed.baseRefName,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
catch: (err) => {
|
|
182
|
+
if (isCommandMissing(err)) {
|
|
183
|
+
return new GhCliMissing({
|
|
184
|
+
message: `gh CLI not found on PATH (${err instanceof Error ? err.message : String(err)})`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return new GithubReadFailed({
|
|
188
|
+
call: `getPullRequest(${prNumber})`,
|
|
189
|
+
stderr: execaStderr(err),
|
|
190
|
+
message: err instanceof Error
|
|
191
|
+
? err.message
|
|
192
|
+
: `gh pr view failed: ${String(err)}`,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
}), {
|
|
196
|
+
call: `getPullRequest(${prNumber})`,
|
|
197
|
+
timeoutMs: 15_000,
|
|
198
|
+
retryOn: (err) => err._tag === "GithubTimeout",
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
getCheckRuns({ repoPath, sha }) {
|
|
202
|
+
return applyGithubPolicy(Effect.tryPromise({
|
|
203
|
+
try: async () => {
|
|
204
|
+
const { stdout } = await execa("gh", [
|
|
205
|
+
"api",
|
|
206
|
+
`repos/{owner}/{repo}/commits/${sha}/check-runs`,
|
|
207
|
+
"--paginate",
|
|
208
|
+
"--jq",
|
|
209
|
+
".check_runs[] | {id, name, status, conclusion, run_id: (.details_url | capture(\"/runs/(?<id>[0-9]+)\") | .id | tonumber? // null)}",
|
|
210
|
+
], { cwd: repoPath });
|
|
211
|
+
// `--jq` with the streaming form returns one JSON object
|
|
212
|
+
// per line. Empty stdout → no check runs yet.
|
|
213
|
+
const lines = stdout.split("\n").filter((s) => s.trim().length > 0);
|
|
214
|
+
return lines.map((line) => {
|
|
215
|
+
const o = JSON.parse(line);
|
|
216
|
+
return {
|
|
217
|
+
id: o.id,
|
|
218
|
+
name: o.name,
|
|
219
|
+
status: canonicaliseCheckStatus(o.status),
|
|
220
|
+
conclusion: canonicaliseCheckConclusion(o.conclusion),
|
|
221
|
+
runId: typeof o.run_id === "number" ? o.run_id : null,
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
catch: (err) => {
|
|
226
|
+
if (isCommandMissing(err)) {
|
|
227
|
+
return new GhCliMissing({
|
|
228
|
+
message: `gh CLI not found on PATH (${err instanceof Error ? err.message : String(err)})`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return new GithubReadFailed({
|
|
232
|
+
call: `getCheckRuns(${sha})`,
|
|
233
|
+
stderr: execaStderr(err),
|
|
234
|
+
message: err instanceof Error
|
|
235
|
+
? err.message
|
|
236
|
+
: `gh api check-runs failed: ${String(err)}`,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
}), {
|
|
240
|
+
call: `getCheckRuns(${sha})`,
|
|
241
|
+
timeoutMs: 15_000,
|
|
242
|
+
retryOn: (err) => err._tag === "GithubTimeout",
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
getRequiredChecks({ repoPath, baseBranch }) {
|
|
246
|
+
return applyGithubPolicy(Effect.tryPromise({
|
|
247
|
+
try: async () => {
|
|
248
|
+
try {
|
|
249
|
+
const { stdout } = await execa("gh", [
|
|
250
|
+
"api",
|
|
251
|
+
`repos/{owner}/{repo}/branches/${baseBranch}/protection/required_status_checks`,
|
|
252
|
+
"--jq",
|
|
253
|
+
".contexts // []",
|
|
254
|
+
], { cwd: repoPath });
|
|
255
|
+
const trimmed = stdout.trim();
|
|
256
|
+
if (!trimmed)
|
|
257
|
+
return null;
|
|
258
|
+
const parsed = JSON.parse(trimmed);
|
|
259
|
+
return parsed;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
// 404 (branch not protected) and 403 (no admin) are
|
|
263
|
+
// both expected — fall back to "filter unknown" by
|
|
264
|
+
// returning null. The watcher treats null as
|
|
265
|
+
// "conservatively require all checks" so a missing
|
|
266
|
+
// protection rule can't silently hide failures.
|
|
267
|
+
const stderr = execaStderr(err);
|
|
268
|
+
if (stderr.includes("404") ||
|
|
269
|
+
stderr.includes("403") ||
|
|
270
|
+
stderr.includes("Branch not protected")) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
catch: (err) => {
|
|
277
|
+
if (isCommandMissing(err)) {
|
|
278
|
+
return new GhCliMissing({
|
|
279
|
+
message: `gh CLI not found on PATH (${err instanceof Error ? err.message : String(err)})`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return new GithubReadFailed({
|
|
283
|
+
call: `getRequiredChecks(${baseBranch})`,
|
|
284
|
+
stderr: execaStderr(err),
|
|
285
|
+
message: err instanceof Error
|
|
286
|
+
? err.message
|
|
287
|
+
: `gh api branch-protection failed: ${String(err)}`,
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
}), {
|
|
291
|
+
call: `getRequiredChecks(${baseBranch})`,
|
|
292
|
+
timeoutMs: 15_000,
|
|
293
|
+
retryOn: (err) => err._tag === "GithubTimeout",
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
getCheckRunLogTail({ repoPath, runId, lines }) {
|
|
297
|
+
return applyGithubPolicy(Effect.tryPromise({
|
|
298
|
+
try: async () => {
|
|
299
|
+
try {
|
|
300
|
+
// `gh run view <id> --log-failed` is the supported way
|
|
301
|
+
// to fetch only the failed-step logs of an Actions run.
|
|
302
|
+
// Non-Actions checks have no runId and don't reach this
|
|
303
|
+
// path; the gateway caller maps `runId === null` to an
|
|
304
|
+
// empty tail upstream.
|
|
305
|
+
const { stdout } = await execa("gh", ["run", "view", String(runId), "--log-failed"], { cwd: repoPath });
|
|
306
|
+
const all = stdout.split("\n");
|
|
307
|
+
if (all.length <= lines)
|
|
308
|
+
return stdout;
|
|
309
|
+
return all.slice(-lines).join("\n");
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
const stderr = execaStderr(err);
|
|
313
|
+
// 404 (run gone) or "no logs" → empty tail rather than
|
|
314
|
+
// throwing; the agent gets the check name only.
|
|
315
|
+
if (stderr.includes("404") ||
|
|
316
|
+
stderr.includes("no logs") ||
|
|
317
|
+
stderr.includes("expired")) {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
catch: (err) => {
|
|
324
|
+
if (isCommandMissing(err)) {
|
|
325
|
+
return new GhCliMissing({
|
|
326
|
+
message: `gh CLI not found on PATH (${err instanceof Error ? err.message : String(err)})`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return new GithubReadFailed({
|
|
330
|
+
call: `getCheckRunLogTail(${runId})`,
|
|
331
|
+
stderr: execaStderr(err),
|
|
332
|
+
message: err instanceof Error
|
|
333
|
+
? err.message
|
|
334
|
+
: `gh run view failed: ${String(err)}`,
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
}), {
|
|
338
|
+
call: `getCheckRunLogTail(${runId})`,
|
|
339
|
+
timeoutMs: 30_000,
|
|
340
|
+
retryOn: (err) => err._tag === "GithubTimeout",
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
getPullRequestComments({ repoPath, prNumber }) {
|
|
344
|
+
return applyGithubPolicy(Effect.tryPromise({
|
|
345
|
+
try: async () => {
|
|
346
|
+
// Three endpoints, three flavors of comment. We fetch
|
|
347
|
+
// them in parallel and merge by createdAt — keeping
|
|
348
|
+
// source labels so the watcher knows where each one
|
|
349
|
+
// came from if it ever needs to round-trip a reply.
|
|
350
|
+
const fetchJson = async (path, jq) => {
|
|
351
|
+
const { stdout } = await execa("gh", ["api", path, "--paginate", "--jq", jq], { cwd: repoPath });
|
|
352
|
+
const lines = stdout
|
|
353
|
+
.split("\n")
|
|
354
|
+
.filter((s) => s.trim().length > 0);
|
|
355
|
+
return lines.map((line) => JSON.parse(line));
|
|
356
|
+
};
|
|
357
|
+
const projection = ".[] | {id, body, login: (.user.login // \"\"), created_at}";
|
|
358
|
+
const [issueComments, reviewComments, reviewSummaries] = await Promise.all([
|
|
359
|
+
fetchJson(`repos/{owner}/{repo}/issues/${prNumber}/comments`, projection),
|
|
360
|
+
fetchJson(`repos/{owner}/{repo}/pulls/${prNumber}/comments`, projection),
|
|
361
|
+
// Review summaries omit empty bodies (a review can be
|
|
362
|
+
// submitted with no top-level body — only inline
|
|
363
|
+
// comments). The `select(.body != "")` filter drops
|
|
364
|
+
// those so they don't show up as ghost entries.
|
|
365
|
+
fetchJson(`repos/{owner}/{repo}/pulls/${prNumber}/reviews`, ".[] | select(.body != \"\" and .body != null) | {id, body, login: (.user.login // \"\"), submitted_at}"),
|
|
366
|
+
]);
|
|
367
|
+
const norm = (o, source, tsField) => ({
|
|
368
|
+
id: Number(o.id ?? 0),
|
|
369
|
+
authorLogin: String(o.login ?? ""),
|
|
370
|
+
body: String(o.body ?? ""),
|
|
371
|
+
createdAtUnixSeconds: Math.floor(new Date(String(o[tsField] ?? 0)).getTime() / 1000),
|
|
372
|
+
source,
|
|
373
|
+
});
|
|
374
|
+
const merged = [
|
|
375
|
+
...issueComments.map((c) => norm(c, "issue", "created_at")),
|
|
376
|
+
...reviewComments.map((c) => norm(c, "review", "created_at")),
|
|
377
|
+
...reviewSummaries.map((c) => norm(c, "review-summary", "submitted_at")),
|
|
378
|
+
];
|
|
379
|
+
merged.sort((a, b) => a.createdAtUnixSeconds - b.createdAtUnixSeconds);
|
|
380
|
+
return merged;
|
|
381
|
+
},
|
|
382
|
+
catch: (err) => {
|
|
383
|
+
if (isCommandMissing(err)) {
|
|
384
|
+
return new GhCliMissing({
|
|
385
|
+
message: `gh CLI not found on PATH (${err instanceof Error ? err.message : String(err)})`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return new GithubReadFailed({
|
|
389
|
+
call: `getPullRequestComments(${prNumber})`,
|
|
390
|
+
stderr: execaStderr(err),
|
|
391
|
+
message: err instanceof Error
|
|
392
|
+
? err.message
|
|
393
|
+
: `gh api comments failed: ${String(err)}`,
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
}), {
|
|
397
|
+
call: `getPullRequestComments(${prNumber})`,
|
|
398
|
+
timeoutMs: 30_000,
|
|
399
|
+
retryOn: (err) => err._tag === "GithubTimeout",
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
getConflictFiles({ repoPath, baseBranch, branch }) {
|
|
403
|
+
return applyGithubPolicy(Effect.tryPromise({
|
|
404
|
+
try: async () => {
|
|
405
|
+
// Step 1: refresh origin/<base> so the merge-tree below
|
|
406
|
+
// reasons about the latest remote state, not whatever
|
|
407
|
+
// the cwd happened to have fetched last.
|
|
408
|
+
await execa("git", ["fetch", "origin", baseBranch], {
|
|
409
|
+
cwd: repoPath,
|
|
410
|
+
});
|
|
411
|
+
// Step 2: simulate the merge. `git merge-tree --name-only`
|
|
412
|
+
// (git >= 2.38) prints only the conflicted paths, one per
|
|
413
|
+
// line, on stdout. Exit code is non-zero when there ARE
|
|
414
|
+
// conflicts, so we read stdout regardless of status.
|
|
415
|
+
const result = await execa("git", [
|
|
416
|
+
"merge-tree",
|
|
417
|
+
"--name-only",
|
|
418
|
+
"--no-messages",
|
|
419
|
+
`origin/${baseBranch}`,
|
|
420
|
+
branch,
|
|
421
|
+
], { cwd: repoPath, reject: false });
|
|
422
|
+
const files = result.stdout
|
|
423
|
+
.split("\n")
|
|
424
|
+
.map((s) => s.trim())
|
|
425
|
+
.filter(Boolean);
|
|
426
|
+
return files;
|
|
427
|
+
},
|
|
428
|
+
catch: (err) => {
|
|
429
|
+
if (isCommandMissing(err)) {
|
|
430
|
+
return new GhCliMissing({
|
|
431
|
+
message: `git not found on PATH (${err instanceof Error ? err.message : String(err)})`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return new GithubReadFailed({
|
|
435
|
+
call: `getConflictFiles(${branch}→${baseBranch})`,
|
|
436
|
+
stderr: execaStderr(err),
|
|
437
|
+
message: err instanceof Error
|
|
438
|
+
? err.message
|
|
439
|
+
: `git merge-tree failed: ${String(err)}`,
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
}), {
|
|
443
|
+
call: `getConflictFiles(${branch})`,
|
|
444
|
+
timeoutMs: 30_000,
|
|
445
|
+
retryOn: (err) => err._tag === "GithubTimeout",
|
|
446
|
+
});
|
|
447
|
+
},
|
|
148
448
|
};
|
|
149
449
|
}
|
|
450
|
+
function canonicaliseCheckStatus(raw) {
|
|
451
|
+
switch (raw.toLowerCase()) {
|
|
452
|
+
case "queued":
|
|
453
|
+
return "queued";
|
|
454
|
+
case "in_progress":
|
|
455
|
+
return "in_progress";
|
|
456
|
+
case "completed":
|
|
457
|
+
return "completed";
|
|
458
|
+
default:
|
|
459
|
+
// Unknown status — treat as queued so the watcher waits for a
|
|
460
|
+
// final conclusion rather than acting on intermediate state.
|
|
461
|
+
return "queued";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function canonicaliseCheckConclusion(raw) {
|
|
465
|
+
if (raw === null)
|
|
466
|
+
return null;
|
|
467
|
+
switch (raw.toLowerCase()) {
|
|
468
|
+
case "success":
|
|
469
|
+
case "failure":
|
|
470
|
+
case "cancelled":
|
|
471
|
+
case "timed_out":
|
|
472
|
+
case "neutral":
|
|
473
|
+
case "skipped":
|
|
474
|
+
case "action_required":
|
|
475
|
+
case "stale":
|
|
476
|
+
case "startup_failure":
|
|
477
|
+
return raw.toLowerCase();
|
|
478
|
+
default:
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function canonicaliseMergeableState(raw) {
|
|
483
|
+
switch (raw.toLowerCase()) {
|
|
484
|
+
case "clean":
|
|
485
|
+
return "clean";
|
|
486
|
+
case "dirty":
|
|
487
|
+
return "dirty";
|
|
488
|
+
case "behind":
|
|
489
|
+
return "behind";
|
|
490
|
+
case "blocked":
|
|
491
|
+
return "blocked";
|
|
492
|
+
case "unstable":
|
|
493
|
+
return "unstable";
|
|
494
|
+
case "draft":
|
|
495
|
+
return "draft";
|
|
496
|
+
default:
|
|
497
|
+
return "unknown";
|
|
498
|
+
}
|
|
499
|
+
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -7,6 +7,7 @@ import { flagHitl, handleProcessFailure } from "./hitl.js";
|
|
|
7
7
|
import { runImplementLoop } from "./implement.js";
|
|
8
8
|
import { formatInRunReviewerFeedback, formatPriorFeedback, } from "./prompts.js";
|
|
9
9
|
import { runReviewPass } from "./review.js";
|
|
10
|
+
import { makeCiWatcher, makeIsMerged, makeMergeabilityWatcher, makeReviewerWatcher, runShepherdPass, } from "./shepherd.js";
|
|
10
11
|
import { finalize, formatRebaseConflictReason, formatRebaseFailureReason, } from "./finalize.js";
|
|
11
12
|
// Re-exports so existing callers (commands/run.ts) and tests
|
|
12
13
|
// (orchestrator.test.ts) keep working without import churn. The
|
|
@@ -302,6 +303,49 @@ const processIssue = (issue, deps) => Effect.gen(function* () {
|
|
|
302
303
|
yield* flagHitl(issue, deps, reason);
|
|
303
304
|
return { kind: "hitl", detail: reason };
|
|
304
305
|
}
|
|
306
|
+
// VA-460: post-finalize shepherd loop. Gated off by default —
|
|
307
|
+
// first ship lands the scaffolding only; VA-461..463 wire the
|
|
308
|
+
// mergeability / CI / reviewer subscriptions, VA-464 the
|
|
309
|
+
// budget knobs. When gated off the composer returns the
|
|
310
|
+
// `opened` outcome verbatim, preserving the historical
|
|
311
|
+
// behavior.
|
|
312
|
+
if (deps.config.shepherdEnabled) {
|
|
313
|
+
const shepherd = yield* runShepherdPass(issue, {
|
|
314
|
+
config: deps.config,
|
|
315
|
+
cwd: deps.cwd,
|
|
316
|
+
baseBranch: deps.baseBranch,
|
|
317
|
+
github: deps.github,
|
|
318
|
+
// VA-461 / VA-462 / VA-463: three real subscriptions
|
|
319
|
+
// wired in precedence order. Mergeability runs first
|
|
320
|
+
// (a dirty base would prevent CI from even running);
|
|
321
|
+
// CI second; reviewer feedback last (CI fixes that
|
|
322
|
+
// address reviewer concerns push commits which will
|
|
323
|
+
// be seen on the next tick).
|
|
324
|
+
isMerged: makeIsMerged(deps.github),
|
|
325
|
+
subscriptions: [
|
|
326
|
+
makeMergeabilityWatcher({
|
|
327
|
+
github: deps.github,
|
|
328
|
+
config: deps.config,
|
|
329
|
+
}),
|
|
330
|
+
makeCiWatcher({
|
|
331
|
+
github: deps.github,
|
|
332
|
+
config: deps.config,
|
|
333
|
+
}),
|
|
334
|
+
makeReviewerWatcher({
|
|
335
|
+
github: deps.github,
|
|
336
|
+
config: deps.config,
|
|
337
|
+
}),
|
|
338
|
+
],
|
|
339
|
+
}, branch, finalized.detail);
|
|
340
|
+
if (shepherd.kind === "hitl") {
|
|
341
|
+
const reason = `Shepherd routed PR to HITL: ${shepherd.reason}`;
|
|
342
|
+
yield* flagHitl(issue, deps, reason);
|
|
343
|
+
return { kind: "hitl", detail: reason };
|
|
344
|
+
}
|
|
345
|
+
// `merged` and `ready` both preserve the `opened` outcome —
|
|
346
|
+
// the PR exists; whether it's merged yet doesn't change the
|
|
347
|
+
// per-issue verdict the operator sees in the exit summary.
|
|
348
|
+
}
|
|
305
349
|
return finalized;
|
|
306
350
|
}
|
|
307
351
|
if (review.kind === "hitl") {
|
package/dist/prompts.js
CHANGED
|
@@ -31,6 +31,43 @@ export async function renderReviewPrompt(args) {
|
|
|
31
31
|
const template = await loadReviewPrompt();
|
|
32
32
|
return renderPrompt(template, reviewVars(args));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* VA-461: render the shepherd-rebase prompt. The agent receives the
|
|
36
|
+
* branch / base / mergeable_state and an optional list of files
|
|
37
|
+
* `getConflictFiles` pre-detected — it executes the rebase inside
|
|
38
|
+
* sandcastle and signals IMPL: DONE or IMPL: BLOCKED.
|
|
39
|
+
*/
|
|
40
|
+
export async function renderShepherdRebasePrompt(args) {
|
|
41
|
+
const template = await loadShepherdRebasePrompt();
|
|
42
|
+
return renderPrompt(template, shepherdRebaseVars(args));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* VA-462: render the shepherd-ci-fix prompt. The agent receives the
|
|
46
|
+
* failing check name and the trailing log lines and executes a
|
|
47
|
+
* targeted fix on the impl branch (commit + push, not force-push).
|
|
48
|
+
*/
|
|
49
|
+
export async function renderShepherdCiFixPrompt(args) {
|
|
50
|
+
const template = await loadShepherdCiFixPrompt();
|
|
51
|
+
return renderPrompt(template, shepherdCiFixVars(args));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* VA-463: render the prompt for a `REVIEW: CHANGES-REQUESTED — <fix>`
|
|
55
|
+
* verdict from the adversarial PR-reviewer agent. The fix line is
|
|
56
|
+
* inserted verbatim; the agent applies the change and pushes.
|
|
57
|
+
*/
|
|
58
|
+
export async function renderShepherdReviewFixPrompt(args) {
|
|
59
|
+
const template = await loadShepherdReviewFixPrompt();
|
|
60
|
+
return renderPrompt(template, shepherdReviewFixVars(args));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* VA-463: render the prompt for a verbatim human comment on the PR.
|
|
64
|
+
* The agent decides whether the comment is actionable; non-actionable
|
|
65
|
+
* comments resolve to IMPL: DONE with no commit.
|
|
66
|
+
*/
|
|
67
|
+
export async function renderShepherdReviewRespondPrompt(args) {
|
|
68
|
+
const template = await loadShepherdReviewRespondPrompt();
|
|
69
|
+
return renderPrompt(template, shepherdReviewRespondVars(args));
|
|
70
|
+
}
|
|
34
71
|
// ---------------------------------------------------------------------------
|
|
35
72
|
// Internal helpers
|
|
36
73
|
// ---------------------------------------------------------------------------
|
|
@@ -40,6 +77,18 @@ async function loadImplementPrompt() {
|
|
|
40
77
|
async function loadReviewPrompt() {
|
|
41
78
|
return readFile(join(PROMPT_DIR, "review.md"), "utf8");
|
|
42
79
|
}
|
|
80
|
+
async function loadShepherdRebasePrompt() {
|
|
81
|
+
return readFile(join(PROMPT_DIR, "shepherd-rebase.md"), "utf8");
|
|
82
|
+
}
|
|
83
|
+
async function loadShepherdCiFixPrompt() {
|
|
84
|
+
return readFile(join(PROMPT_DIR, "shepherd-ci-fix.md"), "utf8");
|
|
85
|
+
}
|
|
86
|
+
async function loadShepherdReviewFixPrompt() {
|
|
87
|
+
return readFile(join(PROMPT_DIR, "shepherd-review-fix.md"), "utf8");
|
|
88
|
+
}
|
|
89
|
+
async function loadShepherdReviewRespondPrompt() {
|
|
90
|
+
return readFile(join(PROMPT_DIR, "shepherd-review-respond.md"), "utf8");
|
|
91
|
+
}
|
|
43
92
|
/**
|
|
44
93
|
* Replace all `{{KEY}}` placeholders with values from `vars`. Done
|
|
45
94
|
* here (not in sandcastle's promptArgs) so the prompt can be passed
|
|
@@ -77,6 +126,56 @@ function reviewVars(args) {
|
|
|
77
126
|
COMMITS: args.commits || "(no commits)",
|
|
78
127
|
};
|
|
79
128
|
}
|
|
129
|
+
function shepherdCiFixVars(args) {
|
|
130
|
+
return {
|
|
131
|
+
ISSUE_IDENTIFIER: args.issue.identifier,
|
|
132
|
+
ISSUE_TITLE: args.issue.title,
|
|
133
|
+
ISSUE_DESCRIPTION: args.issue.description || "(no description)",
|
|
134
|
+
BRANCH: args.branch,
|
|
135
|
+
CHECK_NAME: args.checkName,
|
|
136
|
+
LOG_TAIL: args.logTail.trim() || "(log unavailable)",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function shepherdReviewFixVars(args) {
|
|
140
|
+
return {
|
|
141
|
+
ISSUE_IDENTIFIER: args.issue.identifier,
|
|
142
|
+
ISSUE_TITLE: args.issue.title,
|
|
143
|
+
ISSUE_DESCRIPTION: args.issue.description || "(no description)",
|
|
144
|
+
BRANCH: args.branch,
|
|
145
|
+
FIX_REQUEST: args.fixRequest.trim() || "(empty fix request)",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function shepherdReviewRespondVars(args) {
|
|
149
|
+
return {
|
|
150
|
+
ISSUE_IDENTIFIER: args.issue.identifier,
|
|
151
|
+
ISSUE_TITLE: args.issue.title,
|
|
152
|
+
ISSUE_DESCRIPTION: args.issue.description || "(no description)",
|
|
153
|
+
BRANCH: args.branch,
|
|
154
|
+
COMMENT_AUTHOR: args.commentAuthor || "(unknown author)",
|
|
155
|
+
COMMENT_SOURCE: args.commentSource,
|
|
156
|
+
COMMENT_BODY: args.commentBody.trim() || "(empty body)",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function shepherdRebaseVars(args) {
|
|
160
|
+
// The conflict-files block is one of two shapes: a bulleted list
|
|
161
|
+
// (when `getConflictFiles` returned hits) or a single line saying
|
|
162
|
+
// the branch is "behind" with no expected conflicts. The template
|
|
163
|
+
// substitutes the whole block via `{{CONFLICT_FILES_BLOCK}}` so
|
|
164
|
+
// either shape renders cleanly without dangling headers.
|
|
165
|
+
const filesBlock = args.conflictFiles.length > 0
|
|
166
|
+
? `Pre-detected conflicting files:\n${args.conflictFiles
|
|
167
|
+
.map((f) => ` - ${f}`)
|
|
168
|
+
.join("\n")}`
|
|
169
|
+
: `No conflicts pre-detected — the branch is behind base, expect a clean fast-forward rebase`;
|
|
170
|
+
return {
|
|
171
|
+
ISSUE_IDENTIFIER: args.issue.identifier,
|
|
172
|
+
ISSUE_TITLE: args.issue.title,
|
|
173
|
+
BRANCH: args.branch,
|
|
174
|
+
BASE_BRANCH: args.baseBranch,
|
|
175
|
+
MERGEABLE_STATE: args.mergeableState,
|
|
176
|
+
CONFLICT_FILES_BLOCK: filesBlock,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
80
179
|
/**
|
|
81
180
|
* VA-383: known orchestrator-emitted comment prefixes that are
|
|
82
181
|
* bookkeeping noise, not feedback the impl agent should learn from.
|