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:
@@ -476,3 +476,76 @@ func TestRotationRequestsHandoffBeforeReleasing(t *testing.T) {
|
||||
t.Fatal("rotate never called Release once the handoff file appeared")
|
||||
}
|
||||
}
|
||||
|
||||
type specWorktrees struct{ wtPath, repoPath string }
|
||||
|
||||
func (w specWorktrees) Create(context.Context, domain.Task) (string, error) { return w.wtPath, nil }
|
||||
func (w specWorktrees) Spec(domain.Task) (string, string, bool) { return w.repoPath, "", true }
|
||||
|
||||
type conventionsAdapter struct {
|
||||
fakeAdapter
|
||||
notifications int
|
||||
}
|
||||
|
||||
func (a *conventionsAdapter) NotifyConventionsChanged(context.Context, herdr.Session) error {
|
||||
a.notifications++
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConventionsDriftNotifiesActiveSession guards §6.3's wiring: "on
|
||||
// update, the orchestra injects a notice to agents whose current task is
|
||||
// adjacent" — never left to the agent's own cached view. A session must not
|
||||
// be notified while the base repo's shared docs match what it started with,
|
||||
// and must be notified once they diverge.
|
||||
func TestConventionsDriftNotifiesActiveSession(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
worktree := t.TempDir()
|
||||
if err := os.WriteFile(repo+"/AGENTS.md", []byte("v1"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(worktree+"/AGENTS.md", []byte("v1"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s, err := store.Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{
|
||||
"source": "jsonl", "external_id": "1", "project": "p",
|
||||
})}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := s.Tasks()[0]
|
||||
a := &conventionsAdapter{fakeAdapter: fakeAdapter{occupancy: 0}}
|
||||
c := &orchestrator.Coordinator{Store: s, Worktrees: specWorktrees{wtPath: worktree, repoPath: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"}
|
||||
|
||||
leaseEvt, err := s.Lease(task.ID, "h1", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Start(context.Background(), leaseEvt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go c.Monitor(ctx, .8, time.Millisecond)
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if a.notifications != 0 {
|
||||
t.Fatalf("notified with no actual drift: notifications=%d", a.notifications)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(repo+"/AGENTS.md", []byte("v2"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) && a.notifications == 0 {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if a.notifications == 0 {
|
||||
t.Fatal("session was never notified of the conventions-doc update")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user