@cat-factory/executor-harness 1.37.0 → 1.39.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/dist/agent.js +9 -3
- package/dist/git.js +12 -8
- package/dist/job.js +4 -0
- package/package.json +3 -3
- package/src/agent.ts +9 -3
- package/src/git.ts +12 -8
- package/src/job.ts +11 -0
package/dist/agent.js
CHANGED
|
@@ -529,14 +529,19 @@ async function runMultiRepoExplore(job, opts) {
|
|
|
529
529
|
{ repo: job.repo, cloneBranch: job.branch, ghToken: job.ghToken },
|
|
530
530
|
...peers.map((peer) => ({
|
|
531
531
|
repo: peer.repo,
|
|
532
|
-
|
|
532
|
+
// A read-only peer clones at its default branch (the bug-investigator) unless the job pins
|
|
533
|
+
// an explicit branch — the merger checks each peer out at its PR branch so the combined diff
|
|
534
|
+
// sees the PR change (`git diff origin/<base>...HEAD`).
|
|
535
|
+
cloneBranch: peer.cloneBranch ?? peer.repo.baseBranch,
|
|
533
536
|
ghToken: peer.ghToken ?? job.ghToken,
|
|
534
537
|
})),
|
|
535
538
|
].map((leg) => ({ ...leg, dirName: claimDir(leg.repo) }));
|
|
536
539
|
return withWorkspace('explore-multi', async (root) => {
|
|
537
540
|
// Clone phase: every repo (read-only) into its sibling dir under the workspace root. No
|
|
538
|
-
// work branch, no resume — the
|
|
539
|
-
//
|
|
541
|
+
// work branch, no resume — the agent only reads — so the legs are independent and clone in
|
|
542
|
+
// parallel (wall-clock is the slowest single clone, not the sum). `full` is honoured per the
|
|
543
|
+
// job (the merger needs full history so `git diff origin/<base>...HEAD` has the merge base;
|
|
544
|
+
// the bug-investigator leaves it shallow).
|
|
540
545
|
opts.onPhase?.('clone');
|
|
541
546
|
await Promise.all(legs.map(async (leg) => {
|
|
542
547
|
const dir = join(root, leg.dirName);
|
|
@@ -549,6 +554,7 @@ async function runMultiRepoExplore(job, opts) {
|
|
|
549
554
|
repo: { ...leg.repo, baseBranch: leg.cloneBranch },
|
|
550
555
|
ghToken: leg.ghToken,
|
|
551
556
|
dir,
|
|
557
|
+
full: job.full,
|
|
552
558
|
signal: opts.signal,
|
|
553
559
|
});
|
|
554
560
|
}));
|
package/dist/git.js
CHANGED
|
@@ -51,14 +51,18 @@ const GIT_TIMEOUT_MS = Math.max(GIT_TIMEOUT_FLOOR_MS, loadRunnerLimits().inactiv
|
|
|
51
51
|
// Emptying the helper list (`credential.helper=` with no value RESETS the multi-valued config,
|
|
52
52
|
// dropping the system/global/local helpers) removes GCM from the chain, so git falls back to
|
|
53
53
|
// the harness's own askpass helper — which returns the per-job PAT we already hold (see
|
|
54
|
-
// `authEnv`).
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
// `authEnv`). The token is never in argv; only this non-secret config is.
|
|
55
|
+
//
|
|
56
|
+
// DO NOT re-add `-c credential.interactive=false` here. It reads like harmless belt-and-braces
|
|
57
|
+
// but modern git (≥ 2.47, incl. the executor image + host git) HONORS `credential.interactive`
|
|
58
|
+
// and treats invoking GIT_ASKPASS as "interactive" — so with it set git SKIPS the askpass
|
|
59
|
+
// entirely and dies with "fatal: unable to get password from user", failing EVERY authenticated
|
|
60
|
+
// clone/push on both the native and container paths (it clones a public base repo fine — that
|
|
61
|
+
// needs no auth — then fails only at push, which is what makes it look intermittent). The GCM
|
|
62
|
+
// popup it was meant to belt-and-braces against is already fully handled by the emptied helper
|
|
63
|
+
// list above plus `GIT_TERMINAL_PROMPT=0` / `GCM_INTERACTIVE=never` in the env (see authEnv /
|
|
64
|
+
// nonInteractiveGitEnv). Exported so a unit test can pin that this arg never creeps back in.
|
|
65
|
+
export const NON_INTERACTIVE_CREDENTIAL_ARGS = ['-c', 'credential.helper='];
|
|
62
66
|
/**
|
|
63
67
|
* Env applied to git commands that DON'T carry {@link authEnv} (local ops like config/checkout/
|
|
64
68
|
* rev-parse). Keeps them from ever going interactive too — `GIT_TERMINAL_PROMPT=0` blocks the
|
package/dist/job.js
CHANGED
|
@@ -136,6 +136,10 @@ function parsePeerRepos(value) {
|
|
|
136
136
|
// read-only explore fan-out (bug-investigator) — validate it only when present.
|
|
137
137
|
if (e.newBranch !== undefined)
|
|
138
138
|
spec.newBranch = str(e.newBranch, `peerRepos[${i}].newBranch`);
|
|
139
|
+
// Read-only explore fan-out: the branch to check the peer out at (the merger's PR branch).
|
|
140
|
+
if (e.cloneBranch !== undefined) {
|
|
141
|
+
spec.cloneBranch = str(e.cloneBranch, `peerRepos[${i}].cloneBranch`);
|
|
142
|
+
}
|
|
139
143
|
if (typeof e.frameId === 'string' && e.frameId)
|
|
140
144
|
spec.frameId = e.frameId;
|
|
141
145
|
if (typeof e.ghToken === 'string' && e.ghToken)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Container payload: a thin TypeScript wrapper that runs the Pi coding agent against a cloned repo and opens a PR. Runs in the Cloudflare Container (and, in local native mode, as a host process); carries no secrets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"hono": "^4.12.27",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"vitest": "^4.1.9",
|
|
29
|
-
"@cat-factory/server": "0.
|
|
30
|
-
"@cat-factory/spend": "0.11.
|
|
29
|
+
"@cat-factory/server": "0.104.0",
|
|
30
|
+
"@cat-factory/spend": "0.11.21"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc -p tsconfig.json",
|
package/src/agent.ts
CHANGED
|
@@ -655,15 +655,20 @@ async function runMultiRepoExplore(job: AgentJob, opts: RunOptions): Promise<Age
|
|
|
655
655
|
{ repo: job.repo, cloneBranch: job.branch, ghToken: job.ghToken },
|
|
656
656
|
...peers.map((peer) => ({
|
|
657
657
|
repo: peer.repo,
|
|
658
|
-
|
|
658
|
+
// A read-only peer clones at its default branch (the bug-investigator) unless the job pins
|
|
659
|
+
// an explicit branch — the merger checks each peer out at its PR branch so the combined diff
|
|
660
|
+
// sees the PR change (`git diff origin/<base>...HEAD`).
|
|
661
|
+
cloneBranch: peer.cloneBranch ?? peer.repo.baseBranch,
|
|
659
662
|
ghToken: peer.ghToken ?? job.ghToken,
|
|
660
663
|
})),
|
|
661
664
|
].map((leg) => ({ ...leg, dirName: claimDir(leg.repo) }))
|
|
662
665
|
|
|
663
666
|
return withWorkspace('explore-multi', async (root) => {
|
|
664
667
|
// Clone phase: every repo (read-only) into its sibling dir under the workspace root. No
|
|
665
|
-
// work branch, no resume — the
|
|
666
|
-
//
|
|
668
|
+
// work branch, no resume — the agent only reads — so the legs are independent and clone in
|
|
669
|
+
// parallel (wall-clock is the slowest single clone, not the sum). `full` is honoured per the
|
|
670
|
+
// job (the merger needs full history so `git diff origin/<base>...HEAD` has the merge base;
|
|
671
|
+
// the bug-investigator leaves it shallow).
|
|
667
672
|
opts.onPhase?.('clone')
|
|
668
673
|
await Promise.all(
|
|
669
674
|
legs.map(async (leg) => {
|
|
@@ -677,6 +682,7 @@ async function runMultiRepoExplore(job: AgentJob, opts: RunOptions): Promise<Age
|
|
|
677
682
|
repo: { ...leg.repo, baseBranch: leg.cloneBranch },
|
|
678
683
|
ghToken: leg.ghToken,
|
|
679
684
|
dir,
|
|
685
|
+
full: job.full,
|
|
680
686
|
signal: opts.signal,
|
|
681
687
|
})
|
|
682
688
|
}),
|
package/src/git.ts
CHANGED
|
@@ -61,14 +61,18 @@ const GIT_TIMEOUT_MS = Math.max(
|
|
|
61
61
|
// Emptying the helper list (`credential.helper=` with no value RESETS the multi-valued config,
|
|
62
62
|
// dropping the system/global/local helpers) removes GCM from the chain, so git falls back to
|
|
63
63
|
// the harness's own askpass helper — which returns the per-job PAT we already hold (see
|
|
64
|
-
// `authEnv`).
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
// `authEnv`). The token is never in argv; only this non-secret config is.
|
|
65
|
+
//
|
|
66
|
+
// DO NOT re-add `-c credential.interactive=false` here. It reads like harmless belt-and-braces
|
|
67
|
+
// but modern git (≥ 2.47, incl. the executor image + host git) HONORS `credential.interactive`
|
|
68
|
+
// and treats invoking GIT_ASKPASS as "interactive" — so with it set git SKIPS the askpass
|
|
69
|
+
// entirely and dies with "fatal: unable to get password from user", failing EVERY authenticated
|
|
70
|
+
// clone/push on both the native and container paths (it clones a public base repo fine — that
|
|
71
|
+
// needs no auth — then fails only at push, which is what makes it look intermittent). The GCM
|
|
72
|
+
// popup it was meant to belt-and-braces against is already fully handled by the emptied helper
|
|
73
|
+
// list above plus `GIT_TERMINAL_PROMPT=0` / `GCM_INTERACTIVE=never` in the env (see authEnv /
|
|
74
|
+
// nonInteractiveGitEnv). Exported so a unit test can pin that this arg never creeps back in.
|
|
75
|
+
export const NON_INTERACTIVE_CREDENTIAL_ARGS = ['-c', 'credential.helper=']
|
|
72
76
|
|
|
73
77
|
/**
|
|
74
78
|
* Env applied to git commands that DON'T carry {@link authEnv} (local ops like config/checkout/
|
package/src/job.ts
CHANGED
|
@@ -78,6 +78,13 @@ export interface PeerRepoSpec {
|
|
|
78
78
|
* (the bug-investigator), which only clones the peer to read it and never pushes.
|
|
79
79
|
*/
|
|
80
80
|
newBranch?: string
|
|
81
|
+
/**
|
|
82
|
+
* The EXISTING branch to check the peer out at for a READ-ONLY explore fan-out (the `merger`
|
|
83
|
+
* scoring the combined diff clones each peer at its PR branch so the diff sees the PR change).
|
|
84
|
+
* Absent ⇒ the peer is cloned at its repo default branch (the bug-investigator). Ignored on the
|
|
85
|
+
* coding fan-out, which creates `newBranch` instead.
|
|
86
|
+
*/
|
|
87
|
+
cloneBranch?: string
|
|
81
88
|
/** Open a PR/MR in this peer when set AND the run changed the peer (skipped for a clean repo). */
|
|
82
89
|
pr?: PrSpec
|
|
83
90
|
/** Per-repo GitHub token; defaults to the job's `ghToken` (one installation per workspace today). */
|
|
@@ -236,6 +243,10 @@ function parsePeerRepos(value: unknown): PeerRepoSpec[] {
|
|
|
236
243
|
// `newBranch` is required for a coding fan-out (it pushes to it) but ABSENT for a
|
|
237
244
|
// read-only explore fan-out (bug-investigator) — validate it only when present.
|
|
238
245
|
if (e.newBranch !== undefined) spec.newBranch = str(e.newBranch, `peerRepos[${i}].newBranch`)
|
|
246
|
+
// Read-only explore fan-out: the branch to check the peer out at (the merger's PR branch).
|
|
247
|
+
if (e.cloneBranch !== undefined) {
|
|
248
|
+
spec.cloneBranch = str(e.cloneBranch, `peerRepos[${i}].cloneBranch`)
|
|
249
|
+
}
|
|
239
250
|
if (typeof e.frameId === 'string' && e.frameId) spec.frameId = e.frameId
|
|
240
251
|
if (typeof e.ghToken === 'string' && e.ghToken) spec.ghToken = e.ghToken
|
|
241
252
|
if (typeof e.pr === 'object' && e.pr !== null) {
|