Skip to content

feat(doing): let an agent advertise what it is working on - #109

Closed
joy13975 wants to merge 5 commits into
aannoo:mainfrom
joy13975:feat/doing
Closed

feat(doing): let an agent advertise what it is working on#109
joy13975 wants to merge 5 commits into
aannoo:mainfrom
joy13975:feat/doing

Conversation

@joy13975

@joy13975 joy13975 commented Aug 17, 2026

Copy link
Copy Markdown

Why

hcom tells you whether an agent is awake, not what it is doing. status is a fixed lifecycle enum (active/listening/blocked/inactive/launching/error), and there is no field an agent can deliberately set to describe its current task. A peer deciding whether to interrupt, hand off, or wait has to open a transcript to find out.

This adds one verb so an agent can say it, and shows it everywhere the roster is already displayed.

hcom doing "refactoring the auth layer"   # set
hcom doing                                # show
hcom doing ""                             # clear

Peers read it in hcom list (inline, truncated to 50 chars), hcom list -v, hcom list <name>, --json, --format '{doing}', the TUI agent detail pane, and hcom events --type doing.

Why an event and not an instances column

  • status_context / status_detail are rewritten by hooks on every tool-use tick, and set_gate_status explicitly overwrites status_detail unless it equals the one "cmd:listen" sentinel - so agent-authored text there does not survive. hints is reinjected into the owning agent's own message feedback and never appears in list output. notes is baked into the launch prompt once and is not a live field.
  • No schema change: no SCHEMA_VERSION bump, no MIGRATIONS entry, nothing for existing databases to migrate.
  • hcom list --stopped already derives current state from life events, so latest-event-wins is an established pattern here rather than a new one.
  • Being an event makes it filterable and subscribable through the machinery that already exists.

A column would also have meant touching the positional-index instance_row_to_json, roughly 20 InstanceRow/Agent struct literals in test fixtures, and the TUI's separate hand-written query - about 30 sites for the same user-visible behavior.

The second commit is part of that reasoning rather than an afterthought: --type is a closed clap enum, so hcom events --type doing was rejected with invalid value 'doing' even though the rows were in the table. Adding the type to EventFilterArgs makes the filter, listen, and events sub all reach it, and there is a parse test so the event type and the filter enum cannot drift apart again. I found that by running the real bus, not by reading the diff.

Naming

doing, not intent: hcom send --intent request|inform|ack already means something different, and two unrelated senses of "intent" one space apart on the CLI would be a trap.

Notes

  • An unregistered --name is rejected rather than recorded. Activity filed under a name no listing can resolve would be a silent no-op.
  • The listing uses one grouped query for the whole roster rather than a lookup per agent. doing_map_from_conn takes a raw Connection so the CLI and the TUI snapshot loader share one definition of that query instead of two copies of the SQL.
  • Both bootstrap primers (UNIVERSAL and SUBAGENT_BOOTSTRAP) mention the verb, and it is added to SAFE_HCOM_COMMANDS so agents are not prompted for approval on first use.

Testing

Unit tests in src/db/events.rs: round-trip, latest-wins including the empty-string clear, and that the per-instance map ignores other event types. Parse test in src/commands/events.rs for --type doing.

CLI smoke test in tests/cli_smoke.rs covering the full path: nothing-set message, set, read back, visible in list --json and --format, latest write wins, clear, and a second test asserting an unregistered name fails.

cargo test --test cli_smoke
test result: ok. 25 passed; 0 failed

End to end on a real two-harness bus

Not a CLI-only check: I launched a real headless Claude Code agent and a real headless Codex agent onto one bus at the same time, and had them talk.

$ hcom list
[CLAUDE]  * e2e-nene [headless]  now: listening - done - read peer transcript
[CODEX]   * e2e-kobe [headless]  now: listening

$ hcom list --format '{name} [{tool}] doing={doing}'
e2e-nene [claude] doing=done - read peer transcript
e2e-kobe [codex] doing=codex side - set via absolute path

$ hcom events --type doing --last 10
  nene -> claude side of cross-harness test
  nene -> done - read peer transcript
  kobe -> codex side - set via absolute path

Both agents set their own doing through the CLI, and each read the other's conversation via hcom transcript. The messages went both ways (intent=request from the claude agent, intent=ack back from the codex agent). Against the released 0.7.25 binary the same events --type doing query fails with invalid value 'doing', which is the before-state for the second commit.

Also verified in the TUI rather than by inspection - the agent detail pane renders:

  o hera - adhoc - 4s - doing                                    created 5m
  ~/code/mew
  doing: verifying the TUI renders this

cargo fmt --check and cargo clippy -D warnings are clean via just ci.

Note on the local suite

cargo test on my machine has 17 pre-existing failures in hooks::gemini::tests (all asserting setup_gemini_hooks(false)). They fail identically on unmodified main at 79ebde1 - I diffed the failing node-id sets in both directions and they are the same set, so they are unrelated to this change. They look environment-specific, and CI here is green. Everything else passes: 2161 passed, and the full cli_smoke suite is green.

joy13975 and others added 5 commits August 17, 2026 22:34
hcom tells you whether an agent is awake, not what it is doing. `status`
is a fixed lifecycle enum (active/listening/blocked/inactive/launching/
error) and there is no field an agent can deliberately set to describe
its current task. So a peer deciding whether to interrupt, hand off, or
wait has to read a transcript to find out.

  hcom doing "refactoring the auth layer"   # set
  hcom doing                                # show
  hcom doing ""                             # clear

Peers see it in `hcom list` (inline, truncated), `hcom list -v`,
`hcom list <name>`, `--json`, `--format '{doing}'`, and the TUI agent
detail pane.

Stored as a `doing` event rather than a new instances column:

- status_context/status_detail are rewritten by hooks on every tool-use
  tick, and set_gate_status explicitly overwrites status_detail unless it
  equals the "cmd:listen" sentinel, so agent-authored text there does not
  survive. hints is reinjected into the owning agent's own feedback and is
  not in list output; notes is baked into the launch prompt once.
- no schema change, so no SCHEMA_VERSION bump and no migration for
  existing databases.
- `hcom list --stopped` already derives current state from `life` events,
  so latest-event-wins is an established pattern here.
- it composes with the existing event filters, so `hcom events --type
  doing` and subscriptions work with no extra code.

Named `doing` and not `intent` because `hcom send --intent
request|inform|ack` already means something different.

An unregistered --name is rejected rather than recorded: activity filed
under a name no listing can resolve would be a silent no-op.

The listing reads it with one grouped query for the whole roster instead
of one lookup per agent. doing_map_from_conn takes a raw Connection so the
CLI and the TUI snapshot loader share a single definition of that query.

Tests: db round-trip, latest-wins including clear, per-instance map
isolation from other event types, plus CLI smoke coverage of set/show/
list --json/--format/update/clear and the unregistered-name rejection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--type` is a closed clap enum, so `hcom events --type doing` was rejected
with "invalid value 'doing'" even though the rows were in the events
table. Being filterable is most of the reason to store this as an event,
so add it to the enum shared by events, listen, and events sub, and to the
two places that spell the valid types out for humans and agents.

Found by running the real bus rather than reading the diff: the query
returned nothing while sqlite showed three doing rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- validate `doing` free text at the write boundary (cmd_doing) via a helper
  shared with validate_message: reject C0/C1 control chars incl. ESC 0x1B and
  cap size at MAX_MESSAGE_SIZE, additionally rejecting tabs/newlines since doing
  renders on one roster line; empty stays allowed as the clear
- extract validate_text_field(text, allow_line_whitespace) as the single
  write-boundary guard so every sink (roster, -v, list <name>, TUI, relay
  replication) is safe by construction, closing the unvalidated-free-text class
- regression tests: CLI smoke rejects escape/newline injection; unit tests pin
  the single-line vs multi-line control-char + size invariant

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- validate_text_field: reject DEL (0x7F) and bidi controls (U+202A-202E, U+2066-2069) in both modes, closing the display-control-chars class at the write boundary; ZWJ and other Cf chars stay allowed
- size-cap error now reads "bytes" (the check counts UTF-8 bytes)
- inline-roster newline/tab spoofing closed by the same write boundary (no consumer patch)
- list.rs: move misattributed doc comment onto get_unread_count
- filters.rs: reference DOING_EVENT_TYPE const at the --type gate (re-exported from db) so the accept-gate cannot drift from the write/read side; parse test now asserts against the const
- doing_map_from_conn: log_error on prepare/query failure instead of silently returning an empty map
- start_rebind: reset doing to blank on identity reclaim so a takeover does not inherit the prior occupant's stale activity text

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The round-2 fix commit left two hunks unformatted, which turned CI red on
`cargo fmt --all -- --check` in both rust-tests and windows-build. No behavior
change; `cargo fmt --all -- --check` and `cargo clippy --all-targets -- -D warnings`
are now clean and the cli_smoke suite still passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@joy13975 joy13975 closed this Aug 17, 2026
@joy13975
joy13975 deleted the feat/doing branch August 17, 2026 16:39
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