|
|
|
@@ -42,6 +42,11 @@
|
|
|
|
|
// • The script has no filesystem or shell access of its own — every
|
|
|
|
|
// working-tree mutation (code, stats.json, BLOCKED.md) happens inside
|
|
|
|
|
// an agent() call.
|
|
|
|
|
// • Model policy: every agent() call pins an explicit model — never the
|
|
|
|
|
// session-model inherit (which could route to an unintended tier, e.g.
|
|
|
|
|
// fable — banned for all plugin agents and workflows). Mechanical and
|
|
|
|
|
// in-loop steps run sonnet; the quality gate is the one deliberate opus
|
|
|
|
|
// call (last correctness check before a task is marked DONE).
|
|
|
|
|
//
|
|
|
|
|
// Carrier — passed as the Workflow `args` object by the orchestrator:
|
|
|
|
|
// mode: 'standard' | 'mini'
|
|
|
|
@@ -245,7 +250,7 @@ const pre = await agent(
|
|
|
|
|
'Run `git status --porcelain`, `git rev-parse HEAD`, and `git rev-parse --abbrev-ref HEAD`. ' +
|
|
|
|
|
'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, and any dirty paths. ' +
|
|
|
|
|
'Do NOT modify anything, do NOT commit. The HEAD sha is an informational anchor only — never a reset target.',
|
|
|
|
|
{ label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
|
|
|
|
{ model: 'sonnet', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (!pre) return { status: 'BLOCKED', iter_id, reason: 'infra: preflight agent did not return' }
|
|
|
|
|
if (!pre.clean) {
|
|
|
|
@@ -276,7 +281,7 @@ if (mode === 'mini') {
|
|
|
|
|
`${STANDING}\n\nRead the plan at ${plan_path}. Extract ` +
|
|
|
|
|
(task_range ? `tasks ${task_range[0]}..${task_range[1]} (inclusive, 1-based)` : 'every task') +
|
|
|
|
|
', each as its VERBATIM block with the task id and a one-line title. Do not summarise or reorder. Do not edit anything.',
|
|
|
|
|
{ label: 'plan-extract', phase: 'Per-task loop', schema: TASKS_SCHEMA },
|
|
|
|
|
{ model: 'sonnet', label: 'plan-extract', phase: 'Per-task loop', schema: TASKS_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (!extracted || !extracted.tasks || extracted.tasks.length === 0) {
|
|
|
|
|
return { status: 'NEEDS_CONTEXT', iter_id, reason: `no tasks extracted from ${plan_path}` }
|
|
|
|
@@ -294,7 +299,7 @@ async function runTask(task) {
|
|
|
|
|
'inner-loop discipline — add the failing test inline even if the task text forgot to script it). ' +
|
|
|
|
|
'Build and test must be green before you report DONE. Leave all edits UNSTAGED; never commit.\n\n' +
|
|
|
|
|
`mode: ${mode}\niter_id: ${iter_id}\n\nTASK ${task.id} — ${task.title}\n${task.text}`,
|
|
|
|
|
{ agentType: 'implementer', label: `impl:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
|
|
|
|
{ agentType: 'implementer', model: 'sonnet', label: `impl:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (!impl) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'implementer agent did not return' }
|
|
|
|
|
if (impl.status === 'BLOCKED') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: impl.reason || 'worker-blocked' }
|
|
|
|
@@ -313,7 +318,7 @@ async function runTask(task) {
|
|
|
|
|
`${STANDING}\n\nSpec-compliance review of task ${task.id}. Compare \`git diff HEAD\` against the task ` +
|
|
|
|
|
'text ONLY — list missing requirements and unrequested extras. No quality opinions, no fix proposals. ' +
|
|
|
|
|
`Focus on the files this task claims to touch.\n\nTASK ${task.id} — ${task.title}\n${task.text}`,
|
|
|
|
|
{ agentType: 'spec-reviewer', label: `spec:${task.id}`, phase: 'Per-task loop', schema: SPEC_SCHEMA },
|
|
|
|
|
{ agentType: 'spec-reviewer', model: 'sonnet', label: `spec:${task.id}`, phase: 'Per-task loop', schema: SPEC_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (spec && spec.status === 'compliant') break
|
|
|
|
|
if (spec && spec.status === 'unclear') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'spec-ambiguous', detail: spec.ambiguity }
|
|
|
|
@@ -323,7 +328,7 @@ async function runTask(task) {
|
|
|
|
|
`${STANDING}\n\nRepair task ${task.id} to satisfy the spec-compliance review. Address EACH finding; ` +
|
|
|
|
|
'do not widen scope. Keep build + tests green; leave edits unstaged.\n\n' +
|
|
|
|
|
`TASK ${task.id} — ${task.title}\n${task.text}\n\nFINDINGS:\n- ${(spec?.findings || ['(none reported)']).join('\n- ')}`,
|
|
|
|
|
{ agentType: 'implementer', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
|
|
|
|
{ agentType: 'implementer', model: 'sonnet', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (specFix && specFix.applied_changes === true) wroteAnyFile = true
|
|
|
|
|
}
|
|
|
|
@@ -346,7 +351,9 @@ async function runTask(task) {
|
|
|
|
|
'Nits never gate. Also report `diff_fingerprint` (REQUIRED): the sha256 hex of the FULL `git diff HEAD` ' +
|
|
|
|
|
'(`git diff HEAD | sha256sum` — the whole working-tree diff, NOT narrowed to this task\'s footprint). ' +
|
|
|
|
|
'The loop compares it across rounds to detect a no-op/cyclic repair.',
|
|
|
|
|
{ agentType: 'quality-reviewer', label: `qual:${task.id}`, phase: 'Per-task loop', schema: QUAL_SCHEMA },
|
|
|
|
|
// opus by design: the loop's last correctness gate (spec-reviewer only checks
|
|
|
|
|
// task-text correspondence) — real-bug finding is the documented opus strength.
|
|
|
|
|
{ agentType: 'quality-reviewer', model: 'opus', label: `qual:${task.id}`, phase: 'Per-task loop', schema: QUAL_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (qual && qual.status === 'approved') break
|
|
|
|
|
// No-op / cyclic-repair backstop: a changes_requested verdict over a diff-state already reviewed-and-flagged
|
|
|
|
@@ -380,7 +387,7 @@ async function runTask(task) {
|
|
|
|
|
'A finding whose only remedy is to diverge from the ratified plan must be held or escalated, never chased ' +
|
|
|
|
|
'into a deviation the next review flags.\n\n' +
|
|
|
|
|
`TASK ${task.id} — ${task.title}\n${task.text}\n\nISSUES:\n- ` + (qual?.issues || ['(none reported)']).join('\n- '),
|
|
|
|
|
{ agentType: 'implementer', label: `impl-fix-qual:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
|
|
|
|
{ agentType: 'implementer', model: 'sonnet', label: `impl-fix-qual:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (fix && fix.applied_changes === true) wroteAnyFile = true
|
|
|
|
|
if (fix && (fix.status === 'BLOCKED' || fix.status === 'NEEDS_CONTEXT')) {
|
|
|
|
@@ -464,7 +471,7 @@ if (mode === 'mini' && outcome === 'DONE') {
|
|
|
|
|
'fix). Then run the full suite and confirm it is green (no regression). Also report working_tree_dirty — ' +
|
|
|
|
|
'whether `git status --porcelain` (NOT `git diff HEAD`, which misses new untracked files) shows any change at ' +
|
|
|
|
|
'all (a clean tree means no fix landed). Do NOT edit anything; do NOT commit.',
|
|
|
|
|
{ label: 'mini-verify', phase: 'Verify', schema: MINI_VERIFY_SCHEMA },
|
|
|
|
|
{ model: 'sonnet', label: 'mini-verify', phase: 'Verify', schema: MINI_VERIFY_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (!miniVerify || !miniVerify.red_test_now_green || !miniVerify.suite_green || !miniVerify.working_tree_dirty) {
|
|
|
|
|
outcome = 'BLOCKED'
|
|
|
|
@@ -491,7 +498,7 @@ if (mode === 'mini' && outcome === 'DONE') {
|
|
|
|
|
'`git status --porcelain` and report files_touched = its line count (every changed, added, or untracked ' +
|
|
|
|
|
'path). Use `git status --porcelain`, NOT `git diff`, so brand-new untracked files are counted. Do NOT edit, ' +
|
|
|
|
|
'do NOT commit.',
|
|
|
|
|
{ label: 'tree-check', phase: 'Verify', schema: TREE_SCHEMA },
|
|
|
|
|
{ model: 'sonnet', label: 'tree-check', phase: 'Verify', schema: TREE_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
if (!treeState || treeState.files_touched === 0) {
|
|
|
|
|
outcome = 'BLOCKED'
|
|
|
|
@@ -519,7 +526,7 @@ if (mode === 'standard' && outcome === 'DONE') {
|
|
|
|
|
'E2E fixtures for them in the project\'s canonical fixture form; each test\'s doc comment names the property ' +
|
|
|
|
|
'it protects. Run the project\'s test command — all green. Leave edits unstaged; never commit.\n\n' +
|
|
|
|
|
`iter_id: ${iter_id}\nshipped: ${completed.map((c) => `#${c.id} ${c.title}`).join('; ')}`,
|
|
|
|
|
{ agentType: 'tester', label: 'e2e', phase: 'E2E coverage', schema: E2E_SCHEMA },
|
|
|
|
|
{ agentType: 'tester', model: 'sonnet', label: 'e2e', phase: 'E2E coverage', schema: E2E_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -575,7 +582,7 @@ const fin = await agent(
|
|
|
|
|
'and `## Files touched` (from `git status --porcelain`).\n') +
|
|
|
|
|
'Report the stats path and whether you wrote BLOCKED.md. Do NOT commit anything.\n\nAGGREGATE:\n' +
|
|
|
|
|
JSON.stringify(aggregate, null, 2),
|
|
|
|
|
{ label: 'finalize', phase: 'Report', schema: FINALIZE_SCHEMA },
|
|
|
|
|
{ model: 'sonnet', label: 'finalize', phase: 'Report', schema: FINALIZE_SCHEMA },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// The no-op verdict was already taken before finalize (the tree-check / mini-verify
|
|
|
|
|