Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 35 additions & 5 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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_<context>_<timestamp>.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
Expand All @@ -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")

Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 12 additions & 4 deletions internal/agent/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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)
}
}

Expand Down
13 changes: 13 additions & 0 deletions internal/agent/templates/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
```
13 changes: 13 additions & 0 deletions src/md/STATE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
```
14 changes: 7 additions & 7 deletions test/sync-agents.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

# --------------------------------------------------------------------------
Expand Down
Loading