Skip to content

Add /work-system:manager status slice and worker events (work-system 1.32.0) - #101

Merged
gering merged 2 commits into
mainfrom
task/add-manager-status-and-events
Nov 4, 2026
Merged

gering merged 2 commits into
mainfrom
task/add-manager-status-and-events

Conversation

@gering

@gering gering commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

First slice of /work-system:manager: the status part. Dispatch/collect stay in add-manager-watch-loop.

  • scripts/workers.sh / workers.py — list, screen, question, ready (all read-only) and a one-shot events.
    • Roles come from the shared classify_cwd (exported by the wrapper from herdr-agent.sh, like sessions.sh), so there is no second path classifier.
    • Flags: question (blocked dialog), asks (trailing question), verify (idle while the screen shows activity or could not be read; herdr #4376/#3090), draft (someone types).
    • screen clamps --lines to 20..200 (herdr #5462); question reads only the live dialog's options and never answers.
  • events --state <file>, deduplicated:
    • blocked: one event per pane + session UUID and transition; an /exit successor starts fresh; herdr's unknown status is no observation.
    • ready-for-merge: one event per lane + head. Rule, shared with workers.sh ready: open, non-draft, same-repo PR at the lane's HEAD; merge state CLEAN/HAS_HOOKS; no changes requested; at least one check and all green; mandate.sh covered. Tri-state: an unreadable answer never re-arms an event.
    • Stage 1 pings the Manager through manager.sh prompt, resolved in the worker's workspace. Stage 2 runs the user's on-blocked command (argv, JSON on stdin, either event) only when the ping did not land or after escalate_after; a failing command is retried on at most 3 runs.
    • The command comes only from a CLI flag or ~/.config/work-system/events.toml (regular, own, not group/world-writable), never from a repo. The main checkout's .work-system.toml gets [events] enabled / escalate_after; a lane's own copy is ignored.
    • flock against overlapping timer ticks; the state file is validated before any delivery.
    • No loop ships: herdr 0.9.5 has no status-change hook, so the README documents a crontab example.
  • manager.sh prompt never types into the caller's own pane (reason=manager-is-self).
  • Skill /work-system:manager; marks pane text, titles, recaps and payloads as data, never instructions. The Manager reports "ready for merge", never merges or approves.
  • work-system 1.32.0: CHANGELOG, READMEs, CLAUDE.md, schema (events.*), knowledge entry.

Decisions from the task (Robert, 2026-11-03)

Only this slice now; ready-for-merge from assess + PR/CI + covered until a run record exists; command only from user scope or a flag; events is one-shot for a user timer. Patch PR #100 (1.31.1) is separate — expect a CHANGELOG rebase if it lands first.

Test plan

  • test_workers.py: 123 checks with fake herdr/gh, real git and real mandate.sh; argv-log proof that nothing calls agent send-keys|prompt|send-text or pane send-*
  • test_manager.py (incl. the own-pane guard)
  • scripts/check-structure.py: 0 errors, 0 warnings
  • Live, read-only against herdr 0.9.5: list, list --all --json, screen <manager> --lines 5000 → lines=200, question <manager> → blocked=no
  • events live only with a throwaway tab and test worker (not run in the lane)

Review

Three local /swarm:review rounds. Round 1 ran with claude lenses only (both cc-harness voices failed with "no gateway token" despite ready); round 2 ran with native codex (gpt-6.1-sol) + grok-4.7, 3 families, 7 consensus findings. Agreed fixes applied and recorded (mandate.sh covered → via=agreed-fixes). Declined: deduping the /continue gate ping against the event ping, folding assess + gh pr view into one call (assess is the agreed source), and dropping last_line from the payload (the spec requires it; the README warns it may hold secrets).

Round 3 (/swarm:review --fix, codex gpt-6.1-sol + grok-4.7 + lenses, 25 findings, 3 consensus): fixed ready-for-merge for lanes with several panes (all must be idle), dialog options with description lines and with a prompt below an old list, the hook's captured pipe (output now discarded), re-firing the same head after a red re-run, pruning on an empty agent list, pinging forever on reasons that cannot change, events without tomllib (now exit 3), the task name typed raw into the Manager (slugged), asks=unknown on a failed read, plus doc gaps. 13 new tests (136). Declined: SKIPPED/NEUTRAL-only rollups (GitHub counts them as passing), the on_blocked name (from the spec), assess + gh again, a single combined screen read (recent ≠ visible), and unifying the sanitizers with sessions.py (no canonical one; separate task).

Round 4 (/swarm:review --fix, same voices, 13 findings, 3 consensus, 8 refuted): ready now applies the lane-wide idle rule too (reason=lane-busy), so the skill and events agree; a record is pruned only when its pane, session or lane was seen gone (a role that fell to other on a main-repo-path timeout, or one poll without a UUID, re-fired the event); a retried ping prints retry= instead of a new event=; the ping bound covers manager.sh's four herdr calls; the screen and dialog blocks blank bidi controls; herdr-agent.sh's JSON checks run python3 -I; one rev-parse per lane; doc drift (final ping reasons, re-arm wording, question exit codes, composer consumers). 5 new tests (141). Declined: assess + gh again (Robert's decision).

Round 5 (/swarm:review --fix, same voices, 15 findings, 5 consensus, 6 refuted; review budget now used up): events judges a lane busy over every workspace (a --workspace run missed a busy pane elsewhere) and counts any non-self agent in the lane or a sub-directory of it, as ready does; ready reads only that lane's screens (was O(workers²) per Manager pass); screen/dialog blocks blank C1 and unterminated escapes; the lock file is opened with O_NOFOLLOW; a bad HA_CALL_TIMEOUT_SECS falls back instead of a traceback; manager.sh's task= attribution is a slug; doc fixes (skill and README synopses, settings README for linked worktrees, knowledge frontmatter). 5 new tests (146). Declined: no-main-repo stays final (a deterministic failure), assess + gh (Robert's decision), a lazy screen read for delivered blocked events, and unifying the sanitizers (separate task).

Round 6 (/swarm:review --fix at Robert's request beyond the budget, over the branch including Oskar's checkpoint commit; 20 findings, 5 consensus, 4 refuted): a typed ping carries task= and worktree= only as slugs (a worktree name with spaces read as a sentence outside the bracket), while the SendMessage body keeps the real names (the round-5 slug had changed it too); ping_text no longer repeats the task name, so the slug rule lives in one place; light reads also cover other agents, so an idle-but-working agent in a lane sub-directory keeps the lane busy; a ready record survives while its agent sits in a sub-directory; a scoped run whose workspace emptied prunes its vanished panes (the empty-list guard is over every workspace); the skill calls ready once per lane; doc fixes (lane-busy wording, --escalate-after synopsis, ready_for_merge docstring, CHANGELOG). 4 new tests (150) plus one in test_manager. Declined: replaying a still-current event after re-enabling, failing closed on an unreadable events.enabled (ws-settings never blocks, by design), a hyphenated name inside the attribution bracket (residual), the own-pane guard without HERDR_PANE_ID (herdr always sets it), assess + gh (Robert's decision), scope-limited screen reads for events, and sharing the composer glue (separate task).

Review by Oskar; no merge without Robert's go.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RtoWD1aAxWazpn1eWEyVmq

@gering
gering force-pushed the task/add-manager-status-and-events branch 3 times, most recently from b91d58c to 174e796 Compare November 4, 2026 11:21
@gering
gering force-pushed the task/add-manager-status-and-events branch 2 times, most recently from 5801cf8 to 6c876fa Compare November 4, 2026 20:58
@oskar-portmann

oskar-portmann Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Review by Jonas Schmied (Dev bot), posted by Oskar because the jonas-schmied app is not installed on this repo. Reviewed head 6c876fa, verdict: approve. S1/S2 pushed as follow-up commit a4814f9.

Review of 6c876fa. Mainly workers.py (events), workers.sh, manager.sh, herdr-agent.sh, the tests, the schema and the version files.

Verdict: approve. No blockers.

Headless use (dev-1, no TTY)

  • No read from the terminal, no tput or colours, and no terminal-size assumptions.
  • External calls go through run() with timeouts, and the hook has a 30 s bound with stdin carrying JSON and stdout/stderr discarded.
  • An unreadable herdr exits 3/4/5 before the state is touched, and gh/GitHub blips give unknown, which never re-arms an event.
  • Concurrent runs are excluded by flock (summary: skipped=locked), and the lock is opened with O_NOFOLLOW.
  • The hook is argv-only (shlex.split / TOML list, no shell). The ping text is slugged, and the own-pane guard is in manager.sh:629.

Idempotence and dispatch

  • blocked: one event per pane+session transition (workers.py:918).
  • ready-for-merge: one event per lane+head (workers.py:950-952).
  • The hook runs only on an undelivered ping or after escalate_after (workers.py:797-802), with at most 3 tries.

Mutation probes (each one turns a test red)

  • new = True for blocked
  • hook_due ignoring a sent ping
  • ready-for-merge head dedupe removed
  • hook stdin payload dropped
  • events.enabled=false ignored
  • manager.sh own-pane guard removed

Tests and versions

  • test_workers.py: 146 checks pass. test_manager.py: pass.
  • Version 1.32.0 is in sync in plugin.json and marketplace.json, with a CHANGELOG entry as the repo's CLAUDE.md requires.
  • The schema events.enabled / escalate_after defaults match the code (!= "false", 0).
  • No .github/ changes, and no secrets.

CI on 6c876fa: scan pass, structure pass.

Should (non-blocking, will be addressed in a follow-up commit on this branch)

  • S1 workers.py:805-822 / :982-985: the state is saved only at the end of the run. If the run is killed mid-loop (timer timeout, reboot), already-delivered pings and hooks are sent again on the next run. Save atomically after each deliver() that sent something.
  • S2 workers.py:600-601 (and the matching README/docstring wording): "must not silence its own events" overstates it. A worker under the same user can still edit the main checkout's .work-system.toml, so ignoring the lane's copy is defense in depth, not a boundary.

Nit

  • N1 workers.sh:16: shellcheck SC1007 on CDPATH= cd. It is the same idiom as manager.sh:82 and intentional, so leaving it is fine.

Not checkable here: real herdr/dev-1 behaviour and a live --ping into real Manager panes; the tests use fake herdr/gh.

oskar-portmann Bot pushed a commit that referenced this pull request Nov 4, 2026
…rust note

Save the dedupe state atomically after every ping or on-blocked run, so a
run killed mid-loop does not re-send events it already delivered. Add a test
that kills a run inside the loop, then checks the delivered event is not re-sent.
Reword the README and workers.py note on the main checkout's
.work-system.toml: ignoring a lane's copy is defense in depth, not a boundary.

Refs #101
@oskar-portmann

oskar-portmann Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Delta review 6c876fa → a4814f9.

Verdict: approve. No blockers.

  • range-diff 6c876fa..f6d0a26 vs 6c876fa..a4814f9: 1: f6d0a26 = 1: a4814f9. The content is identical to the S1/S2 patch, and the tree diff f6d0a26 ↔ a4814f9 is empty. Only the committer differs; the author stays jonas-schmied[bot]. No .github/ changes, version unchanged at 1.32.0 (one CHANGELOG line under 1.32.0).
  • S1: checkpoint() saves the state atomically after each delivery that ran a ping or hook. The kill-mid-loop test passes. Mutation probe: with checkpoint disabled, exactly that test fails (FAIL: 1 of 147).
  • S2: wording only (README and workers.py): ignoring the lane's .work-system.toml is defense in depth, not a boundary.
  • Local results: test_workers.py 147 checks pass, test_manager.py pass, check-structure.py OK (0 errors, 0 warnings).

CI on a4814f9: scan pass, structure pass.

Disclosure: I authored the S1/S2 commit (applied by Oskar via git am), so this delta review covers my own change; the rest of #101 was reviewed on 6c876fa.

gering and others added 2 commits November 4, 2026 22:57
…1.32.0)

- workers.sh/workers.py: list, screen, question, ready (read-only) and a
  one-shot, deduplicated `events` for blocked workers and ready-for-merge lanes
- Reuse herdr-agent.sh preludes (classify_cwd, session name, composer check)
  through the wrapper; the only write path is manager.sh prompt
- One tri-state ready-for-merge rule (assess + gh + mandate covered) shared by
  the skill and events; unknown answers never re-arm a delivered event
- on-blocked command only from a CLI flag or the user-scope events.toml;
  the main checkout's [events] enabled / escalate_after, never a lane's copy
- manager.sh prompt never types into the caller's own pane
- test_workers.py with fake herdr/gh and an argv proof that no pane is written
- Skill, READMEs, CHANGELOG, CLAUDE.md, knowledge entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtoWD1aAxWazpn1eWEyVmq
…rust note

Save the dedupe state atomically after every ping or on-blocked run, so a
run killed mid-loop does not re-send events it already delivered. Add a test
that kills a run inside the loop, then checks the delivered event is not re-sent.
Reword the README and workers.py note on the main checkout's
.work-system.toml: ignoring a lane's copy is defense in depth, not a boundary.

Refs #101
@gering
gering force-pushed the task/add-manager-status-and-events branch from a4814f9 to 6191137 Compare November 4, 2026 22:06
@gering
gering merged commit b641f3c into main Nov 4, 2026
2 checks passed
@gering
gering deleted the task/add-manager-status-and-events branch November 4, 2026 22:45
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