Skip to content

feat(main): trigger-watcher — file-based input injection for harness scripts - #24

Merged
JeanBaptisteRenard merged 2 commits into
mainfrom
feat/trigger-watcher
May 31, 2026
Merged

feat(main): trigger-watcher — file-based input injection for harness scripts#24
JeanBaptisteRenard merged 2 commits into
mainfrom
feat/trigger-watcher

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown
Collaborator

Summary

Adds a file-based trigger mechanism so harness scripts can inject keyboard input into any open Switchboard PTY session without requiring Electron IPC.

  • trigger-watcher.js — new module (exports start(ctx)). Watches ~/.switchboard/triggers/ for *.json files, validates shape, optionally waits for the session to go idle, writes command + '\r' into the PTY, and leaves a result file in processed/.
  • main.js — wires trigger-watcher.start() right after startScheduler, passing closures over activeSessions to expose getPtyForSession and isSessionBusy.
  • test/trigger-watcher.test.js — 6 node:test cases: happy path, unknown session, malformed JSON, missing field, idle-wait flip, idle-timeout.
  • .ai/contexts/trigger-watcher.md — context doc for future agents.
  • ~/.skaleet-ai/conventions/how-to/switchboard-trigger.md — harness-author convention doc (outside repo, not in this PR diff).

Contract

Drop a file at ~/.switchboard/triggers/<uuid>.json:

{ "sessionId": "abc-123", "command": "/compact", "wait": "idle" }

Result appears at ~/.switchboard/triggers/processed/<uuid>.result.json:

{ "ok": true, "sessionId": "abc-123", "command": "/compact", "sent_at": "...", "waited_ms": 320 }

Bash one-liner

UUID=$(uuidgen) && \
  echo '{"sessionId":"'"$SID"'","command":"/compact","wait":"idle"}' > ~/.switchboard/triggers/${UUID}.json && \
  sleep 0.5 && cat ~/.switchboard/triggers/processed/${UUID}.result.json

Full switchboard_send bash function in ~/.skaleet-ai/conventions/how-to/switchboard-trigger.md.

Context doc

.ai/contexts/trigger-watcher.md — covers public surface, invariants, non-obvious behaviors (OSC-title busy detection, inFlight dedup), and the change-also checklist.

Test plan

  • task check (lint + tests): 70 pass / 0 fail, 0 new lint errors
  • Happy path: trigger → PTY write → result ok:true
  • Unknown session: result ok:false, no PTY write
  • Malformed JSON: result ok:false, no PTY write
  • Missing command field: result ok:false
  • wait:"idle" — busy flips to idle after 150 ms: write happens, waited_ms >= 100
  • wait:"idle" timeout (200 ms): result ok:false, no PTY write

…scripts

Drop a JSON trigger file into ~/.switchboard/triggers/<uuid>.json to send
a command (e.g. /compact) into any open PTY session.  Optional idle-wait
polls session._cliBusy (the OSC-title spinner flag) before writing, so
the harness can safely call /compact without racing mid-response output.

- trigger-watcher.js: watches triggers dir, processes JSON files, writes
  result to processed/<uuid>.result.json, deletes the trigger on done.
- main.js: wires start() after startScheduler with getPtyForSession and
  isSessionBusy closures over activeSessions.
- test/trigger-watcher.test.js: 6 node:test cases (happy path, unknown
  session, malformed JSON, missing field, idle-wait flip, idle timeout).
- .ai/contexts/trigger-watcher.md: context doc.
- .ai/contexts/README.md: table entry.
- C1: lstatSync size guard — reject trigger files > 64 KB before readFileSync
- C2: lstatSync isFile() — reject symlinks and non-regular files
- W1: SyntaxError retry — wait 50 ms and re-read once before failing on partial writes
- W2: command length cap — reject commands > 4 KB
- W3: control char sanitization — reject \r \n \0 \x1b in command
- W4: concurrency cap — queue triggers when > 8 in-flight; simple semaphore via waitQueue
- W5: session-exit during wait:idle — check getPtyForSession on each poll tick
- W6: tests for C1, C2, W1 retry-then-fail, W2, W3, W4 (12 concurrent), W5, PTY throw, dedup, I4
- I1: atomic result write — write to .tmp then renameSync to final path
- I3: wrap trigger-watcher start() in try/catch in main.js boot path
- I4: NaN guard on parseInt(SWITCHBOARD_TRIGGER_IDLE_TIMEOUT_MS) — fall back to default
@JeanBaptisteRenard
JeanBaptisteRenard merged commit 6196423 into main May 31, 2026
6 checks passed
@JeanBaptisteRenard
JeanBaptisteRenard deleted the feat/trigger-watcher branch May 31, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant