Skip to content

fix(candywalk): read a manifest's DECLARED refs, not the refs in its block scalars - #467

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/candywalk-declared-refs-only
Oct 23, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/candywalk-declared-refs-only

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

sdk/candywalk.collectRefs regex-scanned a fetched repo's root manifest as raw text, so a ref a
manifest merely CONTAINS was collected as though it were DECLARED. plugin-box's R10 bed
box-reconcile-closure-reports-unfetchable writes
@github.com/opencharly/this-repo-does-not-exist-zzz/candy/nope:v1.2.3 into a scratch project at
runtime, on purpose, to prove charly box reconcile --closure reports an unfetchable ref instead
of swallowing it. The scanner collected the bed's own fixture and the corpus generator died resolving
it, rc=1, for every consumer of the corpus (opencharly/sdk#455).

I picked candidate 1 — exclude block scalars — and the instrument is the YAML parser, not a
narrower regex.
Candidate 2 ("only scan a root manifest when the ref named the ROOT") only narrows
when the raw-text scan runs; it leaves the defective instrument in place, so a root-pinned repo with
an embedded fixture still kills the walk. Parsing fixes the class: a ref in a scalar is a declaration,
a ref inside a | / > body is data, whether that body is a shell script, a generated manifest
template, or prose.

The change, per file:

  • candywalk/candywalk.go — collectRefs now parses the file and reads refs off its scalar
    nodes, skipping LiteralStyle/FoldedStyle entirely. A file that does not parse as a manifest
    (prose, a CHANGELOG) keeps the text scan, because there the text IS the declaration surface. The
    ref policy — skip the pre-cutover in-repo charly form, drop prose notation — moved into one
    acceptedRef home shared by both readers, so no policy was duplicated (R2).
    manifestRoot + collectRefsNode are the new parser reader; the package doc's claim that this is
    a raw-byte scan was corrected in the same edit (R4/R5).
  • candywalk/candywalk_refs_declared_test.go (new) — three arms: the fixture-shaped bed's ref is
    NOT collected while the ref declared beside it IS; the SAME ref string in a DECLARED position IS
    collected (the negative arm — the scanner must not blind itself); and a versioned ref in a COMMENT
    is no longer fetched as though declared.

The fixture is untouched. plugin-box's bed and its realistic ref stay exactly as they are — a
predecessor refused to rewrite it, because that "hides the defect and breaks on the next repo with an
embedded fixture" (opencharly/sdk#455, "What I did NOT do").

Two consequences of parsing, both measured rather than assumed:

  • a versioned ref in a COMMENT is no longer collected (a comment declares nothing). Org-wide there is
    exactly one such token — plugin-quickshell documenting its own pin — and that repo is a DIRECT pin
    already, so nothing is lost;
  • four refs org-wide live ONLY inside block scalars: eval-omarchy's generated-manifest template:
    (plugin-record / plugin-spice / plugin-vision) and plugin-migrate's bed (plugin-pipeline).
    plugin-vision owns no skill and plugin-pipeline is already a DIRECT pin, so the corpus cost is
    record + spice — which arrive today only through that template. They are repaid by DIRECT pins
    in the marketplace refs list at the same tags (their newest), which is what
    opencharly/plugin-marketplace#41 asks for anyway: a skill reached only through the closure
    publishes whatever tag a sibling happens to name.

How tested

Change class: Go source in a shared kit (no runtime target, no image). The repo's gate is
ci.yml's Go gate; the changed path is additionally executed LIVE, through the real corpus generator,
because the defect only exists at that boundary.

The control that fails — RED, generator without the fix

A refs list carrying @github.com/opencharly/plugin-box/candy/plugin-box:v2026.295.2027 (the issue's
repo, at the tag its own merge minted) and the pristine tools/marketplace-cli/go.mod (sdk
v0.2026289.157 from the proxy):

$ cd /home/atrawog/Sync/Atrapub/mo/dsh/opencharly/.worktrees/refs-cutover/marketplace
$ PATH=/home/atrawog/Sync/Atrapub/mo/dsh/opencharly/.worktrees/refs-cutover/charly/bin:$PATH charly marketplace generate --root . --out /tmp/cs/red > /tmp/cs/red.log 2>&1; echo "rc=$?"
rc=1
$ tail -4 /tmp/cs/red.log
Downloading github.com/opencharly/layer-charly-internals:v2026.296.1907...
charly marketplace: collect entities: resolve @github.com/opencharly/this-repo-does-not-exist-zzz/candy/nope:v1.2.3: resolving github.com/opencharly/this-repo-does-not-exist-zzz:v1.2.3: git ls-remote https://github.com/opencharly/this-repo-does-not-exist-zzz.git v1.2.3: exit status 128
remote: Repository not found.
fatal: repository 'https://github.com/opencharly/this-repo-does-not-exist-zzz.git/' not found

GREEN — the same refs list, the same generator, this head's candywalk

tools/marketplace-cli/go.mod carries a local replace github.com/opencharly/sdk => <this branch's worktree>; the shim is rebuilt from it (go mod tidy && go build ./...). The consumer-side bump that
carries this fix into the real pin is the producer-first follow-up (sdk tag, then the
plugin-marketplace require, then the marketplace shim's require).

$ cd /home/atrawog/Sync/Atrapub/mo/dsh/opencharly/.worktrees/refs-cutover/marketplace
$ PATH=/home/atrawog/Sync/Atrapub/mo/dsh/opencharly/.worktrees/refs-cutover/charly/bin:$PATH GOTMPDIR=$HOME/.cache/go-tmp charly marketplace generate --root . --out $HOME/.cache/refs-cutover/green > $HOME/.cache/refs-cutover/green.log 2>&1; echo "rc=$?"
rc=0
$ find $HOME/.cache/refs-cutover/green -name SKILL.md | wc -l
361
$ grep -n 'resolving github.com/opencharly/plugin-box' $HOME/.cache/refs-cutover/green.log
220:charly: resolving github.com/opencharly/plugin-box:v2026.295.2027 (218 resolved, 1157 queued)
$ grep -c 'this-repo-does-not-exist' $HOME/.cache/refs-cutover/green.log
0

So plugin-box WAS fetched (line 220) and its root manifest WAS read as a candy source; its bed's
fixture simply no longer reaches the resolver. The 361 is that refs list's own corpus (the baseline
360 plus the skill plugin-box owns), not a claim about the refs cutover.

The negative arm — the scanner is not blinded

A scratch project whose ONLY ref is a DECLARED, genuinely unresolvable ref (a candy: list entry,
i.e. a root ref), generated by the SAME fixed generator:

$ cd $HOME/.cache/refs-cutover/neg
$ cat candy/charly-marketplace/charly.yml
charly-marketplace:
    candy:
        description: negative arm — a DECLARED root ref that genuinely cannot resolve
        candy:
            - '@github.com/opencharly/this-repo-does-not-exist-zzz/candy/nope:v1.2.3'
$ PATH=/home/atrawog/Sync/Atrapub/mo/dsh/opencharly/.worktrees/refs-cutover/charly/bin:$PATH GOTMPDIR=$HOME/.cache/go-tmp charly marketplace generate --root . --out $HOME/.cache/refs-cutover/neg-out > $HOME/.cache/refs-cutover/neg.log 2>&1; echo "rc=$?"
rc=1
$ tail -3 $HOME/.cache/refs-cutover/neg.log
charly marketplace: collect entities: resolve @github.com/opencharly/this-repo-does-not-exist-zzz/candy/nope:v1.2.3: resolving github.com/opencharly/this-repo-does-not-exist-zzz:v1.2.3: git ls-remote https://github.com/opencharly/this-repo-does-not-exist-zzz.git v1.2.3: exit status 128
remote: Repository not found.
fatal: repository 'https://github.com/opencharly/this-repo-does-not-exist-zzz.git/' not found

The same string is ignored inside a block scalar and caught when declared — that is the whole
property, and it is why the fix cannot be "stop collecting refs from this file".

Go gates on this head (2b59aad)

$ cd /home/atrawog/Sync/Atrapub/mo/dsh/opencharly/.worktrees/refs-cutover/sdk
$ git rev-parse HEAD
2b59aad74b921e945aa9b5540b9218bee07c0788
$ git diff --stat origin/main...HEAD
 candywalk/candywalk.go                    |  99 +++++++++++++++++++++-----
 candywalk/candywalk_refs_declared_test.go | 113 ++++++++++++++++++++++++++++++
 2 files changed, 196 insertions(+), 16 deletions(-)
$ test -z "$(gofmt -l .)" && echo 'gofmt: clean'
gofmt: clean
$ go vet ./... && echo 'vet: clean'
vet: clean
$ go test ./candywalk/ -v -run 'TestCollectRefs|TestAcceptableVersion'
=== RUN   TestAcceptableVersion_RejectsEveryProseFormAndKeepsEveryRealOne
--- PASS: TestAcceptableVersion_RejectsEveryProseFormAndKeepsEveryRealOne (0.00s)
=== RUN   TestCollectRefs_DropsTheProseFormsAndKeepsTheRealRefsBesideThem
--- PASS: TestCollectRefs_DropsTheProseFormsAndKeepsTheRealRefsBesideThem (0.00s)
=== RUN   TestCollectRefs_IgnoresRefsInsideBlockScalars
--- PASS: TestCollectRefs_IgnoresRefsInsideBlockScalars (0.00s)
=== RUN   TestCollectRefs_StillCollectsAGenuinelyMalformedRef
--- PASS: TestCollectRefs_StillCollectsAGenuinelyMalformedRef (0.00s)
=== RUN   TestCollectRefs_ReadsDeclarationsNotComments
--- PASS: TestCollectRefs_ReadsDeclarationsNotComments (0.00s)
=== RUN   TestCollectRefs_IgnoresProseTemplates
--- PASS: TestCollectRefs_IgnoresProseTemplates (0.00s)
=== RUN   TestCollectRefs_AcceptsEveryRefForm
--- PASS: TestCollectRefs_AcceptsEveryRefForm (0.00s)
=== RUN   TestCollectRefs_IgnoresATruncatedPlaceholderVersion
--- PASS: TestCollectRefs_IgnoresATruncatedPlaceholderVersion (0.00s)
PASS
ok  	github.com/opencharly/sdk/candywalk	0.029s
$ go test ./...
ok  	github.com/opencharly/sdk/vmshared	(cached)
ok  	github.com/opencharly/sdk/workflowkit	(cached)

The whole module's suite passes (19 packages ok, zero failures); the new arms and the sibling tests
that pin the earlier #322 / #408 / #410 / #412 behaviour pass together, so the fix does not
walk back a precedent.

The ci.yml cross-compile matrix, run from this head:

$ for a in amd64 arm64 arm 386; do GOOS=linux GOARCH=$a CGO_ENABLED=0 go build ./... || echo "FAIL $a"; done
$ echo "all four arches: rc=0, no output"
all four arches: rc=0, no output

The one gate that could not run, and its control

golangci-lint v2.13.2 — the ci.yml pin — cannot typecheck ANY tree on this host: its go/types
understands export data version 4 and the Go 1.27.3 stdlib writes version 5. It fails identically on
origin/main, on four findings in a file this branch does not touch, so the skew is the toolchain's,
not this diff's. Filed as opencharly/sdk#466 with the evidence and the fix. The tier below reflects
that honestly rather than claiming a gate that did not run.

Rulebook compliance

  • R0 — loaded /charly-internals/go, /charly-internals/git-workflow, /charly-internals/plugin,
    /charly-internals/marketplace and this repo's AGENTS.md before the first edit; every command
    here was run after that.
  • R1 — two anomalies met: the defect itself (RCA in opencharly/sdk#455: mechanism = raw-text
    scan, missed control = no parser/unit arm on block scalars, blast radius = every corpus consumer,
    root fix = this) and the local golangci-lint skew (RCA + fix filed as opencharly/sdk#466,
    reproduced on origin/main). Nothing is parked as "pre-existing" without an issue number.
  • R2 — one ref policy: acceptedRef is the single home for the in-repo-charly skip and the
    prose-notation drop, shared by the parser reader and the text fallback. No second implementation.
  • R3 — no sleep, retry, suppression or fallback branch added; the fixture was not rewritten and
    no ref string was split to dodge the scanner.
  • R4 — the package doc's own claim that this scans "RAW file bytes, comments included" is false
    after the change and was corrected in the same commit, beside the code that decides it.
  • R5 — the retired behaviour (comment refs fetched; block-scalar refs fetched) is deleted, not
    shimmed; the new tests name both so a re-introduction fails.
  • R6 — git status --short was read before every staging step; no force-push, no --no-verify,
    no direct main push, no foreign file touched. One commit, one push.
  • R7 — the coverage fails without the change: TestCollectRefs_IgnoresRefsInsideBlockScalars
    asserts exactly the fixture ref the old scanner returned, and the RED/GREEN pair above exercises
    the changed path live through the real generator.
  • R8 — no live service boundary is involved (N/A — pure filesystem discovery; no network
    service is mocked or skipped).
  • R9 — no binary is deployed by this repo and no runtime OS dependency was added; the live runs
    used a worktree-local charly built from origin/main (2026.296.2116), not a shared install.
  • R10 — change class is Go source in a shared kit: the repo's Go gate ran on this head (above)
    plus the live generator run on the changed path; the one unavailable gate is filed, not waved
    through.

Attribution (A3). The runtime names are the session's, not an inference: the delivered system
message reads "You are a coding agent powered by the deepseek-v4.1-flash model." — harness
DeepSeek Harness (the dsh CLI, @deepseek-ai/dsh), provider ollama-cloud, model id
deepseek-v4.1-flash. opencharly/opencharly#538 records why that pair is correct and why a
validator's model allowlist must accept it rather than push an agent to edit a true attribution line.

Change classification

  • Class: Go source change in a shared kit (github.com/opencharly/sdk, package candywalk) —
    a mechanism library consumed by both out-of-process generators (plugin-marketplace,
    plugin-docs) and by deploykit's collectors. NOT documentation-only: two .go files changed.
  • Tier: analysed on a live system — the changed path ran live with retained output (RED/GREEN/
    negative arm above); every local Go gate except golangci-lint passed, and that one cannot run on
    this host at any tree (opencharly/sdk#466).
  • Consumers to follow (producer-first): sdk tag → plugin-marketplace require bump →
    marketplace's tools/marketplace-cli require bump → the refs cutover that needs the fix.

Filed issues

  • opencharly/sdk#466 — golangci-lint v2.13.2 cannot decode the Go 1.27.3 stdlib export data; the
    local lint gate is red on main too. Non-blocking for this diff (the findings name a file this
    branch does not touch), separable, and it has the RCA plus the fix.

Agent: charly

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (analysed on a live system)

…block scalars

collectRefs regex-scanned a manifest's raw bytes, so a `command:` body that
writes a fixture at RUNTIME was collected as though it were a declaration.
plugin-box's R10 bed box-reconcile-closure-reports-unfetchable writes
@github.com/opencharly/this-repo-does-not-exist-zzz/candy/nope:v1.2.3 into a
scratch project to prove `charly box reconcile --closure` REPORTS an
unfetchable ref instead of swallowing it, and the corpus generator then died
resolving the bed's own fixture (rc=1), taking every consumer of the corpus
with it. The bed is legitimate and its ref stays.

The instrument is now the YAML parser: refs are read off the file's scalar
nodes, and a literal or folded scalar is DATA and is not read at all. A file
that does not parse as a manifest (prose, a changelog) keeps the text scan,
because there the text IS the declaration surface. The ref policy (in-repo
charly form skipped, prose notation dropped) is unchanged and now has one
home, shared by both readers.

Retired with the same change: a versioned ref written in a COMMENT was
collected and fetched as though it were declared. A comment declares nothing.

Refs #455.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (analysed on a live system)
@github-actions

Copy link
Copy Markdown

Review — PASS

What the diff does

collectRefs now attempts yaml.Unmarshal on the file; when the result is a single-document root mapping it walks the node tree (collectRefsNode) and reads refs off scalar nodes only, skipping LiteralStyle/FoldedStyle bodies entirely. When the file is not a mapping-shaped document (prose, changelog, unparseable) it falls back to the previous raw-text scan. The ref policy (skip the pre-cutover in-repo charly form, drop prose-notation versions) is hoisted into one acceptedRef helper used by both readers, so no policy is duplicated.

Correctness against the body's claims

  • The stated mechanism matches the code: block scalars (|, >) are excluded, declarations are read, comments are not nodes so comment refs disappear as claimed.
  • The body's stat block (2 files, +196/−16; candywalk.go 99 changed lines, test file 113) reproduces exactly from the diff hunks.
  • The three new tests correspond to the three arms described: (1) block-scalar fixture ignored while the declared ref beside it survives — this arm fails against the old raw scan, since the old regex would return the fixture ref; (2) the same unfetchable string collected when genuinely declared — a guard, correctly labelled "negative arm" rather than a regression test; (3) a comment-only ref dropped while a declared one is kept — also fails without the change, since the old scan returned the commented token. So R7's claim ("coverage fails without the change") holds for two of the three arms and is honestly characterised for the third.
  • Ordering claim ("same order the old byte scan produced") is consistent with node traversal: mapping keys precede values and sequence entries precede successors in document order.
  • The live RED/GREEN/negative-arm transcripts are internally consistent (RED names the fixture ref and rc=1; GREEN shows plugin-box fetched at line 220 with zero occurrences of the fixture token; negative arm still rc=1), and they exercise the exact boundary the defect lives on. The golangci-lint gap is disclosed with an issue number rather than waved through — acceptable, and it does not affect this diff's validity.

Scope and cleanliness

The change is coherent: one reader swap, one shared policy helper, one new test file, and the stale "RAW file bytes" doc claim corrected in the same hunk. No shim or dual path for the retired behaviour; no fixture rewrite; no sleeps/retries. No secrets, no new dependencies, no injection surface — this is filesystem discovery over local manifests.

Comments

There are no comments in the thread, so there is nothing to re-derive and no maintainer sign-off to weigh; the change stands on its own evidence, which it does.

Non-blocking observations (do not gate)

  • manifestRoot's doc comment says a "multi-document bed" makes it return nil so the caller falls back to the text scan. With yaml.v3, Unmarshal decodes only the first document and does not error on trailing ones, so a multi-document file would be parsed from document 1 and later documents would not be walked at all — refs declared there would be silently dropped rather than text-scanned. The author's org-wide measurement (596 manifests, one added skill in the corpus) shows no such loss today, so this is a documentation/edge-case inaccuracy, not a demonstrated defect. Worth a follow-up test if multi-doc manifests ever appear.
  • The parser treats any non-block scalar (including a plain description: value) as a declaration surface. That is the model the body states, so it is consistent, but it means "declared" is broader than require:/candy: entries.

Neither point undermines the fix, the tests, or the honesty of the body.

Verdict: PASS


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

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 23, 2026 21:52
@github-actions
github-actions Bot merged commit 93849f6 into main Oct 23, 2026
8 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 23, 2026
github-actions Bot pushed a commit to opencharly/plugin-marketplace that referenced this pull request Oct 23, 2026
…release (#47)

opencharly/sdk#467 (tag v0.2026296.2152) makes candywalk.collectRefs read a
manifest's DECLARED refs off its scalar nodes and skip block scalars, so a
runtime fixture embedded in a `command:` body no longer kills the corpus walk
(opencharly/sdk#455, the plugin-box bed box-reconcile-closure-reports-unfetchable).

Producer-first consumer bump: without it this plugin builds the generator against
a candywalk that still scans raw bytes, and the marketplace refs cutover cannot
generate its corpus at all. No source change — the plugin already calls
candywalk.CollectEntitiesRemote, and the fix is inside that call.

Gates on this head: go build ./..., go test ./... and `charly box validate`
(OK, 0 warnings, 0 errors) with the worktree-local charly. golangci-lint cannot
typecheck any module on this host (export-data v5 vs v4) — filed as sdk#466,
red on origin/main too.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (fully tested and validated)

Co-authored-by: Andreas Trawoeger <atrawog@gmail.com>
aitrawog pushed a commit to opencharly/marketplace that referenced this pull request Oct 23, 2026
The refs list is the ONE generation input, and it still named 17 repos that are
retired and archived. Each published a second copy of skills that live beside the
code, plugin or config they describe, and each was pinned at its DRAIN tag — where
the carrier no longer declares the skill at all — so the corpus had been publishing
those skills from OLD closure twins (a pre-drain carrier tag reached through a
distro repo's own closure list). That is the defect
opencharly/plugin-marketplace#41 names: a skill reached only through the closure
publishes whatever tag a sibling happens to name.

DROP 17: pod-charly-core, vm-charly-vm, layer-charly-{hermes,jupyter,kubernetes,
image,immich,infrastructure,local,selkies,coder,comfyui,distros,filebrowser,
languages,openwebui,pod}.

ADD 23 direct refs: the 21 destinations the list never named (plugin-doctor,
plugin-alias, plugin-enc, plugin-status, plugin-clean, plugin-tmux, plugin-udev,
plugin-cmd, plugin-fleet, plugin-ssh, plugin-deploy-pod, plugin-vm, plugin-helm,
plugin-deploy-local, pod-hermes-full, layer-playwright, layer-kubernetes, pod-dbus,
pod-selkies-desktop, layer-tmux, layer-wl-overlay), each at its own newest tag,
plus plugin-record and plugin-spice — whose skills were reachable ONLY through a
manifest template inside eval-omarchy's closure, an accidental path that
opencharly/sdk#467 (tag v0.2026296.2152) closes by reading DECLARED scalars
instead of block scalars.

The umbrella is NOT pinned here. It is the right home for the three
general-guidance skills (agents, cutover-policy, root-cause-analyzer), but
spec/refs.DownloadRepo populates EVERY submodule of a fetched repo and fails the
whole fetch when one cannot be reached, which a 412-submodule umbrella cannot pass
on a cold runner (opencharly/spec#279). That is a producer leg of its own; adding
the pin before it is fixed would leave this repo's gate red for a reason unrelated
to the refs, so the umbrella leg follows that fix.

Nine skill bodies move with this change, all in ONE direction: from a stale
pre-drain closure twin to the repo that owns the skill, at its newest tag. The old
copies carried prose the sources have since corrected (charly fleet add ->
charly deploy add, FleetNode -> DeployNode, makepkg leftovers -> build-candy
leftovers, a --cache/--scopes surface the old copy predates, a rootful-builder
section, and the validator markers those bodies now use).

Refs opencharly/opencharly#600.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (fully tested and validated)
github-actions Bot pushed a commit to opencharly/marketplace that referenced this pull request Oct 23, 2026
…566)

* feat(refs): retire the 17 carriers, pin their destinations DIRECT, and make the umbrella a corpus source

The refs list is the ONE generation input, and it still named 17 repos that are
retired and archived. Each published a second copy of skills that live beside the
code, plugin or config they describe, and each was pinned at its DRAIN tag — where
the carrier no longer declares the skill at all — so the corpus had been
publishing those skills from OLD closure twins instead (a pre-drain carrier tag
reached through a distro repo's own closure list). That is the defect
opencharly/plugin-marketplace#41 names: a skill reached only through the closure
publishes whatever tag a sibling happens to name.

DROP 17: pod-charly-core, vm-charly-vm, layer-charly-{hermes,jupyter,kubernetes,
image,immich,infrastructure,local,selkies,coder,comfyui,distros,filebrowser,
languages,openwebui,pod}.

ADD 21 direct destination refs the list never named — plugin-doctor, plugin-alias,
plugin-enc, plugin-status, plugin-clean, plugin-tmux, plugin-udev, plugin-cmd,
plugin-fleet, plugin-ssh, plugin-deploy-pod, plugin-vm, plugin-helm,
plugin-deploy-local, pod-hermes-full, layer-playwright, layer-kubernetes, pod-dbus,
pod-selkies-desktop, layer-tmux, layer-wl-overlay — each at its own newest tag.
(plugin-pod, plugin-agent and plugin-kube were already named and were already
advanced by the refresh wave.)

ADD the umbrella as a DIRECT corpus source: `agents`, `cutover-policy` and
`root-cause-analyzer` are general guidance with no code, plugin or config behind
them, and their bodies live at the umbrella's root `.agents/skills/`, owned by its
`opencharly-guidance` candy (AGENTS.md Part II #5). This is what lets
layer-charly-internals stop publishing its own copies of those three.

ADD plugin-record and plugin-spice directly: their skills were reachable ONLY
through a manifest template inside eval-omarchy's closure (`template: |-`). The
corpus ref collector reads DECLARED scalars now, not block scalars
(opencharly/sdk#467, tag v0.2026296.2152), so the accidental path is gone and the
two become what they should always have been — a direct pin at the tag each repo's
own merge minted. Same tags, so no body moves on their account.

The generator that emitted the corpus is the release carrying that fix
(tools/marketplace-cli's require now pins plugin-marketplace v0.2026296.2215,
whose sdk is v0.2026296.2152). The corpus regenerates byte-identically to the
locally-fixed run, and the whole tree is the generator's own output — the drift
gate is green by construction.

Nine skill bodies and one reference file move with it, all in ONE direction: from
a stale pre-drain closure twin to the repo that owns the skill, at its newest tag.
The stale copies carried prose the sources have since corrected (`charly fleet
add` → `charly deploy add`, `FleetNode` → `DeployNode`, a `makepkg leftovers`
sweep that is now `build-candy leftovers`, cross-references to an `openclaw-full`
metalayer that no longer carries them, a `charly-doctor` claim about
`engine.build` that plugin-doctor's current body does not make).

Refs opencharly/opencharly#600.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (fully tested and validated)

* ci(toolchain): build charly at v2026.296.2116 so the pinned generator's module can compile

The round-1 `build` job — the drift gate that regenerates the corpus — failed:

    go: go.mod requires go >= 1.26.8 (running go 1.26.4; GOTOOLCHAIN=local)

The generator this branch pins (plugin-marketplace v2026.296.2215 -> sdk
v0.2026296.2152) declares `go 1.26.8`, and the action's charly pin
(v2026.277.1528) declares `go 1.26.4`, so setup-go installed a toolchain below the
floor. charly v2026.296.2116 declares `go 1.26.8`, which satisfies both.

Root cause: the CI charly pin — not the toolchain env — decides the Go version
this repo's gate builds with, and it was 19 days behind the org's own floor. The
missed control: a module's `go` directive is a property of the pinned dependency,
so a refs/corpus change that advances that dependency must also check that the CI
toolchain satisfies it; nothing in the pre-push audit compared them.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (fully tested and validated)

* docs(refs): name BOTH the defect and its fix in the scanner citation

The block comment cited `opencharly/sdk#455` (the defect) while the PR body cites
`sdk#467` (the fix and its release). Both are true and one issue number cannot
carry both: a reader following the citation needs the bug report AND the release.

This is a comment-only line. The refs the generator consumes are the manifest's
SCALAR nodes (that is the fix this comment documents), so a comment cannot reach
the emitted corpus; the drift gate on the pushed head re-derives that from the
tree itself rather than from this sentence.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (fully tested and validated)

* fix(refs): retire the 17 carriers and pin their destinations DIRECT

The refs list is the ONE generation input, and it still named 17 repos that are
retired and archived. Each published a second copy of skills that live beside the
code, plugin or config they describe, and each was pinned at its DRAIN tag — where
the carrier no longer declares the skill at all — so the corpus had been publishing
those skills from OLD closure twins (a pre-drain carrier tag reached through a
distro repo's own closure list). That is the defect
opencharly/plugin-marketplace#41 names: a skill reached only through the closure
publishes whatever tag a sibling happens to name.

DROP 17: pod-charly-core, vm-charly-vm, layer-charly-{hermes,jupyter,kubernetes,
image,immich,infrastructure,local,selkies,coder,comfyui,distros,filebrowser,
languages,openwebui,pod}.

ADD 23 direct refs: the 21 destinations the list never named (plugin-doctor,
plugin-alias, plugin-enc, plugin-status, plugin-clean, plugin-tmux, plugin-udev,
plugin-cmd, plugin-fleet, plugin-ssh, plugin-deploy-pod, plugin-vm, plugin-helm,
plugin-deploy-local, pod-hermes-full, layer-playwright, layer-kubernetes, pod-dbus,
pod-selkies-desktop, layer-tmux, layer-wl-overlay), each at its own newest tag,
plus plugin-record and plugin-spice — whose skills were reachable ONLY through a
manifest template inside eval-omarchy's closure, an accidental path that
opencharly/sdk#467 (tag v0.2026296.2152) closes by reading DECLARED scalars
instead of block scalars.

The umbrella is NOT pinned here. It is the right home for the three
general-guidance skills (agents, cutover-policy, root-cause-analyzer), but
spec/refs.DownloadRepo populates EVERY submodule of a fetched repo and fails the
whole fetch when one cannot be reached, which a 412-submodule umbrella cannot pass
on a cold runner (opencharly/spec#279). That is a producer leg of its own; adding
the pin before it is fixed would leave this repo's gate red for a reason unrelated
to the refs, so the umbrella leg follows that fix.

Nine skill bodies move with this change, all in ONE direction: from a stale
pre-drain closure twin to the repo that owns the skill, at its newest tag. The old
copies carried prose the sources have since corrected (charly fleet add ->
charly deploy add, FleetNode -> DeployNode, makepkg leftovers -> build-candy
leftovers, a --cache/--scopes surface the old copy predates, a rootful-builder
section, and the validator markers those bodies now use).

Refs opencharly/opencharly#600.

Assisted-by: DeepSeek Harness ollama-cloud deepseek-v4.1-flash (fully tested and validated)

---------

Co-authored-by: Andreas Trawoeger <atrawog@gmail.com>
github-actions Bot pushed a commit to opencharly/plugin-marketplace that referenced this pull request Oct 24, 2026
…t reproduces (#42) (#52)

`charly task skills:drift` was red in four different ways at once, and no CI ran
it, so nothing could see it. This change makes the gate runnable, gives it a job
that owns the GENERATOR side of the corpus contract, and re-points the corpus pin
at the commit this generator actually reproduces.

THE RECIPE (root `charly.yml`, `skills:drift`)

- The parameter was `out` — lower-case. The task runner's expansion grammar
  matches UPPER-CASE names only (`sdk/kit/checkvars_expand.go:16-17`), so the
  value never reached the command and the shell dropped the empty word
  ("--out: expected string value but got EOL"). It is `CORPUS` now.
- `--root` named the repo root, but the corpus's ONE generation input — the refs
  list, `candy/charly-marketplace/charly.yml` — lives INSIDE the corpus checkout.
  Both flags now name the corpus checkout.
- The "the tree is committed" half ran `git status` in THIS repo, so it checked
  the plugin's tree, not the corpus's. It names the corpus now.
- The drift step carried no `timeout:`, and the per-step never-hang bound is
  1m30s. The drift resolves EVERY corpus source (the refs list names ~1200 repos
  at pinned versions) before it compares a single byte — measured 11m48s in the
  marketplace repo's own deploy workflow — so the step was killed at the ceiling
  and the gate reported a TIMEOUT where the truth was "slow, and green". The
  bound is a hang guard, not a budget.

THE SDK PIN (candy/plugin-marketplace/go.mod)

sdk v0.2026296.2152's `acceptedRef` refuses EVERY `github.com/opencharly/charly`
ref. The corpus's refs list RELIES on the BARE repo-root form
(`@github.com/opencharly/charly:v2026.297.0032`) being a corpus source. A
standalone build of the generator therefore dropped the charly corpus source, the
closure twin `layer-charly-internals` won, and a rule-renumbering difference
(R2/R3 -> R3/R4) left 9 stale artifacts. sdk v0.2026297.129 admits the bare
repo-root form and refuses only the SUBPATH form (opencharly/sdk#468 ->
opencharly/sdk#470), so the generator reproduces the corpus again. The
declared-refs fix PR #47 adopted (opencharly/sdk#467) is present in both
versions — the bump is a strict superset, not a revert.

THE CORPUS PIN (`var: MARKETPLACE_REF`, and the literal that duplicates it)

The current generator no longer reproduces the Oct-15 corpus commit: at
e802da68 the gate is red with 4 stale artifacts
(`plugins/.well-known/skills/index.json`, `plugins/DISPATCHER.md`,
`plugins/check/skills/openclaw/SKILL.md`,
`plugins/tools/skills/crabbox/SKILL.md`). At cc10955e — the corpus's current
`main` head, which is the invariant this repo declares — it is green with 0.

THE CI JOB (.github/workflows/drift-gate.yml)

The marketplace repo's deploy workflow owns the CORPUS side. Nothing owned the
GENERATOR side. The job runs the recipe on every merge to `main`, against the
corpus commit this repo DECLARES, read out of `candy/plugin-marketplace/charly.yml`
rather than repeated as a second literal (one pin, not two that can drift).

It runs on `main` and not on `pull_request`, and that is a design constraint. A
generator change that alters the output makes the declared corpus commit stale,
and the corpus can only be regenerated with a RELEASED generator — so a
pull-request gate would be red by construction for exactly the change it should
inform, and it would deadlock the chain that repairs it (generator merged and
tagged -> the corpus regenerates with that tag -> the pin is bumped here). It
would also be a de-facto required check: the org pr-validator's `arm` job arms
native auto-merge only when EVERY check on the head is green.

THE PARAM IS QUOTED AT ITS USE SITES

A task param reaches a command step as shell TEXT, not as a data channel
(opencharly/plugin-task#34 owns the class-level fix), so `${CORPUS}` is quoted
where it is used. A corpus checkout under a path with a space would otherwise
split into two words and the CLI would reject the arguments.

TWO DEFECTS FOUND AND FIXED IN PASSING

- `task marketplace:pin`'s second step compared the corpus's `main` head against
  the file's LAST 40-hex string — which is the sha256 prefix of the byte-identical
  SKILL.md the candy's file records, not a pin. The step could never be true
  (measured: it compared cc10955e against 7d4d2676). Each occurrence is now
  found by its ANCHOR (`MARKETPLACE_REF:` and `contains:`), which a third string
  cannot satisfy. The same anchoring is what the CI job reads the baseline with.
- The `setup` launcher and the `.claude/hooks` surface were removed at source in
  2026.241.1105, but the README, the box description, the candy's description,
  the plugin's package comments and the prune boundary still described them as
  shipped. They are gone from the prose (R5), with the generator's own test
  (`assertNotWritten(t, out, "setup")`) as the truth they now match.

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

Co-authored-by: Andreas Trawoeger <atrawog@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants