8000 fix: live e2e run 12 gaps — stale worktree checkouts are not duplicates, stuck release for the reviewer and final gate; ROADMAP §0 statuses by adeelahmad · Pull Request #17 · 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ tag in lockstep. Tag releases as `vMAJOR.MINOR.PATCH`.
reading it stays allowed. 2 checks failed before.

### Fixed
- The planner persona lists the exact frontmatter and `##` sections `planning-artifacts.kdl`
requires, so its first write passes (every live run so far wrote each artifact twice: 8–12
write blocks per run).
- `ctx-symbols` (and the gate's grep fallback) no longer scan a host's worktree checkouts
inside the main tree (`.claude/worktrees/agent-<id>/`), nor `.agentic/` or `.transcripts/`:
a stale checkout duplicated every symbol of main and the structural review halted on a
false HIGH finding 20 times (live e2e run 12). The exclusion applies below the walk root, so a
reviewer whose own worktree is such a checkout is still scanned. 3 conformance checks (§6.7),
2 unit tests.
- The stuck release also covers the structural reviewer and the final gate, whose blocks can name
a defect only the supervisor can act on (run 12: 10 identical blocks). 1 conformance check.
- `gate-validate-artifact`'s block now carries md-db's diagnostics (the missing section or field
and its hint): a PostToolUse block hands the model only stderr, and the bare "failed the
schema" made the planner guess (live e2e run 11: every planning artifact was written twice,
Expand Down
4 changes: 2 additions & 2 deletions docs/OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ The model can ignore a skill; it cannot ignore an exit code.
- The full planning → execution loop on Claude Code; Codex and OpenCode via generated
agents and the same gates.
- CI on every PR: Rust fmt/clippy/tests for both backends, shellcheck on every gate, the
ROADMAP §6 conformance fixtures (real gates on a fixture crate, 54 checks), the host tests
ROADMAP §6 conformance fixtures (real gates on a fixture crate, 58 checks), the host tests
(budget/worktree, Codex hooks emulation, supervisor scope, hook matchers, dispatch,
ensure-tools, fail-closed: 73 checks; OpenCode end-to-end: 20), `claude plugin validate
--strict`, eval-suite and version-lockstep checks.
- The north-star run, `make e2e-live YES=1 [INTENT=wordfreq]`: a one-line intent becomes
planned, test-first, gate-verified, merged code in a real Claude Code session with the
approval given up front. Two intents pass every check (semver parser: 11/11, ~$0.65–1.15,
11–19 min; word frequencies with two dependent stories: 12/12, $0.86–1.56, 13–45 min). Every run so
11–19 min; word frequencies with two dependent stories: 12/12, $0.86–1.56, 13–45 min; the last run 13m47s, $0.88, one host-run block). Every run so
far has found a gate false positive that unit tests missed; ROADMAP M0.8 lists them.
- One measured real run (*TheNoteBook*, a Go backend): ~17K LOC, 322 tests, 74 commits in 6
sprints for ~1.55M output tokens, 76% of it on Sonnet. See the
Expand Down
18 changes: 9 additions & 9 deletions docs/ROADMAP.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions plugin/agents/planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ until this persona's output is complete and Stage-2-valid.
- Keep artifacts consistent: every story has a dir `sprintN/sN-NN-<slug>/` (lowercase,
e.g. `s1-01-count-words/` for S1-01) holding its tasks.md, validate.md and plan.md; no "TBW" at Stage 2.

## Artifact shapes (what `planning-artifacts.kdl` requires — write them this way the first time)
Every artifact opens with YAML frontmatter and carries these `##` sections; a missing one is a
write block (`gate-validate-artifact`), so do not improvise the headings.
- `sprintN/stories.md`: `type: stories`, `sprint: N` · `## Sprint goal` · `## Sprint demo` ·
`## Definition of Done` · `## Out of scope` · `## User stories` · `## Story dependency graph`
- `sprintN/plan.md`: `type: sprint-plan`, `sprint: N`, `stage: 1|2` · `## Sprint goal` ·
`## Critical path` · `## Cross-cutting gates`
- `sN-NN-<slug>/tasks.md`: `type: tasks`, `story: SN-NN` · one `## Tn — <title>` per task
- `sN-NN-<slug>/validate.md`: `type: validate`, `story: SN-NN` · `## Pre-flight` · `## Final sign-off`
- `sN-NN-<slug>/plan.md`: `type: plan`, `story: SN-NN`, `scope: tests only` · the planned tests

## Sprint containment (every sprint is a clean-context unit)
The supervisor CLEARS ITS CONTEXT between sprints. Nothing survives except what is on
disk. So each sprint you plan must be SELF-CONTAINED:
Expand Down
4 changes: 3 additions & 1 deletion plugin/bin/_gatelib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ trap '_gate_log_verdict; exit $?' EXIT
_gate_stuck() {
local rh="$1" self="$2" n="${AGENTIC_STUCK_BLOCKS:-3}" prior log="$AGENTIC_LOGS/gates.jsonl"
[ "$self" = false ] && [ -n "${TASK_ID:-}" ] && [ -n "${ATTEMPT:-}" ] || return 1
case "${AGENT_ROLE:-}" in red-worker|scaffolder|green-worker) ;; *) return 1 ;; esac
# 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
[ "$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 Down
4 changes: 2 additions & 2 deletions plugin/bin/gate-structural-integrity
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ else
# Fragile fallback: flag any top-level fn name defined in 2+ files. Not under tests/: each
# integration-test file is its own crate, so helpers there cannot collide.
dups="$(grep -rhoE '^[[:space:]]*(pub[[:space:]]+)?fn[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' \
--include='*.rs' --exclude-dir=target --exclude-dir=tests "$root" 2>/dev/null \
--include='*.rs' --exclude-dir=target --exclude-dir=tests --exclude-dir=.claude --exclude-dir=.agentic "$root" 2>/dev/null \
| sed -E 's/.*fn[[:space:]]+//' | sort | uniq -d)"
if [ -n "$dups" ]; then
echo "HIGH (grep): duplicate fn name(s): $(echo "$dups" | tr '\n' ' ')" >&2
fail "duplicate fn name(s) under grep fallback — install ctx-symbols to confirm"
fail "HIGH (grep fallback): duplicate fn name(s) — install ctx-symbols to confirm"
fi
fi

Expand Down
11 changes: 11 additions & 0 deletions plugin/pipeline/planning/02-planner/persona.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ until this persona's output is complete and Stage-2-valid.
- Keep artifacts consistent: every story has a dir `sprintN/sN-NN-<slug>/` (lowercase,
e.g. `s1-01-count-words/` for S1-01) holding its tasks.md, validate.md and plan.md; no "TBW" at Stage 2.

## Artifact shapes (what `planning-artifacts.kdl` requires — write them this way the first time)
Every artifact opens with YAML frontmatter and carries these `##` sections; a missing one is a
write block (`gate-validate-artifact`), so do not improvise the headings.
- `sprintN/stories.md`: `type: stories`, `sprint: N` · `## Sprint goal` · `## Sprint demo` ·
`## Definition of Done` · `## Out of scope` · `## User stories` · `## Story dependency graph`
- `sprintN/plan.md`: `type: sprint-plan`, `sprint: N`, `stage: 1|2` · `## Sprint goal` ·
`## Critical path` · `## Cross-cutting gates`
- `sN-NN-<slug>/tasks.md`: `type: tasks`, `story: SN-NN` · one `## Tn — <title>` per task
- `sN-NN-<slug>/validate.md`: `type: validate`, `story: SN-NN` · `## Pre-flight` · `## Final sign-off`
- `sN-NN-<slug>/plan.md`: `type: plan`, `story: SN-NN`, `scope: tests only` · the planned tests

## Sprint containment (every sprint is a clean-context unit)
The supervisor CLEARS ITS CONTEXT between sprints. Nothing survives except what is on
disk. So each sprint you plan must be SELF-CONTAINED:
Expand Down
41 changes: 40 additions & 1 deletion plugin/tools/ctx-symbols/src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ pub const DEFAULT_EXCLUDES: &[&str] = &[
"/dist/",
"/build/",
"/.next/",
// A host's worktree checkouts live INSIDE the main tree (Claude Code: .claude/worktrees/
// agent-<id>/), so a stale one duplicates every symbol of main (live e2e run 12: 20
// false HIGH findings). The plugin's own state and transcripts hold no source either.
"/.claude/worktrees/",
"/.agentic/",
"/.transcripts/",
];

const SUPPORTED_EXTS: &[&str] = &["rs", "py", "js", "ts", "tsx", "go"];
Expand All @@ -24,7 +30,11 @@ pub fn collect_source_files(root: &Path) -> Result<Vec<PathBuf>> {
for entry in WalkDir::new(root)
.follow_links(false)
.into_iter()
.filter_entry(|e| !is_excluded(e.path()))
.filter_entry(|e| {
// judge the path below the walk root: a root that itself sits inside an excluded
// fragment (a gate scanning .claude/worktrees/agent-<id>) must still be walked
!is_excluded(e.path().strip_prefix(root).unwrap_or_else(|_| e.path()))
})
{
let entry = entry?;
if !entry.file_type().is_file() {
Expand Down Expand Up @@ -68,4 +78,33 @@ mod tests {
assert!(!names.contains(&"b.txt".to_string()));
assert!(!names.contains(&"c.rs".to_string()), "target/ excluded");
}

#[test]
fn collect_skips_host_worktree_checkouts() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn count_words() {}").unwrap();
for d in [
".claude/worktrees/agent-a1b2/src",
".agentic/x",
".transcripts",
] {
std::fs::create_dir_all(root.join(d)).unwrap();
std::fs::write(root.join(d).join("lib.rs"), "pub fn count_words() {}").unwrap();
}
let files = collect_source_files(root).unwrap();
assert_eq!(files.len(), 1, "only the main tree's file: {:?}", files);
assert!(files[0].ends_with("src/lib.rs"));
}

#[test]
fn collect_walks_a_root_inside_an_excluded_fragment() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join(".claude/worktrees/agent-a1b2");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn count_words() {}").unwrap();
let files = collect_source_files(&root).unwrap();
assert_eq!(files.len(), 1, "{:?}", files);
}
}
29 changes: 29 additions & 0 deletions scripts/conformance/claude-code.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,35 @@ def stuck_wt(attempt):
r = subprocess.run([os.path.join(BIN, "budget"), "watch", "--interval", "1", "--grace", "1"], cwd=MAIN, env=ENV, capture_output=True, text=True)
results.append((9, "a reported stuck release is not reported again", "STUCK" not in r.stdout, r.stdout.strip()[:140]))

# §6.7 Structural review: a stale host worktree checkout inside the main tree (Claude Code keeps
# them under .claude/worktrees/agent-<id>/) duplicates every symbol of main; it is a workspace
# artifact, not a defect (live e2e run 12: 20 false HIGH halts). A real duplicate still halts.
wt = worktree("structural-reviewer")
report("structural-reviewer", [("structural review", "PASS")])
stale = os.path.join(MAIN, ".claude", "worktrees", "agent-a1b2c3", "src", "lib.rs")
write(stale, open(os.path.join(MAIN, "src", "lib.rs")).read())
case(7, "a stale worktree checkout under .claude/worktrees is not a duplicate definition", "gate-structural-integrity", wt, 0)
write(os.path.join(wt, "src", "dup.rs"), "pub fn sub(a: i32, b: i32) -> i32 {\n a - b\n}\n")
commit(wt, "dup")
case(7, "a real duplicate definition in the reviewed tree halts the chain (HIGH)", "gate-structural-integrity", wt, 2, "HIGH")
rc1 = gate("gate-structural-integrity", wt)[0]
rc, lines = gate("gate-structural-integrity", wt)
results.append((9, "a reviewer blocked 3x on the same HIGH finding is released unverified (it cannot fix code)",
rc1 == 2 and rc == 0 and any("STUCK" in l for l in lines), "exits %d then %d" % (rc1, rc)))
shutil.rmtree(os.path.join(MAIN, ".claude"), ignore_errors=True)
sh("git", "worktree", "remove", "--force", wt)
# ...and a reviewer whose OWN worktree is a host checkout under .claude/worktrees/ is still scanned:
# the exclusion applies below the walk root, never to the root itself.
hostwt = os.path.join(MAIN, ".claude", "worktrees", "agent-f00dbabe")
sh("git", "worktree", "add", "-q", "-b", "agentic/hostwt", hostwt)
write(os.path.join(hostwt, ".agentic", "task.env"),
"TASK_ID=S1-SR-W9\nATTEMPT=1\nAGENT_ROLE=structural-reviewer\nSTORY_DIR=%s\nBASE_REF=main\n" % STORY)
write(os.path.join(hostwt, "src", "dup.rs"), "pub fn sub(a: i32, b: i32) -> i32 {\n a - b\n}\n")
commit(hostwt, "dup in a host checkout")
case(7, "a real duplicate inside a reviewer's own .claude/worktrees checkout still halts (HIGH)", "gate-structural-integrity", hostwt, 2, "HIGH")
sh("git", "worktree", "remove", "--force", hostwt)
shutil.rmtree(os.path.join(MAIN, ".claude"), ignore_errors=True)

# FINAL: every test the story's plan.md names must exist on main. A RED re-plan that drops a
# planned test otherwise loses coverage silently (live e2e run 6), since plan-ready.md only
# lists what RED actually wrote.
Expand Down
Loading
0