cuioss

finalize-step-sync-plugin-cache

Synchronize the Claude plugin cache from target/claude/ via the consolidated sync engine

cuioss 5 Updated 2mo ago
GitHub

Install

npx skillscat add cuioss/plan-marshall/finalize-step-sync-plugin-cache

Install via the SkillsCat registry.

SKILL.md

Finalize Step — Sync Plugin Cache (project-local)

Project-local executor for project:finalize-step-sync-plugin-cache.
Invokes the consolidated sync.py engine in the project-local
.claude/skills/sync-plugin-cache/ skill to mirror target/claude/
into the host plugin cache.

This step is project-local rather than a default: built-in for
the same reason as project:finalize-step-deploy-target: the cache-
sync only makes sense for this repo (the plan-marshall meta-project).
Consumer projects have nothing to publish, so they don't get this step
seeded into their marshal.json defaults.

This step runs on the main checkout post-merge, after
project:finalize-step-deploy-target has regenerated target/claude/
from the merged source tree. Syncing the host cache here means the
next session boot and this sync read the same authoritative content.

Staleness guard

sync.py refuses to mirror target/claude/ into the cache unless the
emit sentinel proves the target tree is current. The sentinel is a
JSON file written by project:finalize-step-deploy-target at
target/claude/.emit-marker.json. It carries an ISO-8601
emit_completed_at timestamp and a source_tree_fingerprint computed
from git's native ls-files / hash-object primitives over
marketplace/bundles/.

The guard refuses (exit code 2, status: error) in three cases:

Case Cause summary_message shape
Sentinel absent project:finalize-step-deploy-target was not run staleness_guard: sentinel missing or unreadable at {path} — run finalize-step-deploy-target first.
Sentinel unparseable / missing fingerprint Corrupted or hand-edited sentinel staleness_guard: sentinel ... is missing source_tree_fingerprint.
Fingerprint mismatch Source tree changed since last emit staleness_guard: source tree changed since last emit — re-run finalize-step-deploy-target.

The Phase 6 ordering (project:finalize-step-deploy-target at 80 →
project:finalize-step-sync-plugin-cache at 85) means the sentinel is
written immediately before this step reads it, so the guard normally
passes on the first try. A failure here points at deploy-target having
been skipped, or at concurrent edits to marketplace/bundles/ between
emit and sync (which would also invalidate the cached output).

The --skip-staleness-guard flag remains the escape hatch for tests and
recovery flows. Phase 6 never invokes it; only operators do, after
diagnosing why the sentinel is stale.

Ordering

The canonical Phase 6 ordering surrounding this step is:

default:branch-cleanup (70) →
project:finalize-step-deploy-target (80) →
project:finalize-step-sync-plugin-cache (85) →
default:record-metrics (990)

order: 85 places this step immediately after
project:finalize-step-deploy-target (so the cache mirrors the
just-regenerated target/claude/ content), post-branch-cleanup on the
main checkout. This step is the single project-level owner of on-main
executor regeneration: after the cache sync succeeds it regenerates
.plan/execute-script.py against the freshly-synced cache (Execution
step 3). integrate_into_main performs the move-back only and does NOT
regenerate the executor — the executor stays a per-tree derived artifact
(ADR-002). Coupling regen to this step (CLAUDE.md's canonical "sync cache →
regenerate executor" sequence) closes the no-worktree staleness gap: the
regen runs in BOTH worktree and no-worktree finalize flows because it is
just a finalize step, not a move-back side effect.

Inputs

  • {plan_id} — required. Used for logging.

Execution

Inline-only — this step does NOT delegate to a Task agent. The sync
engine is a fast Python script with deterministic output.

1. Invoke the consolidated sync engine

python3 .claude/skills/sync-plugin-cache/scripts/sync.py

The script returns a TOON document with status (success |
partial | error), synced_count, failed_count,
summary_message, and a synced[N]{bundle,version,status} table.

2. Parse the result

Field Meaning
status: success All bundles synced; record outcome=done and use synced_count for the display detail
status: partial Some bundles failed; record outcome=failed and surface summary_message in display_detail
status: error Hard failure (no bundles synced); record outcome=failed and surface summary_message

3. Regenerate the on-main executor (after a successful sync)

When the sync status is success, regenerate .plan/execute-script.py
against the freshly-synced host cache. This runs on the main checkout, so
the generator's cwd-relative resolution writes main's executor with mappings
that point at the just-synced cache — refreshing the on-main mapping after a
plan that changed the marketplace script set. This is the project-level,
meta-project-only owner of on-main executor regeneration; it replaces the
former integrate_into_main regen and runs in BOTH worktree and no-worktree
finalize flows (closing the no-worktree staleness gap).

python3 .plan/execute-script.py plan-marshall:tools-script-executor:generate_executor generate

Regeneration is unconditional-after-successful-sync (always re-deriving
the executor from the fresh cache is cheap, deterministic, and eliminates the
staleness class without a gating heuristic) and non-fatal: a non-zero exit
or failure logs a WARN and the step still records its sync outcome — finalize
must not block on a mapping refresh.

python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
  work --plan-id {plan_id} --level WARNING \
  --message "[STATUS] (project:finalize-step-sync-plugin-cache) On-main executor regeneration failed (non-fatal); run /marshall-steward to recover"

When the sync status is partial or error, SKIP regeneration — the cache
is incomplete, so regenerating against it would be wrong; the step records the
sync failure (Step 4) and an operator recovers.

3b. Reconcile the build daemon (after a successful sync)

The cache bump this step just performed is exactly what makes a running
marshalld stale: the daemon is version-pinned to the OLD bundle copy while the
fresh cache now carries a newer one. After a success sync (and the executor
regen above), run the meta-project-only reconcile so an idle stale daemon is
upgraded to the verified pin and a busy one is left running with the deferral
recorded:

python3 .claude/skills/sync-plugin-cache/scripts/reconcile_daemon.py

The script queries manage-build-server status and applies the idle-conditional
contract: idle-and-stale → upgrade (drain-then-start-verified); busy-and-stale
(anything in flight or queued) or an undeterminable running provenance →
defer (never drain a live build), leaving a readable reconcile-owed marker;
down-but-enrolled → plain start; already-current → no-op. An absent/disabled
build server (or a session without the executor) is a silent no-op via the
script's fail-open adapters, so this changes no shared-daemon behaviour and a
repository not using marshalld is unaffected.

Like the executor regen, the reconcile is non-fatal: it never blocks finalize.
Read its action / display_detail from the TOON and surface a one-line detail;
a defer is reported (the reconcile-owed marker persists it), never swallowed.
When the sync status is partial or error, SKIP the reconcile — the cache is
incomplete.

4. Mark step complete

python3 .plan/execute-script.py plan-marshall:manage-status:manage_status \
  mark-step-done --plan-id {plan_id} --phase 6-finalize \
  --step project:finalize-step-sync-plugin-cache \
  --outcome {done|failed} \
  --display-detail "{display_detail}"

On status: success, {display_detail} is
"{synced_count} bundles synced; on-main executor regenerated" (append
" (regen failed — run /marshall-steward)" instead when the Step 3 regen
exited non-zero — the sync outcome is still done). Append the reconcile's
display_detail from Step 3b when it did anything other than a plain no-op (e.g.
"; daemon upgraded" or "; daemon reconcile deferred (owed x2)"), so a
deferral is visible at the step level and not only in the marker.
On status: partial or status: error, surface the engine's
summary_message field verbatim in --display-detail so the renderer
shows the underlying failure for triage.