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
- Pass an explicit `--save-dir <path>` so the bundle lands somewhere deterministic and writable.
- Or ensure `~/.config/last30days/` exists and is writable (`mkdir -p ~/.config/last30days`).
- In containers/CI, set HOME to a writable path or mount a volume and point `--save-dir` at it.
- 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
- Standardize on one explicit --save-dir per discovery protocol and use it for all three legs.
- In CI/containers, set HOME or pass --save-dir since the config-dir fallback may not resolve.
- Create the directory up front and confirm writability with a touch before starting leg 1.
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
- Could not write nominations bundle {path}: {exc}
- Could not read {label.lower()} {path}: {exc}
- Could not read {label} file {file_path}: {exc}
- No discovery nominations bundle found. Searched:\n{_searched
- {label} {path} is not valid JSON: {exc}
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/801683fe4d6cfe22.
Report an issue: GitHub.