Skip to content

fix(claude-ops): resolve the Explore and Plan disallowed-tools spread in its own scope - #5717

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/inventory-scoped-spread-5711
Oct 1, 2026
Merged

kyle-sexton merged 6 commits into
mainfrom
fix/inventory-scoped-spread-5711

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes #5711

Summary

On Claude Code 2.1.286 the inventory read the built-in Explore and Plan agents' disallowedTools as partial. Both definitions spread a shared list (...pY). The spread resolver took the nearest pY= binding anywhere in the bundle, which on 2.1.286 is an unrelated pY=p(...) call, so the shared entries, the Artifact tools among them, were dropped.

Fix

  • _array_names resolves a ...spread element through a new _spread_array helper. It reads the binding with _binding_value at the spread's own offset, the same module and scope rule _scoped_constant applies to names and fields (fix(claude-ops): resolve built-in descriptions the inventory left unresolved #5619, fix(claude-ops): leave a built-in name unresolved when it is bound to a non-constant expression #5704). Before, it used _nearest_binding.
  • Fail-closed:
    • When the scoped binding is not an array literal (a call, a conditional, or undeterminable), the list stays partial.
    • When the selected binding is a bare or conditional assignment rather than a declaration (if(c)pY=["B"], pY=c?["B"]:pY, c&&(pY=["B"]), an unbraced for(...)pY=["B"]), or any other write in the same block reaches it, the list stays partial. A var reached back through a chain of simple declarators counts as a declaration, which keeps Explore and Plan resolving: their pY follows a function declaration that has no ;, and _declares misses that statement.
    • When code off the declaring block's straight line assigns the binding without declaring its own, the list also stays partial. That code is a nested block (if(c){pY=["B"]}, a loop body), another function (function init(){pY=["B"]}), or an expression-bodied arrow (var f=()=>pY=["B"], in either order relative to the declaration). A read can then see B, so the list is not static. This check (_written_elsewhere) applies only to the spread path. Applied inside _binding_value, it changed many unrelated description fields on both 2.1.285 and 2.1.286 and made the run about 5x slower, so it is not shared with name and field resolution.
  • Tests in test_inventory.py:
    • A spread whose nearest same-name binding is local to another function resolves to the in-scope array. This test fails on the unfixed resolver.
    • A spread whose in-scope binding is a conditional stays partial.
    • A spread whose binding another function, a nested if or loop block, or an expression-bodied arrow reassigns stays partial.
    • A writer that declares its own local of the same name does not block resolution.
  • reference/extraction.md states the spread rule. claude-ops 0.79.2 -> 0.79.3, with a CHANGELOG entry.

Verification

  • inventory.py --binary-only on 2.1.286, diffed against the prior run (.work/cc-2.1.286/inv-final.json). Only these fields changed:
    • builtin_agents/Explore/disallowed_tools: [Agent, ExitPlanMode, Edit, Write, NotebookEdit] -> [Agent, Artifact, ArtifactComments, ArtifactData, ArtifactCheck, ExitPlanMode, Edit, Write, NotebookEdit]
    • builtin_agents/Explore/disallowed_tools_source: partial -> literal
    • builtin_agents/Plan/disallowed_tools and disallowed_tools_source: the same change
  • On 2.1.285, the inventory output is identical to origin/main's.
  • The reassignment checks (commits f88d9bf and 117a3fb) left the 2.1.285 and 2.1.286 outputs identical to the first fix commit (3075883), and runtime is unchanged (about 22s).
  • inventory.py --self-check: OK: cli 2.1.286, validated against 2.1.286, with all six lanes ok.
  • python3 -m unittest test_inventory: Ran 215 tests ... OK.
  • overlap.py detect: exit 0, with discovered: 0 and resurfaced: 0. overlap.py generate --check reports the docs are in sync. node scripts/generate-catalog.mjs produced no diff.
  • scripts/run-ruff.sh check and format --check pass on both files.

Related

🤖 Generated with Claude Code

… in its own scope

A `...spread` in an agent's tools or disallowedTools now reads the binding
its own module and scope see (`_binding_value`, the rule name and field
resolution use), not the nearest same-name binding in the bundle. On
2.1.286 that nearest binding was an unrelated call, so Explore and Plan
lost the shared entries, Artifact tools included, and read `partial`.
A spread whose scoped binding is not an array literal stays `partial`.

Closes #5711

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kyle-sexton and others added 3 commits October 1, 2026 11:23
…ns its binding

`var pY=[a];function init(){pY=[b]}` makes a later `...pY` read b once
init has run, but the scoped lookup returned the declaration's a. The
spread path now treats a binding that another function in the module
assigns (and does not shadow) as not static, so the list stays
`partial`. The check is confined to the spread path: applied inside
`_binding_value` it changed many unrelated description fields on both
2.1.285 and 2.1.286.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_written_elsewhere` only counted writes in another function, so
`var pY=[a];if(c){pY=[b]}` (or a loop body) and an expression-bodied
arrow `var f=()=>pY=[b]` still read `literal` a. A write now counts when
it sits in a different block than the declaration or inside an arrow
expression, including when the arrow's own assignment is the binding
`_binding_value` picked. The shadow check also read declarations at
doubled offsets; it now uses the absolute positions finditer returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-ops 0.79.3 landed on main (#5706); this change moves to 0.79.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 1, 2026 15:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 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-10-01T16:45:03.371528Z 53372f0 Manual request
ℹ️ 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.

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 50s —— View job


Security review: no issues found

Ran /review:security-review against this PR (HEAD d723da7).

  • Gather context (PR diff, changed files)
  • Run /review:security-review criteria against the changed files
  • Post findings (inline + summary) or report clean

Scope reviewed: all 5 changed files — inventory.py, test_inventory.py, extraction.md, CHANGELOG.md, plugin.json.

Assessment: No security-relevant surface. This PR fixes a scope-resolution bug in a static-analysis script (inventory.py) that parses a locally-installed, trusted Claude Code CLI bundle to extract a read-only documentation inventory (tool/agent lists). The change:

  • Introduces _spread_array and _written_elsewhere (inventory.py#L1924-L1983), replacing a "nearest same-name binding" heuristic with scope-aware resolution for ...spread elements, matching the existing _scoped_constant rule already used for names/fields.
  • Is fail-closed by design: any binding that isn't a statically-determinable array literal, or that's written from outside the declaring block's straight-line path, leaves the field partial rather than guessing — this makes incorrect output less likely, not more.
  • Touches no file I/O paths, subprocess/eval calls, network access, credentials, or authorization logic. No new untrusted input source is introduced; the bundle being parsed is the same locally-shipped file this script already read before the change.
  • No instruction-surface deletions — the extraction.md update documents the new (stricter) resolution rule rather than removing a guardrail.
  • New regex patterns (_spread_array, _written_elsewhere) are fixed, non-nested patterns bounded by SHORT_IDENT_LOCALITY_BYTES/chunk spans, consistent with the existing codebase's patterns — no new catastrophic-backtracking shape introduced.

GitHub Actions hardening (triggers, permissions, pinning) is out of scope for this lane (zizmor's) and not applicable here regardless, since no workflow files changed.

No findings to report.

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 6s —— View job


Code review complete

  • Gather context (PR diff, changed files)
  • Review inventory.py changes (_spread_array, _written_elsewhere, _array_names)
  • Review test_inventory.py additions
  • Review extraction.md / CHANGELOG / plugin.json version bump
  • Post findings

Summary

Traced _spread_array / _written_elsewhere against each new test case (own-scope resolution,
non-constant binding, another-function reassignment, nested-block/loop/arrow writes in both
orders, and the shadowed-write case) by hand against the helpers they build on (_binding_value,
_declaration, _visible, _function_block, _declares). The logic checks out: it follows the
same "read the binding at the spread's own offset" scope rule _scoped_constant already uses for
names/fields (#5619/#5704), and it fails closed (stays partial) in every case where the binding
isn't a static array or might be reassigned off the declaring block's straight line — consistent
with the stated design. The call-site change in _array_names (start instead of the outer at)
is the correct offset for that scope resolution. I did not re-run the Python test suite or ruff
myself (no shell access in this environment); the PR body's 214 tests OK / ruff-clean claims are
author-reported and unverified by me.

One finding, posted inline:

  • plugins/claude-ops/skills/inventory/reference/extraction.md:278-279 — the new reference
    text mislabels its own example. function init(){pY=[...]} is offered as the example for "an
    expression-bodied arrow," but it's a plain function declaration — the same snippet the PR body
    and the _written_elsewhere docstring (inventory.py:1947-1953) use for the function case.
    Left as-is, the reference doc teaches the wrong shape for "expression-bodied arrow" and leaves
    the function case without an example. Suggested fix included inline.
    Permalink

No other correctness issues found in the changed files.

@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: d723da7ff4

ℹ️ 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/claude-ops/skills/inventory/scripts/inventory.py Outdated
Comment thread plugins/claude-ops/skills/inventory/scripts/inventory.py
Comment thread plugins/claude-ops/skills/inventory/reference/extraction.md Outdated
…onal write

`var pY=[a];if(c)pY=[b]` (or `pY=c?[b]:pY`, `c&&(pY=[b])`, an unbraced
`for(...)pY=[b]`) made `_binding_value` select the bare assignment, and
`_written_elsewhere` skipped same-block writes, so the spread read [b]
as a literal. A spread's binding must now be a declaration, and any
other write to it in the module, including one in the same block, makes
the list `partial`. A `var` reached back through a chain of simple
declarators counts as a declaration: on 2.1.285 and 2.1.286 the shared
`pY` follows a function declaration with no `;`, which `_declares`
misses.

extraction.md no longer labels `function init(){...}` as an
expression-bodied arrow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 27s —— View job


Security review: no issues found

  • Gather context (PR diff, changed files)
  • Run /review:security-review against this PR (HEAD 24d26c5)
  • Post findings (inline + summary) or report clean

Scope reviewed: diff from origin/main across all 5 changed files — inventory.py, test_inventory.py, extraction.md, CHANGELOG.md, plugin.json — with particular attention to the new commit since the prior review on d723da7 (117a3fb to 24d26c5), which tightens _written_elsewhere to also treat a bare/conditional assignment (if(c)pY=[...], pY=c?[...]:pY, c&&(pY=[...]), an unbraced for(...)pY=[...]) as disqualifying, addressing the Codex P2 finding on this PR.

Assessment: No security-relevant surface, same conclusion as the prior review of this PR. This is a static-analysis script (inventory.py) that parses a locally-installed, trusted Claude Code CLI bundle to extract a read-only documentation inventory. The new commit only narrows an already fail-closed heuristic further:

  • inventory.py#L1947-L1971 — _written_elsewhere now requires the binding at pos to be a genuine declaration (_declares, or a var reached back through a chain of simple declarators) before treating it as resolvable at all; a bare or conditional write returns True (stays partial) immediately. This can only convert a previously-resolved spread into partial, never the reverse — consistent with the stated fail-closed design, and the author's verification (binary-only diff on 2.1.285/2.1.286 identical before/after) backs that no currently-resolved field is affected in practice.
  • The new chain regex is bounded to a 4096-byte window (max(0, pos - 4096)) with a repeated-group count tied to that window, consistent with the existing codebase's bounded-window patterns (SHORT_IDENT_LOCALITY_BYTES elsewhere) — no unbounded-backtracking shape introduced.
  • No new I/O, subprocess/eval, network access, credential handling, or authorization logic. No new untrusted input source — the bundle parsed is the same locally-shipped file this script already read before the change.
  • extraction.md/CHANGELOG.md updates are documentation describing the (stricter) resolution rule; neither is a standing instruction/guardrail file, so the instruction-surface-deletion lens doesn't apply.
  • GitHub Actions hardening is out of scope for this lane (zizmor's) and not applicable regardless, since no workflow files changed.

No findings to report.

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 51s —— View job


Code review complete

  • Gather context (PR diff at HEAD 24d26c5, prior reviews/comments)
  • Review _spread_array / _written_elsewhere / _array_names call site in inventory.py
  • Review test_inventory.py additions for coverage of the stated fail-closed cases
  • Review extraction.md wording, CHANGELOG.md entry, plugin.json version bump
  • Post findings

Summary

Ran /review:code-review against this PR's final head (24d26c5), scoped to correctness and
maintainability per this repo's code-review lane (security is out of scope here; the
security-review lane already reported clean on this diff).

I traced _spread_array and _written_elsewhere
(inventory.py#L1924-L1988)
against each of the new test_inventory.py cases by hand, including the two gaps Codex flagged
earlier in this PR's history:

  • Same-block conditional write (var pY=[a];if(c)pY=[b], a ternary, &&, or an unbraced
    for): this is now fixed by the last commit (24d26c5). _written_elsewhere requires the
    binding _binding_value selected to itself be a declaration (_declares, or the new
    chain-regex fallback for a var reached back through simple declarators past a
    semicolon-less function declaration); a bare or conditional assignment fails that check and the
    list now correctly stays partial. I confirmed this against
    test_a_spread_written_off_the_straight_line_stays_partial,
    which covers if(c)pY=..., the ternary, &&, and the unbraced for forms.
  • Arrow detection scans the whole statement prefix (in_arrow,
    inventory.py#L1965-L1968):
    this is unchanged, and I agree with the author's disposition in the review thread — it can only
    turn a resolvable spread into partial, never produce a wrong list (fail-closed by construction:
    the function returns True/"not static" on the over-match, not False), and the PR body's
    binary-diff check (identical output on 2.1.285/2.1.286 with and without this check) is a
    reasonable empirical backstop given a precise fix needs real expression parsing (claude-ops inventory: replace the regex bundle reader with a JavaScript parser #5640). Not
    re-flagging it as a new finding.
  • The earlier mislabeled-example finding on
    extraction.md
    is fixed: it now gives a function init(){...} example for the function case and a separate
    ()=>pY=["B"] example for the expression-bodied arrow case.

I also checked the _written_elsewhere shadow-exclusion logic (a write inside a scope that
redeclares ident is excluded) against _visible's hoisting-aware semantics for var vs
let/const, and the 0.79.3→0.79.4 CHANGELOG/plugin.json renumbering the merge commit did
to avoid colliding with #5706's 0.79.3 on main — both check out.

No new findings. The one open item (arrow over-match) is a known, already-discussed, fail-closed
limitation tracked under #5640, not a defect in this PR.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ 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".

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 24d26c52a9

ℹ️ 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/claude-ops/skills/inventory/scripts/inventory.py Outdated
…o a spread's binding

`var pY=["A"];if(c){var pY=f()}` redeclares the same function-scoped
binding, but `_written_elsewhere` skipped every declarator and took it as
a shadow, so the spread read A as a literal. A declaration now shadows
only when it introduces a separate binding: a `var` in another function,
or a `let`/`const` in another block. A `var` in the same function is the
binding again and its initializer counts as a write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 0s —— View job


Security review: no issues found

  • Gather context (PR diff, changed files, HEAD 53372f0)
  • Run /review:security-review against this PR
  • Post findings (inline + summary) or report clean

Scope reviewed: diff from origin/main across all 5 changed files — inventory.py, test_inventory.py, extraction.md, CHANGELOG.md, plugin.json — with focus on the delta since the last security review on this PR (24d26c5 → 53372f0, commit 53372f0), which addresses Codex's P2 finding that a same-function var redeclaration in a nested block (var pY=["A"];if(c){var pY=f()}) was being treated as a shadow instead of a write to the outer binding.

Assessment: No security-relevant surface, consistent with the prior reviews posted on this PR's earlier commits (d723da7, 24d26c5). This remains a static-analysis script (inventory.py) that parses a locally-installed, trusted Claude Code CLI bundle to build a read-only documentation inventory.

  • inventory.py#L1975-L2003 — the new separate() helper only reuses existing, already-reviewed primitives (_declares, _is_var, _function_block, braces.enclosing). It introduces one additional regex, \bvar\s+$, anchored and bounded to an 8-byte lookbehind window (src[max(0, d - 8) : d]) — fixed-length, no nesting or repetition, so no new catastrophic-backtracking shape.
  • The change narrows (not widens) when a declaration is treated as introducing a separate binding: a var now counts as the same binding unless it's in another function, which can only flip a previously-mis-resolved literal to the correct partial — fail-closed, same direction as the design stated in the PR body and borne out by the author's binary-diff verification (2.1.285/2.1.286 output unchanged).
  • No new I/O, subprocess/eval, network access, credential handling, or authorization logic. No new untrusted input source — the bundle parsed is the same locally-shipped file already read before this PR.
  • test_inventory.py additions are test-only. extraction.md/CHANGELOG.md/plugin.json changes are documentation/version metadata, not standing instruction/guardrail files, so the instruction-surface-deletion lens does not apply.
  • GitHub Actions hardening is out of scope for this lane (zizmor's) and not applicable regardless, since no workflow files changed.

No findings to report.

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 7m 52s —— View job


Code review complete

  • Gather context (PR diff at HEAD 53372f0, prior Codex/Claude review history on this PR)
  • Apply skip gate — doesn't apply: open, non-draft, substantive diff, and HEAD 53372f0 has no prior review from this lane
  • Review inventory.py changes (_spread_array, _written_elsewhere, separate(), _array_names call site)
  • Review test_inventory.py additions for coverage
  • Review extraction.md / CHANGELOG.md / plugin.json
  • Post findings

Summary

This is the final commit (53372f0) addressing Codex's last P2 finding on this PR (same-function var redeclaration in a nested block was being treated as a shadow instead of a write). I traced the new separate() helper (inventory.py#L1981-L1990) by hand against several constructed inputs beyond the checked-in tests, specifically trying to break it the same way the prior commit's bug was found:

  • Same-function var in a nested block (if(c){var pY=f()}): correctly not separate (_function_block(d) != home_fn compares function scope, not block scope), so the write is detected and the spread stays partial. Matches test_a_spread_written_off_the_straight_line_stays_partial's new 9th case.
  • Shadowing let/const (function g(){let pY=[];pY=["Other"]}, if(c){let pY=f();pY=["Other"]}): correctly separate (block/function comparison distinguishes the new binding), so the outer spread still resolves. Matches test_a_spread_whose_writer_shadows_the_name_still_resolves.
  • I specifically tried to defeat this through the known _statement_start-crosses-a-semicolonless-function-declaration blind spot (the one _written_elsewhere's own chain regex fallback exists for, e.g. function h(){return 1}var a=1,pY=["Other"] inside a block): in every variant I constructed, _declares (which separate() gates on before ever reaching the var/let branch) fails closed the same way for both the pos check and the d check, so a d that _declares can't confirm is handled conservatively as a plain write rather than wrongly treated as a separate declaration. I could not construct an input where separate() returns a wrong True (the dangerous direction — a missed write making a conditional list look literal).
  • The _array_names call-site change (start instead of the outer array's at) is still the correct offset for _spread_array's scope resolution, confirmed against test_a_spread_reads_its_own_scope_not_the_nearest_binding.
  • extraction.md and CHANGELOG.md/plugin.json accurately describe the final behavior; the earlier mislabeled-example and version-collision issues from this PR's review history are already fixed in this HEAD.

No new findings to report. The one previously-flagged, already-disposed item (arrow-detection over-matching the whole statement prefix) remains an accepted, fail-closed limitation tracked under #5640, not a defect introduced here.

Not independently verified: I don't have code-execution permission in this environment (python3 -m unittest / python3 -c were denied), so I could not re-run test_inventory.py or scripts/run-ruff.sh myself. The PR body's "215 tests OK" and ruff-clean claims are author-reported and unverified by me — my review above is based on manual tracing of the logic against the diff and the existing test file, not execution.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 53372f0a6e

ℹ️ 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/claude-ops/skills/inventory/scripts/inventory.py
@kyle-sexton
kyle-sexton merged commit 10091c4 into main Oct 1, 2026
20 checks passed
@kyle-sexton
kyle-sexton deleted the fix/inventory-scoped-spread-5711 branch October 1, 2026 16:47
kyle-sexton added a commit that referenced this pull request Oct 1, 2026
…e Code 2.1.287 (#5731)

Closes #5730

## Summary

Per-release native-surface pass for Claude Code 2.1.287. Every inventory
lane extracts ok on 2.1.287, so the inventory is revalidated against it.
The one native surface that moved is the hidden built-in command
`/plugin-types`, which the 2.1.287 build no longer ships. Its dismissal
against `code-metrics:audit-type-debt` was orphaned and is removed.

## Fix

- `VALIDATED_AGAINST` in
`plugins/claude-ops/skills/inventory/scripts/inventory.py` is now
`2.1.287`.
- The `plugin-types` / `code-metrics:audit-type-debt` dismissal is
removed from `docs/native-surfaces/records.json`. `overlap.py` has no
undismiss command, so the record was deleted from the store and
`docs/native-surfaces.md` was regenerated with `overlap.py generate`.
`node scripts/generate-catalog.mjs` reported the catalog already in
sync.
- claude-ops is bumped from 0.80.0 to 0.80.1, with a CHANGELOG entry.

## Verification

- Binary string search, not variable names: in 2.1.286, `/plugin-types`
occurs 10 times and `Write claude-code.d.ts` 2 times. In 2.1.287 both
occur 0 times. The only `plugin-types` left in 2.1.287 is the CSP
directive name inside a list of `*-src` directives.
- Surface diff, 2.1.286 final extraction against 2.1.287:
builtin_commands went from 111 to 110 (`plugin-types` removed). Nothing
was added or renamed in any lane. The Explore and Plan
`disallowed_tools` now read `literal` with the Artifact tools included.
That comes from the extractor fix in #5717, not from a change in the
binary.
- `inventory.py --self-check`: `OK: cli 2.1.287, validated against
2.1.287`, all six lanes ok, exit 0. It was `DEGRADED` (exit 3) before
the bump.
- `overlap.py detect` on the final extraction: exit 0, integrity ok,
discovered 0, resurfaced 0, orphaned dismissals 0, 95 suppressed.
- `overlap.py self-check`: exit 3, degraded only by the 2 standing
advisories it also reports on main (older recorded extraction versions
on rows, upstream SHA not locally decidable). 69 rows checked, 0
problems.
- `test_inventory.py`: 215 tests OK. `test_overlap.py`: 184 tests OK.
Pinned ruff passes on `inventory.py`.

## Related

- #5704: the same pass for 2.1.286.
- #5717: the Explore and Plan disallowed-tools extractor fix.
- #5730: the native-drift item
`native-drift:inventory-degraded:2.1.287:inventory`, filed by this pass
and closed by this PR.

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

Co-authored-by: Claude Opus 5.5 <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.

fix(claude-ops): inventory reads Explore and Plan disallowed tools as partial on 2.1.286

1 participant