{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/agent-state.schema.json",
  "title": "Multi-Agent Pipeline  -  agent-state.json",
  "description": "Canonical state file for a single pipeline run. Written to $HOME/.claude/logs/multi-agent/{project}/{task-id}/agent-state.json and read by every phase. Versioned so older runs can be migrated or ignored.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schemaVersion",
    "taskId",
    "shortId",
    "project",
    "branch",
    "baseBranch",
    "currentPhase",
    "status",
    "startedAt",
    "phases",
    "identity"
  ],
  "properties": {
    "schemaVersion": {
      "type": "string",
      "enum": ["2.0.0", "2.1.0"],
      "description": "v2.0.0: single-repo only (scalar project/projectRoot/worktreePath). v2.1.0: adds optional projects[] array for multi-repo tasks  -  single-repo fields remain for backward compat; when projects[] has >1 entries, multi-repo mode is active."
    },
    "taskId": {
      "type": "string",
      "description": "Canonical task id  -  Jira key (PROJ-12345), GitHub issue number (#316), or free-text slug.",
      "minLength": 1
    },
    "shortId": {
      "type": "integer",
      "minimum": 1,
      "description": "Auto-incremented local short id from .worktrees/.multi-agent-counter  -  unique per machine, not per project."
    },
    "jiraId": {
      "type": ["string", "null"],
      "description": "Jira issue key (PROJ-12345) if the task originated from Jira, else null."
    },
    "githubIssue": {
      "type": ["object", "null"],
      "additionalProperties": false,
      "properties": {
        "owner": {
          "type": "string"
        },
        "repo": {
          "type": "string"
        },
        "number": {
          "type": "integer",
          "minimum": 1
        }
      },
      "required": ["owner", "repo", "number"],
      "description": "GitHub issue coordinates if the task originated from GitHub, else null."
    },
    "project": {
      "type": "string",
      "description": "Project slug  -  used to partition logs and knowledge base."
    },
    "projectRoot": {
      "type": "string",
      "description": "Absolute path to the main checkout."
    },
    "worktreePath": {
      "type": ["string", "null"],
      "description": "Absolute path to the task worktree. Null in --local mode."
    },
    "branch": {
      "type": "string",
      "description": "Working branch for this task."
    },
    "baseBranch": {
      "type": "string",
      "description": "PR target branch (e.g. develop, main)."
    },
    "baseBranchSource": {
      "type": "string",
      "enum": ["asked", "input", "remembered", "default"],
      "description": "How baseBranch was decided. asked = the user answered the Step 3 picker; input = it arrived with the task reference; remembered = autopilot took the most recent entry in prefs.global.recentBranches still inside the TTL and still on the remote; default = autopilot fell back to the develop/release/main sort order. An autopilot run cannot be asked anything, so recording which rule fired is what keeps it readable afterwards."
    },
    "remoteType": {
      "type": "string",
      "enum": ["github", "bitbucket"],
      "description": "Determines which PR API and reviewer strategy is used."
    },
    "currentPhase": {
      "type": "integer",
      "minimum": 0,
      "maximum": 7,
      "description": "Last phase the orchestrator entered. Resume re-enters at currentPhase + 1."
    },
    "status": {
      "type": "string",
      "enum": ["in_progress", "paused", "complete", "failed"],
      "description": "Overall task status."
    },
    "startedAt": {
      "type": "string",
      "format": "date-time"
    },
    "finishedAt": {
      "type": ["string", "null"],
      "format": "date-time"
    },
    "haltReason": {
      "type": ["string", "null"],
      "description": "Set when a phase halts on a hard error (validator failed twice, no subagent returned, dispatch error past fallback, lock irrecoverable). Format '<phase>:<cause>'. Surfaced to the user and cleared on successful resume. See operations.md 'Halt visibility'."
    },
    "relatedIssues": {
      "type": "array",
      "maxItems": 20,
      "description": "Jira issues fetched alongside the task at intake: the other sub-tasks under this issue's parent, where a board keeps the analysis and the test scope. NOT the same field as siblings[] below, which is read-only sibling REPOS. Written by the picker bridge from descriptor.relatedIssues; read by Phase 1 as ground truth and by Phase 2's parent-story scope-drift check. Durable on purpose: /multi-agent:resume rebuilds context from artefacts, never from the conversation, so intake-only enrichment would vanish on the first resume. The task's own description and maturity are still NOT persisted here, which is a known asymmetry, not an oversight.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["key", "relation"],
        "properties": {
          "key": { "type": "string" },
          "relation": { "type": "string", "enum": ["sibling"] },
          "type": { "type": "string" },
          "status": { "type": "string" },
          "summary": { "type": "string" },
          "description": { "type": "string" },
          "truncated": { "type": "boolean", "default": false }
        }
      }
    },
    "siblings": {
      "type": "array",
      "maxItems": 10,
      "description": "Repos the dev-context picker offered that this run does not modify: read-only siblings, plus any extra the user selected. Persisted at Phase 0 because the phases that consume them run much later - Phase 4's platform-parity cross-check reads this as the fourth of its four counterpart sources (after --with, prefs.projects[<slug>].counterpartRoots[] and the primary checkout's sibling directories; see multi-agent-refs/platform-parity.md), so a picker result that is not written here is a candidate the check can never see.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "root", "stack"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Repo name as the picker showed it."
          },
          "root": {
            "type": ["string", "null"],
            "description": "Absolute path to the local checkout, or null when the repo is known but not checked out. A null root is skipped by every consumer: nothing clones a repo to review a different one."
          },
          "stack": {
            "type": "string",
            "enum": ["ios", "android", "node", "python", "go", "unknown"],
            "description": "Resolved from the checkout's marker files by the same table Phase 1 Step 2 uses (.xcodeproj/Package.swift -> ios, build.gradle(.kts) -> android, ...). 'unknown' when no marker matched or there is no checkout - never guessed from the repo name."
          },
          "canPush": {
            "type": "boolean",
            "default": false,
            "description": "Carried from the picker. Informational here: a sibling is read-only to Phase 4 regardless."
          }
        }
      }
    },
    "rev": {
      "type": "integer",
      "minimum": 0,
      "description": "Monotonic revision, bumped by write-state.mjs on every successful write. A reader keeps the rev it read and compares before writing back; a changed rev means the record moved underneath it. Optional, so a record written by an earlier version stays valid: treat a missing rev as 0. No schemaVersion bump - the field is additive and nothing needs migrating."
    },
    "pendingSteer": {
      "type": ["object", "null"],
      "additionalProperties": false,
      "required": ["text", "at"],
      "description": "A mid-run instruction from the user, waiting for the next phase boundary to consume it. Written by /multi-agent:steer; a phase consumes it at entry by setting appliedAt, which is what stops it being read again. The record is kept as a trace and overwritten wholesale by the next steer. Never applied mid-phase: changing the target of work already in flight throws that work away.",
      "properties": {
        "text": {
          "type": "string",
          "minLength": 1,
          "maxLength": 4000,
          "description": "The instruction, verbatim as the user typed it."
        },
        "at": {
          "type": "string",
          "format": "date-time",
          "description": "When it was queued."
        },
        "appliedAt": {
          "type": ["string", "null"],
          "format": "date-time",
          "description": "When a phase consumed it. Set alongside clearing the record, so the log keeps the trace."
        },
        "appliedPhase": {
          "type": ["integer", "null"],
          "minimum": 0,
          "maximum": 7,
          "description": "The phase that consumed it."
        }
      }
    },
    "telemetry": {
      "type": "object",
      "additionalProperties": true,
      "description": "Run telemetry. mcpCalls[] is consumed by smoke-no-mcp-in-dev-phases.sh to enforce the 'no MCP outside analysis' contract  -  every mcp__* invocation MUST append an entry tagged with the phase it ran in. skillCalls[] is Phase 3's self-report of which skills, plugin skills and guides it consulted; Phase 4 Step 1.78 reads it as CORROBORATING evidence only and never computes coverage from it.",
      "properties": {
        "mcpCalls": {
          "type": "array",
          "description": "One entry per MCP tool invocation during the run. The gate fails if any entry has phase >= 2.",
          "items": {
            "type": "object",
            "required": ["tool", "phase"],
            "properties": {
              "tool": {
                "type": "string",
                "description": "MCP tool name, e.g. mcp__claude_ai_Figma__get_design_context."
              },
              "phase": {
                "type": "integer",
                "minimum": 0,
                "maximum": 7,
                "description": "Pipeline phase the call ran in. Only 0 (init) and 1 (analysis) are permitted."
              },
              "timestamp": {
                "type": "string",
                "description": "ISO-8601 time of the call."
              }
            }
          }
        },
        "skillCalls": {
          "type": "array",
          "description": "One entry per skill / plugin skill / stack guide consulted while writing code. Append at the moment of consultation, not retrospectively. This is a self-report and shares the known weakness of mcpCalls[]: an unrecorded consultation and no consultation are byte-identical here, so Step 1.78 treats the deterministic resolver as primary, defaults ledger.source to 'derived', and flags a declared skill it cannot bind to a changed file. Recording it still earns its keep - it is the only signal that distinguishes 'the dev phase applied X to the wrong files' from 'X was never opened'.",
          "items": {
            "type": "object",
            "required": ["skill", "phase", "targetFiles"],
            "additionalProperties": true,
            "properties": {
              "skill": {
                "type": "string",
                "minLength": 1,
                "description": "Skill name as invoked, e.g. ios-coding-standard, ai-ios-toolkit:create-component, or a guide path for a stack guide."
              },
              "phase": {
                "type": "integer",
                "minimum": 0,
                "maximum": 7,
                "description": "Phase the consultation happened in. Normally 3."
              },
              "targetFiles": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Files the skill was actually applied to. REQUIRED: without it coverage cannot be attributed, because a skill applied to the wrong files still reads as 'applied'."
              },
              "timestamp": {
                "type": "string",
                "description": "ISO-8601 time of the consultation."
              },
              "routedBy": {
                "type": "string",
                "description": "Set when a stack toolkit's own index skill chose this skill, as '<toolkit>:index@<version>'. Recorded so a finding can be traced to the index version that selected it  -  the skill set differs between plugin versions. Phase 4 Step 1.78 surfaces these separately in the manifest ledger; it does NOT grant them extra trust, because the resolver stays primary either way."
              }
            }
          }
        }
      }
    },
    "autopilot": {
      "type": "boolean",
      "default": false
    },
    "onlyDevelop": {
      "type": "boolean",
      "default": false,
      "description": "Short pipeline  -  phases 1 and 2 are skipped. Set by the Phase 0 Step 7.5 depth picker as of v16.0.0; the key and every reader of it are unchanged. Phase 4 Review still runs (v14.0.0+). Always false in an autopilot run, which never asks the depth question."
    },
    "localMode": {
      "type": "boolean",
      "default": false,
      "description": "--local flag  -  no worktree, direct branch in projectRoot."
    },
    "instructionDriven": {
      "type": "boolean",
      "default": false,
      "description": "A Figma / skill instruction file is steering phases 3-6."
    },
    "mode": {
      "type": ["string", "null"],
      "description": "Pipeline mode for this run (e.g. 'dev', 'local', 'design-check'). Absent = full pipeline."
    },
    "analysis": {
      "type": ["object", "null"],
      "additionalProperties": true,
      "description": "Phase 1 analysis-document outcome. Absent in Short runs, which produce no document by design.",
      "properties": {
        "docStatus": {
          "type": "string",
          "enum": ["produced", "reused", "not-applicable"],
          "description": "Set by Phase 1 Step 4. Phase 2 and Phase 3 pre-flight on it."
        },
        "docPath": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "One emitted path per selected platform."
        },
        "frontMatter": {
          "type": ["object", "null"],
          "additionalProperties": true,
          "description": "Parsed YAML header of the active platform's document."
        }
      }
    },
    "run": {
      "type": ["object", "null"],
      "additionalProperties": true,
      "description": "Cross-phase run bookkeeping that is not a phase record.",
      "properties": {
        "lastAnalysisDigest": {
          "type": ["string", "null"],
          "description": "The evidence_digest the analysis documents were written from, persisted by Phase 1. Phase 3 compares it against the document front-matter; absent means the freshness check is not-verifiable, never fresh."
        },
        "analysisBaseCommit": {
          "type": ["string", "null"],
          "description": "git rev-parse HEAD at analysis emit time. Phase 3 diffs it against HEAD to detect repo drift under a reused spec, which the digest alone cannot see."
        }
      }
    },
    "figmaAccess": {
      "type": ["object", "null"],
      "additionalProperties": true,
      "description": "Resolved Figma ground-truth tier for this run, established in Phase 0 and read by every phase that consumes or verifies a design reference (see the Figma Access Tier rule in multi-agent-refs/rules.md). Absent when the task references no Figma frame.",
      "properties": {
        "tier": {
          "type": "integer",
          "minimum": 1,
          "maximum": 3,
          "description": "1 = Figma MCP, 2 = Figma REST with the `figma` PAT, 3 = user-attached screenshot."
        },
        "tier1Unavailable": {
          "type": ["string", "null"],
          "enum": ["host", "auth", null],
          "description": "Why Tier 1 was not used, when it was not. 'host' = this CLI does not serve the mcp__claude_ai_Figma__* tools (the normal case on Copilot CLI and Codex CLI, since the installer registers only multi-agent-toolkit) and no re-auth was attempted. 'auth' = the tools were served but authentication failed after one retry. The distinction matters downstream: Tier 2 on Codex is routine, Tier 2 on Claude Code points at a dead figma_mcp token worth surfacing."
        },
        "reviewBlocking": {
          "type": "boolean",
          "default": false,
          "description": "Set with tier 3. Phase 4 holds the run at review_blocking until a human signs off on the component choice."
        }
      }
    },
    "designCheck": {
      "type": ["object", "null"],
      "additionalProperties": true,
      "description": "State for a /multi-agent:design-check run. Populated in Phase 0 (mock feasibility, scenario inventory, scope), Phase 2 (Figma variants) and Phase 3 (captures, skips, findings).",
      "properties": {
        "module": {
          "type": "string",
          "description": "Module / submodule path being audited."
        },
        "platform": {
          "type": "string",
          "enum": ["ios", "android"]
        },
        "mock": {
          "type": "object",
          "additionalProperties": true,
          "description": "design_mock_detect result.",
          "properties": {
            "supported": {
              "description": "true | false | 'debug-only'"
            },
            "mechanism": {
              "type": ["string", "null"]
            },
            "activation": {
              "type": ["object", "null"]
            },
            "variantsHint": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "inventory": {
          "type": ["object", "null"],
          "additionalProperties": true,
          "description": "design_scenario_inventory result, persisted verbatim. THIS is the audit's target set  -  Phase 3 iterates it and Phase 4 gates on it. Never re-derive it by reading the repo.",
          "properties": {
            "targetCount": {
              "type": "integer"
            },
            "targets": {
              "type": "array",
              "items": {
                "type": "object",
                "additionalProperties": true
              },
              "description": "{ id, kind, label, screen, driver, evidence } per state driver (launch-arg / scenario-case / code-scenario / fixture / deep-link)."
            },
            "groups": {
              "type": "array",
              "items": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "byKind": {
              "type": "object",
              "additionalProperties": true
            },
            "truncated": {
              "type": "boolean"
            }
          }
        },
        "scope": {
          "type": ["object", "null"],
          "additionalProperties": true,
          "description": "Resolved run scope: the inventory targets this run must audit. The coverage gate applies to this set, not to the whole inventory  -  a scoped run is not penalised for out-of-scope targets.",
          "properties": {
            "argument": {
              "type": ["string", "null"],
              "description": "Raw $ARGUMENTS as given (empty = whole module, '--resume' = remainder of the last run)."
            },
            "targetIds": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "resumedFrom": {
              "type": ["string", "null"],
              "description": "Report dir of the run this scope resumes, when --resume was used."
            }
          }
        },
        "figmaUrl": {
          "type": ["string", "null"]
        },
        "variants": {
          "type": "array",
          "description": "Mapped variants with their Figma spec, capture, and findings.",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "captured": {
          "type": "array",
          "description": "Every capture taken, appended and persisted as it happens so a dying run is resumable. One entry per screenshot, keyed to the target that produced it ('<targetId>', or '<targetId>#<sub-label>' for tap-reachable sub-states).",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "skipped": {
          "type": "array",
          "description": "Targets that could not be audited, each with a CONCRETE reason. 'Requires a scenario / prefix / launch-arg' is not a reason  -  that is the work Phase 3 exists to do. A skip without a reason fails the coverage gate.",
          "items": {
            "type": "object",
            "additionalProperties": true,
            "required": ["id", "reason"],
            "properties": {
              "id": {
                "type": "string"
              },
              "group": {
                "type": ["string", "null"]
              },
              "reason": {
                "type": "string",
                "minLength": 1
              }
            }
          }
        },
        "coverage": {
          "type": ["object", "null"],
          "additionalProperties": true,
          "description": "design_report coverage verdict for this run. gate='fail' means the run is INCOMPLETE and must be reported as such, with the unaccounted ids and the --resume command.",
          "properties": {
            "gate": {
              "type": "string",
              "enum": ["pass", "fail"]
            },
            "audited": {
              "type": "integer"
            },
            "target": {
              "type": "integer"
            },
            "pct": {
              "type": "number"
            },
            "skippedWithReason": {
              "type": "integer"
            },
            "unaccounted": {
              "type": "integer"
            },
            "unaccountedIds": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "reportDir": {
          "type": ["string", "null"],
          "description": "Output dir under ~/DesignChecks."
        }
      }
    },
    "identity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "email"],
      "properties": {
        "name": {
          "type": "string"
        },
        "email": {
          "type": "string",
          "format": "email"
        },
        "username": {
          "type": "string",
          "description": "SCM username (GitHub/Bitbucket). Used to filter out PR author from reviewer list."
        }
      }
    },
    "phases": {
      "type": "object",
      "description": "Per-phase status + outputs. Keys are phase numbers as strings (\"0\"..\"7\").",
      "patternProperties": {
        "^[0-7]$": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "status": {
              "type": "string",
              "enum": ["pending", "in_progress", "done", "skipped", "failed"]
            },
            "startedAt": {
              "type": "string",
              "format": "date-time"
            },
            "finishedAt": {
              "type": "string",
              "format": "date-time"
            },
            "model": {
              "type": "string"
            },
            "files": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Files produced or modified by this phase. Used for semantic revert."
            },
            "retryCount": {
              "type": "integer",
              "minimum": 0,
              "maximum": 3
            },
            "subStep": {
              "type": "string",
              "description": "Sub-step checkpoint for long phases (3, 7) so resume re-does only the unfinished tail instead of the whole phase. Short token, e.g. 'red'|'green'|'build'|'pr-opened'|'confluence-synced'. See operations.md 'Sub-step checkpoints'."
            },
            "notes": {
              "type": "string"
            },
            "clarificationRounds": {
              "type": "integer",
              "minimum": 0,
              "maximum": 2,
              "description": "v5.3.0 Phase 2 Plan Approval Gate  -  number of clarification question/answer rounds the orchestrator ran before producing the first plan. Cap 2; hitting the cap produces a 'best-effort' plan instead of asking more questions. Only emitted on a Full interactive run (a Short run has no plan, autopilot may not ask)."
            },
            "clarificationQuestions": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "v5.3.0 Phase 2  -  ordered log of questions the orchestrator asked the user when the issue description was ambiguous (missing acceptance criteria, vague language, missing Figma/endpoint links, unclear scope vs. parent story)."
            },
            "clarificationAnswers": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "v5.3.0 Phase 2  -  ordered log of user answers aligned with clarificationQuestions (same length)."
            },
            "planIterations": {
              "type": "integer",
              "minimum": 1,
              "description": "v5.3.0 Phase 2 Plan Approval Gate  -  how many plan revisions the user requested before approving. 1 = approved on first show; N = user sent N-1 free-text edit requests and the plan was re-rendered each time."
            },
            "planApprovedAt": {
              "type": ["string", "null"],
              "format": "date-time",
              "description": "v5.3.0 Phase 2 Plan Approval Gate  -  timestamp when the user approved the plan. Null when the gate is not applicable (Short run, or autopilot) or the task was aborted at the gate."
            },
            "planEditRequests": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "v5.3.0 Phase 2  -  free-text edit instructions the user typed between plan renders. Preserved verbatim for audit; the planning model (Fable top tier) parses them conversationally to revise the plan."
            }
          }
        }
      }
    },
    "baseline": {
      "type": "object",
      "additionalProperties": true,
      "description": "Pre-work state of the repo, captured in Phase 0. Exists so Phase 4 can tell an inherited failure from one this run caused.",
      "properties": {
        "tests": {
          "type": "object",
          "additionalProperties": true,
          "description": "Outcome of the Phase 0 baseline test run (gated by prefs.global.testBaseline.enabled). status is three-valued on purpose: collapsing unknown into green would let a skipped baseline read as a clean tree, which is the failure this whole record exists to prevent.",
          "properties": {
            "status": {
              "type": "string",
              "enum": ["green", "red", "unknown"],
              "description": "green = the suite passed before any change. red = it did not. unknown = no test command, the time cap was hit, or the baseline was disabled."
            },
            "failing": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Identifiers of tests already failing before this run. Empty on a red status means the output could not be parsed into names: the evidence is then the logPath alone, and Phase 4 reports 'inherited red, not attributable' rather than inventing a set."
            },
            "logPath": {
              "type": "string",
              "description": "Path to the tee'd baseline log. The evidence behind the status; cited when failing[] could not be parsed."
            },
            "capturedAt": {
              "type": "string",
              "format": "date-time"
            },
            "command": {
              "type": "string",
              "description": "The exact test command that produced this baseline. Phase 4 compares against its own Gate 3 command and treats a mismatch as unknown."
            }
          }
        }
      }
    },
    "reviewIterations": {
      "type": "array",
      "items": {
        "type": "object",
        "description": "Per-iteration review record. It accretes fields across phases - summary counts in Phase 4, plus the merged reviewers[] and triage{} detail and validatorResult that run-metrics.mjs and Phase 7 read - so extra keys are allowed.",
        "additionalProperties": true,
        "properties": {
          "iteration": {
            "type": "integer",
            "minimum": 1
          },
          "blocking": {
            "type": "integer",
            "minimum": 0
          },
          "important": {
            "type": "integer",
            "minimum": 0
          },
          "suggestion": {
            "type": "integer",
            "minimum": 0
          },
          "decision": {
            "type": "string",
            "enum": ["fix", "accept", "escalate"]
          },
          "delta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Written by Phase 4 Step 3.8 from review-delta.mjs: how this round's accepted findings relate to the previous round's rework mandate. Absent on iteration 1 and on runs from before the delta existed.",
            "properties": {
              "previousIteration": {
                "type": "integer",
                "minimum": 1
              },
              "new": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "fingerprint": {
                      "type": "string"
                    },
                    "severity": {
                      "type": ["string", "null"]
                    },
                    "file": {
                      "type": ["string", "null"]
                    },
                    "issue": {
                      "type": ["string", "null"]
                    }
                  }
                }
              },
              "stillPresent": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "fingerprint": {
                      "type": "string"
                    },
                    "severity": {
                      "type": ["string", "null"]
                    },
                    "file": {
                      "type": ["string", "null"]
                    },
                    "issue": {
                      "type": ["string", "null"]
                    }
                  }
                }
              },
              "resolved": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "fingerprint": {
                      "type": "string"
                    },
                    "severity": {
                      "type": ["string", "null"]
                    },
                    "file": {
                      "type": ["string", "null"]
                    },
                    "issue": {
                      "type": ["string", "null"]
                    }
                  }
                }
              },
              "downgraded": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "fingerprint": {
                      "type": "string"
                    },
                    "severity": {
                      "type": ["string", "null"]
                    },
                    "file": {
                      "type": ["string", "null"]
                    },
                    "issue": {
                      "type": ["string", "null"]
                    }
                  }
                }
              },
              "stillPresentBlocking": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "fingerprint": {
                      "type": "string"
                    },
                    "severity": {
                      "type": ["string", "null"]
                    },
                    "file": {
                      "type": ["string", "null"]
                    },
                    "issue": {
                      "type": ["string", "null"]
                    }
                  }
                }
              },
              "recurrence": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer",
                  "minimum": 1
                },
                "description": "fingerprint -> consecutive rework cycles the finding has survived."
              },
              "plateau": {
                "type": "boolean",
                "description": "The still-present set is unchanged from the previous delta and non-empty."
              },
              "tripped": {
                "type": "boolean"
              },
              "computedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "reviewers": {
            "type": "array",
            "description": "One entry per reviewer dispatch that RETURNED. Typed because two consumers depend on the shape: anonymize-findings.mjs needs model+findings to build the label map, and run-metrics.mjs reports acceptedRatio per reviewer. Extra keys are allowed; nothing is required, so a run written before this shape existed still validates and surfaces as model \"unknown\" rather than failing.",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "properties": {
                "model": {
                  "type": "string",
                  "description": "The model that produced this review (fable | sonnet | opus | gpt-*). Absent is not the same as unknown-by-name: an entry with no model is reported as \"unknown\" in the per-reviewer metric instead of being folded into another reviewer's count."
                },
                "findings": {
                  "type": "array",
                  "description": "Raw findings from this reviewer, before triage."
                },
                "roundCount": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "1 normally, 2 when the Step 2.5 rebuttal round replaced this reviewer's output."
                }
              }
            }
          },
          "anonymizationMap": {
            "type": "object",
            "additionalProperties": true,
            "description": "Written by Phase 4 Step 3.0 from anonymize-findings.mjs --map, read by run-metrics.mjs to attribute accepted findings back to a reviewer. Declared because the phase doc names it and a consumer reads it; absent on runs from before anonymization existed, which run-metrics reports as perReviewerAttribution \"unavailable\" rather than as a zero. Never goes into a prompt: it is the mapping the anonymization exists to withhold.",
            "properties": {
              "seed": {
                "type": "string",
                "description": "taskId:iteration - the seed that produced the finding order, so a resume reproduces it."
              },
              "labelToModel": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "Source A|B|C -> model name. \"unknown\" for a reviewer entry that declared no model."
              }
            }
          },
          "triage": {
            "type": "object"
          },
          "accepted": {
            "type": "array"
          },
          "validatorResult": {}
        }
      }
    },
    "circuitBreaker": {
      "type": "object",
      "additionalProperties": false,
      "description": "Autopilot circuit-breaker record (refs/features/autopilot-circuit-breaker.md). Written only when a trigger trips: trigger 2 by Phase 4 Step 3.8 (a mandate finding survived identicalFindingCycles rework cycles), trigger 3 by the Phase 3 re-entry hard-kill. /multi-agent:resume clears tripped and keeps counters.",
      "required": ["tripped"],
      "properties": {
        "tripped": {
          "type": "boolean"
        },
        "trigger": {
          "type": ["integer", "null"],
          "minimum": 1,
          "maximum": 5
        },
        "detail": {
          "type": "string"
        },
        "checkpoint": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "phase": {
              "type": "integer",
              "minimum": 0,
              "maximum": 7
            },
            "step": {
              "type": "string"
            },
            "iteration": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "trippedAt": {
          "type": "string",
          "format": "date-time"
        },
        "counters": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "identicalFindingCycles": {
              "type": "integer",
              "minimum": 0
            },
            "reworkCycles": {
              "type": "integer",
              "minimum": 0
            }
          }
        }
      }
    },
    "diffRisk": {
      "type": "object",
      "additionalProperties": true,
      "description": "Totals from diff-risk-score.mjs, persisted by Phase 4 Step 1.75 so Phase 6 (PR risk section), Phase 7 and run-metrics.mjs read the same numbers the review scope decision used.",
      "properties": {
        "files": {
          "type": "integer",
          "minimum": 0
        },
        "loc_added": {
          "type": "integer",
          "minimum": 0
        },
        "loc_removed": {
          "type": "integer",
          "minimum": 0
        },
        "max_score": {
          "type": "number"
        },
        "signals": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Distinct signal names seen across files (security_path, migration, public_api, no_test_change, test_lines_removed, ...)."
        }
      }
    },
    "confluenceSpace": {
      "type": ["string", "null"],
      "description": "Cached Confluence space key for the project  -  avoids re-asking on every run."
    },
    "confluenceParentId": {
      "type": ["string", "null"],
      "description": "Cached Confluence parent page id."
    },
    "pr": {
      "type": ["object", "null"],
      "additionalProperties": false,
      "properties": {
        "number": {
          "type": "integer",
          "minimum": 1
        },
        "url": {
          "type": "string",
          "format": "uri"
        },
        "version": {
          "type": "integer",
          "minimum": 0,
          "description": "Bitbucket PR version  -  must increment by 1 per PUT."
        },
        "draft": {
          "type": "boolean"
        },
        "reviewers": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "commit": {
      "type": ["object", "null"],
      "additionalProperties": false,
      "properties": {
        "sha": {
          "type": "string",
          "minLength": 7
        },
        "message": {
          "type": "string"
        }
      }
    },
    "projects": {
      "type": "array",
      "description": "v2.1.0+. Per-repo state for multi-repo tasks. When present and len > 1, multi-repo mode is active; the scalar project/projectRoot/worktreePath fields at top level refer to the primary (first) repo. Each entry mirrors the top-level single-repo fields plus its own identity and commit.",
      "minItems": 1,
      "maxItems": 10,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["project", "projectRoot", "branch", "baseBranch", "identity"],
        "properties": {
          "project": {
            "type": "string"
          },
          "projectRoot": {
            "type": "string"
          },
          "worktreePath": {
            "type": ["string", "null"]
          },
          "branch": {
            "type": "string"
          },
          "baseBranch": {
            "type": "string"
          },
          "remoteType": {
            "type": "string",
            "enum": ["github", "bitbucket", "gitlab", "generic-git"]
          },
          "identity": {
            "type": "object",
            "additionalProperties": false,
            "required": ["name", "email"],
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "username": {
                "type": "string"
              }
            }
          },
          "platform": {
            "type": "string",
            "enum": ["bitbucket", "github", "gitlab", "generic-git"],
            "description": "Host platform for this repo (derived from origin URL)."
          },
          "commit": {
            "type": ["object", "null"],
            "additionalProperties": false,
            "properties": {
              "sha": {
                "type": "string",
                "minLength": 7
              },
              "message": {
                "type": "string"
              }
            }
          },
          "pr": {
            "type": ["object", "null"],
            "additionalProperties": false,
            "properties": {
              "number": {
                "type": "integer",
                "minimum": 1
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "draft": {
                "type": "boolean"
              }
            }
          },
          "pushAttempts": {
            "type": "integer",
            "minimum": 0,
            "description": "v2.1.0+ push-must-succeed retry counter. Increments per rebase-retry."
          },
          "buildStatus": {
            "description": "Latest Phase 3 build outcome for this repo. A string in early phases ('pending'/'green'/'passed'/'failed') or null before any attempt; once Phase 3 runs a build it becomes a {ok, attempts, lastError} object (see phase-3-dev.md). run-metrics.mjs reads both the string and object forms.",
            "anyOf": [
              {
                "type": "string",
                "enum": ["pending", "passed", "failed", "green"]
              },
              {
                "type": "null"
              },
              {
                "type": "object",
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "attempts": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "lastError": {
                    "type": ["string", "null"]
                  }
                }
              }
            ]
          }
        }
      }
    },
    "worktreeRemovedAt": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "Set when Phase 6 removed the worktree after opening the PR. Its presence is what tells :resume, :status and :log that a worktree-less task is finished-and-tidied rather than broken  -  without it a missing worktree is indistinguishable from a killed run."
    },
    "artifactsPath": {
      "type": ["string", "null"],
      "description": "Directory the worktree's artefacts were salvaged into before removal (agent-state, phase-tracker, triage-output, .pipeline/, build+test logs, review diff). Phase 7 and :resume read from here when worktreePath is gone."
    },
    "testDepth": {
      "type": ["string", "null"],
      "enum": ["unit", "unit+ui", "unit+mcp", null],
      "description": "How far the run tests, answered at intake because Phase 5 is absent from four of the eight modes and a question asked where it cannot be reached is a question nobody answers. `unit+ui` runs the repo's own UI test and records the screen around it; `unit+mcp` drives the flow through the toolkit MCP instead. The options offered are built from evidenceCapability, never from the model's reading of the repo."
    },
    "testDepthSource": {
      "type": ["string", "null"],
      "enum": ["user", "autopilot", "default", "forced", null],
      "description": "Who chose. `forced` means only one option was open, so nothing was asked - recorded rather than passed off as the user's answer."
    },
    "evidenceCapability": {
      "type": ["object", "null"],
      "additionalProperties": true,
      "description": "What this machine and this repo can actually produce, measured by probe-evidence-capability.sh BEFORE the test-depth question. Every absent value carries its reason, so a closed option can say why instead of vanishing from the menu; a value that could not be measured is null with a reason, never false, because a probe that did not look and a probe that found nothing are different facts. Contract: multi-agent-refs/features/visual-evidence.md.",
      "properties": {
        "platform": { "type": "string", "enum": ["ios", "android", "web", "other"] },
        "uiTestTarget": {
          "type": ["string", "null"],
          "description": "The single chosen target, empty while several candidates exist and no match picks one."
        },
        "uiTestTargets": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Every candidate. A real app has many: the reference iOS app has one XCUITest bundle among 477 files that merely sit under a *UITests path, and the reference Android app has eight instrumentation source sets."
        },
        "uiTestTargetReason": { "type": ["string", "null"] },
        "matchingTests": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Tests that mention a changed file's name. A heuristic, and treated as one: an empty set falls to the next tier rather than concluding the screen is untested."
        },
        "matchingTestsReason": { "type": ["string", "null"] },
        "device": { "type": ["string", "null"] },
        "deviceReason": {
          "type": ["string", "null"],
          "description": "'no booted simulator, but one is available to boot' and 'no iOS simulator available on this machine' are different problems with different fixes, and the user can act on only one of them."
        },
        "recorder": { "type": ["boolean", "null"] },
        "recorderReason": { "type": ["string", "null"] },
        "mcp": { "type": ["boolean", "null"] },
        "mcpReason": { "type": ["string", "null"] },
        "tier1": {
          "type": "string",
          "enum": ["open", "closed", "unknown"],
          "description": "Whether the depth menu may offer tier 1. `unknown` means the target was not probed (a --only device re-check), which is not the same as closed and must not be rendered as one."
        },
        "tier2": { "type": "string", "enum": ["open", "closed", "unknown"] }
      }
    },
    "uiTest": {
      "type": ["object", "null"],
      "additionalProperties": true,
      "description": "The UI test run that produced the tier 1 recording. Subject to the same default-FAIL rule as the build: a zero exit code alone is not a pass, the log is the evidence, and evidence-gate.mjs reads it.",
      "properties": {
        "ran": { "type": "boolean" },
        "target": { "type": ["string", "null"] },
        "selected": { "type": "array", "items": { "type": "string" } },
        "status": { "type": ["string", "null"], "enum": ["passed", "failed", "not-run", null] },
        "notRunReason": {
          "type": ["string", "null"],
          "description": "Why it did not run: no target, no matching test, no device. Each is a reason to fall to the next video tier, never a phase failure."
        },
        "logPath": { "type": ["string", "null"] }
      }
    },
    "visualEvidence": {
      "type": ["object", "null"],
      "additionalProperties": false,
      "description": "Before/after screenshots and the UI flow video for a UI change, attached to the Jira issue and rendered in the Phase 7 comment and the PR body. Required decided mechanically from taskType plus the changed-file list. Contract: multi-agent-refs/features/visual-evidence.md.",
      "properties": {
        "required": {
          "type": "boolean"
        },
        "requiredBy": {
          "type": "string",
          "description": "Which rule made it required, e.g. 'bugfix + ui-file-changed'."
        },
        "platform": {
          "type": "string",
          "enum": ["ios", "android", "web", "other"]
        },
        "before": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "file": {
                "type": "string",
                "description": "Local path under $WORKTREE/.pipeline/evidence/."
              },
              "source": {
                "type": "string",
                "enum": ["ticket", "capture"]
              },
              "capturedAt": {
                "type": "string",
                "description": "Phase that produced it, e.g. 'phase-3'."
              },
              "jiraFilename": {
                "type": "string",
                "description": "Attachment filename the Jira comment references."
              },
              "url": {
                "type": "string",
                "description": "Jira attachment content URL."
              }
            }
          },
          "description": "Images taken from the issue's own attachments. The pipeline never rebuilds the pre-fix state to photograph it; no ticket image means no before, recorded as a gap."
        },
        "after": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "file": {
                "type": "string",
                "description": "Local path under $WORKTREE/.pipeline/evidence/."
              },
              "source": {
                "type": "string",
                "enum": ["ticket", "capture"]
              },
              "capturedAt": {
                "type": "string",
                "description": "Phase that produced it, e.g. 'phase-3'."
              },
              "jiraFilename": {
                "type": "string",
                "description": "Attachment filename the Jira comment references."
              },
              "url": {
                "type": "string",
                "description": "Jira attachment content URL."
              }
            }
          },
          "description": "Captured in Phase 3 after the build+test gate, because Phase 5 is dropped by every autopilot and --local entry."
        },
        "host": {
          "type": ["string", "null"],
          "enum": ["jira", "github-public", "github-private", "none", null],
          "description": "Where the artefacts are published, resolved in Phase 6. `jira` attaches both stills and video. `github-public` pushes the stills to the evidence branch and embeds them in the PR body. `github-private` pushes the same stills but the PR carries a blob permalink instead of an inline image, because GitHub's image proxy cannot fetch a private repo's raw URL and an embedded one renders broken for every reader. `none` publishes nothing and records the gap. Video is Jira-only by decision: without an attachment host there is nothing a recording can be attached to."
        },
        "hostReason": {
          "type": ["string", "null"],
          "description": "Why this host and not the one above it in the order. A host of `none` with no reason is the silence the Phase 6 blocker exists to catch."
        },
        "videoTier": {
          "type": ["integer", "null"],
          "enum": [1, 2, 3, null],
          "description": "1 = the repo's own UI test target drove the flow, 2 = MCP-driven flow, 3 = no recording. Resolved from the capability probe, then RE-CHECKED at capture time: a device booted at intake can be gone by Phase 3, and a tier recorded from a stale measurement is a promise the run cannot keep."
        },
        "videoTierReason": {
          "type": ["string", "null"],
          "description": "Which rule produced the tier, and the tier it came down from when it was downgraded at capture time, e.g. 'tier 1 -> 2: simulator no longer booted'."
        },
        "video": {
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "file": {
              "type": "string",
              "description": "Local path under $WORKTREE/.pipeline/evidence/."
            },
            "source": {
              "type": "string",
              "enum": ["ticket", "capture"]
            },
            "capturedAt": {
              "type": "string",
              "description": "Phase that produced it, e.g. 'phase-3'."
            },
            "jiraFilename": {
              "type": "string",
              "description": "Attachment filename the Jira comment references."
            },
            "url": {
              "type": "string",
              "description": "Jira attachment content URL."
            },
            "seconds": {
              "type": "number",
              "description": "Recorded duration; capped at 60."
            }
          }
        },
        "gaps": {
          "type": "array",
          "description": "Every artefact that is required and absent, with its reason. A recorded reason satisfies the Phase 6 blocker; silence does not.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "what": {
                "type": "string",
                "enum": ["before", "after", "video"]
              },
              "reason": {
                "type": "string"
              }
            },
            "required": ["what", "reason"]
          }
        }
      }
    }
  }
}
