8000 Planner stuck release + logged stop verdicts; dependent-task RED rule (live run 19) by adeelahmad · Pull Request #31 · adeelahmad/aloop · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ tag in lockstep. Tag releases as `vMAJOR.MINOR.PATCH`.

## [Unreleased]

### Added
- The planner's stop gate (`gate-stage2-complete` on SubagentStop) now logs its verdicts to `gates.jsonl` and the event stream like every other gate, and the stuck release covers the planner: the same block three times in a row releases its stop unverified with a `state/stuck/plan-sprintN.<agent>` marker the supervisor acts on (a re-dispatch is a new agent id and starts over). A direct call (the supervisor's own check, `selfcheck`) is logged as a self-check and never releases. Reason: live e2e run 19's re-planning planner was blocked 9 times on one unchanged reason, none counted. Test: `scripts/test/planner-stuck.test.py`.

### Fixed
- Planner persona: a later task's RED tests must fail against what the earlier tasks leave on main, not only the empty tree (live run 19: three of T2's tests passed against T1's placeholder, costing a re-plan).
- `targets check` no longer rejects a `create` change whose file already exists **and defines its planned symbol**: that change has shipped. A mid-sprint re-plan re-runs the planner's stop gate over the whole plan, and live e2e run 19 blocked the planner 9 times on T1's merged `create tests/parse.rs` while its brief said not to touch T1. `target` now resolves such a change to `file:line`. Test: `scripts/test/semantic-targets.test.py`.

### Added
- `gate-supervisor-scope` now also enforces **the supervisor never commits on main**: a `git commit` from the main agent in the main tree while a sprint is live is blocked (a commit inside a task worktree, the initial commit of an unborn repo, `task-worktree merge` and a stale lock pass). Reason: live e2e run 16's `Track Cargo.lock` commit on main traced to nothing; the rule was prose in SKILL.md only. Test: `scripts/test/supervisor-scope.test.py`.

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ test-hosts: ## Budget/worktree regression + Codex hooks emulation + OpenCode plu
python3 scripts/test/codex-hooks.test.py
python3 scripts/test/rename-compat.test.py
python3 scripts/test/supervisor-scope.test.py
python3 scripts/test/planner-stuck.test.py
python3 scripts/test/hook-matchers.test.py
python3 scripts/test/dispatch.test.py
python3 scripts/test/log-execution.test.py
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/TRACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Status of every requirement in [ROADMAP.md](ROADMAP.md), in the four states §8
| 4.7 | Decision rationale + authority record | IMPLEMENTED_TESTED | `scripts/test/events.test.py` |
| 4.7 | Lineage: commit → intent → approver | IMPLEMENTED_TESTED | `scripts/test/commit-trace.test.py` |
| 4.7 | Every commit on main is a task's: the supervisor's own `git commit` on main is blocked mid-sprint | IMPLEMENTED_TESTED | `scripts/test/supervisor-scope.test.py` |
| 6.9 | Stuck release covers the planner; its stop verdicts are logged like every gate | IMPLEMENTED_TESTED | `scripts/test/planner-stuck.test.py` |
| 4.7 | Commit messages from rationale; graph export | IMPLEMENTED_TESTED | `scripts/test/commit-trace.test.py` |
| 4.8 | Cross-project impact provider seam | IMPLEMENTED_TESTED | `scripts/test/impact-provider.test.py` |
| 4.9 | Usage reporting: classes apart, exact or marked, unreported classes named | IMPLEMENTED_TESTED | `scripts/test/budget-meter.test.py` |
Expand Down
8 changes: 8 additions & 0 deletions plugin/agents/planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ hard; 150k / 20 min soft — see `docs/agents/defaults.md`). A worker still runn
the hard limit is STOPPED by the hook and the task comes back to you to split — never retried.
So size each task so one worker can do its RED or its GREEN well inside the soft
limit: a handful of tests, one or two files, one behaviour. When in doubt, split.
A later task's RED tests must fail against what the EARLIER tasks leave on main, not only
against the empty tree: `gate-red-verify` requires every new test to fail by assertion, so
a task whose planned tests already pass against the previous task's skeleton (a placeholder
that maps every non-success to one error variant, a default where a `todo!()` was) cannot be
RED and costs a mid-sprint re-plan (live run 19: three of T2's tests passed against T1's
placeholder). Plan each later task's tests around behaviour the earlier task's contract does
not provide, and state in the earlier task's summary what its skeleton returns for inputs
outside its scope, so the later tests can assert something it gets wrong.
If defaults.md sets `AGENTIC_TIME_MODE=auto`, propose ONE stable task duration for the
project (with a min and a max) to the supervisor, who records it with `timebox propose`;
ask for a revision only with evidence (a stats.md, an event id).
Expand Down
2 changes: 1 addition & 1 deletion plugin/bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ ctx-symbols) → WARN + grep fallback; never a silent pass.
| gate-structural-integrity | SubagentStop · structural-reviewer | appended output.md block; orphan/parallel/duplicate via ctx-symbols | no/stale output.md block / foundation-poisoning finding |
| gate-final | SubagentStop · final-gate | full matrix; zero suppressions; all plan-ready ticked | matrix red / suppression / unticked |
| gate-standards-cited | SubagentStop · standards | standards.md citations resolve | dangling citation |
| gate-stage2-complete | supervisor self-check + SubagentStop · planner | every story Stage-2 valid; no TBW; sprint self-contained (no later-sprint story IDs, no work deferred to a later sprint) | incomplete or spilling planning |
| gate-stage2-complete | supervisor self-check + SubagentStop · planner | every story Stage-2 valid; no TBW; sprint self-contained (no later-sprint story IDs, no work deferred to a later sprint); verdicts logged like every gate, and the stuck release covers the planner (3 identical blocks → stop released unverified, marker `state/stuck/plan-sprintN.<agent>`) | incomplete or spilling planning |
| gate-plan-shape | called by gates | each plan checkbox carries `path::fn` | malformed plan |
| gate-ledger-format | called by gates | execution.log line shape | malformed ledger |

Expand Down
14 changes: 11 additions & 3 deletions plugin/bin/_gatelib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ _gate_stuck() {
[ "$self" = false ] && [ -n "${TASK_ID:-}" ] && [ -n "${ATTEMPT:-}" ] || return 1
# The build roles, plus the two read-only roles whose blocks can name a defect only the
# supervisor can act on (live e2e run 12: a HIGH structural finding blocked the reviewer 10x).
case "${AGENT_ROLE:-}" in red-worker|scaffolder|green-worker|structural-reviewer|final-gate) ;; *) return 1 ;; esac
# And the planner (live e2e run 19: 9 identical blocks on a re-plan); its key is the sprint
# + the host's agent id (gate-stage2-complete), so a re-dispatch starts over.
case "${AGENT_ROLE:-}" in red-worker|scaffolder|green-worker|structural-reviewer|final-gate|planner) ;; *) return 1 ;; esac
[ "$n" -ge 2 ] 2>/dev/null && [ -f "$log" ] || return 1
prior="$(grep -F "\"gate\":\"${GATE_NAME:-?}\"" "$log" | grep -F "\"task\":\"$TASK_ID\",\"attempt\":\"$ATTEMPT\"" \
| grep -F "\"wt\":\"$(repo_dir)\"" | grep -F '"selfcheck":false' | tail -n $((n - 1)) | grep -c -F "\"exit\":2,\"selfcheck\":false,\"reason\":\"$rh\"}")"
Expand All @@ -85,8 +87,14 @@ _gate_stuck() {
printf 'role=%s gate=%s task=%s attempt=%s blocks=%s\n%s\n' "${AGENT_ROLE:-}" "${GATE_NAME:-?}" "$TASK_ID" "$ATTEMPT" "$n" \
"$GATE_BLOCK_REASON" > "$AGENTIC_STATE/stuck/$TASK_ID.$ATTEMPT" 2>/dev/null
echo "[${GATE_NAME:-gate}] STUCK — the same block $n times in a row; stop released, NOT verified." >&2
echo " This attempt is not a pass. Supervisor: do not merge; discard the worktree and re-dispatch" >&2
echo " $TASK_ID with a Feedback block that addresses the reason (.agentic/state/stuck/$TASK_ID.$ATTEMPT), or re-plan." >&2
if [ "${AGENT_ROLE:-}" = planner ]; then
echo " The plan is not approved. Supervisor: read the reason ($AGENTIC_STATE/stuck/$TASK_ID.$ATTEMPT), then" >&2
echo " re-dispatch the planner with a Feedback block that addresses it (fix the plan defect it names, or" >&2
echo " say which changes already shipped), or fix the artifact out of band and run gate-stage2-complete." >&2
else
echo " This attempt is not a pass. Supervisor: do not merge; discard the worktree and re-dispatch" >&2
echo " $TASK_ID with a Feedback block that addresses the reason ($AGENTIC_STATE/stuck/$TASK_ID.$ATTEMPT), or re-plan." >&2
fi
return 0
}

Expand Down
41 changes: 23 additions & 18 deletions plugin/bin/gate-stage2-complete
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
set -uo pipefail
BIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; export PATH="$BIN_DIR:$PATH" # md-db shim -> real binary
GATE_NAME="gate-stage2-complete"
# A direct call (the supervisor's own check, bin/selfcheck) is logged as a self-check; only a
# host SubagentStop payload makes a verdict. Decided before _gatelib.sh reads the payload.
[ -t 0 ] && export AGENTIC_SELFCHECK=1
# shellcheck source=/dev/null
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_failclosed.sh" # a crash blocks, never allows
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_gatelib.sh" # fail-closed + verdict log + stuck release
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
: "${CLAUDE_PLUGIN_ROOT:=$(cd "$HERE/.." && pwd)}"
[ -n "$GATE_PAYLOAD" ] || export AGENTIC_SELFCHECK=1

sprint_dir="${1:-${SPRINT_DIR:-}}"
if [ -z "$sprint_dir" ]; then
Expand All @@ -17,11 +21,19 @@ fi
if [ -z "$sprint_dir" ] || [ ! -d "$sprint_dir" ]; then
echo "WARN[$GATE_NAME]: no sprint dir found; stage-2 check skipped (weaker)" >&2; exit 0
fi
# The planner has no task contract: its stuck key is the sprint it plans and the host's agent
# id (live e2e run 19: one planner was blocked 9 times on one unchanged reason, uncounted).
# A re-dispatch is a new agent id, so it starts the count over.
if [ -z "${TASK_ID:-}" ] && [ -n "$GATE_PAYLOAD" ]; then
_aid="$(printf '%s' "$GATE_PAYLOAD" | sed -nE 's/.*"agent_id"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)"
if [ -n "$_aid" ]; then
export TASK_ID="plan-$(basename "$sprint_dir")" ATTEMPT="${ATTEMPT:-$(printf '%s' "$_aid" | tr -c 'A-Za-z0-9_-' '_')}"
fi
fi

# 1) No TBW placeholders anywhere.
if grep -rl 'TBW' "$sprint_dir" >/dev/null 2>&1; then
echo "BLOCK[$GATE_NAME]: TBW remains in $sprint_dir (partial Stage-2 forbidden)" >&2
exit 2
fail "TBW remains in $sprint_dir (partial Stage-2 forbidden)"
fi

# 2) Every SN-NN story in stories.md has a dir with tasks.md + validate.md + plan.md.
Expand All @@ -33,13 +45,12 @@ if [ -f "$stories" ]; then
lc="$(echo "$id" | tr 'A-Z' 'a-z')" # s1-01
dir="$(find "$sprint_dir" -maxdepth 1 -type d -name "${lc}-*" | head -1)"
if [ -z "$dir" ]; then
echo "BLOCK[$GATE_NAME]: no story dir for $id: expected $sprint_dir/${lc}-<slug>/ (lowercase, e.g. ${lc}-count-words/) holding tasks.md, validate.md and plan.md" >&2
other="$(find "$sprint_dir" -maxdepth 1 -type d -iname "${lc}*" | head -1)"
[ -n "$other" ] && echo " found $other: rename it, e.g. git mv '$other' '$sprint_dir/${lc}-<slug>'" >&2
exit 2
fail "no story dir for $id: expected $sprint_dir/${lc}-<slug>/ (lowercase, e.g. ${lc}-count-words/) holding tasks.md, validate.md and plan.md${other:+
found $other: rename it, e.g. git mv '$other' '$sprint_dir/${lc}-<slug>'}"
fi
for f in tasks.md validate.md plan.md; do
[ -f "$dir/$f" ] || { echo "BLOCK[$GATE_NAME]: $id missing $f" >&2; exit 2; }
[ -f "$dir/$f" ] || fail "$id missing $f"
done
done
else
Expand All @@ -56,32 +67,26 @@ if [ -n "$this_n" ]; then
-exec grep -hoE '\bS[0-9]+-[0-9]+\b' {} + 2>/dev/null \
| sed -E 's/^S([0-9]+)-.*/\1/' | sort -un | awk -v n="$this_n" '$1>n' | head -1)"
if [ -n "$later" ]; then
echo "BLOCK[$GATE_NAME]: sprint $this_n references story IDs from later sprint $later —" \
"a sprint must be self-contained (context is cleared between sprints). Cut the" \
"dependency or move the story into this sprint." >&2
exit 2
fail "sprint $this_n references story IDs from later sprint $later — a sprint must be self-contained (context is cleared between sprints). Cut the dependency or move the story into this sprint."
fi
spill="$(find "$sprint_dir" -name '*.md' ! -name stories.md ! -name init.md ! -name output.md \
-exec grep -lEi '(continue|continued|finish|finished|complete|completed|defer|deferred|carry|carried|spill|pick(ed)? up)[a-z]* (it |this |the rest )?(in|into|to|over to|during) (the )?(next|later|following|future|subsequent) sprint|sprint ?(n ?\+ ?1|'"$((this_n+1))"')\b' {} + 2>/dev/null | head -1)"
if [ -n "$spill" ]; then
echo "BLOCK[$GATE_NAME]: $spill defers work into a later sprint — every task must" \
"finish inside sprint $this_n. Cut the story smaller; list the remainder under" \
"stories.md 'Out of scope'." >&2
exit 2
fail "$spill defers work into a later sprint — every task must finish inside sprint $this_n. Cut the story smaller; list the remainder under stories.md 'Out of scope'."
fi
fi

# 4) Identity + referential integrity (ROADMAP §4.5): issue canonical ids for every story
# and task alias, check the structured references between artifacts, freeze the map.
"$BIN_DIR/ids" freeze "$sprint_dir" \
|| { echo "BLOCK[$GATE_NAME]: broken cross-artifact reference in $sprint_dir (see above)" >&2; exit 2; }
|| fail "broken cross-artifact reference in $sprint_dir (see above)"

# 5) Semantic change targets (ROADMAP §4.6), where the planner wrote them: each target's
# task, operation, path and symbol must hold against tasks.md and the real tree.
for t in "$sprint_dir"/s*-*-*/targets.json; do
[ -f "$t" ] || continue
"$BIN_DIR/targets" check "$(dirname "$t")" \
|| { echo "BLOCK[$GATE_NAME]: invalid change targets in $t (see above)" >&2; exit 2; }
|| fail "invalid change targets in $t (see above)"
done

# 6) Structural validation via md-db (fallback to frontmatter check).
Expand All @@ -91,7 +96,7 @@ if "$BIN_DIR/ensure-tools" --resolve md-db >/dev/null 2>&1; then
find "$sprint_dir" -type f \( -name stories.md -o -name tasks.md -o -name validate.md \
-o -name plan.md -o -name plan-ready.md \) \
| md-db validate --stdin-list --schema "${CLAUDE_PLUGIN_ROOT}/schemas/planning-artifacts.kdl" \
|| { echo "BLOCK[$GATE_NAME]: md-db rejected $sprint_dir" >&2; exit 2; }
|| fail "md-db rejected $sprint_dir"
else
echo "WARN[$GATE_NAME]: md-db absent; structural validation limited to dir/frontmatter checks" >&2
fi
Expand Down
10 changes: 9 additions & 1 deletion plugin/bin/targets
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,10 @@ def problems(sdir, targets, root, doc=None):
p.append("%s: create needs planned_symbol" % where)
if t.get("symbol"):
p.append("%s: create names a planned_symbol, not an existing symbol" % where)
if exists:
# A create whose file now exists AND defines its planned_symbol is realized (its task
# merged; a mid-sprint re-plan re-checks the whole plan), not a defect.
if exists and (not t.get("planned_symbol")
or locate(root, path, t["planned_symbol"])[0] is None):
p.append("%s: create targets %s, which already exists (use modify/extend)" % (where, path))
elif not exists and path not in planned:
p.append("%s: %s targets %s, which is neither in the tree nor planned" % (where, op, path))
Expand All @@ -259,6 +262,11 @@ def problems(sdir, targets, root, doc=None):


def resolve(t, root):
if t.get("operation") == "create" and t.get("planned_symbol") and os.path.exists(os.path.join(root, t["path"])):
line, be = locate(root, t["path"], t["planned_symbol"])
if line is not None: # realized create: the planned file and symbol exist now
return "%s %s %s:%s %s [%s] — %s" % (t["change_ref"], t["operation"], t["path"], line,
t["planned_symbol"], be, t.get("reason", ""))
if t.get("operation") == "create" or not os.path.exists(os.path.join(root, t["path"])):
return "%s %s %s %s (planned, shadow) — %s" % (t["change_ref"], t["operation"], t["path"],
t.get("planned_symbol", ""), t.get("reason", ""))
Expand Down
8 changes: 8 additions & 0 deletions plugin/pipeline/planning/02-planner/persona.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ hard; 150k / 20 min soft — see `docs/agents/defaults.md`). A worker still runn
the hard limit is STOPPED by the hook and the task comes back to you to split — never retried.
So size each task so one worker can do its RED or its GREEN well inside the soft
limit: a handful of tests, one or two files, one behaviour. When in doubt, split.
A later task's RED tests must fail against what the EARLIER tasks leave on main, not only
against the empty tree: `gate-red-verify` requires every new test to fail by assertion, so
a task whose planned tests already pass against the previous task's skeleton (a placeholder
that maps every non-success to one error variant, a default where a `todo!()` was) cannot be
RED and costs a mid-sprint re-plan (live run 19: three of T2's tests passed against T1's
placeholder). Plan each later task's tests around behaviour the earlier task's contract does
not provide, and state in the earlier task's summary what its skeleton returns for inputs
outside its scope, so the later tests can assert something it gets wrong.
If defaults.md sets `AGENTIC_TIME_MODE=auto`, propose ONE stable task duration for the
project (with a min and a max) to the supervisor, who records it with `timebox propose`;
ask for a revision only with evidence (a stats.md, an event id).
Expand Down
2 changes: 1 addition & 1 deletion plugin/skills/aloop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ in a legacy project — the gates read either):
.aladdin/task.env optional copy of the contract (Codex/OpenCode, older sprints);
it must match the contract when both exist
.aladdin/budget-exceeded marker the time-box hook writes at a HARD limit
main tree: .aladdin/state/stuck/<task>.<attempt> a gate released a worker that repeated
main tree: .aladdin/state/stuck/<task>.<attempt> a gate released a worker (or, as plan-sprintN.<agent>, a planner) that repeated
one block 3 times (unverified; `budget watch` reports it once)
STORY_DIR is the ABSOLUTE path to sN-NN-<slug>/ (the comms dir).
.aladdin/scaffold-symbols scaffolder-written production-symbol list (scaffold gate)
Expand Down
Loading
Loading
0