Skip to content

feat(hooks): auto-claim edited files + cross-session overlap warnings#10

Merged
NagyVikt merged 1 commit into
mainfrom
agent/claude/auto-claim
Apr 23, 2026
Merged

feat(hooks): auto-claim edited files + cross-session overlap warnings#10
NagyVikt merged 1 commit into
mainfrom
agent/claude/auto-claim

Conversation

@NagyVikt
Copy link
Copy Markdown
Collaborator

Summary

Turns the soft advisory claims system into something agents actually use — without asking them to call any new tools. PostToolUse observes Edit / Write / MultiEdit / NotebookEdit and writes a claim for the editing session; next turn, UserPromptSubmit surfaces a compact warning naming files that other sessions have recently touched.

Why observed, not predictive

Predictive claims ("I plan to edit X") require agents to call a tool in advance. They won't. Observed claims require nothing from the agent and are always grounded in actual edits — which makes the resulting warnings trustworthy instead of something agents learn to ignore.

What's new

  • Storage.recentClaims(task_id, since_ts) — the query powering the conflict preface. Stale (outside-window) claims are intentionally excluded; they describe finished work, not live collisions.
  • PostToolUse auto-claimextractTouchedFiles + autoClaimFromToolUse in packages/hooks/src/handlers/post-tool-use.ts. Silent-skip on unknown tools and malformed input: PostToolUse runs on every tool call, so any throw would degrade every turn.
  • UserPromptSubmit conflict prefacebuildConflictPreface groups claims by session ("A: src/viewer.tsx, src/api.ts") so the agent reads one clustered warning instead of three disjoint ones. 5-minute window, own-session claims excluded.
  • Tests: 1 new storage test for recentClaims; 7 new hook tests — extractTouchedFiles coverage (write vs non-write tools, malformed input), autoClaimFromToolUse (joined / unjoined / stolen-claim), buildConflictPreface grouping + own-claim exclusion, plus an end-to-end runHook integration proving A edits → B's next turn sees the warning naming src/viewer.tsx and A.

Test plan

  • Storage tests: 17/17 (added recentClaims window test)
  • Hook tests: 18/18 (added 7 auto-claim tests)
  • Core tests: 15/15
  • MCP server tests: 13/13
  • Worker tests: 12/12
  • CLI tests: 4/4
  • Typecheck 12/12, build, biome on touched files clean

Deliberately deferred

Stale handoff reclamation — the next turn's "your handoff expired" nudge — is deferred until we've run the system on a real two-agent task for a day or two. The shape of the right notification depends on how often expiries actually happen; a theoretical design here would probably pick the wrong primitive.

🤖 Generated with Claude Code

Observed-not-predictive claims: PostToolUse watches Edit / Write /
MultiEdit / NotebookEdit, extracts file_path, and writes a claim into
task_claims for the current session's joined task. Agents don't have to
remember to call task_claim_file for the claim system to protect their
work.

The warning half lives in UserPromptSubmit. A new buildConflictPreface
queries recentClaims(task_id, now - 5m) for claims held by *other*
sessions on the same task, groups them by session, and injects one line
per collaborator ("A: src/viewer.tsx, src/api.ts") plus a nudge to
coordinate via task_post / task_hand_off before editing those files.

Storage: new recentClaims(task_id, since_ts) method — the conflict
preface's query surface. Older-than-window claims are intentionally
excluded; stale claims describe finished work, not live collisions.

Ordering choice worth flagging: claims are written by the session that
did the edit, not reserved by the session that intends to edit.
Predictive claims require agents to remember to call a tool in advance
(they won't). Observed claims require nothing and are always grounded
in actual edits, which makes the resulting warnings trustworthy. Agents
tune out warnings that are wrong a third of the time.

Tests: storage test for recentClaims window + 7 hook-layer tests
(extractTouchedFiles coverage, auto-claim on joined / unjoined / stolen
cases, buildConflictPreface grouping + own-claim exclusion, plus an
end-to-end runHook integration where A edits -> B's next turn sees the
warning naming viewer.tsx and A). All gates green.
@NagyVikt NagyVikt merged commit 1f15b2e into main Apr 23, 2026
0 of 3 checks passed
@NagyVikt NagyVikt deleted the agent/claude/auto-claim branch April 23, 2026 21:26
NagyVikt added a commit that referenced this pull request Apr 24, 2026
Add non-negotiable rule #10 and a Worktree discipline section to
CLAUDE.md: never edit on the local `main` checkout, always run work in
an `agent/*` branch + worktree via `gx branch start`, claim files via
`gx locks claim`, finish via PR with `gx branch finish ... --via-pr
--wait-for-merge --cleanup`, and coordinate with codex through colony
MCP `task_post` / `task_claim_file` / `task_hand_off`.

This matches how codex already operates via Guardex and keeps parallel
lanes safe from primary-checkout drift.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
NagyVikt added a commit that referenced this pull request May 5, 2026
* wip: bridge daemon fast-path wrapper (no daemon endpoint yet)

Adds apps/cli/bin/colony.sh — POSIX shell wrapper that fast-paths
`colony bridge lifecycle --json` via curl POST to a (not-yet-existing)
daemon endpoint at /api/bridge/lifecycle. Falls back to in-process Node
on any failure with stdin replayed from a temp file.

Not yet wired:
- Worker endpoint /api/bridge/lifecycle (next commit)
- package.json bin entry change
- Tests
- CLAUDE.md rule #10 reconciliation (blocked on user decision)

Why WIP commit: rule #10 forbids hooks crossing an HTTP boundary on
the write path. The fast-path violates the literal text but preserves
the spirit (writes still succeed via fallback when daemon is down).
That's an architectural call that needs explicit user sign-off, not
my judgement. Committing the wrapper makes the branch durable while
that decision is pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli,worker): daemon fast-path for `colony bridge lifecycle`

Every IDE tool event triggers `colony bridge lifecycle ...` from external
hook integrations. Cold-starting Node + bundle load on each event pegs
~one core for ~300ms — multiplied across concurrent agents this is a
measurable CPU storm visible at the system level.

The CLI bin entry is now a POSIX shell wrapper at apps/cli/bin/colony.sh.
When invoked as `colony bridge lifecycle --json`, it POSTs the envelope
to the long-lived worker daemon at POST /api/bridge/lifecycle (new) and
exits — no Node startup. Anything else execs the Node CLI exactly as
before.

The worker's daemon route reuses the long-lived MemoryStore so the
SQLite connection isn't reopened per event. CLAUDE.md rule #10 is
reworded to match the new contract: writes still succeed in-process
when the daemon is unreachable. The wrapper buffers stdin to a temp
file and falls back to Node on curl missing, connection refused,
2s timeout, non-200, unknown flags, or invocation without --json.

Tests:
- apps/worker/test/server.test.ts: POST /api/bridge/lifecycle routes
  envelopes through the long-lived store, dedupes duplicate event_ids,
  surfaces ok:false on malformed input.
- apps/cli/test/bin-shim.test.ts: load-bearing rule-10 contract test.
  With daemon unreachable, the wrapper falls back to Node with stdin
  and quoted args (including paths with spaces) intact. Also covers
  COLONY_BRIDGE_FAST=0 disable, --version pass-through, and that
  bare `bridge lifecycle` (no --json) keeps human-readable output.

Opt out at any time with COLONY_BRIDGE_FAST=0.

Notes:
- pnpm lint OOM'd locally (host has 13GB active swap from unrelated
  processes); CI lint should run cleanly. Will re-run before merge.
- e2e-publish.sh re-run pending — it covers the bin-shim install path
  end-to-end and is the official guard for changes to the publish
  surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(bench): bridge-lifecycle daemon vs in-process Node fallback harness

Adds scripts/bench-bridge-fastpath.mjs. Boots a fresh worker against a
temp COLONY_HOME on a non-default port, then runs N concurrent shell-shim
invocations of `bridge lifecycle --json` once with the daemon reachable
(fast path) and once with COLONY_BRIDGE_FAST=0 (forced in-process Node
fallback, mirrors today's behavior). Reports mean, median, p95, p99.

Local results, 8 concurrent × 4 iterations = 32 events per path:

  [fast (daemon)]         mean=54.2ms   p95=74.1ms    p99=130.4ms
  [slow (force-fallback)] mean=260.9ms  p95=725.4ms   p99=728.7ms

  speedup (mean): 4.8x   saved: 206.7ms/event
  speedup (p95):  9.8x

The p95 collapse from 725ms → 74ms is the Node cold-start tail under
burst load — exactly the spawn-storm pattern that motivated this PR
(four `colony bridge lifecycle` processes simultaneously at 100% CPU
each in the original screenshot).

Caveats: includes sh + curl + JSON serialization on the fast path; the
"slow" path includes the wrapper buffering stdin then exec'ing Node
(~5-10ms extra vs. raw `colony bridge lifecycle`), but that's swamped
by the ~250ms Node cold-start it's measuring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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