Skip to content

bump: a pin wave needs the full set — advance the pin from its own source (remote tip, or the source's gitlink) and record it in the index, so a wave never has to materialize 431 checkouts #37

Closed

Description

@aitrawog

What is wrong

charly task sync — the bump mode of the verb:git-submodules verb — advances a pin by moving the path's checkout to the remote's tip and recording what the checkout says. A path with no checkout has no pin to advance, so the verb classifies it and fails loud, deliberately: the invariant TestGitSubmodules_BumpUninitializedFailsLoud pins it, and it is the reason opencharly/charly#768 exists — "a sync over a tree it could not fully advance must never report success, and must never touch the umbrella."

The consequence for the pin contract is a cost, not a bug: a pin wave needs the full set. The umbrella's 431 submodules must be materialized before a wave can run, which costs charly task init --all — measured at ≈2 G on disk and ≈2 min of wall clock — even though a wave advances ~50 of them. The umbrella's own guidance already carries the workaround, in charly.yml:

# A WAVE NEEDS THE FULL SET: the pin verb fails loud on an uninitialized path

and in the README ("the bump fails loud on a path with no checkout").

So a repo that merely wants to move one pin must first materialize 431 checkouts, and the materialization is not free of side effects: it brings in the vendored upstream mirrors (the openclaw mirror alone is 7.8 G of .git) that the session never touches.

RCA

Mechanism. The pin lives in the superproject's INDEX — the 160000 gitlink — but the advance path reaches it through the CHECKOUT: bump moves the submodule's HEAD to the remote's tip and then records the sha the checkout landed on. A path with no checkout therefore has no tip to move and no sha to record from, and the verb's fail-loud precondition (runGitSubmodules, candy/plugin-task/verbs.go:369-386) fires.

Missed control. Nothing reads the remote ref straight into the index for a path that has no checkout. The ls-remote → git update-index --cacheinfo 160000,<sha>,<path> path does not exist, and for an edge path it could not be a remote ref at all: an edge's tip is the source's gitlink, which is already in the index. The checkout is the only place the verb looks, and the checkout is exactly what is missing.

Blast radius. Every session that works in a light umbrella clone: a pin wave costs a full materialization, the materialization is the expensive part of the clone's on-disk footprint, and the "one wave" is therefore a once-a-few-weeks operation that a session is tempted to skip — which is how a pin drifts in the first place. charly task prune has the same need for the same reason.

Root fix. Advance the pin from the pin's own source, and record it in the index:

  • a leaf (upstream:<ref>) advances to the remote ref's tip — git ls-remote <url> <ref> → sha → git update-index --cacheinfo 160000,<sha>,<path>;
  • an edge (derived-from:<source>, and every pin_map entry) advances to the source's gitlink, read from the index — no network, no checkout.

The index then carries the pin, and the checkout follows on the next charly task init.

The behavioural delta is explicit, and the rulebook has to carry it. After a wave, a materialized checkout is behind the index until it is refreshed, so the order becomes sync → init → verify. The umbrella's README.md and AGENTS.md (Part II #3) say this in one place today; after the change they must say it in the three places the order is used.

What must NOT change. The fail-loud invariant stays for every path the verb cannot advance — an unjudgeable pin (no intent, no branch=), a divergent pin, a remote ref that cannot be read. The pin contract's states (cur, behind, blocked-by <source>:<twin>, not-on-the-served-line) and the producer-first leg order are unchanged: a wave still advances a leaf to its remote's tip and an edge to its source's gitlink, and a blocked-by pin is still not advanced past its producer.

The proof. A fixture superproject with a declared but unmaterialized path, asserting that (a) the index's gitlink advanced to the remote's tip, (b) no checkout was created, (c) the run still fails loud for a path it cannot judge. A test that cannot fail is invalid, so the fixture must be shown to fail against the current shape.

Why it is filed here and not done here

The plan that carries this leg is the umbrella's own ("a working set instead of 431 checkouts"), and it names this as its wave 3 — the OPTIONAL last leg — because it changes the pin contract's central invariant ("the index and the checkouts agree") and is therefore not in waves 1–2. Wave 1 landed: charly task init and scripts/init-submodules.sh exist in the umbrella, with the working set derived, and the umbrella's charly.yml already documents the constraint this issue removes.

This issue is filed in the repo that owns the verb, because that is where the change lands. It is filed before the branch, per the umbrella's issue-first coordination rule.

Not filed away

This is filed, not fixed here. It changes a pin contract, so it needs its own PR, its own gate (scripts/gate-task.sh, the only live execution of these verbs) and the org validator.

Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)

Activity

  1. aitrawog commented on Oct 23, 2026

    @aitrawog
    ContributorAuthor

    CLAIM

    CLAIM

    CLAIM

    This session owns the last leg of the umbrella's working-set plan: the pin wave that advances a pin from the pin's own source, so a wave never has to materialize 431 checkouts.

    The shape is already decided — it is wave 3 of the umbrella's plan, and the umbrella's own charly.yml and README.md already carry the constraint this removes ("A WAVE NEEDS THE FULL SET: the pin verb fails loud on an uninitialized path"). The change lands HERE, in the repo that owns the verb, because that is where the pin contract's central invariant lives ("the index and the checkouts agree").

    What the change is. A leaf advances to the remote ref's tip (ls-remote → git update-index --cacheinfo 160000,<sha>,<path>); an edge advances to the SOURCE's gitlink, read from the index — no network, no checkout. The index carries the pin; the checkout follows on the next charly task init. The behavioural delta is explicit and goes into the rulebook with it: after a wave a materialized checkout is behind the index until it is refreshed, so the order is sync → init → verify.

    What must not change. The fail-loud precondition stays for every path the verb cannot advance — an unjudgeable pin, a divergent pin, a remote ref it cannot read. The pin contract's states and the producer-first leg order are unchanged, and a blocked-by pin is still not advanced past its producer.

    The proof. A fixture superproject with a declared but unmaterialized path: the index's gitlink advances, no checkout is created, and the run still fails loud for a path it cannot judge. The fixture is shown to fail against the current shape, because a test that cannot fail is not coverage.

    What is next. The umbrella's own pin wave lands first (opencharly/docs#175, then opencharly/opencharly#637) — this leg does not change the wave's own mechanics, and it is the OPTIONAL last leg of the plan.

    Next update due: when the umbrella's wave has landed, 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)

  2. self-assigned this
    on Oct 23, 2026
  3. aitrawog commented on Oct 23, 2026

    @aitrawog
    ContributorAuthor

    STATUS

    The umbrella's wave has landed, and the verb change that removes its "needs the full set" cost is implemented and committed.

    Where the work stands.

    • The leg this issue was waiting for is behind us: the umbrella's own wave landed (the vendored corpus pin is behind the corpus's merged main — the corpus's own consumer report names this repo docs#175, then chore(pins): advance the umbrella's 55 gitlinks to their producers' merged mains opencharly#640 — merge b0b50292d2c3dadf9480dd79894db40aa2835a28, merge-time tag v2026.296.1826).
    • The implementation is committed on this repo's branch feat/pin-wave-without-materialization, head af406fc60fc6c6d4b8a0f4436144e2561f04306b. A path with no checkout now advances its pin in the index — a leaf to the tip its remote serves for branch=, an edge to the twin the source's tree records — while a path with a checkout is still rolled in that checkout, so the checkout and the index keep agreeing. The legacy "fail loud on an uninitialized path" precondition is deleted in the same cutover, and the fail-loud discipline for everything the verb cannot advance (an unjudgeable path, an unserved branch, a missing url, an unreadable source twin) is unchanged.
    • The repo's live gate — scripts/gate-task.sh, the only live execution of these verbs — is running on the committed tree now; the PR opens with its output pasted, and the coverage's differential against the pre-change tree is in the PR body.

    What is next. The PR, then the umbrella's own legs in producer-first order: the skill's sentence, the corpus regeneration, the docs page, and the umbrella's pin bump together with the three places the umbrella states the wave's order (sync → init → verify).

    Next update due: when the PR is open.

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

  4. aitrawog commented on Oct 23, 2026

    @aitrawog
    ContributorAuthor

    STATUS

    The PR is open: #38 — #38

    Where the work stands.

    • Head af406fc60fc6c6d4b8a0f4436144e2561f04306b on feat/pin-wave-without-materialization, pushed and open as a ready PR (not a draft). The body carries the whole story: the summary, the repo's live gate pasted in full, both R7 differentials, the repo's Go gates, the R5 sweep, the rulebook compliance and the change classification.
    • The proof is green on the committed tree: the live gate (scripts/gate-task.sh) ends GATE_RC=0 with the new arm advancing an unmaterialized pin in the index (b84e6586… -> 384e5fc61) and creating no checkout; the Go gates are green on the module's declared toolchain (go 1.26.4); charly box validate reports 0 warnings and 0 errors.
    • The coverage discriminates at both levels, and the differentials are pasted in the body: the two new unit tests fail against the pre-change tree, and the new live arm turns the gate red (GATE_RC=1) against that same tree.

    What is next. The validator runs on the PR; every finding it raises is fixed or rebutted on the thread before any next push. After it lands, the umbrella's own legs follow in producer-first order: the skill's sentence in layer-charly-internals, the corpus regeneration in marketplace, the docs page, and the umbrella's pin bump together with the three places the umbrella states the wave's order (sync -> init -> verify).

    Next update due: when the validator's verdict lands, or when the PR merges.

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

  5. aitrawog commented on Oct 23, 2026

    @aitrawog
    ContributorAuthor

    RESOLVED

    The wave's last leg landed: the PR is merged and the merge-time tags are written.

    Where the work stands.

    • feat(git-submodules): advance an unmaterialized pin in the index, so a wave needs no checkout #38 merged at 2026-10-23T18:52:03Z — merge commit b5e24d287c6e813a5b3933f42188f4d41ac591a5, root tag v2026.296.1852, module tag candy/plugin-task/v0.2026296.1852. The fresh charly/pr-validator returned PASS on the head af406fc after re-deriving the body's claims against the four-file diff, including the new advance path, charly#768's invariant and both differentials.
    • The change is live: a path with no checkout advances its pin in the index (a leaf to the tip its remote serves for branch=, an edge to the twin the source's tree records), a path with a checkout is still rolled in that checkout, and the fail-loud discipline for everything the verb cannot advance is unchanged.

    What is next. The umbrella's own legs follow in producer-first order: the skill's sentence in layer-charly-internals (the branch is already committed locally, and the producer's tag now exists), charly's adoption of the new module tag candy/plugin-task/v0.2026296.1852, the corpus regeneration in marketplace, the docs page, and the umbrella's pin bump together with the three places the umbrella states the wave's order (sync -> init -> verify).

    Next update due: when the umbrella's leg has landed.

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

  6. aitrawog commented on Oct 23, 2026

    @aitrawog
    ContributorAuthor

    Landed by #38 (merge b5e24d2, root tag v2026.296.1852, module tag candy/plugin-task/v0.2026296.1852). The umbrella's own legs follow in producer-first order.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions