--- name: watch license: MIT description: >- File sentinel that monitors the working directory for changes and marker comments, then auto-triggers appropriate skills. Poll-based via git diff against the last scan commit. Writes intake items for batch processing and routes marker actions through /do. Use for automatic reactions to file changes; do NOT use for one-off inspection or tasks needing human judgment per file. user-invocable: true auto-trigger: false trigger_keywords: - watch - watch files - watch changes - file sentinel - monitor files - watch start - watch stop - watch scan - marker comments - "@citadel" last-updated: 2026-03-29 --- # /watch -- File Sentinel Use when the user wants automatic reactions to file changes or marker comments (`@citadel:`). Do NOT use for one-off file inspection or tasks that need human judgment per file. ## Orientation **Use when:** running a file sentinel that triggers a skill or command automatically when watched paths change. **Don't use when:** monitoring CI status on a PR (use /pr-watch); running a one-shot verification check (use /verify). ## Default execution path (READ FIRST) **`/watch start` does NOT call `CronCreate` by default.** Only pass `--remote` to use Anthropic's routine system, and only after explicit user confirmation. `CronCreate` counts against the **15 routine runs / 24h** cap — at the default 5-minute interval a watch exhausts the quota in under an hour. ### Default flow — `/watch start` (no `--remote` flag) 1. Do Steps 1 and 2 below (check existing watch, determine baseline commit). 2. **Skip Step 3** — do NOT call `CronCreate`. Leave `cronId: null` in state. 3. Write the state file (Step 4) with `status: "watching"`. 4. Output: ``` Watch state created: .planning/watch-state.json Baseline: {commit hash, first 7 chars} To start real-time watching, run in a separate terminal: npm run watch:local For cloud-persistent polling (machine off, user away): /watch start --remote (uses CronCreate, counts against 15/day cap) ``` ### Opt-in routine flow — `/watch start --remote` Only when `--remote` is explicitly passed: 1. Confirm: "This will use `CronCreate`, which counts against your 15 routine runs / 24h quota. At a 5-minute interval this exhausts the quota in under an hour. Continue? (y/N)" 2. On confirmation, run the full Step 1–5 protocol including `CronCreate`. ## Commands | Command | Behavior | |---|---| | `/watch start` | Default: create state, prompt user to run `npm run watch:local` | | `/watch start --remote` | Use `CronCreate` polling (counts against 15/day quota — requires confirmation) | | `/watch start --interval {N}m` | Set poll interval for `--remote` mode (default: 5m) | | `/watch stop` | Stop watching, tear down cron | | `/watch status` | Show watch state, last scan time, pending actions | | `/watch scan` | Run a single scan now (manual trigger) | ## Protocol ### /watch start #### Step 1: Check for existing watch 1. Read `.planning/watch-state.json` if it exists 2. If `status` is `"watching"`: - Show current state: last scan time, interval, pending actions count - Ask: "A watch is already active. Stop it and start a new one?" - If yes: run `/watch stop` first, then continue - If no: abort #### Step 2: Determine baseline commit 1. Run `git rev-parse HEAD` to get the current commit hash 2. If not a git repo: fall back to timestamp-based detection (store current time as `lastScanTime`, skip commit-based diffing) 3. Store this as `lastScanCommit` #### Step 3: Create poll schedule (--remote only) ``` CronCreate: interval: "{N}m" (default: 5m) command: "/watch scan" ``` Save the cron ID in the state file. #### Step 4: Write state file Write `.planning/watch-state.json`: ```json { "status": "watching", "lastScanCommit": "abc1234", "lastScanTime": null, "interval": "5m", "cronId": "{id from step 3 or null}", "pendingActions": {}, "processedMarkers": {}, "stats": { "scansRun": 0, "markersFound": 0, "intakeItemsCreated": 0, "skillsDispatched": 0 } } ``` #### Step 5: Confirm (--remote only) ``` Watch started. Interval: every {N}m Baseline: {commit hash, first 7 chars} State: .planning/watch-state.json ``` --- ### /watch stop 1. Read `.planning/watch-state.json`. If missing or not `"watching"`: "No watch is active." 2. `CronDelete: {cronId}` — if cronId is missing or deletion fails, continue. 3. Update state: `"status": "stopped", "cronId": null`. Preserve all other fields. 4. Output: ``` Watch stopped. Scans completed: {stats.scansRun} Markers found: {stats.markersFound} Intake items created: {stats.intakeItemsCreated} Skills dispatched: {stats.skillsDispatched} ``` --- ### /watch status 1. If `.planning/watch-state.json` missing: "No watch configured. Use `/watch start` to begin." 2. Output state fields: status, lastScanTime, lastScanCommit, interval, pendingActions.length, stats. 3. If `pendingActions` non-empty, list each: `[{action}] {file}:{line} -- {description}` --- ### /watch scan #### Step 1: Load state 1. Read `.planning/watch-state.json` 2. If missing: create default state with `lastScanCommit` from `git rev-parse HEAD` and `status: "watching"`. #### Step 2: Detect changed files **Git mode (primary):** 1. `git diff --name-only {lastScanCommit} HEAD` (committed changes) 2. `git diff --name-only` (unstaged) and `git diff --name-only --cached` (staged) 3. Merge and deduplicate all three lists **Fallback mode (no git):** 1. `find . -newer {timestamp_file} -type f` 2. Exclude `node_modules/`, `.git/`, `.planning/`, `dist/`, `build/` If no files changed: update `lastScanTime` and `stats.scansRun`, exit early. #### Step 3: Scan for marker comments Search changed files for: | Pattern | Languages | |---|---| | `// @citadel: {action} {description}` | JS, TS, Go, Rust, C, Java | | `# @citadel: {action} {description}` | Python, Shell, YAML, Ruby | | `/* @citadel: {action} {description} */` | CSS, multi-line C-style | | `` | HTML, Markdown | **Action-to-skill mapping:** | Action | Skill | |---|---| | `review` | `/review` | | `test` | `/test-gen` | | `fix` | `/systematic-debugging` | | `document` | `/doc-gen` | | `refactor` | `/refactor` | | `todo` | intake item | Unknown actions become intake items with the action preserved as metadata. **Deduplication:** Every marker gets a stable identity hash: sha256 over `{file path}`, `{action}`, and the normalized marker text (trimmed, internal whitespace collapsed), joined with NUL separators and truncated to 16 hex chars. Line numbers are excluded, so the hash survives line shifts. `processedMarkers` is a map keyed by this hash with `{file, action, firstSeen, lastSeen}`; `pendingActions` uses the same hash keys. Both maps are pruned past 500 entries by dropping the oldest `lastSeen`. Markers whose hash is already in `processedMarkers` are skipped. #### Step 4: Classify unmarked changes | File pattern | Auto-action | |---|---| | `*.test.*`, `*.spec.*`, `__tests__/*` | Queue: "run tests" intake item | | `*.md` in `docs/` or project root | Queue: "doc staleness check" intake item | | `src/**/*.ts`, `src/**/*.tsx` | Queue: "changed source" intake item | | `package.json`, `tsconfig.json` | Queue: "config change" intake item (high priority) | #### Step 5: Dispatch markers For each new marker: 1. `/do {action} in {file} at line {line}: {description}` 2. Log dispatch, add to `processedMarkers`, increment `stats.skillsDispatched` **Batch limit:** Dispatch at most 5 per scan. Queue overflow in `pendingActions`. #### Step 6: Write intake items Filename: `watch-{action}-{file slug}-{epoch ms}.md` in `.planning/intake/` ```markdown --- title: "{action} {file}:{line}" status: pending priority: normal target: {file path} source: watch marker_hash: {16-char marker hash} --- Marker comment found at {file}:{line}: `{raw marker line}` {description, if any} ``` Before writing, skip the item if the marker hash is already in `processedMarkers`, or if any existing `.planning/intake/*.md` carries a matching `marker_hash` in its frontmatter. Every new intake item records its `marker_hash` so future scans (and concurrent processes) can detect it. #### Step 7: Update state - `lastScanCommit`: `git rev-parse HEAD` - `lastScanTime`: current ISO timestamp - Increment `stats.scansRun`, `stats.markersFound` - Update `pendingActions` and `processedMarkers` #### Step 8: Report Manual scan: ``` Scan complete. Files changed: {N} Markers found: {new} ({total} total) Actions dispatched: {N} (batch limit: 5) Intake items: {N} written to .planning/intake/ Pending actions: {N} ``` Cron poll: silent. --- ## Integration Points - **Intake pipeline:** Writes to `.planning/intake/` for `/autopilot`. - **Intent router:** Routes markers through `/do` — never invokes skills directly. - **Daemon:** `/daemon` can start a watch alongside a campaign. --- ## Fringe Cases **`.planning/` missing:** Create on first scan. **Not a git repo:** Fall back to timestamp detection; warn once. **No files changed:** Update stats, exit silently. **Unknown action:** Treat as intake item, preserve raw action. **Deleted file:** Skip marker scanning; write intake item noting deletion. **Large diff (100+ files):** Cap at 50 per scan, queue rest. **Binary files:** Skip during marker scanning. **Corrupted state:** Reset to defaults, preserve `processedMarkers` if readable. **CronCreate not available:** Warn and suggest manual `/watch scan`. **Scan overlap:** Scans serialize through a lock directory (`.planning/watch-state.json.lock`, acquired via atomic `mkdir` with ~10 retries at 100ms). Each scan records `scanStartedAt` and `scanPid` in state under the lock, and holds the lock across the state read-modify-write and intake writes. A scan that cannot acquire the lock and finds another scan started under 60 seconds ago logs a skip notice and exits cleanly. A lock older than 30 seconds (by mtime) is treated as stale and removed. **Marker removed:** Stale `processedMarkers` entries age out via the 500-entry oldest-`lastSeen` prune. --- ## Contextual Gates **Disclosure:** "Starting file watch on [paths]. Triggers [skill] on change. Stop with Ctrl+C." **Reversibility:** amber — runs sentinel that triggers other skills on file change; triggered skills may modify files; stop with Ctrl+C and run `/watch stop` **Trust gates:** - Any: start watch and view scan reports - Familiar (5+ sessions): triggered skills run autonomously on file change; novices should use with caution and review dispatched actions ## Quality Gates - Scan completes in under 10 seconds for repos up to 100K lines - No duplicate intake items for the same file and classification - No re-dispatched already-processed markers - Batch limit of 5 dispatches per scan enforced - State file updated atomically at end of scan - Works on Windows, macOS, Linux (Node.js fs + git CLI) - CronCreate failure does not leave watch in inconsistent state ## Exit Protocol - **`/watch start`:** Output confirmation block. No HANDOFF. - **`/watch stop`:** Output stop summary with lifetime stats. - **`/watch scan` (manual):** Output scan report with counts. - **`/watch scan` (cron):** Silent. - **`/watch status`:** Output current state. - **On error:** Clear message with fix. Never leave cron running if state is inconsistent.