{
  "$comment": [
    "THE KIT MANIFEST — the single description of what PostMQ can install into a user's project,",
    "read by every surface that installs it: the MCP resource postmq://kit/manifest.json, the",
    "`pmq kit install` command, the published files on postmq.com/kit, and the Claude Code plugin.",
    "",
    "One description, four consumers. A component described here is offered identically everywhere,",
    "and a component added here is offered everywhere on the next release without a second edit.",
    "",
    "`source` paths are repo-relative and are what gets embedded into the shipped assembly. Every",
    "one of them is asserted to exist by tests/PostMQ.Tooling.Tests/KitContentTests.cs, so a",
    "renamed or deleted file fails the build rather than producing an installer that 404s halfway",
    "through writing a user's .claude directory.",
    "",
    "FOUR CLIENTS, ONE SOURCE. `clients` lists the clients a component installs into. `target` is",
    "where a file lands in the DEFAULT client (claude-code); `client_targets` overrides that path",
    "per client, and `client_settings_merge` does the same for the settings block. Note what is",
    "deliberately NOT here: a second copy of any file. A component has exactly one `source` per",
    "file whatever the client, so the SHARED entries in the .claude, .agents and .zcode trees are",
    "paths onto the same bytes and cannot drift apart — there is nothing to keep in sync. The trees as a WHOLE are not identical and should not be described that way: ZCode gets a wrapper of its own, and each client's wiring differs (Antigravity has none).",
    "KitClientTargetTests enforces that.",
    "",
    "ANTIGRAVITY IS THE FIRST CLIENT WITH NO SETTINGS MERGE, and the reason is measured rather",
    "than assumed. Its binary carries a hook system (PreInvocationHook, StopHook, PreToolHook and",
    "an injectSteps/ephemeralMessage payload), and ~/.gemini/config/hooks.json is a real discovery",
    "path that parses into `named hooks` — but on Antigravity 2.8.0 no hook could be made to FIRE",
    "across seven binding spellings, and a project-scoped .agents/hooks.json was never loaded at",
    "all. So there is nothing to wire, and shipping a snippet that wires nothing would be a file",
    "that reports success and does nothing. Instead its two scripts install under `.agents/tools/`",
    "rather than `.agents/hooks/` — they are not hooks on this client — and the two SKILL.md files",
    "tell the agent to RUN them. That is weaker than an event and is stated as such in",
    "`honest_limits`. Boundary on the negative result: only the CLI in print mode was driven; the",
    "IDE may behave differently.",
    "",
    "A file with a `target` installs for the default client; a file with only `client_targets`",
    "installs for exactly the clients it names (the Codex/ZCode SessionEnd wrapper and the ZCode",
    "SessionStart wrapper are the cases).",
    "",
    "A settings snippet is merged by MIRRORING ITS SHAPE into the target: an array of hook entries",
    "is appended to the array at the same path, and a scalar leaf is set when the target does not",
    "already carry one. That is what lets `hooks.SessionStart` (Claude Code, Codex) and",
    "`hooks.events.SessionStart` + `hooks.enabled` (ZCode) both work with no per-client branch in",
    "the installer. `merge_at` names the path a reader should look at; the snippet is the truth."
  ],
  "kit_version": "1.1.0",
  "components": [
    {
      "id": "session-state",
      "title": "Session-state kit",
      "one_line": "A /start-session and /end-session protocol, so your agent records how the project got built instead of losing it in a chat log.",
      "what_it_does": "start-session recovers where the last session stopped — the active build session and the lead it left, the last ten decisions, the open backlog — and asks which rules apply before touching a file. end-session appends a decision-log entry for each material decision, files a backlog item for anything deferred, resolves what was finished, and closes the session with a history entry the next session reads first.",
      "offer_when": "Always. The session-state tools are most of the PostMQ catalogue and they only pay off if something calls them at the right moments.",
      "clients": [
        "claude-code",
        "codex",
        "zcode",
        "antigravity"
      ],
      "requires_scopes": [
        "write_session_state"
      ],
      "files": [
        {
          "source": "kit/session-state/skills/start-session/SKILL.md",
          "target": ".claude/skills/start-session/SKILL.md",
          "client_targets": {
            "codex": ".agents/skills/start-session/SKILL.md",
            "zcode": ".zcode/skills/start-session/SKILL.md",
            "antigravity": ".agents/skills/start-session/SKILL.md"
          }
        },
        {
          "source": "kit/session-state/skills/end-session/SKILL.md",
          "target": ".claude/skills/end-session/SKILL.md",
          "client_targets": {
            "codex": ".agents/skills/end-session/SKILL.md",
            "zcode": ".zcode/skills/end-session/SKILL.md",
            "antigravity": ".agents/skills/end-session/SKILL.md"
          }
        },
        {
          "source": "kit/session-state/hooks/postmq-session-start.sh",
          "target": ".claude/hooks/postmq-session-start.sh",
          "executable": true,
          "client_targets": {
            "codex": ".codex/hooks/postmq-session-start.sh",
            "zcode": ".zcode/hooks/postmq-session-start.sh",
            "antigravity": ".agents/tools/postmq-session-start.sh"
          }
        },
        {
          "source": "kit/session-state/hooks/postmq-session-start.zcode.sh",
          "executable": true,
          "client_targets": {
            "zcode": ".zcode/hooks/postmq-session-start.zcode.sh"
          }
        }
      ],
      "settings_merge": {
        "source": "kit/session-state/settings.snippet.json",
        "target": ".claude/settings.json",
        "merge_at": "hooks.SessionStart",
        "note": "APPEND to the existing SessionStart array if there is one. Replacing it silently disables whatever else was wired there."
      },
      "verify": "Restart the client, then run the start-session skill (`/start-session` in Claude Code and ZCode, `$start-session` in Codex, `/start-session` in Antigravity). It should name the active session (or start one) and print the drift block from the hook. On Codex and ZCode, approve the hook when it asks — until you do, the skill still works and the drift block is simply absent. On ZCode the drift block also names this session's id, which is what the start-session skill uses to name the build-session pointer file. On Antigravity there is no hook to approve and the kit wires none (no hook could be made to fire on its CLI; its IDE was not tested): the drift block appears only because the skill runs `.agents/tools/postmq-session-start.sh` itself, so if the skill is not run there is no block at all.",
      "client_settings_merge": {
        "codex": {
          "source": "kit/session-state/codex-hooks.snippet.json",
          "target": ".codex/hooks.json",
          "merge_at": "hooks.SessionStart",
          "note": "APPEND to the existing SessionStart array if there is one. The command path is relative because a Codex hook runs with its working directory set to the project root — there is no $CLAUDE_PROJECT_DIR equivalent and none is needed. Codex will not run a project hook until it has been reviewed once: an interactive session asks at startup and remembers the approval against a hash of the hook config, so editing it asks again."
        },
        "zcode": {
          "source": "kit/session-state/zcode-hooks.snippet.json",
          "target": ".zcode/config.json",
          "merge_at": "hooks.events.SessionStart",
          "note": "APPEND to the existing SessionStart array if there is one. TWO things differ from the other two clients. The events nest under `hooks.events`, and the whole runner is gated on `hooks.enabled`, which defaults to FALSE — a hook installed without it is loaded, listed and never run. And the wired command is the .zcode.sh WRAPPER, not the shared script: ZCode discards a hook's plain-text stdout silently, so the upstream script alone would run and inject nothing. ZCode reviews a project hook once before running it, against a digest of the declaration."
        }
      }
    },
    {
      "id": "usage-reporter",
      "title": "Usage reporter",
      "one_line": "Sums the session's token usage and records it against the build session \u2014 a SessionEnd hook on Claude Code and Codex, a Stop hook on ZCode, and a command the end-session skill runs on Antigravity.",
      "what_it_does": "Reads the finished session's transcript and any sub-agent transcripts under it, sums the five token classes and the turn counts, and reports them with record_build_session_usage. Reads Claude Code transcripts, Codex rollouts, ZCode rollouts and Antigravity conversation databases, and reports which client it read. Idempotent per transcript, so a re-report overwrites and never double-counts. One Python file, standard library only.",
      "offer_when": "Claude Code, Codex, ZCode and Antigravity users, once the session-state kit is in — it reports against the build session that kit opens. It is not useful on its own.",
      "clients": [
        "claude-code",
        "codex",
        "zcode",
        "antigravity"
      ],
      "requires_scopes": [
        "write_session_state"
      ],
      "requires": [
        "session-state"
      ],
      "files": [
        {
          "source": "tools/hooks/postmq-usage-reporter/postmq-usage-reporter.py",
          "target": ".claude/hooks/postmq-usage-reporter.py",
          "client_targets": {
            "codex": ".codex/hooks/postmq-usage-reporter.py",
            "zcode": ".zcode/hooks/postmq-usage-reporter.py",
            "antigravity": ".agents/tools/postmq-usage-reporter.py"
          }
        },
        {
          "$comment": "No antigravity entry, deliberately. The wrapper exists to return inside a host's hook timeout; nothing on Antigravity invokes the reporter from a timed hook, so installing it would ship a file with no caller.",
          "source": "tools/hooks/postmq-usage-reporter/postmq-usage-reporter-detach.sh",
          "client_targets": {
            "codex": ".codex/hooks/postmq-usage-reporter-detach.sh",
            "zcode": ".zcode/hooks/postmq-usage-reporter-detach.sh"
          },
          "executable": true
        }
      ],
      "settings_merge": {
        "source": "tools/hooks/postmq-usage-reporter/hooks.json",
        "target": ".claude/settings.json",
        "merge_at": "hooks.SessionEnd",
        "note": "APPEND to the existing SessionEnd array if there is one. On Windows the command is `py -3` rather than `python3`."
      },
      "honest_limits": "The numbers are self-reported — they are as good as the client that reports them. PostMQ stores and rolls up token counts; it does not meter your model provider, and it prices nothing. On Codex and ZCode two figures are coarser than on Claude Code, because neither transcript carries the finer ones: cache writes are not split by TTL (they are recorded in the 5-minute class), and there is no sub-agent turn count, so sidechain_turns is 0. ZCode has one further limit that is its own, not the reporter's: it keeps only the three most recent rollout files and truncates any that passes 64 MB, so a very long session can be reported from less than all of itself. ANTIGRAVITY IS WEAKER THAN THE OTHER THREE IN ONE WAY THAT MATTERS AND TWO THAT DO NOT. The one that matters: no hook could be made to fire on Antigravity 2.8.0 (measured against its CLI; its IDE was not tested), so nothing reports automatically — the end-session skill runs the reporter, which means a session that never runs end-session records no usage at all. On the other three a hook covers that case. The two that do not: cache-WRITE tokens are 0 because no field in the store records them (not because they are unparsed), and sidechain_turns is 0 because sub-agent work is written into the same trajectory rather than a separate transcript. Input, cache-read, output, turns and models come from the client's own per-request records rather than an estimate — output includes reasoning tokens, which is what the store reports as the total. A report is a SNAPSHOT of the conversation database when it runs, so a turn the client has not finished writing is not in it.",
      "verify": "End a session, then read get_usage. The session should carry a row with a non-zero turn count, and an `agent` matching the client you ran. On ZCode the row appears after the FIRST turn rather than at the end, and is rewritten as the session goes — see the note on its wiring. On Antigravity the row appears only once the end-session skill has run, because no event triggers the reporter there; if the row is missing, that is the first thing to check rather than a credential problem.",
      "client_settings_merge": {
        "codex": {
          "source": "tools/hooks/postmq-usage-reporter/codex-hooks.json",
          "target": ".codex/hooks.json",
          "merge_at": "hooks.SessionEnd",
          "note": "APPEND to the existing SessionEnd array if there is one. The command is the DETACH WRAPPER, never the reporter itself: a Codex SessionEnd hook is killed at 1 second by default and 3 at most, which a transcript read plus one network call does not fit inside."
        },
        "zcode": {
          "source": "tools/hooks/postmq-usage-reporter/zcode-hooks.json",
          "target": ".zcode/config.json",
          "merge_at": "hooks.events.Stop",
          "note": "APPEND to the existing Stop array if there is one. It is on Stop because ZCode HAS no SessionEnd — its seven events do not include one — so this reports once per turn rather than once per session. The report is an idempotent upsert, so the turns converge on one row rather than accumulating, and a per-field high-water mark refuses, best-effort, any report that would REDUCE the row — which is what stops a rollout ZCode truncated mid-session from ratcheting the total down. Best-effort because the mark is local: an unreadable one counts as no mark, and a send whose response is lost is never recorded. Two concurrent detached reports are ordered by the SERVER rather than here: the upsert is conditional on window_end, so a report older than the stored row's is discarded and the response carries applied false. What it buys is that a session which is never formally ended still has its usage recorded. The command is the detach wrapper so that per-turn reporting never sits between the user and an answer."
        }
      }
    }
  ]
}
