feat(continuity): wire §6.3 shared-docs staleness notice

MarkdownChanges was deleted as dead code, but the underlying spec
requirement wasn't abandoned — rebuilt it independently. Adds
continuity.ConventionsHash for AGENTS.md/CLAUDE.md/VOCAB.md, tracks a
per-session snapshot on herdr.Session, and adds
Coordinator.checkConventions (run every Monitor tick) which compares
each active session's snapshot against its project's base repo and
pushes an in-pane notice via a new herdr.ConventionsNotifier
capability when they drift.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 22:25:53 +04:00
parent 3fe3aee5b7
commit 0ca78243b9
7 changed files with 225 additions and 8 deletions
+61
View File
@@ -382,6 +382,7 @@ func (c *Coordinator) Monitor(ctx context.Context, hard float64, interval time.D
case <-t.C:
c.refreshSessionHealth(ctx)
c.cleanupCompleted(ctx)
c.checkConventions(ctx)
expired, err := c.expire(ctx)
c.setMonitorHealth(err, len(expired))
if err != nil {
@@ -420,6 +421,61 @@ func (c *Coordinator) cleanupCompleted(ctx context.Context) {
}
}
// checkConventions is §6.3: "on update, the orchestra injects a notice to
// agents whose current task is adjacent" — adjacency here is "same project's
// base repo," and staleness is tracked by comparing each session's own
// last-known continuity.ConventionsHash against the base repo's current one,
// never by trusting the agent to notice on its own.
func (c *Coordinator) checkConventions(ctx context.Context) {
spec, ok := c.Worktrees.(WorktreeSpec)
if !ok {
return
}
c.loadSessions()
c.mu.Lock()
sessions := make(map[string]herdr.Session, len(c.sessions))
for id, s := range c.sessions {
sessions[id] = s
}
c.mu.Unlock()
changed := false
for taskID, session := range sessions {
t, ok := c.Store.Task(taskID)
if !ok || t.State != domain.StateLeased {
continue
}
repo, _, valid := spec.Spec(t)
if !valid {
continue
}
hash, err := continuity.ConventionsHash(repo)
if err != nil || hash == session.ConventionsHash {
continue
}
a, err := c.adapterFor(taskID, session)
if err != nil {
continue
}
notifier, ok := a.(herdr.ConventionsNotifier)
if !ok {
continue
}
if err := notifier.NotifyConventionsChanged(ctx, session); err != nil {
continue
}
session.ConventionsHash = hash
c.mu.Lock()
c.sessions[taskID] = session
c.mu.Unlock()
changed = true
}
if changed {
c.mu.Lock()
_ = c.saveSessionsLocked()
c.mu.Unlock()
}
}
func (c *Coordinator) expire(ctx context.Context) ([]domain.Event, error) {
// pane.exited is the low-latency path; lease expiry below remains the
// authoritative backstop when herdr misses an exit notification.
@@ -610,6 +666,11 @@ func (c *Coordinator) Start(ctx context.Context, e domain.Event) error {
}
s.HerdrID = p.HarnessID
s.TaskFileSHA = taskFileSHA
// Best-effort, same caveat as taskFileSHA above: only meaningful for a
// worktree this process can read locally. Snapshots the shared-docs
// state this session starts trusting; checkConventions notices drift
// from here, not from whatever the agent's own cached view is (§6.3).
s.ConventionsHash, _ = continuity.ConventionsHash(w)
c.mu.Lock()
if c.sessions == nil {
c.sessions = map[string]herdr.Session{}