@renzrollon/interlock 1.1.1 → 1.2.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +94 -1
- package/CHANGELOG.md +135 -1
- package/bin/interlock +409 -49
- package/bin/interlock-run +119 -7
- package/hooks/guard-relaunch.mjs +115 -0
- package/hooks/guard-tests.mjs +51 -3
- package/hooks/hooks.json +1 -0
- package/hooks/mod.mjs +655 -0
- package/hooks/preflight.mjs +83 -13
- package/hooks/recorder.mjs +189 -0
- package/lib/agent-usage.mjs +524 -0
- package/lib/doctor.mjs +488 -28
- package/lib/eval-history.mjs +233 -0
- package/lib/evals-capture.mjs +5 -2
- package/lib/graph/cli.mjs +28 -5
- package/lib/graph/retrieve/context.mjs +8 -3
- package/lib/host/acp.mjs +77 -14
- package/lib/host/claude-cli.mjs +391 -21
- package/lib/host/claude-env.mjs +179 -0
- package/lib/host/codex.mjs +9 -1
- package/lib/host/effort.mjs +163 -0
- package/lib/host/model-map.mjs +43 -0
- package/lib/host/qwen.mjs +9 -1
- package/lib/host/registry.mjs +44 -10
- package/lib/host.mjs +46 -5
- package/lib/interrupted.mjs +167 -0
- package/lib/launch-ledger.mjs +198 -0
- package/lib/launch-rule.mjs +174 -0
- package/lib/limits.mjs +149 -41
- package/lib/meter-quiet.mjs +45 -0
- package/lib/plan-fingerprint.mjs +80 -11
- package/lib/prompts/implementer.mjs +1 -1
- package/lib/prompts/verify.mjs +24 -1
- package/lib/receipt.mjs +59 -8
- package/lib/report-html.mjs +89 -6
- package/lib/report.mjs +313 -12
- package/lib/resume-card.mjs +90 -2
- package/lib/run-log.mjs +192 -5
- package/lib/run.mjs +1177 -84
- package/lib/state-home.mjs +298 -0
- package/lib/verify.mjs +37 -0
- package/lib/waves.mjs +53 -16
- package/package.json +2 -1
- package/shared/TEST-PROFILE.md +10 -5
- package/skills/fix-tests/SKILL.md +1 -1
- package/types/index.d.ts +28 -0
- package/workflows/ship.js +75 -51
package/workflows/ship.js
CHANGED
|
@@ -148,6 +148,10 @@ const pingExtra = { type: PING_AGENT, tools: PING_TOOLS }
|
|
|
148
148
|
|
|
149
149
|
const WORK = '.claude/ship'
|
|
150
150
|
const RESULTS = `${WORK}/results.json`
|
|
151
|
+
// Where the CLI keeps the exact bytes it last printed (LAST_STEP_PATH in
|
|
152
|
+
// lib/run.mjs). Named here only so a halt can point a reader at it; this script
|
|
153
|
+
// cannot read a file.
|
|
154
|
+
const LAST_STEP = `${WORK}/last-step.json`
|
|
151
155
|
|
|
152
156
|
// The runtime caps a run at 1000 agents. This is that ceiling, named so nobody
|
|
153
157
|
// mistakes it for policy: it is NOT a bound on the loop, and it is deliberately
|
|
@@ -262,6 +266,11 @@ const {
|
|
|
262
266
|
|
|
263
267
|
let steps = 0
|
|
264
268
|
let resolvedChange = changeArg || '(unresolved)'
|
|
269
|
+
// Why the last relay produced no step, when it did not. A relay's copy that
|
|
270
|
+
// fails to parse used to vanish into a generic halt, and the only way to learn
|
|
271
|
+
// it had been a 23 KB step retyped with two brackets missing was to dig the
|
|
272
|
+
// relay's transcript out by hand.
|
|
273
|
+
let relayFailure = ''
|
|
265
274
|
|
|
266
275
|
/** A control-plane ping: mechanical, cheap, and never asked to decide anything. */
|
|
267
276
|
const ping = (name, prompt, schema) => agent(prompt, { label: name, schema, ...pingExtra })
|
|
@@ -275,7 +284,8 @@ const ping = (name, prompt, schema) => agent(prompt, { label: name, schema, ...p
|
|
|
275
284
|
*/
|
|
276
285
|
async function spawnOne(s) {
|
|
277
286
|
const result = await agent(BOOTSTRAP(s.label, s.promptPath, s.promptSha256), {
|
|
278
|
-
label
|
|
287
|
+
// Shown as its title; every record below still names it by its label.
|
|
288
|
+
label: s.title || s.label,
|
|
279
289
|
type: s.type,
|
|
280
290
|
tools: s.tools,
|
|
281
291
|
...(s.model ? { model: s.model } : {}),
|
|
@@ -287,13 +297,25 @@ async function spawnOne(s) {
|
|
|
287
297
|
properties: { ...(s.schema.properties || {}), briefing: { type: 'string' } }
|
|
288
298
|
}
|
|
289
299
|
})
|
|
300
|
+
// No result at all is what `agent()` resolves to when the runtime stopped the
|
|
301
|
+
// agent, the API failed unrecoverably, or a usage limit ended it outside an
|
|
302
|
+
// interactive session. The host was the cause, so it is named as the host —
|
|
303
|
+
// reporting it as an unacknowledged briefing would send the reader to the
|
|
304
|
+
// briefing. Still a null result, which the recorder treats as a failed task.
|
|
305
|
+
if (result === null || result === undefined) {
|
|
306
|
+
banners.push(
|
|
307
|
+
`AGENT RETURNED NO RESULT: ${s.label} — the runtime stopped it, the API failed, or a usage limit ` +
|
|
308
|
+
`ended it; the step is recorded as failed`
|
|
309
|
+
)
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
290
312
|
// Fail closed on an unacknowledged briefing. An agent that did not read its
|
|
291
313
|
// instructions did not do this task, whatever it reports about it — and a
|
|
292
314
|
// silently under-instructed worker is exactly the failure prompt integrity
|
|
293
315
|
// exists to catch. So it is a null result with a named reason, which the
|
|
294
316
|
// recorder already treats as a failed task, rather than a warning beside a
|
|
295
317
|
// result that gets counted.
|
|
296
|
-
const ack =
|
|
318
|
+
const ack = typeof result.briefing === 'string' ? result.briefing.trim() : ''
|
|
297
319
|
if (ack !== s.promptSha256) {
|
|
298
320
|
banners.push(
|
|
299
321
|
`BRIEFING NOT ACKNOWLEDGED: ${s.label} reported ${ack || '(nothing)'} for a briefing whose ` +
|
|
@@ -333,8 +355,10 @@ async function cli(argv, results, extraWrites = []) {
|
|
|
333
355
|
const quote = a => (/[^\w@%+=:,./-]/.test(String(a)) ? `'${String(a).replace(/'/g, `'\\''`)}'` : String(a))
|
|
334
356
|
const command =
|
|
335
357
|
`interlock ${argv.map(quote).join(' ')}${results === undefined ? '' : ` --results ${RESULTS}`} --json`
|
|
358
|
+
// Shown with the subcommand it relays (`cli-12 · run next`), so a stalled
|
|
359
|
+
// relay can be read off the progress view; `label` stays what errors name.
|
|
336
360
|
const relayed = await ping(
|
|
337
|
-
label,
|
|
361
|
+
`${label} · ${argv.slice(0, 2).join(' ')}`,
|
|
338
362
|
`You are a mechanical relay for one command. Do not interpret it, and do not do the work it ` +
|
|
339
363
|
`describes.\n\n` +
|
|
340
364
|
(writeLines ? `${writeLines}\n\n` : '') +
|
|
@@ -348,14 +372,28 @@ async function cli(argv, results, extraWrites = []) {
|
|
|
348
372
|
properties: { cliStdout: { type: 'string' }, action: { type: 'string' } }
|
|
349
373
|
}
|
|
350
374
|
)
|
|
351
|
-
|
|
375
|
+
let problem
|
|
376
|
+
if (!relayed) {
|
|
377
|
+
problem = 'the relay returned nothing'
|
|
378
|
+
} else if (typeof relayed.cliStdout !== 'string') {
|
|
379
|
+
problem = 'the relay returned no cliStdout'
|
|
380
|
+
} else {
|
|
352
381
|
try {
|
|
353
382
|
const parsed = JSON.parse(relayed.cliStdout)
|
|
354
383
|
if (parsed && typeof parsed.action === 'string') return parsed
|
|
355
|
-
|
|
384
|
+
problem = `${relayed.cliStdout.length} relayed chars parsed but carry no "action"`
|
|
385
|
+
} catch (err) {
|
|
356
386
|
// stdout was not JSON. Nothing is inferred from that: the caller stops.
|
|
387
|
+
// What is kept is WHY — V8's message carries the failing position when it
|
|
388
|
+
// has one — so the halt says what broke instead of that something did.
|
|
389
|
+
problem =
|
|
390
|
+
`${relayed.cliStdout.length} relayed chars do not parse as JSON ` +
|
|
391
|
+
`(${(err && err.message) || String(err)})`
|
|
357
392
|
}
|
|
358
393
|
}
|
|
394
|
+
relayFailure =
|
|
395
|
+
`${label} (interlock ${argv.slice(0, 2).join(' ')}): ${problem} — ` +
|
|
396
|
+
`the CLI's own bytes are in ${LAST_STEP}`
|
|
359
397
|
return null
|
|
360
398
|
}
|
|
361
399
|
|
|
@@ -376,67 +414,44 @@ if (tddModeConflict) {
|
|
|
376
414
|
)
|
|
377
415
|
}
|
|
378
416
|
|
|
379
|
-
// The environment probe.
|
|
380
|
-
// whether
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
417
|
+
// The environment probe. It asks only what is a property of THIS HOST's shell —
|
|
418
|
+
// whether the effort level is overridden — and creates the working directory.
|
|
419
|
+
// Whether the graph was built and whether a test profile exists are NOT asked:
|
|
420
|
+
// `run start` locates both itself, root first and then the state home, and
|
|
421
|
+
// raises their banners (ship/run-program). Two probes of one file in two places
|
|
422
|
+
// is how a driver and the CLI would disagree about whether it exists. The
|
|
423
|
+
// model-routing variables are not asked either: `run start` reads them from its
|
|
424
|
+
// own environment, together with the host's version, and decides the banners
|
|
425
|
+
// and the ping model itself.
|
|
384
426
|
const probed = await ping(
|
|
385
427
|
'validate',
|
|
386
428
|
`Report this environment. Change nothing, and do not start any work.\n\n` +
|
|
387
|
-
`Run:
|
|
388
|
-
`
|
|
389
|
-
`Run: test -f .claude/testing/profile.json && echo yes || echo no\n` +
|
|
390
|
-
`Report it as hasTestProfile.\n\n` +
|
|
391
|
-
`Run: printenv CLAUDE_CODE_SUBAGENT_MODEL\n` +
|
|
392
|
-
`If it prints a value, report it as subagentModelOverride. If it is unset the command exits ` +
|
|
429
|
+
`Run: printenv CLAUDE_CODE_EFFORT_LEVEL\n` +
|
|
430
|
+
`If it prints a value, report it as effortLevelOverride. If it is unset the command exits ` +
|
|
393
431
|
`non-zero and prints nothing — that is the normal case, so leave the field out rather than ` +
|
|
394
432
|
`reporting an empty string.\n\n` +
|
|
395
|
-
`Then run: printenv CLAUDE_CODE_USE_BEDROCK; printenv AWS_BEDROCK\n` +
|
|
396
|
-
`If subagentModelOverride is set, leave haikuAvailable out — routing is already overridden.\n` +
|
|
397
|
-
`If either Bedrock variable prints a non-empty value other than 0 or false, report ` +
|
|
398
|
-
`haikuAvailable:false. Bedrock accounts often cannot reach haiku and a failed ping halts the loop.\n` +
|
|
399
|
-
`Otherwise report haikuAvailable:true. When unsure, haikuAvailable:false so pings inherit ` +
|
|
400
|
-
`the session model rather than hard-failing.\n\n` +
|
|
401
433
|
`Then create the working directory ${WORK}/.`,
|
|
402
434
|
{
|
|
403
435
|
type: 'object',
|
|
404
436
|
properties: {
|
|
405
|
-
|
|
406
|
-
graphReason: { type: 'string' },
|
|
407
|
-
hasTestProfile: { type: 'boolean' },
|
|
408
|
-
subagentModelOverride: { type: 'string' },
|
|
409
|
-
haikuAvailable: { type: 'boolean' }
|
|
437
|
+
effortLevelOverride: { type: 'string' }
|
|
410
438
|
}
|
|
411
439
|
}
|
|
412
440
|
)
|
|
413
441
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
// CLAUDE_CODE_SUBAGENT_MODEL overrides both the session model and the per-agent
|
|
423
|
-
// model a step asks for, so when it is set the planner's tier ladder — the opus
|
|
424
|
-
// clamp, the haiku pings — is not in effect and the run costs whatever that
|
|
425
|
-
// model costs. Nothing here can prevent that; it is the user's environment. But
|
|
426
|
-
// a summary claiming no degradation while the entire model ladder was bypassed
|
|
427
|
-
// is exactly the silence the banner block exists to remove.
|
|
428
|
-
const subagentModel =
|
|
429
|
-
probed && typeof probed.subagentModelOverride === 'string'
|
|
430
|
-
? probed.subagentModelOverride.trim()
|
|
442
|
+
// The effort override. CLAUDE_CODE_EFFORT_LEVEL outranks both the session's effort
|
|
443
|
+
// and every per-agent effort a step asks for, so while it is set the plan's
|
|
444
|
+
// per-step effort is not in effect. It is the operator's environment and is
|
|
445
|
+
// never stripped; it is bannered so the summary cannot claim otherwise.
|
|
446
|
+
const effortLevel =
|
|
447
|
+
probed && typeof probed.effortLevelOverride === 'string'
|
|
448
|
+
? probed.effortLevelOverride.trim()
|
|
431
449
|
: ''
|
|
432
|
-
if (
|
|
450
|
+
if (effortLevel) {
|
|
433
451
|
banners.push(
|
|
434
|
-
`
|
|
435
|
-
`that
|
|
452
|
+
`EFFORT ROUTING OVERRIDDEN: CLAUDE_CODE_EFFORT_LEVEL=${effortLevel} — every agent runs at ` +
|
|
453
|
+
`that effort, so the per-step effort in the plan is not in effect`
|
|
436
454
|
)
|
|
437
|
-
} else if (probed && probed.haikuAvailable === true) {
|
|
438
|
-
// Mutate rather than rebind — `ping` closes over the object.
|
|
439
|
-
pingExtra.model = 'haiku'
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
// --- the loop ---------------------------------------------------------------
|
|
@@ -462,6 +477,13 @@ let step = await cli([
|
|
|
462
477
|
'workflow'
|
|
463
478
|
])
|
|
464
479
|
|
|
480
|
+
// The control-plane ping model `run start` decided from the host's version and
|
|
481
|
+
// environment. Forwarded, never decided here: when the step names none — an
|
|
482
|
+
// override is in force, or a Bedrock account may not reach haiku — every later
|
|
483
|
+
// relay inherits the session model. Mutate rather than rebind: `ping` closes
|
|
484
|
+
// over the object.
|
|
485
|
+
if (step && typeof step.pingModel === 'string' && step.pingModel) pingExtra.model = step.pingModel
|
|
486
|
+
|
|
465
487
|
while (step && step.then) {
|
|
466
488
|
if (steps > RUNAWAY_BACKSTOP) {
|
|
467
489
|
return await stop(`the interpreter exceeded the runtime's agent ceiling (${RUNAWAY_BACKSTOP})`)
|
|
@@ -488,7 +510,9 @@ while (step && step.then) {
|
|
|
488
510
|
}
|
|
489
511
|
|
|
490
512
|
if (!step) {
|
|
491
|
-
|
|
513
|
+
if (relayFailure) banners.push(`RELAY UNREADABLE: ${relayFailure}`)
|
|
514
|
+
return await stop('the run program returned no step — the CLI relay could not be read' +
|
|
515
|
+
(relayFailure ? `: ${relayFailure}` : ''))
|
|
492
516
|
}
|
|
493
517
|
|
|
494
518
|
// The last argv was `run close`, so `step` IS the close: its summary and its
|