Skip to content

Let panels in a group read and type into each other when the operator switches it on - #146

Merged
cmj0121 merged 11 commits into
mainfrom
feat/group-peer-interaction
Oct 13, 2026
Merged

cmj0121 merged 11 commits into
mainfrom
feat/group-peer-interaction

Conversation

@cmj0121

@cmj0121 cmj0121 commented Oct 13, 2026

Copy link
Copy Markdown
Owner

What

A per-group peers flag. While the operator has it on for a group, the panels in that exact group may list each other, read each other's recent output, and type one line into each other's terminal.

  • Server: group.peers sets the flag; peer.list, peer.tail and peer.send are authorised per call from live state. The flag is persisted, carried on the snapshot as GroupView.Peers, and dropped when the group's last direct member leaves.
  • Worker MCP: baton_peers, baton_peek and baton_tell beside score_submit. The conductor's tool table is unchanged.
  • CLI: baton ctl group peers <group> on|off.
  • TUI: g p on a group row. Enabling asks once and only y confirms; disabling does not ask. The mark is ⋈ on the tree row, card head, split title, member tile and zoom footer.
  • Docs: CONTROL, SPEC, KEYS, PLUGIN, SCORE and their zh-TW twins.

Rules a send follows

  • Never self, the conductor, the global shell, an exited panel, or a panel in another group or a sub-group.
  • An agent target gets one line prefixed [from <title> (<id>)]. Line breaks and tabs fold to a space; control and invisible characters are refused. A shell target gets the text as written.
  • Every send ends in Enter. 4096-byte cap. Three in a row per sender, then one every two seconds.
  • Refused while the target is waiting for a human.

Who may change what

  • A conductor connection and an agent panel are refused group.peers.
  • A conductor or an agent panel cannot file an existing panel into a peers-on group (panel.group, a merging rename, a dissolve). Spawning a new panel there is still allowed. A conductor that creates a panel and then groups it therefore cannot add it to a peers-on group; the operator does.
  • The flag follows a pure rename. A merge never switches a group on.

Known limits, all documented in CONTROL.md

  • The caller is the panel id a connection declares, not a proven one. An agent that unsets BATON_PANEL_ID is taken for the operator and can switch its own group on. This is a guardrail against accidents, like the conductor fence.
  • A peer can type into a group-mate that runs with looser permissions.
  • A group-mate's output can carry secrets into another agent's context.
  • The waiting-for-a-human refusal is best-effort; the state is set on a monitor tick.
  • The rate cap bounds each sender, not an A→B→A loop.
  • The worker's MCP process must inherit BATON_PANEL_ID from the agent CLI. Checked with a scripted baton mcp, not with each vendor's CLI.

Also in this branch

chore(cgroup): one //nolint:staticcheck on the non-Linux detectRoot stub. Off Linux the function always errors, so staticcheck reported SA4023 and the pre-commit lint failed on macOS.

Verification

  • make cover (race + per-package 80%) exits 0 locally; golangci-lint run exits 0 for darwin and GOOS=linux.
  • End to end on a private socket and home directory with the real binary: refusals with the flag off, the operator switching it on, an agent refused the switch, a prefixed line reaching an agent, a command running in a shell, the out-of-group, self, control-byte, no-identity, attention and rate refusals, and the dashboard key (Enter and a late y do not enable; y does; a second press disables). No recovered panic in the daemon log.

Closes #145

    Off Linux detectRoot always returns an error, so staticcheck reports
    the err check in Probe as always true and the pre-commit lint fails
    on macOS. The check is real on Linux.
    group.peers sets it and the snapshot carries it as GroupView.Peers. It
    is persisted with the other per-group settings and is off for a new
    group.

    A conductor connection and an agent panel are refused the verb. A
    group needs a direct member to be switched on, and the flag is dropped
    when its last member leaves. It follows a pure rename. A merge into a
    group that already had members leaves that group's flag as it was.

    Refs #145
    Covers save and restore, the conductor and agent-panel refusals, the
    group.change event, and each path that empties a group. A merge and a
    re-created group name both come up off.

    Refs #145
…ach other

    peer.list, peer.tail and peer.send are authorised per call: the
    caller and the target must share a group the operator switched on.
    Never self, the conductor, the global shell or an exited panel.

    An agent target gets one line prefixed with who sent it. Control
    bytes are refused and line breaks fold to a space. A shell target
    gets the text as typed. A send to a panel waiting for a human is
    refused. Each send is capped at 4096 bytes and each sender at three
    in a row, then one every two seconds.

    Refs #145
    Covers each caller and target refusal, the prefix on an agent and
    the verbatim shell, control bytes and folded line breaks, the size
    and rate caps, the flag turned off mid-conversation, and one run
    over the socket against real PTYs.

    Refs #145
    g p on a group row flips whether its panels may read and type
    into each other. Switching it on asks first and only y confirms.
    Enter cancels, like any other key. Switching it off goes at once.

    A group with no panel directly in it is refused with a reason,
    since the server would refuse it anyway.

    The question lasts as long as its prompt is on screen. It is
    withdrawn when the prompt fades or is replaced, when the group
    loses its last direct panel, and when the view leaves the
    dashboard. A prompt still showing is taken down with it.

    The flag is drawn before the server answers and the next snapshot
    settles it. A refused switch shows as an error and asks for the
    fleet, so the mark does not stay lit on a group that stayed closed.

    A peers-on group carries a mark on its tree row, its card head,
    the split title, each member tile and the zoom strip of its
    panels. The mark follows the exact group path, never a sub-group,
    and never a lens bucket of the same name.

    A narrow tile drops the mark instead of wrapping its head. A narrow
    zoom strip shows the bare mark and cuts the title to fit the screen.

    Every new string has a zh-TW translation. The key map frame gains
    one binding, so its count reads 55.

    Refs #145
    Enabling asks once and sends nothing until the y. Enter does not
    confirm. Disabling sends at once. A cancelled switch sends nothing
    and does not leak its key into the dashboard. A group of only
    sub-groups is refused with a reason.

    A late y is not an answer: after the prompt fades, after another
    status replaces it, after the group loses its direct panels, and
    after the view leaves the dashboard. A withdrawn question takes
    its prompt down.

    The flip is drawn at once and a snapshot settles it. A refusal
    reaches the status line and the next snapshot takes the mark back.

    The mark is checked on and off on the tree row, the card head, the
    split title, the member tile and the zoom strip. It is absent on a
    sub-group, a parent group and a lens bucket.

    A narrow tile keeps its height. A narrow zoom strip stays inside
    the screen, with CJK titles and with titles that carry a middle dot.

    Refs #145
    A worker's MCP server now lists baton_peers, baton_peek and
    baton_tell beside score_submit. Each call is checked by the daemon,
    so a refusal says why. baton_peek returns plain text with the
    terminal's control sequences removed. The conductor's tools are
    unchanged.

    baton ctl group peers <group> on|off switches a group from the
    command line. --score-only is still accepted and selects the
    worker table.

    The mcp package's tests move with it, since NewScore is renamed
    NewWorker and they call it.

    Refs #145
    Covers the control client against a daemon, a send answered by its
    own notice and not a broadcast one, the ctl on, off and usage
    forms, and the tool descriptions quoting the server's caps.

    Refs #145
    An agent panel or a conductor can no longer file an existing panel
    into a peers-on group, by panel.group, a merging rename or a
    dissolve. The operator adds panels to it. Spawning a new panel
    there is unchanged.

    group.peers validates the group name in both directions and says
    nothing when the state does not change. A refusal no longer quotes
    a long name back in full. A persisted panel with an unclean id is
    not restored, and the ids in the prefix and the notice are cleaned.

    baton_peek keeps the text after an escape string that has no
    terminator and reads no more than one tail. The dashboard question
    is withdrawn when the group's members change while it is up.

    The tests come with it: setGroupPeers changed its signature and
    the existing ones call it.

    Refs #145
…tect

    CONTROL gains a Peers section: the verbs, the worker tools, what a
    send does, who may add a panel, and the limits. The guardrail table
    and the conductor's may-not list follow. SPEC, KEYS and PLUGIN
    carry the wire fields, the g p key and the two events. SCORE no
    longer says score_submit is the only worker tool.

    Refs #145
@cmj0121
cmj0121 merged commit 875a9c7 into main Oct 13, 2026
2 checks passed
@cmj0121
cmj0121 deleted the feat/group-peer-interaction branch October 13, 2026 00:39
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.64903% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.68%. Comparing base (cb7e1c8) to head (a175778).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
internal/mcp/mcp.go 87.71% 12 Missing and 2 partials ⚠️
internal/control/control.go 90.90% 2 Missing and 2 partials ⚠️
internal/server/server.go 99.10% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #146      +/-   ##
==========================================
+ Coverage   88.36%   88.68%   +0.32%     
==========================================
  Files         151      152       +1     
  Lines       23465    24001     +536     
==========================================
+ Hits        20735    21286     +551     
+ Misses       1930     1910      -20     
- Partials      800      805       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

Let panels in a group read and type into each other when the operator switches it on

2 participants