NAI OS

/nai-issue-check — noticing client activity without watching for it

One script, two triggers, one decision: notify or stay quiet. The only path into nai-analysis that never writes anything back to it.

Two things can start this: you typing /nai-issue-check, or an hourly cron firing on its own (7 * * * * — seven minutes past the hour, deliberately off the top-of-hour spike). Both run the exact same steps, because both are just the command calling scripts/issue-bridge/check-activity.mjs with the same cursor file. There is no orchestrator wave here and nothing to dispatch — the whole system is one deterministic script plus one decision about whether to notify.

The script queries nai-analysis’s Supabase directly — the same service-role credential /nai-issue-to-agent already holds — for anything new since a local, gitignored cursor (.notify-cursor.json) was last advanced: comments on issue_comments, decisions on issues, and, if OWNER_USER_ID is configured, brand-new issues created by anyone other than Udo. The cursor is captured as “now” before querying and only advanced after every query succeeds, so a mid-run write is reported next time rather than silently skipped. A missing cursor defaults to “now,” not “the beginning of time” — a first run never dumps all history as if it just happened.

%%{init: {'theme':'neutral', 'flowchart':{'htmlLabels':false,'nodeSpacing':45,'rankSpacing':55}}}%%
flowchart TD
    H1["You type<br/>/nai-issue-check"] --> C
    H2["Hourly cron<br/>7 * * * * (this session)"] --> C

    subgraph L1 ["check-activity.mjs — no orchestrator wave, just a script"]
        C["Read cursor → query → advance cursor<br/>prints OK or ERROR, always"]
    end

    C -->|"select"| DB[("nai-analysis Supabase<br/>issue_comments · issues · profiles")]
    DB -.-> C

    C --> R{"OK {findings, warnings}<br/>or ERROR"}
    R -->|"findings non-empty"| N["PushNotification<br/>one line, everything this run"]
    R -->|"findings empty"| Q["Nothing new — no notification"]
    R -->|"ERROR"| E["Relay the error —<br/>never treated as quiet"]

    classDef orch fill:#dcefe9,stroke:#0f6f63,stroke-width:2px
    classDef tool fill:#f4f1ea,stroke:#8a7a5a
    classDef human fill:#fdf3e0,stroke:#8a5a00,stroke-width:2px
    class C orch
    class DB tool
    class H1,H2,N,Q,E human
The script itself never calls PushNotification — a plain Node process has no access to Claude Code's tool layer. It only prints a status line; the calling layer (the command, or the scheduled routine running the same steps) reads it and decides. One notification per run summarizing everything found, never one per finding, never on an empty result, and never on ERROR — an unknown outcome is not the same as a quiet one.

The boundary here is the plainest in the repo: this path never writes to nai-analysis at all. Compare /nai-issue-to-agent, which holds the same credential but uses it for a single scoped insert after a human confirms — this script only ever selects. The one thing it does write is local: its own cursor file, and the notification itself, which is a Claude Code push to Udo, not a row in anyone's database.

The two triggers exist for a specific, deliberate reason and aren't interchangeable conveniences. The hourly cron runs via CronCreate inside this local session rather than as a cloud routine, because a cloud routine runs in an isolated environment with no access to local files or environment variables — it could not read scripts/issue-bridge/.env. Embedding the Supabase service-role key directly in a cloud routine's stored config instead was considered and rejected: that key bypasses row-level security entirely, and putting it in Anthropic's routine storage is a materially broader exposure than the project's existing local-only, gitignored .env. The accepted trade-off: the hourly check only fires while a session stays open, and the cron auto-expires after 7 days and needs periodic re-arming — /nai-issue-check is what covers the gaps in between.

Next/nai-paper-ingest