{
  "$schema": "./registry.schema.json",
  "version": "1",
  "description": "Deft framework event registry -- unified structural artifact listing every framework event in a single data file, partitioned by the required `category` enum (`detection-bound` | `behavioral`). Wired under epic #635 per the canonical #642 workflow comment, unified per the Repair Authority [AXIOM] proposal (#709) and the data-file-convention check follow-up (#710); the prior `events/behavioral.yaml` registry has been folded into this file. Detection-bound events have detectors that already exist in the framework; behavioral events require runtime instrumentation (agent emission point or script-internal capture). Each entry pins an event name, category, payload contract, detector / emission pointer, and consumer pointers. The shape of an emitted event record is governed by event-record.schema.json. Two emission helpers consume this registry: scripts/_event_detect.py for detection-bound events (validates against the full registry) and scripts/_events.py for behavioral events (validates against the behavioral subset, manages 1:1 session-pair invariants, persists to a JSONL log).",
  "events": [
    {
      "name": "pre-cutover:detected",
      "category": "detection-bound",
      "description": "Pre-v0.20 legacy artifacts present at the project root: SPECIFICATION.md or PROJECT.md exist without the deft:deprecated-redirect sentinel. Indicates the project has not yet completed the vBRIEF-centric document model migration (#312, RFC #309).",
      "trigger": "run::_check_upgrade_gate calls run::_detect_pre_cutover_legacy and emits when SPECIFICATION.md or PROJECT.md exist at project root without the redirect sentinel. Emission flows through scripts/_event_detect.py::emit.",
      "payload": {
        "project_root": "string -- absolute path to the project root scanned",
        "legacy_files": "array<string> -- filenames at project root that lack the redirect sentinel"
      },
      "consumers": [
        "skills/deft-directive-setup/SKILL.md Pre-Cutover Detection Guard -- routes the operator to task migrate:vbrief",
        "run::_check_upgrade_gate -- emits a pre-v0.20 warning",
        "QUICK-START.md Step 2c -- equivalent prose-encoded detection"
      ]
    },
    {
      "name": "vbrief:invalid",
      "category": "detection-bound",
      "description": "One or more vBRIEF files failed validator D1-D11 checks (schema, status enum, lifecycle folder mapping, filename convention, narratives, items, references) or post-migration placeholder integrity. Emitted by scripts/vbrief_validate.py on every run that surfaces errors or warnings.",
      "trigger": "scripts/vbrief_validate.py validate_all collects errors and warnings across the vbrief/ tree; emits when len(errors) > 0 or len(warnings) > 0. Existing exit-code semantics (0 with warnings, 1 on errors, 1 with --warnings-as-errors) are unchanged.",
      "payload": {
        "vbrief_dir": "string -- absolute path to the vbrief/ directory scanned",
        "error_count": "integer -- D1-D11 hard errors",
        "warning_count": "integer -- D1-D11 warnings (origin missing, render staleness, deprecated placeholder)",
        "errors": "array<string> -- error messages (capped at 50 to keep payload bounded)",
        "warnings": "array<string> -- warning messages (capped at 50)"
      },
      "consumers": [
        "tasks/vbrief.yml validate -- consumed via exit code today; event surface is the structured equivalent",
        "Taskfile.yml check (deps: vbrief:validate) -- pre-commit gate"
      ]
    },
    {
      "name": "agents-md:stale",
      "category": "detection-bound",
      "description": "AGENTS.md references one or more skill paths that either do not exist on disk or contain the deft:deprecated-skill-redirect sentinel. Indicates the consumer's AGENTS.md was generated by a prior framework version and needs the QUICK-START refresh path. Detection logic codified from QUICK-START.md Step 2b prose; the canonical 200-character window is preserved verbatim.",
      "trigger": "scripts/_event_detect.py::detect_agents_md_stale parses AGENTS.md for deft/skills/<name>/SKILL.md tokens, then checks each path's existence and the first 200 characters for the deprecated-skill-redirect sentinel. Emits when missing_paths or redirect_paths is non-empty.",
      "payload": {
        "agents_md_path": "string -- absolute path to AGENTS.md",
        "missing_paths": "array<string> -- referenced skill paths absent from disk",
        "redirect_paths": "array<string> -- referenced skill paths whose first 200 chars contain the deft:deprecated-skill-redirect sentinel"
      },
      "consumers": [
        "QUICK-START.md Step 2b / Case G -- routes the operator to refresh AGENTS.md from templates/agents-entry.md",
        "skills/deft-directive-sync/SKILL.md -- detects stale references during sync"
      ]
    },
    {
      "name": "version:drift",
      "category": "detection-bound",
      "description": "vbrief/.deft-version (or fallback project-root .deft-version) records a framework version that differs from the current run::VERSION constant. Indicates the consumer ran an older framework against this project last and may need task migrate:vbrief or .deft/core/run upgrade.",
      "trigger": "run::_check_upgrade_gate compares _read_version_marker() to VERSION; emits when both are non-None and unequal. The deft-repo self-detection short-circuit (run::_running_inside_deft_repo) suppresses the gate but does not suppress the event so framework maintainers can still observe drift in test fixtures.",
      "payload": {
        "project_root": "string -- absolute path to the consumer project root",
        "recorded_version": "string -- version read from .deft-version",
        "current_version": "string -- run.VERSION (the framework version currently executing)",
        "marker_path": "string -- absolute path of the .deft-version file that won the precedence rule (vbrief/.deft-version > .deft-version)"
      },
      "consumers": [
        "run::cmd_upgrade -- writes the marker to clear drift",
        "skills/deft-directive-sync/SKILL.md -- routes the operator through the upgrade flow"
      ]
    },
    {
      "name": "dirty-tree:detected",
      "category": "detection-bound",
      "description": "git status --porcelain reports uncommitted changes in the project root. Migration is destructive by default; the migrator refuses to run with a dirty tree unless --force is set (#497-3). The event is emitted whenever the dirty-tree guard trips so any orchestrator (skill, task, CI) can react uniformly.",
      "trigger": "scripts/_vbrief_safety.py::is_tree_dirty returns True; the migrator's dirty-tree refusal call site (migrate_vbrief.migrate, line ~1169) emits before returning the canonical refusal message.",
      "payload": {
        "project_root": "string -- absolute path of the project root checked"
      },
      "consumers": [
        "tasks/migrate.yml migrate:vbrief -- the gate this event annotates",
        "skills/deft-directive-pre-pr/SKILL.md -- pre-PR loop already requires a clean tree before the lint phase"
      ]
    },
    {
      "name": "framework:remote-drift",
      "category": "detection-bound",
      "description": "The directive framework's upstream remote ships a higher semver tag than the run::VERSION currently executing in the consumer's project. Indicates the consumer's `./deft` submodule (or framework checkout) is behind upstream and a `task sync` / `git submodule update --remote --merge deft` would pull a new release. Emitted by run::_maybe_emit_remote_drift_warning (#801) only when the read-only `git ls-remote --tags --refs <upstream>` probe returns BEHIND, throttled per-tag at 24h via vbrief/.deft-remote-probe.json.",
      "trigger": "run::_maybe_emit_remote_drift_warning runs the probe via _run_remote_probe (which shells out to `git ls-remote --tags --refs <upstream>` once per 24h, parses the highest semver tag, and compares it to VERSION). Emits when result.status == 'behind' AND the per-tag notification cadence allows. Honors DEFT_NO_NETWORK=1 (silent skip) and DEFT_FORCE_REMOTE_PROBE=1 (bypass throttle).",
      "payload": {
        "project_root": "string -- absolute path of the consumer project root scanned",
        "current_version": "string -- run.VERSION currently executing",
        "remote_version": "string -- highest semver tag visible on the upstream remote (with leading `v` preserved)",
        "upstream_url": "string -- upstream remote URL the probe consulted (origin of ./deft submodule, or origin of the run-rooted repo)",
        "commits_behind": "integer | null -- number of commits the consumer is behind upstream when computable, null when the probe cannot determine it without a fetch"
      },
      "consumers": [
        "run::_check_upgrade_gate -- emits the warn line and event from the gate-integration site",
        "skills/deft-directive-sync/SKILL.md Phase 2 -- the user-triggered remote refresh that resolves the drift",
        "tasks/framework.yml framework:check-updates -- the synchronous interactive surface that reuses the same probe"
      ]
    },
    {
      "name": "session:interrupted",
      "category": "behavioral",
      "description": "Agent detected a session-state shift (context window pressure, session resume, 'are you using Deft?' alignment probe) and is about to re-confirm Deft alignment per AGENTS.md. Paired with session:resumed -- every session:resumed MUST reference the id of a prior session:interrupted via payload.interrupted_id; orphan resumed records are surfaced by scripts/_events.py::validate_pairing.",
      "trigger": "Agent runtime emission point: scripts/_events.py::emit when AGENTS.md 'Deft Alignment Confirmation' fires (context-window-shift, alignment-probe, manual-resume). Skills emit via `python -m scripts._events emit session:interrupted --session-id <id> --reason <enum>`.",
      "payload": {
        "session_id": "string -- stable identifier for the agent session being interrupted; agents SHOULD reuse a single session_id across the lifetime of one logical conversation so multiple interrupt/resume pairs share provenance",
        "reason": "string enum -- one of context-window-shift | alignment-probe | manual-resume | other; agents SHOULD prefer the named values over 'other'",
        "detail": "string (optional) -- free-form one-liner with additional context"
      },
      "consumers": [
        "skills/deft-directive-sync/SKILL.md Framework Events Emitted Here -- emits the pair on every alignment confirmation",
        "scripts/_events.py::validate_pairing -- enforces the 1:1 session:interrupted -> session:resumed invariant"
      ]
    },
    {
      "name": "session:resumed",
      "category": "behavioral",
      "description": "Agent has finished re-confirming Deft alignment after a session:interrupted and is resuming work. MUST reference a prior session:interrupted event id via payload.interrupted_id; one interrupted_id may satisfy at most one session:resumed (1:1 pairing per Greptile #706 P2). Orphan or double-resumed records are flagged by scripts/_events.py::validate_pairing.",
      "trigger": "Agent runtime emission point: scripts/_events.py::emit immediately after the agent emits the alignment confirmation line. Skills emit via `python -m scripts._events emit session:resumed --session-id <id> --interrupted-id <id-from-prior-emit>`.",
      "payload": {
        "session_id": "string -- same identifier used in the matching session:interrupted",
        "interrupted_id": "string -- id of the matching session:interrupted event; cross-referenced by scripts/_events.py::validate_pairing",
        "detail": "string (optional) -- free-form one-liner with additional context"
      },
      "consumers": [
        "skills/deft-directive-sync/SKILL.md Framework Events Emitted Here -- emits the pair on every alignment confirmation",
        "scripts/_events.py::validate_pairing -- enforces the 1:1 session:interrupted -> session:resumed invariant"
      ]
    },
    {
      "name": "plan:approved",
      "category": "behavioral",
      "description": "User approved a plan reaching the Phase 5 -> 6 merge gate per skills/deft-directive-review-cycle and the canonical #642 workflow comment ready-to-merge templates. Emitted when the user types yes / confirmed / approve on a ready-to-merge PR thread or equivalent gate. Downstream effects (auto-merge, status update, notifications) are explicitly deferred to follow-up work; this event currently emits a record only.",
      "trigger": "Agent runtime emission point: scripts/_events.py::emit when a skill detects an affirmative reply (yes / confirmed / approve) on a ready-to-merge PR thread. Skills emit via `python -m scripts._events emit plan:approved --plan-ref <pr-url> --approver <login> --approval-phrase <yes|confirmed|approve> --pr-number <N>`.",
      "payload": {
        "plan_ref": "string -- pointer to the plan being approved; format: GitHub PR URL (preferred) or vBRIEF path; agents SHOULD use the canonical URL when available",
        "approver": "string -- identifier of the approving user (GitHub login when available, 'user' otherwise)",
        "approval_phrase": "string enum (optional) -- one of yes | confirmed | approve | other; the exact affirmative phrase the user used, normalised to lowercase; skills SHOULD prefer one of the canonical phrases per the canonical #642 workflow comment",
        "pr_number": "integer (optional) -- GitHub PR number, when applicable"
      },
      "consumers": [
        "skills/deft-directive-review-cycle/SKILL.md Framework Events Emitted Here -- Phase 5 -> 6 merge gate emission",
        "(deferred -- no downstream consumer in this PR; future auto-merge / status-update / audit handlers will subscribe by name)"
      ]
    },
    {
      "name": "legacy:detected",
      "category": "behavioral",
      "description": "Migrator captured a non-canonical pre-cutover section (SPECIFICATION.md / PROJECT.md / PRD.md hand-edit) to vbrief/legacy/ or an inline LegacyArtifacts narrative during task migrate:vbrief (#505). Emitted once per captured section; additive to the existing vbrief/migration/LEGACY-REPORT.md write -- this event is structural emission only. Action policy (warn vs auto-migrate vs block) is explicitly out of scope for this PR and is deferred to follow-up.",
      "trigger": "Script-internal emission point: scripts/_vbrief_legacy.emit_legacy_artifacts invokes the event_emitter callback wired by scripts/migrate_vbrief.py once per captured section. Emission flows through scripts/_events.py::emit (the migrator wires a project-root-scoped emitter; failures are swallowed so legacy capture remains the primary contract).",
      "payload": {
        "title": "string -- original section heading (e.g. 'Open Questions')",
        "source": "string -- source filename (e.g. SPECIFICATION.md, PROJECT.md, PRD.md)",
        "range": "string -- 1-indexed line range (e.g. '12-34' or '12')",
        "size_bytes": "integer -- UTF-8 byte size of the captured section body",
        "inline": "boolean (optional) -- true if the body fit inline; false if it overflowed to a sidecar at vbrief/legacy/{stem}-{slug}.md",
        "sidecar": "string (optional) -- sidecar relative path when inline is false; null/absent otherwise",
        "flagged": "boolean (optional) -- true for PRD.md hand-edit captures (warning prefix applied)"
      },
      "consumers": [
        "scripts/migrate_vbrief.py -- wires the emitter into all three _emit_legacy_artifacts call sites (SPECIFICATION / PROJECT / PRD captures)",
        "(deferred -- no downstream handler in this PR; future migrate:vbrief audit reporting and consumer-facing legacy-handling skills will subscribe by name)"
      ]
    },
    {
      "name": "value:gate-catch",
      "category": "behavioral",
      "description": "A detection-bound gate refused an action (branch protection, session ritual, encoding, etc.) while value feedback is enabled. Powers attributed value readbacks (#1709).",
      "trigger": "packages/core/src/events/attribution-ledger.ts::recordGateCatch when verify:branch (or other wired gate) returns a blocking exit and plan.policy.valueFeedback.emitEvents is allowed.",
      "payload": {
        "signal_class": "string enum -- always `value` for this event name",
        "source": "string -- canonical gate id (e.g. verify:branch)",
        "detail": "string -- one-line human context for the catch"
      },
      "consumers": [
        "packages/core/src/events/attribution-ledger.ts -- emit helper gated on valueFeedback",
        "(deferred) skills/deft-directive-sync -- budgeted session readback (#1709 child 4)"
      ]
    },
    {
      "name": "value:wip-cap-protect",
      "category": "behavioral",
      "description": "verify:wip-cap refused a merge/promote because pending+active count exceeds plan.policy.wipCap (#1124).",
      "trigger": "packages/core/src/wip-cap/evaluate.ts when count >= cap without --allow-over-cap and valueFeedback emitEvents is allowed.",
      "payload": {
        "signal_class": "string enum -- always `value`",
        "source": "string -- verify:wip-cap",
        "count": "integer -- in-flight scope count",
        "cap": "integer -- resolved wipCap"
      },
      "consumers": [
        "packages/core/src/events/attribution-ledger.ts::recordWipCapProtect",
        "(deferred) task deft:value:show trend readout (#1709 child 4)"
      ]
    },
    {
      "name": "bypass:off-flow",
      "category": "behavioral",
      "description": "Consumer went off-flow (skipped checks, wrote code without active vBRIEF, skipped pre-PR, etc.) while value feedback is enabled.",
      "trigger": "packages/core/src/events/attribution-ledger.ts::recordBypassSignal from wired bypass detectors (initial taxonomy entry; sources land in follow-up stories).",
      "payload": {
        "signal_class": "string enum -- always `bypass`",
        "source": "string -- detector id",
        "detail": "string -- one-line context"
      },
      "consumers": [
        "packages/core/src/events/attribution-ledger.ts",
        "(deferred) boundary-awareness readback (#1709 child 4)"
      ]
    },
    {
      "name": "adoption:unused-capability",
      "category": "behavioral",
      "description": "Directive capability was applicable to the current work but unused (decompose, cost, pre-PR, etc.).",
      "trigger": "packages/core/src/events/attribution-ledger.ts::recordAdoptionSignal and future adoption-registry module (#1709 child 3).",
      "payload": {
        "signal_class": "string enum -- always `adoption`",
        "source": "string -- registry module id",
        "capability": "string -- capability slug (e.g. decompose, pre-pr)",
        "detail": "string -- one-line applicability context"
      },
      "consumers": [
        "packages/core/src/events/attribution-ledger.ts",
        "(deferred) packages/core/src/value/adoption-registry.ts (#1709 child 3)"
      ]
    },
    {
      "name": "friction:directive-gap",
      "category": "behavioral",
      "description": "Directive fell short (improvised workflow, gate false-positive, ambiguous rule) and may warrant upstream feedback.",
      "trigger": "packages/core/src/events/attribution-ledger.ts::recordFrictionSignal from friction detectors and gap-escalation skill (#1709 child 5).",
      "payload": {
        "signal_class": "string enum -- always `friction`",
        "source": "string -- detector id",
        "detail": "string -- one-line gap description"
      },
      "consumers": [
        "packages/core/src/events/attribution-ledger.ts",
        "(deferred) task deft:feedback:file / deft-directive-feedback skill (#1709 child 5)"
      ]
    },
    {
      "name": "session:start",
      "category": "behavioral",
      "description": "Mutation session:start ceremony finished (cold or re-arm). Local process-cost observability for ceremony wall-clock (#2994 / WWYSYDH pilot). Always-on best-effort local JSONL; not gated on valueFeedback; no remote upload.",
      "trigger": "packages/core/src/session/process-cost.ts::emitSessionStartProcessCost from packages/core/src/session/session-start.ts after cold or re-arm ceremony completes and ritual state is written.",
      "payload": {
        "ceremony_tier": "string enum -- cold | rearm",
        "duration_ms": "integer -- total ceremony wall-clock milliseconds",
        "exit_code": "integer -- 0 when ready, non-zero when a quick step failed",
        "ready": "boolean (optional) -- true when exit_code is 0",
        "optional_network": "boolean (optional) -- whether optional network steps ran (#2991)",
        "steps": "array (optional) -- { name, duration_ms, skipped? } per major phase from session:start --json"
      },
      "consumers": [
        "packages/core/src/session/process-cost.ts -- emit helper",
        "packages/core/src/session/session-start.ts -- cold + re-arm completion call sites",
        "content/commands.md § Process-cost events -- WWYSYDH ceremony label mapping (#2994)"
      ]
    },
    {
      "name": "session:ritual-blocked",
      "category": "behavioral",
      "description": "PreToolUse denied a mutation tool because the gated session ritual was not ready (missing/stale/invalid). Local process-cost / friction signal for ceremony blocks (#2994). Always-on best-effort local JSONL; not gated on valueFeedback; no remote upload.",
      "trigger": "packages/core/src/session/process-cost.ts::emitSessionRitualBlockedProcessCost from packages/core/src/hooks/dispatcher.ts::inspectMutationGates when inspectSessionRitual returns non-zero or throws (ritual-not-ready deny path).",
      "payload": {
        "tool_name": "string -- host tool name that was denied",
        "code": "string -- always ritual-not-ready for this event",
        "recovery_tier": "string enum (optional) -- cold | rearm -- preferred recovery path (#2992)",
        "detail": "string (optional) -- one-line inspect message (not the full recovery instruction)"
      },
      "consumers": [
        "packages/core/src/session/process-cost.ts -- emit helper",
        "packages/core/src/hooks/dispatcher.ts -- PreToolUse ritual deny path",
        "content/commands.md § Process-cost events -- WWYSYDH ceremony label mapping (#2994)"
      ]
    }
  ]
}
