@cat-factory/executor-harness 1.60.0 → 1.64.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 +10 -1
- package/dist/agent-runner.js +81 -8
- package/dist/agent.js +9 -3
- package/dist/claude-stream.js +18 -0
- package/dist/coding-agent.js +32 -4
- package/dist/embed.js +2 -1
- package/dist/git.js +0 -319
- package/dist/host-markdown.js +142 -0
- package/dist/pi-workspace.js +35 -2
- package/dist/pi.js +15 -184
- package/dist/pr-description.js +157 -0
- package/dist/progress-guard.js +211 -0
- package/dist/subagents.js +1 -50
- package/dist/vcs-api.js +402 -0
- package/package.json +4 -3
- package/src/agent-runner.ts +88 -8
- package/src/agent.ts +8 -3
- package/src/claude-stream.ts +19 -0
- package/src/coding-agent.ts +45 -3
- package/src/embed.ts +5 -3
- package/src/git.ts +1 -385
- package/src/host-markdown.ts +155 -0
- package/src/pi-workspace.ts +40 -4
- package/src/pi.ts +26 -252
- package/src/pr-description.ts +171 -0
- package/src/progress-guard.ts +285 -0
- package/src/subagents.ts +7 -16
- package/src/vcs-api.ts +512 -0
package/README.md
CHANGED
|
@@ -73,7 +73,16 @@ The implementation job (`POST /run`) is the canonical sequence:
|
|
|
73
73
|
6. **commit, push** a branch and **open a PR**, returning `{ prUrl, branch, summary }` — but
|
|
74
74
|
ONLY if step 4 ended green. A spent budget returns an error result with the validation report
|
|
75
75
|
and opens no PR. Absent `validationChecks` / `reproduction`, steps 4 and 5 do not happen at
|
|
76
|
-
all.
|
|
76
|
+
all. The PR's description prefers the agent-authored reviewer briefing over the generic
|
|
77
|
+
dispatch-time text the job body carries: a PR-opening agent is prompted to write one to the
|
|
78
|
+
`.cat-pr-description.md` sentinel at the checkout root (one per sibling repo in a multi-repo
|
|
79
|
+
run; an optional leading `# <title>` line, when it is the file's only `#` heading, sets the PR
|
|
80
|
+
title), and `src/pr-description.ts` lifts it — secret-scrubbed, size-capped with a visible
|
|
81
|
+
note, made inert for the host by `src/host-markdown.ts`, kept out of the commit like the
|
|
82
|
+
effort/follow-ups sentinels — onto `openPullRequest`. Absent or unusable ⇒ the fallback text,
|
|
83
|
+
unchanged. On a RESUMED run the PR already exists, so an agent briefing additionally refreshes
|
|
84
|
+
its title/description in place (carrying the engine's managed report region across); the
|
|
85
|
+
generic fallback never does, so a human's edit is safe.
|
|
77
86
|
|
|
78
87
|
Bootstrap differs at the ends — it may start from an empty dir, and **resets
|
|
79
88
|
history to one commit and force-pushes** the default branch instead of opening a
|
package/dist/agent-runner.js
CHANGED
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { claudeAssistantContent, claudeCallUsage, isObject, numberOf, redactBody, } from './claude-stream.js';
|
|
6
6
|
import { createCallMetricPublisher, publishCallMetric, } from './pi.js';
|
|
7
|
+
import { ProgressGuard } from './progress-guard.js';
|
|
7
8
|
import { killChildProcess, spawnDetached } from './process.js';
|
|
8
9
|
import { redact, secretsToRedact } from './redact.js';
|
|
9
10
|
import { createSliceTracker, startSubagentWatcher } from './subagents.js';
|
|
@@ -58,7 +59,12 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
58
59
|
let aborted = false;
|
|
59
60
|
let lineBuffer = '';
|
|
60
61
|
const killChild = () => killChildProcess(child);
|
|
61
|
-
|
|
62
|
+
// `final` marks the at-close flush of a trailing unterminated line: the CLI has already
|
|
63
|
+
// exited, so an observer must not act on that record in a way that KILLS the run (mirrors
|
|
64
|
+
// `runPi`'s `runGuard = false` flush — without it, a guard tripping on the last buffered
|
|
65
|
+
// record could turn a clean exit into a spurious "no progress" failure). The record's
|
|
66
|
+
// progress/telemetry signal is still delivered; only kill decisions are suppressed.
|
|
67
|
+
const processLine = (line, final = false) => {
|
|
62
68
|
if (!line.startsWith('{'))
|
|
63
69
|
return;
|
|
64
70
|
let event;
|
|
@@ -69,7 +75,7 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
69
75
|
return;
|
|
70
76
|
}
|
|
71
77
|
try {
|
|
72
|
-
onEvent(event);
|
|
78
|
+
onEvent(event, { final });
|
|
73
79
|
}
|
|
74
80
|
catch {
|
|
75
81
|
// A faulty observer must never break the run.
|
|
@@ -106,11 +112,14 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
106
112
|
});
|
|
107
113
|
child.on('close', (code) => {
|
|
108
114
|
opts.signal?.removeEventListener('abort', onAbort);
|
|
109
|
-
if (lineBuffer.trim())
|
|
110
|
-
processLine(lineBuffer.trim());
|
|
111
115
|
const stderrTail = redact(stderr, secrets).slice(-700);
|
|
116
|
+
if (lineBuffer.trim())
|
|
117
|
+
processLine(lineBuffer.trim(), true);
|
|
112
118
|
if (aborted) {
|
|
113
|
-
|
|
119
|
+
// Carry the tail on the rejection so a caller that REPLACES this generic message with a
|
|
120
|
+
// more specific cause (the no-progress guard's diagnostic) can still append it — the
|
|
121
|
+
// stderr is often the only evidence of what the CLI was doing when it was killed.
|
|
122
|
+
reject(Object.assign(new Error('agent run aborted by watchdog'), { stderrTail }));
|
|
114
123
|
return;
|
|
115
124
|
}
|
|
116
125
|
if (code !== 0) {
|
|
@@ -246,7 +255,44 @@ export async function runClaudeCode(opts) {
|
|
|
246
255
|
if (progress)
|
|
247
256
|
opts.onProgress(progress);
|
|
248
257
|
};
|
|
249
|
-
|
|
258
|
+
// No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
259
|
+
// absent on this path until now. Claude Code reports a tool CALL (its name) on the `assistant`
|
|
260
|
+
// turn and that call's RESULT (`is_error`) on the following `user` turn, so correlate them by
|
|
261
|
+
// `tool_use` id to feed the guard a {name,isError} signal. A tripped guard aborts the CLI via
|
|
262
|
+
// `guardAbort` (folded into streamCli's signal below) and the run then fails with its
|
|
263
|
+
// diagnostic. Disabled when the caller supplies no limits (only the external watchdog bounds it).
|
|
264
|
+
const guard = opts.guardLimits
|
|
265
|
+
? new ProgressGuard(opts.guardLimits, opts.expectsEdits ?? true)
|
|
266
|
+
: undefined;
|
|
267
|
+
const toolNames = new Map();
|
|
268
|
+
const guardAbort = new AbortController();
|
|
269
|
+
let guardReason;
|
|
270
|
+
// Feed a user turn's settled tool calls to the guard, pairing each `tool_result`'s `is_error`
|
|
271
|
+
// with the name captured for its `tool_use` id on the assistant turn. The FIRST reason trips
|
|
272
|
+
// it: record the diagnostic and abort the CLI (streamCli's close handler rejects; the catch
|
|
273
|
+
// below surfaces `guardReason` over the generic abort message). A standalone closure so the
|
|
274
|
+
// per-block loop doesn't nest onEvent past the readable-depth limit.
|
|
275
|
+
const feedGuard = (content) => {
|
|
276
|
+
if (!guard || guardReason)
|
|
277
|
+
return;
|
|
278
|
+
for (const block of content) {
|
|
279
|
+
if (!isObject(block) || block.type !== 'tool_result')
|
|
280
|
+
continue;
|
|
281
|
+
const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
|
|
282
|
+
const name = id ? toolNames.get(id) : undefined;
|
|
283
|
+
if (id)
|
|
284
|
+
toolNames.delete(id);
|
|
285
|
+
if (!name)
|
|
286
|
+
continue;
|
|
287
|
+
const reason = guard.observeSignal({ name, isError: block.is_error === true });
|
|
288
|
+
if (reason) {
|
|
289
|
+
guardReason = reason;
|
|
290
|
+
guardAbort.abort();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const onEvent = (event, meta) => {
|
|
250
296
|
const type = event.type;
|
|
251
297
|
if (type === 'assistant' && isObject(event.message)) {
|
|
252
298
|
const message = event.message;
|
|
@@ -255,7 +301,14 @@ export async function runClaudeCode(opts) {
|
|
|
255
301
|
stats.assistantChars += text.length;
|
|
256
302
|
stats.toolCalls += toolUses;
|
|
257
303
|
for (const block of content) {
|
|
258
|
-
if (isObject(block)
|
|
304
|
+
if (!isObject(block) || block.type !== 'tool_use')
|
|
305
|
+
continue;
|
|
306
|
+
// Remember each call's name against its id so the guard can pair it with the
|
|
307
|
+
// `is_error` its `tool_result` carries on the next `user` turn.
|
|
308
|
+
if (typeof block.id === 'string' && typeof block.name === 'string') {
|
|
309
|
+
toolNames.set(block.id, block.name);
|
|
310
|
+
}
|
|
311
|
+
if (block.name === 'TodoWrite') {
|
|
259
312
|
const progress = todosToProgress(block.input?.todos);
|
|
260
313
|
if (progress)
|
|
261
314
|
lastTodo = progress;
|
|
@@ -288,6 +341,10 @@ export async function runClaudeCode(opts) {
|
|
|
288
341
|
sliceTracker.onUser(content);
|
|
289
342
|
planTracker.onUser(content);
|
|
290
343
|
emitProgress();
|
|
344
|
+
// Not on the at-close flush: the CLI has already exited, so tripping the guard there
|
|
345
|
+
// would kill nothing and only convert a clean exit into a spurious failure.
|
|
346
|
+
if (!meta?.final)
|
|
347
|
+
feedGuard(content);
|
|
291
348
|
messages.push({ role: 'tool', content });
|
|
292
349
|
}
|
|
293
350
|
}
|
|
@@ -350,6 +407,11 @@ export async function runClaudeCode(opts) {
|
|
|
350
407
|
...(opts.log ? { log: opts.log } : {}),
|
|
351
408
|
})
|
|
352
409
|
: undefined;
|
|
410
|
+
// Fold the guard's abort into the run signal so a tripped guard kills the CLI the same way the
|
|
411
|
+
// external watchdog does; `guardReason` (set above) distinguishes the two at the catch below.
|
|
412
|
+
const runSignal = opts.signal
|
|
413
|
+
? AbortSignal.any([opts.signal, guardAbort.signal])
|
|
414
|
+
: guardAbort.signal;
|
|
353
415
|
try {
|
|
354
416
|
const { stderrTail } = await streamCli({
|
|
355
417
|
command: 'claude',
|
|
@@ -368,7 +430,7 @@ export async function runClaudeCode(opts) {
|
|
|
368
430
|
opts.model,
|
|
369
431
|
...appendArgs,
|
|
370
432
|
],
|
|
371
|
-
}, prompt, opts, env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
|
|
433
|
+
}, prompt, { ...opts, signal: runSignal }, env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
|
|
372
434
|
return await assembleClaudeOutcome({
|
|
373
435
|
summary,
|
|
374
436
|
stats,
|
|
@@ -379,6 +441,17 @@ export async function runClaudeCode(opts) {
|
|
|
379
441
|
subagents,
|
|
380
442
|
});
|
|
381
443
|
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
// A tripped no-progress guard aborted the CLI; streamCli rejects with its generic abort
|
|
446
|
+
// message, so replace it with the guard's actionable diagnostic — carrying the stderr tail
|
|
447
|
+
// it attached, since that is usually the only evidence of what the CLI was doing when it was
|
|
448
|
+
// killed. Byte-for-byte the shape `runPi` fails with.
|
|
449
|
+
if (guardReason) {
|
|
450
|
+
const tail = err?.stderrTail;
|
|
451
|
+
throw new Error(tail ? `${guardReason} Agent stderr: ${tail}` : guardReason);
|
|
452
|
+
}
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
382
455
|
finally {
|
|
383
456
|
await subagents?.stop();
|
|
384
457
|
if (configHome) {
|
package/dist/agent.js
CHANGED
|
@@ -6,7 +6,9 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { standUpFrontend, tearDownFrontend } from './frontend-infra.js';
|
|
7
7
|
import { configurePackageRegistries } from './package-registries.js';
|
|
8
8
|
import { captureRedactedOutput, redactSecrets, registerKnownSecrets } from './redact.js';
|
|
9
|
-
import { cloneRepo, commitAll, conflictDiff, fetchPullRequestHead, fetchReferenceBranches, hasAgentChanges, headCommit,
|
|
9
|
+
import { cloneRepo, commitAll, conflictDiff, fetchPullRequestHead, fetchReferenceBranches, hasAgentChanges, headCommit, mergeBranch, prepareExistingCheckout, pushBranch, reinitAndPush, unmergedPaths, } from './git.js';
|
|
10
|
+
import { inferVcsProvider, openPullRequest } from './vcs-api.js';
|
|
11
|
+
import { applyPrDescription } from './pr-description.js';
|
|
10
12
|
import { makeDirClaimer, noChangesReason, runCodingAgent, runMultiRepoCoding, } from './coding-agent.js';
|
|
11
13
|
import { validationFailureMessage } from './validation-checks.js';
|
|
12
14
|
import { acquireRepoCheckout, agentNeverActed, agentOutputTail, NEVER_ACTED_CAUSE, runAgentInWorkspace, unusableFinalAnswerCause, withWorkspace, } from './pi-workspace.js';
|
|
@@ -822,7 +824,7 @@ function buildSingleRepoCodingSpec(job, pushBranch) {
|
|
|
822
824
|
*/
|
|
823
825
|
async function runSingleRepoCoding(job, opts) {
|
|
824
826
|
const pushBranch = job.pushBranch ?? job.newBranch ?? job.branch;
|
|
825
|
-
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, validationReport, reproductionReport, effortReport, } = await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts);
|
|
827
|
+
const { summary, stats, stderrTail, pushed, usage, callMetrics, validation, validationReport, reproductionReport, effortReport, prDescription, } = await runCodingAgent(buildSingleRepoCodingSpec(job, pushBranch), opts);
|
|
826
828
|
// Ralph loop: the harness-computed validation verdict, forwarded onto the coding result as
|
|
827
829
|
// `ralphVerdict` so the backend's `toRunResult` lifts it onto `AgentRunResult.ralphVerdict`.
|
|
828
830
|
const ralphVerdict = validation ? { ralphVerdict: validation } : {};
|
|
@@ -894,7 +896,11 @@ async function runSingleRepoCoding(job, opts) {
|
|
|
894
896
|
ghToken: job.ghToken,
|
|
895
897
|
head: pushBranch,
|
|
896
898
|
base: job.repo.baseBranch,
|
|
897
|
-
|
|
899
|
+
// The agent-authored briefing (title/body) wins field-wise over the dispatch-time text.
|
|
900
|
+
pr: applyPrDescription(job.pr, prDescription),
|
|
901
|
+
// A resumed run's PR is already open, so refresh it rather than lose the briefing to the
|
|
902
|
+
// duplicate-PR 422 — only from a REAL briefing (see `refreshExisting` for why).
|
|
903
|
+
...(prDescription ? { refreshExisting: true } : {}),
|
|
898
904
|
apiBase: job.githubApiBase,
|
|
899
905
|
// The provider (set by the server from the configured backend) selects GitHub-PR vs
|
|
900
906
|
// GitLab-MR authoritatively; the clone URL supplies the GitLab REST base + project path.
|
package/dist/claude-stream.js
CHANGED
|
@@ -7,6 +7,24 @@ import { redact } from './redact.js';
|
|
|
7
7
|
export function isObject(value) {
|
|
8
8
|
return typeof value === 'object' && value !== null;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* The tool names the Claude Code CLI dispatches a parallel subagent under. `Agent` is what the
|
|
12
|
+
* shipped schema declares (`AgentInput` in `sdk-tools.d.ts`, carrying `description` / `prompt` /
|
|
13
|
+
* `subagent_type`); `Task` is the older name for the same dispatch. Both are matched because the
|
|
14
|
+
* harness runs against whatever CLI the image happens to bundle, and matching only the old name
|
|
15
|
+
* is what left a CLI 2.1.x pr-review reporting no slices at all.
|
|
16
|
+
*
|
|
17
|
+
* Note the asymmetry: keeping the legacy `Task` here is the one place a CLI rename could produce a
|
|
18
|
+
* FALSE signal rather than merely no signal — if a future build were to name a plain task-list
|
|
19
|
+
* tool `Task`, its writes would be counted as in-flight slices. We accept that because no shipped
|
|
20
|
+
* build does (the incremental plan tool is `TaskCreate`/`TaskUpdate`, tracked separately in
|
|
21
|
+
* `progress.ts`), and dropping legacy coverage is the more likely regression.
|
|
22
|
+
*
|
|
23
|
+
* Lives here rather than in `subagents.ts` because BOTH the slice tracker and the no-progress
|
|
24
|
+
* guard (`pi.ts`, which `subagents.ts` imports — so it cannot import back) must agree on what a
|
|
25
|
+
* subagent dispatch looks like.
|
|
26
|
+
*/
|
|
27
|
+
export const SUBAGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
|
|
10
28
|
export function numberOf(value) {
|
|
11
29
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
12
30
|
}
|
package/dist/coding-agent.js
CHANGED
|
@@ -3,9 +3,11 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { killChildProcess, spawnDetached } from './process.js';
|
|
5
5
|
import { MAX_CAPTURED_OUTPUT_CHARS, redactSecrets } from './redact.js';
|
|
6
|
-
import { branchAheadOfBase, changedFilesSinceBase, branchHasCommitsSince, cloneExistingBranch, cloneRepo, commitTrackedEdits, createBranch, excludeFromGit, fetchReferenceBranches, headCommit, listUntrackedFiles,
|
|
6
|
+
import { branchAheadOfBase, changedFilesSinceBase, branchHasCommitsSince, cloneExistingBranch, cloneRepo, commitTrackedEdits, createBranch, excludeFromGit, fetchReferenceBranches, headCommit, listUntrackedFiles, prepareExistingCheckout, pushBranch, refreshFromBaseIfClean, remoteBranchExists, } from './git.js';
|
|
7
|
+
import { openPullRequest } from './vcs-api.js';
|
|
7
8
|
import { FOLLOW_UPS_FILENAME, FollowUpTailer } from './follow-ups.js';
|
|
8
9
|
import { EFFORT_REPORT_FILE } from './effort.js';
|
|
10
|
+
import { applyPrDescription, PR_DESCRIPTION_FILE, readPrDescription, } from './pr-description.js';
|
|
9
11
|
import { acquireRepoCheckout, agentNeverActed, agentOutputTail, runAgentInWorkspace, withWorkspace, } from './pi-workspace.js';
|
|
10
12
|
import { log } from './logger.js';
|
|
11
13
|
import { runValidationLoop, } from './validation-checks.js';
|
|
@@ -115,6 +117,9 @@ export async function runCodingAgent(spec, opts = {}) {
|
|
|
115
117
|
// but that cannot un-stage a mid-run commit; the per-clone exclude is what prevents it. A bare
|
|
116
118
|
// filename pattern matches the file in any subdirectory, so it covers a monorepo `workDir` too.
|
|
117
119
|
await excludeFromGit(dir, EFFORT_REPORT_FILE, signal);
|
|
120
|
+
// Same treatment for the agent-authored PR-description sentinel: excluded locally so the
|
|
121
|
+
// agent's own `git add` can never stage the briefing into the PR it describes.
|
|
122
|
+
await excludeFromGit(dir, PR_DESCRIPTION_FILE, signal);
|
|
118
123
|
// Follow-up companion: tail the Coder's sentinel file and stream new items out on the
|
|
119
124
|
// job view. Locally exclude it from git first so the agent's own `git add` can never
|
|
120
125
|
// stage it and it never surfaces as an untracked leftover or in the PR. The sentinel
|
|
@@ -376,6 +381,12 @@ async function finalizeCodingRun(args) {
|
|
|
376
381
|
// Safety net for forgotten edits: commit changes to TRACKED files only (never
|
|
377
382
|
// untracked scratch files/artifacts — the agent owns committing new files).
|
|
378
383
|
await commitTrackedEdits(dir, spec.commitMessage, signal);
|
|
384
|
+
// The agent-authored PR description, read AFTER the validation loop (a repair round may have
|
|
385
|
+
// changed what the briefing should say) and removed so it never lingers in the checkout. The
|
|
386
|
+
// prompt asks for it at the top level of the checkout; a monorepo agent working in a service
|
|
387
|
+
// subdirectory may drop it in its cwd instead, so probe the checkout root first, then the cwd.
|
|
388
|
+
const prDescription = (await readPrDescription(dir)) ??
|
|
389
|
+
(workDir !== dir ? await readPrDescription(workDir) : undefined);
|
|
379
390
|
// Stop periodic checkpoints and let any in-flight one settle BEFORE the final
|
|
380
391
|
// push, so the two never run a concurrent `git push` to the same branch (the
|
|
381
392
|
// final push below is then a fresh attempt whose failure is the real signal).
|
|
@@ -438,6 +449,7 @@ async function finalizeCodingRun(args) {
|
|
|
438
449
|
...(usage ? { usage } : {}),
|
|
439
450
|
...(callMetrics ? { callMetrics } : {}),
|
|
440
451
|
...(effortReport ? { effortReport } : {}),
|
|
452
|
+
...(prDescription ? { prDescription } : {}),
|
|
441
453
|
};
|
|
442
454
|
}
|
|
443
455
|
// Ralph loop: run the programmatic completion command against the pushed/committed
|
|
@@ -698,7 +710,7 @@ export async function runMultiRepoCoding(job, opts = {}) {
|
|
|
698
710
|
multiRepo: true,
|
|
699
711
|
}, opts);
|
|
700
712
|
// Commit forgotten tracked edits, then push + open a PR for each repo the run actually changed.
|
|
701
|
-
const { primaryPushed, primaryPrUrl, peerPullRequests } = await pushMultiRepoLegs(legs, job, logger, opts);
|
|
713
|
+
const { primaryPushed, primaryPrUrl, peerPullRequests } = await pushMultiRepoLegs(legs, job, logger, opts, root);
|
|
702
714
|
const anyWork = primaryPushed || peerPullRequests.length > 0;
|
|
703
715
|
if (!anyWork) {
|
|
704
716
|
// Nothing changed in ANY repo. For the implementer this is a failure (as in the
|
|
@@ -799,6 +811,9 @@ async function prepareMultiRepoCheckouts(root, legs, job, logger, opts) {
|
|
|
799
811
|
await createBranch(dir, leg.workBranch, signal);
|
|
800
812
|
}
|
|
801
813
|
leg.dir = dir;
|
|
814
|
+
// Exclude the agent-authored PR-description sentinel locally (as the single-repo path does)
|
|
815
|
+
// so the agent's own `git add` can never stage the briefing into the PR it describes.
|
|
816
|
+
await excludeFromGit(dir, PR_DESCRIPTION_FILE, signal);
|
|
802
817
|
// The branch tip before the agent runs. Captured BEFORE the resume base refresh below so
|
|
803
818
|
// that refresh's merge commit counts as advancement and is pushed (as in the single-repo
|
|
804
819
|
// path). A fresh leg produced work iff its branch advances past this; a resumed leg already
|
|
@@ -844,7 +859,9 @@ async function prepareMultiRepoCheckouts(root, legs, job, logger, opts) {
|
|
|
844
859
|
* no PR; a read-only reference leg is never committed or pushed). Extracted so the multi-repo body
|
|
845
860
|
* stays small; returns the primary's push/PR state plus the peer PRs.
|
|
846
861
|
*/
|
|
847
|
-
async function pushMultiRepoLegs(legs, job, logger, opts
|
|
862
|
+
async function pushMultiRepoLegs(legs, job, logger, opts,
|
|
863
|
+
/** The workspace root the agent ran in — the fallback probe for the primary's briefing. */
|
|
864
|
+
root) {
|
|
848
865
|
const { signal } = opts;
|
|
849
866
|
opts.onPhase?.('push');
|
|
850
867
|
let primaryPushed = false;
|
|
@@ -855,6 +872,14 @@ async function pushMultiRepoLegs(legs, job, logger, opts) {
|
|
|
855
872
|
// guarantee (the spec carries no branch/PR, and the clone phase gave it no work branch).
|
|
856
873
|
if (leg.readOnly)
|
|
857
874
|
continue;
|
|
875
|
+
// Lift (and remove) the agent-authored PR description for THIS repo's PR before anything
|
|
876
|
+
// else touches the checkout — each sibling checkout carries its own briefing for its own PR.
|
|
877
|
+
// The agent's cwd here is the WORKSPACE ROOT rather than any one checkout, so an agent that
|
|
878
|
+
// read the prompt loosely may well have written a single briefing there instead. Fall back
|
|
879
|
+
// to it for the PRIMARY leg only: at the root there is nothing to say which repo it
|
|
880
|
+
// describes, and the primary is the one the run is actually about.
|
|
881
|
+
const agentPrDescription = (await readPrDescription(leg.dir)) ??
|
|
882
|
+
(leg.primary ? await readPrDescription(root) : undefined);
|
|
858
883
|
await commitTrackedEdits(leg.dir, job.commitMessage ?? leg.pr?.title ?? 'Agent changes', signal);
|
|
859
884
|
const advanced = await branchHasCommitsSince(leg.dir, leg.baseSha, signal);
|
|
860
885
|
let hasWork = advanced || leg.resumed;
|
|
@@ -884,7 +909,10 @@ async function pushMultiRepoLegs(legs, job, logger, opts) {
|
|
|
884
909
|
ghToken: leg.ghToken,
|
|
885
910
|
head: leg.workBranch,
|
|
886
911
|
base: leg.repo.baseBranch,
|
|
887
|
-
pr: leg.pr,
|
|
912
|
+
pr: applyPrDescription(leg.pr, agentPrDescription),
|
|
913
|
+
// See the single-repo call site: refresh a resumed leg's already-open PR, but only
|
|
914
|
+
// when the text is the agent's own briefing rather than the dispatch-time fallback.
|
|
915
|
+
...(agentPrDescription ? { refreshExisting: true } : {}),
|
|
888
916
|
apiBase: job.githubApiBase,
|
|
889
917
|
cloneUrl: leg.repo.cloneUrl,
|
|
890
918
|
...(leg.repo.provider ? { provider: leg.repo.provider } : {}),
|
package/dist/embed.js
CHANGED
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
// repo, write the agent context, point Pi at an OpenAI-compatible endpoint, run
|
|
5
5
|
// it, and inspect what changed. The HTTP server / job lifecycle stays internal;
|
|
6
6
|
// only the reusable primitives are exposed here.
|
|
7
|
-
export { PI_MAX_OUTPUT_TOKENS,
|
|
7
|
+
export { PI_MAX_OUTPUT_TOKENS, writePiModelsConfig, writeAgentsContext, runPi, summarizePiRun, parsePiOutput, parseTodoProgress, terminalRunError, } from './pi.js';
|
|
8
|
+
export { DEFAULT_PROGRESS_GUARD_LIMITS, progressGuardLimitsFromEnv, } from './progress-guard.js';
|
|
8
9
|
export { cloneRepo, createBranch, changedPathsFromPorcelain, hasAgentChanges, redactSecrets, } from './git.js';
|