{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://raw.githubusercontent.com/yaniv-golan/cowork-harness/main/schema/run-result.json",
  "title": "RunResult",
  "description": "Structured output written to runs/<scenario>/<runId>/turns/<N>/result.json by cowork-harness executeScenario",
  "type": "object",
  "required": ["scenario", "fidelity", "baseline", "result", "decisions", "egress", "assertions", "outDir"],
  "properties": {
    "$schema": {
      "type": "string"
    },
    "generator": {
      "type": "string"
    },
    "turn": {
      "type": "integer",
      "minimum": 1,
      "description": "1-based turn number within a resumed (--session-id + --resume) session; 1 for a normal single-shot run. Each turn owns its artifacts under turns/<N>/ (result.json, run.jsonl, trace.json, resources.jsonl). Absent on replay/chat lanes."
    },
    "referencesRead": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Skill reference/script files the agent Read (skill-relative: references/foo.md, scripts/bar.py), deduped in first-seen order. A progressive-disclosure signal for skill-quality measurement. Main-agent Reads only (sub-agent reads not attributed); references/ and scripts/ under a mounted plugin root, not assets/, never SKILL.md (delivered whole). Present on both live and replay. READ referencesAccessed INSTEAD for 'did the agent open this reference?': this counts the Read TOOL, and its absence is not evidence the content went unread."
    },
    "referencesAccessed": {
      "type": "array",
      "description": "every skill reference/script file the agent REACHED, with the tool channel(s) \u2014 the WIDE progressive-disclosure signal; referencesRead is this field's `read`-channel projection. All channels apply the same mounted-plugin-root predicate. DELIBERATELY UNDER-APPROXIMATES (a cd then a bare relative cat, a heredoc, or a $VAR-built path is invisible), so absence is weak evidence, never proof the content went unread. PRESENCE is the cannot-verify channel: [] = the drive ran and observed nothing (a real negative); ABSENT = no observable drive (replay error result, torn partial result, or a result written before the field existed) \u2014 never read absent as 'none'. Present on both live and replay.",
      "items": {
        "type": "object",
        "required": ["path", "via"],
        "additionalProperties": false,
        "properties": {
          "path": {
            "type": "string",
            "description": "skill-relative reference/script path, e.g. references/foo.md"
          },
          "via": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["read", "grep", "bash"]
            },
            "description": "the tool channel(s) this path was reached through, first-seen order"
          }
        }
      }
    },
    "ablated": {
      "type": "boolean",
      "description": "True when the run was ablated (--ablate-skill): the skill(s)-under-test were removed so the same prompt runs with no skill \u2014 a negative control for skill-lift measurement. Never read an ablated run as a real (with-skill) pass."
    },
    "finalMessage": {
      "type": "string",
      "description": "The agent's final answer text \u2014 the SDK result message's designated answer, not the joined transcript of every assistant turn. Absent when no result event carried text (a spawn/exit error before the result)."
    },
    "resources": {
      "type": "object",
      "description": "Resource telemetry sampled during the run (tier, sample count/interval, peak RSS bytes, avg/peak CPU%, malformed-line and failed-probe counts). Live lane only."
    },
    "contextEvents": {
      "type": "array",
      "items": {
        "type": "object"
      },
      "description": "Context lifecycle events incl. compaction boundaries ({subtype, ts?, data?})."
    },
    "mcpErrors": {
      "type": "array",
      "items": {
        "type": "object"
      },
      "description": "Failed MCP round-trips ({server, code?, message}). Live lane only."
    },
    "hookEvents": {
      "type": "array",
      "items": {
        "type": "object"
      },
      "description": "PreToolUse hook block/allow decisions ({callbackId, decision, reason?, tool?})."
    },
    "fileToolAttempts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["tool", "paths", "origin"],
        "properties": {
          "tool": {
            "type": "string"
          },
          "paths": {
            "type": "object",
            "properties": {
              "file_path": {
                "type": "string"
              },
              "path": {
                "type": "string"
              }
            }
          },
          "gatePath": {
            "type": "string"
          },
          "origin": {
            "enum": ["main", "subagent", "unknown"]
          },
          "parentToolUseId": {
            "type": "string"
          },
          "toolUseId": {
            "type": "string"
          }
        }
      },
      "description": "Attempt-level gated-file-tool telemetry (raw paths as sent, per Read/Write/Edit/Glob/Grep/MultiEdit tool_use). Undefined = evidence unavailable; [] = captured, no gated attempts."
    },
    "pathDenials": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["source", "tool", "decision"],
        "properties": {
          "source": {
            "enum": ["pretooluse", "can_use_tool", "permission_denied"]
          },
          "tool": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "callbackId": {
            "type": "string"
          },
          "decisionReasonType": {
            "type": "string"
          },
          "agentId": {
            "type": "string"
          },
          "decision": {
            "enum": ["deny"]
          },
          "reason": {
            "type": "string"
          },
          "toolUseId": {
            "type": "string"
          }
        }
      },
      "description": "Decision-level path-denial telemetry from three filtered producers: the PreToolUse path gate's own callback, a denied can_use_tool ask on a gated file tool with a path, and a pre-ask permission_denied correlated to a recorded gated attempt. Undefined = evidence unavailable (older result, or replay without controlOut); [] = captured, no path denials."
    },
    "presentedFiles": {
      "type": "array",
      "items": {
        "type": "object"
      },
      "description": "present_files promotions ({from, to, promoted, leaked}) \u2014 scratch\u2192outputs deliverable surfacing (container tier)."
    },
    "presentFilesCalls": {
      "type": "integer",
      "description": "How many present_files calls carried at least one well-formed file_path \u2014 the presence evidence present_files_called reads, counted from the tool_use input's shape so it survives redaction (presentedFiles entries are dropped when a path can't be classified, which a host-path policy guarantees at hostloop). Absent on pre-field results."
    },
    "mode": {
      "type": "string",
      "enum": ["run", "chat"],
      "description": "Which lane produced this result. 'run' = an asserted run/skill/record/replay (carries a verdict); 'chat' = an interactive exploratory session (no assertions, no verdict). Absent on results written before this field existed \u2014 treat absent as 'run'."
    },
    "command": {
      "type": "string",
      "enum": ["run", "skill", "record", "chat", "replay"],
      "description": "The CLI command that produced this result \u2014 finer than 'mode' (skill/record both have mode:'run'). The run index prefers it during a reindex so a skill/record row isn't relabeled 'run' (#48). Absent on pre-field results."
    },
    "scenario": {
      "type": "string"
    },
    "prompt": {
      "type": "string"
    },
    "fidelity": {
      "type": "string"
    },
    "baseline": {
      "type": "string"
    },
    "result": {
      "type": "string",
      "enum": ["success", "error"]
    },
    "scratchpadEvidenceComplete": {
      "type": "boolean",
      "description": "Did a COMPLETE scratchpad walk observe this run? Persisted because the verdict is computed from RunResult alone, and 'no scratchpad files' must be distinguishable from 'no scratchpad walk'. False = the undelivered-deliverables signal stays silent because it cannot tell, not because the run was clean. Absent on results written before this field existed - also cannot-tell."
    },
    "lane": {
      "type": "string",
      "enum": ["local", "remote"],
      "description": "Which Cowork product lane's DELIVERY CONTRACT this run was held to, as declared by the scenario. Distinct from `execution.location` (where the run physically happened): the lane is declared intent, because Cowork's lane is a per-session human choice that leaves no trace in a run's evidence. Absent = local. On `remote`, location delivers nothing (a remote container has no auto-delivering outputs dir and is reclaimed at session end) and present_files is not served."
    },
    "execution": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "enum": ["local", "cloud"]
        },
        "environmentId": {
          "type": "string"
        },
        "taskKind": {
          "type": "string",
          "enum": ["interactive", "scheduled"]
        }
      },
      "required": ["location"],
      "description": "Execution location taxonomy \u2014 orthogonal to fidelity (a privilege/sandbox tier, all local). Stamped location:'local' on every locally-executed run so a future cloud-run artifact's differing/absent stamp is a detectable signal, never a silent mislabel. environmentId is the cloud environment_id (bridge sessions); taskKind distinguishes an interactive run from a scheduled-trigger run. ABSENCE IS NOT A POSITIVE 'local' SIGNAL: absent means either a pre-taxonomy result or the error-replay lane (no environment recoverable)."
    },
    "resultErrorKind": {
      "type": "string",
      "enum": ["transport", "agent", "usage_limit"],
      "description": "when result is 'error', the KIND: a tail-end transport drop (connection closed after a clean result), a genuine agent/skill failure, or usage_limit (quota exhausted \u2014 an is_error result with HTTP 429 + a terminal usage-limit message; not the skill's fault, retry after reset)"
    },
    "errorSource": {
      "type": "string",
      "enum": ["spawn", "protocol", "exit", "agent", "result", "no_result", "timeout"],
      "description": "How the run terminated in error \u2014 the error event's finer source (spawn/protocol/exit/agent, or 'result' for the SDK-wrapped is_error result), 'no_result' when the stream ended with no terminal event (turn/time exhaustion), or 'timeout' when the harness wall-clock limit killed the run. Additive diagnostic alongside resultErrorKind; not read by the verdict."
    },
    "resultSubtype": {
      "type": "string",
      "description": "The SDK result message's subtype verbatim (e.g. error_max_turns, error_during_execution, success) \u2014 a pass-through diagnostic so a debugger can tell turn-exhaustion from a generic execution error without the harness inventing a taxonomy."
    },
    "stderrLogPath": {
      "type": "string",
      "description": "Absolute path to the agent's full stderr log (<outDir>/agent.stderr.log), surfaced so an OOM/crash debugger knows where to look. Live path only \u2014 absent on replay."
    },
    "stalledOnQuestion": {
      "type": "boolean",
      "description": "H2/H3: the run ended on a question having done no productive tool work after its last gate (the agent asked for input and stopped \u2014 incl. re-asking in plain text after answering an AskUserQuestion) while result==='success'. computeVerdict fails on this (a 'stalled' signal) unless the scenario asserts allow_stall."
    },
    "capabilityProbe": {
      "type": "string",
      "enum": ["definitive", "unverified", "skipped"],
      "description": "Fix 6h: capability-probe outcome \u2014 definitive (ran, omitted-set known), unverified (probe failed), or skipped (protocol/replay/skip-env). Drives the guard roster's ran-clean vs not-run distinction."
    },
    "requiresCapabilityUnmet": {
      "type": "object",
      "description": "Fix 4b: declared requires_capabilities the running tier could not satisfy (omitted by the image, or unverifiable on this tier). computeVerdict hard-fails on this unless allow_missing_capability.",
      "properties": {
        "caps": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "reason": {
          "type": "string",
          "enum": ["omitted", "unverifiable", "unknown"],
          "description": "omitted = the image lacks the capabilities; unverifiable = the tier couldn't probe (protocol/replay/skip); unknown = requires_capabilities named a capability family the harness does not recognize. computeVerdict handles all three."
        }
      }
    },
    "decisions": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["kind", "name", "decision"],
        "properties": {
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "decision": {
            "type": "string"
          },
          "by": {
            "type": "string"
          },
          "requestId": {
            "type": "string",
            "description": "request_id (UUID) of a question gate \u2014 pairs a decision to its event row by id."
          },
          "model": {
            "type": "string"
          },
          "detail": {},
          "rationale": {
            "type": "string"
          },
          "questions": {
            "type": "array",
            "description": "Full AskUserQuestion option set as originally offered \u2014 present only on kind:'question' decisions.",
            "items": {
              "type": "object",
              "required": ["question", "options"],
              "properties": {
                "question": {
                  "type": "string"
                },
                "header": {
                  "type": "string"
                },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": ["label"],
                    "properties": {
                      "label": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      }
                    }
                  }
                },
                "multiSelect": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      }
    },
    "toolCounts": {
      "type": "object",
      "additionalProperties": {
        "type": "number"
      },
      "description": "Truthful per-tool CALL-COUNT map: a plain number per tool name (NOT usage.server_tool_use, which is host-routed-blind in cowork). Shape is always {tool: number} \u2014 for per-tool error counts use the separately-shaped toolErrors ({tool: {calls, errors}}); for per-tool timing use toolDurations ({tool: {calls, totalMs, maxMs}}). Do not expect an object value here."
    },
    "infraErrors": {
      "type": "array",
      "description": "Infrastructure errors, tagged by origin. 'hostloop-sidecar'/'egress-sidecar' mean a supervising process died \u2014 a hard verdict fail on BOTH lanes, NOT author-suppressible, because the run's evidence is contaminated. 'hostloop-exec' is a single failed container exec: the tool call failed, the run did not, so it only warns.",
      "items": {
        "type": "object",
        "required": ["source", "message"],
        "properties": {
          "source": {
            "type": "string",
            "enum": ["hostloop-sidecar", "hostloop-exec", "egress-sidecar"]
          },
          "message": {
            "type": "string"
          }
        }
      }
    },
    "evidenceErrors": {
      "type": "object",
      "description": "Companion counters for malformed/dropped telemetry streams. A >0 count makes the dependent assertion fail 'malformed' rather than silently dropping the bad entries.",
      "properties": {
        "taskTracking": {
          "type": "number"
        },
        "webSearchParse": {
          "type": "number"
        },
        "presentFilesMalformed": {
          "type": "number"
        },
        "egressParse": {
          "type": "number"
        },
        "protocolMalformed": {
          "type": "number",
          "description": "Count of malformed control-stream user/tool-result blocks skipped at ingress (a non-object block, or a tool_result missing a non-empty tool_use_id). A >0 count means delivery/tool-result pairing evidence is incomplete."
        }
      }
    },
    "webSearches": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "toolUseId": {
            "type": "string"
          },
          "query": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              },
              "required": ["title", "url"],
              "additionalProperties": false
            }
          }
        },
        "required": ["query", "results"],
        "additionalProperties": false
      }
    },
    "toolDurations": {
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "calls": {
            "type": "number"
          },
          "totalMs": {
            "type": "number"
          },
          "maxMs": {
            "type": "number"
          }
        },
        "required": ["calls", "totalMs", "maxMs"],
        "additionalProperties": false
      }
    },
    "models": {
      "type": "array",
      "description": "distinct model ids seen across the run's assistant messages, in first-seen order \u2014 verbatim from the agent, NOT validated as live model ids. The agent stamps the literal '<synthetic>' on assistant messages it fabricates locally (no API call, zero-filled usage), so an angle-bracket-wrapped entry is a marker, not a model, and two runs of the same pinned model can differ here purely by whether a synthesized turn occurred. Drop '<\u2026>'-wrapped entries before reading this array as run provenance.",
      "items": {
        "type": "string"
      }
    },
    "thinking": {
      "type": "array",
      "description": "main-loop reasoning blocks (capped at the last 50). NOTE: on newer models (Opus 4.8, Sonnet 5) the API defaults the thinking display mode to 'omitted', so blocks arrive with empty text + a signature and are surfaced as {text:'', redacted:true} \u2014 read that as 'reasoned, text omitted by request', NOT 'no reasoning'. The harness passes no --thinking-display (faithful to real Cowork); the fenced debug.thinking_display:'summarized' opt-in can surface summarized (never raw) text. redacted is omitted (not false) on blocks that carry text.",
      "items": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "redacted": {
            "type": "boolean",
            "description": "present and true ONLY when the model reasoned but returned empty thinking text (empty thinking + present signature) because the display mode was 'omitted'. Omitted (not false) on blocks with text."
          }
        },
        "required": ["text"]
      }
    },
    "thinkingElided": {
      "type": "number"
    },
    "toolErrors": {
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "calls": {
            "type": "number"
          },
          "errors": {
            "type": "number"
          }
        },
        "required": ["calls", "errors"],
        "additionalProperties": false
      }
    },
    "modelUsage": {
      "type": "object",
      "description": "Per-model cost/token breakdown, denormalized from the SDK result message's own modelUsage field (\u00a74.7, M3) \u2014 cumulative for the whole run. Field names match the real observed SDK payload.",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "inputTokens": {
            "type": "number"
          },
          "outputTokens": {
            "type": "number"
          },
          "cacheReadInputTokens": {
            "type": "number"
          },
          "cacheCreationInputTokens": {
            "type": "number"
          },
          "costUSD": {
            "type": "number"
          },
          "contextWindow": {
            "type": "number"
          },
          "maxOutputTokens": {
            "type": "number"
          },
          "webSearchRequests": {
            "type": "number"
          }
        },
        "additionalProperties": false
      }
    },
    "redundantToolCalls": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "argHash": {
            "type": "string"
          },
          "count": {
            "type": "number"
          }
        },
        "required": ["name", "argHash", "count"],
        "additionalProperties": false
      }
    },
    "skillActivity": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "skillId": {
            "type": "string"
          },
          "invocationSeq": {
            "type": "number"
          },
          "toolCounts": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "toolCallCount": {
            "type": "number"
          },
          "dispatchCount": {
            "type": "number"
          },
          "durationMs": {
            "type": "number"
          }
        },
        "required": ["skillId", "invocationSeq", "toolCounts", "toolCallCount", "dispatchCount"],
        "additionalProperties": false
      }
    },
    "gateDeliveries": {
      "type": "array"
    },
    "egress": {
      "type": "array"
    },
    "assertions": {
      "type": "array"
    },
    "outcome": {
      "type": "string",
      "enum": ["errored", "no_deliverable", "delivered_with_verdict_fail", "delivered_clean"],
      "description": "One-field rollup of the result x verdict.pass x exit-code matrix, for consumers driving an iterative loop. A pure function of `result` and `verdict` (see deriveOutcome, src/run/outcome.ts) - adds no judgement and cannot disagree with them. `errored` dominates; then the no-deliverable signals (stalled/ended_with_question); then the verdict. ABSENT whenever `verdict` is absent (chat, or a pre-existing kept run) - treat absence as unknown, never as a pass. NOTE: 'delivered_*' means no stall/question signal fired, NOT positive evidence a deliverable exists - check artifacts/workspaceFiles for that. `no_deliverable` is reachable only on open-ended scenarios on the live lane, and is warn-severity, so it can coexist with verdict.pass true and exit 0 - do not infer the exit code from this field."
    },
    "verdict": {
      "type": "object",
      "description": "The overall run/asserted-lane verdict: computeVerdict's full return value, persisted VERBATIM (the same shape the --output-format json stdout envelope attaches to every result, since envelope.ts calls computeVerdict too) so the persisted and streamed channels can never diverge. pass/exitCode are the SAME values every verdict site (the run/skill exit, the footer, the JSON envelope ok) routes through computeVerdict for. signals[]/guards[] are the raw verdict inputs; failures[] collapses signals into a flat, jq-friendly list \u2014 names the failing assertion key when a failure traces to one, else carries just a hard-verdict guard reason (infra error, unanswered gate, scan-based host-path leak, etc.); empty on a pass. Run/asserted lane ONLY \u2014 absent (undefined) on a chat result, and on a result.json written before this field existed.",
      "properties": {
        "pass": {
          "type": "boolean"
        },
        "exitCode": {
          "type": "integer",
          "enum": [0, 1]
        },
        "signals": {
          "type": "array",
          "description": "the raw verdict signals computeVerdict evaluated (assertion failures + guard-fired reasons + warn-only signals like non_determinism); [] on a clean pass.",
          "items": {
            "type": "object",
            "required": ["code", "severity", "message"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "assertion",
                  "result_error",
                  "transport_error",
                  "usage_limit",
                  "permissive_auto_allow",
                  "outputs_delete",
                  "mount_delete",
                  "host_path_leak",
                  "non_deterministic",
                  "l0_plugin_divergence",
                  "missing_capability",
                  "infra_error",
                  "exec_infra_error",
                  "stalled",
                  "prompt_asset_missing",
                  "scan_unavailable",
                  "ended_with_question",
                  "undelivered_deliverables",
                  "delivery_unobservable"
                ]
              },
              "severity": {
                "type": "string",
                "enum": ["fail", "warn"]
              },
              "message": {
                "type": "string"
              }
            }
          }
        },
        "guards": {
          "type": "array",
          "description": "the \"guards active this run\" roster \u2014 one entry per fail-when-silent guard, with its visibility status (ok = ran and found nothing; fired = caught its failure mode; na = not applicable on this lane/tier; unverified = ran but couldn't conclude). Never ok for a guard that didn't run.",
          "items": {
            "type": "object",
            "required": ["name", "status"],
            "properties": {
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": ["ok", "fired", "na", "unverified"]
              }
            }
          }
        },
        "failures": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["message", "kind"],
            "properties": {
              "assertion": {
                "type": "string",
                "description": "the failing assertion's key (e.g. \"tool_called\"), when the failure traces to one. NOT a reliable discriminator on its own \u2014 filter on `kind`: verify-run's answer-coverage misses also carry a key, and guard/staleness/cassette-format failures all carry none."
              },
              "kind": {
                "type": "string",
                "enum": ["assertion", "guard", "staleness", "cassette-format", "coverage"],
                "description": "what produced this failure. \"assertion\" = one of your own assert: items; \"guard\" = a hard-verdict signal you did not write (infra error, stall, host-path leak, unanswered gate, \u2026); \"staleness\" = skill/baseline drift escalated by --strict / --fail-on-skill-drift; \"cassette-format\" = the cassette cannot be interpreted (too new a version, or corrupt: duplicate/malformed control frames, a truncated recording); \"coverage\" = a verify-run answer-coverage miss. This is the field that separates \"did my assertions pass?\" from \"is the cassette stale?\" \u2014 both land on exit 1, so the exit code cannot."
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "required": ["pass", "exitCode", "signals", "guards", "failures"]
    },
    "subagents": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["toolUseId", "dispatchAgentType", "declaredTools", "toolsUsed"],
        "properties": {
          "toolUseId": {
            "type": "string"
          },
          "parentToolUseId": {
            "type": "string",
            "description": "the dispatching parent's toolUseId \u2014 reconstructs the nested dispatch tree (#16)"
          },
          "dispatchAgentType": {
            "type": "string",
            "description": "the DISPATCH-INPUT type (\"unknown\" when the input omitted subagent_type) \u2014 unchanged meaning, renamed from the ambiguous agentType now that a resolved type lands beside it"
          },
          "resolvedAgentType": {
            "type": "string",
            "description": "the BINARY-resolved child type from task_started (incl. the general-purpose fallback) \u2014 strictly better evidence than dispatchAgentType for a type-less dispatch"
          },
          "dispatchTypeOmitted": {
            "type": "boolean",
            "description": "the dispatch input carried no subagent_type at all (proven by the full input parse) \u2014 the wildcard-fallback trap fired"
          },
          "declaredTools": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "toolsUsed": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "count"],
              "properties": {
                "name": {
                  "type": "string"
                },
                "count": {
                  "type": "number"
                }
              }
            }
          },
          "referencesRead": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "skill reference/script files THIS sub-agent Read (same shape/dedupe as the top-level referencesRead), attributed via the dispatch's toolUseId"
          },
          "referencesAccessed": {
            "type": "array",
            "description": "THIS sub-agent's wide reference-access list \u2014 same shape, channels and caveats as the top-level referencesAccessed",
            "items": {
              "type": "object",
              "required": ["path", "via"],
              "additionalProperties": false,
              "properties": {
                "path": {
                  "type": "string",
                  "description": "skill-relative reference/script path, e.g. references/foo.md"
                },
                "via": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "enum": ["read", "grep", "bash"]
                  },
                  "description": "the tool channel(s) this path was reached through, first-seen order"
                }
              }
            }
          },
          "description": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "dispatchModel": {
            "type": "string",
            "description": "the DISPATCHING message's model (ex-\"model\" \u2014 renamed when resolvedModel landed beside it)"
          },
          "resolvedModel": {
            "type": "string",
            "description": "the RESOLVED child model from the dispatch's tool_use_result envelope"
          },
          "output": {
            "type": "string"
          },
          "outputTruncated": {
            "type": "boolean",
            "description": "the dispatch output was cut at the assert cap \u2014 a negative content check is unverifiable, not a proven absence (#9)"
          },
          "attributedSkillId": {
            "type": "string"
          },
          "reasoning": {
            "type": "array",
            "description": "the sub-agent's own THINKING and TEXT turns, in transcript order (tool_use/tool_result excluded \u2014 already covered by toolsUsed/referencesRead). Read from the on-disk child session transcript (LIVE/record lane only \u2014 undefined on replay, never embedded in a cassette). [] = a child transcript was found but captured no thinking/text turns; undefined = no child transcript joined to this dispatch. NOTE: sub-agent thinking TEXT is empty by default \u2014 the harness's non-interactive spawn forces the API's thinking.display to 'omitted' for sub-agent turns, so the model returns empty thinking blocks (signature-only) that the transcript records; a thinking turn is surfaced as {kind:'thinking', text:'', redacted:true}. `redacted:true` means the sub-agent reasoned here but the text was omitted by request \u2014 do NOT read empty text as 'no reasoning.' TEXT turns are never redacted (they persist verbatim). An opt-in --thinking-display summarized lever could surface summarized (never raw) sub-agent thinking, but the 'omitted' default is real-Cowork-faithful.",
            "items": {
              "type": "object",
              "required": ["kind", "text"],
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": ["thinking", "text"]
                },
                "text": {
                  "type": "string"
                },
                "redacted": {
                  "type": "boolean",
                  "description": "present and true ONLY on a thinking turn whose text was stripped upstream (empty thinking + non-empty signature in the child transcript) \u2014 'the sub-agent reasoned here, text withheld upstream.' Omitted (not false) on turns with text and on all TEXT turns."
                }
              }
            }
          },
          "reasoningElided": {
            "type": "number",
            "description": "count of reasoning turns dropped by the cap (oldest-first) \u2014 mirrors thinkingElided"
          },
          "webSearches": {
            "type": "array",
            "description": "the sub-agent's WebSearch calls (query + the paired tool_result's text, bounded), read from the same child session transcript as `reasoning` (LIVE/record lane only \u2014 undefined on replay). Sub-agent searches never enter the top-level webSearches[] (main-agent/fork-scoped) or toolCounts, so this is the only place a sub-agent's research is grounded. RAW bounded text rather than the top-level field's parsed {title,url} shape: grounding needs the content the sub-agent actually saw. Absent = never captured OR the dispatch made no searches; a present array is 1+ captured searches. Entries tagged `viaAgentId` were made by a DESCENDANT dispatch (a sub-agent's own sub-agent, which the parent stream never surfaced as its own subagents[] entry) and are attributed to the nearest ancestor that has one \u2014 without them an empty array read as 'no research' when a descendant had researched.",
            "items": {
              "type": "object",
              "required": ["query", "resultText"],
              "properties": {
                "query": {
                  "type": "string"
                },
                "resultText": {
                  "type": "string"
                },
                "resultTruncated": {
                  "type": "boolean",
                  "description": "present and true ONLY when the result text was cut at the per-entry byte cap"
                },
                "viaAgentId": {
                  "type": "string",
                  "description": "present ONLY when a DESCENDANT dispatch made this search: the child agent id (agent-<id>.jsonl) that ran it. Absent = this dispatch's own search."
                },
                "viaSpawnDepth": {
                  "type": "number",
                  "description": "the descendant's own spawnDepth from its agent-<id>.meta.json (this dispatch is shallower). Only meaningful alongside viaAgentId; absent when the meta carried no depth."
                }
              }
            }
          },
          "webSearchesElided": {
            "type": "number",
            "description": "count of WebSearch entries dropped past the per-dispatch cap (oldest-first) \u2014 mirrors reasoningElided"
          }
        }
      }
    },
    "nonReproducibleAnswers": {
      "type": "array"
    },
    "usage": {
      "type": "object",
      "description": "SDK usage payload (input_tokens, output_tokens, etc.), pass-through, plus a harness-computed 'turns' field (from the SDK result message's num_turns) when available. Wave 0 seam.",
      "properties": {
        "turns": {
          "type": "number"
        }
      }
    },
    "cost": {
      "type": "object",
      "description": "usd = the SDK result message's total_cost_usd for this invocation, when available (Wave 0 seam). raw = the api_metrics event payload (pre-existing, independent source).",
      "properties": {
        "usd": {
          "type": "number"
        },
        "raw": {
          "type": "object"
        }
      }
    },
    "durationMs": {
      "type": "number"
    },
    "fingerprint": {
      "type": "object",
      "description": "Skill/plugin staleness fingerprint recorded at run time; persisted so `verify-run` can detect a kept run that predates a skill change and refuse to vouch for answer-coverage against stale gate labels.",
      "required": ["baseline"],
      "properties": {
        "baseline": {
          "type": "string",
          "description": "appVersion at record time"
        },
        "frozen": {
          "type": "boolean",
          "description": "true when surfaced from a cassette's record-time fingerprint on the replay lane (not a fresh run-time recompute) (#46)"
        },
        "hashFormat": {
          "type": "string",
          "enum": ["jcs1"],
          "description": "which manifest-transform algorithm produced these digests. ABSENT means the LEGACY (pre-epoch) transform \u2014 not raw bytes. `verify-run` compares this BEFORE comparing skillHash, so an algorithm change is reported as a format difference rather than as source drift."
        },
        "skillHash": {
          "type": "string",
          "description": "hash of the session's local skill/plugin/marketplace dir contents (if any)"
        },
        "skillSources": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "skillScope": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "sharedHash": {
          "type": "string",
          "description": "shared-root hash for scoped cassettes; absent on whole-tree or non-plugin-root mounts"
        },
        "contentSig": {
          "type": "string",
          "description": "content fingerprint over the same file set as skillHash. NOT the rehash proof: it follows the SAME manifest transform skillHash does, so it is not comparable across a hash-format epoch. Since CONTENTSIG_ALGO 5 it also folds directory markers, so an added or removed EMPTY directory moves it. The migration proof recomputes the LEGACY skillHash instead."
        },
        "fileSigs": {
          "type": "array",
          "description": "per-file manifest [relpath, contentSha] of the exact files feeding skillHash A manifest entry's sha depends on `hashFormat`: absent/legacy = JSON.parse -> delete `version` -> JSON.stringify -> sha256; `jcs1` = canonical (JCS-style) serialization instead. The legacy recipe will not match a jcs1 cassette for any unsorted manifest.",
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 2,
            "maxItems": 2
          }
        },
        "fileSigsOmitted": {
          "type": "boolean"
        },
        "mode": {
          "type": "string",
          "enum": ["git", "raw"],
          "description": "the file-set boundary used for skillHash \u2014 \"git\" (git-tracked set \u2014 the DEFAULT for a git work tree unless COWORK_HARNESS_GITSET=0) or \"raw\" (filesystem walk; used when GITSET=0 or the dir is not a git work tree)"
        },
        "agentScope": {
          "type": "string",
          "enum": ["skill"]
        },
        "promptAssetsHash": {
          "type": "string",
          "description": "sha16 over the baseline's committed prompt-asset FILE bytes (spawn.promptTemplate / subagentAppend / subagentAppendHostLoop, key-ordered, comment-stripped). Prompt identity was previously keyed on `baseline` (appVersion) alone, so a prompt-asset edit under the SAME appVersion silently replayed old-prompt behavior. Absent on a run recorded before this field existed \u2192 informational note, never a finding."
        }
      }
    },
    "outDir": {
      "type": "string"
    },
    "workDir": {
      "type": "string"
    },
    "outputsDir": {
      "type": "string"
    },
    "userVisibleRoots": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "The user-visible mount roots (relative to mnt/) for this run \u2014 `outputs` plus each connected work folder's resolved mount name. Plugins are not included (read-only inputs, never artifact roots)."
    },
    "readonlyFolderRoots": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Subset of userVisibleRoots that are read-only (mode:\"r\") connected-folder mounts \u2014 inputs, not deliverables. `artifacts` excludes them so `scaffold` doesn't emit file_exists for an input."
    },
    "artifacts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["path", "bytes"],
        "properties": {
          "path": {
            "type": "string"
          },
          "bytes": {
            "type": "number"
          }
        }
      }
    },
    "workspaceFiles": {
      "type": "array",
      "description": "Working folder panel's canonical file model (\u00a76.3, M6) \u2014 every file under the user-visible roots, classified output|mount|input and sha256-fingerprinted. The Scratch pad's \"scratchpad\" class is deliberately not implemented (no host-materialization mechanism exists for it).",
      "items": {
        "type": "object",
        "required": ["path", "bytes", "class"],
        "properties": {
          "path": {
            "type": "string"
          },
          "bytes": {
            "type": "number"
          },
          "sha256": {
            "type": "string"
          },
          "hashError": {
            "type": "string"
          },
          "class": {
            "type": "string",
            "enum": ["output", "mount", "input", "scratchpad"],
            "description": "output/mount/input = under a user-visible root. scratchpad = written OUTSIDE every user-visible root, i.e. produced but not delivered by location. Scratchpad entries are absent on tiers where that walk cannot run, which is evidence-unavailable rather than 'none existed'."
          }
        }
      }
    },
    "preRunPaths": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "workRoot-relative paths that existed under the user-visible roots BEFORE the agent ran (captured post-staging, pre-spawn) \u2014 the baseline `no_unexpected_files` diffs against. Absent when the tier didn't capture it (microvm) or the run predates the seam."
    },
    "preRunLinkAware": {
      "type": "boolean",
      "description": "true iff preRunPaths was captured with the link-aware walk (manifest v2+); absent/false \u21d2 a pre-#38 baseline, no_unexpected_files then excludes link entries from the post walk. Live/verify-run only."
    },
    "preRunHashes": {
      "type": "object",
      "additionalProperties": {
        "type": ["string", "null"]
      },
      "description": "Per-path sha256 of the user-visible tree BEFORE the agent ran (null = over the pre-run hash cap). Powers `input_unmodified`. Absent when the tier didn't capture it (microvm) or the run predates the seam."
    },
    "preRunOrigin": {
      "type": "string",
      "enum": ["local-walk", "remote-unavailable", "local-unreadable"],
      "description": "Provenance of the pre-run baseline. local-walk = complete local walk; local-unreadable = a connected-folder source could not be walked so the baseline is partial (no_unexpected_files / input_unmodified then fail evidence-unavailable); remote-unavailable is reserved for a future cloud producer. Absent on an older run predating the field."
    },
    "partial": {
      "type": "boolean"
    },
    "unansweredGate": {
      "type": "object"
    },
    "nonDeterministic": {
      "type": "boolean"
    },
    "nonDeterministicTerminal": {
      "type": "boolean"
    },
    "permissiveAutoAllow": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "#6: off-registry auto-allows (real Cowork blocks these); non-empty means NOT a faithful pass"
    },
    "scan": {
      "type": "object",
      "description": "Post-run scan signals (live lane only)",
      "properties": {
        "outputsDeletes": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "hostPathLeaked": {
          "type": "boolean"
        },
        "selfHealRan": {
          "type": "boolean"
        },
        "mountDeletes": {
          "type": "array",
          "description": "Per-mount delete detections across every delete-denied (rw) user-visible mount, including outputs. Superset of outputsDeletes, which is unchanged. Reported, not verdict-moving.",
          "items": {
            "type": "object",
            "properties": {
              "mount": {
                "type": "string"
              },
              "command": {
                "type": "string"
              }
            },
            "required": ["mount", "command"]
          }
        }
      }
    },
    "effectiveFidelity": {
      "type": "string",
      "description": "The fidelity tier actually used (differs from fidelity when fidelity:'cowork')"
    },
    "runLabel": {
      "type": "string",
      "description": "User --label generation tag (iterate-across-fixes loop); ergonomics only, authoritative version key is fingerprint.skillHash"
    },
    "skillCommit": {
      "type": ["string", "null"],
      "description": "Best-effort git HEAD shared by the session's skill source dirs \u2014 commit provenance, not a grouping key; null if dirs span >1 repo / non-git / unresolved"
    },
    "fidelityWarnings": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "#49: structured prompt warnings visible to JSON callers"
    },
    "l0PluginDivergence": {
      "type": "boolean",
      "description": "#20: failing fidelity signal for protocol+plugins"
    },
    "missingCapabilityUse": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "capability families the agent image omits but the skill used (live built-image tiers) \u2014 computeVerdict fails unless allow_missing_capability"
    },
    "gateProvenance": {
      "type": "object",
      "description": "Per-gate answer provenance (how each AskUserQuestion gate was answered) with a bySource histogram. Informational; never affects the verdict. Absent when the run had no gates, and absent on the replay lane (which reports reproducibility via nonDeterministic:false, not per-gate provenance).",
      "properties": {
        "total": {
          "type": "number"
        },
        "bySource": {
          "type": "object",
          "additionalProperties": {
            "type": "number"
          }
        },
        "gates": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["question", "answeredBy", "answer"],
            "properties": {
              "question": {
                "type": "string"
              },
              "answeredBy": {
                "type": "string"
              },
              "answer": {
                "type": "string"
              },
              "model": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "skillsInvoked": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Wave 1 / E8: skill/plugin ids invoked via the Skill tool_use event, in call order, duplicates kept. Backs skill_triggered/no_skill_triggered. Absent on a run predating E8."
    },
    "skillToolAvailable": {
      "type": "boolean",
      "description": "Wave 1 / E8: whether the agent's init tool list included \"Skill\" \u2014 false means skill_triggered/no_skill_triggered cannot be evaluated (agent-version tool-name drift)."
    },
    "staleness": {
      "type": "array",
      "description": "Replay only: class-tagged cassette-staleness findings, surfaced for a token-free JSON gate. Drift classes are non-failing by default (a stale but passing replay stays ok:true); `unverifiable-skill` FAILS the default verdict since 2.0.0 — \"could not be checked\" is not \"checked and unchanged\". --strict fails on every class; --fail-on-skill-drift adds skill/shared-root. See SPEC \u00a711 for per-class semantics.",
      "items": {
        "type": "object",
        "required": ["class", "message"],
        "properties": {
          "class": {
            "type": "string",
            "enum": [
              "baseline",
              "skill",
              "shared-root",
              "format",
              "unverifiable-baseline",
              "unverifiable-skill",
              "resolved-tier",
              "unverifiable-tier",
              "prompt-assets",
              "unverifiable-prompt-assets"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      }
    },
    "skippedAssertions": {
      "type": "object",
      "description": "Replay only: count of live-only assertions NOT evaluated (full = whole assertion skipped; partial = content half ran, fs/egress half dropped). The skipped ones are absent from assertions[].",
      "properties": {
        "full": {
          "type": "number"
        },
        "partial": {
          "type": "number"
        }
      }
    },
    "toolResults": {
      "type": "array",
      "description": "Tool-result text at assertion-fidelity cap (10 KB per result). Used by tool_result_contains / tool_result_not_contains. `assertText` is preferred when present; falls back to `text` (500-char display cap) for cassettes recorded before this field was added.",
      "items": {
        "type": "object",
        "required": ["isError", "text"],
        "properties": {
          "toolUseId": {
            "type": "string"
          },
          "isError": {
            "type": "boolean"
          },
          "text": {
            "type": "string"
          },
          "assertText": {
            "type": "string"
          },
          "assertTextTruncated": {
            "type": "boolean",
            "description": "assertText was cut at the 10 KB assert cap \u2014 a substring miss is unverifiable, not a proven absence (#9)"
          }
        }
      }
    },
    "tasks": {
      "type": "array",
      "description": "Progress panel (\u00a76.1, M6): TaskCreate/TaskUpdate tool calls folded into structured tasks. Deleted tasks are omitted (never appear here). `status` is a plain string, not a narrow enum \u2014 this milestone's live verification only observed pending/in_progress/completed + no delete/cancel path.",
      "items": {
        "type": "object",
        "required": ["id", "subject", "status"],
        "properties": {
          "id": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "activeForm": {
            "type": "string"
          }
        },
        "additionalProperties": false
      }
    },
    "context": {
      "type": "object",
      "description": "Context/Connectors panel (\u00a76.2, M6). mcpServers is SDK-owned pass-through data (loosely typed). availableSkills is a later addition to this same field.",
      "properties": {
        "tools": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "mcpServers": {
          "type": "array",
          "description": "SDK-owned per-server shape, pass-through \u2014 deliberately left loosely typed (open object) so extra SDK keys are preserved, not rejected.",
          "items": {
            "type": "object"
          }
        },
        "availableSkills": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id"],
            "properties": {
              "id": {
                "type": "string"
              },
              "whenToUse": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "mutation": {
      "type": "object",
      "description": "Replay lane only, and only under --mutate: which recorded values were perturbed and which perturbations no assertion caught. `sampled` is post-cap; `eligible` is the pre-cap total, so a consumer can distinguish 'N unguarded fields' from 'N unguarded OUT OF eligible'. `truncatedBy` names the cap that bound \u2014 raising the other one would not change the sample.",
      "properties": {
        "sampled": {
          "type": "number"
        },
        "eligible": {
          "type": "number"
        },
        "truncatedBy": {
          "type": ["string", "null"],
          "enum": ["per-file", "total", null]
        },
        "caps": {
          "type": "object",
          "properties": {
            "perFile": {
              "type": "number"
            },
            "total": {
              "type": "number"
            }
          }
        },
        "uncaught": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    }
  }
}
