{
  "$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)."
    },
    "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'."
    },
    "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 (it was the --dev flag before); 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"]
          },
          "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": {}
        }
      }
    },
    "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."
    }
  }
}
