Skip to content

chore(deps): adopt plugin-task v0.2026297.1542 and cut the containerd provider path over to the renamed plugin repo - #1059

Merged
github-actions[bot] merged 2 commits into
mainfrom
feat/plugin-task-third-bucket
Oct 24, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
feat/plugin-task-third-bucket

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Closes #1057

Summary

Head c0abdf6eaf868f336531597fcdea96ce4259c469 — 5 files changed, 11 insertions(+), 10 deletions(-).

Two changes, one gate, one PR — and they are one scope, not two cutovers: the second is a prerequisite of the first's gate, because the gate the bump's class demands was already red on origin/main (the evidence is pasted below).

1. The task runner's third bucket reaches the product. plugin-task is compiled into the charly binary, so the pinned module version IS the behaviour of charly task — and of the git-submodules verb that charly task sync runs.

At the pinned version the task runner counts steps and failures only. A step that ran nothing is neither: it is not a failure, so it is counted among the N steps, and it is not disclosed. A green task line can therefore read "26 step(s), 0 failed" over a run in which six comparisons never happened. The org already owns the vocabulary for exactly this state — sdk.CheckSkippedExitCode = 3 ("the phase RAN NOTHING — neither a pass nor a failure") — and the check runner honours it. The task runner had no equivalent.

This adopts the release that adds it. Two merged releases carry it: opencharly/plugin-task#45 (squash 7e26679, root tag v2026.297.1509, module tag candy/plugin-task/v0.2026297.1509) adds the third bucket — the headline becomes task %s: %d step(s), %d failed, %d skipped, the hop's aggregate ran %d task(s), %d step(s) failed, %d skipped is kept as the prefix whenever a step ran nothing, the JSON report carries "skipped", and the exit status is untouched (a skip is not a failure) — and opencharly/plugin-task#46 (squash c24c743d, root tag v2026.297.1542, module tag candy/plugin-task/v0.2026297.1542) carries the stderr-capture fix that #45's PASS asked for.

charly/go.mod:141 moves github.com/opencharly/plugin-task/candy/plugin-task from v0.2026296.2153 to v0.2026297.1542. charly/go.sum moves with it, derived by go mod tidy: the two superseded pairs (v0.2026296.1911, v0.2026296.2153) go and the one live pair (v0.2026297.1542) arrives. The v0.2026296.1911 pair was already stale on this tree — #1050 added the 2153 pair without tidying — so the tidy also restores the invariant this repo's own precedent (#1045) holds: exactly ONE plugin-task version in charly/go.sum.

No cascade. The two versions' own candy/plugin-task/go.mod are byte-identical in their requirements (sdk v0.2026276.1822, spec v0.2026276.1636), so this is a single-module bump with no sdk/spec move behind it. The pin is stated once: charly/charly.yml names the compiled-in plugin word (plugin-task), never a version.

2. The containerd provider path is cut over to the renamed plugin repo. The out-of-tree engine plugin was renamed when the engine word changed (the engine word is containerd; the retired nerdctl spelling is only the CLI binary), and its module path moved with it. Three surfaces in this repo still named the pre-rename path:

surface what it is
charly/plugin_corpus.txt the corpus entry for the plugin repo — the list pluginsgen walks to learn which plugin serves which word
charly/plugins_refs_generated.go the generated word→provider index: the three containerd words still pointed at the pre-rename module path
charly/engine_word_cutover_test.go the guardrail's canonical ref — the constant the index must equal

The index is a projection of the corpus's live word→provider map: pluginsgen fetches each corpus repo at its default branch — deliberately, because the word→ref FACT lives in the plugin repos — and records where each word is served (internal/pluginsgen/main.go, collectWordRefs). A projection does not follow its source by itself, so the index was re-projected with the same pluginsgen invocation the build uses (scripts/bootstrap-charly.sh). The corpus entry and the guardrail's constant follow the rename; regenerating the index is the replacement for the three generated keys.

This is also what the repo's own gate demands. TestPluginsRefsIndexSound compares the committed index against what the corpus serves right now, and its own failure message says "the index is FALSE; regenerate and commit it". The gate had gone red for every checkout of this repo — including a fresh clone of main — because the corpus moved after the index was last committed. The build's scripts/check-generated-clean.sh deliberately classifies this file as upstream and restores the tracked copy, so a build never leaves the tree carrying a fresh projection: the remedy is a commit here.

What this change does not do. It changes no line of product logic of its own — the task runner's behaviour is the module's, and the module is upstream. It does not touch the umbrella's guidance; the umbrella's leg follows this one, because the umbrella's binary is built from this repo's pin. The containerd programme's remaining consumer legs stay with their own repos, tracked at opencharly/opencharly#589.

How tested

The change class is a dependency bump plus a generated-artifact cutover: the pinned module IS the behaviour, so the class's gate is this repo's own CI shape — the worktree-local build, the generated-wiring check, go vet, go test, the tools/* sweep, gofmt, and golangci-lint over charly/ and every tools module — plus the live differential, because a bump's proof is the behaviour the two binaries differ in: the same fixture task, run against the binary built from origin/main and against the binary this change builds. The check must FAIL against the before binary, or it proves nothing.

The toolchain the gate runs on. The gate pins GOTOOLCHAIN=go1.26.8 — the version this repo's own charly/go.mod declares, and the version CI uses (go-version-file: charly/go.mod). The host's Go is 1.27.3, whose export data (version 5) the pinned golangci-lint v2.13.2 cannot decode ("export data version 5 is greater than maximum supported version 4"), so a lint run on the host toolchain fails on every stdlib import for a reason that has nothing to do with the tree. Pinning the repo's own toolchain makes the local gate the same gate CI runs.

The gate, run on the committed tree this PR pushes:

=== go version ===
go version go1.26.8 linux/amd64

=== 1. build (R9): scripts/bootstrap-charly.sh ===
bootstrap-charly: regenerating compiled-in plugin wiring
bootstrap-charly: building bin/charly
bootstrap-charly: built ./bin/charly (2026.297.1708)
BUILD_RC=0

=== 2. the generated wiring is clean (scripts/check-generated-clean.sh) ===
GENERATED_RC=0

=== 3. the binary is the worktree-local build ===
2026.297.1708
VERSION_RC=0

=== 4. go vet ./... (from charly/) ===
VET_RC=0

=== 5. go test ./charly/... (from the repo root, CI shape) ===
ok  	github.com/opencharly/charly/charly	178.350s
ok  	github.com/opencharly/charly/charly/internal/pluginsgen	128.382s
TEST_RC=0

=== 6. the tools/* sweep (GOWORK=off, -count=1) ===
== tools/generate-packages/ ==
?   	github.com/opencharly/charly/candy/generate-packages/cmd/serve	[no test files]
== tools/golden-cloudinit/ ==
?   	github.com/opencharly/charly/tools/golden-cloudinit	[no test files]
== tools/golden-compile/ ==
?   	github.com/opencharly/charly/tools/golden-compile	[no test files]
== tools/gomod-canonical/ ==
ok  	github.com/opencharly/charly/tools/gomod-canonical	0.001s
== tools/ledger-validate/ ==
ok  	github.com/opencharly/charly/tools/ledger-validate	0.002s
TOOLS_RC=0

=== 7. gofmt (gofmt -l charly tools) ===
gofmt: no files to rewrite
GOFMT_RC=0

=== 8. golangci-lint fmt --diff (charly/) — must print nothing ===
FMT_RC=0

=== 9. golangci-lint v2.13.2 (charly/) ===
0 issues.
LINT_RC=0

=== 10. golangci-lint v2.13.2 (each tools module) ===
== lint tools/generate-packages/ ==
0 issues.
== lint tools/golden-cloudinit/ ==
0 issues.
== lint tools/golden-compile/ ==
0 issues.
== lint tools/gomod-canonical/ ==
0 issues.
== lint tools/ledger-validate/ ==
0 issues.
TOOLSLINT_RC=0

=== 11. the binary's module set carries the new plugin-task ===
	dep	github.com/opencharly/plugin-task/candy/plugin-task	v0.2026297.1542	h1:Av9Ibh+iYdIe0jCnRZqZFg91GgeJx0ZYBn28AK2k7Hg=
MODS_RC=0

=== 12. the tree is still clean (the generated files are committed state) ===
STATUS_RC=0

GATE_DONE: all steps passed
GATE_EXIT=0

The live differential. The SAME fixture task — one step that runs, one step that ran nothing (skip: true) — run against two binaries built from two trees: this branch's tree, and origin/main (the tree this branch's parent is). The check must FAIL against the before binary, or it proves nothing (R7).

=== the BEFORE tree: build its binary (a detached worktree at origin/main) ===
bootstrap-charly: regenerating compiled-in plugin wiring
bootstrap-charly: building bin/charly
check-generated-clean: NOTICE — charly/plugins_refs_generated.go differs because it was indexed from a source this checkout does not pin
check-generated-clean:   (a corpus plugin's default branch, or a --dev-plugin local checkout); restoring the tracked file.
bootstrap-charly: built ./bin/charly (2026.297.0121)
BUILD_BEFORE_RC=0
=== the binaries under test ===
  NEW    : 2026.297.1708
  BEFORE : 2026.297.0121
=== the fixture ===
# The fixture for the live differential of opencharly/charly#1057: ONE task whose
# plan holds a step that RUNS and a step that RAN NOTHING (skip: true).
repo: github.com/opencharly/ptthird-fixture
skipped-bucket:
  task:
    description: one step that runs, one step that ran nothing
    plan:
      - run: the step that runs
        command: echo ran
      - run: the step that ran nothing
        skip: true
        command: echo never

=== the NEW binary: the same fixture ===
task skipped-bucket: 2 step(s), 0 failed, 1 skipped
  [pass] run — the step that runs
      exit=0
  [skip] run — the step that ran nothing
      skip: true
TASK_RC=0
--- the same task, --json ---
[
  {
    "name": "skipped-bucket",
    "status": "ran",
    "failed": 0,
    "skipped": 1,
    "steps": [
      {
        "keyword": "run",
        "text": "the step that runs",
        "status": "pass",
        "message": "exit=0"
      },
      {
        "keyword": "run",
        "text": "the step that ran nothing",
        "status": "skip",
        "message": "skip: true"
      }
    ]
  }
]
JSON_RC=0

=== the BEFORE binary: the SAME fixture ===
task skipped-bucket: 2 step(s), 0 failed
  [pass] run — the step that runs
      exit=0
  [skip] run — the step that ran nothing
      skip: true
TASK_RC=0
--- the same task, --json ---
[
  {
    "name": "skipped-bucket",
    "status": "ran",
    "failed": 0,
    "steps": [
      {
        "keyword": "run",
        "text": "the step that runs",
        "status": "pass",
        "message": "exit=0"
      },
      {
        "keyword": "run",
        "text": "the step that ran nothing",
        "status": "skip",
        "message": "skip: true"
      }
    ]
  }
]
JSON_RC=0

=== the CHECK: the headline must disclose the step that ran nothing ===
  [NEW] headline: task skipped-bucket: 2 step(s), 0 failed, 1 skipped
  [NEW] PASS: the third bucket is disclosed
  [BEFORE] headline: task skipped-bucket: 2 step(s), 0 failed
  [BEFORE] FAIL: the headline does not disclose a step that ran nothing
NEW_RC=0 BEFORE_RC=1

LIVE_PROOF_OK: the check passes against the new binary and FAILS against the before binary
LIVE_RC=0
DIFF_EXIT=0

The BEFORE build's check-generated-clean NOTICE is the drift this PR cuts over: on origin/main the committed index differs from what the corpus serves, so the build restores the tracked file and says so. The gate on THIS tree prints no such notice — step 2 above, GENERATED_RC=0.

The re-projection is derived, not hand-written. Regenerating the index on the cut-over corpus, with the build's own invocation, produces no diff — the committed file is byte-identical to the one the build derives:

$ (cd charly && GOWORK=off go run ./internal/pluginsgen -root .. -config charly/charly.yml \
    -out charly/plugins_generated.go -gowork go.work -gowork-dev go.work.dev \
    -outrefs charly/plugins_refs_generated.go -corpus charly/plugin_corpus.txt)
Downloading github.com/opencharly/plugin-containerd:HEAD...
$ git status --short
<empty>

The R5 sweeps. Two claims, two keys, each swept over the product tree (charly/) — and over the WHOLE tree for the second claim, CHANGELOG/ excepted.

Claim 1 — the superseded plugin-task versions are gone from the tree.

$ git grep -n "v0\.2026296\.2153" origin/main -- charly
origin/main:charly/go.mod:141:	github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.2153
origin/main:charly/go.sum:264:github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.2153 h1:p/YKcCgnHZuQkf9axRdZJbenbQ0XWcv4pFyqselThnk=
origin/main:charly/go.sum:265:github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.2153/go.mod h1:PZ2K4/S3bIRJWjgiLEmyxWWQnPnKGYIMUrl4vpUXftc=
grep exit=0

$ git grep -n "v0\.2026296\.2153" -- charly
grep exit=1

$ git grep -n "v0\.2026296\.1911" -- charly
grep exit=1

$ grep -n "plugin-task/candy/plugin-task" charly/go.mod
141:	github.com/opencharly/plugin-task/candy/plugin-task v0.2026297.1542

$ grep -n "plugin-task/candy/plugin-task" charly/go.sum
262:github.com/opencharly/plugin-task/candy/plugin-task v0.2026297.1542 h1:Av9Ibh+iYdIe0jCnRZqZFg91GgeJx0ZYBn28AK2k7Hg=
263:github.com/opencharly/plugin-task/candy/plugin-task v0.2026297.1542/go.mod h1:PZ2K4/S3bIRJWjgiLEmyxWWQnPnKGYIMUrl4vpUXftc=

Claim 2 — the pre-rename plugin repo path is gone from the tree.

$ git grep -n "plugin-nerdctl" origin/main -- charly
origin/main:charly/engine_word_cutover_test.go:106:	const wantRef = "github.com/opencharly/plugin-nerdctl/candy/plugin-nerdctl"
origin/main:charly/plugin_corpus.txt:79:github.com/opencharly/plugin-nerdctl
origin/main:charly/plugins_refs_generated.go:35:	"command:containerd":        "github.com/opencharly/plugin-nerdctl/candy/plugin-nerdctl",
origin/main:charly/plugins_refs_generated.go:98:	"engine:containerd":         "github.com/opencharly/plugin-nerdctl/candy/plugin-nerdctl",
origin/main:charly/plugins_refs_generated.go:154:	"verb:containerd":           "github.com/opencharly/plugin-nerdctl/candy/plugin-nerdctl",
grep exit=0

$ git grep -n "plugin-nerdctl" -- charly
grep exit=1

$ git grep -n "plugin-nerdctl" -- . ":!CHANGELOG"
grep exit=1

$ grep -n "plugin-containerd" charly/plugin_corpus.txt
27:github.com/opencharly/plugin-containerd

$ git grep -n "plugin-containerd/candy/plugin-containerd" -- charly
charly/plugins_refs_generated.go:35:	"command:containerd":        "github.com/opencharly/plugin-containerd/candy/plugin-containerd",
charly/plugins_refs_generated.go:98:	"engine:containerd":         "github.com/opencharly/plugin-containerd/candy/plugin-containerd",
charly/plugins_refs_generated.go:154:	"verb:containerd":           "github.com/opencharly/plugin-containerd/candy/plugin-containerd",
charly/engine_word_cutover_test.go:106:	const wantRef = "github.com/opencharly/plugin-containerd/candy/plugin-containerd"

The old version strings and the old repo path survive only in CHANGELOG/, where history belongs.

The anchor — every pasted line above belongs to this commit:

$ git rev-parse HEAD
c0abdf6eaf868f336531597fcdea96ce4259c469

Rulebook compliance

  • R0 — skills first. The skills this area selects were loaded before the first edit: /charly-internals:git-workflow (the landing mechanics), /charly-internals:go (this repo's Go gate recipe), /charly-internals:go-quality (the formatter gate and the lint sweep), /charly-internals:plugin (the generated plugin wiring and the provider index), /charly-internals:strict-policy and /charly-internals:root-cause-analyzer (the discipline and the anomalies), plus this repo's own AGENTS.md.
  • R1 — every anomaly got a root-cause analysis, and every finding is fixed or filed. Three findings, each with a fresh root-cause analysis, each fixed in this change or filed.

1. charly/go.sum carried a superseded version's hash pair — FIXED here, control gap filed.
Mechanism: a pin bump performed with go get adds the new version's hashes and never prunes the superseded pair; nothing in the bump path re-derives charly/go.sum. #1045's bump replaced the v0.2026296.641 pair with the v0.2026296.1911 pair, and #1050's later bump added the v0.2026296.2153 pair without re-deriving the file — so the lock carried two plugin-task pairs.
Missed control: this repo has no gate that asserts the core module's lock is the tidy artifact of its go.mod: CI runs the tests, the tools/* sweep, gofmt and golangci-lint but no tidy, and the mods-tidy task tidies the tools/* modules only. The file that would have shown the drift is exactly the file nothing checks.
Blast radius: one stale pair in charly/go.sum; no build, test or runtime effect (Go ignores entries the module graph does not need), but the lock stops being answerable — a reader cannot tell from charly/go.sum which version is live.
Root fix: derive charly/go.sum with go mod tidy — which is what this change's diff does, and it restores the one-version invariant the repo's own precedent (#1045) holds. The control gap behind it is filed as #1058, with the RCA and the shape of the gate that would close it (tidy the core module, then git diff --exit-code on charly/go.mod + charly/go.sum).

2. The generated provider-ref index named a candy path the renamed repo no longer carries — FIXED here.
Mechanism: charly/plugins_refs_generated.go is a projection of the corpus's live word→provider map: pluginsgen fetches each corpus repo at its default branch — deliberately, because the word→ref fact lives in the plugin repos — and records where each word is served. The out-of-tree engine plugin was renamed when the engine word changed, and its module path moved with it. A projection does not follow its source by itself, so the committed index kept the pre-rename module path for the three containerd words while the corpus already served them from the renamed one.
Missed control: scripts/check-generated-clean.sh deliberately classifies this file as upstream and restores the tracked copy, so no build ever leaves the tree carrying a fresh projection — the remedy is a commit here. The only gate is TestPluginsRefsIndexSound, which needs the network to compare the committed index against what the corpus serves now; it went red for every checkout of this repo, including a fresh clone of main, and nothing in the build or CI surfaced it earlier.
Blast radius: the three containerd words resolved to a ref whose candy path no longer exists in the repo the old path resolves to — that repo's candy/ holds plugin-containerd alone — and whose module path publishes nothing newer than v0.2026295.1321, while the renamed path is at v0.2026297.553; and the repo's own gate was red on every checkout, blocking every PR in the repo. The old repo path still RESOLVES — GitHub's rename redirect answers it — which is exactly why the drift stayed invisible. Measured, not inferred:
Root fix: cut the corpus entry and the guardrail's constant over to the renamed repo, and re-project the index with the build's own pluginsgen invocation (scripts/bootstrap-charly.sh) — the derivation, not a hand edit that satisfies the gate. The regeneration is a no-op on the committed tree (pasted above). The tracker's thread is opencharly/opencharly#589.

$ (cd /tmp/modprobe2 && GOTOOLCHAIN=go1.26.8 GOPROXY=https://proxy.golang.org go list -m github.com/opencharly/plugin-nerdctl/candy/plugin-nerdctl@latest)
github.com/opencharly/plugin-nerdctl/candy/plugin-nerdctl v0.2026295.1321
$ (cd /tmp/modprobe2 && GOTOOLCHAIN=go1.26.8 GOPROXY=https://proxy.golang.org go list -m github.com/opencharly/plugin-containerd/candy/plugin-containerd@latest)
github.com/opencharly/plugin-containerd/candy/plugin-containerd v0.2026297.553
$ gh api repos/opencharly/plugin-containerd/contents/candy --jq '.[].name'
plugin-containerd
$ gh api repos/opencharly/plugin-nerdctl --jq '{full_name, created_at, pushed_at, message}'
{"created_at":"2026-09-23T17:01:01Z","full_name":"opencharly/plugin-containerd","message":null,"pushed_at":"2026-10-24T05:53:39Z"}

3. The corpus entry and the guardrail's constant still named the pre-rename repo — FIXED here.
Mechanism: charly/plugin_corpus.txt is a hand-maintained list of plugin repo paths, and the plugin repo's rename landed without a consumer leg in this repo. Nothing in the build or CI asserts that a corpus entry is a repo that exists under that name.
Missed control: no sweep of the corpus against the org's live repo names; the drift stays invisible because GitHub's rename redirect answers the old path, so the fetch succeeds and returns the renamed module path — the mismatch only shows up as the index disagreement above.
Blast radius: one corpus entry and one test constant; the index followed from them.
Root fix: both are cut over in this change, and the claim-keyed sweep is pasted above — the pre-rename path no longer occurs anywhere in the tree outside CHANGELOG/.

One anomaly that is a gate-hygiene fact, not a product defect. The repo's pinned golangci-lint v2.13.2 cannot decode the host Go 1.27.3's export data (version 5 > the maximum version 4 it supports), so a lint run on the host toolchain failed on every stdlib import — on a tree CI lints green. Missed control: the local gate recipe did not pin the repo's declared toolchain. Root fix: the gate runs with GOTOOLCHAIN=go1.26.8 — the version this repo's own charly/go.mod declares and the version CI uses (go-version-file: charly/go.mod). No tree change: CI was already right; the local gate now runs the same toolchain.

  • R2 — no duplication. The module version is stated once, in charly/go.mod; charly/go.sum carries the two hashes that Go derives from it — and, after the tidy, exactly one version's pair, as the repo's own precedent (chore(deps): adopt plugin-task v0.2026296.1911 (the bump advances an unmaterialized pin in the index) #1045) holds — and charly/charly.yml names the compiled-in plugin word, not a version. The provider path is stated once per surface it must appear on, and the index that repeats it is generated from the corpus, not hand-maintained beside it.
  • R3 — no workaround. The releases are adopted as published; nothing is retried, slept, suppressed or worked around. The index is re-derived with the build's own tool, not patched by hand to satisfy the gate. The proof runs the product's own verb against real fixtures.
  • R4 — the product first, then the prose. The producer landed first: feat(task): give the task summary its third bucket — a step that ran nothing plugin-task#45 and chore(box/cachyos): bump gitlink — charly-cachyos is no longer a check bed #46 are merged and tagged before this consumer leg, and the plugin repo's rename landed before this cut-over. No prose is written here; the behaviour is the module's.
  • R5 — the legacy is gone, with the claim-keyed greps pasted. Two sweeps, two keys, both pasted above: the superseded plugin-task versions, and the pre-rename plugin repo path. The retired provider words (engine:nerdctl, verb:nerdctl, command:nerdctl) stay retired — the guardrail's negative arms still reject them.
  • R6 — user work and git safety. git status was read before the first edit; the change is two commits on a branch cut from origin/main; no history was rewritten, no force-push, no amend after a push, no hook bypass, no direct push to main, and no submodule was edited. The before-arm's detached worktree is a sibling worktree of the same repo, created for the differential.
  • R7 — the coverage fails without the change. The live check is the discriminating one: against the binary built from origin/main it fails — the headline does not disclose the step that ran nothing — and against the binary this PR builds it passes. The same fixture, the same assertion, two binaries, two different headlines. The repo's own TestPluginsRefsIndexSound is the second discriminator: it was red on the tree before this change and green on the tree this PR pushes.
  • R8 — live or skip. No service is crossed and none is mocked: the proof drives the product's own verb against a real fixture task, and the binaries are the product's own builds.
  • R9 — binary equals source. The gate used the worktree-local build — charly version reports the CalVer of this commit — and go version -m on that binary shows the new module in its dependency set. The shared host install was never used.
  • R10 — a fresh, on-commit proof. The gate ran on the committed tree this PR pushes, with zero warnings. The org-wide charly/pr-validator is the merge gate.

Change classification

A dependency bump — the pinned module IS the behaviour, so the class's gate is this repo's own CI shape (build, generated-wiring check, go vet, go test, the tools/* sweep, gofmt, golangci-lint) plus a live differential between the binary built from origin/main and the binary this PR builds — together with a generated-artifact cutover, whose gate is the repo's own TestPluginsRefsIndexSound plus a regeneration that is a no-op on the committed tree. All of it is pasted above, run on the committed tree this PR pushes, with zero warnings. The two parts are one scope, not two cutovers in one PR: the containerd cut-over is the prerequisite of the bump's gate — TestPluginsRefsIndexSound was already red on origin/main (the BEFORE build's check-generated-clean NOTICE, pasted above, is that drift), so the bump alone could not pass the gate its own class demands, and R1 classifies a red gate as blocking on this change. The tier earned is fully tested and validated: the changed path executed live, and the check that proves it fails without the change.

Filed issues

Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

…s a step that ran nothing)

`plugin-task` is compiled into the charly binary, so the pinned module version IS
the behaviour of `charly task` — and of the `git-submodules` verb that `charly task
sync` runs.

At v0.2026296.2153 the task runner counts STEPS and FAILURES only. A step that ran
nothing is neither: it is not a failure, so it is counted among the N steps, and it
is not disclosed. A green task line can read "26 step(s), 0 failed" over a run in
which six comparisons never happened. The org already owns the vocabulary for
exactly this state — `sdk.CheckSkippedExitCode = 3` ("the phase RAN NOTHING —
neither a pass nor a failure") — and the check runner honours it. The task runner
had no equivalent.

Two merged releases carry it: opencharly/plugin-task#45 (squash 7e26679, root tag
v2026.297.1509, module tag candy/plugin-task/v0.2026297.1509) adds the third
bucket, and opencharly/plugin-task#46 (squash c24c743d, root tag v2026.297.1542,
module tag candy/plugin-task/v0.2026297.1542) carries the stderr-capture fix that
#45's PASS asked for. This adopts the second.

`charly/go.mod:141` moves `github.com/opencharly/plugin-task/candy/plugin-task`
from `v0.2026296.2153` to `v0.2026297.1542`. `charly/go.sum` moves with it,
derived by `go mod tidy`: the two superseded pairs (`v0.2026296.1911`,
`v0.2026296.2153`) go and the one live pair (`v0.2026297.1542`) arrives. The
`v0.2026296.1911` pair was already stale on this tree — #1050 added the `2153` pair
without tidying — so the tidy also restores the invariant this repo's own precedent
(#1045) holds: exactly ONE plugin-task version in `charly/go.sum`.

The two versions' own `candy/plugin-task/go.mod` are byte-identical in their
requirements (`sdk v0.2026276.1822`, `spec v0.2026276.1636`), so no `sdk`/`spec`
move is behind this bump. The pin is stated once: `charly/charly.yml` names the
compiled-in plugin WORD (`plugin-task`), never a version.

Closes #1057

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
…gin repo

The out-of-tree engine plugin was RENAMED `plugin-nerdctl` ->
`plugin-containerd` (the engine word changed first; the repo name followed),
and its module path moved with it: `candy/plugin-nerdctl` ->
`candy/plugin-containerd`, module
`github.com/opencharly/plugin-containerd/candy/plugin-containerd`.

Three surfaces in this repo still named the pre-rename path:

- `charly/plugin_corpus.txt:79` — the corpus entry for the plugin repo;
- `charly/plugins_refs_generated.go` — the generated word->provider index,
  whose three containerd words still pointed at the old module path;
- `charly/engine_word_cutover_test.go:106` — the guardrail's canonical ref.

`charly/plugins_refs_generated.go` is a PROJECTION of the corpus's
word->provider map: pluginsgen fetches each corpus repo at its default branch
— deliberately, because the word->ref FACT lives in the plugin repos — and
records where each word is served (internal/pluginsgen/main.go,
collectWordRefs). A projection does not follow its source by itself, so the
index was re-projected with the SAME pluginsgen invocation the build uses
(scripts/bootstrap-charly.sh). The corpus entry and the guardrail's constant
follow the rename; regenerating the index is the replacement for the three
generated keys.

This is what the repo's own gate demands: `TestPluginsRefsIndexSound` compares
the committed index against what the corpus serves RIGHT NOW, and its own
failure message says "the index is FALSE; regenerate and commit it". The gate
had gone red for every checkout of this repo — including a fresh clone of
main — because the corpus moved after the index was last committed.

The build's `scripts/check-generated-clean.sh` deliberately classifies this
file as `upstream` and RESTORES the tracked copy, so a build never leaves the
tree carrying a fresh projection: the remedy is a commit here.

The retired words (`engine:nerdctl`, `verb:nerdctl`, `command:nerdctl`) stay
retired: the guardrail's negative arms still reject them.

Refs: opencharly/opencharly#589 (the containerd programme's consumer legs).

Agent: charly

*Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)*
@github-actions

Copy link
Copy Markdown
Contributor

Review — PASS

What the diff actually does

The change is exactly what the title claims, and it is small and coherent:

file change verdict
charly/go.mod plugin-task pin v0.2026296.2153 → v0.2026297.1542 (line 141, direct require) matches body
charly/go.sum removes the 1911 and 2153 hash pairs, adds the single 1542 pair matches body ("one-version invariant")
charly/plugin_corpus.txt plugin-nerdctl → plugin-containerd (sorted position preserved) matches body
charly/plugins_refs_generated.go three containerd keys re-pointed to plugin-containerd/candy/plugin-containerd matches body
charly/engine_word_cutover_test.go wantRef constant follows the rename (+ explanatory comment) matches body

Counts: the body claims 5 files, 11 insertions, 10 deletions. Summing the diffs: insertions 4+1+2+1+3 = 11, deletions 1+1+4+1+3 = 10. ✔ Reproduced.

Correctness / consistency checks

  • The go.sum 1542 go.mod hash (h1:PZ2K4…) is identical to the 2153 go.mod hash — consistent with the body's "the two versions' own candy/plugin-task/go.mod are byte-identical in their requirements", i.e. no sdk/spec cascade. The charly/go.mod spec line is untouched and no new transitive entries appear in the diff, confirming no cascade. ✔
  • The three generated keys and the test constant are the same ref, so TestPluginRefsIndexUsesContainerdWords is internally consistent (it would fail if only one of the two surfaces were cut over — a genuine guardrail for an incomplete rename). ✔
  • Corpus ordering is maintained (containerd before crabbox; nerdctl removed between mount and oci). ✔
  • No secrets, no injection surface, no privilege change; dependency-hash and path edits only. ✔
  • No dead code, no duplicated pin: the version lives once in go.mod, the word once in charly.yml (unchanged), and the index is generated from the corpus. ✔

Tests

The bump's behavioural proof is the pasted live differential (same fixture, before vs. new binary, check fails on before). That is self-reported and cannot be re-executed here, but it is the class-appropriate proof for a compiled-in module bump, and the guardrail test in the diff is updated consistently. The cutover itself is additionally covered by the existing TestPluginsRefsIndexSound (unchanged, network-dependent). No in-repo unit test asserts the new "skipped" bucket — acceptable for a pure dependency bump of this repo's style, but worth noting as residual risk for future regressions.

Body honesty

File count, ±counts, version strings and hashes are all reproduced by the diff. Two cosmetic notes, neither affecting the code:

  1. The R5 grep output cites charly/go.sum lines 264–265 (old 2153) and 262–263 (new 1542); a strict read of the hunk places them one line lower. This is a line-number transcription artifact, not a substantive contradiction — the content (versions/hashes) matches the diff exactly.
  2. The extensive gate/differential transcripts are self-reported; I re-derived everything I can from the diff and it holds.

Comments

There are no comments (<comment_thread> empty). No sign-offs to validate, and no designated maintainers were provided, so the change is judged on its own evidence — which it carries.

Disposition

The diff is complete, coherent, and faithful to the body's substantive claims. The only issues are cosmetic line-number drift in pasted evidence and the absence of an in-repo test for the module's skipped-bucket behaviour — neither is a correctness, security, or scope defect in the change itself.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — the review's two notes are answered with measurements; the diff stands as it is, and no follow-up commit is needed.

Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

Note 1 — the go.sum line numbers: the body's numbers are the measured ones

The review says the body's R5 sweep cites go.sum lines "264–265 (old 2153)" and "262–263 (new 1542)", and that "a strict read of the hunk places them one line lower". The body cites the output of the command it shows; here is that command, run on this head, verbatim:

$ git show origin/main:charly/go.sum | grep -n "plugin-task/candy/plugin-task"
262:github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.1911 h1:JUDd7mSE4p28Vip8alskNmU3yb1r0/tsnN6cSd32c7U=
263:github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.1911/go.mod h1:PZ2K4/S3bIRJWjgiLEmyxWWQnPnKGYIMUrl4vpUXftc=
264:github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.2153 h1:p/YKcCgnHZuQkf9axRdZJbenbQ0XWcv4pFyqselThnk=
265:github.com/opencharly/plugin-task/candy/plugin-task v0.2026296.2153/go.mod h1:PZ2K4/S3bIRJWjgiLEmyxWWQnPnKGYIMUrl4vpUXftc=

$ grep -n "plugin-task/candy/plugin-task" charly/go.sum
262:github.com/opencharly/plugin-task/candy/plugin-task v0.2026297.1542 h1:Av9Ibh+iYdIe0jCnRZqZFg91GgeJx0ZYBn28AK2k7Hg=
263:github.com/opencharly/plugin-task/candy/plugin-task v0.2026297.1542/go.mod h1:PZ2K4/S3bIRJWjgiLEmyxWWQnPnKGYIMUrl4vpUXftc=

The offset in the note comes from counting the hunk's section label as a diff row. git diff prints @@ -259,10 +259,8 @@ <text>, where the text after the closing @@ is git's section heading — the last context line of the preceding region, repeated — and it is not a row of the hunk. Counted correctly, the hunk has three leading context rows (old 259–261), so the removed v0.2026296.1911 pair is old 262–263, the removed v0.2026296.2153 pair is old 264–265, and the added v0.2026297.1542 pair is new 262–263 — which is what the body pastes, and what the two commands above reproduce on this head. The row count closes exactly: old 3 + 4 + 3 = 10 and new 3 + 2 + 3 = 8, matching the header.

So the two numbers in the body are correct as pasted, and the note's inference from the hunk header is the thing that is off by one. Nothing in the body needs changing.

Note 2 — "no in-repo unit test asserts the new skipped bucket": the behaviour is tested, by the module that owns it

The skipped bucket is implemented and tested in plugin-task, and the pinned version carries that test with it. From the module cache of the exact version this PR pins (v0.2026297.1542):

$ ls $(go env GOMODCACHE)/github.com/opencharly/plugin-task/candy/plugin-task@v0.2026297.1542/skipped_test.go
$ grep -n "^func Test" .../skipped_test.go
48:func TestRunTask_SkippedStepIsCountedAndNotFailed(t *testing.T) {
78:func TestPrintResultText_HeadlineNamesTheSkippedCount(t *testing.T) {
97:func TestPrintResultText_SilentStillDisclosesTheSkippedStep(t *testing.T) {
126:func TestHopTasks_SkippedCountSurvivesTheHop(t *testing.T) {
147:func TestPrintResultsJSON_CarriesTheSkippedCount(t *testing.T) {
176:func TestRunTaskCLI_SkippedStepExitsZero(t *testing.T) {

Those cover each surface the bump changes: the third bucket is counted and not counted as failed; the headline names the skipped count; the silent headline still discloses it; the hop aggregate carries it; the JSON report carries it; and the CLI still exits zero.

charly is the host of that module, not its owner. The behaviour lives in the plugin, and its test lives with it — re-asserting a vendored module's message shape inside charly's test suite would duplicate the owning module's coverage in the wrong repo (R2, and the architecture placement rule in charly/AGENTS.md). What charly owns here, this PR cuts over: the containerd ref, guarded by charly/engine_word_cutover_test.go — a test that fails if only one of the two surfaces moved. The repo-level proof that the compiled-in module really ships the new behaviour is the live differential in the body: the same fixture against two binaries built from two trees, where the new binary prints task skipped-bucket: 2 step(s), 0 failed, 1 skipped and the old one prints task skipped-bucket: 2 step(s), 0 failed and exits non-zero. That is the discriminating proof for this class, and it is in the body.

Nothing else

The verdict is PASS, the diff is five files, and no finding here requires a change to the tree or to the body. The remaining coordination — the charly consumer leg's landing, the issue's closure, and the umbrella's own gitlink leg that follows it — continues on its own thread.

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

Labels

None yet

Projects

None yet

1 participant