Fix TOOL-12: one line on every dev hand-back, by the model that ran
An Opus-run dev got no verification and no message, so silence meant both "still verifying" and "will never verify" (SAFETY-135 waited overnight, SAFETY-126 8 h). Verdicts were also labelled with the routing choice, not the model the run used. - Every dev hand-back posts one line at once: "[model-router] Tn verifying on Opus..." (a verdict follows), or "[model-router] Tn no verification: dev ran on Opus. Go straight to review." A run that ends with an error or names no clone says so. - Each round is decided and labelled by the model the run reported (its end usage), the routing choice only as fallback. A difference is shown as "(routed to X, ran on Y)", also on later rounds. - 8 new tests; each new rule was also broken on purpose and caught, apart from one equivalent change. Version 0.4.6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MdqPWvuDpbpRvDEdURoAwo
This commit is contained in:
+22
-8
@@ -19,6 +19,8 @@ import {
|
||||
parseBrief,
|
||||
parsePatterns,
|
||||
parseVerdict,
|
||||
handBackLine,
|
||||
ranTier,
|
||||
safetyHit,
|
||||
skipReason,
|
||||
validTimeout,
|
||||
@@ -32,7 +34,10 @@ import {
|
||||
/** One agent the router started, kept from spawn to its last verified round. */
|
||||
type AgentRec = {
|
||||
taskId: string
|
||||
/** The model the last round actually ran on (the routing choice until a round reports one). */
|
||||
tier: Tier
|
||||
/** The model the router chose at spawn; tier can differ when the run used another. */
|
||||
routed?: Tier
|
||||
role?: Role
|
||||
clone?: string
|
||||
baseBranch?: string
|
||||
@@ -124,7 +129,7 @@ The model-router mod picks each subagent's model and verifies dev work before it
|
||||
- Every dev, review and test brief carries one line \`Clone: <path>\` (add \`(Role: dev|review|test)\` if the path does not end in the role). The router reads the role from that line only; a brief naming several clones without it runs on Opus, unverified.
|
||||
- Dev agents are routed: Sonnet when the brief decides the approach (Expected / acceptance criteria), Opus when the design is still open. Naming a model on a dev brief has no effect. Give each dev brief its clone path and base branch.
|
||||
- Review and test agents keep the model you name (Opus when you name none).
|
||||
- When a dev agent below Opus finishes a round, the router verifies its clone's diff on Opus and posts a "[model-router]" verdict. Wait for that verdict before opening or advancing the PR.
|
||||
- Every dev hand-back gets one "[model-router] Tn" line at once. "verifying on Opus...": wait for the verdict before advancing the PR. "no verification: dev ran on Opus. Go straight to review.": no verdict will come. Lines are decided by the model the dev actually ran on.
|
||||
- On "[model-router] ... FAIL ... resume agent", resume that same agent with SendMessage, passing the issues. On "start a new agent ... [router-rewrite Tn]", stop the old agent and call Agent with exactly that prompt. On "handed back", stop and show the issues to the person. Do not fix rejected work yourself.`
|
||||
|
||||
// ---------- store helpers (top-level, as the engine requires for `$`) ----------
|
||||
@@ -501,14 +506,21 @@ async function skipRound($: EngineInterface, agentId: string, rec: AgentRec, out
|
||||
reason: outcome,
|
||||
outcome,
|
||||
})
|
||||
if (rec.role === 'dev' && outcome === 'no-clone')
|
||||
await tell($, `[model-router] ${rec.taskId} (agent ${agentId}): NOT VERIFIED - the brief names no clone path.`)
|
||||
}
|
||||
|
||||
async function runRound($: EngineInterface, agentId: string, rec: AgentRec, ended: string, cfg: Settings) {
|
||||
const skip = skipReason(rec, ended)
|
||||
if (skip) await skipRound($, agentId, rec, skip)
|
||||
else await verifyRound($, agentId, rec, cfg)
|
||||
/**
|
||||
* One finished run of a tracked agent. Decides on the model that actually ran,
|
||||
* says at once what happens next for a dev, then verifies or logs the skip.
|
||||
*/
|
||||
async function runRound($: EngineInterface, agentId: string, rec: AgentRec, ended: string, reported: string | undefined, cfg: Settings) {
|
||||
const routed = rec.routed ?? rec.tier
|
||||
const ran = ranTier(reported, routed)
|
||||
const round: AgentRec = { ...rec, routed, tier: ran.tier }
|
||||
const skip = skipReason(round, ended)
|
||||
const line = handBackLine({ taskId: rec.taskId, role: rec.role, ran: ran.tier, routed, skip, ended })
|
||||
if (line) await tell($, line)
|
||||
if (skip) await skipRound($, agentId, round, skip)
|
||||
else await verifyRound($, agentId, round, cfg)
|
||||
}
|
||||
|
||||
// ---------- the hooks ----------
|
||||
@@ -612,6 +624,7 @@ export const register: Register = (on, options) => {
|
||||
await putIn<AgentRec>($, S.agents, ran.agentId, {
|
||||
taskId,
|
||||
tier,
|
||||
routed: tier,
|
||||
role: brief.role,
|
||||
clone: brief.clone,
|
||||
baseBranch: brief.baseBranch,
|
||||
@@ -656,7 +669,8 @@ export const register: Register = (on, options) => {
|
||||
const rec = (await getMap<AgentRec>($, S.agents))[agentId]
|
||||
if (!rec) return out
|
||||
// Verification takes minutes: run it after this event has gone on.
|
||||
$.clock.after(0, () => void runRound($, agentId, rec, e.reason, cfg))
|
||||
const reported = e.usage?.model
|
||||
$.clock.after(0, () => void runRound($, agentId, rec, e.reason, reported, cfg))
|
||||
return out
|
||||
})
|
||||
|
||||
|
||||
@@ -262,3 +262,39 @@ export function validTimeout(raw: unknown): number {
|
||||
return Number.isFinite(n) && n >= 1 ? Math.round(n) : 1800
|
||||
}
|
||||
|
||||
|
||||
// ---------- the hand-back line (TOOL-12) ----------
|
||||
|
||||
const LABEL: Record<Tier, string> = { haiku: 'Haiku', sonnet: 'Sonnet', opus: 'Opus', fable: 'Fable' }
|
||||
|
||||
/**
|
||||
* The model a run actually used, from the model id its end reports; the
|
||||
* routing choice only when the run reported none.
|
||||
*/
|
||||
export const ranTier = (reportedModel: string | undefined, routed: Tier): { tier: Tier; isMismatch: boolean } => {
|
||||
const seen = asTier(reportedModel)
|
||||
return seen ? { tier: seen, isMismatch: seen !== routed } : { tier: routed, isMismatch: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* The one line every dev hand-back gets at once, so silence never stands for
|
||||
* both "still verifying" and "will never verify". Undefined: no line (not a dev).
|
||||
*/
|
||||
export const handBackLine = (args: {
|
||||
taskId: string
|
||||
role?: Role
|
||||
ran: Tier
|
||||
routed: Tier
|
||||
skip?: Skip
|
||||
ended: string
|
||||
}): string | undefined => {
|
||||
const { taskId, role, ran, routed, skip, ended } = args
|
||||
if (role !== 'dev') return undefined
|
||||
const note = ran !== routed ? ` (routed to ${LABEL[routed]}, ran on ${LABEL[ran]})` : ''
|
||||
const head = `[model-router] ${taskId}`
|
||||
if (!skip) return `${head} verifying on ${LABEL[verifierFor(ran)]}...${note}`
|
||||
if (skip === 'opus-unverified') return `${head} no verification: dev ran on ${LABEL[ran]}. Go straight to review.${note}`
|
||||
if (skip === 'agent-ended') return `${head} no verification: the dev run ended without an answer (${ended}).${note}`
|
||||
if (skip === 'no-clone') return `${head} no verification: the brief names no clone path.${note}`
|
||||
return undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user