Skip to content

fix(toolchain): execute resolved ecosystem gates array in check/lint (#926)#1020

Open
kyle-sexton wants to merge 4 commits into
mainfrom
fix/926-wire-ecosystem-gates-execution
Open

fix(toolchain): execute resolved ecosystem gates array in check/lint (#926)#1020
kyle-sexton wants to merge 4 commits into
mainfrom
fix/926-wire-ecosystem-gates-execution

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

docs/conventions/ecosystem-commands/ecosystem.schema.json defines a structured gates array (name/cmd/trigger-globs/remediation) that any ecosystem config — bundled default or consumer-tracked — can declare, and both plugins/toolchain/reference/ecosystems/go.yaml (go-mod-tidy-drift) and docs/conventions/ecosystem-commands/examples/dotnet.yaml (nuget-lockfile-drift) ship one. The array was resolved as part of each ecosystem's command surface (check/SKILL.md §1.5) but never invoked by any workflow step — so every declared gate was inert. The only gate-execution mechanism check/SKILL.md documented was a prose, consumer-CLAUDE.md-only step whose closing sentence ("this plugin ships none of its own") was itself stale once go.yaml shipped a bundled gate.

Fix

Scoped to plugins/toolchain/skills/check/SKILL.md, precise and additive (no restructuring of unrelated parts):

  • New structured gate-execution step in §2 "Run checks", replacing the stale prose step at the old :121. After an affected ecosystem's build → test → lint, iterate its resolved gates array (§1.5 ladder — bundled default or consumer file). Per gate:
    • Fire conditiontrigger-globs present → run only when ≥1 changed file matches, against the full changed-files set (a gate's trigger files need not classify into the ecosystem's own globs); omitted → run whenever the ecosystem runs; present-but-no-match (e.g. check all on a clean tree) → does not fire.
    • Independent of the build/test/lint short-circuit — a fired gate runs even when an earlier phase failed and stopped (a lockfile / go mod tidy gate is meaningful regardless of build success).
    • Tool presence — missing tool → skip with the ecosystem's install-hint, never FAIL.
    • Outcomepass/FAIL by name; on FAIL surface gate.remediation; a fired-and-failed gate counts toward the run's FAIL verdict.
  • Overall verdict now covers gate-only failures — a failed fired gate flips Overall to FAIL and is counted even when every ecosystem's build/test/lint cell passed (e.g. Overall: FAIL (0 of 2 ecosystems failed, 1 gate failed)).
  • Stale sentence removed — the "this plugin ships none of its own" framing is replaced with an accurate ladder pointer (bundled default e.g. go.yaml's go-mod-tidy-drift; consumer-declared in .claude/ecosystems/<ecosystem>.yaml).
  • Cross-reference reconciled at the old :119 ("CI-parity gates … run independent of check-cmd") and the report scaffold (:140).

lint/SKILL.md deliberately unchanged (the one maintainer-vetoable fork, per the planning comment): every schema-described gate and both shipped examples are CI-parity checks beyond build/test/lint (lockfile drift, module drift, generated-artifact freshness) — none is a lint/format op. They belong with the heavier check step and /verification:confirm (the sanctioned pre-commit gate); adding them to /toolchain:lint would break its "fast" contract. Veto before merge if a maintainer wants gates on the lint path.

Version bump (rule 6d): fresh origin/main had plugins/toolchain at 0.9.0 (claimed by the lychee PR #867); this is a new executable behavior, so bumped to 0.10.0 with a matching ## [0.10.0]### Added CHANGELOG entry. (The planning comment's 0.8.0 → 0.9.0 predated #867 landing on main.)

Verification

No skill-behavior test harness exists (find plugins/toolchain -iname '*test*' and a repo-level search for toolchain:check/gates tests both empty), so verification is static gate + hand-trace:

  • /skill-quality:check on the edited check skill: PASS — 0 errors, markdownlint clean, 165/500 lines, all base-ref trigger phrases preserved (frontmatter description untouched). The lone WARN (description lacks Use when: phrasing) is pre-existing and out of scope.
  • Hand-trace — bundled default (go.yaml, a real rung-4 default that runs for this repo): a change set including go.sum (or any *.go) makes go affected; §1.5 resolves go.yaml; the new §2 step reaches its gates and matches go-mod-tidy-drift's trigger-globs: ["go.mod","go.sum","*.go"] against the full changed-files set → gate fires → runs go mod tidy -diff from $REPO_ROOT. Drift → FAIL + remediation + Overall FAIL; clean → pass; missing Go toolchain → skip with install-hint.
  • Hand-trace — consumer-declared shape (examples/dotnet.yaml, an example fixture under docs/, not a bundled default): for a consumer whose tracked .claude/ecosystems/dotnet.yaml carries the nuget-lockfile-drift gate, a changed *.csproj matches trigger-globs: ["*.csproj","Directory.Packages.props","packages.lock.json"] → gate fires → runs dotnet restore --locked-mode. Same pass/FAIL/skip semantics.

Both gate entries are invoked by the new step given a matching changed-files set; neither required a per-ecosystem change.

Closes #926

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_019xSPACJvbsqGsjYCP91DoX

Work-class: C3 (bug-fix-shaped) — attended triage 2026-07-23, operator-ratified. 🤖

The ecosystem-commands `gates` array (name/cmd/trigger-globs/remediation) was
resolved as part of each ecosystem's command surface but never invoked by any
workflow step, leaving bundled and consumer-declared gates inert.

check/SKILL.md §2 now iterates each affected ecosystem's resolved `gates` after
build → test → lint: a gate fires on a `trigger-globs` match (full changed-files
set) or unconditionally when omitted, runs independent of the build/test/lint
short-circuit, reports `skip` on a missing tool, and on failure surfaces
`remediation` and flips Overall to FAIL. Replaces the stale prose gate step
(which claimed the plugin ships none of its own) with the structured-array
channel that setup/SKILL.md already routes gates into.

lint/SKILL.md is unchanged — gates are CI-parity checks beyond lint's fast path.

Bumps plugin 0.9.0 → 0.10.0 with a matching CHANGELOG Added entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton added the automated Opened by automation. label Jul 22, 2026
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: fix(toolchain): execute resolved ecosystem gates array in check (#926)

  • Gather context and understand the PR scope
  • Read changed files and diff
  • Analyze SKILL.md changes for correctness and alignment
  • Review plugin.json version bump
  • Review CHANGELOG.md entry
  • Post comprehensive review findings

Overall verdict: APPROVE with minor observations. The fix is correct and well-scoped. The three changed files are internally consistent, the gate-execution specification is accurate, and no blocking issues were found.


plugins/toolchain/skills/check/SKILL.md

The core change (lines 121–129) replaces a stale prose gate step — which falsely claimed the plugin ships no bundled gates — with a structured, actionable spec. This is correct:

  • Fire condition (trigger-globs matched against full changed-files set, not ecosystem-scoped subset) is the right semantic and consistent with the schema, which defines trigger-globs at the gate level independently of globs.
  • Independence from short-circuit is explicit and correct — stating that a gate fires even when build/test/lint stopped early. Matches real CI behavior where lockfile checks are independent of compile success.
  • Tool presence → skip, not FAIL is consistent with the same rule for check-cmd.
  • Verdict propagation (gate failure flips Overall to FAIL) is correctly limited to fired-and-failed gates; fired-and-passed gates don't count toward FAIL.

Observation 1 — Report scaffold example not updated: The §3 code block at L135–L144 still shows Overall: FAIL (1 of 2 ecosystems failed) — no gate-failure example lives inside the fenced block. The gate-verdict format is described in the prose at L148, but the code block is what a reader skims for a complete output template. A future implementer reading only the scaffold might not know to include gate rows. Consider adding a gate-summary line to the code block example, e.g.:

Gates: go-mod-tidy-drift — FAIL (run go mod tidy and commit updated go.mod/go.sum)

Overall: FAIL (0 of 2 ecosystems failed, 1 gate failed)

Low priority — prose at L148 is clear — but the scaffold is the canonical template. Fix this →

Observation 2 — <files> placeholder semantics in gate context are ambiguous: L125 says "substitute the same placeholders as other commands: <files>, resolved anchor, etc." For the main check commands, <files> is the ecosystem-scoped changed-files subset. For gates, the fire condition uses the full changed-files set — but what does <files> expand to in a gate cmd? Both shipped gate cmds (go mod tidy -diff, dotnet restore --locked-mode) don't use <files> so this is practically inert today, but it's a potential trap for future gate authors. A parenthetical clarifying "(<files> expands to the full changed-files set in gate context)" would close this. Very low priority given no shipped gate uses it.

Observation 3 — "both counts whenever a gate fires" is ambiguous for the pass case: L148 says "The Overall line names both counts whenever a gate fires." This implies that even when a gate fires and passes, the Overall line should include a gate count. But the only example given is the FAIL case. A reader could reasonably produce Overall: PASS (2 of 2 ecosystems passed, 1 gate passed) or just Overall: PASS (2 of 2 ecosystems passed). This doesn't affect FAIL detection or correctness, but consistent reporting format matters for composing skills. Worth clarifying which form is intended. Low priority.


plugins/toolchain/.claude-plugin/plugin.json

Version bump 0.9.00.10.0 is appropriate — this is new executable behavior (gates were previously inert), making it a backward-compatible minor feature addition. Correct semver choice.


plugins/toolchain/CHANGELOG.md

Follows Keep a Changelog format correctly. The ## [0.10.0] / ### Added entry is accurate, mentions both the bundled default (go.yaml's go-mod-tidy-drift) and the consumer-declared example (nuget-lockfile-drift), and correctly notes that lint is unchanged. The entry is well-scoped and would serve as a clear upgrade note for plugin consumers.


Overall

The change is narrow and additive (no restructuring of unrelated sections), correctly fixes the gap, and the three-file scope is exactly right. Observations 1–3 above are minor polish items; none are blocking. Observation 1 (incomplete report scaffold) is the most worth addressing before merge since the code block is a commonly-skimmed reference.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 22s —— View job


Security Review — PR #1020

  • Gather context and understand the PR scope
  • Read changed files and diff
  • Analyze SKILL.md for injection surfaces and logic flaws in gate execution
  • Review plugin.json version bump for security implications
  • Review CHANGELOG.md for any sensitive information
  • Post security review findings

No security issues found. This PR is a documentation-layer change (a SKILL.md instruction file, a version bump, and a changelog entry) with no executable code.

What was reviewed

File Change
plugins/toolchain/skills/check/SKILL.md Adds a structured gate-execution step instructing the AI to iterate each resolved gates array after build → test → lint
plugins/toolchain/.claude-plugin/plugin.json Version bump 0.9.00.10.0
plugins/toolchain/CHANGELOG.md New [0.10.0] section

Analysis

gate.cmd execution / shell injection — The new step at SKILL.md L125 instructs the AI to run gate.cmd as an opaque shell string, substituting <files> and other placeholders. This is structurally identical to how build-cmd, test-cmd, and check-cmd have been handled throughout this skill — the same trust model and the same placeholder substitution semantics already exist for lines 103–110 and were not introduced by this PR. Gates do not expand the command-injection attack surface beyond what consumer ecosystem YAML files could already declare via the other command fields.

Consumer-supplied gate commands — A consumer's .claude/ecosystems/<ecosystem>.yaml can declare a gates[].cmd with arbitrary shell content. This is the same trust relationship the consumer already has with build-cmd/test-cmd/check-cmd; the consumer controls their own repo's tracked config and is already trusted with those fields. No new trust boundary is crossed.

Bundled go.yaml gate (go-mod-tidy-drift, go.yaml L41–44) — The only bundled gate that fires under this new step uses go mod tidy -diff, which does not take a <files> argument at all. No filename from git status is shell-interpolated into this command.

trigger-globs against the full changed-files set — Pattern matching filenames for gate fire decisions is safe; the filenames are used for comparison only, not passed to a shell or eval'd.

Fail-safe behaviors preserved — Missing tool → skip with install-hint (not FAIL). Gates run independent of build/test/lint short-circuit. Both are safe-fail by design.

No GitHub Actions workflows touched. No credentials, tokens, or secrets involved. plugin.json and CHANGELOG.md are administrative with no security implications.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 22s —— View job


PR Review: fix(toolchain): execute resolved ecosystem gates array in check (#926)

  • Gather context and understand the PR scope
  • Read changed files and diff
  • Analyze SKILL.md changes for correctness and alignment
  • Review plugin.json version bump
  • Review CHANGELOG.md entry
  • Post comprehensive review findings

Overall verdict: APPROVE. The fix is correct, narrowly scoped, and closes a real behavioral gap — declared gates were resolved but never invoked. No blocking issues. Four 🟡 Nit items below (at the REVIEW.md cap); the first is the most worth addressing before merge.


plugins/toolchain/skills/check/SKILL.md

The new gate-execution block at L121–L129 is well-specified and internally consistent:

  • Fire condition against the full changed-files set (not the ecosystem-scoped subset) is correct — a gate's trigger-globs describe CI-parity triggers independent of what classifies a file into the ecosystem.
  • Independence from the build/test/lint short-circuit is correct — lockfile and module drift checks are meaningful regardless of compile success, and this matches how real CI pipelines work.
  • Tool-missing → skip (not FAIL) is consistent with the same rule applied to check-cmd at L111.
  • Verdict propagation at L148 is correctly limited to fired-and-failed gates.
  • Ladder cross-reference at L129 is accurate — go.yaml's go-mod-tidy-drift is a rung-4 bundled default; the consumer YAML shape (dotnet.yaml example) is correctly described as consumer-declared.
  • Schema alignment verified: ecosystem.schema.json's gates items define name, cmd, trigger-globs, remediation — all four are referenced correctly in the new step.

🟡 Nit 1 — Report scaffold code block lacks a gate-failure example

L135–L144 — the fenced code block — is the canonical output template readers skim. It still shows Overall: FAIL (1 of 2 ecosystems failed) with no gate row or gate-failure Overall format. The gate-verdict format is described in the prose at L148, but a future implementer composing this skill who reads only the scaffold will not know to include gate rows. Suggested addition to the code block:

Gates: go-mod-tidy-drift — FAIL (run go mod tidy and commit updated go.mod/go.sum)

Overall: FAIL (0 of 2 ecosystems failed, 1 gate failed)

Fix this →

🟡 Nit 2 — <files> expansion inside gate.cmd is ambiguous

L125 says "substitute the same placeholders as other commands: <files>, resolved anchor, etc." For ecosystem commands, <files> expands to the ecosystem-scoped changed-files subset. For gates, the fire condition uses the full changed-files set. If a future gate author uses <files> in their cmd, it is unclear which set they get. Both shipped gates (go mod tidy -diff, dotnet restore --locked-mode) don't use <files>, so this is practically inert today — but a parenthetical on the line clarifying which set applies in gate context (<files> = full changed-files set, or ecosystem-scoped subset?) would close the gap for future authors.

🟡 Nit 3 — PASS-case gate-count format not specified

L148: "The Overall line names both counts whenever a gate fires." Only the FAIL format is given as an example. It is unclear whether a gate that fires-and-passes should produce Overall: PASS (2 of 2 ecosystems passed, 1 gate passed) or Overall: PASS (2 of 2 ecosystems passed). Both are reasonable readings of "names both counts whenever a gate fires." The distinction doesn't affect correctness or FAIL detection, but consistent report format matters when composing skills.

🟡 Nit 4 — Ecosystem install-hint reused for gates that may use different tools

L126: "report skip (reuse the ecosystem's install-hint)". The gate schema (ecosystem.schema.json L66–L75) has no per-gate install-hint field. For consumer-declared gates that invoke a tool not covered by the ecosystem-level install-hint (e.g. a dotnet gate that invokes a custom code-gen tool, not dotnet itself), the shown hint will be wrong. This is a pre-existing schema gap, not introduced by this PR. Worth a comment on the spec line noting the limitation, or a follow-on schema issue to add an optional per-gate install-hint.


plugins/toolchain/.claude-plugin/plugin.json

Version bump 0.9.00.10.0 is appropriate — gates were previously inert (no behavioral effect); making them execute is a new backward-compatible capability, which is a minor-version addition under semver. The prior 0.9.0 (lychee PR #867) is correctly identified as the predecessor rather than the planning comment's 0.8.0.


plugins/toolchain/CHANGELOG.md

Follows Keep a Changelog format correctly. The [0.10.0] / ### Added entry is accurate, mentions both the bundled default (go.yaml's go-mod-tidy-drift) and the consumer-declared pattern, correctly notes lint is unchanged, and closes #926. Well-scoped.


Security (summary)

The gate.cmd execution trust model is identical to build-cmd/test-cmd/check-cmd — consumer ecosystem YAML files were already trusted with opaque shell strings; this adds no new trust boundary. Both shipped gate commands are go mod tidy -diff and dotnet restore --locked-mode, neither of which shell-interpolates filenames. The REVIEW.md blocking checks (object-level authorization, tenant scoping, secrets, injection, atomicity) do not apply to this documentation-layer change.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


Security Review — PR #1020

  • Gather context and understand the PR scope
  • Read changed files (SKILL.md, plugin.json, CHANGELOG.md)
  • Analyze SKILL.md for injection surfaces and logic flaws in gate execution
  • Review plugin.json for security implications
  • Review CHANGELOG.md for sensitive information
  • Post security review findings

No security issues found. This PR is a documentation-layer change (a SKILL.md instruction file, a version bump, and a changelog entry) with no executable code. The security model is unchanged from the existing build-cmd/test-cmd/check-cmd pattern.

What was reviewed

File Change Security-relevant?
plugins/toolchain/skills/check/SKILL.md Adds structured gate-execution step (§2, L121–129) and verdict propagation (L148) Yes — introduces gate-cmd execution semantics
plugins/toolchain/.claude-plugin/plugin.json Version bump 0.9.00.10.0 No
plugins/toolchain/CHANGELOG.md New [0.10.0] section No — no credentials or PII

Analysis

gate.cmd shell execution / injection surface (L125)

The new step instructs the AI to execute gate.cmd as an opaque shell string with <files> and anchor placeholder substitution. This is structurally identical to how build-cmd, test-cmd, and check-cmd are handled throughout the existing skill — the same trust model and substitution semantics were already present at lines 103–110. Consumer-supplied gate commands do not cross a new trust boundary: a consumer who can write .claude/ecosystems/<ecosystem>.yaml could already declare arbitrary shell in build-cmd/test-cmd/check-cmd. No new privilege is granted.

<files> in gate context (L125)

Gates substitute <files> from the full changed-files set rather than the ecosystem-scoped subset used by other commands. This slightly broadens the interpolated filename set, but the injection risk is equivalent to check-cmd's existing <files> substitution. Crucially, neither shipped gate (go mod tidy -diff, dotnet restore --locked-mode) uses <files>, so no filename from the working tree is shell-interpolated by any bundled gate today.

Bundled go.yaml gatego-mod-tidy-drift at go.yaml L41–44

go mod tidy -diff takes no user-controlled arguments and reads the module graph from the filesystem. No filename from git status is interpolated into this command. No injection surface.

trigger-globs matching (L123)

Pattern matching filenames for gate fire decisions is safe; the filenames are used for comparison only, not passed to a shell or eval'd.

Gate independence from build/test/lint short-circuit (L124)

Gates that were previously inert now execute after a failed build/test/lint phase. This is a behavioral change but not a security regression — the gate commands are still consumer/bundled YAML content subject to the same trust rules. There is no privilege escalation from executing in the presence of a prior failure.

plugin.json and CHANGELOG.md — administrative only; no credentials, tokens, secrets, or PII present.

No GitHub Actions workflows touched. No pull_request_target, workflow_run, or permissions: changes. No unpinned action pins added.

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

ℹ️ 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/toolchain/skills/check/SKILL.md Outdated

- **Fire condition** — if `trigger-globs` is present, run the gate only when ≥1 changed file matches, matched against the **full** changed-files set (not the ecosystem-scoped subset — a gate's trigger files need not classify into the ecosystem's own `globs`). If `trigger-globs` is omitted, run whenever the ecosystem runs. A present `trigger-globs` with no matching changed file (e.g. `check all` on a clean tree) → the gate does not fire.
- **Independent of the build/test/lint short-circuit** — a fired gate runs even when this ecosystem's build, test, or lint already failed and stopped (line above). Gates mirror CI checks that are independent of build success (a lockfile or `go mod tidy` gate is meaningful whether or not the build compiled), so a failed earlier phase never suppresses them.
- **Run** `gate.cmd` (an opaque shell string — substitute the same placeholders as other commands: `<files>`, resolved anchor, etc.) from `$REPO_ROOT` with absolute paths.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Execute module gates from each discovered Go module root

In Go monorepos, project-discovery: ["go.mod"] deliberately runs build/test/lint from every module root, but this instruction runs go mod tidy -diff only from $REPO_ROOT. If the only module is nested, the gate exits with “go.mod file not found” and falsely fails every matching change; if a root module also exists, drift in nested modules is never checked. Run project-discovery-backed gates once per resolved <project-dir> instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

# Finding Classification Evidence Reacted
1 Execute module gates from each discovered Go module root (Go monorepos) VALID — fixed Genuine defect. go.yaml sets project-discovery: ["go.mod"] precisely because go build/test/list ./... silently skip nested-module packages (documented in go.yaml L6-11), so build/test/lint already walk each module root — but the gate step ran gate.cmd only from $REPO_ROOT. go mod tidy -diff is inherently per-module: a nested-only monorepo falsely fails with go.mod file not found, and a root+nested repo never checks nested-module drift. 👍

Fixed in c8862de: the gate now executes from the same location the ecosystem's own build/test/lint use — once per resolved <project-dir> for a project-discovery ecosystem, from the anchor's directory otherwise, $REPO_ROOT only when neither is defined. The fire condition stays repo-wide (trigger-globs vs the full changed-files set decides whether the gate runs); only the execution location becomes per-project.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

🔒 merge-drive lane claiming this PR for on-branch fix work this cycle (amendment-round: 16, worker tier). Will fix clear branch-owned CI/bot findings, defer non-DEFER-FORBIDDEN items with reply+resolve, push, then re-check the merge gate.

Freshen branch with latest main (disk-hygiene updates #1010, #1012, #1028,
#1031). No overlap with toolchain plugin changes; no conflicts.
…REPO_ROOT

The gate-execution step ran `gate.cmd` from `$REPO_ROOT` regardless of the
ecosystem's scoping model. For a `project-discovery` ecosystem this broke the
bundled `go.yaml` `go-mod-tidy-drift` gate in Go monorepos: `go mod tidy -diff`
is inherently per-module, so a repo-root-only run falsely fails when the sole
module is nested (`go.mod file not found`) and never checks drift in nested
modules when a root module also exists — the exact monorepo case go.yaml's
`project-discovery: ["go.mod"]` walk exists to cover for build/test/lint.

Gate execution now mirrors the ecosystem's own build/test/lint location: once
per resolved `<project-dir>` for a `project-discovery` ecosystem, from the
`anchor`'s directory otherwise, `$REPO_ROOT` only when neither is defined. The
fire condition stays repo-wide; only the execution location is per-project, and
a gate `cmd` using `<files>` gets that project's scoped subset. Also completes
the report scaffold with a gate-failure row and clarifies the PASS-case
gate-count format.

Addresses codex P2 (Go-monorepo gate roots) and claude[bot] review Nits 1-3.
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


Security Review — PR #1020

  • Gather context and understand PR scope
  • Fetch PR diff
  • Read changed files (SKILL.md, plugin.json, CHANGELOG.md, go.yaml, ecosystem.schema.json, examples/dotnet.yaml)
  • Analyze SKILL.md gate-execution step for injection surfaces and logic flaws
  • Review plugin.json and CHANGELOG.md
  • Post security findings

No security issues found. This PR is a documentation-layer change that wires a previously-inert feature (gates array execution) using the same trust model and shell-execution semantics already established for build-cmd/test-cmd/check-cmd. Three observations below — all SUGGESTION-level, all pre-existing in scope.

What was reviewed

File Change Security-relevant?
plugins/toolchain/skills/check/SKILL.md Adds structured gate-execution step (L121–129) and verdict propagation (L148–150) Yes — introduces gate.cmd execution semantics
plugins/toolchain/.claude-plugin/plugin.json Version bump 0.9.00.10.0 No
plugins/toolchain/CHANGELOG.md New [0.10.0] section No

Referenced for trust-model verification: plugins/toolchain/reference/ecosystems/go.yaml, docs/conventions/ecosystem-commands/ecosystem.schema.json, docs/conventions/ecosystem-commands/examples/dotnet.yaml.


Analysis

gate.cmd shell execution — no new trust boundary

L125 instructs the AI to execute gate.cmd as an opaque shell string with placeholder substitution. This is structurally identical to how build-cmd, test-cmd, and check-cmd are already handled — the same trust model and the same substitution semantics. A consumer who controls .claude/ecosystems/<ecosystem>.yaml could already declare arbitrary shell in those three fields; adding gates[].cmd to the set of executed keys does not cross any new trust boundary.

Bundled go.yaml gate — no filename interpolation

The only bundled gate that fires under this new step (go.yaml L41–44) runs go mod tidy -diff, which takes no user-controlled arguments. No filename from the working tree is shell-interpolated into this command.

Per-project execution location (HEAD commit c8862de) — gates now run from each <project-dir>

The latest commit changes gate execution location from $REPO_ROOT to per-<project-dir> for project-discovery ecosystems. This matches how build-cmd/test-cmd/check-cmd already work for those ecosystems — project roots are discovered by walking the repo for files matching project-discovery patterns (e.g. go.mod). A consumer who can place a go.mod in an unexpected location already controls where those build/test/lint commands run. No new surface.

trigger-globs fire condition — pattern matching only

L123: filenames from git status are matched against globs for fire decisions, not passed to a shell or eval'd. No injection surface.

Gates running after a build failure — behavioral change, not a security regression

Gates now execute even when the ecosystem's build/test/lint already failed (L124). A consumer-controlled gate.cmd will execute in a potentially inconsistent build state. But the consumer already controlled build-cmd/test-cmd/check-cmd, which also run in that state — no privilege change.


SUGGESTION — <files> quoting semantics unspecified for gate context (pre-existing scope)

L125 says gate cmd substitutes <files> the same way other commands do, but neither the old nor new text specifies that <files> entries are shell-quoted before interpolation into the opaque shell string. A filename containing a space, semicolon, or shell metacharacter could cause unintended shell splitting or command injection when substituted into a gate.cmd that passes <files> to a tool. Both shipped gates (go mod tidy -diff, dotnet restore --locked-mode) do not use <files>, so no bundled gate exercises this path. This risk class already existed for build-cmd/test-cmd/check-cmd; it is not introduced by this PR. Worth a follow-on schema-level note or quoting convention for future gate authors who may use <files> in cmd.

No GitHub Actions workflows touched. No credentials, tokens, or secrets in any changed file. No external HTTP requests introduced.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: fix(toolchain): execute resolved ecosystem gates array in check (#926)

  • Gather context and understand the PR scope
  • Read changed files and diff (including the latest commit c8862de)
  • Analyze SKILL.md changes for correctness and alignment
  • Review plugin.json version bump
  • Review CHANGELOG.md entry
  • Post comprehensive review findings

Overall verdict: APPROVE. This is a clean, well-scoped fix. All previously identified nits (1–3 from earlier reviews) and the Codex P2 finding are addressed. One very minor documentation gap noted below — not blocking.


What changed in the latest commit (c8862de)

The Codex P2 finding ("Execute module gates from each discovered Go module root") is correctly resolved. The new bullet at L125 now specifies the gate's execution location as matching its ecosystem's own build/test/lint location:

once per resolved <project-dir> for a project-discovery ecosystem, from the anchor's directory for an anchor ecosystem, and from $REPO_ROOT only when neither is defined

The motivating failure mode is also documented inline: a $REPO_ROOT-only go mod tidy -diff falsely fails when the sole module is nested (go.mod file not found) and silently misses drift in nested modules when both a root and nested module exist. Both cases are now caught. The CHANGELOG entry for 0.10.0 was updated in the same commit to reflect this behavior.


Status of prior review observations

Nit Status
Nit 1 — report scaffold lacked gate-failure example ResolvedL143–145 now shows a Gates: summary line and gate-failure Overall in the fenced code block
Nit 2 — <files> expansion inside gate.cmd was ambiguous ResolvedL125 now explicitly states <files> expands to the project's scoped changed-files subset in gate context, same as ecosystem commands
Nit 3 — PASS-case gate-count format unspecified ResolvedL150 now provides both the FAIL example and an explicit PASS example: Overall: PASS (2 of 2 ecosystems passed, 1 gate passed)
Nit 4 — per-gate install-hint gap (pre-existing schema gap) Still open; schema has no per-gate install-hint; out of scope for this PR
Codex P2 — gates only ran from $REPO_ROOT, not per module root Resolved in c8862de

Remaining observation (low priority, not blocking)

Per-project gate run reporting format is underspecified. When project-discovery results in N project roots and a gate fires, it runs N times (once per <project-dir>). The fire condition (trigger-globs vs the full changed-files set) is correctly specified as a single repo-wide decision. But the report spec at L143 shows Gates: go-mod-tidy-drift — FAIL (...) as a single line — it doesn't say whether a gate that runs 3 times (e.g. 3 Go module roots) should appear as 3 separate per-project lines or one aggregated line. If ./cmd passes and ./internal fails, an aggregated FAIL is the right verdict, but a reader would lose the per-project detail. This is a documentation completeness gap that would be noticeable in a real monorepo failure. A follow-on can address it (e.g. go-mod-tidy-drift [./internal] — FAIL (...)), but it doesn't block this PR — the fix is correct and the FAIL verdict propagates either way.


plugins/toolchain/.claude-plugin/plugin.json

Version bump 0.9.00.10.0 is correct — bundled gates were previously inert (no behavioral effect); making them execute and adding per-project-root execution is new backward-compatible behavior, warranting a minor-version bump under semver.


plugins/toolchain/CHANGELOG.md

The [0.10.0] / ### Added entry is accurate and complete. It mentions: gate array execution, the per-project-root fix and the failure modes it prevents, both the bundled (go-mod-tidy-drift) and consumer-declared gate shapes, and that lint is unchanged. Well-scoped.

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

ℹ️ 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 on lines +121 to +123
**CI-parity gates (resolved `gates` array).** After an affected ecosystem's build → test → lint, iterate its resolved `gates` array (§1.5 — bundled default or consumer file, per the ladder). Gates cover the CI-parity checks plain build / test / lint don't catch: lockfile drift, generated-artifact freshness, schema regeneration. For each gate:

- **Fire condition** — if `trigger-globs` is present, run the gate only when ≥1 changed file matches, matched against the **full** changed-files set (not the ecosystem-scoped subset — a gate's trigger files need not classify into the ecosystem's own `globs`). If `trigger-globs` is omitted, run whenever the ecosystem runs. A present `trigger-globs` with no matching changed file (e.g. `check all` on a clean tree) → the gate does not fire.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Select ecosystems when a gate trigger matches

With automatic targeting, an ecosystem is selected only when a changed file matches its globs; consequently, a consumer gate such as a Go buf/protobuf freshness check with trigger-globs: ["*.proto"] is never reached when only a .proto file changes, because Go's normal globs do not include that file. This contradicts the stated support for gate trigger files that do not classify into the ecosystem and leaves those CI-parity gates inert unless the user explicitly runs /toolchain:check go or all. Include ecosystems whose gates match the full changed-file set before iterating their gates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

# Finding Classification Evidence Reacted
1 Select ecosystems when a gate trigger-globs matches, even if the ecosystem's own globs don't classify any changed file VALID (defer — author design decision) Real gap. §1 Ecosystem detection selects the affected set by matching git status against each ecosystem's own globs; the gate loop lives inside "For each affected ecosystem" (§2 CI-parity gates). So an ecosystem whose only relevance to a change is a gate trigger-globs match — not its own globs — is never selected under auto-targeting, and its gates never evaluate. That contradicts the new §2 claim that a gate's trigger files "need not classify into the ecosystem's own globs." Scope is narrow (P2-consistent): the only bundled gate, go.yaml's go-mod-tidy-drift, has trigger-globs: [go.mod, go.sum, *.go] — a strict subset of go's own globs: [*.go, go.mod, go.sum] — so no shipped gate exercises the cross-ecosystem path; the gap bites only consumer-declared cross-ecosystem gates (e.g. a Go buf/protobuf gate with trigger-globs: ["*.proto"]), and even then check all / check go still fire it. 👍

Deferring rather than fixing on-branch because the remediation is a genuine design fork the author should own, not a mechanical edit — the two directions point opposite ways:

  • (a) Expand auto-selection (this comment's suggestion): pull an ecosystem into the affected set when any of its resolved gates' trigger-globs match the full changed-file set. This then raises "do you also run that ecosystem's build/test/lint, or only its gates?" — likely a new gate-only sweep concept the workflow doesn't currently model — and it requires resolving gates (§1.5, ladder) during detection (§1), before ladder resolution currently runs.
  • (b) Narrow the claim: qualify the "need not classify into the ecosystem's own globs" wording to state that, under auto-targeting, a cross-ecosystem gate fires only via check all / check <ecosystem>.

Add-capability vs. document-reduced-behavior is the maintainer's call, so surfacing for a human decision rather than picking one autonomously. Branch was freshened with main in the interim (merge 41fa7c5) — no toolchain overlap; this finding is unaffected by that merge.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Classifying the four 🟡 Nit items from the PR review (verdict was APPROVE — no blocking issues):

# Finding Classification Evidence Reacted
1 Report scaffold code block lacks a gate-failure example VALID — fixed The ### 3. Report results scaffold now includes a Gates: go-mod-tidy-drift — FAIL (...) row and an Overall: FAIL (1 of 2 ecosystems failed, 1 gate failed) line, so the template is complete for an implementer who skims only the code block. 👍
2 <files> expansion inside gate.cmd is ambiguous (full vs ecosystem-scoped set) VALID — fixed The gate-execution line now states that when a gate cmd uses <files> it expands to that project's scoped changed-files subset, consistent with §2 (<files> per L107). This landed alongside the per-project execution-location fix, which is what made the scope well-defined. 👍
3 PASS-case gate-count format not specified VALID — fixed L148 now states the Overall line names the gate count on a fired-and-passed gate too (e.g. Overall: PASS (2 of 2 ecosystems passed, 1 gate passed)), removing the ambiguity. 👍
4 Ecosystem install-hint reused for gates that may use a different tool VALID (defer) Correctly identified as a pre-existing schema gapecosystem.schema.json's gates items have no per-gate install-hint field, so this is not introduced by this PR and cannot be fixed without a schema change. Deferring to a follow-on: add an optional per-gate install-hint to the gate schema. No shipped gate is affected today (both go mod tidy -diff and dotnet restore --locked-mode use the ecosystem's own tool). 👍

Fix commit: c8862de. Nits 1-3 fixed on-branch; Nit 4 deferred as a schema follow-on. Branch was also freshened with main (merge fe44e9c) — no toolchain overlap, no version collision (0.10.0 is unreleased and remains above main's 0.9.0).

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: fix(toolchain): execute resolved ecosystem gates array in check (#926)

  • Gather context and understand the PR scope
  • Read changed files and diff
  • Analyze new Codex P2 finding (ecosystem selection for gate trigger matching)
  • Classify and write fix for Codex P2
  • Post comprehensive review findings

Overall verdict: APPROVE with one blocking fix. The PR is correct and well-scoped. The new Codex P2 finding (from the c8862de review) is valid — it identifies a genuine spec gap that makes the gate fire-condition clause unreachable in automatic mode. All prior nits (1–3 from earlier reviews) and the prior Codex P2 (gate execution location) are confirmed resolved. One fix needed before merge, described below with the exact text.


Status of prior findings

Finding Status
Nit 1 — report scaffold lacked gate-failure example ✅ Resolved — Gates: row + gate-failure Overall now in fenced block
Nit 2 — <files> in gate cmd was ambiguous ✅ Resolved — <files> explicitly expands to project-scoped subset in gate context
Nit 3 — PASS-case gate-count format unspecified ✅ Resolved — PASS example added at L150
Nit 4 — per-gate install-hint schema gap 🔵 Deferred (pre-existing schema gap, out of scope)
Prior Codex P2 — gates ran from $REPO_ROOT only ✅ Resolved in c8862de — gates now run per-project-root

🔴 Blocking: New Codex P2 — gate-trigger ecosystems never reached in automatic detection

Finding from commit c8862de review:

With automatic targeting, an ecosystem is selected only when a changed file matches its globs; a gate with trigger-globs: ["*.proto"] on the Go ecosystem is never reached when only a .proto file changes, because Go's normal globs don't include that file. Include ecosystems whose gates match the full changed-file set before iterating their gates.

Verdict: VALID. Fix required.

The spec contains a self-contradiction:

  • §1 (ecosystem detection) selects ecosystems where ≥1 changed file matches the ecosystem's own globs.
  • §2 gate fire condition (L123) says: "matched against the full changed-files set (not the ecosystem-scoped subset — a gate's trigger files need not classify into the ecosystem's own globs)".

The §2 clause is unreachable: if the ecosystem's globs didn't match, §1 never includes the ecosystem, so §2 is never entered. A gate with trigger-globs: ["*.proto"] on the Go ecosystem is permanently inert in automatic mode unless the user explicitly runs /toolchain:check go.

Proposed fix — add a gate-trigger inclusion pass in §1, immediately after the current first sentence:

**Gate-trigger inclusion (automatic detection only).** After the `globs` pass, also
resolve each non-selected covered ecosystem's `gates` array and include it if at least
one gate's `trigger-globs` matches ≥1 file in the full changed-files set. A
gate-only-included ecosystem skips build/test/lint entirely — all three columns show
`—` in the report table, because its `globs` matched nothing and there is nothing to
build, test, or lint; only its gates run, under the same fire-condition, tool-presence,
and outcome rules in §2. (This is what makes the gate fire-condition clause "a gate's
trigger files need not classify into the ecosystem's own `globs`" reachable — without
this pass, an ecosystem whose `globs` did not match would never be reached and its
gates would be inert.)

Also add a bullet to the [0.10.0] CHANGELOG entry (under the existing ### Added bullet):

- **Gate-trigger ecosystem inclusion.** Automatic ecosystem detection now includes any
  covered ecosystem whose resolved `gates` array has a gate with `trigger-globs`
  matching ≥1 changed file, even when the ecosystem's own `globs` matched nothing.
  Without this, a gate declaring `trigger-globs` outside the ecosystem's own file
  patterns (e.g. a Go buf/proto freshness gate on `*.proto`) was inert in automatic
  mode — the ecosystem was never selected and its gates were never reached. A
  gate-only-included ecosystem skips build/test/lint (all `—`); only its gates run.

Fix this →


plugins/toolchain/.claude-plugin/plugin.json

Version bump 0.9.00.10.0 is correct. The gate-trigger inclusion fix (if applied) remains a minor-version addition within the 0.10.0 release.

plugins/toolchain/CHANGELOG.md

Accurate, well-scoped. The gate-trigger inclusion bullet above should be added to complete the entry.


I prepared a commit with this exact fix locally, but push permissions are restricted to the PR author for this branch. The "Fix this →" link above will apply the change directly.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 40s —— View job


Security Review — PR #1020

  • Gather context and understand PR scope
  • Fetch PR diff (commits fb0afb9, c8862de, merge 41fa7c5)
  • Read changed files (SKILL.md, plugin.json, CHANGELOG.md)
  • Analyze gate-execution step for injection surfaces and logic flaws
  • Review per-project-dir execution change and ecosystem selection gap (Codex P2)
  • Post security findings

No blocking security issues found. This PR is a documentation-layer change — a SKILL.md instruction file, a version bump, and a changelog entry. One SUGGESTION below regarding a design gap that this PR activates for the first time.

What was reviewed

File Change Security-relevant?
plugins/toolchain/skills/check/SKILL.md Adds structured gate-execution step (§2, L121–129); updates per-project-dir execution (c8862de); adds report scaffold and verdict propagation Yes — introduces gate.cmd execution semantics and changes execution location
plugins/toolchain/.claude-plugin/plugin.json Version bump 0.9.00.10.0 No
plugins/toolchain/CHANGELOG.md New [0.10.0] section No — no credentials or PII

Referenced for trust-model verification: plugins/toolchain/reference/ecosystems/go.yaml, docs/conventions/ecosystem-commands/ecosystem.schema.json.


Analysis

gate.cmd shell execution — no new trust boundary

L125 instructs the AI to execute gate.cmd as an opaque shell string with placeholder substitution. This is structurally identical to how build-cmd, test-cmd, and check-cmd are already handled. A consumer who controls .claude/ecosystems/<ecosystem>.yaml could already declare arbitrary shell in those fields — adding gates[].cmd does not cross any new trust boundary.

Per-project-dir execution (introduced in c8862de)

The latest commit changed gate execution from $REPO_ROOT to per-<project-dir> for project-discovery ecosystems. <project-dir> values are discovered by walking the repo for files matching project-discovery patterns (e.g. go.mod) — they are repo-relative paths. This matches how build-cmd/test-cmd/check-cmd already behave for those ecosystems; no new privilege is granted. A symlink-based traversal outside the repo would require repo write access, and that access already allows build-cmd to run arbitrary content.

Bundled go.yaml gate — no filename interpolation

The only bundled gate that fires (go.yaml L41–44) runs go mod tidy -diff, which takes no user-controlled arguments. No filename from git status is interpolated into this command.

trigger-globs fire condition — pattern matching only

L123: filenames are matched against globs to decide whether a gate fires — they are used for comparison only, not passed to a shell or eval'd.

Gates running after a build failure — behavioral change, not a regression

L124: gates now execute even when a prior build/test/lint phase failed. The consumer already controlled build-cmd/test-cmd/check-cmd which run in that same partial-build state. No new privilege.

No GitHub Actions workflows touched. No credentials, tokens, or secrets in any changed file.


SUGGESTION — Ecosystem selection gap silently bypasses declared gates on non-ecosystem-classified files

Confidence: High — confirmed by reading the spec and go.yaml. Severity: SUGGESTION.

The gate-execution step in L121 iterates gates "after an affected ecosystem's build → test → lint." An ecosystem is "affected" only when a changed file matches its globs. Consequently, a consumer gate whose trigger-globs cover files not in the ecosystem's own globs (e.g. a Go ecosystem gate with trigger-globs: ["*.proto"] for protobuf freshness) will never fire when only those files change — because the Go ecosystem (globs: ["*.go", "go.mod", "go.sum"]) is not selected, so its gates array is never reached.

Before this PR all gates were inert anyway; this PR is the first time gates actually execute, which makes this silent bypass newly exploitable. A consumer who declares a security-critical gate (e.g. a schema regeneration or auth-proto freshness check) for a file type that doesn't classify into the parent ecosystem's globs will have a gate that reports no output and gives false assurance — the gate never runs, but nothing tells the developer it didn't.

This is a pre-existing architectural gap in the design (the spec correctly notes "a gate's trigger files need not classify into the ecosystem's own globs" as the intent, but the ecosystem-must-be-affected prerequisite contradicts it). The fix would be to include ecosystems in the affected set when any changed file matches a gate's trigger-globs, before iterating that ecosystem's gates — the Codex P2 suggestion. Not blocking this PR, but worth resolving in a follow-on before consumers author gates on cross-ecosystem file types.


@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Classifying the one open item from this review (verdict was APPROVE; nits 1–3 and the Codex module-root P2 are marked resolved in the review's own status table, and were classified earlier on this PR):

# Finding Classification Evidence Reacted
1 Per-project gate-run reporting format underspecified — a project-discovery ecosystem with N project roots runs a fired gate N times, but the §3 scaffold shows a single Gates: <name> — <verdict> line VALID (defer — follow-on) Legitimate documentation-completeness gap, non-blocking. The gate's fire decision is correctly repo-wide (trigger-globs vs the full changed-files set), and the FAIL verdict propagates correctly regardless of shape (any project failing → gate FAIL), so correctness is unaffected. What's unspecified is whether a gate that runs across 3 module roots reports as 3 per-project lines (e.g. go-mod-tidy-drift [./internal] — FAIL) or one aggregated line — the latter loses per-project detail in a real monorepo failure. Deferring to a follow-on report-format clarification; it doesn't block this PR. 👍

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Classifying the one SUGGESTION from this security review (verdict: No security issues found):

# Finding Classification Evidence Reacted
1 <files> quoting semantics unspecified for gate context — a filename with a space/semicolon/shell metachar could split or inject when interpolated into an opaque gate.cmd VALID (defer — pre-existing, cross-cutting) Accurate, and correctly scoped as pre-existing. The SKILL.md never states that <files> entries are shell-quoted before interpolation — but this risk class is identical for build-cmd/test-cmd/check-cmd's existing <files> substitution and is not introduced by this PR (gates reuse the same substitution semantics, §2). No bundled path exercises it: the only shipped gate, go mod tidy -diff, takes no <files> argument at all. The right home for a fix is a quoting convention at the ecosystem-commands contract level (it governs every <files>-bearing command, not just gates), so deferring to a follow-on there rather than patching one clause in this skill. 👍

@kyle-sexton kyle-sexton added the agent-ready Fully specified and briefed; eligible for autonomous pickup from the frontier. label Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-ready Fully specified and briefed; eligible for autonomous pickup from the frontier. automated Opened by automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(toolchain): wire the ecosystem-commands gates schema array into an actual execution step

1 participant