Chat-first team simulation: bus SoT, wake daemon, sessions, Slack bridge + legacy purge#2
Merged
Merged
Conversation
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>
3a231e9 to
7f9fda4
Compare
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.hatch/run/bus/) = chat-native SoT, đúng nguyên lý Slack conversation structure (channel · message · thread-qua-InReplyTo· mention). 1 thread = 1 task.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.(agent, thread): claude assign UUID, codex capturesession_meta.id; agy/kiro stateless đọc lại bus/KB. KB/bus là SoT, session chỉ là cache.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 (xemdocs/chat-native-model.md).Dọn dẹp lớn
hatch_legacy→ một binary.chat/board) → kéo theo toàn bộ cây charmbracelet. Direct deps còn 4: cobra, mcp-go-sdk, slack-go, yaml..hatch/run/→ gitignore một dòng; top-level.hatchthuần SSOT.Headless wake contract (verified trên CLI đã cài)
claude
-p --resume· codexexec resume· agy-p --conversation· kirokiro-cli chat --no-interactive --resume-id.Kiểm thử
make lint+make testxanh; build sạch (một binary). Net: ~−4600 dòng legacy. Specs trongdocs/superpowers/specs/.Còn treo (follow-up)
port.OnCallinterface mồ côi (1 dòng).roster.SessionIDvs session store.hatch postCLI; renamebus/threads→channels.🤖 Generated with Claude Code