@cat-factory/executor-harness 1.127.1 → 1.131.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.d.ts CHANGED
@@ -53,6 +53,22 @@ export declare function testSecretEnv(secrets: TestSecretSpec[] | undefined): Re
53
53
  * `process.env` — and a second copy is a second place to forget the redaction.
54
54
  */
55
55
  export declare function secretEnv(secrets: TestSecretSpec[] | undefined): Record<string, string>;
56
+ /**
57
+ * Which refs a REUSED (warm-pool) explore checkout must end up holding: the branch to explore, and
58
+ * the repo's own base branch beside it.
59
+ *
60
+ * Its own function, and named, because the bug it removes is a swap between two branch names both
61
+ * in scope at the call site, and nothing downstream can tell them apart. A read-only reviewer's
62
+ * whole instruction is `git diff origin/<base>...HEAD`, so the base ref has to be as fresh as the
63
+ * branch: passing the explored branch as the base collapses the two refspecs into one, leaves
64
+ * `origin/<base>` at whatever tip the pool dir was first cloned at, and moves the merge base back
65
+ * to that tip. The diff then reports every commit merged into base since as part of the change
66
+ * under review, which is wrong in the direction nothing notices.
67
+ */
68
+ export declare function exploreCheckoutRefs(job: Pick<AgentJob, 'branch' | 'repo'>): {
69
+ branch: string;
70
+ baseBranch: string;
71
+ };
56
72
  /**
57
73
  * Whether a Ralph iteration ({@link AgentJob.validation} set) landed on a MULTI-REPO job (writable
58
74
  * peer repos or read-only reference repos). The post-commit validation command is only wired into
package/dist/agent.js CHANGED
@@ -383,6 +383,21 @@ export function secretEnv(secrets) {
383
383
  registerKnownSecrets(secrets.map((s) => s.value));
384
384
  return Object.fromEntries(secrets.map(({ key, value }) => [key, value]));
385
385
  }
386
+ /**
387
+ * Which refs a REUSED (warm-pool) explore checkout must end up holding: the branch to explore, and
388
+ * the repo's own base branch beside it.
389
+ *
390
+ * Its own function, and named, because the bug it removes is a swap between two branch names both
391
+ * in scope at the call site, and nothing downstream can tell them apart. A read-only reviewer's
392
+ * whole instruction is `git diff origin/<base>...HEAD`, so the base ref has to be as fresh as the
393
+ * branch: passing the explored branch as the base collapses the two refspecs into one, leaves
394
+ * `origin/<base>` at whatever tip the pool dir was first cloned at, and moves the merge base back
395
+ * to that tip. The diff then reports every commit merged into base since as part of the change
396
+ * under review, which is wrong in the direction nothing notices.
397
+ */
398
+ export function exploreCheckoutRefs(job) {
399
+ return { branch: job.branch, baseBranch: job.repo.baseBranch };
400
+ }
386
401
  /**
387
402
  * Read-only exploration: clone `branch`, run the agent making no edits, and return its
388
403
  * prose report — or, when `output.kind==='structured'`, the parsed JSON object as
@@ -408,12 +423,17 @@ async function runExploreMode(job, opts) {
408
423
  let workDir;
409
424
  if (job.persistentCheckout) {
410
425
  logger.info('agent(explore): preparing reused checkout');
426
+ // Both refs, resolved by {@link exploreCheckoutRefs}: which one is the base is the whole
427
+ // decision, so it is made there rather than inline here.
428
+ //
429
+ // `job.full` is deliberately not consulted: the fresh-clone leg inside
430
+ // `prepareExistingCheckout` always clones with full history, so a reused checkout already
431
+ // has the merge base a shallow explore clone would not.
411
432
  await prepareExistingCheckout({
412
433
  dir,
413
434
  repo: job.repo,
414
435
  ghToken: job.ghToken,
415
- branch: job.branch,
416
- baseBranch: job.branch,
436
+ ...exploreCheckoutRefs(job),
417
437
  existing: true,
418
438
  signal: opts.signal,
419
439
  });
@@ -841,6 +861,10 @@ export function buildSingleRepoCodingSpec(job, pushBranch) {
841
861
  guardLimits: job.guardLimits,
842
862
  ...(job.persistentCheckout ? { persistentCheckout: true } : {}),
843
863
  ...(job.streamFollowUps ? { streamFollowUps: true } : {}),
864
+ // The task's linked documents, materialised into `.cat-context/` for the agent to read on
865
+ // demand. Every other caller of `runAgentInWorkspace` forwards these; this spread was the one
866
+ // that did not, so the implementer's prompt named files its checkout had never been given.
867
+ ...(job.contextFiles?.length ? { contextFiles: job.contextFiles } : {}),
844
868
  // Whether a pull request will open at all is exactly `job.pr` (see the `if (job.pr)` guard in
845
869
  // `runSingleRepoCoding`), and it is what decides whether the repo's PR template is worth
846
870
  // resolving. Read off the same field rather than a new job-body flag, so the two can't drift.
@@ -1,4 +1,4 @@
1
- import type { HarnessAuthFields, ImageManifestSpec, RepoSpec, SkillSpec, McpServerSpec } from './job.js';
1
+ import type { ContextFileSpec, HarnessAuthFields, ImageManifestSpec, RepoSpec, SkillSpec, McpServerSpec } from './job.js';
2
2
  import type { HarnessCallMetric } from './pi.js';
3
3
  import type { PiRunStats } from './pi-reduction.js';
4
4
  import { type EffortReport } from './effort.js';
@@ -99,6 +99,21 @@ export interface CodingAgentSpec extends HarnessAuthFields {
99
99
  * `docs/initiatives/bugfix-reproduction-proof.md`.
100
100
  */
101
101
  reproduction?: ReproductionSpec;
102
+ /**
103
+ * The run's LINKED CONTEXT documents (the task's attached brief, an RFC, a tracker issue body),
104
+ * materialised into `.cat-context/<path>` in the checkout and enumerated for the agent in its
105
+ * `AGENTS.md` context block. Absent ⇒ none.
106
+ *
107
+ * Carried here because the CODING agent is the one that needs them most and was the ONE path
108
+ * that dropped them: every sibling caller of {@link runAgentInWorkspace} (the explore paths, the
109
+ * conflict path, `multi-repo-coding.ts`) forwarded `job.contextFiles` and this one did not, so a
110
+ * task whose brief was too long for `description` and therefore rode an attached document
111
+ * reached the implementer as a prompt naming `.cat-context/<file>.md` and a checkout with no
112
+ * such directory. The agent then rebuilt the brief from whatever summary the prompt carried and
113
+ * reported the gap as a follow-up question, which is the most expensive way to discover a
114
+ * missing field spread.
115
+ */
116
+ contextFiles?: ContextFileSpec[];
102
117
  /**
103
118
  * The skills to make available for this run — a `skill` step's pick and/or the running kind's
104
119
  * declared playbooks. Threaded into {@link runAgentInWorkspace}, which installs them
@@ -246,6 +246,8 @@ export async function runCodingAgent(spec, opts = {}) {
246
246
  webToolsGuidance: spec.webToolsGuidance,
247
247
  webSearchProxy: spec.webSearchProxy,
248
248
  guardLimits: spec.guardLimits,
249
+ // Materialised into `.cat-context/` and enumerated in the agent's AGENTS.md block.
250
+ ...(spec.contextFiles?.length ? { contextFiles: spec.contextFiles } : {}),
249
251
  ...(spec.skills?.length ? { skills: spec.skills } : {}),
250
252
  ...(spec.mcpServers?.length ? { mcpServers: spec.mcpServers } : {}),
251
253
  ...(spec.referenceScreenshots
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/executor-harness",
3
- "version": "1.127.1",
3
+ "version": "1.131.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",
@@ -25,14 +25,14 @@
25
25
  "access": "public"
26
26
  },
27
27
  "devDependencies": {
28
+ "@cat-factory/kernel": "0.318.0",
29
+ "@cat-factory/server": "0.305.0",
30
+ "@cat-factory/spend": "0.16.12",
28
31
  "@hono/node-server": "^2.1.1",
29
32
  "@types/node": "^26.2.0",
30
33
  "hono": "^4.13.3",
31
34
  "typescript": "7.0.2",
32
- "vitest": "^4.1.11",
33
- "@cat-factory/kernel": "0.314.1",
34
- "@cat-factory/server": "0.299.1",
35
- "@cat-factory/spend": "0.16.7"
35
+ "vitest": "^4.1.11"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -p tsconfig.json",
package/src/agent.ts CHANGED
@@ -464,6 +464,25 @@ export function secretEnv(secrets: TestSecretSpec[] | undefined): Record<string,
464
464
  return Object.fromEntries(secrets.map(({ key, value }) => [key, value]))
465
465
  }
466
466
 
467
+ /**
468
+ * Which refs a REUSED (warm-pool) explore checkout must end up holding: the branch to explore, and
469
+ * the repo's own base branch beside it.
470
+ *
471
+ * Its own function, and named, because the bug it removes is a swap between two branch names both
472
+ * in scope at the call site, and nothing downstream can tell them apart. A read-only reviewer's
473
+ * whole instruction is `git diff origin/<base>...HEAD`, so the base ref has to be as fresh as the
474
+ * branch: passing the explored branch as the base collapses the two refspecs into one, leaves
475
+ * `origin/<base>` at whatever tip the pool dir was first cloned at, and moves the merge base back
476
+ * to that tip. The diff then reports every commit merged into base since as part of the change
477
+ * under review, which is wrong in the direction nothing notices.
478
+ */
479
+ export function exploreCheckoutRefs(job: Pick<AgentJob, 'branch' | 'repo'>): {
480
+ branch: string
481
+ baseBranch: string
482
+ } {
483
+ return { branch: job.branch, baseBranch: job.repo.baseBranch }
484
+ }
485
+
467
486
  /**
468
487
  * Read-only exploration: clone `branch`, run the agent making no edits, and return its
469
488
  * prose report — or, when `output.kind==='structured'`, the parsed JSON object as
@@ -490,12 +509,17 @@ async function runExploreMode(job: AgentJob, opts: RunOptions): Promise<AgentRes
490
509
  let workDir: string
491
510
  if (job.persistentCheckout) {
492
511
  logger.info('agent(explore): preparing reused checkout')
512
+ // Both refs, resolved by {@link exploreCheckoutRefs}: which one is the base is the whole
513
+ // decision, so it is made there rather than inline here.
514
+ //
515
+ // `job.full` is deliberately not consulted: the fresh-clone leg inside
516
+ // `prepareExistingCheckout` always clones with full history, so a reused checkout already
517
+ // has the merge base a shallow explore clone would not.
493
518
  await prepareExistingCheckout({
494
519
  dir,
495
520
  repo: job.repo,
496
521
  ghToken: job.ghToken,
497
- branch: job.branch,
498
- baseBranch: job.branch,
522
+ ...exploreCheckoutRefs(job),
499
523
  existing: true,
500
524
  signal: opts.signal,
501
525
  })
@@ -1000,6 +1024,10 @@ export function buildSingleRepoCodingSpec(
1000
1024
  guardLimits: job.guardLimits,
1001
1025
  ...(job.persistentCheckout ? { persistentCheckout: true } : {}),
1002
1026
  ...(job.streamFollowUps ? { streamFollowUps: true } : {}),
1027
+ // The task's linked documents, materialised into `.cat-context/` for the agent to read on
1028
+ // demand. Every other caller of `runAgentInWorkspace` forwards these; this spread was the one
1029
+ // that did not, so the implementer's prompt named files its checkout had never been given.
1030
+ ...(job.contextFiles?.length ? { contextFiles: job.contextFiles } : {}),
1003
1031
  // Whether a pull request will open at all is exactly `job.pr` (see the `if (job.pr)` guard in
1004
1032
  // `runSingleRepoCoding`), and it is what decides whether the repo's PR template is worth
1005
1033
  // resolving. Read off the same field rather than a new job-body flag, so the two can't drift.
@@ -2,6 +2,7 @@ import { mkdir } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { runCapturedCommand } from './captured-command.js'
4
4
  import type {
5
+ ContextFileSpec,
5
6
  HarnessAuthFields,
6
7
  ImageManifestSpec,
7
8
  RepoSpec,
@@ -162,6 +163,21 @@ export interface CodingAgentSpec extends HarnessAuthFields {
162
163
  * `docs/initiatives/bugfix-reproduction-proof.md`.
163
164
  */
164
165
  reproduction?: ReproductionSpec
166
+ /**
167
+ * The run's LINKED CONTEXT documents (the task's attached brief, an RFC, a tracker issue body),
168
+ * materialised into `.cat-context/<path>` in the checkout and enumerated for the agent in its
169
+ * `AGENTS.md` context block. Absent ⇒ none.
170
+ *
171
+ * Carried here because the CODING agent is the one that needs them most and was the ONE path
172
+ * that dropped them: every sibling caller of {@link runAgentInWorkspace} (the explore paths, the
173
+ * conflict path, `multi-repo-coding.ts`) forwarded `job.contextFiles` and this one did not, so a
174
+ * task whose brief was too long for `description` and therefore rode an attached document
175
+ * reached the implementer as a prompt naming `.cat-context/<file>.md` and a checkout with no
176
+ * such directory. The agent then rebuilt the brief from whatever summary the prompt carried and
177
+ * reported the gap as a follow-up question, which is the most expensive way to discover a
178
+ * missing field spread.
179
+ */
180
+ contextFiles?: ContextFileSpec[]
165
181
  /**
166
182
  * The skills to make available for this run — a `skill` step's pick and/or the running kind's
167
183
  * declared playbooks. Threaded into {@link runAgentInWorkspace}, which installs them
@@ -508,6 +524,8 @@ export async function runCodingAgent(
508
524
  webToolsGuidance: spec.webToolsGuidance,
509
525
  webSearchProxy: spec.webSearchProxy,
510
526
  guardLimits: spec.guardLimits,
527
+ // Materialised into `.cat-context/` and enumerated in the agent's AGENTS.md block.
528
+ ...(spec.contextFiles?.length ? { contextFiles: spec.contextFiles } : {}),
511
529
  ...(spec.skills?.length ? { skills: spec.skills } : {}),
512
530
  ...(spec.mcpServers?.length ? { mcpServers: spec.mcpServers } : {}),
513
531
  ...(spec.referenceScreenshots