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 = {
|
const TASK_INDEX_SCHEMA = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['total_tasks', 'task_ids'],
|
required: ['clean', 'head_sha', 'branch', 'total_tasks', 'task_ids'],
|
||||||
properties: {
|
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: {
|
total_tasks: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
description:
|
description:
|
||||||
@@ -404,25 +413,32 @@ const FINALIZE_SCHEMA = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Phase 0 — clean-tree preflight ------------------------------------
|
// ---- Phase 0 — clean-tree preflight (mini mode only) --------------------
|
||||||
phase('Preflight')
|
// Standard mode folds these checks into the plan-index dispatch (issue #29):
|
||||||
const pre = await agent(
|
// 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. ` +
|
`${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`. ' +
|
'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. ' +
|
'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.',
|
'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 },
|
{ 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) return { status: 'BLOCKED', iter_id, reason: 'infra: preflight agent did not return' }
|
||||||
if (!pre.clean) {
|
if (!pre.clean) {
|
||||||
return {
|
return {
|
||||||
status: 'BLOCKED',
|
status: 'BLOCKED',
|
||||||
iter_id,
|
iter_id,
|
||||||
reason: 'infra: working tree dirty — the orchestrator must clean it before re-invoking',
|
reason: 'infra: working tree dirty — the orchestrator must clean it before re-invoking',
|
||||||
dirty_paths: pre.dirty_paths || [],
|
dirty_paths: pre.dirty_paths || [],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
startSha = pre.head_sha
|
||||||
}
|
}
|
||||||
const startSha = pre.head_sha
|
|
||||||
|
|
||||||
// ---- Phase 1 — assemble the task list ----------------------------------
|
// ---- Phase 1 — assemble the task list ----------------------------------
|
||||||
let tasks
|
let tasks
|
||||||
@@ -471,7 +487,11 @@ if (mode === 'mini') {
|
|||||||
// narrower than the closed across-tasks vector and rests on the planner's
|
// 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.
|
// bite-sized-task invariant; it is the one truncation slice left open here.
|
||||||
const index = await agent(
|
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
|
(task_range
|
||||||
? `ids in the range ${task_range[0]}..${task_range[1]} (inclusive, 1-based)`
|
? `ids in the range ${task_range[0]}..${task_range[1]} (inclusive, 1-based)`
|
||||||
: 'ids of EVERY task') +
|
: '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.',
|
'`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 },
|
{ 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}` }
|
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.
|
// Dedup the enumerated ids (first title wins); they define the enumerate set.
|
||||||
|
|||||||
Reference in New Issue
Block a user