Skip to content

Chat-first team simulation: bus SoT, wake daemon, sessions, Slack bridge + legacy purge#2

Merged
fioenix merged 18 commits into
mainfrom
feat/chat-first-team-simulation
Jun 27, 2026
Merged

Chat-first team simulation: bus SoT, wake daemon, sessions, Slack bridge + legacy purge#2
fioenix merged 18 commits into
mainfrom
feat/chat-first-team-simulation

Conversation

@fioenix

@fioenix fioenix commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Mục tiêu

Chuyển Hatch từ một controller điều phối công việc sang một embedded harness chat-based cho squad coding-agent chung repo. Tương tác chính là chat (bus), không còn engine assign/lane/ticket. Con người là boss, làm việc qua root-agent (Claude Code) + Slack bridge.

Mô hình mới

  • bus (.hatch/run/bus/) = chat-native SoT, đúng nguyên lý Slack conversation structure (channel · message · thread-qua-InReplyTo · mention). 1 thread = 1 task.
  • daemon (hatch daemon) = delivery: wake teammate được @mention bằng cách resume đúng session, theo wake-policy 7 luật. Không tự sinh việc.
  • session store = warm session per (agent, thread): claude assign UUID, codex capture session_meta.id; agy/kiro stateless đọc lại bus/KB. KB/bus là SoT, session chỉ là cache.
  • slack (hatch slack) = bridge 2 chiều (Socket Mode), mỗi agent một Slack app/bot thật (mention/DM native), hub app fallback. Slack thay được bằng Discord — chỉ là projection của bus (xem docs/chat-native-model.md).
  • roster = presence; working-agreement = chuẩn làm việc, compile thành L0.

Dọn dẹp lớn

  • Gỡ toàn bộ engine legacy (orchestrator/wf/gate/ceremony/decide/metrics/mux/oncall/presence + lệnh run/plan/watch/ticket/…), bỏ build tag hatch_legacy → một binary.
  • Gỡ model ticket/lane/board — task giờ là bus thread.
  • Gỡ TUI (chat/board) → kéo theo toàn bộ cây charmbracelet. Direct deps còn 4: cobra, mcp-go-sdk, slack-go, yaml.
  • Gom toàn bộ runtime vào .hatch/run/ → gitignore một dòng; top-level .hatch thuần SSOT.

Headless wake contract (verified trên CLI đã cài)

claude -p --resume · codex exec resume · agy -p --conversation · kiro kiro-cli chat --no-interactive --resume-id.

Kiểm thử

make lint + make test xanh; build sạch (một binary). Net: ~−4600 dòng legacy. Specs trong docs/superpowers/specs/.

Còn treo (follow-up)

  • port.OnCall interface mồ côi (1 dòng).
  • Reconcile roster.SessionID vs session store.
  • (tuỳ chọn) hatch post CLI; rename bus/threadschannels.

🤖 Generated with Claude Code

fioenix and others added 15 commits June 22, 2026 12:54
Research paperclip (backlog-as-comms) vs overclaud, then the approved
design: chat is the primary peer-to-peer channel, task management is
demoted to a plan/docs layer, and a wake daemon delivers @mentions to
teammates without orchestrating work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add model.Member/Roster (the live in-room projection of a registry
agent) and a filesystem roster store. Expose join/roster/leave MCP
tools and refresh presence on any chat activity, plus a `hatch roster`
view. This is the team-simulation presence layer, distinct from the
legacy presence package (availability-for-assignment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
internal/wake.Decide is an IO-free function that, given the roster, a
batch of new messages and prior state, decides who to wake and what to
hold/escalate: trigger-only-if-addressed, coalesce, debounce, no-self,
depth cap, rate cap, and loop-break. Wakes are always a consequence of
a sent message — never invented work. Table-tested across all rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
internal/daemon tails the bus, runs the wake policy, and drives a
per-kind Runner that resumes a teammate's CLI session so it keeps its
memory: claude `-p --resume`, codex `exec resume`. Background dispatch
makes the mid-turn debounce real; held payloads are redelivered, never
dropped; unresolvable cascades escalate to the boss's DM. Exposed as
`hatch daemon [--once]`. Delivery only — it never assigns work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…frame

Add .hatch/working-agreement.md (professional norms + their daemon
backstops) and compile it into every surface as an L0 section, hashed
for change detection and scaffolded into new workspaces. Charter now
splits work-orchestration (none) from delivery/wake (yes), names the
human user as boss, and makes chat primary / task a docs layer; the
conductor role is reframed as the boss's delegate. Surfaces recompiled;
MCP tool list now advertises join/roster/leave.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A long-running adapter that gives the boss a real Slack window into the room:
mirrors every bus message into one #squad channel (each agent impersonated by
name + icon via one bot token) and ingests the boss's Slack messages back onto
the bus, where the wake daemon delivers them. Tagging "@codex …" in Slack wakes
codex exactly like a peer message — the human in Slack is just a peer who sits
outside the terminal. It is delivery/presentation, never work-orchestration.

- Socket Mode (no public URL): runs behind NAT on the boss's Mac.
- Loop break: OUT skips From==boss; IN skips bot_id/echo/system events.
- 1 task = 1 bus channel = 1 Slack thread (threadmap persisted, atomic write).
- Tokens from env or gitignored .hatch/slack/config.json — never committed.
- --dry-run mirrors to stdout with no credentials; --once does one OUT pass.

slack-go pinned at v0.26.0; pure OUT/IN methods table-tested with a fake poster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch from one bot impersonating everyone to one Slack app per agent, so each
agent posts and is @mentioned as a real Slack principal (native mention, DM,
avatar, presence). A single hub app runs Socket Mode (inbound) and is the
fallback voice for escalations and any agent without its own token.

- Config: AppToken + HubToken + per-agent Agents{id→token} (env HATCH_SLACK_TOKEN_<ID>
  or config.json); validated.
- multiPoster posts via the agent's own client; falls back to hub + username
  override when an agent has no token (graceful degrade, no crash).
- Inbound: auth.test on each agent token builds botUserID→agentID; translateMentions
  rewrites native <@U…> to @agent so bus.Post routes it. Bot/echo events still
  dropped (loop break unchanged).
- dry-run uses an in-memory threadmap so it never pollutes the real one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All four agent kinds can now be resumed from chat (Slack @tag → daemon):
- agy: `agy -p [--conversation <id>] <prompt>` (Antigravity/Gemini-CLI lineage,
  positional prompt).
- kiro: `kiro-cli chat --no-interactive [--resume-id <id>] <prompt>` (binary is
  kiro-cli, not the kiro IDE; needs KIRO_API_KEY).

Both contracts verified against the installed CLIs' help (agy also probed live;
kiro: 'chat [OPTIONS] [INPUT]' + --no-interactive + --resume-id <SESSION_ID>).
Only manual/user kinds stay interactive (never daemon-woken). Specs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A project (one Slack channel / .hatch workspace) accumulates many sessions per
agent over time; roster.json's single mutable SessionID could not represent or
manage that. Introduce a session store keyed by (agent, bus-thread): the wake
daemon resumes the warm session for the task it is waking about, and falls back
gracefully when there is none.

- model.Session + internal/session.Store (.hatch/sessions.json, atomic write,
  Get/Put-with-history/MarkStale/All).
- ExecRunner.planWake picks the strategy per kind: claude assigns its own UUID
  (--session-id → --resume), codex captures session_meta.id from --json (→ exec
  resume), agy/kiro run stateless (read bus/KB each wake — KB/bus is the SoT, a
  session is only a warm cache).
- commit: assign/capture store a live session; resume bumps the timestamp; a
  failed resume marks it stale so the next wake self-heals to fresh.
- hatch sessions: view of warm sessions per (agent, thread).
- Capture is best-effort: format drift just means no store, never a crash.

Deferred: per-thread parallelism (wip>1) needs daemon+wake working-set keyed by
(member,thread); v1 keeps per-member serialization. kiro capture via
--list-sessions deferred (racy).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d only

Hatch pivoted from a work-orchestration controller to an embedded, chat-based
harness; the old engine has lived behind the hatch_legacy build tag since. Delete
it outright and drop the dual-build machinery.

Removed (~50 files): the orchestrator, wf, gate, ceremony, decide, metrics, mux,
oncall and presence packages, and the run/plan/watch/orchestrate/ticket/gate/
ceremony/standup/tick/mob/pair/cost/metrics/oncall/presence/deps/escalate/report
CLI commands. Dropped the hatch_legacy / !hatch_legacy build tags, the
root_legacy/root_default split and addLegacyCommands hook.

Kept everything chat-based: the bus (SoT for squad chat + tasks, 1 thread = 1
task), wake daemon, Slack bridge, roster, sessions, KB, ledger (used by kb),
compile/SSOT. Removed orphaned .hatch data (board lanes, protocol/claim-lock.md);
gitignore roster.json. Charter constraint now: one binary, no dual build.

Single build/test (no more -tags hatch_legacy). lint + test green; surfaces
recompiled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime was scattered across .hatch/{bus,ledger,compiled,mcp,logs,slack} plus
roster.json/sessions.json, needing nine gitignore lines. Move it all under
.hatch/run/ so the top level is pure SSOT (charter, registry, roles, context,
protocol, workflow, kb) and one gitignore line (/.hatch/run/) covers runtime.

- paths.go: SSOT helpers stay at top level; runtime helpers (Bus, Board, Ledger,
  Compiled, Manifest, Logs, MCPLog, MCPSnippets, Roster, Sessions, Slack*) route
  through a run() helper. Dropped dead Presence/Oncall helpers.
- bus.go + compile/mcp.go + client.go use the helpers instead of hardcoding paths.
- init.go writes a single /.hatch/run/ ignore line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hatch's own chat substrate (the bus) is the source of truth; Slack/Discord/etc.
are interchangeable projections. Documents the conversation primitives
(workspace/channel/message/thread-via-reply/mention/type), what is deliberately
omitted, and how a bridge maps the model — so a new client (e.g. Discord) is a
thin adapter, not a model change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lane-board task model (tickets in lane directories, status↔lane sync, board
validation) was the last of the orchestration-era task management still wired
into the default build. In the chat-first model a task IS a bus thread, so remove
it outright.

Removed: model.Ticket, store.Board, validate.Board (+test), the port.Board
interface and its store assertion, and the paths Board()/Lane() helpers.
Trimmed: `hatch sync` now reconciles only KB index + compile freshness (no
ticket↔lane, no --fix); `hatch validate` checks registry + workflow only.
Kept: model.Workflow as a *prose* description (compiled into the surfaces) — the
squad still follows backlog→in-progress→review→done via chat, with no engine.

The TUI already derives its footer stats from chat threads, not the board.
lint + test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Slack-style `hatch chat`/`hatch board` TUI was a local viewer of the bus. In
the chat-first concept the human works through the root agent (Claude Code) and
the Slack bridge, so the local viewer is redundant; the bus stays readable as
raw markdown or via the MCP chat tools.

Removed internal/tui (chat/shared) and the chat/board/watch commands. This drops
the entire charmbracelet dependency tree (bubbletea, bubbles, lipgloss,
glamour, chroma, …) — direct deps are now just cobra, mcp-go-sdk, slack-go, yaml.

lint + test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fioenix added a commit that referenced this pull request Jun 23, 2026
Addresses the changes-requested review on PR #2:

- [#1] daemon: replace (not merge) the wake-policy Working snapshot each tick, so
  a finished runner stops debouncing future wakes (agents no longer go silent).
- [#2] slack: guard the threadmap with a mutex — the Socket Mode consumer and the
  mirror ticker share it (was an unsynchronised map → data race).
- [#3] slack: config/help/spec now point at .hatch/run/slack/config.json (drifted
  after the runtime-dir move).
- [#4] daemon: persist the processed cursor (.hatch/run/daemon.cursor) so a
  restart resumes instead of replaying the whole backlog.
- [#5] daemon: on a failed Wake, requeue the payload before clearing the working
  flag — a spawn/resume error is retried next tick, not dropped. pending is now
  mutex-guarded for the dispatch goroutines. Added Wait() for graceful drain.
- [#6] slack: persist the mirror cursor in the threadmap, so a bridge restart
  does not re-post history.
- [#7] roster + session: serialize read-modify-write with a package mutex and
  per-pid temp files (concurrent runners no longer clobber/lose updates).
- [#8] slack: optional BossUserID (HATCH_SLACK_BOSS_USER) — when set, only the
  boss's Slack user may drive the squad; empty = single-tenant.

Deferred (documented): per-thread coalescing/dispatch (#9) stays with the
parallelism work. lint + test green; -race clean on daemon/slack/session/roster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the changes-requested review on PR #2:

- [#1] daemon: replace (not merge) the wake-policy Working snapshot each tick, so
  a finished runner stops debouncing future wakes (agents no longer go silent).
- [#2] slack: guard the threadmap with a mutex — the Socket Mode consumer and the
  mirror ticker share it (was an unsynchronised map → data race).
- [#3] slack: config/help/spec now point at .hatch/run/slack/config.json (drifted
  after the runtime-dir move).
- [#4] daemon: persist the processed cursor (.hatch/run/daemon.cursor) so a
  restart resumes instead of replaying the whole backlog.
- [#5] daemon: on a failed Wake, requeue the payload before clearing the working
  flag — a spawn/resume error is retried next tick, not dropped. pending is now
  mutex-guarded for the dispatch goroutines. Added Wait() for graceful drain.
- [#6] slack: persist the mirror cursor in the threadmap, so a bridge restart
  does not re-post history.
- [#7] roster + session: serialize read-modify-write with a package mutex and
  per-pid temp files (concurrent runners no longer clobber/lose updates).
- [#8] slack: optional BossUserID (HATCH_SLACK_BOSS_USER) — when set, only the
  boss's Slack user may drive the squad; empty = single-tenant.

Deferred (documented): per-thread coalescing/dispatch (#9) stays with the
parallelism work. lint + test green; -race clean on daemon/slack/session/roster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fioenix fioenix force-pushed the feat/chat-first-team-simulation branch from 3a231e9 to 7f9fda4 Compare June 23, 2026 06:29
fioenix and others added 2 commits June 27, 2026 16:11
… session

Codex review #9: coalescing by agent merged messages from different threads into
one wake, and the runner then resumed whichever thread came last — so a teammate
could be resumed into the wrong task's session.

Now wakes are keyed by (agent, thread): each task thread coalesces and dispatches
separately, carrying its own session context. WakeDecision gains Thread; the
daemon keys held payloads by (agent, thread) and splits them on flush.

Dispatch stays serial per agent (one in-flight runner per member): running two
processes for the same agent in one working tree would race the git tree, so
true per-thread parallelism (wip>1) is deferred until worktree isolation lands.

lint + test green; -race clean on daemon + wake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex re-review round 2 (7/9 already resolved). Remaining:
- [HIGH] hatch daemon --once / shutdown now d.Wait() before returning, so a wake
  whose cursor was checkpointed in Tick isn't dropped when the process exits.
  (Abrupt SIGKILL can still drop in-flight delivery — documented; true
  at-least-once would need a disk-persisted pending queue.)
- [MEDIUM] pending flush re-checks roster Reachable before dispatch: a member
  that left while a wake was queued is held, not woken.
- [nit] config.go package comment path → .hatch/run/slack/config.json.

lint + test green; -race clean on daemon + cli.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fioenix fioenix merged commit 93f5938 into main Jun 27, 2026
2 checks passed
@fioenix fioenix deleted the feat/chat-first-team-simulation branch June 27, 2026 09:19
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