Skip to content

fix(drift): a runnable, CI-owned drift gate — and the corpus commit it reproduces (#42) - #52

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/skills-drift-gate-42
Oct 24, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
fix/skills-drift-gate-42

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

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

Four defects, one root: the recipe was written against an interface that does not
exist.

  1. 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.
  2. --root named the wrong tree. The generator's ONE generation input — the refs
    list, candy/charly-marketplace/charly.yml — lives INSIDE the corpus checkout, so
    --root must name it. Both flags now name the corpus checkout.
  3. The "the tree is committed" step ran git status in THIS repo, so it checked
    the plugin's tree, not the corpus's. It names the corpus now.
  4. The drift step carried no timeout:. The per-step never-hang bound is 1m30s, and
    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, v0.2026296.2152 → v0.2026297.129).
The old version's acceptedRef refuses EVERY github.com/opencharly/charly ref, and
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. The new version admits the bare repo-root form and refuses only the
SUBPATH form (opencharly/sdk#468 → opencharly/sdk#470). The declared-refs fix that
PR #47 adopted (opencharly/sdk#467) is present in both versions, so the bump is a
strict superset, not a revert.

The corpus pin (var: MARKETPLACE_REF and the literal that duplicates it by
design, e802da68 → cc10955e). The current generator no longer reproduces the Oct-15
corpus commit; cc10955e is the corpus's current main head, which is the invariant this
repository declares.

A CI job (.github/workflows/drift-gate.yml). The marketplace repo's deploy
workflow owns the CORPUS side of the contract; 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, not an
omission. 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.

Two more defects, found 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. 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, and the generator's own test
    (assertNotWritten(t, out, "setup")) is the truth they now match.

Files:

  • charly.yml — the skills:drift recipe (the parameter, both flags, the corpus's
    committed-state step, the timeout) and the marketplace:pin recipe (the anchored
    greps).
  • candy/plugin-marketplace/charly.yml — the MARKETPLACE_REF var, the literal that
    duplicates it, the candy's description, and the box description's stale "setup".
  • candy/plugin-marketplace/go.mod, go.sum — the SDK bump.
  • .github/workflows/drift-gate.yml — the new CI job.
  • candy/plugin-marketplace/command.go, emit_marketplace.go, generate.go,
    generate_test.go, plugin.go, prune.go — the --root help, the package
    comments and the prune boundary.
  • README.md — the commands section and the new "The drift gate" section.
  • .gitignore — the CI worktree directory.

Head 9364e0a976e9ee04de9dea715efbac9e7a4c92a7 — 13 files changed, 273 insertions(+), 67 deletions(-).

Change classification

plugin/candy — the corpus generator's task recipes, its dependency pin, and the CI
job that runs the gate. The generator's emission pipeline, the corpus it generates and
the marketplace-app box's shape are untouched.

How tested

Every command below was run on the tree this PR pushes, on charly v2026.296.2116 —
the same toolchain the marketplace repo's own build action pins, so the two halves of
the contract are compared like for like.

Block 1 — the gate, on the pushed tree.

$ charly task skills:drift --param CORPUS=/tmp/corpus-cc-git > /tmp/pm-42-final-gate.log 2>&1
$ echo "TASK_EXIT=$?" >> /tmp/pm-42-final-gate.log
$ cat /tmp/pm-42-final-gate.log
task skills:drift: 2 step(s), 0 failed
  [pass] check — regeneration is a no-op against the on-disk artifacts
      exit=0
  [pass] check — the corpus tree is committed -- the half `drift` cannot see
      exit=0
TASK_EXIT=0

Block 2 — the negative control: the gate CAN fail (R7). A gate that cannot fail is
not a gate. One byte in the emitter every generated markdown carries was mutated, the
same command was run, and the gate went red; the file was then restored and the tree
came back clean. The paste is the run's own log with the resolver's progress lines and
the runner's truncated stderr excerpt filtered out — the filter is the command shown.

$ sed -i 's/DO NOT EDIT\./DO NOT EDIT ./' candy/plugin-marketplace/emit.go
$ git diff --stat -- candy/plugin-marketplace/emit.go
 candy/plugin-marketplace/emit.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ charly task skills:drift --param CORPUS=/tmp/corpus-cc-git > /tmp/pm-42-negative.log 2>&1
$ echo "NEGATIVE_EXIT=$?" >> /tmp/pm-42-negative.log
$ sed 's/ (stderr:.*//' /tmp/pm-42-negative.log | grep -vE '^ +charly: (resolving|re)'
task skills:drift: 2 step(s), 1 failed
  [fail] check — regeneration is a no-op against the on-disk artifacts
      exit=1, want 0
  [pass] check — the corpus tree is committed -- the half `drift` cannot see
      exit=0
charly: error: command "task": task "skills:drift" failed (1 step(s) failed)
NEGATIVE_EXIT=1
$ git checkout -- candy/plugin-marketplace/emit.go
$ git status --porcelain > /tmp/pm-42-restore.log 2>&1
$ wc -l < /tmp/pm-42-restore.log
0

Block 3 — the R10 bed, on the pushed tree. A fresh build of the box, a deploy on the
disposable: true target, the live check, the feature run, the update, the rebuild,
and the cleanup.

$ charly check run check-marketplace > /tmp/pm-42-bed.log 2>&1
$ echo "BED_EXIT=$?" >> /tmp/pm-42-bed.log
$ cat /tmp/pm-42-bed.log
charly: resolving project (first run — may take a moment)...
charly: resolving project (first run — may take a moment)...
Resolved @github.com/opencharly/plugin-vm -> main (default branch)
Removed /home/atrawog/.config/containers/systemd/charly-check-marketplace.container
Reloaded systemd user daemon
charly check run check-marketplace: [image-build] START (charly box build; log: .check/check-marketplace/2026.297.1402/image-build.log)
charly check run check-marketplace: [image-build] PASS after 5.108s (log: .check/check-marketplace/2026.297.1402/image-build.log)
charly check run check-marketplace: [check-image] START (charly check box; log: .check/check-marketplace/2026.297.1402/check-image.log)
charly check run check-marketplace: [check-image] PASS after 12.441s (log: .check/check-marketplace/2026.297.1402/check-image.log)
charly check run check-marketplace: [deploy-add] START (charly deploy add; log: .check/check-marketplace/2026.297.1402/deploy-add.log)
charly check run check-marketplace: [deploy-add] PASS after 4.139s (log: .check/check-marketplace/2026.297.1402/deploy-add.log)
charly check run check-marketplace: [config] START (charly config; log: .check/check-marketplace/2026.297.1402/config.log)
charly check run check-marketplace: [config] PASS after 1.567s (log: .check/check-marketplace/2026.297.1402/config.log)
charly check run check-marketplace: [start] START (charly start; log: .check/check-marketplace/2026.297.1402/start.log)
charly check run check-marketplace: [start] PASS after 1.28s (log: .check/check-marketplace/2026.297.1402/start.log)
charly check run check-marketplace: [wait-ready] START
charly check run check-marketplace: [wait-ready] PASS after 126ms
charly check run check-marketplace: [bring-up-members] START
charly check run check-marketplace: [bring-up-members] PASS after 16ms
charly check run check-marketplace: [check-live] START (charly check live; log: .check/check-marketplace/2026.297.1402/check-live.log)
charly check run check-marketplace: [check-live] PASS after 4.101s (log: .check/check-marketplace/2026.297.1402/check-live.log)
charly check run check-marketplace: [feature-run] START (charly check feature; log: .check/check-marketplace/2026.297.1402/feature-run.log)
charly check run check-marketplace: [feature-run] PASS after 1.935s (log: .check/check-marketplace/2026.297.1402/feature-run.log)
charly check run check-marketplace: [update] START (charly update; log: .check/check-marketplace/2026.297.1402/update.log)
charly check run check-marketplace: [update] PASS after 17.861s (log: .check/check-marketplace/2026.297.1402/update.log)
charly check run check-marketplace: [rebuild-ready] START
charly check run check-marketplace: [rebuild-ready] PASS after 125ms
charly check run check-marketplace: [check-live-rebuild] START (charly check live; log: .check/check-marketplace/2026.297.1402/check-live-rebuild.log)
charly check run check-marketplace: [check-live-rebuild] PASS after 2.564s (log: .check/check-marketplace/2026.297.1402/check-live-rebuild.log)
charly check run check-marketplace: [wait-ready] START
charly check run check-marketplace: [wait-ready] PASS after 102ms
charly check run check-marketplace: [feature-run-rebuild] START (charly check feature; log: .check/check-marketplace/2026.297.1402/feature-run-rebuild.log)
charly check run check-marketplace: [feature-run-rebuild] PASS after 1.876s (log: .check/check-marketplace/2026.297.1402/feature-run-rebuild.log)
charly check run check-marketplace: [cleanup] START (charly remove; log: .check/check-marketplace/2026.297.1402/cleanup.log)
charly check run check-marketplace: [cleanup] PASS after 10.978s (log: .check/check-marketplace/2026.297.1402/cleanup.log)
charly check run check-marketplace: [cleanup-members] START
charly check run check-marketplace: [cleanup-members] PASS after 0s
charly check run check-marketplace: PASS (steps=16)
BED_EXIT=0

Block 4 — the pin bump is necessary: the SAME generator against the PREVIOUS corpus
pin.
The run resolves the same ~1200 sources and then compares against e802da68
instead of cc10955e; the verdict is at the end of the log.

$ charly marketplace drift --root /tmp/corpus-e802-git --out /tmp/corpus-e802-git > /tmp/pm-42-old-pin.log 2>&1
$ echo "OLD_PIN_EXIT=$?" >> /tmp/pm-42-old-pin.log
$ tail -n 1 /tmp/pm-42-old-pin.log
OLD_PIN_EXIT=1
$ grep -A 5 'marketplace drift:' /tmp/pm-42-old-pin.log
charly marketplace: marketplace drift: 4 generated artifact(s) are stale:
  plugins/.well-known/skills/index.json
  plugins/DISPATCHER.md
  plugins/check/skills/openclaw/SKILL.md
  plugins/tools/skills/crabbox/SKILL.md
run `charly marketplace generate`

Block 5 — the module builds, its tests pass, the manifest validates, and the pin
recipe passes.

== head ==
$ git log --oneline -1
9364e0a fix(drift): a runnable, CI-owned drift gate — and the corpus commit it reproduces (#42)
$ git status --porcelain

== go build ==
$ cd candy/plugin-marketplace && go build ./...
BUILD_EXIT=0

== go test ==
$ cd candy/plugin-marketplace && go test ./...
ok  	github.com/opencharly/plugin-marketplace/candy/plugin-marketplace	3.108s
?   	github.com/opencharly/plugin-marketplace/candy/plugin-marketplace/cmd/serve	[no test files]
TEST_EXIT=0

== gofmt ==
$ gofmt -l candy/plugin-marketplace/
GOFMT_LINES=0

== box validate ==
$ charly box validate
charly box validate: OK — checked 1 candy, 1 box, 1 deploy, 7 distros, 7 builders; 0 warnings, 0 errors
BOX_EXIT=0

== task marketplace:pin ==
$ charly task marketplace:pin
task marketplace:pin: 2 step(s), 0 failed
  [pass] check — the declared MARKETPLACE_REF var equals the marketplace repo's current main head
      exit=0
  [pass] check — the duplicated literal in the check step equals the same head
      exit=0
PIN_EXIT=0
QUICK_DONE

Block 6 — the R5 sweep, claim-keyed. The claim is "nothing in this repository
describes the setup launcher or the .claude/hooks surface as a shipped feature"
.
Ten occurrences survive, and every one is classified here:

  • candy/plugin-marketplace/charly.yml:185 — the bed arm's comment, which names the
    arm it REPLACED. It is history, and the arm it replaced is exactly what the comment
    explains.
  • candy/plugin-marketplace/generate_test.go:378 and :397 — comments stating that
    the setup launcher is gone.
  • candy/plugin-marketplace/generate.go:11 and :44 — the package comments, which
    state that the generator no longer writes charly's harness surface.
  • candy/plugin-marketplace/generate_test.go:195 — the same statement, in the test.
  • candy/plugin-marketplace/generate_test.go:199, :200 and :383 — the module's own
    GUARANTEE, expressed as three assertNotWritten calls: those paths are not emitted.
  • candy/plugin-marketplace/generate_test.go:116 — a hook path held as DATA, in the
    JSON fixture the generator must not emit.

No surface presents either as shipped.

$ git grep -n -iE 'setup[- ]launcher' -- . ':(exclude)CHANGELOG'; echo "exit=$?"
candy/plugin-marketplace/charly.yml:185:                # Replaces the setup-launcher arm, which asserted an executable `setup` the marketplace
candy/plugin-marketplace/generate_test.go:378:	// The setup launcher is gone: it existed only to run generation LOCALLY from a charly
candy/plugin-marketplace/generate_test.go:397:// (charly's .claude/ surface, the CLAUDE.md/AGENTS.md dispatcher splice, the setup launcher) is
exit=0
$ git grep -n 'claude/hooks' -- . ':(exclude)CHANGELOG'; echo "exit=$?"
candy/plugin-marketplace/generate.go:11:// longer writes charly's harness surface (.claude/hooks, the .claude/settings.json merge, the R0
candy/plugin-marketplace/generate.go:44:// wrote charly's own harness surface (.claude/hooks, a .claude/settings.json merge, and the R0
candy/plugin-marketplace/generate_test.go:116:      {"matcher": "Bash", "hooks": [{"type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit-gate.sh"}]}
candy/plugin-marketplace/generate_test.go:195:	// harness surface — .claude/hooks/*, a .claude/settings.json merge and the R0 dispatcher
candy/plugin-marketplace/generate_test.go:199:	assertNotWritten(t, dir, ".claude/hooks/pre-commit-gate.sh")
candy/plugin-marketplace/generate_test.go:200:	assertNotWritten(t, dir, ".claude/hooks/gitcmd.py")
candy/plugin-marketplace/generate_test.go:383:	assertNotWritten(t, dir, ".claude/hooks/pre-commit-gate.sh")
exit=0

The lower-case parameter is gone: the claim-keyed grep for it returns no matches.

$ git grep -n "param out=" -- . ":(exclude)CHANGELOG"; echo "exit=$?"
exit=1

Rulebook compliance

  • R0 — the skills this change's area loads were read before the first edit:
    /charly-internals:marketplace (the corpus generation and ownership split, and the
    drift gate), /charly-internals:plugin (the plugin: block and the per-plugin CUE
    schema), /charly-internals:git-workflow (before any git or PR action) and
    /charly-build:docs (the prose surface). This repository's own AGENTS.md names the
    same four, and it was read first.
  • R1 — every anomaly this change touched got a fresh root-cause analysis before
    the remediation: the four recipe defects, the SDK acceptedRef refusal, the pin
    recipe's unreachable second step, and the stale setup prose. Two findings that are
    genuinely separable from this change were filed as issues and are linked under
    ## Filed issues below — the pin-currency comparison is still manual, and the
    module's Go tests still run in no CI job. Nothing was filed away as "pre-existing".
  • R2 — one canonical implementation per behaviour. The declared corpus commit is
    read from ONE place (var: MARKETPLACE_REF), by both the CI job and the pin recipe;
    the duplicated literal inside the candy file is the one the recipe asserts against,
    as this repository's AGENTS.md requires. The generator and the drift command keep
    sharing the same emission pipeline. The CI job builds the charly host from SOURCE at
    the same pinned tag the corpus's own build action uses
    (opencharly/marketplace's .github/actions/build-charly, v2026.296.2116), so the
    two halves of the contract are compared like for like; the build steps are mirrored
    rather than referenced, because a composite action's uses: takes a literal ref and
    this repository's corpus commit is READ from its own file — a literal corpus sha
    here would be a SECOND corpus pin, which is the duplication this rule forbids more
    strictly than one toolchain tag appearing in two repositories.
  • R3 — no workaround: no sleep, no blind retry, no suppression, no magic number.
    The never-hang ceiling was raised to the budget the operation actually needs rather
    than worked around with a wrapper, and the recipe's inputs are the real ones. The one
    place a value crosses a shell boundary — the task parameter — is quoted at its use
    site, because the expansion is text and not a data channel
    (opencharly/plugin-task#34 owns the class-level fix).
  • R4 — the product was fixed before the prose. The recipe, the pin recipe and the
    CI job are the product; the README's new "The drift gate" section describes what they
    do, and it was written after they were green.
  • R5 — the legacy prose is deleted, with the claim-keyed repo-wide sweep in block 6
    and every survivor classified there. The lower-case parameter is gone, with its own
    sweep. No legacy path, alias or dual-mode survives.
  • R6 — git status was read before every mutation, the work belongs to this
    session's own worktree, no submodule was edited, no history was rewritten, no hook
    was bypassed, and the branch is a fix/ branch cut from fresh origin/main.
  • R7 — coverage that can fail: the negative control in block 2 turns the gate red
    from the pushed tree, and the file is restored afterwards. The changed path executes
    live: the gate, the bed, the module's own tests and the pin recipe all run against
    the real corpus checkout and the real plugin binary.
  • R8 — every boundary that crosses a real service ran live: the corpus checkout is
    a real clone at a real commit, the plugin is built from this tree by the charly
    host, and the R10 bed runs against the disposable: true target. Nothing is mocked.
  • R9 — the binary under test was built from this tree by the charly host, and the
    witness in block 5 names the module and the SDK version it was built against.
  • R10 — a fresh rebuild and a live run on the affected disposable: true target
    (check-marketplace), on the exact tree this PR pushes, with zero warnings.
  • RDD / ADE / SDD — RDD: the one high-risk assumption is that the old SDK refuses
    the corpus's bare github.com/opencharly/charly source, and it was proved by
    measurement (the 9 stale artifacts, and the direct acceptedRef behaviour) before
    the pin moved. ADE: the candy's plan: is the acceptance test — the two
    check: steps of skills:drift are exactly the two the drift gate runs, and the
    box's checks are the ones the bed runs. SDD: N/A — no schema and no .cue change.
  • Hard cutover — one cutover in one PR: the recipe, the pin recipe, the CI job and
    the prose move together, with no phase, TODO or deferred cleanup left inside the
    scope. One commit, one squash.
  • Coordination — the scope is not contended: issue charly task skills:drift is RED: the task own --param out=<path> is inert (the expander grammar is uppercase-only) and no CI runs this gate #42 was filed and CLAIMED
    before the branch was cut, and no other open PR or issue touches this recipe. The
    two filed issues below are the ones this change's own findings produced.
    Agent: FIRST and Assisted-by: LAST on the issue and on this PR.
  • Disposable-only autonomy — the only mutation was on the disposable: true
    check-marketplace target, and it went through the owning charly check run
    command. No other pod, container or VM was touched.
  • Clean architecture + Go gates — the generator's own source changed only in
    comments and help text; the emission pipeline, the schema and the plugin's
    placement are untouched. gofmt and the module's tests are in block 5.

Attribution tier

fully tested and validated — the change-class gate passed on the final tree with
zero warnings (block 1), the R10 bed passed on a fresh build of that tree against the
disposable: true target (block 3), the changed path executed live (blocks 1, 2, 4
and 5), and the coverage was shown to fail when the change is removed from the tree
(block 2).

Filed issues

  • the declared MARKETPLACE_REF can go stale again, and nothing would see it #50 — the declared MARKETPLACE_REF can go stale
    again and nothing would see it. The drift gate compares the generator against the
    DECLARED corpus commit; it never consults git, so it cannot answer the
    pin-currency question, and the recipe that does answer it (task marketplace:pin) is
    manual. Non-blocking for this change: the pin is current in it, and the fix is a CI
    job that REPORTS the behind state rather than failing on it — the umbrella
    rulebook (Part II fix: the setup launcher must not require the removed charly submodule #3) defines a behind pin as legal and the remedy as a bump PR.
  • the plugin's Go tests run in no CI job #51 — the plugin's Go tests run in no CI job. The
    repository's only CI is the org-wide pr-validator, whose lint job skips this
    repository cleanly because it declares no golangci-lint config, and no job compiles
    or tests the module. Non-blocking for this change: the module's tests pass on the
    pushed tree, as block 5 shows.

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

…t reproduces (#42)

`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>
@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS

The PR is open and its whole body is written. The gate, the R7 negative control, the R10 bed, the old-pin red measurement, the module's own tests and the pin recipe were all run on this exact head (9364e0a976e9ee04de9dea715efbac9e7a4c92a7) and are pasted in the body with their exit codes; the R10 bed ran on the disposable: true check-marketplace target with a fresh build at 2026.297.1402.

What is next: the fresh charly/pr-validator verdict. Every finding is fixed or rebutted on this thread before any further push, and the body is rewritten for the new head before the push that carries it.

One note for the reader of the diff: the new .github/workflows/drift-gate.yml runs on push: [main] and workflow_dispatch, not on pull_request. That is a design constraint, not an omission, and the workflow's own header states the mechanism: the corpus can only be regenerated with a RELEASED generator, so a pull-request gate would be red by construction for exactly the generator change it should inform, and it would deadlock the chain that repairs it. The same header names the two remedies for a red run on main.

Next update due: when the validator's verdict lands, or within two hours, whichever comes first.

Agent: charly · session light-init
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)

@github-actions

Copy link
Copy Markdown

Review — PASS

I re-derived every material claim in the body against the current diff and the commit state. The PR is internally consistent and the changes match their stated purpose.

Verification performed

File/line accounting. The body claims 13 files, +273/−67. Summing the per-file diffs gives exactly 119+4+50+23+8+2+3+2+1+2+6+7+46 = 273 insertions and 12+11+7+1+3+3+1+2+4+7+16 = 67 deletions across 13 files. Exact match — a strong honesty signal.

The four recipe defects (charly.yml) are each reproduced by the diff:

  1. Parameter renamed out → CORPUS (UPPER-CASE), with the command now --root "${CORPUS}" --out "${CORPUS}" — value reaches the command.
  2. --root now names the corpus checkout (same ${CORPUS}), matching the "refs list lives in the corpus" rationale.
  3. Committed-state step is now git -C "${CORPUS}" status --porcelain, so it checks the corpus tree, not this repo.
  4. Drift step carries timeout: "1800s" (task-level 2100s), addressing the 1m30s never-hang ceiling vs the ~12m measured runtime.

Pin bump. candy/plugin-marketplace/charly.yml moves MARKETPLACE_REF and the duplicated contains: literal from e802da68… → cc10955e…, and the SDK bump v0.2026296.2152 → v0.2026297.129 is reflected in both go.mod and go.sum (hash updated). No stray old pin remains in the diff.

marketplace:pin anchoring. Both greps are now anchored (MARKETPLACE_REF: and contains:). The body's own root-cause (the old tail -1 caught the trailing SKILL.md sha256) establishes that the sha256 is the last 40-hex, so grep -m 1 'contains: …' resolves the pin, not the sha256; block 5's PIN_EXIT=0 is consistent. YAML single-quote doubling ('') yields valid shell quoting.

New CI job. .github/workflows/drift-gate.yml reads the baseline from MARKETPLACE_REF in the candy file (no second literal), checks out the corpus at that SHA and charly at the pinned tag, builds the host from source, puts it on PATH, and runs charly task skills:drift --param CORPUS=…. permissions: contents: read, all actions SHA-pinned, concurrency guard, timeout-minutes: 60. .gitignore adds .ci/ so the nested checkouts don't dirty the workspace. The main-only trigger is a documented design constraint, not an omission.

Prose cleanup. README.md, the box/candy descriptions, package comments, prune.go boundary, and emit_marketplace.go all drop the removed "setup launcher / harness surface" language consistently; block 6's claim-keyed sweep classifies the surviving mentions as history/guarantees/data, which matches the cited assertNotWritten guarantees.

Security/scope. No secrets, no privilege changes, no new dependency beyond a version bump of an already-declared module; the functional Go surface changes are comments/help text only, with gofmt/tests reported green in block 5.

Comments

The thread is empty (0 comments); nothing to disposition.

Minor, non-blocking observations

  • The new workflow does not run on pull_request, so it will first execute post-merge. This is explicitly reasoned in the body and is not a defect.
  • Correctness of the contains: anchor depends on the pin's contains: being the first 40-hex contains: in the file (the body's analysis and the passing pin recipe support this); a future edit that adds an earlier 40-hex contains: would need the anchor tightened — worth a one-line note in the recipe, but not a blocker.

No contradiction, stale claim, security issue, or missing coverage was found that would justify blocking.

Verdict: PASS


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

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 24, 2026 14:11
@github-actions
github-actions Bot merged commit ef8ee0d into main Oct 24, 2026
3 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 24, 2026
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.

1 participant