- name
- state-persistence
- description
- Provides patterns for reading, writing, and recovering AgLoop state from state.json and plan.yaml. Includes checkpoint management and corruption recovery. Use when implementing state management logic or hook scripts.
- disable-model-invocation
- true
- argument-hint
- State operation to perform (read, write, checkpoint, recover)
- tags
- ["agloop","state","json","checkpoint","recovery","persistence"]
# State Persistence
## When to Use This Skill
Load this skill when:
- Reading or writing `.agloop/state.json` (any agent, hook scripts)
- Implementing hook scripts that interact with state files
- Creating or restoring checkpoints (pre-compact hook, session-start hook)
- Recovering from corrupted or missing state
- Appending entries to `.agloop/log.json`
- Reading or validating `.agloop/plan.yaml`
## state.json Schema Reference
The single source of truth for the agentic loop's runtime state. Every agent reads it before acting. Only the coordinator mutates it.
### Field Reference
| Field | Type | Required | Default | Constraints |
| ----------------------------- | ------------ | -------- | ------------------- | ----------------------------------------------------------------------- |
| `$schema` | string | yes | `"agloop-state-v1"` | Literal value |
| `feature_name` | string | yes | `""` | Non-empty after init, max 500 chars |
| `feature_description` | string | no | `""` | Set after research phase |
| `complexity_level` | enum | yes | `"medium"` | `simple \| medium \| complex`. Set during init based on feature scope |
| `current_phase` | enum | yes | `"init"` | `init \| research \| plan \| critique \| execute \| verify \| complete` |
| `iteration` | number | yes | `0` | 0 ≤ n ≤ 3 |
| `current_task_id` | string\|null | yes | `null` | Must reference existing task or null |
| `tasks` | array | yes | `[]` | Populated after plan phase |
| `tasks[].id` | string | yes | — | Format: `task-NNN` |
| `tasks[].title` | string | yes | — | Deliverable-focused |
| `tasks[].description` | string | yes | — | Detailed requirements |
| `tasks[].status` | enum | yes | `"pending"` | `pending \| in_progress \| done \| failed` |
| `tasks[].depends_on` | string[] | yes | `[]` | All referenced IDs must exist |
| `tasks[].acceptance_criteria` | string[] | yes | — | ≥1 criterion per task |
| `tasks[].failure_modes` | object[] | yes | `[]` | Each: mode, likelihood, impact, mitigation |
| `tasks[].result` | object\|null | yes | `null` | Set by executor |
| `tasks[].verification` | object\|null | yes | `null` | Set by verifier |
| `stop_hook_active` | boolean | yes | `false` | Prevents infinite stop-hook loops |
| `last_action` | object | yes | — | Updated on every agent action |
| `last_action.agent` | string | yes | — | Which agent performed the action |
| `last_action.action` | string | yes | — | What was done |
| `last_action.timestamp` | string | yes | — | ISO 8601 |
| `last_action.task_id` | string\|null | yes | — | Related task or null |
| `phase_history` | array | yes | `[]` | Append-only phase transitions |
| `retry_count` | object | yes | `{}` | Keys: task IDs, values: integers ≥ 0 |
| `previous_objections` | array | yes | `[]` | Critic objections from plan↔critique loop. Each: `{ iteration, objection, response, resolved, objection_category, severity }`. Reset on new plan phase |
| `compaction_context` | object | yes | — | Recovery context for compaction survival. Sub-fields: `last_delegation`, `last_result_summary`, `research_digest`, `plan_digest`, `critique_digest`, `pending_decision`, `compaction_count` |
### Task Result Schema
Set by the executor after task completion:
```json
{
"files_modified": ["src/api/search.ts", "src/api/search.test.ts"],
"files_deleted": [],
"acceptance_criteria_confirmation": [
{
"criterion": "GET /api/search returns 200 with results array",
"met": true,
"evidence": "Handler returns res.json({ data: results, meta: { total, page }, error: null })"
}
],
"summary": "Implemented search endpoint with pagination and filtering",
"warnings": [],
"verified": false
}
```
### Task Verification Schema
Set by the verifier after task verification:
```json
{
"verdict": "PASS",
"criteria_results": [
{
"criterion": "GET /api/search returns 200 with results array",
"result": "pass",
"evidence": "Response: { results: [...], total: 42, page: 1 }"
}
],
"specification_compliance": {
"required_patterns": { "compliant": true, "violations": [] },
"forbidden_patterns": { "compliant": true, "violations": [] },
"tech_choices": { "compliant": true, "violations": [] }
},
"overall_summary": "All acceptance criteria met, specification compliant"
}
```
On FAIL, the verifier adds a `failure_details` field with specific failure information.
```
## Read-Modify-Write Pattern
**Every state mutation must follow this exact pattern.** Never read and write without validation.
### Step-by-Step Procedure
```javascript
// 1. READ — Load current state from disk
const raw = await fs.readFile(".agloop/state.json", "utf-8");
// 2. PARSE — Convert to object with error handling
let state;
try {
state = JSON.parse(raw);
} catch (err) {
// State corrupted — attempt checkpoint recovery
return await recoverFromCheckpoint(workspaceFolder);
}
// 3. VALIDATE — Confirm state structure before using
if (!state.$schema || state.$schema !== "agloop-state-v1") {
throw new Error("Invalid state schema version");
}
if (
![
"init",
"research",
"plan",
"critique",
"execute",
"verify",
"complete",
].includes(state.current_phase)
) {
throw new Error(`Invalid phase: ${state.current_phase}`);
}
// 4. MODIFY — Apply changes to the in-memory object
state.current_phase = "execute";
state.current_task_id = "task-001";
state.last_action = {
agent: "agloop",
action: "phase_transition",
timestamp: new Date().toISOString(),
task_id: "task-001",
};
state.phase_history.push({
phase: "execute",
entered_at: new Date().toISOString(),
exited_at: null,
});
// 5. VALIDATE AGAIN — Confirm changes are consistent
if (state.current_task_id) {
const taskExists = state.tasks.some((t) => t.id === state.current_task_id);
if (!taskExists) throw new Error(`Task ${state.current_task_id} not found`);
}
// 6. WRITE ATOMICALLY — Write to temp file, then rename
const tmpPath = ".agloop/state.json.tmp";
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
await fs.rename(tmpPath, ".agloop/state.json");
```
### Critical Rules
- **Never write without reading first** — always base writes on current disk state
- **Double-validate** — validate before modification AND before writing
- **Atomic writes** — write to `.tmp` file then rename to prevent corruption on crash
- **No partial updates** — always write the complete state object
## plan.yaml Schema Reference
Produced by the Planner agent, consumed by Coordinator, Critic, Executor, and Verifier.
### Field Reference
| Field | Type | Required | Constraints |
| ----------------------------------- | ------ | -------- | --------------------------------------------------------------- |
| `plan_id` | string | yes | Format: `plan-YYYYMMDD-HHmmss` |
| `feature` | string | yes | Non-empty, verbatim user request |
| `created_at` | string | yes | ISO 8601 |
| `updated_at` | string | yes | ISO 8601, ≥ created_at |
| `status` | enum | yes | `draft \| under_review \| approved \| in_progress \| completed` |
| `pre_mortem.overall_risk_level` | enum | yes | `low \| medium \| high \| critical` |
| `pre_mortem.critical_failure_modes` | array | yes | ≥1 entry |
| `pre_mortem.assumptions` | array | yes | ≥1 entry |
| `tasks` | array | yes | ≥1 task |
| `tasks[].id` | string | yes | Format: `task-NNN`, unique |
| `tasks[].depends_on` | array | yes | All referenced IDs must exist |
| `tasks[].acceptance_criteria` | array | yes | ≥1 criterion per task |
| `tasks[].estimated_effort` | enum | yes | `small \| medium \| large` |
| `dag_edges` | array | yes | Each edge: `[from_id, to_id]`, both must exist |
| `critique_history` | array | yes | Empty initially |
## log.json Append Pattern
The execution log uses **JSON Lines format** — one JSON object per line, no surrounding array. This enables efficient appending without reading the entire file.
### Append Procedure
```javascript
// CORRECT: Append a single line to log.json (JSON Lines format)
async function appendLog(logPath, entry) {
// Ensure required fields
const logEntry = {
timestamp: entry.timestamp || new Date().toISOString(),
agent: entry.agent,
action: entry.action,
task_id: entry.task_id || null,
phase: entry.phase,
input_summary: truncate(entry.input_summary || "", 500),
output_summary: truncate(entry.output_summary || "", 500),
status: entry.status || "success",
...(entry.metadata ? { metadata: entry.metadata } : {}),
};
// Append as a single line — do NOT overwrite
const line = JSON.stringify(logEntry) + "\n";
await fs.appendFile(logPath, line, "utf-8");
}
// WRONG: Do NOT do this — it reads and rewrites the entire file
// const log = JSON.parse(await fs.readFile(logPath));
// log.push(entry);
// await fs.writeFile(logPath, JSON.stringify(log));
```
### Log Entry Fields
| Field | Type | Required | Constraints |
| ---------------- | ------------ | -------- | ------------------------------- |
| `timestamp` | string | yes | ISO 8601 with milliseconds |
| `agent` | string | yes | Agent or hook identifier |
| `action` | string | yes | One of the defined action types |
| `task_id` | string\|null | yes | Existing task ID or null |
| `phase` | string | yes | Current phase name |
| `input_summary` | string | yes | Max 500 chars |
| `output_summary` | string | yes | Max 500 chars |
View on GitHub