Skip to content

feat(coordination): passive sibling-task awareness across worktrees#2048

Open
maxonary wants to merge 2 commits into
generalaction:mainfrom
maxonary:max/multiple-agent-communication-stf1z
Open

feat(coordination): passive sibling-task awareness across worktrees#2048
maxonary wants to merge 2 commits into
generalaction:mainfrom
maxonary:max/multiple-agent-communication-stf1z

Conversation

@maxonary
Copy link
Copy Markdown
Contributor

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:

Need Reuses
Authenticated per-agent HTTP channel Existing agent-hooks/hook-server.ts + EMDASH_HOOK_PORT/TOKEN/PTY_ID env vars
Teach the agent the capability Existing Skills system — bundled emdash-coord SKILL.md
Task lifecycle hooks taskManager.hooks (task:provisioned / task:torn-down)
File-touch ground truth workspace.git.getFullStatus()

Flow

  1. Add two GET routes to the existing hook server: /coord/siblings and /coord/overlap?paths=.... Same X-Emdash-Token auth.
  2. Bundled emdash-coord SKILL.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.
  3. On task:provisioned and on agentEventChannel events: mark task active, stamp lastEventAt, schedule a debounced git scan.
  4. Scan reads workspace.git.getFullStatus() (staged + unstaged), replaces the task's touched-file set. Universal across providers — no per-agent tool-call parsing.
  5. A 60s decay sweep demotes active → idle (after 5 min silence) → inactive (after 2 h). Inactive tasks are excluded from sibling queries.

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)

  • idx_task_touched_files_path (for cross-task overlap lookups)
    ```

Repo-relative paths only, so worktrees/foo/src/X.ts and worktrees/bar/src/X.ts collapse 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

  • `pnpm typecheck` ✅
  • `pnpm lint` ✅
  • `pnpm format:check` ✅
  • `pnpm test` — 3 pre-existing failures in `db/legacy-port/importers/relational/relational.test.ts` (verified by re-running with this branch's changes stashed; failure is identical). All other 546 tests pass.

Deferred (intentional next slices)

  • Renderer badges + RPC controller for them
  • Push-side: detect overlap on edit-tool hook events and surface a warning back via the hook response payload where the provider supports it
  • Tests for the coordination module
  • A `bundled-catalog.json` entry so `emdash-coord` shows up in the Skills UI alongside other skills

Risks worth naming

  • Hook event volume. Edit events fire constantly. Writes are debounced (2s flush) and per-task batched.
  • Path normalisation. Always store repo-relative — guaranteed by going through `workspace.git` (which yields repo-relative paths).
  • Skill install on user homes. Only writes when the agent's config dir already exists, version-marked, leaves unmarked user-authored files at the same path untouched.

🤖 Generated with Claude Code

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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR adds a passive sibling-task awareness layer to emdash by reusing existing primitives: the hook server gains two GET routes (/coord/siblings, /coord/overlap), a new coordination module tracks task activity and touched files via two new SQLite tables, and a bundled emdash-coord SKILL.md is installed into each detected agent's config directory at boot.

  • New coordination module (activity-store.ts, scanner.ts, service.ts, http-handlers.ts, skill-installer.ts): debounced git scans populate task_touched_files after each agent event; a 60s decay timer demotes active → idle → inactive; /coord/siblings and /coord/overlap serve the aggregated view to any agent with the shared token.
  • DB migration 0009 adds task_activity (per-task status/summary/timestamps) and task_touched_files (repo-relative paths, indexed for cross-task lookup), both with FK cascade-on-delete from tasks.
  • SKILL.md curl examples are missing X-Emdash-Pty-Id: $EMDASH_PTY_ID, which the handlers require to resolve the caller's task context — every agent invocation via the skill will receive a 400 until this is added.

Confidence Score: 3/5

The 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.

Important Files Changed

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)
Loading
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

Comment on lines +25 to +41
## 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"
\`\`\`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
## 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.

Comment on lines +87 to +89
scanScheduler.cancelAll();
activityStore.flush();
for (const d of this.disposers) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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
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