Skip to content

feat(formatters): emit data.changed so the per-session rewrote report fills - #3765

Merged
kyle-sexton merged 5 commits into
mainfrom
claude/formatters-data-changed-ji6rl5
Sep 5, 2026
Merged

feat(formatters): emit data.changed so the per-session rewrote report fills#3765
kyle-sexton merged 5 commits into
mainfrom
claude/formatters-data-changed-ji6rl5

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #3755

Summary

The per-session observability report's "Rewrote" block reads a changed boolean that the reference sink already copies from data.changed, and no producer sent it, so the block rendered the no-data line in every report. The eight rewriting formatters now send it.

Fix

  • lib/rewrite-guard.sh (synced into its six carriers): the take records a byte verdict in HOOK_REWRITE_CHANGED: true when the file differs from the snapshot, false when identical or when no rewrite was attempted (begin never ran), and empty when begin could not snapshot. The first take's verdict survives a later take, so a producer that emits after its take still reads it. Five new cases in lib/rewrite-guard.test.sh (22/22).
  • Six carriers (bash-format, biome-format, eol-normalizer, go-format, powershell-format, ruff-format): build_data_json adds changed from the verdict and omits the key when it is unknown, never guessing. Every arm that emitted telemetry before taking the disclosure (biome, go and ruff clean arms; go, powershell and ruff findings and tool-break arms) now takes first, with stdout unchanged. bash-format takes unconditionally before its emit so a run where shfmt never ran (no .editorconfig opt-in) reports false rather than nothing.
  • markdown-format derives it from the same "Attempted: N fixes" count line that drives its user-channel disclosure; typos-format from whether applied is non-empty, and its clean arm sends false. Both omit the key on skip arms where the tool never ran.
  • Eight docs/conventions/hook-telemetry/data/*.schema.json gain the optional changed boolean (additive rule).
  • claude-ops: the observability context now renders a row per changed == true envelope, _nothing rewritten_ when rows carry the key and every value is false, and the no-data line only when no row carries it; the suite fixture carries one changed: true and one changed: false row, and the case that asserted the block empty now asserts the file is named (60/60).
  • Nine patch bumps with CHANGELOG entries: bash-format 0.7.37, biome-format 0.6.35, eol-normalizer 0.6.36, go-format 0.3.39, powershell-format 0.7.38, ruff-format 0.6.36, markdown-format 0.11.45, typos-format 0.6.43, claude-ops 0.42.10.

actionlint is named in the issue but does not rewrite files, so it is left alone.

Fix (2): a description regression on main

Merging main after #3770 surfaced a defect that is not this change's. #3770 reverted plugins/claude-ops/.claude-plugin/plugin.json's description to a pre-#3750 state, dropping 730 characters: the per-session hook event log, the setup action's log-root and self-ignoring-guard wording, the sink's per-session routing, and audit-performance's Windows kernel-object census. Sentence-level comparison of the pre-#3770 and post-#3770 descriptions shows #3770 contributed no new wording to this field, so it is a clobber rather than an edit.

The conflict resolution restores the full text and regenerates docs/CATALOG.md from it, since the catalog is generated from plugin descriptions and would otherwise fail scripts/validate-plugins.sh.

Verification

  • scripts/affected-tests.sh --run: all 14 selected suites passed or were skipped. Suites run with their tool on this host: bash-format 54/54, eol-normalizer 54/54, ruff-format 63/63, typos-format 148/148, markdown-format 174/174, rewrite-guard 22/22, claude-observability 60/60. Each pins changed on a rewriting run and a no-op run.
  • Re-run on the merged head after the docs: apply the fleet-wide prompt-audit against Claude Fable 5.1 #3770 renumber: rewrite-guard 22/22, claude-observability 60/60, typos-format 148/148.
  • Not verified on this host: biome-format and go-format skip (no biome, no goimports), and powershell-format's behavioral cases skip (no PSScriptAnalyzer). Their new cases assert false on the existing lint-finding fixtures (already-formatted input) and true on a new unformatted fixture for biome and go; powershell asserts the key is a boolean, since its formatter settings decide the value. The test-linux lane is where those run.
  • scripts/sync-rewrite-guard.sh --check, scripts/check-changelog-parity.sh --check-bump origin/main, --check , scripts/check-shell-portability.sh origin/main, scripts/validate-plugins.sh: all pass on the merged head. shellcheck, shfmt, markdownlint, typos clean on the changed files (the one shfmt note in markdown-format.test.sh is a pre-existing line this PR does not touch).

Renumbered 0.42.9 to 0.42.10

#3770 bumped 64 plugins and took claude-ops 0.42.9. main was merged in, this change renumbered above it, and the CHANGELOG conflict resolved so #3770's released 0.42.9 entry stays intact with this change's entry above it. None of the eight formatter plugins were touched by #3770, so their numbers are unchanged.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_019DaWEB8Daq1xAXy2Xj1Pme

… fills

The per-session observability report's "Rewrote" block reads a changed
boolean the sink copies from data.changed, and no producer sent it. The
shared rewrite guard now records a byte verdict at the take
(HOOK_REWRITE_CHANGED: true, false, or unknown when it could not snapshot),
its six carriers add the key to their telemetry data, and markdown-format
and typos-format derive it from their own rewrite signals (the fix-count
line, applied corrections). biome, go, powershell and ruff took the
disclosure after emitting on some arms; those arms now take first, with
stdout unchanged. bash-format takes unconditionally before its emit so a
run where shfmt never ran reports false rather than nothing. Every data
schema gains the optional key; each suite pins it on a rewriting run and a
no-op run; the observability fixture carries one changed row and the case
that asserted the block empty now asserts the file is named. Nine patch
bumps: bash 0.7.37, biome 0.6.35, eol 0.6.36, go 0.3.39, powershell
0.7.38, ruff 0.6.36, markdown 0.11.45, typos 0.6.43, claude-ops 0.42.7.

Closes #3755

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DaWEB8Daq1xAXy2Xj1Pme
@kyle-sexton
kyle-sexton marked this pull request as ready for review September 5, 2026 18:01
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T18:09:41.377842Z 4ec6611 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ec661124a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/typos-format/hooks/typos-format.sh
Comment thread plugins/ruff-format/hooks/ruff-format.sh
typos-format's clean arm reached typos and knows the answer, so it now
sends changed: false instead of omitting the key; an all-clean session
reads "nothing rewritten" rather than "no data". ruff-format's tool-break
arm emitted telemetry before taking the disclosure, so a rewrite the fix
or format pass had already written was not recorded; the take now precedes
the emit as on every other arm. One suite case pins the clean verdict.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DaWEB8Daq1xAXy2Xj1Pme
…a-changed-ji6rl5

# Conflicts:
#	plugins/claude-ops/CHANGELOG.md
…a-changed-ji6rl5

# Conflicts:
#	plugins/claude-ops/.claude-plugin/plugin.json
#	plugins/claude-ops/CHANGELOG.md
@kyle-sexton
kyle-sexton merged commit c74d032 into main Sep 5, 2026
18 checks passed
@kyle-sexton
kyle-sexton deleted the claude/formatters-data-changed-ji6rl5 branch September 5, 2026 21:53
kyle-sexton added a commit that referenced this pull request Sep 5, 2026
…e at 1.1 (#3769)

Closes #3758

Its two bases (#3765, #3762) have merged, `main` is merged in, and the
diff is reduced to this change alone.

## Summary

The reference sink files an envelope per session only when the envelope
carries a session id, and only the nine claude-ops audit hooks sent one,
so the per-session report's "hooks fired" table never listed the
formatters or the guards. The library now puts the payload's correlation
keys on the envelope spine for every producer, at contract 1.1.

## Fix

- **`lib/hook-utils.sh`** (synced into its 17 carriers):
`hook::emit_telemetry` reads the payload the producer buffered
(`HOOK_TELEMETRY_PAYLOAD` when set, else `INPUT`, the variable every
fleet hook assigns from `hook::buffer_stdin`) and copies `session_id`,
`prompt_id`, `tool_use_id` and `agent_id` onto the envelope between
`duration_ms` and `data`, each only when present as a plain id
(`[A-Za-z0-9._-]+`), on both the builtin and the jq path. No jq, no
subprocess, no producer change. `schema_version` reads `1.1`.
- **`docs/conventions/hook-telemetry`**: `envelope.schema.json` gains
the four optional properties; the README gains a "Correlation keys"
section and rewrites the sink-routing note; the contract CHANGELOG
records 1.1 as an additive minor.
- **Sink** (`plugins/claude-ops/hooks/hook-telemetry-sink.sh` and the
repo-local `.claude/hooks/` copy): routes on the spine `session_id`
first and falls back to `data.session_id`, so envelopes from 1.0
producers keep their route.
- **Seventeen carrier bumps** with CHANGELOG entries: actionlint 0.8.37,
autonomy 0.22.28, bash-format 0.7.38, biome-format 0.6.36, claude-ops
0.42.11, context-guard 0.7.44, desktop-notification 0.6.31,
eol-normalizer 0.6.37, go-format 0.3.40, guardrails 0.32.7,
instruction-placement 0.11.28, markdown-format 0.11.46,
powershell-format 0.7.39, rate-limit-guard 0.7.35, ruff-format 0.6.37,
source-control 0.55.54, typos-format 0.6.44.

## The keys are selected by depth

Review found that searching the raw payload takes the leftmost match
anywhere in it. Two failures, both reproduced against the first version
of this branch:

-
`{"session_id":"sess-real","tool_input":{"options":{"prompt_id":"NESTED-WRONG"}}}`
emitted `"prompt_id":"NESTED-WRONG"`.
-
`{"tool_input":{"n":{"session_id":"NESTED-WRONG"}},"session_id":"sess-real"}`
emitted `"session_id":"NESTED-WRONG"` — not a mis-join, the row lands in
**another session's** `sessions/<id>.jsonl`.

Cutting the search at the first nested container fixed those and broke
something else: the documented payload places `tool_use_id` **after**
`tool_input` (`session-event-log.sh` says so in its early-stop note,
"tool_input closed, tool_use_id still to come"), so the cut dropped it
on every tool event, and the fixture added alongside listed the four ids
up front so it passed anyway.

So the keys are selected by depth instead. Escapes are neutralized, the
payload is split on the quote character, and the alternating fields are
walked — even fields structure, odd fields string bodies. A string body
is kept only at depth 1, so nested objects collapse to brace-and-colon
rubble carrying no quotes and no nested key can match, while a root key
after a container is still reached. The walk costs one step per string,
not one per byte, and what it renders is short, so the four searches run
over a small string whatever the payload size.

## Bounded, and the gap is filed

The neutralizing passes are superlinear in escape count. Per emit,
escape-bearing payload, this container:

| payload | ungated walk | shipped |
|---|---|---|
| 16 KiB | 4 ms | 4 ms |
| 64 KiB | 13 ms | 4 ms |
| 128 KiB | 38 ms | 6 ms |
| 512 KiB | **486 ms** | **22 ms** |

The walk is gated at 65536 bytes; past it the payload takes the head
cut. That stays safe at any size — nothing nested is reachable — but it
is not complete: a root key after the first container is omitted above
the gate, so `tool_use_id` is dropped on payloads over 64 KiB.
`session_id` and `prompt_id` lead the payload, so routing is unaffected.
**#3784** carries that gap with these measurements and two candidate
approaches, and the code comment points at it.

## Verification

- `lib/hook-utils.test.sh` **303/303**. The suite discriminates against
both wrong versions: **3 failures against the un-anchored original** and
**3 against the truncating fix**. New cases cover the documented key
order (`tool_use_id` after `tool_input`), a nested decoy with no root
key, a nested key ahead of the root one, decoys inside a root array, a
multi-megabyte payload, and the size-gate boundary.
- `hook-telemetry-sink.test.sh` 41/41, `api-error-audit.test.sh` 11/11,
`run-guards.test.sh` 99/99, `claude-observability.test.sh` 60/60,
`lib/rewrite-guard.test.sh` 22/22.
- `scripts/affected-tests.sh --run`: one failure, not this PR's —
`block-hook-bypass.test.sh`, "symlink: a genuine temp write in the same
root stays allowed". Reproduced identically on unmodified `main` at
`73eb4d98` in a clean worktree (PASS=601 FAIL=1).
- `scripts/sync-hook-utils.sh --check`, `scripts/sync-rewrite-guard.sh
--check`, `scripts/check-changelog-parity.sh --check-bump origin/main`,
`--check`, `--check-preserved origin/main`,
`scripts/validate-plugins.sh`, `scripts/check-shell-portability.sh
origin/main`, `scripts/check-killswitch-hoist.sh`: all pass. shellcheck
clean; the one shfmt hunk is pre-existing (`main` carries 49, this
branch 1, and it is not in the changed region).

## Renumbered three times

#3770 (fleet-wide prompt audit) bumped 64 plugins and took eight of the
numbers this branch claimed; #3762 and #3765 then took guardrails 0.32.5
and claude-ops 0.42.10; #3781 then took guardrails 0.32.6. Every one of
the seventeen was re-derived as the next patch above `origin/main` and
verified against `git show
origin/main:plugins/<p>/.claude-plugin/plugin.json` after each move.
Each CHANGELOG conflict was resolved by keeping the released entry at
its own heading and lifting this change's entry above it;
`--check-preserved origin/main` passing is what proves no released entry
was dropped.

## Related

- Refs #930 (closed, the thread this finishes), #3750 (the nine-hook
step and the sink route), #3765 and #3762 (the bases, now merged), #3770
/ #3781 (took claimed numbers), #3784 (the size-gate gap), #3410, #3408.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019DaWEB8Daq1xAXy2Xj1Pme

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

formatters: emit data.changed so the per-session "rewrote" report fills

2 participants