feat(coordination): passive sibling-task awareness across worktrees#2048
feat(coordination): passive sibling-task awareness across worktrees#2048maxonary wants to merge 2 commits into
Conversation
Adds a coordination layer so agents working in different worktrees of the
same project can discover what siblings are doing — passive awareness only,
no claims or locks. Built entirely on existing emdash primitives (hook
server + skills system), no MCP, no new HTTP server, no new SDK deps.
How it works
- `coordinationService` (new) wires into `agentHookService`,
`taskManager.hooks`, and `agentEventChannel`.
- File touches are derived from `workspace.git.getFullStatus()` per task —
universal across providers, accurate, no per-agent tool-call parsing.
- Hook events drive *when* to rescan (debounced via `scanScheduler`) and
stamp `lastEventAt`; git is the truth for *which* paths.
- A 60s decay sweep demotes active → idle → inactive on schedule.
Delivery to agents
- Two GET endpoints on the existing hook server, auth'd by the same
`X-Emdash-Token` already injected into every spawned agent:
- `GET /coord/siblings` — other active/idle tasks + their touched files
- `GET /coord/overlap?paths=...` — which siblings have touched each path
- A bundled `emdash-coord` SKILL.md installed once at boot into each
detected agent's skill directory (`~/.claude/commands/`,
`~/.codex/skills/`, `~/.config/opencode/skills/`). Idempotent, version-
marked, and only writes when the agent's config dir already exists.
Schema
- New tables `task_activity` and `task_touched_files` with cascading FKs to
`tasks`. Generated as Drizzle migration 0009.
Deferred (per the saved design)
- Renderer badges / RPC controller for them
- Push-side warning injection on hook responses
- Coordination-module tests
- Bundled-catalog entry so the skill shows up in the Skills UI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds a passive sibling-task awareness layer to emdash by reusing existing primitives: the hook server gains two GET routes (
Confidence Score: 3/5The coordination module is well-structured and the database changes are solid, but the bundled skill's curl examples are missing a required header, causing every agent-initiated request to fail with a 400. The skill is the primary agent-facing interface for the entire feature. Both example commands omit X-Emdash-Pty-Id: $EMDASH_PTY_ID, which the handlers hard-require to resolve the caller's task and project — an agent following the skill verbatim will never get a successful response. The rest of the implementation (DB schema, decay logic, scanner, installer) is correct and the existing hook server auth is properly reused. src/main/core/coordination/skill-content.ts needs the missing header added to both curl examples before the feature works end-to-end.
|
| Filename | Overview |
|---|---|
| src/main/core/coordination/skill-content.ts | Bundled SKILL.md content with curl examples missing the required X-Emdash-Pty-Id header — every agent call following these instructions will receive a 400 error |
| src/main/core/coordination/activity-store.ts | Debounced touch recording, synchronous SQLite read/write with proper upsert semantics; decay logic and sibling queries look correct |
| src/main/core/coordination/http-handlers.ts | GET /coord/siblings and /coord/overlap handlers — auth via shared token, caller resolution via pty-id header, paths passed to parameterized query (no injection risk) |
| src/main/core/coordination/service.ts | CoordinationService wiring: routes, task lifecycle hooks, agent event subscription, decay timer, skill install; flush() in dispose() unguarded but otherwise clean |
| src/main/core/coordination/scanner.ts | Debounced per-task git scan using workspace.git.getFullStatus(); cancellation on teardown is correct |
| src/main/core/coordination/skill-installer.ts | Idempotent skill install: checks config dir existence, compares version marker, skips user-authored files without the marker |
| drizzle/0009_massive_jamie_braddock.sql | New task_activity and task_touched_files tables with correct FK cascade-on-delete and path/task_id indexes |
Sequence Diagram
sequenceDiagram
participant Agent as Agent PTY
participant HS as HookServer
participant CS as CoordinationService
participant AS as ActivityStore
participant SS as ScanScheduler
participant Git as workspace.git
Note over CS: initialize()
CS->>HS: addRoute GET /coord/siblings
CS->>HS: addRoute GET /coord/overlap
Note over Agent: task:provisioned
CS->>AS: markActive(taskId)
CS->>Git: getFullStatus()
Git-->>CS: staged + unstaged paths
CS->>AS: replaceTouches(taskId, paths)
Note over Agent: agent event fires
CS->>AS: markActive(taskId, summary)
CS->>SS: schedule(taskId)
SS->>Git: getFullStatus() [after 1.5s debounce]
Git-->>SS: paths
SS->>AS: replaceTouches(taskId, paths)
Agent->>HS: GET /coord/siblings X-Emdash-Token + X-Emdash-Pty-Id
HS->>AS: listSiblings(projectId, taskId)
AS-->>HS: SiblingTask[]
HS-->>Agent: siblings response
Agent->>HS: "GET /coord/overlap?paths=..."
HS->>AS: findOverlap(projectId, taskId, paths)
AS-->>HS: FileOverlap[]
HS-->>Agent: overlaps response
Note over Agent: task:torn-down
CS->>SS: cancel(taskId)
CS->>AS: markInactive(taskId)
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
src/main/core/coordination/skill-content.ts:25-41
The curl examples in both skill commands omit the `X-Emdash-Pty-Id` header that `handleSiblings` and `handleOverlap` require to resolve the calling task's project context. Without it, every request from an agent following these instructions returns `400 missing X-Emdash-Pty-Id header`, making the skill completely non-functional. The `$EMDASH_PTY_ID` env var is already injected into every agent PTY — the same pattern used in `agent-notify-command.ts`.
```suggestion
## List active sibling tasks
\`\`\`bash
curl -sf \\
-H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\
-H "X-Emdash-Pty-Id: $EMDASH_PTY_ID" \\
"http://127.0.0.1:$EMDASH_HOOK_PORT/coord/siblings"
\`\`\`
Returns JSON with each active sibling task's branch, name, status, last activity timestamp, and the files it has touched.
## Check overlap for specific paths
\`\`\`bash
curl -sf \\
-H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\
-H "X-Emdash-Pty-Id: $EMDASH_PTY_ID" \\
"http://127.0.0.1:$EMDASH_HOOK_PORT/coord/overlap?paths=src/foo.ts,src/bar.ts"
\`\`\`
```
### Issue 2 of 2
src/main/core/coordination/service.ts:87-89
`activityStore.flush()` is called without a try-catch in `dispose()`. If the database operation throws (e.g., the SQLite connection is already closed during shutdown), the exception propagates and the `for (const d of this.disposers)` loop never runs, leaving all event subscriptions (`task:provisioned`, `task:torn-down`, `agentEventChannel`) dangling. Each disposer already has its own try-catch, so wrapping the flush call is the natural fix.
```suggestion
scanScheduler.cancelAll();
try {
activityStore.flush();
} catch {
// best-effort — flush may fail if the db is already closed
}
for (const d of this.disposers) {
```
Reviews (1): Last reviewed commit: "feat(coordination): passive sibling-task..." | Re-trigger Greptile
| ## List active sibling tasks | ||
|
|
||
| \`\`\`bash | ||
| curl -sf \\ | ||
| -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\ | ||
| "http://127.0.0.1:$EMDASH_HOOK_PORT/coord/siblings" | ||
| \`\`\` | ||
|
|
||
| Returns JSON with each active sibling task's branch, name, status, last activity timestamp, and the files it has touched. | ||
|
|
||
| ## Check overlap for specific paths | ||
|
|
||
| \`\`\`bash | ||
| curl -sf \\ | ||
| -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\ | ||
| "http://127.0.0.1:$EMDASH_HOOK_PORT/coord/overlap?paths=src/foo.ts,src/bar.ts" | ||
| \`\`\` |
There was a problem hiding this comment.
The curl examples in both skill commands omit the
X-Emdash-Pty-Id header that handleSiblings and handleOverlap require to resolve the calling task's project context. Without it, every request from an agent following these instructions returns 400 missing X-Emdash-Pty-Id header, making the skill completely non-functional. The $EMDASH_PTY_ID env var is already injected into every agent PTY — the same pattern used in agent-notify-command.ts.
| ## List active sibling tasks | |
| \`\`\`bash | |
| curl -sf \\ | |
| -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\ | |
| "http://127.0.0.1:$EMDASH_HOOK_PORT/coord/siblings" | |
| \`\`\` | |
| Returns JSON with each active sibling task's branch, name, status, last activity timestamp, and the files it has touched. | |
| ## Check overlap for specific paths | |
| \`\`\`bash | |
| curl -sf \\ | |
| -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\ | |
| "http://127.0.0.1:$EMDASH_HOOK_PORT/coord/overlap?paths=src/foo.ts,src/bar.ts" | |
| \`\`\` | |
| ## List active sibling tasks | |
| \`\`\`bash | |
| curl -sf \\ | |
| -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\ | |
| -H "X-Emdash-Pty-Id: $EMDASH_PTY_ID" \\ | |
| "http://127.0.0.1:$EMDASH_HOOK_PORT/coord/siblings" | |
| \`\`\` | |
| Returns JSON with each active sibling task's branch, name, status, last activity timestamp, and the files it has touched. | |
| ## Check overlap for specific paths | |
| \`\`\`bash | |
| curl -sf \\ | |
| -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\ | |
| -H "X-Emdash-Pty-Id: $EMDASH_PTY_ID" \\ | |
| "http://127.0.0.1:$EMDASH_HOOK_PORT/coord/overlap?paths=src/foo.ts,src/bar.ts" | |
| \`\`\` |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/core/coordination/skill-content.ts
Line: 25-41
Comment:
The curl examples in both skill commands omit the `X-Emdash-Pty-Id` header that `handleSiblings` and `handleOverlap` require to resolve the calling task's project context. Without it, every request from an agent following these instructions returns `400 missing X-Emdash-Pty-Id header`, making the skill completely non-functional. The `$EMDASH_PTY_ID` env var is already injected into every agent PTY — the same pattern used in `agent-notify-command.ts`.
```suggestion
## List active sibling tasks
\`\`\`bash
curl -sf \\
-H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\
-H "X-Emdash-Pty-Id: $EMDASH_PTY_ID" \\
"http://127.0.0.1:$EMDASH_HOOK_PORT/coord/siblings"
\`\`\`
Returns JSON with each active sibling task's branch, name, status, last activity timestamp, and the files it has touched.
## Check overlap for specific paths
\`\`\`bash
curl -sf \\
-H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \\
-H "X-Emdash-Pty-Id: $EMDASH_PTY_ID" \\
"http://127.0.0.1:$EMDASH_HOOK_PORT/coord/overlap?paths=src/foo.ts,src/bar.ts"
\`\`\`
```
How can I resolve this? If you propose a fix, please make it concise.| scanScheduler.cancelAll(); | ||
| activityStore.flush(); | ||
| for (const d of this.disposers) { |
There was a problem hiding this comment.
activityStore.flush() is called without a try-catch in dispose(). If the database operation throws (e.g., the SQLite connection is already closed during shutdown), the exception propagates and the for (const d of this.disposers) loop never runs, leaving all event subscriptions (task:provisioned, task:torn-down, agentEventChannel) dangling. Each disposer already has its own try-catch, so wrapping the flush call is the natural fix.
| scanScheduler.cancelAll(); | |
| activityStore.flush(); | |
| for (const d of this.disposers) { | |
| scanScheduler.cancelAll(); | |
| try { | |
| activityStore.flush(); | |
| } catch { | |
| // best-effort — flush may fail if the db is already closed | |
| } | |
| for (const d of this.disposers) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/core/coordination/service.ts
Line: 87-89
Comment:
`activityStore.flush()` is called without a try-catch in `dispose()`. If the database operation throws (e.g., the SQLite connection is already closed during shutdown), the exception propagates and the `for (const d of this.disposers)` loop never runs, leaving all event subscriptions (`task:provisioned`, `task:torn-down`, `agentEventChannel`) dangling. Each disposer already has its own try-catch, so wrapping the flush call is the natural fix.
```suggestion
scanScheduler.cancelAll();
try {
activityStore.flush();
} catch {
// best-effort — flush may fail if the db is already closed
}
for (const d of this.disposers) {
```
How can I resolve this? If you propose a fix, please make it concise.…mmunication-stf1z # Conflicts: # drizzle/meta/0009_snapshot.json # drizzle/meta/_journal.json # src/main/db/schema.ts
Why
When several emdash worktrees of the same project are open, each agent is siloed — nothing tells task B that task A is already editing the same file or working the same feature. This adds the missing comms layer so siblings can discover each other and avoid redundant or conflicting work.
Passive awareness only. No claims, no locks. Agents see what's happening; they decide what to do about it.
How
Built entirely on primitives emdash already had — no MCP, no new HTTP server, no new SDK deps:
agent-hooks/hook-server.ts+EMDASH_HOOK_PORT/TOKEN/PTY_IDenv varsemdash-coordSKILL.mdtaskManager.hooks(task:provisioned/task:torn-down)workspace.git.getFullStatus()Flow
/coord/siblingsand/coord/overlap?paths=.... SameX-Emdash-Tokenauth.emdash-coordSKILL.md installed once at boot into each detected agent's skill dir (~/.claude/commands/,~/.codex/skills/,~/.config/opencode/skills/). Idempotent, version-marked, only installs where the agent's config dir exists.task:provisionedand onagentEventChannelevents: mark task active, stamplastEventAt, schedule a debounced git scan.workspace.git.getFullStatus()(staged + unstaged), replaces the task's touched-file set. Universal across providers — no per-agent tool-call parsing.Schema (migration 0009)
```
task_activity (task_id PK→tasks, status, summary, last_event_at, updated_at)
task_touched_files (task_id+file_path PK, last_touched_at)
```
Repo-relative paths only, so
worktrees/foo/src/X.tsandworktrees/bar/src/X.tscollapse onto the same logical file.Try it
Inside any task's PTY (env vars are auto-injected):
```bash
curl -sf -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \
"http://127.0.0.1:\$EMDASH_HOOK_PORT/coord/siblings"
curl -sf -H "X-Emdash-Token: $EMDASH_HOOK_TOKEN" \
"http://127.0.0.1:\$EMDASH_HOOK_PORT/coord/overlap?paths=src/foo.ts"
```
Or just ask the agent to "check what other emdash tasks are working on" — the bundled skill teaches it the rest.
Checks
Deferred (intentional next slices)
Risks worth naming
🤖 Generated with Claude Code