@cat-factory/executor-harness 1.62.0 → 1.64.2
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-runner.js +92 -18
- package/dist/claude-stream.js +18 -0
- package/dist/embed.js +2 -1
- package/dist/pi-workspace.js +35 -2
- package/dist/pi.js +15 -184
- package/dist/progress-guard.js +211 -0
- package/dist/progress.js +122 -13
- package/dist/subagents.js +1 -50
- package/package.json +4 -4
- package/src/agent-runner.ts +99 -17
- package/src/claude-stream.ts +19 -0
- package/src/coding-agent.ts +1 -1
- package/src/embed.ts +5 -3
- package/src/pi-workspace.ts +40 -4
- package/src/pi.ts +26 -252
- package/src/progress-guard.ts +285 -0
- package/src/progress.ts +138 -14
- package/src/subagents.ts +7 -16
package/src/agent-runner.ts
CHANGED
|
@@ -19,11 +19,13 @@ import {
|
|
|
19
19
|
type PiRunStats,
|
|
20
20
|
type TodoProgress,
|
|
21
21
|
} from './pi.js'
|
|
22
|
+
import { ProgressGuard, type ProgressGuardLimits } from './progress-guard.js'
|
|
22
23
|
import { killChildProcess, spawnDetached } from './process.js'
|
|
23
24
|
import { redact, secretsToRedact } from './redact.js'
|
|
24
25
|
import { createSliceTracker, startSubagentWatcher } from './subagents.js'
|
|
25
26
|
import {
|
|
26
27
|
createTaskPlanTracker,
|
|
28
|
+
mergeProgress,
|
|
27
29
|
normalizeStatus,
|
|
28
30
|
pickProgress,
|
|
29
31
|
toProgress,
|
|
@@ -101,6 +103,17 @@ export interface SubscriptionRunOptions {
|
|
|
101
103
|
extraEnv?: Record<string, string>
|
|
102
104
|
/** Aborting this kills the CLI (the job's inactivity/max-duration watchdog). */
|
|
103
105
|
signal?: AbortSignal
|
|
106
|
+
/**
|
|
107
|
+
* Fully-resolved no-progress guard limits (env defaults merged loosen-only with the kind's
|
|
108
|
+
* tuning + any complexity-scaled allowance). When set, the claude-code runner runs the SAME
|
|
109
|
+
* {@link ProgressGuard} as Pi over the CLI's tool stream and kills a run that has plainly
|
|
110
|
+
* stopped making progress (no-edit probing, error-retry loop, web rabbit-hole) rather than
|
|
111
|
+
* letting it burn the whole wall-clock budget. Omitted ⇒ the guard is disabled for this run
|
|
112
|
+
* (only the external watchdog bounds it), preserving the pre-guard behaviour.
|
|
113
|
+
*/
|
|
114
|
+
guardLimits?: ProgressGuardLimits
|
|
115
|
+
/** Whether this run is expected to edit files (false for assess-only runs); gates the no-edit bound. */
|
|
116
|
+
expectsEdits?: boolean
|
|
104
117
|
/** Called on every chunk of CLI output, so the watchdog sees the agent is alive. */
|
|
105
118
|
onActivity?: () => void
|
|
106
119
|
/** Called with the latest subtask counts each time the CLI updates its todo/plan list. */
|
|
@@ -154,7 +167,7 @@ function streamCli(
|
|
|
154
167
|
opts: SubscriptionRunOptions,
|
|
155
168
|
env: Record<string, string>,
|
|
156
169
|
secrets: string[],
|
|
157
|
-
onEvent: (event: Record<string, unknown
|
|
170
|
+
onEvent: (event: Record<string, unknown>, meta?: { final?: boolean }) => void,
|
|
158
171
|
): Promise<{ stderrTail: string }> {
|
|
159
172
|
const { command, args } = cli
|
|
160
173
|
return new Promise((resolve, reject) => {
|
|
@@ -178,7 +191,12 @@ function streamCli(
|
|
|
178
191
|
|
|
179
192
|
const killChild = (): void => killChildProcess(child)
|
|
180
193
|
|
|
181
|
-
|
|
194
|
+
// `final` marks the at-close flush of a trailing unterminated line: the CLI has already
|
|
195
|
+
// exited, so an observer must not act on that record in a way that KILLS the run (mirrors
|
|
196
|
+
// `runPi`'s `runGuard = false` flush — without it, a guard tripping on the last buffered
|
|
197
|
+
// record could turn a clean exit into a spurious "no progress" failure). The record's
|
|
198
|
+
// progress/telemetry signal is still delivered; only kill decisions are suppressed.
|
|
199
|
+
const processLine = (line: string, final = false): void => {
|
|
182
200
|
if (!line.startsWith('{')) return
|
|
183
201
|
let event: Record<string, unknown>
|
|
184
202
|
try {
|
|
@@ -187,7 +205,7 @@ function streamCli(
|
|
|
187
205
|
return
|
|
188
206
|
}
|
|
189
207
|
try {
|
|
190
|
-
onEvent(event)
|
|
208
|
+
onEvent(event, { final })
|
|
191
209
|
} catch {
|
|
192
210
|
// A faulty observer must never break the run.
|
|
193
211
|
}
|
|
@@ -226,10 +244,13 @@ function streamCli(
|
|
|
226
244
|
})
|
|
227
245
|
child.on('close', (code) => {
|
|
228
246
|
opts.signal?.removeEventListener('abort', onAbort)
|
|
229
|
-
if (lineBuffer.trim()) processLine(lineBuffer.trim())
|
|
230
247
|
const stderrTail = redact(stderr, secrets).slice(-700)
|
|
248
|
+
if (lineBuffer.trim()) processLine(lineBuffer.trim(), true)
|
|
231
249
|
if (aborted) {
|
|
232
|
-
|
|
250
|
+
// Carry the tail on the rejection so a caller that REPLACES this generic message with a
|
|
251
|
+
// more specific cause (the no-progress guard's diagnostic) can still append it — the
|
|
252
|
+
// stderr is often the only evidence of what the CLI was doing when it was killed.
|
|
253
|
+
reject(Object.assign(new Error('agent run aborted by watchdog'), { stderrTail }))
|
|
233
254
|
return
|
|
234
255
|
}
|
|
235
256
|
if (code !== 0) {
|
|
@@ -358,31 +379,67 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
358
379
|
// may still rewrite below (a published call must be final — see the publisher).
|
|
359
380
|
const publisher = createCallMetricPublisher(calls, opts.onCallMetric)
|
|
360
381
|
|
|
361
|
-
// ADR 0026 D2.1 + ADR 0027 Defect B: surface live slice progress from
|
|
362
|
-
//
|
|
363
|
-
// stream (only a subagent's intermediate turns don't), so `sliceTracker`
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
// update, so neither masks the other — the pr-reviewer prompt writes its plan ONCE and never
|
|
367
|
-
// marks it done, which used to gate the slice signal off and pin progress at 0%.
|
|
382
|
+
// ADR 0026 D2.1 + ADR 0027 Defect B: surface live slice progress from the two views the run
|
|
383
|
+
// produces of the SAME slicing. The parent's subagent dispatches + their terminal tool_results
|
|
384
|
+
// DO appear on this stream (only a subagent's intermediate turns don't), so `sliceTracker`
|
|
385
|
+
// knows which slices are in flight and which have returned; the parent's own plan (tracked by
|
|
386
|
+
// `planTracker` + `lastTodo`) is the only place a not-yet-dispatched slice is named at all.
|
|
368
387
|
//
|
|
369
388
|
// The plan arrives in one of two tool vocabularies depending on the bundled CLI build:
|
|
370
389
|
// `TodoWrite` (whole-list snapshots, tracked in `lastTodo`) or the incremental
|
|
371
390
|
// `TaskCreate`/`TaskUpdate` pair (tracked by `planTracker`, which needs the tool RESULTS too
|
|
372
|
-
// because the task id is minted there). Both are read
|
|
391
|
+
// because the task id is minted there). Both are read, and `pickProgress` resolves that
|
|
392
|
+
// either/or; the plan then MERGES with the dispatch view (`mergeProgress`) rather than
|
|
393
|
+
// competing with it — picking the further-along view collapsed the list to the dispatched
|
|
394
|
+
// slices alone the moment the first subagent returned. See ./progress.ts.
|
|
373
395
|
const sliceTracker = createSliceTracker()
|
|
374
396
|
const planTracker = createTaskPlanTracker()
|
|
375
397
|
let lastTodo: TodoProgress | undefined
|
|
376
398
|
const emitProgress = (): void => {
|
|
377
399
|
if (!opts.onProgress) return
|
|
378
|
-
const progress =
|
|
400
|
+
const progress = mergeProgress(
|
|
379
401
|
pickProgress(lastTodo, planTracker.progress()),
|
|
380
402
|
sliceTracker.progress(),
|
|
381
403
|
)
|
|
382
404
|
if (progress) opts.onProgress(progress)
|
|
383
405
|
}
|
|
384
406
|
|
|
385
|
-
|
|
407
|
+
// No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
408
|
+
// absent on this path until now. Claude Code reports a tool CALL (its name) on the `assistant`
|
|
409
|
+
// turn and that call's RESULT (`is_error`) on the following `user` turn, so correlate them by
|
|
410
|
+
// `tool_use` id to feed the guard a {name,isError} signal. A tripped guard aborts the CLI via
|
|
411
|
+
// `guardAbort` (folded into streamCli's signal below) and the run then fails with its
|
|
412
|
+
// diagnostic. Disabled when the caller supplies no limits (only the external watchdog bounds it).
|
|
413
|
+
const guard = opts.guardLimits
|
|
414
|
+
? new ProgressGuard(opts.guardLimits, opts.expectsEdits ?? true)
|
|
415
|
+
: undefined
|
|
416
|
+
const toolNames = new Map<string, string>()
|
|
417
|
+
const guardAbort = new AbortController()
|
|
418
|
+
let guardReason: string | undefined
|
|
419
|
+
|
|
420
|
+
// Feed a user turn's settled tool calls to the guard, pairing each `tool_result`'s `is_error`
|
|
421
|
+
// with the name captured for its `tool_use` id on the assistant turn. The FIRST reason trips
|
|
422
|
+
// it: record the diagnostic and abort the CLI (streamCli's close handler rejects; the catch
|
|
423
|
+
// below surfaces `guardReason` over the generic abort message). A standalone closure so the
|
|
424
|
+
// per-block loop doesn't nest onEvent past the readable-depth limit.
|
|
425
|
+
const feedGuard = (content: unknown[]): void => {
|
|
426
|
+
if (!guard || guardReason) return
|
|
427
|
+
for (const block of content) {
|
|
428
|
+
if (!isObject(block) || block.type !== 'tool_result') continue
|
|
429
|
+
const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined
|
|
430
|
+
const name = id ? toolNames.get(id) : undefined
|
|
431
|
+
if (id) toolNames.delete(id)
|
|
432
|
+
if (!name) continue
|
|
433
|
+
const reason = guard.observeSignal({ name, isError: block.is_error === true })
|
|
434
|
+
if (reason) {
|
|
435
|
+
guardReason = reason
|
|
436
|
+
guardAbort.abort()
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const onEvent = (event: Record<string, unknown>, meta?: { final?: boolean }): void => {
|
|
386
443
|
const type = event.type
|
|
387
444
|
if (type === 'assistant' && isObject(event.message)) {
|
|
388
445
|
const message = event.message as Record<string, unknown>
|
|
@@ -391,7 +448,13 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
391
448
|
stats.assistantChars += text.length
|
|
392
449
|
stats.toolCalls += toolUses
|
|
393
450
|
for (const block of content) {
|
|
394
|
-
if (isObject(block)
|
|
451
|
+
if (!isObject(block) || block.type !== 'tool_use') continue
|
|
452
|
+
// Remember each call's name against its id so the guard can pair it with the
|
|
453
|
+
// `is_error` its `tool_result` carries on the next `user` turn.
|
|
454
|
+
if (typeof block.id === 'string' && typeof block.name === 'string') {
|
|
455
|
+
toolNames.set(block.id, block.name)
|
|
456
|
+
}
|
|
457
|
+
if (block.name === 'TodoWrite') {
|
|
395
458
|
const progress = todosToProgress((block.input as Record<string, unknown>)?.todos)
|
|
396
459
|
if (progress) lastTodo = progress
|
|
397
460
|
}
|
|
@@ -422,6 +485,9 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
422
485
|
sliceTracker.onUser(content)
|
|
423
486
|
planTracker.onUser(content)
|
|
424
487
|
emitProgress()
|
|
488
|
+
// Not on the at-close flush: the CLI has already exited, so tripping the guard there
|
|
489
|
+
// would kill nothing and only convert a clean exit into a spurious failure.
|
|
490
|
+
if (!meta?.final) feedGuard(content)
|
|
425
491
|
messages.push({ role: 'tool', content })
|
|
426
492
|
}
|
|
427
493
|
} else if (type === 'result') {
|
|
@@ -488,6 +554,12 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
488
554
|
})
|
|
489
555
|
: undefined
|
|
490
556
|
|
|
557
|
+
// Fold the guard's abort into the run signal so a tripped guard kills the CLI the same way the
|
|
558
|
+
// external watchdog does; `guardReason` (set above) distinguishes the two at the catch below.
|
|
559
|
+
const runSignal = opts.signal
|
|
560
|
+
? AbortSignal.any([opts.signal, guardAbort.signal])
|
|
561
|
+
: guardAbort.signal
|
|
562
|
+
|
|
491
563
|
try {
|
|
492
564
|
const { stderrTail } = await streamCli(
|
|
493
565
|
{
|
|
@@ -509,7 +581,7 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
509
581
|
],
|
|
510
582
|
},
|
|
511
583
|
prompt,
|
|
512
|
-
opts,
|
|
584
|
+
{ ...opts, signal: runSignal },
|
|
513
585
|
env,
|
|
514
586
|
opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [],
|
|
515
587
|
onEvent,
|
|
@@ -524,6 +596,16 @@ export async function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRun
|
|
|
524
596
|
usage,
|
|
525
597
|
subagents,
|
|
526
598
|
})
|
|
599
|
+
} catch (err) {
|
|
600
|
+
// A tripped no-progress guard aborted the CLI; streamCli rejects with its generic abort
|
|
601
|
+
// message, so replace it with the guard's actionable diagnostic — carrying the stderr tail
|
|
602
|
+
// it attached, since that is usually the only evidence of what the CLI was doing when it was
|
|
603
|
+
// killed. Byte-for-byte the shape `runPi` fails with.
|
|
604
|
+
if (guardReason) {
|
|
605
|
+
const tail = (err as { stderrTail?: string } | undefined)?.stderrTail
|
|
606
|
+
throw new Error(tail ? `${guardReason} Agent stderr: ${tail}` : guardReason)
|
|
607
|
+
}
|
|
608
|
+
throw err
|
|
527
609
|
} finally {
|
|
528
610
|
await subagents?.stop()
|
|
529
611
|
if (configHome) {
|
package/src/claude-stream.ts
CHANGED
|
@@ -10,6 +10,25 @@ export function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
10
10
|
return typeof value === 'object' && value !== null
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The tool names the Claude Code CLI dispatches a parallel subagent under. `Agent` is what the
|
|
15
|
+
* shipped schema declares (`AgentInput` in `sdk-tools.d.ts`, carrying `description` / `prompt` /
|
|
16
|
+
* `subagent_type`); `Task` is the older name for the same dispatch. Both are matched because the
|
|
17
|
+
* harness runs against whatever CLI the image happens to bundle, and matching only the old name
|
|
18
|
+
* is what left a CLI 2.1.x pr-review reporting no slices at all.
|
|
19
|
+
*
|
|
20
|
+
* Note the asymmetry: keeping the legacy `Task` here is the one place a CLI rename could produce a
|
|
21
|
+
* FALSE signal rather than merely no signal — if a future build were to name a plain task-list
|
|
22
|
+
* tool `Task`, its writes would be counted as in-flight slices. We accept that because no shipped
|
|
23
|
+
* build does (the incremental plan tool is `TaskCreate`/`TaskUpdate`, tracked separately in
|
|
24
|
+
* `progress.ts`), and dropping legacy coverage is the more likely regression.
|
|
25
|
+
*
|
|
26
|
+
* Lives here rather than in `subagents.ts` because BOTH the slice tracker and the no-progress
|
|
27
|
+
* guard (`pi.ts`, which `subagents.ts` imports — so it cannot import back) must agree on what a
|
|
28
|
+
* subagent dispatch looks like.
|
|
29
|
+
*/
|
|
30
|
+
export const SUBAGENT_TOOL_NAMES = new Set(['Agent', 'Task'])
|
|
31
|
+
|
|
13
32
|
export function numberOf(value: unknown): number {
|
|
14
33
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
15
34
|
}
|
package/src/coding-agent.ts
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
runAgentInWorkspace,
|
|
47
47
|
withWorkspace,
|
|
48
48
|
} from './pi-workspace.js'
|
|
49
|
-
import type { ProgressGuardLimits } from './
|
|
49
|
+
import type { ProgressGuardLimits } from './progress-guard.js'
|
|
50
50
|
import type { RunOptions } from './runner.js'
|
|
51
51
|
import { log, type Logger } from './logger.js'
|
|
52
52
|
import {
|
package/src/embed.ts
CHANGED
|
@@ -7,21 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
export {
|
|
9
9
|
PI_MAX_OUTPUT_TOKENS,
|
|
10
|
-
DEFAULT_PROGRESS_GUARD_LIMITS,
|
|
11
10
|
writePiModelsConfig,
|
|
12
11
|
writeAgentsContext,
|
|
13
12
|
runPi,
|
|
14
13
|
summarizePiRun,
|
|
15
14
|
parsePiOutput,
|
|
16
15
|
parseTodoProgress,
|
|
17
|
-
progressGuardLimitsFromEnv,
|
|
18
16
|
terminalRunError,
|
|
19
17
|
type PiRunOutcome,
|
|
20
18
|
type PiRunStats,
|
|
21
|
-
type ProgressGuardLimits,
|
|
22
19
|
type TodoItem,
|
|
23
20
|
type TodoProgress,
|
|
24
21
|
} from './pi.js'
|
|
22
|
+
export {
|
|
23
|
+
DEFAULT_PROGRESS_GUARD_LIMITS,
|
|
24
|
+
progressGuardLimitsFromEnv,
|
|
25
|
+
type ProgressGuardLimits,
|
|
26
|
+
} from './progress-guard.js'
|
|
25
27
|
export {
|
|
26
28
|
cloneRepo,
|
|
27
29
|
createBranch,
|
package/src/pi-workspace.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
|
|
1
|
+
import { mkdir, mkdtemp, readdir, rm, stat } from 'node:fs/promises'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import type { RepoSpec, SkillSpec } from './job.js'
|
|
@@ -8,13 +8,10 @@ import {
|
|
|
8
8
|
type ContextFileInfo,
|
|
9
9
|
type PiRunOutcome,
|
|
10
10
|
type PiRunStats,
|
|
11
|
-
type ProgressGuardLimits,
|
|
12
11
|
type RunDiagnostics,
|
|
13
12
|
CONTEXT_DIR,
|
|
14
13
|
materializeContextFiles,
|
|
15
14
|
materializeSkillResources,
|
|
16
|
-
mergeGuardLimits,
|
|
17
|
-
progressGuardLimitsFromEnv,
|
|
18
15
|
runPi,
|
|
19
16
|
webSearchConfigFromEnv,
|
|
20
17
|
webSearchProxyEnv,
|
|
@@ -22,6 +19,11 @@ import {
|
|
|
22
19
|
writePiModelsConfig,
|
|
23
20
|
writeWebToolsConfig,
|
|
24
21
|
} from './pi.js'
|
|
22
|
+
import {
|
|
23
|
+
type ProgressGuardLimits,
|
|
24
|
+
mergeGuardLimits,
|
|
25
|
+
progressGuardLimitsFromEnv,
|
|
26
|
+
} from './progress-guard.js'
|
|
25
27
|
import type { RunOptions } from './runner.js'
|
|
26
28
|
import { type SubscriptionHarness, runSubscriptionHarness } from './agent-runner.js'
|
|
27
29
|
|
|
@@ -226,6 +228,31 @@ export interface AgentRunSpec {
|
|
|
226
228
|
multiRepo?: boolean
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Whether the run's checkout actually ships a `blueprints/` folder — what gates the blueprint
|
|
233
|
+
* orientation note in AGENTS.md (an external repo has none, so the note would be ~10 lines of
|
|
234
|
+
* dead guidance pointing at files that don't exist, re-sent on every turn).
|
|
235
|
+
*
|
|
236
|
+
* A MULTI-REPO run's `dir` is the workspace ROOT with each repo checked out as a sibling under
|
|
237
|
+
* it, so the root itself never holds `blueprints/`: the legs are checked too, and the note is
|
|
238
|
+
* included when ANY leg ships one (it orients the agent to the concept, and the agent finds the
|
|
239
|
+
* per-repo folder from there). Best-effort throughout — any stat/readdir failure simply omits
|
|
240
|
+
* the note rather than failing the dispatch.
|
|
241
|
+
*/
|
|
242
|
+
export async function checkoutHasBlueprints(dir: string, multiRepo: boolean): Promise<boolean> {
|
|
243
|
+
const isBlueprintDir = (path: string): Promise<boolean> =>
|
|
244
|
+
stat(join(path, 'blueprints'))
|
|
245
|
+
.then((s) => s.isDirectory())
|
|
246
|
+
.catch(() => false)
|
|
247
|
+
if (await isBlueprintDir(dir)) return true
|
|
248
|
+
if (!multiRepo) return false
|
|
249
|
+
const legs = await readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
250
|
+
const checks = await Promise.all(
|
|
251
|
+
legs.filter((e) => e.isDirectory()).map((e) => isBlueprintDir(join(dir, e.name))),
|
|
252
|
+
)
|
|
253
|
+
return checks.some(Boolean)
|
|
254
|
+
}
|
|
255
|
+
|
|
229
256
|
/**
|
|
230
257
|
* Write Pi's global agent context (`~/.pi/agent/AGENTS.md`) + provider config,
|
|
231
258
|
* then run Pi once in `spec.dir` and return its summary/stats/stderr. The context
|
|
@@ -272,6 +299,13 @@ export async function runAgentInWorkspace(
|
|
|
272
299
|
...(spec.skill ? { skill: spec.skill } : {}),
|
|
273
300
|
...(opts.agentEnv ? { extraEnv: opts.agentEnv } : {}),
|
|
274
301
|
signal: opts.signal,
|
|
302
|
+
// Run the SAME no-progress guard Pi gets (previously claude-code/codex had none): env
|
|
303
|
+
// defaults merged loosen-only with the kind's tuning + the backend's complexity-scaled
|
|
304
|
+
// no-edit allowance, so a claude-code run that stops making progress is killed early
|
|
305
|
+
// instead of burning the full wall-clock budget. The claude runner consumes it; codex
|
|
306
|
+
// ignores it for now (its stream isn't wired to the guard).
|
|
307
|
+
guardLimits: mergeGuardLimits(progressGuardLimitsFromEnv(), spec.guardLimits),
|
|
308
|
+
expectsEdits: spec.expectsEdits ?? true,
|
|
275
309
|
onActivity: opts.onActivity,
|
|
276
310
|
onProgress: opts.onProgress,
|
|
277
311
|
// Stream this run's per-call telemetry to the job's live drain. The subscription
|
|
@@ -303,11 +337,13 @@ export async function runAgentInWorkspace(
|
|
|
303
337
|
}
|
|
304
338
|
const webSearch = webSearchConfigFromEnv({ ...process.env, ...extraEnv })
|
|
305
339
|
if (webSearch) await writeWebToolsConfig(webSearch)
|
|
340
|
+
const hasBlueprints = await checkoutHasBlueprints(spec.dir, spec.multiRepo === true)
|
|
306
341
|
await writeAgentsContext(spec.systemPrompt, {
|
|
307
342
|
webSearch: Boolean(webSearch),
|
|
308
343
|
guidance: spec.webToolsGuidance,
|
|
309
344
|
serviceDirectory: spec.serviceDirectory,
|
|
310
345
|
contextFiles,
|
|
346
|
+
hasBlueprints,
|
|
311
347
|
...(spec.multiRepo ? { multiRepo: true } : {}),
|
|
312
348
|
})
|
|
313
349
|
await writePiModelsConfig({ model: spec.model, proxyBaseUrl })
|