fix(implement-loop): guard against a task discarding sibling tasks' uncommitted work
During a multi-task run nothing commits between tasks, so one task's file-level `git checkout -- <file>` / `git restore <file>` silently destroys an earlier task's uncommitted DONE work in a shared file. The loss was structurally invisible: spec- and quality-reviewers are scoped to the current task's footprint, diff_fingerprint only detects same-task repair cycles, and the end-of-iteration tree gate counts files (the surviving changes keep it nonzero). Observed in the wild as a PARTIAL run whose DONE reports did not match the tree. Two layers, both plugin-side (harness-level prevention is ruled out — decision log on the issue): Prose guard — implementer.md Step 8 no longer sanctions file checkout for scope curation: over-reach is undone by editing back, a broken intermediate state is repaired forward or reported BLOCKED (new fourth BLOCKED bucket in the status protocol). A matching Iron Law line in implement/SKILL.md covers all in-loop agents, whole-file and --patch; docs/conventions.md now marks the checkout discard idiom as the orchestrator's, between iterations. Mechanical guard — after every task of a multi-task run a snapshot agent records `git stash create` (a dangling commit; HEAD, index, and tree untouched — semantics verified empirically in a scratch repo) plus the `git diff HEAD --name-only --no-renames` path set. A path that was HEAD-modified at one boundary and gone at the next trips a hard BLOCKED naming the lost paths, the boundary, and the recovery snapshot sha (`git show <sha>:<path>`); the discard verdict outranks per-task outcomes in blocked_detail since the reports and the tree have diverged. Coarse by design, in both directions, and documented as such: a checkout-then-re-edit or a --patch hunk restore escapes the comparison (the snapshot keeps it diagnosable); a legitimate back-to-HEAD edit trips it (the verdict says adjudicate against the snapshot). --no-renames keeps a staged rename from reading as a loss; untracked files are outside the threat model (checkout cannot discard them). Cost: one sonnet/medium call per task, multi-task runs only; single-task and mini runs are unchanged. Verified: node --check on the async-wrapped script and a stub-agent harness — discard trips the hard BLOCKED and stops the loop; an accumulating happy path, a plan-intended deletion, a single-task run, and a dead snapshot agent all pass without a false positive. closes #23
This commit is contained in:
@@ -159,8 +159,8 @@ pin exists to remove. Effort follows the model split:
|
|||||||
|
|
||||||
Workflow scripts pin a third tier inline: schema-bound
|
Workflow scripts pin a third tier inline: schema-bound
|
||||||
extraction/verification stages that author no code (preflight,
|
extraction/verification stages that author no code (preflight,
|
||||||
plan-index, plan-extract, mini-verify, tree-check, finalize, build/suite verify)
|
plan-index, plan-extract, snapshot, mini-verify, tree-check, finalize,
|
||||||
run `medium` via the `agent()` call's `effort:` option. As with
|
build/suite verify) run `medium` via the `agent()` call's `effort:` option. As with
|
||||||
`model:`, every `agent()` call passes `effort:` explicitly — whether
|
`model:`, every `agent()` call passes `effort:` explicitly — whether
|
||||||
frontmatter effort propagates through an `agentType` dispatch is
|
frontmatter effort propagates through an `agentType` dispatch is
|
||||||
undocumented, so the scripts do not rely on it (see the policy header
|
undocumented, so the scripts do not rely on it (see the policy header
|
||||||
|
|||||||
+8
-2
@@ -130,8 +130,14 @@ too.
|
|||||||
- **main HEAD is sacrosanct — below the session anchor.** Nobody runs
|
- **main HEAD is sacrosanct — below the session anchor.** Nobody runs
|
||||||
`git reset` / `git revert` on user-ratified main history (or any
|
`git reset` / `git revert` on user-ratified main history (or any
|
||||||
protected branch). main moves forward only via orchestrator commits; a
|
protected branch). main moves forward only via orchestrator commits; a
|
||||||
wrong agent diff is discarded with `git checkout -- <paths>` /
|
wrong agent diff is discarded — by the orchestrator, between
|
||||||
`git stash`, never by rewinding ratified history.
|
iterations — with `git checkout -- <paths>` / `git stash`, never by
|
||||||
|
rewinding ratified history. That discard idiom is the orchestrator's
|
||||||
|
alone: an agent running INSIDE a multi-task `implement` iteration
|
||||||
|
never checkouts/restores a file — nothing commits between tasks, so
|
||||||
|
it would silently destroy sibling tasks' uncommitted work; in-loop
|
||||||
|
recovery is forward-editing or `BLOCKED` (`implement/SKILL.md` Iron
|
||||||
|
Law, issue #23).
|
||||||
**One narrow `/boss` exception — the rollback sandbox:** inside an
|
**One narrow `/boss` exception — the rollback sandbox:** inside an
|
||||||
autonomous run the orchestrator anchors on the `main` HEAD it started
|
autonomous run the orchestrator anchors on the `main` HEAD it started
|
||||||
from (the last user-ratified state) and MAY `git reset --hard` its
|
from (the last user-ratified state) and MAY `git reset --hard` its
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ regression around the RED-first gate.
|
|||||||
```
|
```
|
||||||
THE LOOP NEVER COMMITS. CODE EDITS, TESTS, AND THE STATS FILE LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM.
|
THE LOOP NEVER COMMITS. CODE EDITS, TESTS, AND THE STATS FILE LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM.
|
||||||
WITHIN THIS LOOP, MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT; THE LOOP ONLY EVER WRITES UNSTAGED CHANGES. MAIN MOVES FORWARD ONLY VIA ORCHESTRATOR COMMITS. (THE `/boss` ROLLBACK SANDBOX WINDS BACK ALREADY-COMMITTED AUTONOMOUS WORK — A BOSS-LEVEL RECOVERY, NOT A STEP IN THIS LOOP.)
|
WITHIN THIS LOOP, MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT; THE LOOP ONLY EVER WRITES UNSTAGED CHANGES. MAIN MOVES FORWARD ONLY VIA ORCHESTRATOR COMMITS. (THE `/boss` ROLLBACK SANDBOX WINDS BACK ALREADY-COMMITTED AUTONOMOUS WORK — A BOSS-LEVEL RECOVERY, NOT A STEP IN THIS LOOP.)
|
||||||
|
NO IN-LOOP AGENT EVER DISCARDS WORKING-TREE STATE VIA `git checkout` / `git restore` — WHOLE-FILE OR `--patch`: NOTHING COMMITS BETWEEN TASKS, SO A SHARED FILE CARRIES EARLIER TASKS' UNCOMMITTED DONE WORK, AND A CHECKOUT SILENTLY DESTROYS IT (issue #23). RECOVERY IS FORWARD — EDIT BACK, OR REPORT `BLOCKED`. (THE ORCHESTRATOR'S OWN END-OF-ITERATION `git checkout -- .` DISPOSITION HAPPENS OUTSIDE THE LOOP.)
|
||||||
PER-TASK PHASES ARE SEPARATE AGENT CALLS IN THE WORKFLOW SCRIPT — implementer, then spec-compliance, then quality. SPEC COMPLIANCE IS GATED BEFORE QUALITY.
|
PER-TASK PHASES ARE SEPARATE AGENT CALLS IN THE WORKFLOW SCRIPT — implementer, then spec-compliance, then quality. SPEC COMPLIANCE IS GATED BEFORE QUALITY.
|
||||||
TASKS RUN SEQUENTIALLY; A BLOCKED TASK STOPS THE LOOP — NO SKIP-AHEAD (TASK ORDERING DEPENDENCIES ARE UNKNOWN).
|
TASKS RUN SEQUENTIALLY; A BLOCKED TASK STOPS THE LOOP — NO SKIP-AHEAD (TASK ORDERING DEPENDENCIES ARE UNKNOWN).
|
||||||
NEVER PUSH PAST `BLOCKED` BY HAND.
|
NEVER PUSH PAST `BLOCKED` BY HAND.
|
||||||
@@ -155,6 +156,32 @@ ground truth, never a self-report** — a self-report must never be able to
|
|||||||
The E2E phase's `status` is also read now (not just its fixtures): a
|
The E2E phase's `status` is also read now (not just its fixtures): a
|
||||||
non-`DONE` E2E status or a zero-fixture run surfaces as a concern.
|
non-`DONE` E2E status or a zero-fixture run surfaces as a concern.
|
||||||
|
|
||||||
|
**Cross-task discard guard** (issue #23). In a multi-task run nothing
|
||||||
|
commits between tasks, so one task's file-level `git checkout`/`git
|
||||||
|
restore` can silently destroy an earlier task's DONE work in a shared
|
||||||
|
file — invisible to the reviewers (scoped to the current task's
|
||||||
|
footprint) and to the tree-count gate (the surviving changes keep the
|
||||||
|
count nonzero). The loop therefore records a snapshot boundary after
|
||||||
|
every task (`git stash create` — a dangling commit; HEAD, index, and
|
||||||
|
working tree untouched) and hard-`BLOCK`s when a path that was
|
||||||
|
HEAD-modified at one boundary is HEAD-identical at the next: the
|
||||||
|
end-report names the lost paths, the boundary, and the recovery
|
||||||
|
snapshot sha the discarded content can be read from
|
||||||
|
(`git show <sha>:<path>`). Coarse by design, in both directions: a
|
||||||
|
checkout followed by re-edits of the same file, or a `--patch`-level
|
||||||
|
restore of a single hunk, escapes the comparison (the path never
|
||||||
|
leaves the diff) — the snapshot still keeps any loss diagnosable and
|
||||||
|
recoverable; conversely a task that legitimately edits a shared file
|
||||||
|
back to byte-identical HEAD content trips the guard, so a tripped
|
||||||
|
verdict means *adjudicate against the snapshot*, not proven malice.
|
||||||
|
Renames do not trip it (the boundary diff runs `--no-renames`, so a
|
||||||
|
renamed file's old path stays listed as a deletion). Untracked files
|
||||||
|
sit outside the threat model (`git checkout -- <path>` cannot discard
|
||||||
|
them). Cost: one extra sonnet/medium snapshot call per task of a
|
||||||
|
multi-task run; single-task and mini runs skip the guard. The
|
||||||
|
detection details live in `workflows/implement-loop.js`, the single
|
||||||
|
source.
|
||||||
|
|
||||||
## The Process — orchestrator side
|
## The Process — orchestrator side
|
||||||
|
|
||||||
### Step 1 — Run the `implement-loop` workflow
|
### Step 1 — Run the `implement-loop` workflow
|
||||||
|
|||||||
@@ -162,8 +162,17 @@ substitute.
|
|||||||
Law from `tester.md` applies to your tests too.
|
Law from `tester.md` applies to your tests too.
|
||||||
8. Self-review: re-read `git diff HEAD`. Did it match the
|
8. Self-review: re-read `git diff HEAD`. Did it match the
|
||||||
task text? Did you do anything not in the task text?
|
task text? Did you do anything not in the task text?
|
||||||
If yes, undo that part with `git checkout -- <path>` or
|
If yes, undo that part BY EDITING BACK — never with
|
||||||
by editing back — controller curates scope, not you.
|
`git checkout` / `git restore`, whole-file or
|
||||||
|
`-p`/`--patch`; the same holds for recovering from a
|
||||||
|
broken intermediate state: repair forward by editing,
|
||||||
|
or report `BLOCKED`.
|
||||||
|
Nothing commits between the tasks of an iteration, so a
|
||||||
|
file you touched may also carry EARLIER tasks'
|
||||||
|
uncommitted, already-reviewed work — `git diff HEAD`
|
||||||
|
shows those sibling chunks, and a file-level checkout
|
||||||
|
silently destroys them along with yours. Controller
|
||||||
|
curates scope, not you.
|
||||||
Did the test you wrote actually fail before the GREEN
|
Did the test you wrote actually fail before the GREEN
|
||||||
code, or did you write it after? If after, delete the
|
code, or did you write it after? If after, delete the
|
||||||
production code and start over. (TDD is letter-and-
|
production code and start over. (TDD is letter-and-
|
||||||
@@ -196,6 +205,10 @@ End every report with exactly one of:
|
|||||||
design ledger forbids)
|
design ledger forbids)
|
||||||
- hypothesis-space exhaustion (≥ 3 implementation
|
- hypothesis-space exhaustion (≥ 3 implementation
|
||||||
strategies failed — architecture is wrong, escalate)
|
strategies failed — architecture is wrong, escalate)
|
||||||
|
- unrecoverable intermediate state (the tree broke
|
||||||
|
mid-task and forward repair by editing failed —
|
||||||
|
checkout/restore is not an option: it destroys
|
||||||
|
sibling tasks' uncommitted work, Step 8)
|
||||||
Never push past BLOCKED by hand.
|
Never push past BLOCKED by hand.
|
||||||
|
|
||||||
## Output format
|
## Output format
|
||||||
|
|||||||
@@ -39,6 +39,28 @@
|
|||||||
// implementer's call, not enforced in code (the script cannot tell a
|
// implementer's call, not enforced in code (the script cannot tell a
|
||||||
// cosmetic hold from an ignored bug) — hence the neutral concern label
|
// cosmetic hold from an ignored bug) — hence the neutral concern label
|
||||||
// and the orchestrator backstop, same trust placed in its other reports.
|
// and the orchestrator backstop, same trust placed in its other reports.
|
||||||
|
// • Cross-task discard guard (issue #23): nothing commits between tasks,
|
||||||
|
// so a task's file-level `git checkout`/`git restore` can silently
|
||||||
|
// destroy an EARLIER task's uncommitted DONE work in a shared file —
|
||||||
|
// invisible to the footprint-scoped reviewers and to the tree-count
|
||||||
|
// gate (the surviving changes keep the count nonzero). In a multi-task
|
||||||
|
// run the loop records a snapshot boundary after every task
|
||||||
|
// (`git stash create`: a dangling commit — HEAD, index, and tree
|
||||||
|
// untouched) and hard-BLOCKs when a path that was HEAD-modified at one
|
||||||
|
// boundary is HEAD-identical at the next, naming the lost paths and the
|
||||||
|
// recovery snapshot sha. Coarse by design, in BOTH directions: a
|
||||||
|
// checkout-then-re-edit of the same file or a `--patch`-level hunk
|
||||||
|
// restore escapes the comparison (the path never leaves the diff) — the
|
||||||
|
// snapshot still keeps any loss diagnosable; conversely a task that
|
||||||
|
// legitimately edits a shared file back to byte-identical HEAD content
|
||||||
|
// trips the guard, so the verdict reads "adjudicate against the
|
||||||
|
// snapshot", not proof of a discard. `--no-renames` keeps a staged
|
||||||
|
// rename's old path listed (as a deletion) rather than reading as a
|
||||||
|
// loss. The snapshot agent itself is prompt-bound to the read-only
|
||||||
|
// `stash create` (a bare `git stash` would move the tree; the following
|
||||||
|
// boundary would catch that wipe, but only after the fact). Untracked
|
||||||
|
// files sit outside the threat model (`git checkout -- <path>` cannot
|
||||||
|
// discard them).
|
||||||
// • The script has no filesystem or shell access of its own — every
|
// • The script has no filesystem or shell access of its own — every
|
||||||
// working-tree mutation (code, stats.json, BLOCKED.md) happens inside
|
// working-tree mutation (code, stats.json, BLOCKED.md) happens inside
|
||||||
// an agent() call.
|
// an agent() call.
|
||||||
@@ -50,8 +72,9 @@
|
|||||||
// • Effort policy mirrors the model policy: every agent() call pins an
|
// • Effort policy mirrors the model policy: every agent() call pins an
|
||||||
// explicit effort — never the session inherit. Code-writing and review
|
// explicit effort — never the session inherit. Code-writing and review
|
||||||
// steps run high, the opus quality gate runs xhigh, and schema-bound
|
// steps run high, the opus quality gate runs xhigh, and schema-bound
|
||||||
// extraction/check steps (preflight, plan-index, plan-extract, mini-verify,
|
// extraction/check steps (preflight, plan-index, plan-extract, snapshot,
|
||||||
// tree-check, finalize) run medium. See docs/agent-template.md § effort.
|
// mini-verify, tree-check, finalize) run medium. See
|
||||||
|
// docs/agent-template.md § effort.
|
||||||
//
|
//
|
||||||
// Carrier — passed as the Workflow `args` object by the orchestrator:
|
// Carrier — passed as the Workflow `args` object by the orchestrator:
|
||||||
// mode: 'standard' | 'mini'
|
// mode: 'standard' | 'mini'
|
||||||
@@ -319,6 +342,31 @@ const TREE_SCHEMA = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Cross-task discard guard (issue #23): the per-boundary snapshot verdict.
|
||||||
|
// `git stash create` writes a dangling commit capturing the tracked
|
||||||
|
// working-tree state without touching HEAD, the index, or the tree; the
|
||||||
|
// HEAD-modified path set is the comparison key across boundaries.
|
||||||
|
const SNAP_SCHEMA = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['snapshot_sha', 'modified_paths'],
|
||||||
|
properties: {
|
||||||
|
snapshot_sha: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'the sha printed by `git stash create "<label>"` — empty string if it printed nothing (no tracked ' +
|
||||||
|
'modification to snapshot)',
|
||||||
|
},
|
||||||
|
modified_paths: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description:
|
||||||
|
'every path printed by `git diff HEAD --name-only --no-renames` (tracked modifications and deletions; ' +
|
||||||
|
'`--no-renames` keeps a renamed file\'s old path listed as a deletion; untracked files are invisible ' +
|
||||||
|
'here by design)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
const FINALIZE_SCHEMA = {
|
const FINALIZE_SCHEMA = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -652,9 +700,50 @@ async function runTask(task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = []
|
const results = []
|
||||||
|
// Cross-task discard guard (issue #23) state: the previous task boundary
|
||||||
|
// (snapshot sha + HEAD-modified path set) and, when tripped, the discard
|
||||||
|
// evidence. Only a multi-task run needs the guard — with a single task there
|
||||||
|
// is no sibling work to protect.
|
||||||
|
let prevBoundary = null
|
||||||
|
let discard = null
|
||||||
|
const guardConcerns = []
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
const r = await runTask(task)
|
const r = await runTask(task)
|
||||||
results.push(r)
|
results.push(r)
|
||||||
|
if (tasks.length > 1) {
|
||||||
|
const snap = await agent(
|
||||||
|
`${STANDING}\n\nRecord a working-tree snapshot boundary WITHOUT changing anything. Run exactly these two ` +
|
||||||
|
'commands and report their output:\n' +
|
||||||
|
`1. \`git stash create "iter ${iter_id} post-task ${task.id}"\` — prints a sha (a dangling commit; it ` +
|
||||||
|
'touches neither HEAD, nor the index, nor the working tree). Report it as snapshot_sha; empty string if ' +
|
||||||
|
'it printed nothing.\n' +
|
||||||
|
'2. `git diff HEAD --name-only --no-renames` — report every printed path as modified_paths.\n' +
|
||||||
|
'NEVER run bare `git stash` or `git stash push/pop/apply/drop` — those MOVE the changes. Do NOT edit, ' +
|
||||||
|
'stage, or commit anything.',
|
||||||
|
{ model: 'sonnet', effort: 'medium', label: `snapshot:${task.id}`, phase: 'Per-task loop', schema: SNAP_SCHEMA },
|
||||||
|
)
|
||||||
|
if (snap && prevBoundary) {
|
||||||
|
const now = new Set(snap.modified_paths || [])
|
||||||
|
const lost = [...prevBoundary.paths].filter((p) => !now.has(p))
|
||||||
|
if (lost.length) {
|
||||||
|
// A path that was HEAD-modified at the previous boundary is
|
||||||
|
// HEAD-identical now: a file-level checkout/restore discarded
|
||||||
|
// sibling-task work mid-run. Hard stop — the per-task DONE reports
|
||||||
|
// and the tree have diverged, so nothing downstream is trustworthy.
|
||||||
|
discard = {
|
||||||
|
lost_paths: lost,
|
||||||
|
modified_through_task: prevBoundary.taskId,
|
||||||
|
discarded_during_task: task.id,
|
||||||
|
recovery_snapshot: prevBoundary.sha || null,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!snap) guardConcerns.push(`discard-guard snapshot after task #${task.id} did not return — that boundary went unchecked`)
|
||||||
|
// A non-returning snapshot agent skips the NEXT comparison (prevBoundary
|
||||||
|
// null) rather than false-positiving against a stale boundary.
|
||||||
|
prevBoundary = snap ? { taskId: task.id, sha: snap.snapshot_sha || null, paths: new Set(snap.modified_paths || []) } : null
|
||||||
|
}
|
||||||
if (r.outcome === 'BLOCKED') break // do not skip ahead — task ordering dependencies are unknown
|
if (r.outcome === 'BLOCKED') break // do not skip ahead — task ordering dependencies are unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +764,29 @@ else outcome = 'BLOCKED'
|
|||||||
let synthBlock = null
|
let synthBlock = null
|
||||||
let noopClean = false
|
let noopClean = false
|
||||||
|
|
||||||
|
// Cross-task discard override (issue #23): a tripped discard guard outranks
|
||||||
|
// every per-task outcome — the reports and the tree have diverged, so neither
|
||||||
|
// DONE nor PARTIAL is trustworthy (the reported-DONE work may be the very
|
||||||
|
// content that was destroyed). Hard BLOCKED; the recovery snapshot is a
|
||||||
|
// dangling commit the orchestrator reads the discarded content from
|
||||||
|
// (`git show <sha>:<path>`, `git diff <sha> HEAD -- <path>`).
|
||||||
|
if (discard) {
|
||||||
|
outcome = 'BLOCKED'
|
||||||
|
synthBlock = {
|
||||||
|
task: discard.discarded_during_task,
|
||||||
|
reason:
|
||||||
|
`working-tree discard detected: [${discard.lost_paths.join(', ')}] carried uncommitted edits through ` +
|
||||||
|
`task #${discard.modified_through_task} and reverted to HEAD-identical during task #${discard.discarded_during_task} — ` +
|
||||||
|
'a file-level checkout/restore destroyed sibling-task work (or, rarely, a task legitimately edited the ' +
|
||||||
|
'file back to HEAD content); adjudicate against the recovery snapshot — until then the DONE reports ' +
|
||||||
|
'covering these paths are not trustworthy',
|
||||||
|
detail: discard.recovery_snapshot
|
||||||
|
? `recovery snapshot (dangling commit): ${discard.recovery_snapshot} — read the discarded content via ` +
|
||||||
|
`git show ${discard.recovery_snapshot}:<path>`
|
||||||
|
: 'no recovery snapshot available: the pre-discard boundary printed no sha',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Phase 2.5 — verify (issue #12, facets 1+2+4) ----------------------
|
// ---- Phase 2.5 — verify (issue #12, facets 1+2+4) ----------------------
|
||||||
// mini mode: independently OBSERVE the handed-off RED test is now GREEN (a no-op
|
// mini mode: independently OBSERVE the handed-off RED test is now GREEN (a no-op
|
||||||
// leaves it RED while the implementer can still report DONE). standard mode: the
|
// leaves it RED while the implementer can still report DONE). standard mode: the
|
||||||
@@ -781,10 +893,12 @@ const aggregate = {
|
|||||||
tasks_total: tasks.length,
|
tasks_total: tasks.length,
|
||||||
tasks_completed: completed.length,
|
tasks_completed: completed.length,
|
||||||
task_summaries: results.map((r) => ({ id: r.id, title: r.title, outcome: r.outcome })),
|
task_summaries: results.map((r) => ({ id: r.id, title: r.title, outcome: r.outcome })),
|
||||||
concerns: [...results.flatMap((r) => r.concerns || []), ...e2eConcerns],
|
concerns: [...results.flatMap((r) => r.concerns || []), ...e2eConcerns, ...guardConcerns],
|
||||||
blocked_detail: blocked
|
// synthBlock (a discard or iteration-level no-op verdict) outranks the
|
||||||
? { task: blocked.id, reason: blocked.reason, detail: blocked.detail || null }
|
// per-task block: when the discard guard broke the loop on a task that
|
||||||
: synthBlock,
|
// ALSO blocked, the discard is the cause and the task block the symptom.
|
||||||
|
blocked_detail:
|
||||||
|
synthBlock || (blocked ? { task: blocked.id, reason: blocked.reason, detail: blocked.detail || null } : null),
|
||||||
}
|
}
|
||||||
|
|
||||||
const fin = await agent(
|
const fin = await agent(
|
||||||
|
|||||||
Reference in New Issue
Block a user