{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://loopops.dev/schema/handoff-packet.json","title":"LoopOps Issue Handoff Packet","description":"The canonical per-issue handoff object — the snapshot a coding agent picks up when a human hands it one diagnosed issue (prior art: GitHub Copilot's cloud agent snapshots the issue at assign; see AGENTS.md ledger). One builder produces it for the MCP read (loopops_issue), the console's handoff control, and the copyable-prompt fallback, so those surfaces cannot drift. It binds issue identity, diagnosis, the latest attributed decision receipt, bounded evidence references, repository context, loop history, success criteria, and the approval constraints into one schema-backed object. It is a READ projection: producing or copying it writes nothing anywhere.","type":"object","additionalProperties":false,"required":["packet","generated_at","issue","project","diagnosis","decision_receipt","evidence","loop_history","success_criteria","approval"],"properties":{"packet":{"const":"loopops.handoff/v2"},"generated_at":{"type":"string","minLength":1},"issue":{"type":"object","additionalProperties":false,"required":["fingerprint","id","title","state","decision","severity"],"properties":{"fingerprint":{"type":"string","pattern":"^if_[a-z0-9_]{1,120}$","description":"The immutable grouping and lookup key captured at admission (issueFingerprint cascade: rule → cluster_id → normalized title). It is not the canonical Issue identifier."},"id":{"type":"string","minLength":1,"maxLength":200,"description":"The canonical opaque, project-scoped Issue identifier. Persist this when linking decisions and work; fingerprint remains a stable lookup alias."},"title":{"type":"string","minLength":1,"maxLength":300},"state":{"type":"string","enum":["needs_decision","selected","in_progress","awaiting_verification","verification_blocked","deferred","observing","skipped","verified","regression"]},"decision":{"type":"string","enum":["patch","observe","defer","skip","unknown"]},"severity":{"type":"string","enum":["error","warn"]},"issue_class":{"enum":["tooling","trajectory",null]},"harness_area":{"oneOf":[{"type":"null"},{"type":"string","maxLength":200}]},"priority":{"type":"number"},"first_observed_at":{"oneOf":[{"type":"null"},{"type":"string"}]}}},"project":{"type":"object","additionalProperties":false,"required":["project_id"],"properties":{"project_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1,"description":"Human-facing project name; project_id remains the immutable storage and authorization join."},"default_environment":{"oneOf":[{"type":"null"},{"type":"string"}],"description":"The project's fallback telemetry environment, not an access scope."},"environment":{"oneOf":[{"type":"null"},{"type":"string"}]},"repository_full_name":{"oneOf":[{"type":"null"},{"type":"string"}],"description":"owner/repo when the project is linked to a repository; null means the agent must ask the human which repo serves this project."},"repository_url":{"oneOf":[{"type":"null"},{"type":"string"}]}}},"diagnosis":{"type":"object","additionalProperties":false,"required":["diagnosed_at","diagnosed_by","recommended_next"],"properties":{"diagnosed_at":{"oneOf":[{"type":"null"},{"type":"string"}],"description":"occurred_at of the diagnosis event; null only when legacy provenance was not recorded. Undiagnosed clusters are not issues and never receive handoff packets."},"diagnosed_by":{"oneOf":[{"type":"null"},{"type":"string"}],"description":"Coding harness recorded on the diagnosis event itself, kept separate from the agent currently working the issue."},"hypothesis":{"oneOf":[{"type":"null"},{"type":"string","maxLength":2000}]},"recommended_next":{"type":"string","maxLength":2000,"description":"The diagnosing agent's recommended next action. It is advisory and has no Issue-disposition authority; only decision_receipt does."}}},"decision_receipt":{"description":"The latest attributed internal Issue disposition. Null means the recommendation is still awaiting a human or explicit policy decision. This receipt never approves an external write.","oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["record_id","decision","decided_by","decided_at","decided_via","rationale","approval_scope"],"properties":{"record_id":{"type":"string","minLength":1,"maxLength":200,"description":"Immutable telemetry record carrying this receipt."},"decision":{"type":"string","enum":["patch","observe","defer","skip"]},"decided_by":{"type":"string","minLength":1,"maxLength":300},"decided_at":{"type":"string","minLength":1},"decided_via":{"type":"string","enum":["cf_access","api_self_reported"],"description":"Identity provenance: verified Access identity or key-authenticated API self-report."},"rationale":{"type":"string","minLength":1,"maxLength":2000},"approval_scope":{"const":"issue_disposition"}}}]},"evidence":{"type":"object","additionalProperties":false,"required":["occurrences","evidence_ids","how_to_read"],"properties":{"cluster_id":{"oneOf":[{"type":"null"},{"type":"string","pattern":"^[a-z0-9_]{1,120}$"}],"description":"Stable cluster join key when the issue clustered from telemetry — resolve raw records via loopops_telemetry({ cluster_id })."},"occurrences":{"type":"number","minimum":0},"evidence_ids":{"type":"array","maxItems":20,"items":{"type":"string"},"description":"Bounded telemetry record_ids the diagnosis stands on — resolvable one by one, never the whole lake (same bound as loop-run.schema.json)."},"how_to_read":{"type":"string","description":"The exact MCP calls that resolve this evidence, so any harness can follow them."}}},"loop_history":{"type":"array","maxItems":20,"description":"Prior fix attempts on this issue, most advanced first — what was already tried, by whom, and whether it held. Empty = no attempt yet.","items":{"type":"object","additionalProperties":false,"required":["run_id","furthest_stage","merged","verified","verification_blocked","verification_blocked_reason","verification_attempts","verification_failed","verification_failed_verdict","verification_failed_reason"],"properties":{"run_id":{"type":"string"},"furthest_stage":{"type":"string"},"coding_agent":{"oneOf":[{"type":"null"},{"type":"string"}],"description":"Self-reported harness attribution from loopops_improve — not authenticated."},"approved_by":{"oneOf":[{"type":"null"},{"type":"string"}]},"issue_url":{"oneOf":[{"type":"null"},{"type":"string"}]},"pr_url":{"oneOf":[{"type":"null"},{"type":"string"}]},"pr_number":{"oneOf":[{"type":"null"},{"type":"number"}]},"merged":{"type":"boolean"},"verified":{"type":"boolean"},"verification_blocked":{"type":"boolean","description":"True when automatic verification exhausted its bounded retries without comparable evidence."},"verification_blocked_reason":{"oneOf":[{"type":"null"},{"type":"string"}]},"verification_attempts":{"oneOf":[{"type":"null"},{"type":"number"}]},"verification_failed":{"type":"boolean","description":"True when the server completed an exact joined comparison and the issue did not improve."},"verification_failed_verdict":{"oneOf":[{"type":"null"},{"type":"string"}]},"verification_failed_reason":{"oneOf":[{"type":"null"},{"type":"string"}]},"delta":{"oneOf":[{"type":"null"},{"type":"string"}],"description":"Human-readable before→after yield when the attempt verified."}}}},"success_criteria":{"type":"object","additionalProperties":false,"required":["definition","verification"],"properties":{"definition":{"type":"string","description":"What 'fixed' means for this issue, stated against telemetry — not against 'the PR merged'."},"verification":{"type":"object","additionalProperties":false,"required":["tool","metric"],"properties":{"tool":{"const":"loopops_verify"},"metric":{"type":"string","enum":["failure_rate","feedback_rate"]},"guidance":{"type":"string"}}}}},"approval":{"type":"object","additionalProperties":false,"required":["required","constraints"],"properties":{"required":{"const":true,"description":"Fixed product-policy invariant — never a per-project setting (AGENTS.md)."},"constraints":{"type":"array","minItems":1,"items":{"type":"string"}}}}}}