12 KiB
title, date, category, module, problem_type, component, severity, applies_when, symptoms, root_cause, resolution_type, tags, related_components
| title | date | category | module | problem_type | component | severity | applies_when | symptoms | root_cause | resolution_type | tags | related_components | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Context-absent skill handoffs need ownership transfer, a loaded callee, and fail-closed refusal | 2026-07-31 | skill-design | skill-design | design_pattern | development_workflow | medium |
|
|
missing_workflow_step | workflow_improvement |
|
|
Context-absent skill handoffs need ownership transfer, a loaded callee, and fail-closed refusal
Context
ce-commit-push-pr auto-hands off to ce-babysit-pr after opening a PR, so the ship pipeline can keep watching a PR through review and CI without the user asking twice. Two live failures against Esper-Labs/nugget show complementary gaps at that seam:
-
Nugget #1933 — reinvent babysit mechanics. The shipping agent's handoff did not go through the skill at all: instead of loading
ce-babysit-pr, it called that skill's helper CLI directly —pr-snapshot watch --pr 1933 --interval 60.ce-babysit-prStep 2 requires a bootstrap-then-arm sequence: create a state dir, runpr-snapshot snapshot ... --start-invocation --invocation-budget-seconds N, capture the emittedinvocation_id/invocation_started_at/invocation_budget_seconds, and only then callwatchwith all three tokens plus--state-dir. The barewatch --pr 1933 --interval 60call skipped bootstrap entirely and was missing every required flag, so argparse correctly failed closed with exit 2. The diagnostic tell was the--interval 60value: the skill's own documented command block uses--interval 150with the full bootstrap flag set. The agent hadn't misremembered one flag — it had reconstructed the entire command from a vague memory of "there's a watch command." That is strong evidence (not proof — loaded-but-ignored can't be fully excluded) that it did not havece-babysit-pr'sSKILL.mdin context when it acted. -
Nugget #1983 — substitute a narrower watcher when load fails. On Cursor (no Claude Code
Skilltool), the agent attempted handoff, failed to loadce-babysit-prfrom a stale catalog path, then soft-degraded toTask(ci-watcher). That is CI-only and is not babysitting (no review-thread resolution, nopr-snapshotwatch loop, noce-resolve-pr-feedback/ce-debugownership). The ship run reported the PR URL as done, then silence. Mechanism-shaped prose ("invoke via the Skill tool or equivalent") without a completion gate or explicit non-substitutes licensed that degrade.
Guidance
When a skill (the caller) auto-hands off to a follow-on skill (the callee) that has a multi-step bootstrap protocol, soft prose "auto-invoke <callee>" is not enough — an agent at the end of a long turn can satisfy that instruction by doing callee-shaped things directly, without the callee's SKILL.md in context, and silently skip its state machine. Harden both ends:
- Ownership + completion at the handoff seam, in the caller's skill body. The "Babysit handoff" paragraph in
skills/ce-commit-push-pr/SKILL.mdtreats interactive full workflow as not done untilce-babysit-prowns follow-on for that PR (or an explicit skip applies). Reporting the PR URL alone is not success. Transfer ownership by loading that skill's full instructions through whatever this host uses for skill-to-skill handoff — harness-agnostic, not Claude-Code-Skill-shaped. Success =ce-babysit-prhas started on that PR (its own bootstrap/watch path). Keep the anti-reinvention negative: never start babysit mechanics yourself (pr-snapshot, arm a watcher, reconstruct the watch loop from memory). Name explicit non-substitutes (ci-watcher,gh pr checks --watch, ad-hoc poll loops, memory reconstruction, deferred "I'll babysit later"). If the callee cannot be loaded or started, hard-fail and report blocked — do not invent a parallel or narrower watch. Soft-degrade (checkpoint-only harness) applies only after successful ownership transfer. - Make the callee's CLI fail closed AND self-explain. The refusal message is the only channel that reliably reaches an agent operating without the skill's instructions loaded — it can't read
SKILL.md, but it does read its own error output.skills/ce-babysit-pr/scripts/pr-snapshotdefines_WatchHintingParser, anargparse.ArgumentParsersubclass wired in viaparser_classon the subcommand registry. Whenwatchis missing any bootstrap flag (--state-dir,--invocation-id,--session-started-at,--invocation-budget-seconds), the parser still exits 2 but appendsWATCH_BOOTSTRAP_HINT: whatwatchactually is (arms an invocation already bootstrapped bysnapshot --start-invocation, the ce-babysit-pr Step 2 bootstrap), a recovery instruction to invoke thece-babysit-prskill through the harness's callable skill mechanism, and "Never mint the bootstrap values yourself." The hint deliberately does not carry a copyable bootstrap command recipe — a follow-up review round caught that teaching the raw snapshot+watch sequence would let a context-absent agent arm a watcher and keep operating outside the skill's mutation, wake-handling, and stop protocol, the exact ad-hoc path the caller-side boundary forbids. The check is scoped to thewatchsubcommand and to missing-required-bootstrap-flag errors specifically, sosnapshot/markerrors and a plain missing--prnever carry the hint. - Regression guards pin both layers so they can't silently regress:
tests/ce-babysit-pr-watch-bootstrap.test.tsasserts a barewatchcall exits 2 with the bootstrap guidance and that unrelated errors stay unscoped; the "babysit handoff requires ownership transfer..." test intests/commit-push-pr-contract.test.tspins the completion gate, non-substitutes, hard-fail-on-load-failure, and never-start-mechanics constraints.
A tempting alternative — a packaged single start entrypoint like pr-snapshot start-watch that bundles bootstrap+arm into one call — was considered and rejected. It doesn't close this failure class: (a) a context-absent agent doesn't know the wrapper exists either; it will still type the watch --pr N it half-remembers. (b) A convenient one-shot start command becomes a second budget-minting path. ce-babysit-pr's documented rule is "never use --start-invocation after the first snapshot" — a wrapper that's easy to reach for on re-arm or a stack transition would tempt an agent into minting a fresh invocation budget exactly when it shouldn't. (c) The correlated tokens (invocation_id, invocation_started_at, invocation_budget_seconds) pervade the entire loop — every mark, every re-arm, every --continue-invocation — so packaging only the start step hides where the tokens come from without removing the need to understand and hold them correctly later. A self-bootstrapping watch (auto-run snapshot internally when bootstrap flags are absent) was rejected for the identical budget-minting reason. Over-prescribing a Cursor-specific Read SKILL.md recipe was also rejected: the missing piece is ownership + success/fail criteria that work on any harness, not a host-specific load recipe.
Why This Matters
The failure mode here is specific to skill-to-skill handoffs where the callee has stateful, multi-step mechanics (bootstrap tokens, budgets, watch loops) rather than being a single stateless action. A caller that only says "auto-invoke X" trusts the executing agent to also independently know X's internal protocol — but the entire reason X is a separate skill is that its protocol lives in X's own SKILL.md, which the caller's agent has not read. Pinning Claude Code's Skill tool alone still fails on harnesses without that primitive: the agent invents a narrower substitute and treats the ship as done. The fix is an ownership/completion gate, harness-agnostic load language, explicit non-substitutes, hard-fail when load/start fails, plus making the callee itself refuse-and-explain when its stateful protocol is skipped.
When to Apply
- A skill (A) auto-hands off to a follow-on skill (B) as a default/background behavior (not a one-shot user-initiated call), especially near the end of a long-running turn where context pressure is highest.
- B exposes a CLI or script with multi-step state (bootstrap -> correlated tokens -> repeated re-arm/continuation calls), where doing a "shaped-like-B" action without B's protocol produces a plausible-looking but illegal call.
- You observe an agent invoking a sibling skill's helper script directly instead of the skill itself, particularly with parameter values that don't match that skill's own documented defaults — a strong signal the skill's instructions were never loaded.
- You observe an agent substituting a narrower watcher (
ci-watcher,gh pr checks --watch, ad-hoc poll) when skill load/start fails, and treating the caller's primary outcome (PR URL) as done.
Examples
Before (observed failure #1933):
pr-snapshot watch --pr 1933 --interval 60
Missing --state-dir, --invocation-id, --session-started-at, --invocation-budget-seconds; --interval 60 doesn't match the skill's documented --interval 150. Argparse failed with a bare "required arguments" usage dump — correct exit code, no actionable recovery path.
Before (observed failure #1983):
# Skill load failed (stale/missing catalog path on Cursor)
Task(ci-watcher) # CI-only — not babysitting
Ship reported the PR URL as done; babysitting only resumed after the user asked why ce-babysit-pr wasn't used.
Legal bootstrap sequence (ce-babysit-pr Step 2):
pr-snapshot snapshot --pr 1933 --repo OWNER/REPO --state-dir DIR \
--start-invocation --invocation-budget-seconds SECS
# capture emitted invocation_id / invocation_started_at / invocation_budget_seconds
pr-snapshot watch --pr 1933 --repo OWNER/REPO --state-dir DIR \
--invocation-id ID --session-started-at TS --invocation-budget-seconds SECS --interval 150
Refusal message, after: a bare watch --pr N still exits 2, but now the stderr also carries WATCH_BOOTSTRAP_HINT — it names what is missing (the --start-invocation bootstrap, ce-babysit-pr Step 2), directs the agent to invoke the ce-babysit-pr skill through the harness's callable skill mechanism rather than teaching the raw command sequence, and closes with "Never mint the bootstrap values yourself" — so the same context-absent agent that produced the illegal call is routed back into the skill (whose instructions own bootstrap, arming, marks, and stopping) instead of being handed enough helper commands to keep operating outside it.
Handoff seam, after: the "Babysit handoff — default on; completion gate." paragraph in skills/ce-commit-push-pr/SKILL.md requires ownership transfer to a loaded ce-babysit-pr, defines success as that skill started on the PR, names CI-only substitutes as invalid, and hard-fails when load/start fails instead of soft-degrading to a narrower watch.
Related
docs/solutions/skill-design/cross-harness-cross-model-tool-invocation.md— same principle (pin portable outcomes and host adapters rather than letting an agent invent a substitute), applied to tool selection across harnesses instead of skill-to-skill handoff.docs/solutions/agent-friendly-cli-principles.md— Principle 4 (fail fast with actionable errors) is the general rubric that thepr-snapshotrefusal hint instantiates concretely.docs/solutions/skill-design/dispatch-script-failure-degrade-outcome-not-boundary.md— shares the "never weaken the enforced boundary on failure" shape in a different subsystem.