Skip to content

mcpx testing: every formatter and linter in the CI matrix, pinned by the flake (and run the plugin's bun test) #1996

Closed

Description

@dezren39

Relocated from dezren39/darwin-nix#141 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #95

Summary

Put every formatter (check mode) and every linter (at warning severity or stricter, not the minimal preset) for mcpx into the test matrix, with tool versions pinned by the flake so nix fmt / the dev shell locally gives the same answer as CI. Also run the one TypeScript test file CI has never run (plugin/opencode/mcpx/daemon.test.ts). The repo already has most of the plumbing -- treefmt.nix, a flake formatter, checks.formatting, checks.tidy -- but none of it covers Go, TypeScript or shell, and none of it runs in CI.

Original notes

  • testing
    • make a comprehensive github action ci.yaml workflow (or test.yaml) that is as fast as possible,
      • all formatters and linters in test matrix, formatters in check mode, linters set to warning or more than minimal.

Current state

Existing plumbing (repo root, main d0b7e01):

  • treefmt.nix: programs.nixfmt and programs.rustfmt enabled; # programs.shellcheck.enable = true; # failures. projectRootFile = "flake.nix", so it formats the whole repo.
  • flake.nix:422-429: formatter = the treefmt wrapper (nix fmt); checks.formatting = treefmt's check derivation over inputs.self; checks.tidy = flake-tidy all --check.
  • justfile fmt: nixfmt *.nix pkgs/*.nix then nix run .#flake-tidy -- all || true -- the glob does not reach pkgs/mcpx/package.nix, and the || true swallows tidy failures. tidy-check: nix run .#flake-tidy -- all --check. So just fmt and nix fmt disagree about package.nix, and a CI check-mode formatter would fail on a file the documented local command cannot fix. Repair (from Phase 2: keep_prior revives earlier grants for a re-added member #169, see the comment below): just fmt calls nix fmt, which also picks up every program later added to treefmt.nix without a second edit.
  • pkgs/mcpx/docs/release-runbook.md:158-168 "Checks" lists gofmt -l ., go vet ./..., go test ./... -count=1, scripts/stress.sh, scripts/bench.sh, nix build .#mcpx, and "README.md must stay a symlink to docs/story.md" (test -L README.md). None of these except go test and nix build run in CI.
  • go test already runs a subset of vet (atomic, bool, buildtags, directive, errorsas, ifaceassert, nilfunc, printf, stringintconv, tests; go help test). Full go vet adds copylocks, loopclosure, lostcancel, unusedresult and others.
  • No .golangci.yml, no deno.json, no .shellcheckrc, no .editorconfig in pkgs/mcpx.
  • plugin/opencode/mcpx/daemon.test.ts: 21 tests at 05c78b2 (Phase 5: tag event bodies must travel with the event #182 added two) over the plugin's daemon discovery ladder, written for bun test (import ... from "bun:test", Bun.serve({unix}); the header says bun test plugin/opencode/mcpx/daemon.test.ts). Still never run by CI: .github/workflows/ has no bun test or deno test step. Plugin direction: build(deps): bump actions/checkout from 3 to 4 #47 (opencode v2), Update Rust crate anyhow to 1.0.75 #48 (harness-free core).

Baseline, run read-only on d0b7e01 with the tools installed on a dev machine (not the flake's pinned versions):

Check Result
gofmt -l . clean
deno fmt --check plugin (deno 2.9.6, defaults) 9 files differ: 4 TS/TSX (daemon.ts, daemon.test.ts, mcpx-session.ts, mcpx-tui.tsx) + 5 Markdown (README.md, four SKILL.md). The plugin is written in opencode's style (no semicolons); deno's default adds them. With --no-semicolons --line-width=120 all 9 still differ.
deno lint plugin 13 problems in 4 files: no-explicit-any x6, require-await x5, no-unreachable x1, no-unused-vars x1 (summarise)
shellcheck scripts/*.sh 3: SC2034 and SC2319 (warnings, stress.sh:13,87), SC2012 (info, bench.sh:60)
shfmt -l scripts/*.sh (defaults) both files differ
nixfmt --check pkgs/mcpx/package.nix "not formatted" (system nixfmt; the flake's may differ)
golangci-lint, staticcheck, gofumpt, actionlint not installed; unknown

So turning any of this on needs one clean-up commit first (or a baseline), and a style decision for TypeScript.

The tool set

Area Formatter (check mode) Linter (>= warning) Notes
Go gofmt -l (via treefmt programs.gofmt) go vet ./... + golangci-lint v2 gofumpt: stricter, reformats; opt-in, not needed
TypeScript (plugin, docs examples) deno fmt --check with a deno.json matching the existing style deno lint (recommended rules) + deno check Deno is already a CI runtime; prettier/eslint would need node_modules
Shell (scripts/*.sh) shfmt -d (pick -i to match the files) shellcheck at default severity (style, the strictest) the treefmt comment says shellcheck "failures" -- for the whole repo; scope to mcpx first
Nix (pkgs/mcpx/package.nix, flake) nixfmt (already in treefmt) statix, deadnix (optional)
Workflows -- actionlint (also shellchecks run: blocks when shellcheck is on PATH) covers mcpx-test.yml, format.yaml; zizmor is security -> deferred
Flake -- flake-tidy all --check (exists: checks.tidy, just tidy-check) repo-wide; run when flake.nix/flake.lock change
Repo invariants -- test -L pkgs/mcpx/README.md from the release runbook; one line

golangci-lint configuration, "warning or more than minimal": v2 with linters.default: standard (errcheck, govet, ineffassign, staticcheck, unused) plus a curated set that fits this codebase -- e.g. errorlint, bodyclose, noctx, misspell, unconvert, usestdlibvars, gocritic (diagnostic tags), revive (a few rules). From #169: staticcheck with checks: ["all"] rather than its default subset, and copyloopvar (Go 1.22 made the rt := rt idiom redundant; internal/e2e/runtime_test.go:15,47 still carry it). Not gosec in the blocking set: security is deferred, and on a process-spawning CLI it yields a large low-signal backlog. Not default: all: it enables style linters that contradict each other and would turn the first PR into a rewrite. Commit pkgs/mcpx/.golangci.yml; new linters are added one PR at a time with their fixes.

TypeScript style decision: either adopt deno's defaults and reformat the plugin once, or write a deno.json fmt block that matches the current opencode style. The plugin is loaded by opencode and #47/#48 may move or reshape it -- coordinate there before a mass reformat; lint can start immediately since it is independent of style.

Markdown: deno fmt also formats .md. The book is the docs tree's; exclude docs/** and AGENTS.md until the docs tree decides its own formatting.

Approaches -- how tools are installed and pinned

A -- marketplace actions per tool (least recommended)

golangci/golangci-lint-action, reviewdog/action-shellcheck, raven-actions/actionlint, each with its own version input. Fast to write, and golangci-lint-action caches well. But every tool's version lives in YAML, local runs use whatever is installed (the baseline above already used unpinned local tools), and Renovate bumps them independently of the flake. This is how "passes locally, fails in CI" starts.

B -- Go tools via the tool directive, the rest via actions

Go 1.24+ tool directives in go.mod pin Go tools and go tool golangci-lint run works locally and in CI. But it adds golangci-lint's large dependency graph to pkgs/mcpx/go.mod/go.sum, which changes the Nix vendorHash and Renovate's grouped Go bumps get much bigger (the vendorHash repair exists precisely because those bumps are painful). Pins Go tools only; shell/Nix/TS tools stay unpinned.

C -- the flake pins everything; treefmt for formatters, a dev shell for linters (recommended)

  • Formatters: extend treefmt.nix with programs.gofmt, programs.deno (fmt), programs.shfmt, keeping nixfmt; scope the new ones to pkgs/mcpx/** with per-program includes so the rest of the repo is not reformatted by this issue. treefmt-nix ships modules for gofmt, gofumpt, goimports, golangci-lint, deno, prettier, biome, shfmt, shellcheck, actionlint, nixfmt, statix, deadnix (seen in a local copy of the input; confirm against the pinned rev). CI runs nix fmt -- --ci (or builds checks.x86_64-linux.formatting) -- check mode, non-zero on any diff, prints the diff. Locally nix fmt gives byte-identical results because it is the same derivation.
  • Linters: the devShells.mcpx that Update Rust crate rand to v0.9.3 [SECURITY] #134 proposes (go, gopls, deno, bun, node, git, python3, golangci-lint), extended with shellcheck, actionlint, statix, deadnix -- one shell, pinned by flake.lock; CI runs nix develop .#mcpx -c <linter>; locally the same command. Do not run golangci-lint through treefmt: its treefmt module runs golangci-lint run --fix per file, which needs module downloads (not available in the checks sandbox) and rewrites code.
  • Costs: the Nix install (~8 s) plus substitution per lint job; competitive only with a store cache (Update Rust crate tokio to v1.38.2 [SECURITY] #135 decision 7). One flake.lock bump updates every tool at once -- that is the point, and also a risk (a formatter upgrade reformats; ⚡ Bolt: Optimize search match counting performance #142 handles that).

Recommended because it is the only option where local == CI by construction, it reuses what the repo already has, and it keeps Go tool dependencies out of mcpx's go.mod.

Matrix layout

Three lint legs, each small, plus a plugin test leg:

Leg Runs Expected
fmt nix fmt -- --ci (all formatters, scoped) seconds after Nix setup
lint-go go vet ./..., golangci-lint run (with Go build cache from #135) the slowest lint leg; type-checks the module
lint-other deno lint, deno check, shellcheck, actionlint, statix/deadnix, README symlink, flake-tidy --check when the flake changed seconds
plugin bun test plugin/opencode/mcpx/daemon.test.ts (needs Bun only; pin the flake's Bun) seconds

If queue pressure shows up (#140), fmt and lint-other merge first. Run plugin under Deno and Node too only if #48 makes the core runtime-neutral (today the test imports bun:test and Bun.serve, so it is Bun-only). scripts/stress.sh and scripts/bench.sh stay manual (they use real servers from .mcpx.json); #97 decides whether a stress leg exists.

Implementation order (small PRs)

  1. .golangci.yml with default: standard, go vet, gofmt check, shellcheck/actionlint -- fix what they find (small: gofmt is clean, shellcheck has 3).
  2. plugin leg: bun test (no reformat needed).
  3. deno lint (13 fixes) and deno check.
  4. TS/shell/Nix formatting once the style is agreed: one reformat commit per language, then the check turns on. Add the reformat commits to .git-blame-ignore-revs.
  5. Curated extra golangci linters, one per PR.

Alternative sequencing (from #169), for any linter whose backlog is too big for one fix-up PR: land it continue-on-error: true with ::warning:: annotations, fix the backlog in a dedicated PR, then flip it to blocking. Formatters block from day one either way -- a formatter has no judgement calls. Whichever path, the end state must be "clean and enforced", never another # ... # failures line like treefmt.nix:6.

Open questions

  1. TypeScript style: deno defaults vs matching opencode's (no semicolons, width 120)? Depends on build(deps): bump actions/checkout from 3 to 4 #47/Update Rust crate anyhow to 1.0.75 #48 plans for the plugin.
  2. shfmt flags for scripts/*.sh (indent width) -- match the existing files.
  3. Scope: mcpx only, or turn the whole-repo checks.formatting into a CI job too (build(deps): bump build/buck/preludes/buck2-prelude from d956689 to 3cfc766 #49's territory)?
  4. Is nix flake check usable as a CI step for mcpx, or does it evaluate/build too much of the repo (opencode patches, darwin-only packages)? Prefer building the single checks.x86_64-linux.formatting attribute.
  5. deno check of the plugin needs opencode's plugin types -- available offline, or does it need Update Rust crate anyhow to 1.0.75 #48 first?
  6. Does checks.formatting pass on main today? Nothing runs it; the baseline above already found pkgs/mcpx/package.nix unformatted by a system nixfmt. If it is red, step one is the backlog, and its size is unknown (from Phase 2: keep_prior revives earlier grants for a re-added member #169).

Ease · Impact · Related

Ease M (mostly clean-up and one style decision) · Impact med · Depends on / related: #95, #135 (legs, Nix cache, tool-version decision), #142 (auto-applies the same formatters), #140 (merge legs under queue pressure), #134 (the shared mcpx dev shell), #47, #48 (plugin shape and its tests), #49 (repo-wide format/tidy checks), #99 (which of these agents run locally: formatters yes, full lint maybe), #35 (version drift), #129 (its settings-effect guard needs this issue's plugin leg to cover plugin-scoped settings, which only the plugin's TypeScript reads).

Security/auth deferred; see tracker.

Acceptance criteria

  • Every formatter/linter named in the body's CI matrix runs in CI pinned by the flake, the plugin's bun test runs, and the workflow is green.
  • The work described in the Summary/Scope above is delivered via a branch + PR (no direct push to main); verification runs in GitHub Actions CI, not on the sandbox.
  • Evidence (CI links, timings, or probe output as applicable) is recorded on this issue before it is closed.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#141 (verbatim, oldest first):


    @dezren39 · 2026-09-30T06:50:28Z

    Salvaged from a duplicate issue (#169, now closed) -- one fact it had that this issue does not.

    just fmt and nix fmt disagree about pkgs/mcpx/package.nix.

    justfile:120-122:

    fmt:
        nixfmt *.nix pkgs/*.nix
        nix run .#flake-tidy -- all || true
    

    pkgs/*.nix is one level deep. pkgs/mcpx/package.nix is two, so the just fmt a contributor runs locally never touches it, while treefmt.nix (projectRootFile = "flake.nix", programs.nixfmt.enable = true) walks the tree and nix fmt does. A CI check-mode formatter job will therefore fail on a file that the documented local format command cannot fix, and the contributor's only recourse is to know to run nix fmt instead.

    Whichever this issue lands on, the two must be made the same command. just fmt calling nix fmt is the obvious repair, and it also picks up programs.rustfmt and anything added to treefmt.nix later without a second edit.

    Note also that nix run .#flake-tidy -- all || true swallows its own failure -- the same shape as the commented-out # programs.shellcheck.enable = true; # failures on treefmt.nix:6 that this issue already cites.


    @dezren39 · 2026-09-30T06:51:37Z

    Overlaps #169, written concurrently by a second worker on the same slice. Both are kept until reconciled; see the tracker (#95) for the differences and which one to keep.


    @dezren39 · 2026-09-30T07:21:00Z

    Integration pass. Placed in the DAG rooted at #181.


    @dezren39 · 2026-09-30T12:03:17Z

    Issues pass (2026-09-30, at 05c78b2).


    @dezren39 · 2026-09-30T12:09:47Z

    Issues pass (2026-09-30, architect follow-up). Related adds #129: its settings-effect guard (#181 collision 15) cannot cover plugin-scoped settings without this issue's plugin (bun test) leg.

  3. added
    enhancementNew feature or request
    queue/queuedHas a queued task in the Supabase agent queue
    kind/testTest coverage, test infrastructure, or test-only fixes
    priority/normalOrdinary priority (the default)
    effort/mMedium: expected under 1 day of work
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Intake review

    • Related: part of the mcpx testing relocation batch (parent/epic references in the body; re-home tracked at mcpx: re-home the open mcpx backlog from dezren39/nix, with this repo as the single source #309). Queued triage task is live in repo-health-code-pilot — this issue is not ownerless.
    • Live check: filed 2026-10-28 ~22:5xZ as a relocation; no open or merged PR implements it yet (checked against the open-PR list this run), and no duplicate open issue covers it.
    • Notes: body is well-scoped with evidence from the original filing; the acceptance criteria appended this pass are drawn from its own Summary/Scope.
    • Risks: historical issue references inside the body use dezren39/nix numbering — a worker must not chase those numbers in this repo. CI-first verification applies (GitHub Actions, never sandbox builds).
    • Suggested approach: triage task confirms the current-state claims on current main, then routes a scoped follow-up packet per the body's scope.

    🤖 Agent note · state: filed

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-28 18:19 CDT
    • Found via: monitor reconciliation (intake enrichment, protocol §5)
    • Task: Intake enrichment — acceptance criteria appended, labels normalized (kind/test, priority/normal, effort/m, app/mcpx), meta/enriched applied.
    • Expectation: effort/m class per the appended acceptance criteria.
  5. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  6. 73 remaining items

  7. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 30, 2026
  9. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    Agent note — done: PR #2718 merged (queue task 3291)

    Lane B shift resumed from the implementation_complete checkpoint and re-verified fresh:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/mMedium: expected under 1 day of workenhancementNew feature or requestkind/testTest coverage, test infrastructure, or test-only fixesmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions