fix(workflows): fail fast on a malformed args carrier instead of misreporting it
Both workflow scripts destructured object-form `args` behind `args || {}`
only. A named-workflow invocation can deliver `args` as a string; a
non-empty string is truthy, so every carrier field silently read
`undefined` and the failure surfaced far from the cause: implement-loop
dispatched its plan-index agent against "Read the plan at undefined" and
stopped only via a mislabelled NEEDS_CONTEXT after burning agent calls;
compiler-driven-edit briefed its edit agent with "EDIT: undefined" and
mapped the agent's refusal onto the straddle-rule BOUNCE to specify —
a mechanics failure dressed as a design finding.
Guards now run before any agent dispatch, mirroring the existing
malformed-input pattern (the task_range guard):
- A string carrier is first JSON-parsed: a JSON-serialized object
carrier (a stringifying caller or substrate layer that still authored
the documented object form) is accepted; free text does not parse to
an object and is rejected.
- A carrier that is not an object (string, number, boolean, array) is a
distinct infra BLOCKED naming the received type (compiler-driven-edit:
kind bad-carrier) — never a bounce, never NEEDS_CONTEXT.
- Missing or blank required fields fail fast by name, per mode:
standard iter_id+plan_path, mini iter_id+red_test_path+cause_summary,
compiler-driven-edit edit_description+def_site. implement-loop also
rejects an unknown mode, since the required-field set keys off it.
String-form args are NOT a documented interface — the only invocation
examples in the plugin are object-form (implement/SKILL.md); the issue's
free-text-passthrough fix part was dropped on that ground (triage
verification on the issue). implement/SKILL.md now also documents the
infra early-exit end-report shape (minimal {status, iter_id, reason},
no BLOCKED.md, no blocked_detail) in the Iron Law exceptions, Step 2,
and Step 4's no-file special case.
Verified: both scripts pass node --check (async-wrapped, as the Workflow
substrate runs them) and a 17-case stub-agent harness — malformed
carriers block with zero agent dispatches; valid and JSON-serialized
object carriers reach the first agent unchanged.
closes #24
This commit is contained in:
+13
-3
@@ -74,7 +74,9 @@ Its executor is **observe-then-bounce**, shipped as
|
||||
would otherwise read as DONE. A clean working tree after the edit
|
||||
phase is a no-op, not a success → distinct `BLOCKED` (no-op-edit),
|
||||
never DONE. Positive evidence (tree differs from HEAD) is required,
|
||||
checked both at the edit phase and independently at verify.
|
||||
checked both at the edit phase and independently at verify. (A
|
||||
malformed or missing carrier is likewise a distinct `BLOCKED`
|
||||
(bad-carrier) before any agent runs — never a bounce.)
|
||||
4. **Bounce** otherwise — the change was not purely behaviour-preserving
|
||||
after all, so route up per the straddle rule: a site that forces a
|
||||
design decision → `specify`; a site that turns out to **encode new
|
||||
@@ -102,7 +104,7 @@ WITHIN THIS LOOP, MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT; THE LOOP ONLY
|
||||
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).
|
||||
NEVER PUSH PAST `BLOCKED` BY HAND.
|
||||
ON `PARTIAL` OR `BLOCKED`, THE SCRIPT WRITES `BLOCKED.md` AT THE REPO ROOT — UNCOMMITTED BY CONVENTION. ON `DONE`, NO SEPARATE FILE. (ONE EXCEPTION: A NO-OP ITERATION IS `BLOCKED` WITH NO `BLOCKED.md` — ITS TREE IS ALREADY CLEAN, SO THE STATUS + REASON RIDE THE END-REPORT AND THERE IS NOTHING TO CLEAN UP.)
|
||||
ON `PARTIAL` OR `BLOCKED`, THE SCRIPT WRITES `BLOCKED.md` AT THE REPO ROOT — UNCOMMITTED BY CONVENTION. ON `DONE`, NO SEPARATE FILE. (EXCEPTIONS THAT WRITE NO `BLOCKED.md`: A NO-OP ITERATION — ITS TREE IS ALREADY CLEAN — AND ANY PRE-LOOP EARLY EXIT (MALFORMED CARRIER, DIRTY-TREE PREFLIGHT, PLAN-EXTRACT GUARD): NOTHING RAN AND NOTHING WAS WRITTEN, SO THE STATUS + `infra:`-PREFIXED REASON RIDE THE END-REPORT AND THERE IS NOTHING TO CLEAN UP OR EXPLAIN.)
|
||||
`DONE` REQUIRES POSITIVE EVIDENCE THAT AN EDIT LANDED — A NO-OP ITERATION (`git status --porcelain` EMPTY, untracked files INCLUDED) IS NEVER `DONE`, IT IS `BLOCKED` (the issue #11 vacuous-green shape, guarded on the main path). THE VERDICT IS THE GIT TREE COUNT, NEVER A SELF-REPORT.
|
||||
IN MINI MODE THE RED->GREEN IS OBSERVED, NOT ASSERTED: AN INDEPENDENT VERIFY RE-RUNS THE HANDED-OFF RED TEST AND THE SUITE; A STILL-RED TEST OR A REGRESSION IS `BLOCKED` (route back to debug, RED-first).
|
||||
THE COMPILER-DRIVEN ARM COMMITS ONLY ON CLEAN BUILD + SUITE GREEN UNCHANGED; ELSE IT BOUNCES (specify for a design hole, tdd for discovered new behaviour, debug for a regression).
|
||||
@@ -197,7 +199,10 @@ per-task summaries, concerns, E2E fixtures, the stats path, and — on
|
||||
PARTIAL/BLOCKED — the blocked detail. Per-task chatter stayed inside
|
||||
the workflow's agent contexts and never reached the orchestrator. Read
|
||||
the end-report; it is the per-task summary you build the commit body
|
||||
from.
|
||||
from. An **infra early-exit** (malformed or missing carrier, dirty-tree
|
||||
preflight, a plan-extract guard) returns a minimal report instead:
|
||||
`status: BLOCKED` plus an `infra:`-prefixed `reason`, no `blocked_detail`
|
||||
and none of the per-task fields — nothing ran.
|
||||
|
||||
### Step 3 — Orchestrator inspect + commit step (on DONE)
|
||||
|
||||
@@ -243,6 +248,11 @@ tree:
|
||||
already clean. The diagnostic is in the end-report's `blocked_detail`, not
|
||||
a file. Nothing to clean up or stash; re-plan or re-run from the same
|
||||
clean tree.
|
||||
The same no-file shape occurs for an **infra early-exit** (`reason`
|
||||
prefixed `infra:` — malformed/missing carrier, dirty preflight tree, a
|
||||
plan-extract guard): no `BLOCKED.md`, no `blocked_detail`, and the tree
|
||||
exactly as the loop found it. Fix the invocation (or clean the tree) and
|
||||
re-invoke.
|
||||
1. Read `BLOCKED.md` — `## What did not` names the failure mode and the
|
||||
blocked task's reason. Read `git diff` to see what was attempted.
|
||||
2. Decide:
|
||||
|
||||
Reference in New Issue
Block a user