diff --git a/docs/commands/index.md b/docs/commands/index.md index 01a7504..d10143c 100644 --- a/docs/commands/index.md +++ b/docs/commands/index.md @@ -10,7 +10,10 @@ Regenerates `AGENTS.md` from the contents of `.agents/`. content. Plans and specs are listed **recursively**, so documents grouped per effort in subdirectories (`plans/auth-effort/rollout.md`) index correctly. -- **State** — `STATE_*.md` snapshots. +- **State** — a pointer to the state convention rule + (`.agents/rules/state.md`). `STATE_*.md` snapshots are per-engineer + working files and are **not** listed; a snapshot that represents a + shared task opts into the index with `shared: true` frontmatter. - **Inherits** — preserved verbatim across regenerations. - **Managed Claude import block** — `@`-imports for passive rules (and explicitly-passive workflows) so Claude actually loads them; fully diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a39c9db..13e1bce 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1583,13 +1583,26 @@ func (a *App) generateAgentsMD() { } } - // State + // State — STATE_*.md snapshots are per-engineer working files, so + // the index must not enumerate them: that leaks one engineer's + // scratch state into the shared, committed AGENTS.md and churns it + // on every regeneration. The section is a pointer to the state + // convention rule; a snapshot appears here only when it opts in as + // a shared task via `shared: true` frontmatter (mirrors the + // `import: true` opt-in for reference docs). b.WriteString("## State\n\n") + b.WriteString("Follow [rules/state.md](.agents/rules/state.md): record progress in `.agents/STATE__.md` snapshots. Snapshots are per-engineer and not indexed unless marked `shared: true` in frontmatter.\n") hasState := false if entries, err := os.ReadDir(agentsDir); err == nil { for _, entry := range entries { name := entry.Name() if strings.HasPrefix(name, "STATE_") && strings.HasSuffix(name, ".md") { + if !stateSnapshotIsShared(filepath.Join(agentsDir, name)) { + continue + } + if !hasState { + b.WriteString("\n### Shared\n\n") + } baseName := strings.TrimSuffix(name, ".md") b.WriteString(fmt.Sprintf("- [%s](.agents/%s)\n", baseName, name)) hasState = true @@ -1598,11 +1611,11 @@ func (a *App) generateAgentsMD() { } legacyState := filepath.Join(agentsDir, "STATE.md") if _, err := os.Stat(legacyState); err == nil { + if !hasState { + b.WriteString("\n### Shared\n\n") + hasState = true + } b.WriteString("- [STATE.md](.agents/STATE.md)\n") - hasState = true - } - if !hasState { - b.WriteString("_No state snapshots yet. Agents will create STATE_*context*_*timestamp*.md files as they work._\n") } b.WriteString("\n") @@ -1785,6 +1798,23 @@ func indexEntry(name, link, srcPath string) string { return fmt.Sprintf("- [%s](%s)\n", name, link) } +// stateSnapshotIsShared reports whether a STATE_*.md snapshot opts +// into the AGENTS.md index as a shared task via `shared: true` +// frontmatter. Snapshots are per-engineer by default and stay out of +// the shared index. +func stateSnapshotIsShared(path string) bool { + raw, err := os.ReadFile(path) + if err != nil { + return false + } + block, err := parseFMBlock(string(raw)) + if err != nil || !block.present { + return false + } + v, _ := block.get("shared") + return strings.EqualFold(strings.TrimSpace(v), "true") +} + // isBucketActive reports whether the target with the given ID is in // ActiveTargets. Used to gate hooks merge/local-sync operations. func (a *App) isBucketActive(targetID string) bool { diff --git a/internal/agent/index_test.go b/internal/agent/index_test.go index 5b9fbe3..be8fc5a 100644 --- a/internal/agent/index_test.go +++ b/internal/agent/index_test.go @@ -467,8 +467,10 @@ func TestCmdIndex_WithStateSnapshots(t *testing.T) { os.MkdirAll(filepath.Join(dir, ".agents", "rules"), 0o755) os.WriteFile(filepath.Join(dir, ".agents", "rules", "test.md"), []byte("body"), 0o644) - // Add a state snapshot. - os.WriteFile(filepath.Join(dir, ".agents", "STATE_test_2026-07-02.md"), []byte("state content"), 0o644) + // Per-engineer snapshot (no frontmatter) must stay out of the index. + os.WriteFile(filepath.Join(dir, ".agents", "STATE_private_2026-07-02.md"), []byte("state content"), 0o644) + // Shared-task snapshot opts in via frontmatter. + os.WriteFile(filepath.Join(dir, ".agents", "STATE_shared_2026-07-02.md"), []byte("---\nshared: true\n---\n\nstate content"), 0o644) var buf bytes.Buffer app := &App{ @@ -485,8 +487,14 @@ func TestCmdIndex_WithStateSnapshots(t *testing.T) { data, _ := os.ReadFile(filepath.Join(dir, "AGENTS.md")) content := string(data) - if !strings.Contains(content, "STATE_test") { - t.Errorf("state snapshot not indexed:\n%s", content[:500]) + if strings.Contains(content, "STATE_private") { + t.Errorf("per-engineer state snapshot leaked into index:\n%s", content) + } + if !strings.Contains(content, "STATE_shared") { + t.Errorf("shared state snapshot not indexed:\n%s", content) + } + if !strings.Contains(content, "rules/state.md") { + t.Errorf("state section missing pointer to rules/state.md:\n%s", content) } } diff --git a/internal/agent/templates/state.md b/internal/agent/templates/state.md index 46c07d4..e2a2022 100644 --- a/internal/agent/templates/state.md +++ b/internal/agent/templates/state.md @@ -22,3 +22,16 @@ Save both if necessary, but the state file is more important for tracking progre Description of current state, progress, blockers, and next steps. Be sure to indicate whats left and what done via - [ ] checkboxes in state file + +## Sharing + +STATE_* snapshots are per-engineer working files. `sync-agents index` does +not list them in AGENTS.md; only the pointer to this rule appears there. +If a snapshot tracks a shared task the whole team should see, opt it into +the index with frontmatter: + +```yaml +--- +shared: true +--- +``` diff --git a/src/md/STATE_TEMPLATE.md b/src/md/STATE_TEMPLATE.md index 46c07d4..e2a2022 100644 --- a/src/md/STATE_TEMPLATE.md +++ b/src/md/STATE_TEMPLATE.md @@ -22,3 +22,16 @@ Save both if necessary, but the state file is more important for tracking progre Description of current state, progress, blockers, and next steps. Be sure to indicate whats left and what done via - [ ] checkboxes in state file + +## Sharing + +STATE_* snapshots are per-engineer working files. `sync-agents index` does +not list them in AGENTS.md; only the pointer to this rule appears there. +If a snapshot tracks a shared task the whole team should see, opt it into +the index with frontmatter: + +```yaml +--- +shared: true +--- +``` diff --git a/test/sync-agents.bats b/test/sync-agents.bats index 6a0eb11..2a93c7f 100644 --- a/test/sync-agents.bats +++ b/test/sync-agents.bats @@ -1344,20 +1344,20 @@ CONF [ -f "$TEST_DIR/.agents/rules/state.md" ] } -@test "AGENTS.md State section lists STATE_ files" { +@test "AGENTS.md State section indexes only shared STATE_ files" { "$SCRIPT" -d "$TEST_DIR" init - # Create some state snapshot files + # Per-engineer snapshot (no frontmatter) must stay out of the index echo "state 1" > "$TEST_DIR/.agents/STATE_feature-work_20260425120000.md" - echo "state 2" > "$TEST_DIR/.agents/STATE_bugfix_20260426080000.md" + # Shared-task snapshot opts in via frontmatter + printf -- '---\nshared: true\n---\n\nstate 2\n' > "$TEST_DIR/.agents/STATE_bugfix_20260426080000.md" "$SCRIPT" -d "$TEST_DIR" index - # AGENTS.md should reference the state files - grep -q "STATE_feature-work_20260425120000" "$TEST_DIR/AGENTS.md" + ! grep -q "STATE_feature-work_20260425120000" "$TEST_DIR/AGENTS.md" grep -q "STATE_bugfix_20260426080000" "$TEST_DIR/AGENTS.md" } -@test "AGENTS.md State section shows placeholder when no state files" { +@test "AGENTS.md State section points at the state convention rule" { "$SCRIPT" -d "$TEST_DIR" init - [[ "$(cat "$TEST_DIR/AGENTS.md")" == *"No state snapshots yet"* ]] + grep -q "rules/state.md" "$TEST_DIR/AGENTS.md" } # --------------------------------------------------------------------------