Skip to content

fix(loop): parallel hand-off + serial scope filter + summary in-flight + .hew.toml config (hew-zt4z, hew-s9mb, hew-cn2y, hew-c0pa)#62

Merged
droidnoob merged 12 commits into
mainfrom
chore/loop-fixes-and-config
May 30, 2026
Merged

fix(loop): parallel hand-off + serial scope filter + summary in-flight + .hew.toml config (hew-zt4z, hew-s9mb, hew-cn2y, hew-c0pa)#62
droidnoob merged 12 commits into
mainfrom
chore/loop-fixes-and-config

Conversation

@droidnoob

@droidnoob droidnoob commented May 30, 2026

Copy link
Copy Markdown
Owner

Combines four follow-up branches from the autonomous loop run that closed hew-c0pa (project-local config epic) — three loop fixes plus the feature itself. Smoke-tested end-to-end before push.

Closes

  • hew-zt4z P1 bug — parallel --jobs>=2 stranded tasks (dispatcher claimed, workers re-polled bd.ready() and saw empty)
  • hew-s9mb P2 bug — serial --jobs=1 --scope=epics bypassed the scope filter at the worker's bd.ready() poll
  • hew-cn2y P3 chore — hew loop summary errored with raw No such file when the run was in flight
  • hew-c0pa P2 epic — project-local .hew.toml config file (+ 6 children)

Commits

commit what
970f3dd feat(config): discover_project_root + discover_project_config — pure fs walks for .beads/ then .git/; resolves worktrees to real repo root
be906ad feat(config): load_layered + per-struct merge (hew-36of) — scalars: project wins; arrays: cargo-style concat-dedupe; tables: recurse
9f7b0c0 feat(init): emit starter .hew.toml after install (hew-3r8v) — embeds templates/hew.toml.starter; skips if a file already exists
16ee6e9 feat(config): config set --global/--project write-target (hew-k2gm) — refuses silent fallback when .hew.toml exists
397c5cd feat(config): hew config show with per-key source provenance (hew-leh9)
69cd50e docs(config): docs/CONFIG.md + CHANGELOG + CLAUDE cross-ref (hew-u181)
f192e44 fix(loop): render in-flight summary instead of ENOENT when run.json absent (hew-cn2y)
eae322b fix(loop): apply Scope filter on serial path's bd.ready() poll (hew-s9mb)
2221c27 fix(loop): thread dispatcher pre-claim to worker so jobs>=2 actually runs (hew-zt4z)

Plus three merge commits stitching the four branches together. One textual conflict at hew/src/commands/loop_cmd.rs:1500-ish between the scope-filter and parallel-handoff branches — resolved by composing both (assign-then-filter, so the pre-claimed task still passes through the scope check for safety).

Smoke proof — both fixes verified end-to-end

Set up 4 ready tasks: hew-togn (smoke epic), hew-62c2 (P2 in epic), hew-44ge + hew-ybq3 (P1 standalones).

Test 1 — serial scope filter (hew-s9mb):

$ hew loop run --scope=epics --epics=hew-togn --dry-run --max-iter 1
iter 1 — task=hew-62c2 prefix_hash=b5521b8b0e7ace68
scope:     epics [hew-togn]

✓ Picked hew-62c2 (in-epic P2). The two higher-priority P1 standalones were never considered. Pre-fix this would have grabbed a P1.

Test 2 — parallel hand-off (hew-zt4z), real end-to-end:

Set up a 2-task epic with trivial "create a file, commit, close" tasks. Kicked the real loop (no --dry-run, real claude -p spawns, real worktrees at ~/.hew/wt/<run-id>/{0,1}/, real merge-back).

$ hew loop run --jobs=2 --scope=epics --epics=hew-omh2 --max-iter 2
(1m 50s wall, 3 iters)

per-worker:
  wkr   iters  closed runtime       tokens stop
  0         2       2 claude       936,962 max_iter
  1         1       1 claude       185,554 ready_empty
  all       3       3            1,122,516

outcomes:  3 closed
scope:     epics [hew-omh2]
planner:   agent=0, runtime=2, fallback=0
cache:     iter 2-3 hit (prefix_hash stable from iter 1)

End-state verification:

  • All 3 tasks (epic + 2 children) actually closed in bd, not stranded in_progress.
  • Both worker branches merged back to main: hew-core/SMOKE_A.txt and hew-core/SMOKE_B.txt both present in tree, each from a different worker's commit.
  • manifest.json shows iter_count: 2 and iter_count: 1 for the two workers (pre-fix this was 0/0 with ready_empty).
  • The batch planner ran twice between iters as designed (agent=0, runtime=2, fallback=0).
  • hew loop summary mid-run rendered the in-flight view correctly (the hew-cn2y fix in action).

(Smoke commits + files cleaned out of this branch; only the fix code is in the PR.)

What the smoke fixed for the next time

Previous smoke (hew-mu5j in PR #58) tested only argv-parsing: --scope=bogus rejected, missing --scope errors with MissingFlag, etc. It never asserted "a scoped run claims only descendants" or "--jobs N actually drives N workers." That's why hew-zt4z and hew-s9mb shipped: those tests didn't exist.

This PR adds real-bd-graph smoke as a discipline, and the bug bodies + the test additions called out in hew-zt4z / hew-s9mb carry that forward as required test additions for any future loop change.

Gates

  • cargo fmt --all --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --workspace ✓ (43 suites, 0 failed)
  • Manual end-to-end smoke ✓ (above)
  • bd-PATH parity (CI doesn't install bd; precheck guard from PR feat(loop): hew loop run --scope={ready|epics} flag (hew-b3yl) #58 still holds) ✓ implicitly via loop_scope_e2e 7/7

Net change

16 files changed, 2963 insertions(+), 11 deletions(-)

Largest contributor is hew-core/src/config.rs (+951) for the project-local config loader; otherwise it's well-scoped diffs across the loop + tests.

🤖 Generated with Claude Code

Smoke proof — .hew.toml feature

Verified after the loop-fix smoke (separate tempdir, fresh hew init):

$ hew init --non-interactive --runtime=claude
$ ls .hew.toml
.hew.toml          # ✓ starter emitted

$ head -7 .hew.toml
# hew project config — https://hew.sh/docs/config
# ...
version = 1
# [loop]
...

$ hew config set loop.model.default opus-4-7
Error: refusing to write to user-global config when `.hew.toml` exists ...
       Use one of:
         hew config set --project loop.model.default opus-4-7   # commit-shared
         hew config set --global  loop.model.default opus-4-7   # personal override
                                                                 # ✓ loud refusal

$ hew config set --project loop.model.default claude-opus-4-7
set loop.model.default = claude-opus-4-7 (...../.hew.toml)        # ✓ writes to project

$ hew config get loop.model.default
claude-opus-4-7                                                    # ✓ read-back

$ hew config show | grep loop.model.default
  loop.model.default = claude-opus-4-7              (project)      # ✓ provenance

All five concerns from the locked plan verified: starter emission, --project writes, refusal-when-project-exists-without-flag, get round-trip, show-with-source-attribution.

droidnoob added 12 commits May 30, 2026 19:50
…runs

The parallel loop's atomic claim flipped each ready task to in_progress
before any worker ran, which removed it from bd.ready(). The worker
then re-polled bd.ready() for its initial task, saw an empty set, and
exited iter 0 with `stop_reason: ready_empty` — stranding every claim
and producing 0 iters per worker.

Worker now carries `assigned_task: Option<ReadyTask>` populated from
the dispatcher's `DispatchTick::assignments`. Iter 1 prepends the
pre-claim to the worker's bd.ready() view so the existing task pick
+ signal eval paths stay unchanged; iter 2+ source from bd.ready()
like the serial path.

Closes hew-zt4z.
- discover_project_root walks cwd ancestors; .beads wins over .git per level
- worktree gitlink resolves via git rev-parse --git-common-dir (--show-toplevel returns the worktree dir, not the main repo)
- discover_project_config prefers .hew.toml, falls back to hew.toml, warns when both exist
- 9 tests including a real-worktree e2e via tempdir

Closes hew-ja44
- New load_layered(user, project) reads each file (missing = empty) and
  merges per documented rules: scalars project-wins, Option<T> uses
  Option::or, Vec<T> concats with order-preserving dedupe, nested
  structs recurse, BTreeMap extends with project-wins on collision.
- Config::load() now resolves the XDG user path + walks cwd for a
  project root, then calls load_layered. HEW_CONFIG bypasses layering.
- 7 new tests in hew-core/src/config.rs cover the merge axes plus the
  HEW_CONFIG bypass path.
…9mb)

`run_worker_loop_with_scope` polled `bd.ready()` and grabbed the first
task without consulting `cfg.scope`, so `hew loop run --scope=epics
--epics=<id>` on the serial (`--jobs=1`) path would claim any unrelated
ready bug. The parallel `Dispatcher::dispatch_tick` already filtered
correctly; the surgical fix mirrors its `resolve_descendants` +
`Scope::includes` call at the serial path's candidate boundary.

- 3 new regression tests in hew/tests/loop_scope_serial.rs covering the
  reproducer, the empty-after-filter ReadyEmpty stop, and the no-regression
  `Scope::Ready` baseline.
- Existing loop_scope_e2e 7/7 still green (argv-contract layer unchanged).
- New template hew/templates/hew.toml.starter (version=1 + commented
  loop/model/planner/end_of_run/compact sections + docs link).
- emit_starter_dot_hew_toml helper in init.rs: Fresh/Refresh skip when
  .hew.toml or hew.toml already exists; Reconfigure prompts before
  overwriting (default no, only when interactive). Best-effort; never
  aborts init on write failure.
- 10 unit tests cover template content + per-mode helper state via an
  injected regen picker.
- 3 e2e tests cover fresh emit, dotfile preservation, and plain
  hew.toml blocking creation of .hew.toml.
- Add --global / --project flags on `hew config set` (clap conflicts_with).
- Resolve write target across 5 branches:
  1. both flags → clap rejects upstream
  2. --global → user-global (~/.config/hew/config.toml)
  3. --project → existing project file or <root>/.hew.toml (created
     with `# hew project config` + `version = 1` header on first write)
  4. neither + project file present → refuse with dual-option error
     listing both `--project <key> <value>` and `--global <key> <value>`
  5. neither + no project file → user-global (back-compat)
- New `hew_core::config::save_project_to(path, cfg)` prepends starter
  header on create; subsequent writes overwrite without re-adding it.
- 7 new e2e tests cover all 5 resolution branches + refusal message
  shape + --help surface; 3 new core tests cover save_project_to.
- New `hew config show` subcommand renders a sources header in
  precedence order (user-global → project → env) plus the effective
  config with `(source)` attribution on every key.
- `hew_core::config::LoadedConfig` carries the merged Config plus a
  BTreeMap<String, ConfigSource> and the contributing file paths.
- `ConfigSource` ∈ { UserGlobal, Project, Env, Default, Merged }.
  `Merged` is reserved for keys whose values are unioned across both
  files (compact.exempt, loop.model.by_priority, loop.model.by_type).
- Attribution walks the raw on-disk toml::Table for each side so a
  user value that happens to equal the default still attributes to
  user-global, not Default.
- New `HEW_USER_CONFIG` env var overrides the XDG user-global path
  without bypassing layered project discovery. `HEW_CONFIG` keeps its
  single-file bypass semantics; tests for the layered path use
  `HEW_USER_CONFIG` to stay off the host's real config.
- 6 new e2e tests cover sources-in-precedence, scalar/array/default
  attribution, the env single-source rendering, and the stable
  --json shape. 5 new core tests cover the attribution primitives.
- New docs/CONFIG.md walks the layered config story for operators:
  two-file overview, discovery order, where-each-setting-belongs
  table, hew config set write-rules table, hew config show sample +
  --json shape, merge semantics with worked example, migration recipe,
  and explicit non-goals (.hew.local.toml, ancestor walk, schema
  migration).
- CHANGELOG.md [Unreleased] entry summarizes the hew-c0pa epic.
- CLAUDE.md "Where to look next" links docs/CONFIG.md.
- 3 grep-based smoke tests pin the precedence table, merge-semantics
  section, and CHANGELOG entry against regression.
…bsent (hew-cn2y)

`hew loop summary` against a freshly-started run surfaced
`read run.json: No such file or directory` because run.json is only
written after iter 1 completes. Classify the run-dir state up front
(empty-start / parallel-in-flight / iter-logs-only / completed) and
render a degraded "iter N in flight" view for every pre-completed
case. The genuinely-missing run-id case still errors cleanly.

- hew_core::loop_log::RunDirState + inspect_run_dir + run_dir_started_at
- hew_core::loop_summary::InFlight + render_in_flight
- 6 + 4 unit tests, 4 e2e tests (loop_summary_in_flight_e2e.rs)
…-fixes-and-config

# Conflicts:
#	hew/src/commands/loop_cmd.rs
@droidnoob droidnoob merged commit 9bea5c3 into main May 30, 2026
14 checks passed
@droidnoob droidnoob mentioned this pull request May 30, 2026
droidnoob added a commit that referenced this pull request May 30, 2026
- workspace Cargo.toml: 0.11.0 -> 0.12.0
- 23 skill body hew:version= markers bumped to match
- .claude/ install snapshot refreshed via hew init --runtime=claude
- CHANGELOG.md: [Unreleased] content moved to [0.12.0] — 2026-05-31
- CHANGELOG.md: added Fixed entries for hew-zt4z / hew-s9mb / hew-cn2y
- hew/tests/config_e2e.rs: changelog_unreleased_has_project_local_config_entry
  retargeted at the [0.12.0] section (now that the entry has been released)

Release contents since 0.11.0:
- #62 parallel loop hand-off fix + serial scope filter fix + loop summary
       in-flight fix + .hew.toml project-local config (hew-zt4z, hew-s9mb,
       hew-cn2y, hew-c0pa)

No code changes in this commit beyond version bumps + CHANGELOG promotion
+ test target update for the moved CHANGELOG entry. CI will verify.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant