@cat-factory/executor-harness 1.64.4 → 1.68.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 +33 -0
- package/dist/agent-capabilities.js +354 -0
- package/dist/agent-runner.js +111 -25
- package/dist/agent-shared.js +23 -0
- package/dist/agent.js +9 -139
- package/dist/bootstrap-mode.js +141 -0
- package/dist/claude-call-aggregator.js +6 -3
- package/dist/claude-stream.js +12 -6
- package/dist/coding-agent.js +85 -67
- package/dist/inline.js +29 -1
- package/dist/job.js +6 -75
- package/dist/pi-workspace.js +15 -14
- package/dist/pi.js +24 -16
- package/dist/subagents.js +14 -3
- package/package.json +4 -4
- package/src/agent-capabilities.ts +414 -0
- package/src/agent-runner.ts +155 -44
- package/src/agent-shared.ts +34 -0
- package/src/agent.ts +8 -165
- package/src/bootstrap-mode.ts +174 -0
- package/src/claude-call-aggregator.ts +8 -4
- package/src/claude-stream.ts +15 -7
- package/src/coding-agent.ts +102 -71
- package/src/inline.ts +34 -1
- package/src/job.ts +51 -94
- package/src/pi-workspace.ts +30 -20
- package/src/pi.ts +38 -17
- package/src/subagents.ts +17 -3
package/dist/job.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseValidationChecksSpec, } from './validation-checks.js';
|
|
2
2
|
import { parseReproductionSpec, } from './reproduction-proof.js';
|
|
3
|
+
import { parseMcpServerSpecs, parseSkillSpecs, } from './agent-capabilities.js';
|
|
3
4
|
function str(value, path) {
|
|
4
5
|
if (typeof value !== 'string' || value.length === 0) {
|
|
5
6
|
throw new Error(`Invalid job: '${path}' must be a non-empty string`);
|
|
@@ -411,78 +412,6 @@ function parseContextFiles(value) {
|
|
|
411
412
|
}
|
|
412
413
|
return files;
|
|
413
414
|
}
|
|
414
|
-
/**
|
|
415
|
-
* Sanitize a skill resource's relative path: keep the subdirectory structure (so
|
|
416
|
-
* `templates/report.md` materialises nested) but reject anything that could escape the skill
|
|
417
|
-
* directory — absolute paths, `..` traversal, backslashes, empty/dot segments. Returns undefined
|
|
418
|
-
* for an unsafe path (the resource is then dropped).
|
|
419
|
-
*/
|
|
420
|
-
function sanitizeSkillRelPath(value) {
|
|
421
|
-
if (typeof value !== 'string')
|
|
422
|
-
return undefined;
|
|
423
|
-
const segments = value.replace(/\\/g, '/').split('/');
|
|
424
|
-
const clean = [];
|
|
425
|
-
for (const seg of segments) {
|
|
426
|
-
if (seg === '' || seg === '.')
|
|
427
|
-
continue;
|
|
428
|
-
if (seg === '..')
|
|
429
|
-
return undefined;
|
|
430
|
-
// Same character class as a context-file name, per segment.
|
|
431
|
-
const c = seg.replace(/[^A-Za-z0-9._-]/g, '');
|
|
432
|
-
if (!c || c === '.' || c === '..' || c.startsWith('.'))
|
|
433
|
-
return undefined;
|
|
434
|
-
clean.push(c);
|
|
435
|
-
}
|
|
436
|
-
return clean.length ? clean.join('/') : undefined;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Fallback native-skill directory name when the authored name has no id-safe characters (e.g. a
|
|
440
|
-
* purely non-ASCII skill name). The name is only a path segment / manifest label, so a safe
|
|
441
|
-
* default keeps the skill installable rather than dropping it — which, on the claude-code path,
|
|
442
|
-
* would leave the prompt pointing at a skill that was never installed (a blind run).
|
|
443
|
-
*/
|
|
444
|
-
const FALLBACK_SKILL_NAME = 'skill';
|
|
445
|
-
/** A skill's own directory name, sanitized to a safe single path segment (undefined if empty). */
|
|
446
|
-
function sanitizeSkillName(value) {
|
|
447
|
-
if (typeof value !== 'string')
|
|
448
|
-
return undefined;
|
|
449
|
-
const base = value.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
450
|
-
const cleaned = base.replace(/[^A-Za-z0-9._-]/g, '');
|
|
451
|
-
if (!cleaned || cleaned === '.' || cleaned === '..' || cleaned.startsWith('.'))
|
|
452
|
-
return undefined;
|
|
453
|
-
return cleaned;
|
|
454
|
-
}
|
|
455
|
-
/** Validate the optional `skill` field, or undefined when absent/malformed. */
|
|
456
|
-
function parseSkillSpec(value) {
|
|
457
|
-
if (typeof value !== 'object' || value === null)
|
|
458
|
-
return undefined;
|
|
459
|
-
const o = value;
|
|
460
|
-
const instructions = typeof o.instructions === 'string' ? o.instructions : undefined;
|
|
461
|
-
// No instructions ⇒ there is nothing to run — drop the skill (the prompt still carries the
|
|
462
|
-
// folded-in directive on the Pi/codex path). An unsafe/empty NAME only affects the install
|
|
463
|
-
// directory, so fall back to a safe default rather than dropping the whole skill.
|
|
464
|
-
if (!instructions)
|
|
465
|
-
return undefined;
|
|
466
|
-
const name = sanitizeSkillName(o.name) ?? FALLBACK_SKILL_NAME;
|
|
467
|
-
const description = typeof o.description === 'string' ? o.description : '';
|
|
468
|
-
const resources = [];
|
|
469
|
-
if (Array.isArray(o.resources)) {
|
|
470
|
-
const used = new Set();
|
|
471
|
-
for (const entry of o.resources) {
|
|
472
|
-
if (typeof entry !== 'object' || entry === null)
|
|
473
|
-
continue;
|
|
474
|
-
const e = entry;
|
|
475
|
-
const relPath = sanitizeSkillRelPath(e.relPath);
|
|
476
|
-
if (!relPath || used.has(relPath))
|
|
477
|
-
continue;
|
|
478
|
-
if (typeof e.content !== 'string')
|
|
479
|
-
continue;
|
|
480
|
-
used.add(relPath);
|
|
481
|
-
resources.push({ relPath, content: e.content });
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return { name, description, instructions, resources };
|
|
485
|
-
}
|
|
486
415
|
/** Parse the explore-mode infra stand-up spec, or undefined when absent/unrecognised. */
|
|
487
416
|
function parseAgentInfraSpec(value) {
|
|
488
417
|
if (typeof value !== 'object' || value === null)
|
|
@@ -669,7 +598,8 @@ export function parseAgentJob(input) {
|
|
|
669
598
|
bootstrap: parseAgentBootstrapSpec(o.bootstrap),
|
|
670
599
|
contextFiles: parseContextFiles(o.contextFiles),
|
|
671
600
|
packageRegistries: parsePackageRegistries(o.packageRegistries),
|
|
672
|
-
|
|
601
|
+
skills: parseSkillSpecs(o.skills),
|
|
602
|
+
mcpServers: parseMcpServerSpecs(o.mcpServers),
|
|
673
603
|
testSecrets: parseTestSecrets(o.testSecrets),
|
|
674
604
|
guardLimits: parseGuardLimits(o.guardLimits),
|
|
675
605
|
validation: parseValidationSpec(o.validation),
|
|
@@ -731,7 +661,7 @@ function parseAgentPrSpec(raw) {
|
|
|
731
661
|
* literal doesn't blow the complexity budget; behaviour is byte-identical (spread order preserved).
|
|
732
662
|
*/
|
|
733
663
|
function assembleAgentJob(o, mode, agentField, parts) {
|
|
734
|
-
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, packageRegistries,
|
|
664
|
+
const { output, pr, infra, peerRepos, referenceRepos, referenceBranches, bootstrap, contextFiles, packageRegistries, skills, mcpServers, testSecrets, guardLimits, validation, validationChecks, reproduction, reviewPrNumber, } = parts;
|
|
735
665
|
const repo = (o.repo ?? {});
|
|
736
666
|
return {
|
|
737
667
|
jobId: str(o.jobId, 'jobId'),
|
|
@@ -748,7 +678,8 @@ function assembleAgentJob(o, mode, agentField, parts) {
|
|
|
748
678
|
...(output ? { output } : {}),
|
|
749
679
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
750
680
|
...(packageRegistries.length ? { packageRegistries } : {}),
|
|
751
|
-
...(
|
|
681
|
+
...(skills ? { skills } : {}),
|
|
682
|
+
...(mcpServers ? { mcpServers } : {}),
|
|
752
683
|
...(testSecrets.length ? { testSecrets } : {}),
|
|
753
684
|
...(infra ? { infra } : {}),
|
|
754
685
|
...(pr ? { pr } : {}),
|
package/dist/pi-workspace.js
CHANGED
|
@@ -142,14 +142,14 @@ export async function runAgentInWorkspace(spec, opts = {}) {
|
|
|
142
142
|
// harness paths; kept out of the agent's commits via a local git exclude entry.
|
|
143
143
|
const contextFiles = spec.contextFiles ?? [];
|
|
144
144
|
await materializeContextFiles(spec.dir, contextFiles);
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
if (spec.
|
|
152
|
-
await materializeSkillResources(spec.dir, spec.
|
|
145
|
+
// Skills: claude-code installs them natively into its ISOLATED config dir, so it reads from
|
|
146
|
+
// there. Everything else reads the checkout, so materialise each skill's resources under
|
|
147
|
+
// `.cat-context/skill/<name>/` (their instructions are folded into the prompt by the backend) —
|
|
148
|
+
// Pi, codex, and AMBIENT claude-code, which has no isolated config dir to install into (the
|
|
149
|
+
// runner refuses to write a skill into the developer's own `~/.claude`; see `runClaudeCode`).
|
|
150
|
+
// Resource-free skills are a no-op here.
|
|
151
|
+
if (spec.skills?.length && !installsSkillNatively(spec)) {
|
|
152
|
+
await materializeSkillResources(spec.dir, spec.skills);
|
|
153
153
|
}
|
|
154
154
|
// Subscription harnesses (Claude Code / Codex) authenticate with the leased
|
|
155
155
|
// token and talk direct to the vendor — no proxy config, no AGENTS.md. The
|
|
@@ -169,7 +169,8 @@ export async function runAgentInWorkspace(spec, opts = {}) {
|
|
|
169
169
|
...(spec.subscriptionToken ? { subscriptionToken: spec.subscriptionToken } : {}),
|
|
170
170
|
subscriptionBaseUrl: spec.subscriptionBaseUrl,
|
|
171
171
|
...(spec.ambientAuth ? { ambientAuth: true } : {}),
|
|
172
|
-
...(spec.
|
|
172
|
+
...(spec.skills?.length ? { skills: spec.skills } : {}),
|
|
173
|
+
...(spec.mcpServers?.length ? { mcpServers: spec.mcpServers } : {}),
|
|
173
174
|
...(opts.agentEnv ? { extraEnv: opts.agentEnv } : {}),
|
|
174
175
|
signal: opts.signal,
|
|
175
176
|
// Run the SAME no-progress guard Pi gets (previously claude-code/codex had none): env
|
|
@@ -240,12 +241,12 @@ export async function runAgentInWorkspace(spec, opts = {}) {
|
|
|
240
241
|
return withEffortReport(spec.dir, piOutcome);
|
|
241
242
|
}
|
|
242
243
|
/**
|
|
243
|
-
* Whether the claude-code runner will install this run's
|
|
244
|
-
*
|
|
244
|
+
* Whether the claude-code runner will install this run's skills natively (into the CLI's config
|
|
245
|
+
* dir) rather than the caller materialising them into the checkout. True ONLY for a
|
|
245
246
|
* leased-credential claude-code run, which gets a throwaway per-run config home. An AMBIENT run
|
|
246
|
-
* uses the developer's own `~/.claude`, which the runner will not write a
|
|
247
|
-
*
|
|
248
|
-
*
|
|
247
|
+
* uses the developer's own `~/.claude`, which the runner will not write a skill into — it would
|
|
248
|
+
* outlive the run in their personal setup, and two concurrent jobs carrying same-named skills
|
|
249
|
+
* would overwrite each other's.
|
|
249
250
|
*/
|
|
250
251
|
export function installsSkillNatively(spec) {
|
|
251
252
|
return spec.harness === 'claude-code' && !spec.ambientAuth;
|
package/dist/pi.js
CHANGED
|
@@ -203,26 +203,34 @@ export async function materializeContextFiles(cwd, files) {
|
|
|
203
203
|
// No writable .git/info; the files simply stay untracked (still not auto-added on most flows).
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
/** Subdirectory of {@link CONTEXT_DIR} where a
|
|
206
|
+
/** Subdirectory of {@link CONTEXT_DIR} where a skill's resources are materialised, per skill. */
|
|
207
207
|
export const SKILL_CONTEXT_SUBDIR = 'skill';
|
|
208
208
|
/**
|
|
209
|
-
* Materialise
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
209
|
+
* Materialise the run's skills' RESOURCE files under `.cat-context/skill/<name>/` in the checkout
|
|
210
|
+
* — the path for every run that does NOT get a native install: Pi, codex, and ambient claude-code
|
|
211
|
+
* (no isolated `CLAUDE_CONFIG_DIR` to install into). Their agents read the checkout, and the
|
|
212
|
+
* skills' instructions are folded into their prompt by the backend (`renderSkillsForHarness`,
|
|
213
|
+
* which keys off ambient auth as well as the harness).
|
|
214
|
+
*
|
|
215
|
+
* Each skill gets its OWN subdirectory: several skills can apply to one run (a step's pick plus
|
|
216
|
+
* the kind's declared playbooks), and a flat directory would let two skills' `templates/report.md`
|
|
217
|
+
* overwrite each other — silently handing the agent the wrong template. The names were sanitized
|
|
218
|
+
* to a single safe path segment at the job boundary, as were the resource sub-paths (no
|
|
219
|
+
* traversal), so nested dirs are created as needed. Kept out of the agent's commits via the same
|
|
220
|
+
* `.cat-context/` git exclude entry. Skills with no resource bodies are a no-op.
|
|
216
221
|
*/
|
|
217
|
-
export async function materializeSkillResources(cwd,
|
|
218
|
-
|
|
222
|
+
export async function materializeSkillResources(cwd, skills) {
|
|
223
|
+
const withResources = skills.filter((s) => s.resources.length);
|
|
224
|
+
if (!withResources.length)
|
|
219
225
|
return;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
for (const skill of withResources) {
|
|
227
|
+
const dir = join(cwd, CONTEXT_DIR, SKILL_CONTEXT_SUBDIR, skill.name);
|
|
228
|
+
await mkdir(dir, { recursive: true });
|
|
229
|
+
for (const r of skill.resources) {
|
|
230
|
+
const dest = join(dir, r.relPath);
|
|
231
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
232
|
+
await writeFile(dest, r.content, 'utf8');
|
|
233
|
+
}
|
|
226
234
|
}
|
|
227
235
|
const gitRoot = await findGitRoot(cwd);
|
|
228
236
|
if (!gitRoot)
|
package/dist/subagents.js
CHANGED
|
@@ -137,7 +137,13 @@ export function startSubagentWatcher(root, opts) {
|
|
|
137
137
|
return;
|
|
138
138
|
const message = event.message;
|
|
139
139
|
const u = claudeCallUsage(message.usage);
|
|
140
|
-
|
|
140
|
+
// Every input class counts towards "did this turn report usage at all": a turn riding a
|
|
141
|
+
// warm cache legitimately reports 0 fresh input, and skipping it would drop precisely the
|
|
142
|
+
// cache-heavy calls this telemetry exists to weigh.
|
|
143
|
+
if (u.inputTokens === 0 &&
|
|
144
|
+
u.cacheReadTokens === 0 &&
|
|
145
|
+
u.cacheWriteTokens === 0 &&
|
|
146
|
+
u.outputTokens === 0)
|
|
141
147
|
return;
|
|
142
148
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
143
149
|
const { text, reasoning } = claudeAssistantContent(content);
|
|
@@ -154,11 +160,16 @@ export function startSubagentWatcher(root, opts) {
|
|
|
154
160
|
responseText: redactBody(text, secrets),
|
|
155
161
|
reasoningText: redactBody(reasoning, secrets),
|
|
156
162
|
inputTokens: u.inputTokens,
|
|
157
|
-
|
|
163
|
+
cacheReadTokens: u.cacheReadTokens,
|
|
164
|
+
cacheWriteTokens: u.cacheWriteTokens,
|
|
158
165
|
outputTokens: u.outputTokens,
|
|
159
166
|
finishReason: typeof message.stop_reason === 'string' ? message.stop_reason : null,
|
|
160
167
|
}, opts.onCallMetric);
|
|
161
|
-
usage
|
|
168
|
+
// The run-level `usage` is the COARSE rotation-window weight, which counts every billed
|
|
169
|
+
// input bucket — unlike the per-call metric above, whose `inputTokens` is fresh-only. Sum
|
|
170
|
+
// all three classes back together here or a cache-heavy subagent looks nearly free to the
|
|
171
|
+
// rotation.
|
|
172
|
+
usage.inputTokens += u.inputTokens + u.cacheReadTokens + u.cacheWriteTokens;
|
|
162
173
|
usage.outputTokens += u.outputTokens;
|
|
163
174
|
};
|
|
164
175
|
const NEWLINE = 0x0a;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.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,9 +26,9 @@
|
|
|
26
26
|
"hono": "^4.12.32",
|
|
27
27
|
"typescript": "7.0.2",
|
|
28
28
|
"vitest": "^4.1.10",
|
|
29
|
-
"@cat-factory/kernel": "0.
|
|
30
|
-
"@cat-factory/server": "0.
|
|
31
|
-
"@cat-factory/spend": "0.12.
|
|
29
|
+
"@cat-factory/kernel": "0.176.0",
|
|
30
|
+
"@cat-factory/server": "0.166.0",
|
|
31
|
+
"@cat-factory/spend": "0.12.105"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc -p tsconfig.json",
|