Repository navigation
fix(candywalk): read a manifest's DECLARED refs, not the refs in its block scalars - #467
Conversation
…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)
Review — PASSWhat the diff does
Correctness against the body's claims
Scope and cleanlinessThe 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. CommentsThere 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)
Neither point undermines the fix, the tests, or the honesty of the body. Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
…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>
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)
…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>
…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>
Summary
sdk/candywalk.collectRefsregex-scanned a fetched repo's root manifest as raw text, so a ref amanifest merely CONTAINS was collected as though it were DECLARED.
plugin-box's R10 bedbox-reconcile-closure-reports-unfetchablewrites@github.com/opencharly/this-repo-does-not-exist-zzz/candy/nope:v1.2.3into a scratch project atruntime, on purpose, to prove
charly box reconcile --closurereports an unfetchable ref insteadof 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 manifesttemplate, or prose.
The change, per file:
candywalk/candywalk.go—collectRefsnow parses the file and reads refs off its scalarnodes, skipping
LiteralStyle/FoldedStyleentirely. 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
charlyform, drop prose notation — moved into oneacceptedRefhome shared by both readers, so no policy was duplicated (R2).manifestRoot+collectRefsNodeare the new parser reader; the package doc's claim that this isa 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 isNOT 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 — apredecessor 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:
exactly one such token —
plugin-quickshelldocumenting its own pin — and that repo is a DIRECT pinalready, so nothing is lost;
eval-omarchy's generated-manifesttemplate:(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 pinsin the marketplace refs list at the same tags (their newest), which is what
opencharly/plugin-marketplace#41asks for anyway: a skill reached only through the closurepublishes 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'srepo, at the tag its own merge minted) and the pristine
tools/marketplace-cli/go.mod(sdkv0.2026289.157from the proxy):GREEN — the same refs list, the same generator, this head's
candywalktools/marketplace-cli/go.modcarries a localreplace github.com/opencharly/sdk => <this branch's worktree>; the shim is rebuilt from it (go mod tidy && go build ./...). The consumer-side bump thatcarries this fix into the real pin is the producer-first follow-up (sdk tag, then the
plugin-marketplacerequire, then the marketplace shim's require).So plugin-box WAS fetched (
line 220) and its root manifest WAS read as a candy source; its bed'sfixture simply no longer reaches the resolver. The 361 is that refs list's own corpus (the baseline
360 plus the skill
plugin-boxowns), 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:
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)
The whole module's suite passes (19 packages
ok, zero failures); the new arms and the sibling teststhat pin the earlier
#322/#408/#410/#412behaviour pass together, so the fix does notwalk back a precedent.
The
ci.ymlcross-compile matrix, run from this head:The one gate that could not run, and its control
golangci-lintv2.13.2 — theci.ymlpin — cannot typecheck ANY tree on this host: itsgo/typesunderstands 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#466with the evidence and the fix. The tier below reflectsthat honestly rather than claiming a gate that did not run.
Rulebook compliance
/charly-internals/go,/charly-internals/git-workflow,/charly-internals/plugin,/charly-internals/marketplaceand this repo'sAGENTS.mdbefore the first edit; every commandhere was run after that.
opencharly/sdk#455: mechanism = raw-textscan, missed control = no parser/unit arm on block scalars, blast radius = every corpus consumer,
root fix = this) and the local
golangci-lintskew (RCA + fix filed asopencharly/sdk#466,reproduced on
origin/main). Nothing is parked as "pre-existing" without an issue number.acceptedRefis the single home for the in-repo-charly skip and theprose-notation drop, shared by the parser reader and the text fallback. No second implementation.
no ref string was split to dodge the scanner.
after the change and was corrected in the same commit, beside the code that decides it.
shimmed; the new tests name both so a re-introduction fails.
git status --shortwas read before every staging step; no force-push, no--no-verify,no direct
mainpush, no foreign file touched. One commit, one push.TestCollectRefs_IgnoresRefsInsideBlockScalarsasserts exactly the fixture ref the old scanner returned, and the RED/GREEN pair above exercises
the changed path live through the real generator.
N/A— pure filesystem discovery; no networkservice is mocked or skipped).
used a worktree-local
charlybuilt fromorigin/main(2026.296.2116), not a shared install.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(thedshCLI,@deepseek-ai/dsh), providerollama-cloud, model iddeepseek-v4.1-flash.opencharly/opencharly#538records why that pair is correct and why avalidator's model allowlist must accept it rather than push an agent to edit a true attribution line.
Change classification
github.com/opencharly/sdk, packagecandywalk) —a mechanism library consumed by both out-of-process generators (
plugin-marketplace,plugin-docs) and bydeploykit's collectors. NOT documentation-only: two.gofiles changed.analysed on a live system— the changed path ran live with retained output (RED/GREEN/negative arm above); every local Go gate except
golangci-lintpassed, and that one cannot run onthis host at any tree (
opencharly/sdk#466).plugin-marketplacerequire bump →marketplace'stools/marketplace-clirequire bump → the refs cutover that needs the fix.Filed issues
opencharly/sdk#466—golangci-lintv2.13.2 cannot decode the Go 1.27.3 stdlib export data; thelocal lint gate is red on
maintoo. Non-blocking for this diff (the findings name a file thisbranch 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)