{"record":{"id":"801683fe4d6cfe22","repo":"mvanhorn/last30days-skill","slug":"no-handoff-location-available-to-write-the-nominat","errorCode":null,"errorMessage":"No handoff location available to write the nominations bundle: pass --save-dir or configure ~/.config/last30days/.","messagePattern":"No handoff location available to write the nominations bundle: pass --save-dir or configure ~/\\.config/last30days/\\.","errorType":"exception","errorClass":"HandoffContractError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/discovery_handoff.py","lineNumber":266,"sourceCode":"    save_dir: str | Path | None = None,\n    config_dir: Path | None = None,\n) -> NominationsBundle:\n    \"\"\"Write the leg-1 nominations bundle and return its parsed form.\n\n    Nomination ids are assigned ``n1, n2, ...`` in pool order. The leg-1\n    invocation context (enrichment source boundary, requested discovery\n    sources, lookback days) rides along so leg 2 resumes with identical\n    settings. ``None`` boundaries are preserved as null - \"no boundary\" and\n    \"empty boundary\" are different contracts. ``source_status`` is the\n    sweep's finalized per-source outcome map (serialized via the same\n    ``schema.to_dict`` round trip every report uses) so degraded coverage\n    survives into legs 2-3; ``mock`` stamps the writing run's provenance.\n    \"\"\"\n    if tier not in _VALID_TIERS:\n        raise ValueError(f\"tier must be one of {_VALID_TIERS}, got {tier!r}\")\n    state_dir = handoff_state_dir(save_dir, config_dir)\n    if state_dir is None:\n        raise HandoffContractError(\n            \"No handoff location available to write the nominations bundle: \"\n            \"pass --save-dir or configure ~/.config/last30days/.\"\n        )\n    bundle_id = secrets.token_hex(8)\n    generated_at = schema._utc_now()\n\n    rows: list[dict[str, Any]] = []\n    nominations: list[BundleNomination] = []\n    for index, entry in enumerate(entries, start=1):\n        nomination_id = f\"n{index}\"\n        sources = sorted({item.source for item in entry.nomination.items})\n        engagement = pipeline._discovery_engagement(entry.nomination.items)\n        rows.append({\n            \"id\": nomination_id,\n            \"cluster_id\": entry.cluster_id,\n            \"heuristic_name\": entry.heuristic_name,\n            \"heuristic_junk\": bool(entry.heuristic_junk),\n            \"sources\": sources,","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/discovery_handoff.py#L248-L284","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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`.","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."],"exampleFix":"# before\npython3 last30days.py \"topic\" --discover --nominate-only   # HOME unset, no config dir\n# after\npython3 last30days.py \"topic\" --discover --nominate-only --save-dir /tmp/discovery-run","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nsave_dir = Path(\"/tmp/discovery-run\")\nsave_dir.mkdir(parents=True, exist_ok=True)  # ensure the store exists BEFORE the run\n# then always invoke every leg with --save-dir str(save_dir)","typeGuard":null,"tryCatchPattern":"from lib import discovery_handoff\nstate = discovery_handoff.handoff_state_dir(save_dir, config_dir)\nif state is None:\n    raise SystemExit(\"no handoff store: pass --save-dir or create ~/.config/last30days/\")","preventionTips":["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."],"tags":["discovery","handoff","filesystem","config","exit-code-2"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}