From b8bc7af8fcb1ed670d98a616588ebfe702aa83fd Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:16:20 -0400 Subject: [PATCH 1/9] fix(claude-ops): let a local declaration shadow an imported name in the inventory Refs: #5640 Co-Authored-By: Claude Opus 5.5 --- plugins/claude-ops/skills/inventory/scripts/inventory.py | 5 +++++ .../skills/inventory/scripts/test_inventory.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index 2331d62162..41effd317f 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -1725,6 +1725,11 @@ def _declaration( return found lo, hi = _chunk_span(src, at) exported = _chunk_imports(src, lo, hi).get(ident) + if exported is not None and any( + braces.enclosing(m.start()) is not None and _visible(braces, m.start(), at, src) + for m in pattern_for(ident).finditer(src, lo, hi) + ): + exported = None if exported is not None: homes = _export_index(src).get(exported, []) if len(homes) != 1: diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index 2870f14017..cd4e8ec183 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1507,6 +1507,15 @@ def test_an_imported_name_resolves_in_its_exporting_module(self) -> None: ) self.assertEqual(_tool(src, "Probe")["description"], "Write a Workflow script") + def test_a_local_declaration_shadows_an_imported_name(self) -> None: + src = _modules( + 'var jd="WRONG";export{jd};', + 'import{jd}from"/$bunfs/root/chunk-a.js";var Qz="Probe";' + 'function ff(){let jd="LOCAL";return jd}' + "$t({name:Qz,maxResultSizeChars:1,description:ff()});", + ) + self.assertNotEqual(_tool(src, "Probe")["description"], "WRONG") + def test_a_name_neither_imported_nor_declared_is_a_runtime_value(self) -> None: src = _modules( 'var jd="host_exit";', From c786dbbecf36c5fe6d6d47eaedd9949c8e61eddd Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:18:17 -0400 Subject: [PATCH 2/9] fix(claude-ops): let a for-head let/const shadow outer names in the loop body Refs: #5640 Co-Authored-By: Claude Opus 5.5 --- .../skills/inventory/scripts/inventory.py | 23 ++++++++++++++++++- .../inventory/scripts/test_inventory.py | 16 +++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index 41effd317f..178ed8fafb 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -650,6 +650,25 @@ def _catch_params(src: str, braces: BraceMap, brace: int) -> Scope: return _param_names(src[k + 1 : j]) +_FOR_KEYWORD_RE = re.compile(r"(? Scope: + """The `let`/`const` names of the `for (...)` head whose body opens at + `brace`, each a runtime value; empty for any other block.""" + j = brace - 1 + while j >= 0 and src[j] in " \t\r\n": + j -= 1 + if j < 0 or src[j] != ")": + return NO_SCOPE + k = _head_open(src, braces, j) + if not _FOR_KEYWORD_RE.search(src[max(0, k - 24) : k]): + return NO_SCOPE + decl = _FOR_DECL_RE.match(_mask_strings(src[k + 1 : j])) + return _param_names(decl.group(1)) if decl else NO_SCOPE + + def _head_open(src: str, braces: BraceMap, close: int) -> int: """The `(` matching the `)` at `close`, matched with quoted text blanked from the enclosing block's start. Raises ValueError when unmatched.""" @@ -1021,7 +1040,9 @@ def _scan( block=True, hops=hops, anchor=anchor, - shadow=shadow | _catch_params(src, braces, i), + shadow=shadow + | _catch_params(src, braces, i) + | _for_params(src, braces, i), deferred=deferred, ) i, at_value, prev, prev_word = close + 1, False, "}", "" diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index cd4e8ec183..96fd878e30 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1688,6 +1688,22 @@ def test_a_catch_parameter_shadows_a_bound_parameter(self) -> None: ) self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_a_for_head_binding_shadows_a_bound_parameter(self) -> None: + src = _modules( + 'var Qz="Probe";' + 'function ff(x){for(const x of ["LOCAL"]){return x}}' + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + + def test_a_for_head_binding_shadows_an_outer_binding(self) -> None: + src = _modules( + 'var Qz="Probe";var xx="WRONG";' + "function ff(){for(let xx of a){return xx}}" + "$t({name:Qz,maxResultSizeChars:1,description:ff()});" + ) + self.assertNotEqual(_tool(src, "Probe").get("description"), "WRONG") + def test_a_quoted_paren_in_a_control_head_keeps_a_var_function_scoped( self, ) -> None: From 0a54db326361e7d1ee4d9b4dc244c6d892453243 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:19:08 -0400 Subject: [PATCH 3/9] fix(claude-ops): bump to 0.78.1 and document inventory scope shadowing Refs: #5640 Co-Authored-By: Claude Opus 5.5 --- plugins/claude-ops/.claude-plugin/plugin.json | 2 +- plugins/claude-ops/CHANGELOG.md | 11 +++++++++++ .../skills/inventory/reference/extraction.md | 5 +++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index cee081573c..bddcb09122 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.78.0", + "version": "0.78.1", "description": "Claude Code operations toolkit. Thirteen skills: audit-skill-visibility (audit whether each installed skill is actually VISIBLE to the model, and diagnose why most of a fleet never gets used: a skill is invisible when its description is dropped by Claude Code's skill-listing context budget, which sheds descriptions lowest-score-first so an unused skill loses the keywords that would let it be matched, from skills genuinely not wanted, from skills the run cannot observe at all; computes whether the listing overflows from documented settings, and withholds every cold verdict the data cannot support rather than reporting absence of data as absence of use), inventory (read-only enumeration of the complete invocable surface: every built-in CLI command with aliases and hidden/gated status, every bundled skill, every built-in subagent and tool, and every component of every installed plugin across all marketplaces; reads the shipped binary because upstream publishes no built-in command list, and carries an integrity verdict so a drifted build reports counts as floors rather than silently short totals), audit-install-state (read-only audit of the machine-scope ~/.claude installation directory and ~/.claude.json: full inventory split into an authored surface and rolled-up bulk trees, product-managed retention vs genuinely unmanaged state, filename-scheme resolution before any process-liveness check, and deliberate/mid-experiment detection; reports, never deletes), audit-performance (read-only slowness-diagnostic capture run at the moment the machine or a session feels slow: CLI version, retention-sweep health including the unparsable-settings pause, which warns in /status, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and the fan-out layer, which covers a load-labeled no-op spawn baseline, every hook that will fire bucketed per-tool-call versus per-turn with its invocation shape, the configured statusline, subagent concurrency and spawn-depth ceilings against documented defaults, whether running sessions predate the settings file they are judged by, and orphan attribution by parent liveness rather than age, plus on Windows a kernel-object census (Token objects against uptime, paged pool) that names a host-level leak beneath all four suspects; read against a bundled known-performance-issues reference that also records the causes tested and cleared; separates the four documented suspects of accumulated state, version regression, component bloat, and per-spawn fan-out cost, and routes remediation out; reports, never mutates, and never executes a discovered hook or statusline command), audit-native-overlap (map native Claude Code surfaces, namely built-in CLI commands, bundled skills, plugin-backed built-ins, and session-provided skills, against the current repo's plugin skills and agents, so a custom component never silently duplicates what Claude Code itself ships; bare invocation is a read-only overlap report carrying the extraction's integrity floors and a shared-listing-budget exposure section, verdicts are human-gated in a committed store rendered into a generated registry whose every row carries an observable recheck trigger, and only an explicit apply step bakes presence-gated native references into descriptions and Boundary sections), observability (read locally captured telemetry from the OTEL store, the collector, the per-session hook event log and hook-event JSONL, and ccusage, with trend reports, a per-session report of what fired, what was blocked and the event timeline, and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and turn them into decisions: apply executes those in scope one PR per owner plugin and hands larger ones off as work items, then re-extract the native surface and file its drift as work items), prerequisites (read-only table of external binaries declared by enabled plugins; never installs), plugins (bring a machine's plugin fleet current on demand: marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view: queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action, an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry, the skill-usage log and the hook log root live, places the root's self-ignoring guard, and detects retired conventions. Plus an opt-in, default-off per-session hook event log (one JSON line per hook event on every event the generated registry marks observable, written to /sessions/.jsonl, with SessionEnd retention by session count or age and an optional detached pre-prune command), a family of eight advisory *-audit hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures, and unsurfaced hook failures. The last also warns the user via systemMessage, since a hook that fails to launch enforces nothing and Claude Code surfaces the failure to nobody) that emit the shared hook-telemetry envelope, and a reference sink that routes envelopes under the same root: per session when the envelope carries a session id, else into the shared hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index 2f30d49c0c..10fdae703a 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.78.1] - 2026-10-01 + +### Fixed + +- **A local declaration shadows an imported name in the inventory.** A name the module imports + but the reader's own enclosing block declares now resolves as a runtime value, not through the + import to the exporting module's value. +- **A `for` head's `let`/`const` shadows outer names in the loop body.** `for (const x of ...)`, + `for (let i = 0; ...)` and `for await` bind their names for the body only, so a same-named + outer binding no longer supplies the value. + ## [0.78.0] - 2026-09-30 ### Added diff --git a/plugins/claude-ops/skills/inventory/reference/extraction.md b/plugins/claude-ops/skills/inventory/reference/extraction.md index 9330aef9ce..80446c6107 100644 --- a/plugins/claude-ops/skills/inventory/reference/extraction.md +++ b/plugins/claude-ops/skills/inventory/reference/extraction.md @@ -199,8 +199,9 @@ the else branch of a ternary, the final `return` of a getter, which is the defau every 2.1.284 case. An operand is a `+` concatenation whose parts may also be a parenthesized expression (`d+(x()?m:c)+p`) or a literal array's `.join(sep)`. -A parameter of the function or method being read is a runtime value: it is shadowed, so it never -resolves to a same-named binding elsewhere, and what depends on it becomes a condition, an +A parameter of the function or method being read, a `catch` parameter, a `let`/`const` bound in a +`for (...)` head, and a declaration in an enclosing block that the reader can see are runtime +values: each is shadowed, so it never resolves to a same-named import or outer binding, and what depends on it becomes a condition, an ellipsis, or nothing. A result whose ellipses leave no static word (`${a}\n\n${b}` with neither resolved) is unresolved, not a value. From 5ac013ad78ada98f3787e7b686b223eb0b310693 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:22:47 -0400 Subject: [PATCH 4/9] fix(claude-ops): shadow outer names for a for-head destructured without a space The for-head declaration pattern required whitespace after let/const, so the minified form for(const{x}of...) and for(const[x]of...) bound nothing. Refs: #5640 Co-Authored-By: Claude Opus 5.5 --- .../skills/inventory/scripts/inventory.py | 2 +- .../skills/inventory/scripts/test_inventory.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index 178ed8fafb..c969328c52 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -651,7 +651,7 @@ def _catch_params(src: str, braces: BraceMap, brace: int) -> Scope: _FOR_KEYWORD_RE = re.compile(r"(? Scope: diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index 96fd878e30..4c47630a25 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1696,6 +1696,19 @@ def test_a_for_head_binding_shadows_a_bound_parameter(self) -> None: ) self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_a_destructured_for_head_without_a_space_shadows_a_bound_parameter( + self, + ) -> None: + for head in ("const{x}", "let{x}", "const[x]", "let[x]"): + of = "[{x:'L'}]" if "{" in head else "[['L']]" + with self.subTest(head=head): + src = _modules( + 'var Qz="Probe";' + f"function ff(x){{for({head}of{of}){{return x}}}}" + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_a_for_head_binding_shadows_an_outer_binding(self) -> None: src = _modules( 'var Qz="Probe";var xx="WRONG";' From 3ea6bb6b7e0c0503641b00012f701eeb7dfac6cf Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:41:02 -0400 Subject: [PATCH 5/9] fix(claude-ops): shadow the for-head binding in an unbraced loop body and for a binding named of Co-Authored-By: Claude Opus 5.5 --- .../skills/inventory/scripts/inventory.py | 25 ++++++++++++++++-- .../inventory/scripts/test_inventory.py | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index c969328c52..2e45d8c7b1 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -651,7 +651,7 @@ def _catch_params(src: str, braces: BraceMap, brace: int) -> Scope: _FOR_KEYWORD_RE = re.compile(r"(? Scope: @@ -662,6 +662,17 @@ def _for_params(src: str, braces: BraceMap, brace: int) -> Scope: j -= 1 if j < 0 or src[j] != ")": return NO_SCOPE + return _for_head_names(src, braces, j) + + +def _for_head_names(src: str, braces: BraceMap, close: int) -> Scope: + """The `let`/`const` names of the `for (...)` head closing at `close`. + + The whole head after the keyword is taken, iterable included: shadowing + extra names only leaves more unresolved, and a binding named `of` or `in` + is still caught. + """ + j = close k = _head_open(src, braces, j) if not _FOR_KEYWORD_RE.search(src[max(0, k - 24) : k]): return NO_SCOPE @@ -986,6 +997,7 @@ def _scan( active = at_value = not block depth = 0 prev, prev_word = "", "" + loop = NO_SCOPE n = min(end, len(src)) while i < n: c = src[i] @@ -1011,7 +1023,7 @@ def _scan( acc, hops=hops, anchor=anchor, - shadow=shadow, + shadow=shadow | loop, deferred=deferred, ) at_value, prev, prev_word = False, "x", "" @@ -1055,7 +1067,16 @@ def _scan( if depth == 0: break depth -= 1 + if depth == 0 and c == ")" and block: + # An unbraced loop body is no block, so its head binds here. + nxt = _skip_ws(src, i + 1, n) + loop = ( + NO_SCOPE + if src.startswith("{", nxt) + else _for_head_names(src, braces, i) + ) elif depth == 0 and c in ",;": + loop = NO_SCOPE if not block: break if c == ";": diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index 4c47630a25..ef3393449b 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1709,6 +1709,32 @@ def test_a_destructured_for_head_without_a_space_shadows_a_bound_parameter( ) self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_an_unbraced_for_body_sees_the_head_binding(self) -> None: + src = _modules( + 'var Qz="Probe";' + "function ff(x){for(const x of a)return x}" + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + + def test_an_unbraced_for_body_ends_at_its_statement(self) -> None: + src = _modules( + 'var Qz="Probe";' + "function ff(x){for(const x of a)g(x);return x}" + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertEqual(_tool(src, "Probe")["description"], "REAL") + + def test_a_for_head_binding_named_of_shadows_a_bound_parameter(self) -> None: + for head in ("const of of a", "const{of}of a"): + with self.subTest(head=head): + src = _modules( + 'var Qz="Probe";' + f"function ff(of){{for({head}){{return of}}}}" + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_a_for_head_binding_shadows_an_outer_binding(self) -> None: src = _modules( 'var Qz="Probe";var xx="WRONG";' From 5812371da4b58b1d685ecb8cae8d3beba50c79c7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:47:44 -0400 Subject: [PATCH 6/9] fix(claude-ops): keep a for-head binding through a nested statement in an unbraced loop body Co-Authored-By: Claude Opus 5.5 --- .../claude-ops/skills/inventory/scripts/inventory.py | 8 +++----- .../skills/inventory/scripts/test_inventory.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index 2e45d8c7b1..4cb1d5fc29 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -1053,6 +1053,7 @@ def _scan( hops=hops, anchor=anchor, shadow=shadow + | loop | _catch_params(src, braces, i) | _for_params(src, braces, i), deferred=deferred, @@ -1070,11 +1071,8 @@ def _scan( if depth == 0 and c == ")" and block: # An unbraced loop body is no block, so its head binds here. nxt = _skip_ws(src, i + 1, n) - loop = ( - NO_SCOPE - if src.startswith("{", nxt) - else _for_head_names(src, braces, i) - ) + if not src.startswith("{", nxt): + loop = loop | _for_head_names(src, braces, i) elif depth == 0 and c in ",;": loop = NO_SCOPE if not block: diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index ef3393449b..482c0fe4c1 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1725,6 +1725,18 @@ def test_an_unbraced_for_body_ends_at_its_statement(self) -> None: ) self.assertEqual(_tool(src, "Probe")["description"], "REAL") + def test_a_nested_statement_in_an_unbraced_for_body_keeps_the_head_binding( + self, + ) -> None: + for body in ("if(x)return x", "for(const y of b)return x"): + with self.subTest(body=body): + src = _modules( + 'var Qz="Probe";' + f"function ff(x){{for(const x of a){body}}}" + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_a_for_head_binding_named_of_shadows_a_bound_parameter(self) -> None: for head in ("const of of a", "const{of}of a"): with self.subTest(head=head): From ca4f2ffd7a49e6fd78f75ed8c34de36692c194ff Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:08:54 -0400 Subject: [PATCH 7/9] fix(claude-ops): end an unbraced for body's head binding at the block that closes it Co-Authored-By: Claude Opus 5.5 --- plugins/claude-ops/skills/inventory/scripts/inventory.py | 2 ++ .../claude-ops/skills/inventory/scripts/test_inventory.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index 4cb1d5fc29..0b38c6999a 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -1059,6 +1059,8 @@ def _scan( deferred=deferred, ) i, at_value, prev, prev_word = close + 1, False, "}", "" + if not re.match(r"\s*(?:else|catch|finally)(?![\w$])", src[i : i + 16]): + loop = NO_SCOPE continue if c == "}": break diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index 482c0fe4c1..2bbaaa827b 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1737,6 +1737,14 @@ def test_a_nested_statement_in_an_unbraced_for_body_keeps_the_head_binding( ) self.assertNotEqual(_tool(src, "Probe").get("description"), "REAL") + def test_an_unbraced_for_body_ending_in_a_block_ends_its_head_binding(self) -> None: + src = _modules( + 'var Qz="Probe";' + "function ff(x,c){for(const x of a)if(c){g()}return x}" + '$t({name:Qz,maxResultSizeChars:1,description:ff("REAL")});' + ) + self.assertEqual(_tool(src, "Probe")["description"], "REAL") + def test_a_for_head_binding_named_of_shadows_a_bound_parameter(self) -> None: for head in ("const of of a", "const{of}of a"): with self.subTest(head=head): From 6244eb1d848f990b1526c4951ecfc7ade39a322c Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:24:34 -0400 Subject: [PATCH 8/9] fix(claude-ops): clear an unbraced for body's binding only at a statement-level block Co-Authored-By: Claude Opus 5.5 --- plugins/claude-ops/skills/inventory/scripts/inventory.py | 4 +++- .../claude-ops/skills/inventory/scripts/test_inventory.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index 0b38c6999a..dc45b6397b 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -1059,7 +1059,9 @@ def _scan( deferred=deferred, ) i, at_value, prev, prev_word = close + 1, False, "}", "" - if not re.match(r"\s*(?:else|catch|finally)(?![\w$])", src[i : i + 16]): + if depth == 0 and not re.match( + r"\s*(?:else|catch|finally)(?![\w$])", src[i : i + 16] + ): loop = NO_SCOPE continue if c == "}": diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index 2bbaaa827b..5ff893c19f 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1728,7 +1728,11 @@ def test_an_unbraced_for_body_ends_at_its_statement(self) -> None: def test_a_nested_statement_in_an_unbraced_for_body_keeps_the_head_binding( self, ) -> None: - for body in ("if(x)return x", "for(const y of b)return x"): + for body in ( + "if(x)return x", + "for(const y of b)return x", + "if(t(x,{k:1}))return x", + ): with self.subTest(body=body): src = _modules( 'var Qz="Probe";' From d86624e34a628e6c0d62a5775f0314f681ad86d8 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:59:39 -0400 Subject: [PATCH 9/9] fix(claude-ops): reset an unbraced for body's binding only when a statement block ends it Co-Authored-By: Claude Opus 5.5 --- .../claude-ops/skills/inventory/scripts/inventory.py | 10 ++++++---- .../skills/inventory/scripts/test_inventory.py | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/claude-ops/skills/inventory/scripts/inventory.py b/plugins/claude-ops/skills/inventory/scripts/inventory.py index dc45b6397b..e697a1d94b 100755 --- a/plugins/claude-ops/skills/inventory/scripts/inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/inventory.py @@ -1058,11 +1058,13 @@ def _scan( | _for_params(src, braces, i), deferred=deferred, ) + # A statement block ends the unbraced loop body holding it. + ends = not re.match( + r"\s*(?:else|catch|finally)(?![\w$])", src[close + 1 : close + 17] + ) + if ends: + loop = NO_SCOPE i, at_value, prev, prev_word = close + 1, False, "}", "" - if depth == 0 and not re.match( - r"\s*(?:else|catch|finally)(?![\w$])", src[i : i + 16] - ): - loop = NO_SCOPE continue if c == "}": break diff --git a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py index 5ff893c19f..83e8dd7c40 100755 --- a/plugins/claude-ops/skills/inventory/scripts/test_inventory.py +++ b/plugins/claude-ops/skills/inventory/scripts/test_inventory.py @@ -1732,6 +1732,7 @@ def test_a_nested_statement_in_an_unbraced_for_body_keeps_the_head_binding( "if(x)return x", "for(const y of b)return x", "if(t(x,{k:1}))return x", + "return c?{k:1}:x", ): with self.subTest(body=body): src = _modules(