---
name: ao-state
description: "Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently."
category: core
invokes: [ao-git, ao-usage]
invoked_by: [all-skills]
state_files:
  read: [constitution.md, memory.md, focus.json, issues/events.jsonl, issues/active.jsonl, baseline.md, time-tracking.jsonl]
  write: [focus.json, issues/events.jsonl, memory.md, time-tracking.jsonl]
---

# AO State Discipline

**Uses `ao` CLI for all issue operations and all `focus.json` writes.** Direct file operations only for `constitution.md`, `memory.md`, and `baseline.md`.

> ⛔ **`focus.json` MUST be updated via `ao focus` CLI subcommands — NEVER edit it directly.**
> Do not use Python, `json.dump`, shell writes, or text editors to modify `focus.json`.

## State File Operations

All state files are managed as follows:

| File | Operations |
|------|-----------|
| `.agent/ops/constitution.md` | Read/write directly |
| `.agent/ops/focus.json` | **`ao focus` CLI only** — NEVER edit directly (see below) |
| `.agent/ops/memory.md` | Read/write directly |
| `.agent/ops/baseline.md` | Read/write directly |
| `.agent/ops/issues/events.jsonl` | Append via `ao` CLI only — never edit directly |
| `.agent/ops/issues/active.jsonl` | Read via `ao ls` / `ao issue show` — rebuilt by `ao rebuild` |
| `.agent/ops/time-tracking.jsonl` | Append-only event log (auto-managed) |

### focus.json via `ao focus` CLI (MANDATORY)

> ⛔ **FORBIDDEN:** `python -c "import json; d=json.load(open('...focus.json')); d['doing_now']=...; ..."` or any direct file write.

| What to update | Command |
|----------------|---------|
| Set active issue (doing_now) | `ao focus set-doing ISSUE_ID --task "desc" --confidence normal` |
| Set next step | `ao focus set-next "what to do next"` |
| Record completion | `ao focus set-just-did "summary of what was done"` |
| Update iteration tracking | `ao focus iteration --increment --issues "ID1,ID2" --confidence-mix "3 normal"` |
| Rebuild queue arrays | `ao focus sync` |
| Show current focus | `ao focus` |

### Issue Management via ao CLI
| Show issue details | `ao issue show ISSUE_ID` |
| Create issue | pipe IssueCreateInput JSON to `ao issue add --in -` |
| Update issue status | `ao issue patch ISSUE_ID --in - <<<'{"set":{"status":"in_progress"}}'` |
| Close issue | `ao issue close ISSUE_ID --log "Done" --status done` |
| Get summary | `ao ls` |
| Rebuild active store | `ao rebuild` |

### Git Status

```bash
# Check current branch
git branch --show-current

# Check for uncommitted changes
git status --porcelain

# Get last commit hash
git rev-parse --short HEAD
```

## When to Use
- At the start of a session/response
- After any meaningful step (analysis/plan decision/implementation/test run)
- When adding/updating issues
- Before concluding a response

## Session Start

At session start:
1. **Check for staleness**: Delegate to `ao-git` stale detection
2. **Read state files** in order (see below)
3. **Validate issue dependencies** before starting work

## Required Reads (in this order)
1) `.agent/ops/constitution.md` — includes default confidence policy
2) `.agent/ops/focus.json` — includes current issue's confidence value
3) `.agent/ops/memory.md`
4) `.agent/ops/time-tracking.jsonl` — check for active session heartbeat
5) `ao ls --limit 10` — quick overview of active issues (or `ao ls priority:high` when working on high-priority)
6) `ao issue show ISSUE_ID` — full issue details when actively working on an issue
7) `.agent/ops/baseline.md` (if present)

## Required Updates

### `.agent/ops/focus.json`
Must always contain:
- `session_info`: last_updated timestamp, branch, last_commit
- `just_did`: what changed since last update
- `doing_now`: current objective + issue_id (or null), confidence, task
- `iteration_tracking`: current_iteration, issues_in_iteration, confidence_mix, iteration_started
- `next`: single best next step + prerequisites/unknowns
- `in_progress`: list of issue IDs (strings) currently in progress
- `next_queue`: list of issue IDs (strings) ready to work on (up to 10)
- `recent_work`: list of issue IDs (strings) for last 5 completed issues

> **IDs only**: Use `ao focus` to read live-resolved issue details.

> ⛔ **Write via CLI only**: Never write `focus.json` directly. Use `ao focus set-doing`, `set-next`, `set-just-did`, `iteration`, or `sync`.

**Confidence Context (REQUIRED):**
When reading issues/focus, always identify the active issue's `confidence:` value:
- **Low confidence** → 1 issue per iteration, mandatory baseline, hard review gate
- **Normal confidence** → 2-3 issues per iteration, soft gates
- **High confidence** → batch OK, minimal gates

If issue has no `confidence:` field, inherit from constitution's default.

### `.agent/ops/issues/events.jsonl`
- Append-only event log — all mutations go through `ao` CLI
- Never edit directly; run `ao rebuild` after any manual fix
- Dependencies: check `ao issue show ISSUE_ID` for `depends_on` field

### `.agent/ops/memory.md`
- Only store durable learnings (workflow rules, stable conventions)
- Do not duplicate constitution content

### `.agent/ops/time-tracking.jsonl`
- Automatic time tracking for all skill invocations
- Tracks sessions with 5-minute timeout between commands
- Associates time with issues/epics/commands automatically

## Time-Tracking (Automatic)

The ao-state skill automatically tracks time spent on commands without manual intervention.

### Storage Location
`.agent/ops/time-tracking.jsonl` — append-only, one JSON event per line.

### Event Schema

```json
{"event_id": "<uuid>", "ts": "<ISO8601>", "op": "<session_start|session_heartbeat|session_end>", "payload": {...}}
```

| Op | Payload fields |
|----|---------------|
| `session_start` | `issue_id`, `epic`, `confidence`, `command` |
| `session_heartbeat` | `issue_id` |
| `session_end` | `issue_id`, `duration_minutes`, `epic`, `command` |

Aggregated totals (by_epic, by_issue, by_command) are computed at read-time by scanning `session_end` events — no stored summary object.

### Session Logic

**Session Start** (when no active session or >5min since last heartbeat):
1. Generate new event_id (UUID)
2. Append `session_start` event with current timestamp
3. Extract context (issue_id, epic, confidence) from focus.json
4. Set command to the skill being invoked (e.g., "ao-implement")

**Heartbeat** (every ao-state call AND at each phase transition):
1. Append `session_heartbeat` event with current timestamp and issue_id

**Context Change** (issue switch within a batch):
Append `session_end` for the old issue, then `session_start` for the new issue.

**Session End** (when >5min since last event or on explicit close):
1. Calculate duration = now - start_time (in minutes)
2. Append `session_end` event with duration_minutes

Example bootstrap check: scan last N events in `time-tracking.jsonl` for a `session_start` without a matching `session_end` — if found with heartbeat <5min ago, session is active.

### Context Extraction

Extract context using these commands:

```bash
# Extract issue ID from focus.json (doing_now.issue_id)
issue_id=$(python3 -c "import json; d=json.load(open('.agent/ops/focus.json')); print(d.get('doing_now',{}).get('issue_id','') or '')" 2>/dev/null || echo "")

# Extract confidence from focus.json (doing_now.confidence)
confidence=$(python3 -c "import json; d=json.load(open('.agent/ops/focus.json')); print(d.get('doing_now',{}).get('confidence','normal'))" 2>/dev/null || echo "normal")

# Extract epic from issue (if issue_id found)
if [ -n "$issue_id" ]; then
  epic=$(uv run ao --format json issue show "$issue_id" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('epic',''))" || echo "")
fi
```

### Implementation Notes

- **Timeout**: 5 minutes (300 seconds) of inactivity ends the session
- **Duration calculation**: Round to 2 decimal places: `echo "scale=2; $duration_seconds / 60" | bc`
- **Time format**: Use ISO 8601 with timezone: `date -u +"%Y-%m-%dT%H:%M:%SZ"`
- **UUID generation**: `uuidgen` or Python fallback
- **Aggregation**: Scan `session_end` events at read-time to compute totals

### Auto-Update Issue Time Fields

When closing a session (after 5min timeout), automatically update the associated issue's time fields:

```bash
# After session ends, compute total time from session_end events
if [ -n "$issue_id" ]; then
  # Aggregate duration_minutes from all session_end events for this issue
  total_time=$(grep '"session_end"' .agent/ops/time-tracking.jsonl \
    | python3 -c "
import sys, json
total = 0
for line in sys.stdin:
  e = json.loads(line)
  if e.get('payload',{}).get('issue_id','') == '$issue_id':
    total += e.get('payload',{}).get('duration_minutes', 0)
print(round(total, 2))
")
  # Update issue via ao patch using the computed total
fi
```

**Issue Time Fields to Update:**

| Field | When to Set | Value |
|-------|-------------|-------|
| `started_at` | First session for this issue | First session's start_time (format: "YYYY-MM-DD HH:MM") |
| `completed_at` | When issue status → done | Last session's end_time (format: "YYYY-MM-DD HH:MM") |
| `duration_minutes` | Every session end | Cumulative total from summary.by_issue[issue_id] |

**Update Pattern:**

```yaml
# Time Tracking (auto-populated)
started_at: "2026-02-02 14:35"       # Set on first session
completed_at: ""                     # Set when status: done
duration_minutes: 145                # Updated every session end
```

## Dependency Validation

Before starting an issue:
1. Run `ao issue show ISSUE_ID`
2. Check `depends_on` field
3. For each dependency ID: `ao issue show DEP_ID`
4. Check each dependency's `status`
5. If any dependency is not `done`/`cancelled`/`dropped`: note issue as `blocked`
6. Only proceed if all dependencies satisfied

## Templates
- [focus template](./templates/focus.template.json)
- [memory template](./templates/memory.template.md)
