CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-orders

Order-email triage for NanoClaw as a per-chat overlay plugin: fetches order-related Gmail, maintains the orders SQLite table, flags recent anomalies (cancellations/refunds, overdue deliveries, orders stuck in 'ordered' that never shipped), and ships a cadence companion that runs it on a schedule.

77

Quality

96%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

snooze-orders.pyskills/check-orders/scripts/

#!/usr/bin/env python3
"""Snooze a set of orders past a date, by id, without touching status.

Owner-ack tool for the *genuinely stuck* case (`jbaruch/nanoclaw-orders#63`,
column from `jbaruch/nanoclaw#917`). `ack-orders.py` answers "these
arrived" by transitioning rows to `assumed_delivered`. It cannot answer the
other half of the same owner reply — "this one truly not shipped, all the
rest shipped and delivered" (2026-08-10, the Ragnar Armoury case). Marking
that row `assumed_delivered` records a delivery that never happened;
leaving it `ordered` re-flags it every night.

This script writes `snooze_until` instead, so the row keeps its honest
`status = 'ordered'` while the nightly run stops reporting it. Status,
`flagged`, and `flag_reason` are deliberately left alone: a snooze says
"stop asking", not "this is resolved".

Two readers honour the marker while the window is open:
`compute-stuck-orders.py` (Step 8) drops the row from `stuck_ids`, and
`flag-anomalies.py` (Step 9) suppresses EVERY anomaly rule for it and
unflags it if already flagged. Step 9's half is what makes a snooze hold
for the rows Step 8 never reaches — an `ordered` row whose overdue
`expected_delivery` fires the higher-priority rule, and the `shipped`
rows this script accepts.

Only `ordered`/`shipped` rows are eligible — snoozing is meaningful only
for a live, in-flight order. A `cancelled`/`refunded`/already-terminal row
is left untouched (counted as `not_snoozed`) rather than silently marked.

Inputs:
  - ids on stdin, one per line (leading/trailing whitespace tolerated,
    blank lines ignored)
  - `SNOOZE_UNTIL` env var: the ISO date (`YYYY-MM-DD`) the suppression
    runs until, exclusive — `compute-stuck-orders.py` re-flags ON that
    date. Must parse as an ISO date and be strictly in the future; a past
    or same-day value is rejected rather than written as a no-op snooze
    the owner would believe had taken effect. Assign it to the python3
    process, not to a command earlier in the pipeline:
    `printf '%s\\n' <id> | SNOOZE_UNTIL=<YYYY-MM-DD> python3 snooze-orders.py`

Stdout on success: `{"snoozed": <int>, "not_snoozed": <int>,
"snooze_until": "<date>"}`. `snoozed` counts ids whose row was eligible and
updated; `not_snoozed` counts ids matching no eligible row (stale id, never
imported, or already terminal — diagnostic, not an error).

Unlike the Step 8 reader, this script does NOT tolerate a missing
`snooze_until` column: a reader degrading to "nothing is snoozed" is
harmless, but a writer silently dropping the owner's acknowledgement is
not. An un-migrated database fails loudly with the migration to deploy.

This is an ad-hoc tool invoked outside the nightly flow, in response to an
owner acknowledgement — not a Step in the sequential check-orders run.

Exit codes: 0 success, 1 IO/schema error, 2 bad or missing SNOOZE_UNTIL.
"""

from __future__ import annotations

import json
import os
import re
import sqlite3
import sys
from datetime import date, datetime, timezone

DB_PATH = os.environ.get("ORDERS_DB_PATH", "/workspace/store/messages.db")

# The canonical extended calendar date, the only shape this script writes
# and the only one `compute-stuck-orders.py` honours. `date.fromisoformat`
# additionally accepts the ISO basic form (`20260601`) and week form
# (`2026-W40-1`) and would normalize either into a stored snooze, so the
# shape is enforced before parsing rather than after.
_CANONICAL_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z")


def _resolve_snooze_until() -> str:
    """Read and validate SNOOZE_UNTIL, or exit 2 with an actionable message.

    Returns the `YYYY-MM-DD` string to persist.
    """
    raw = os.environ.get("SNOOZE_UNTIL", "").strip()
    if not raw:
        sys.stderr.write(
            "snooze-orders: SNOOZE_UNTIL is required. Set it to the ISO date "
            "the snooze runs until (exclusive), assigning it to the python3 "
            "process so it survives the pipe, e.g. "
            "`printf '%s\\n' <id> | SNOOZE_UNTIL=<YYYY-MM-DD> "
            "python3 snooze-orders.py`.\n"
        )
        raise SystemExit(2)
    if not _CANONICAL_DATE.match(raw):
        sys.stderr.write(
            f"snooze-orders: SNOOZE_UNTIL={raw!r} is not a YYYY-MM-DD date. "
            f"The ISO basic (20260601) and week (2026-W40-1) forms are "
            f"rejected on purpose — the stuck detector honours the canonical "
            f"form alone. Use e.g. SNOOZE_UNTIL=2026-09-01.\n"
        )
        raise SystemExit(2)
    try:
        parsed = date.fromisoformat(raw)
    except ValueError:
        # Right shape, impossible date (2026-13-45).
        sys.stderr.write(
            f"snooze-orders: SNOOZE_UNTIL={raw!r} is not a real calendar "
            f"date. Use e.g. SNOOZE_UNTIL=2026-09-01.\n"
        )
        raise SystemExit(2) from None
    if parsed <= date.today():
        sys.stderr.write(
            f"snooze-orders: SNOOZE_UNTIL={raw} is not in the future "
            f"(today is {date.today().isoformat()}). A past or same-day "
            f"value would suppress nothing — pick a later date.\n"
        )
        raise SystemExit(2)
    return parsed.isoformat()


def main() -> int:
    snooze_until = _resolve_snooze_until()

    try:
        raw = sys.stdin.read()
    except OSError as exc:
        sys.stderr.write(
            f"snooze-orders: failed to read order ids from stdin: {exc}. "
            f"Pipe the ids to snooze, one per line, e.g. "
            f"`printf '%s\\n' <id1> <id2> | python3 snooze-orders.py`.\n"
        )
        return 1
    ids = [line.strip() for line in raw.splitlines() if line.strip()]
    if not ids:
        # No ids to snooze — degenerate but legal.
        json.dump(
            {"snoozed": 0, "not_snoozed": 0, "snooze_until": snooze_until},
            sys.stdout,
        )
        sys.stdout.write("\n")
        return 0

    now_iso = datetime.now(timezone.utc).isoformat()
    snoozed = 0
    not_snoozed = 0
    conn = None
    try:
        conn = sqlite3.connect(DB_PATH)
        # Single transaction so the snooze set is all-or-nothing per
        # invocation — a transient failure mid-batch leaves the table
        # untouched and the agent can safely re-run with the same ids.
        with conn:
            for order_id in ids:
                cur = conn.execute(
                    """
                    UPDATE orders
                       SET snooze_until = ?,
                           last_updated = ?
                     WHERE id = ?
                       AND status IN ('ordered', 'shipped')
                    """,
                    (snooze_until, now_iso, order_id),
                )
                # rowcount is 1 when the id existed AND was in a snoozable
                # status; 0 when the id is missing or already terminal.
                if cur.rowcount > 0:
                    snoozed += 1
                else:
                    not_snoozed += 1
        json.dump(
            {
                "snoozed": snoozed,
                "not_snoozed": not_snoozed,
                "snooze_until": snooze_until,
            },
            sys.stdout,
        )
        sys.stdout.write("\n")
        return 0
    except sqlite3.Error as exc:
        sys.stderr.write(
            f"snooze-orders: SQLite error against {DB_PATH}: {exc}. "
            f"Verify the database file exists, is writable, and the orders "
            f"table carries the snooze_until column (added by the "
            f"orchestrator's state-018 migration, jbaruch/nanoclaw#917) — "
            f"deploy that migration if this run reports no such column.\n"
        )
        return 1
    finally:
        if conn is not None:
            conn.close()


if __name__ == "__main__":
    sys.exit(main())

README.md

tile.json