4996f2d919
This reverts commitcdbc2d0. The bump was a judgment call without evidence, and the planner design intent argues the other way: the plan Iron Law is "EVERY STEP THAT CHANGES CODE INCLUDES THE CODE" and tasks are sized so a subagent executes one "without judgement calls outside its remit" (planner/SKILL.md) — the planner exists precisely to remove design latitude from the implementer, which points at sonnet, not opus. opus is only justified to the degree real plans leave slack (RED-step invention, tree reconciliation, review repair), and that slack is measurable, not felt. Restore sonnet/high; a future bump should be grounded in the loop metrics (spec re-loop count, quality changes_requested rate, end-verify BLOCKED rate) via postmortem, not a gut call. The four fable gates (b591e75) are untouched.
282 lines
16 KiB
JavaScript
282 lines
16 KiB
JavaScript
// compiler-driven-edit — the observe-then-bounce loop for a
|
|
// behaviour-preserving type/signature edit, as a Workflow script.
|
|
//
|
|
// This is the executor for the `compiler-driven` selector arm (see
|
|
// boss/SKILL.md "Entry-path reflection"). The arm is entered on an
|
|
// OBSERVABLE/syntactic trigger — "a type or signature edit at a definition
|
|
// site that propagates mechanically across the sites the type checker
|
|
// enumerates" — NOT on a prediction that the change is behaviour-preserving.
|
|
// A real build + suite run then settles the verdict:
|
|
//
|
|
// an edit landed AND clean build AND suite green unchanged -> DONE (edits unstaged; orchestrator commits)
|
|
// the edit phase produced NO change (clean tree) -> BLOCKED (no-op-edit) — never DONE
|
|
// a hole the edit cannot resolve -> BOUNCE up, per the straddle rule:
|
|
// forces a design choice -> specify
|
|
// is test-specifiable new behaviour -> tdd (RED-first)
|
|
// the suite is not green / not unchanged -> BOUNCE to debug (RED-first)
|
|
//
|
|
// "An edit landed" is load-bearing: the done-signal `build_clean &&
|
|
// suite_green_unchanged` is vacuously true on an EMPTY tree, so doing nothing
|
|
// would otherwise read as DONE. DONE requires positive evidence the tree
|
|
// differs from HEAD — checked twice: the edit agent's applied_changes self-
|
|
// report (cheap, pre-suite) and the verify agent's independent working_tree_
|
|
// dirty observation (authoritative backstop).
|
|
//
|
|
// The fallible ex-ante guess ("this is behaviour-preserving") is replaced by
|
|
// a cheap ex-post detection: a mis-routed behaviour change fails the
|
|
// post-condition and bounces instead of shipping green-but-wrong. This
|
|
// generalises the plugin's RED-first philosophy — observe, do not predict.
|
|
//
|
|
// "Suite green UNCHANGED" is load-bearing: the suite must pass with NO test
|
|
// added, removed, or weakened to make it pass. A new test would mean the
|
|
// edit encoded new behaviour — which is the tdd/spec arm, not this one.
|
|
//
|
|
// The `verify` step below (build + suite-green-unchanged) is the single
|
|
// phase the implement loop's pure-refactor case could never invoke on its
|
|
// own; here it is a standalone, independently-invokable agent call.
|
|
//
|
|
// Discipline: the script never commits and never moves main HEAD; all edits
|
|
// stay unstaged for the orchestrator. The script has no shell of its own —
|
|
// the edit and the verify both run inside agent() calls.
|
|
//
|
|
// Model policy: every agent() call pins an explicit model — never the
|
|
// session-model inherit (which could route to an unintended tier, e.g. fable —
|
|
// reserved for the four ratified frontmatter gates, never for workflow stages;
|
|
// agent-template § model rule 1). Both phases here are mechanical
|
|
// (compiler-enumerated propagation, build/suite re-run), so both run sonnet.
|
|
// Effort policy mirrors it: every call pins an explicit effort — the edit
|
|
// (real code work) runs high, the build/suite verify (schema-bound check)
|
|
// runs medium. See docs/agent-template.md § effort.
|
|
//
|
|
// Carrier — Workflow `args`:
|
|
// edit_description: what type/signature change to make
|
|
// def_site: the definition site (file + symbol) it originates from
|
|
// repo_root: optional but strongly recommended (required in worktree
|
|
// sessions — issue #25): absolute path of the working tree
|
|
// this edit operates on, stamped into both stage prompts
|
|
// as a `git -C` anchor so a dispatched agent whose cwd
|
|
// resolves to the primary checkout cannot verify the
|
|
// wrong tree.
|
|
// A non-object carrier or a missing required field fails fast as an infra
|
|
// BLOCKED before any agent is dispatched (issue #24).
|
|
|
|
export const meta = {
|
|
name: 'compiler-driven-edit',
|
|
description:
|
|
'Execute a behaviour-preserving type/signature edit at a definition site, propagate it mechanically across the sites the build enumerates, then let a real build + suite run settle the verdict: clean build AND suite green unchanged -> done (edits unstaged for the orchestrator to commit); a hole the edit cannot resolve -> bounce up per the straddle rule (specify for a design choice, tdd for test-specifiable new behaviour); suite not green-unchanged -> bounce to debug (RED-first). Never commits, never moves main HEAD.',
|
|
phases: [{ title: 'Edit + propagate' }, { title: 'Verify' }, { title: 'Verdict' }],
|
|
}
|
|
|
|
// Carrier guard (issue #24): a named-workflow invocation can deliver `args`
|
|
// as a STRING; `args || {}` alone does not catch it (a non-empty string is
|
|
// truthy), so both fields would read undefined and the edit agent would be
|
|
// briefed with the literal "EDIT: undefined" — its refusal would then ride
|
|
// the straddle-rule branch below as a BOUNCE to specify, dressing a
|
|
// mechanics failure as a design finding. Normalize, then reject a malformed
|
|
// carrier HERE, before any agent is dispatched — a distinct infra verdict,
|
|
// never a bounce.
|
|
let carrier = args
|
|
if (typeof carrier === 'string') {
|
|
// Tolerate a JSON-SERIALIZED object carrier (a stringifying caller or
|
|
// substrate layer still authored the documented object form); free text
|
|
// does not parse to an object and falls through to the rejection below.
|
|
try {
|
|
carrier = JSON.parse(carrier)
|
|
} catch {}
|
|
}
|
|
if ((carrier !== undefined && carrier !== null && typeof carrier !== 'object') || Array.isArray(carrier)) {
|
|
return {
|
|
status: 'BLOCKED',
|
|
kind: 'bad-carrier',
|
|
reason:
|
|
`infra: args arrived as ${Array.isArray(carrier) ? 'an array' : `a ${typeof carrier}`}, not the documented ` +
|
|
'object carrier — nothing was dispatched; re-invoke with args: { edit_description, def_site }',
|
|
}
|
|
}
|
|
|
|
const { edit_description, def_site, repo_root } = carrier || {}
|
|
|
|
const missingFields = Object.entries({ edit_description, def_site })
|
|
.filter(([, v]) => typeof v !== 'string' || !v.trim())
|
|
.map(([k]) => k)
|
|
if (missingFields.length) {
|
|
return {
|
|
status: 'BLOCKED',
|
|
kind: 'bad-carrier',
|
|
reason:
|
|
`infra: missing required carrier field(s): ${missingFields.join(', ')} — nothing was dispatched; ` +
|
|
'the edit brief never reached the edit agent',
|
|
}
|
|
}
|
|
|
|
const STANDING =
|
|
"Standing reading first (the plugin convention): read the project's CLAUDE.md " +
|
|
'(its "## Skills plugin: project facts" name the build and test commands) and ' +
|
|
'`git log -10 --format=full`. Resolve the build/test commands from those facts.'
|
|
|
|
// Standing-reading tiering (issue #31): the verify stage is a schema-bound
|
|
// check that only needs the build/test commands — no git log, no full
|
|
// CLAUDE.md read. The edit stage keeps the full standing read (it authors
|
|
// code and must match project idiom).
|
|
const STANDING_FACTS =
|
|
'Project facts first: read ONLY the "## Skills plugin: project facts" section of the project\'s CLAUDE.md for ' +
|
|
'the build and test commands — no other standing reading (no git log). This is a schema-bound check stage; ' +
|
|
'do not assume a toolchain.'
|
|
|
|
// Working-directory anchor (issue #25): same defect class as implement-loop —
|
|
// in a worktree session a dispatched agent's cwd can resolve to the primary
|
|
// checkout and verify the wrong (clean) tree. Carrier-supplied only here (two
|
|
// stages, no cheap probe slot); '' when absent.
|
|
const ANCHOR =
|
|
typeof repo_root === 'string' && repo_root.trim()
|
|
? `\n\nWORKING-DIRECTORY ANCHOR (do not skip): the working tree lives at ${repo_root.trim()}. Run EVERY git ` +
|
|
`command as \`git -C ${repo_root.trim()} ...\` and run the build/test commands from that directory. An empty ` +
|
|
'`git diff HEAD` or an unexpected HEAD sha means you are in the WRONG directory — re-run against the anchor; ' +
|
|
'never report a clean tree from an unanchored cwd.'
|
|
: ''
|
|
|
|
const EDIT_SCHEMA = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['build_clean', 'hole_needs_decision', 'applied_changes'],
|
|
properties: {
|
|
build_clean: { type: 'boolean', description: 'the project build is clean after the edit + mechanical propagation' },
|
|
applied_changes: {
|
|
type: 'boolean',
|
|
description:
|
|
'GROUND TRUTH, not intent: the working tree actually differs from HEAD after the edit. Determine it by running ' +
|
|
'`git status --porcelain` (any line) or `git diff HEAD --stat` (non-empty) — true only if at least one file changed. ' +
|
|
'false means a no-op edit (you touched nothing); report it as false rather than guessing true.',
|
|
},
|
|
hole_needs_decision: {
|
|
type: 'boolean',
|
|
description:
|
|
'a site could not be resolved mechanically: it either forces a design choice, or resolving it would encode new behaviour — so the change was not purely behaviour-preserving after all',
|
|
},
|
|
bounce_to: {
|
|
type: 'string',
|
|
enum: ['specify', 'tdd'],
|
|
description:
|
|
'on hole_needs_decision, per the straddle rule: `specify` if the hole forces a genuine design choice; `tdd` if the discovered work is new behaviour pinnable as one falsifiable assertion. When unsure, `specify` (the conservative heavy path, which itself bounces onward).',
|
|
},
|
|
hole_detail: { type: 'string', description: 'on hole_needs_decision: what the hole forces (the decision, or the new behaviour discovered)' },
|
|
sites: { type: 'integer', description: 'number of sites the build flagged and the edit touched' },
|
|
},
|
|
}
|
|
const VERIFY_SCHEMA = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['build_clean', 'suite_green_unchanged', 'working_tree_dirty'],
|
|
properties: {
|
|
build_clean: { type: 'boolean' },
|
|
suite_green_unchanged: {
|
|
type: 'boolean',
|
|
description: 'the full suite passes with NO test added, removed, or weakened to make it pass',
|
|
},
|
|
working_tree_dirty: {
|
|
type: 'boolean',
|
|
description:
|
|
'GROUND TRUTH: the working tree differs from HEAD — i.e. an edit actually landed. Run `git status --porcelain` ' +
|
|
'(any line) or `git diff HEAD --stat` (non-empty). false means a clean tree / no-op edit, which is NOT a ' +
|
|
'behaviour-preserving success no matter how green the build and suite are.',
|
|
},
|
|
detail: { type: 'string', description: 'on failure: which tests went red, or which test was changed' },
|
|
},
|
|
}
|
|
|
|
// ---- Phase 1 — make the edit and let the compiler enumerate the sites ----
|
|
phase('Edit + propagate')
|
|
const edit = await agent(
|
|
`${STANDING}${ANCHOR}\n\nMake this behaviour-preserving type/signature edit at its definition site, then propagate it ` +
|
|
'MECHANICALLY across every site the build flags — let the type checker enumerate the edit sites. Introduce NO ' +
|
|
'new behaviour, add NO new test, change NO existing test. If a flagged site cannot be resolved without making a ' +
|
|
'design decision, OR resolving it would encode new behaviour, STOP and report it as a hole rather than guessing. ' +
|
|
'When you report a hole, CLASSIFY it (straddle rule): set bounce_to = "specify" if it forces a genuine design ' +
|
|
'choice, or "tdd" if the discovered work is new behaviour pinnable as one falsifiable assertion. When unsure, ' +
|
|
'"specify". Leave all edits UNSTAGED; never commit.\n\n' +
|
|
`EDIT: ${edit_description}\nDEFINITION SITE: ${def_site}\n\n` +
|
|
'Then run the project build and report: is the build clean, does any hole need a decision (with detail + bounce_to), ' +
|
|
'how many sites were touched, and — checked against `git status --porcelain` / `git diff HEAD`, NOT from memory — ' +
|
|
'whether the working tree actually changed (applied_changes).',
|
|
{ agentType: 'implementer', model: 'sonnet', effort: 'high', label: 'edit+propagate', phase: 'Edit + propagate', schema: EDIT_SCHEMA },
|
|
)
|
|
if (!edit) return { status: 'BLOCKED', reason: 'edit agent did not return' }
|
|
if (edit.hole_needs_decision) {
|
|
// The change was not purely mechanical after all. Per the straddle rule,
|
|
// route UP: a genuine design choice -> specify; test-specifiable new
|
|
// behaviour -> tdd (RED-first). Default to specify when unclassified —
|
|
// the conservative heavy path, which itself bounces onward.
|
|
const to = edit.bounce_to === 'tdd' ? 'tdd' : 'specify'
|
|
return {
|
|
status: 'BOUNCE',
|
|
to,
|
|
reason: to === 'tdd' ? 'discovered test-specifiable new behaviour' : 'a hole forces a design decision',
|
|
detail: edit.hole_detail || '',
|
|
}
|
|
}
|
|
// No-op guard (cheap, first line of defence): the done-signal must not be
|
|
// satisfiable by doing nothing. If the edit phase reports no hole yet left the
|
|
// working tree clean, no edit landed — a no-op is not a behaviour-preserving
|
|
// success, and running the suite against a clean tree would only manufacture a
|
|
// vacuous green. Stop here with a distinct infra-style BLOCKED rather than
|
|
// proceeding toward DONE. (`sites` is the wrong signal: a legitimate edit can
|
|
// touch only the definition site, sites 0, and still dirty the tree.)
|
|
if (!edit.applied_changes) {
|
|
return {
|
|
status: 'BLOCKED',
|
|
kind: 'no-op-edit',
|
|
reason:
|
|
'the edit phase produced no changes — the working tree is clean against HEAD, so no edit landed. ' +
|
|
'A no-op edit is not a behaviour-preserving success. Likely the task fell outside the shape this arm ' +
|
|
'handles (a type/signature edit the compiler enumerates), e.g. a crate scaffold/extraction; re-route it.',
|
|
}
|
|
}
|
|
|
|
// ---- Phase 2 — verify (the independently-invokable phase) ---------------
|
|
phase('Verify')
|
|
const verify = await agent(
|
|
`${STANDING_FACTS}${ANCHOR}\n\nVerify the edit is behaviour-preserving WITHOUT changing any code or test. Run the project build ` +
|
|
'and the full test suite. Report whether the build is clean and whether the suite is green AND UNCHANGED — i.e. ' +
|
|
'every test passes and NO test was added, removed, or weakened versus the pre-edit baseline (compare `git diff ' +
|
|
'HEAD` for test-file changes). Also report working_tree_dirty — whether `git status --porcelain` / `git diff HEAD` ' +
|
|
'shows ANY change at all (a clean tree means no edit landed). Do NOT edit anything; do NOT commit.',
|
|
{ model: 'sonnet', effort: 'medium', label: 'verify(build+suite)', phase: 'Verify', schema: VERIFY_SCHEMA },
|
|
)
|
|
|
|
// ---- Phase 3 — verdict --------------------------------------------------
|
|
phase('Verdict')
|
|
if (verify && verify.build_clean && verify.suite_green_unchanged && verify.working_tree_dirty) {
|
|
return {
|
|
status: 'DONE',
|
|
sites: edit.sites ?? null, // ?? not ||: a legitimate sites:0 (def-site-only edit) must report 0, not null
|
|
note: 'behaviour-preserving edit verified; edits unstaged in the working tree for the orchestrator to commit',
|
|
}
|
|
}
|
|
// Vacuous-green backstop (authoritative, second line of defence): build + suite
|
|
// are green but verify INDEPENDENTLY observed a clean tree. A clean tree is not
|
|
// a regression, so debug (RED-first) would be the wrong route — there is nothing
|
|
// to reproduce. It is a no-op edit the Phase-1 self-report missed; surface it as
|
|
// the same distinct BLOCKED, never as DONE.
|
|
if (verify && verify.build_clean && verify.suite_green_unchanged && !verify.working_tree_dirty) {
|
|
return {
|
|
status: 'BLOCKED',
|
|
kind: 'no-op-edit',
|
|
reason:
|
|
'verify observed a clean working tree (no diff against HEAD) — no edit landed, so the green build + suite are ' +
|
|
'vacuous. DONE requires positive evidence that an edit actually landed; a no-op edit is not a success.',
|
|
}
|
|
}
|
|
// observe-then-bounce: the post-condition failed, so the change was NOT
|
|
// behaviour-preserving after all. Route RED-first via debug rather than
|
|
// shipping a green-but-wrong commit.
|
|
return {
|
|
status: 'BOUNCE',
|
|
to: 'debug',
|
|
reason: !verify
|
|
? 'verify agent did not return'
|
|
: !verify.build_clean
|
|
? 'build not clean after the edit'
|
|
: 'suite not green-unchanged — a regression surfaced, so the edit was not behaviour-preserving',
|
|
detail: verify ? verify.detail || '' : '',
|
|
}
|