Bug Summary
When af plan --spec <name> is used to create a filtered plan, the hot-loader at sync barriers discovers and injects unrelated specs into the running graph — including specs that were completed long ago. This causes af code to create worktrees, dispatch sessions, and burn tokens on work that was never requested.
Reproduction
- Have multiple spec directories on disk, some already completed (e.g.
01_nightshift_afspec_models, 06_duckdb_reader_writer_split)
- Run
af plan --spec 09_worktree_path_collision — DB correctly contains only spec 09
- Run
af code
- After the first sync barrier fires (default: every 5 completed tasks), the hot-loader scans the full specs directory and injects specs 01 and 06 into the live graph
- Worktrees, branches, and sessions are created for those unrelated specs
Root Cause
The bug is a logic interaction between save_plan's full-table wipe and the hot-loader's Gate 4 ("already complete") check.
hot_load.py:294:
if are_all_tasks_done(spec_path) and _are_all_plan_nodes_done(spec.name, db_conn):
Gate 4 requires both conditions:
are_all_tasks_done() — checks tasks.json checkboxes
_are_all_plan_nodes_done() — checks plan_nodes table in DuckDB
But save_plan() (persistence.py:86-88) does DELETE FROM plan_nodes before inserting the filtered plan. So after af plan --spec 09, specs 01 and 06 have zero rows in plan_nodes. _are_all_plan_nodes_done() returns False when total == 0 (line 220: total > 0 and total == done), causing Gate 4 to fail open — treating "not in this plan" as "not done yet."
hot_load.py:306-329 (discover_new_specs):
all_specs = discover_specs(specs_dir) # no filter_spec passed
The hot-loader scans the entire specs directory with no awareness of the filtered_spec metadata stored in plan_meta.
Impact
- Wastes API tokens on sessions for already-completed specs
- Creates stale worktrees and branches that collide with each other (multiple nodes sharing group 0 worktree paths)
- Cascade of workspace setup failures (git worktree collisions) that trigger retry loops
- Undermines the purpose of
--spec filtering entirely
Observed Log Evidence
[INFO] agentfox.engine.barrier: Sync barrier 1 triggered at 5 completed tasks
[INFO] agentfox.engine.hot_load: Hot-loaded 2 new spec(s): 01_nightshift_afspec_models, 06_duckdb_reader_writer_split
[INFO] agentfox.graph.injection: Injected reviewer node '01_nightshift_afspec_models:0:reviewer:pre-review' at runtime
[INFO] agentfox.engine.graph_sync: State transition: node=01_nightshift_afspec_models:0:reviewer:drift-review from=pending to=in_progress reason=dispatched
[INFO] agentfox.engine.graph_sync: State transition: node=06_duckdb_reader_writer_split:0:reviewer:drift-review from=pending to=in_progress reason=dispatched
Suggested Fix
Two complementary changes:
-
_are_all_plan_nodes_done() should return True (not False) when total == 0 and the plan was filtered. Alternatively, Gate 4 should treat "no plan nodes AND plan has filtered_spec set AND this spec is not the filtered spec" as "skip."
-
discover_new_specs_gated() should respect filtered_spec from plan_meta. If the persisted plan has a filtered_spec, the hot-loader should only consider that spec as a candidate — or at minimum exclude specs not matching the filter.
Affected Files
packages/agentfox/agentfox/engine/hot_load.py — discover_new_specs_gated(), _are_all_plan_nodes_done()
packages/agentfox/agentfox/graph/persistence.py — plan_meta.filtered_spec (already stored, just not consulted by hot-loader)
Bug Summary
When
af plan --spec <name>is used to create a filtered plan, the hot-loader at sync barriers discovers and injects unrelated specs into the running graph — including specs that were completed long ago. This causesaf codeto create worktrees, dispatch sessions, and burn tokens on work that was never requested.Reproduction
01_nightshift_afspec_models,06_duckdb_reader_writer_split)af plan --spec 09_worktree_path_collision— DB correctly contains only spec 09af codeRoot Cause
The bug is a logic interaction between
save_plan's full-table wipe and the hot-loader's Gate 4 ("already complete") check.hot_load.py:294:Gate 4 requires both conditions:
are_all_tasks_done()— checkstasks.jsoncheckboxes_are_all_plan_nodes_done()— checksplan_nodestable in DuckDBBut
save_plan()(persistence.py:86-88) doesDELETE FROM plan_nodesbefore inserting the filtered plan. So afteraf plan --spec 09, specs 01 and 06 have zero rows inplan_nodes._are_all_plan_nodes_done()returnsFalsewhentotal == 0(line 220:total > 0 and total == done), causing Gate 4 to fail open — treating "not in this plan" as "not done yet."hot_load.py:306-329(discover_new_specs):The hot-loader scans the entire specs directory with no awareness of the
filtered_specmetadata stored inplan_meta.Impact
--specfiltering entirelyObserved Log Evidence
Suggested Fix
Two complementary changes:
_are_all_plan_nodes_done()should returnTrue(notFalse) whentotal == 0and the plan was filtered. Alternatively, Gate 4 should treat "no plan nodes AND plan hasfiltered_specset AND this spec is not the filtered spec" as "skip."discover_new_specs_gated()should respectfiltered_specfromplan_meta. If the persisted plan has afiltered_spec, the hot-loader should only consider that spec as a candidate — or at minimum exclude specs not matching the filter.Affected Files
packages/agentfox/agentfox/engine/hot_load.py—discover_new_specs_gated(),_are_all_plan_nodes_done()packages/agentfox/agentfox/graph/persistence.py—plan_meta.filtered_spec(already stored, just not consulted by hot-loader)