mvanhorn/last30days-skill · error · HandoffContractError

No handoff location available to write the nominations bundl

Error message

No handoff location available to write the nominations bundle: pass --save-dir or configure ~/.config/last30days/.

What it means

`discovery_handoff.write_nominations_bundle` raises HandoffContractError (mapped to exit code 2 by the CLI layer) when `handoff_state_dir(save_dir, config_dir)` returns None: the engine has nowhere to write the leg-1 nominations bundle. The bundle location is save-dir when `--save-dir` was supplied, else the config dir (`~/.config/last30days/`); there is deliberately no cross-store fallback, so neither present means no writable contract location.

Source

Thrown at skills/last30days/scripts/lib/discovery_handoff.py:266

    save_dir: str | Path | None = None,
    config_dir: Path | None = None,
) -> NominationsBundle:
    """Write the leg-1 nominations bundle and return its parsed form.

    Nomination ids are assigned ``n1, n2, ...`` in pool order. The leg-1
    invocation context (enrichment source boundary, requested discovery
    sources, lookback days) rides along so leg 2 resumes with identical
    settings. ``None`` boundaries are preserved as null - "no boundary" and
    "empty boundary" are different contracts. ``source_status`` is the
    sweep's finalized per-source outcome map (serialized via the same
    ``schema.to_dict`` round trip every report uses) so degraded coverage
    survives into legs 2-3; ``mock`` stamps the writing run's provenance.
    """
    if tier not in _VALID_TIERS:
        raise ValueError(f"tier must be one of {_VALID_TIERS}, got {tier!r}")
    state_dir = handoff_state_dir(save_dir, config_dir)
    if state_dir is None:
        raise HandoffContractError(
            "No handoff location available to write the nominations bundle: "
            "pass --save-dir or configure ~/.config/last30days/."
        )
    bundle_id = secrets.token_hex(8)
    generated_at = schema._utc_now()

    rows: list[dict[str, Any]] = []
    nominations: list[BundleNomination] = []
    for index, entry in enumerate(entries, start=1):
        nomination_id = f"n{index}"
        sources = sorted({item.source for item in entry.nomination.items})
        engagement = pipeline._discovery_engagement(entry.nomination.items)
        rows.append({
            "id": nomination_id,
            "cluster_id": entry.cluster_id,
            "heuristic_name": entry.heuristic_name,
            "heuristic_junk": bool(entry.heuristic_junk),
            "sources": sources,

View on GitHub (pinned to c7460f6114)

Solutions

  1. Pass an explicit `--save-dir <path>` so the bundle lands somewhere deterministic and writable.
  2. Or ensure `~/.config/last30days/` exists and is writable (`mkdir -p ~/.config/last30days`).
  3. In containers/CI, set HOME to a writable path or mount a volume and point `--save-dir` at it.
  4. Keep using the same location for legs 2-3: state is searched save-dir-first, config-dir otherwise — never both.

Example fix

# before
python3 last30days.py "topic" --discover --nominate-only   # HOME unset, no config dir
# after
python3 last30days.py "topic" --discover --nominate-only --save-dir /tmp/discovery-run
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

save_dir = Path("/tmp/discovery-run")
save_dir.mkdir(parents=True, exist_ok=True)  # ensure the store exists BEFORE the run
# then always invoke every leg with --save-dir str(save_dir)

Try / catch

from lib import discovery_handoff
state = discovery_handoff.handoff_state_dir(save_dir, config_dir)
if state is None:
    raise SystemExit("no handoff store: pass --save-dir or create ~/.config/last30days/")

Prevention

When it happens

Trigger: Running `--discover --nominate-only` without `--save-dir` in an environment where the config dir cannot be resolved (HOME unset, config directory not creatable), or in a sandboxed harness where neither `--save-dir` nor a config dir is provided.

Common situations: CI jobs or containers running with a stripped HOME; headless gateways (OpenClaw, cron) invoking the engine without configuring `~/.config/last30days/`; first-run usage where onboarding never created the config dir and the user passed no `--save-dir`.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/801683fe4d6cfe22. Report an issue: GitHub.