feat(implement-loop): fold the standard-mode preflight into plan-index
A standard iteration paid a dedicated ~37k-real-token dispatch for a clean-tree check. The check itself stays deterministic in the script: plan-index now carries clean/head_sha/branch/dirty_paths (run FIRST, before reading the plan; a dirty tree returns an empty index), and the script evaluates the folded verdict before any cardinality guard. Mini mode keeps its dedicated preflight — it has no plan-index to fold into. refs #29
This commit is contained in:
@@ -202,8 +202,17 @@ const PREFLIGHT_SCHEMA = {
|
||||
const TASK_INDEX_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['total_tasks', 'task_ids'],
|
||||
required: ['clean', 'head_sha', 'branch', 'total_tasks', 'task_ids'],
|
||||
properties: {
|
||||
// Folded clean-tree preflight (standard mode; issue #29): the same checks
|
||||
// the dedicated preflight dispatch runs in mini mode, carried by the
|
||||
// plan-index agent so a standard iteration saves one whole primed context.
|
||||
// The script evaluates `clean` BEFORE any cardinality guard — a dirty tree
|
||||
// never reaches the per-task loop.
|
||||
clean: { type: 'boolean', description: '`git status --porcelain` produced no output' },
|
||||
head_sha: { type: 'string' },
|
||||
branch: { type: 'string' },
|
||||
dirty_paths: { type: 'array', items: { type: 'string' } },
|
||||
total_tasks: {
|
||||
type: 'integer',
|
||||
description:
|
||||
@@ -404,25 +413,32 @@ const FINALIZE_SCHEMA = {
|
||||
},
|
||||
}
|
||||
|
||||
// ---- Phase 0 — clean-tree preflight ------------------------------------
|
||||
phase('Preflight')
|
||||
const pre = await agent(
|
||||
`${STANDING}\n\nYou are the clean-tree preflight for an implement iteration. ` +
|
||||
'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.',
|
||||
{ model: 'sonnet', effort: 'medium', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
||||
)
|
||||
if (!pre) return { status: 'BLOCKED', iter_id, reason: 'infra: preflight agent did not return' }
|
||||
if (!pre.clean) {
|
||||
return {
|
||||
status: 'BLOCKED',
|
||||
iter_id,
|
||||
reason: 'infra: working tree dirty — the orchestrator must clean it before re-invoking',
|
||||
dirty_paths: pre.dirty_paths || [],
|
||||
// ---- Phase 0 — clean-tree preflight (mini mode only) --------------------
|
||||
// Standard mode folds these checks into the plan-index dispatch (issue #29):
|
||||
// the check itself stays deterministic in the script (schema fields evaluated
|
||||
// before anything else), only the dedicated carrier dispatch is saved. Mini
|
||||
// mode has no plan-index to fold into, so it keeps the dedicated preflight.
|
||||
let startSha = null
|
||||
if (mode === 'mini') {
|
||||
phase('Preflight')
|
||||
const pre = await agent(
|
||||
`${STANDING}\n\nYou are the clean-tree preflight for an implement iteration. ` +
|
||||
'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.',
|
||||
{ model: 'sonnet', effort: 'medium', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
||||
)
|
||||
if (!pre) return { status: 'BLOCKED', iter_id, reason: 'infra: preflight agent did not return' }
|
||||
if (!pre.clean) {
|
||||
return {
|
||||
status: 'BLOCKED',
|
||||
iter_id,
|
||||
reason: 'infra: working tree dirty — the orchestrator must clean it before re-invoking',
|
||||
dirty_paths: pre.dirty_paths || [],
|
||||
}
|
||||
}
|
||||
startSha = pre.head_sha
|
||||
}
|
||||
const startSha = pre.head_sha
|
||||
|
||||
// ---- Phase 1 — assemble the task list ----------------------------------
|
||||
let tasks
|
||||
@@ -471,7 +487,11 @@ if (mode === 'mini') {
|
||||
// narrower than the closed across-tasks vector and rests on the planner's
|
||||
// bite-sized-task invariant; it is the one truncation slice left open here.
|
||||
const index = await agent(
|
||||
`${STANDING}\n\nRead the plan at ${plan_path}. List the task ` +
|
||||
`${STANDING}\n\nFIRST the folded clean-tree preflight: run \`git status --porcelain\`, \`git rev-parse HEAD\`, ` +
|
||||
'and `git rev-parse --abbrev-ref HEAD`; report `clean` (no porcelain output), `head_sha`, `branch`, and any ' +
|
||||
'`dirty_paths`. The HEAD sha is an informational anchor only — never a reset target. If the tree is DIRTY, ' +
|
||||
'stop there: return task_ids: [] and total_tasks: 0 without reading the plan.\n\n' +
|
||||
`Otherwise read the plan at ${plan_path}. List the task ` +
|
||||
(task_range
|
||||
? `ids in the range ${task_range[0]}..${task_range[1]} (inclusive, 1-based)`
|
||||
: 'ids of EVERY task') +
|
||||
@@ -480,7 +500,20 @@ if (mode === 'mini') {
|
||||
'`total_tasks`: the count of ALL tasks the entire plan defines, ignoring any range filter. Do not edit anything.',
|
||||
{ model: 'sonnet', effort: 'medium', label: 'plan-index', phase: 'Per-task loop', schema: TASK_INDEX_SCHEMA },
|
||||
)
|
||||
if (!index || !index.task_ids || index.task_ids.length === 0) {
|
||||
if (!index) {
|
||||
return { status: 'BLOCKED', iter_id, reason: 'infra: plan-index agent did not return' }
|
||||
}
|
||||
// Folded preflight verdict — evaluated before every cardinality guard.
|
||||
if (index.clean === false) {
|
||||
return {
|
||||
status: 'BLOCKED',
|
||||
iter_id,
|
||||
reason: 'infra: working tree dirty — the orchestrator must clean it before re-invoking',
|
||||
dirty_paths: index.dirty_paths || [],
|
||||
}
|
||||
}
|
||||
startSha = index.head_sha
|
||||
if (!index.task_ids || index.task_ids.length === 0) {
|
||||
return { status: 'NEEDS_CONTEXT', iter_id, reason: `no tasks found in ${plan_path}` }
|
||||
}
|
||||
// Dedup the enumerated ids (first title wins); they define the enumerate set.
|
||||
|
||||
Reference in New Issue
Block a user