Citadel watch
git clone https://github.com/SethGammon/Citadel
T=$(mktemp -d) && git clone --depth=1 https://github.com/SethGammon/Citadel "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/watch" ~/.claude/skills/sethgammon-citadel-watch && rm -rf "$T"
skills/watch/SKILL.md/watch -- File Sentinel
Default execution path (READ FIRST)
does NOT call /watch start
by default. The local runner is
the default. Only pass CronCreate
--remote to use Anthropic's routine system, and only
after explicit user confirmation.
Why:
CronCreate counts against the account-wide 15 routine runs / 24h
cap. At the default 5-minute interval a watch exhausts the quota in under an
hour and pauses every other routine on the account. See
docs/ROUTINE-QUOTA.md.
Default flow — /watch start
(no --remote
flag)
/watch start--remote- Do Steps 1 and 2 below (check existing watch, determine baseline commit).
- Skip Step 3 — do NOT call
. LeaveCronCreate
in the state file.cronId: null - Write the state file (Step 4) with
.status: "watching" - Output (instead of Step 5):
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 It uses filesystem events (not polling), triggers scans on change, and consumes zero Anthropic routine quota. Stop with Ctrl+C. 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
/watch start --remoteOnly when
--remote is explicitly passed:
- Confirm: "This will use
, 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)"CronCreate - On confirmation, run the full Step 1–5 protocol below including Step 3's
call.CronCreate
The rest of the protocol documents the full routine-path flow for reference and for
--remote invocations.
Identity
You are the file sentinel. You detect what changed since the last scan, find marker comments that request specific actions, and dispatch work to the right skill or queue it for batch processing. You do not do the work yourself -- you detect and route.
Orientation
Use
/watch when:
- The user wants automatic reactions to file changes (tests on test edits, doc staleness checks on doc changes, review on marker comments)
- A daemon or autopilot pipeline needs a change-detection feed
- The team uses
marker comments to request actions inline@citadel:
Do NOT use
/watch for:
- One-off file inspection (just read the file)
- Continuous real-time filesystem monitoring (Claude Code sessions are ephemeral)
- Tasks that need human judgment per file (use
directly)/review
Commands
| Command | Behavior |
|---|---|
| Default: create state, prompt user to run (real-time, zero routine cost) |
| Use polling instead (counts against 15/day routine quota — requires confirmation) |
| Set poll interval for mode (default: 5m) |
| Stop watching, tear down cron |
| Show watch state, last scan time, pending actions |
| Run a single scan now (manual trigger) |
Protocol
/watch start
Step 1: Check for existing watch
- Read
if it exists.planning/watch-state.json - If
isstatus
:"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
first, then continue/watch stop - If no: abort
Step 2: Determine baseline commit
- Run
to get the current commit hashgit rev-parse HEAD - If not a git repo: fall back to timestamp-based detection (store current
time as
, skip commit-based diffing)lastScanTime - Store this as
-- the first scan will diff against thislastScanCommit
Step 3: Create poll schedule
Use CronCreate to set up recurring scans:
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:
{ "status": "watching", "lastScanCommit": "abc1234", "lastScanTime": null, "interval": "5m", "cronId": "{id from step 3}", "pendingActions": [], "processedMarkers": [], "stats": { "scansRun": 0, "markersFound": 0, "intakeItemsCreated": 0, "skillsDispatched": 0 } }
Step 5: Confirm
Output:
Watch started. Interval: every {N}m Baseline: {commit hash, first 7 chars} State: .planning/watch-state.json The sentinel will scan for changes every {N} minutes and: - Route @citadel: marker comments to the appropriate skill - Queue unmarked changes as intake items for /autopilot - Auto-trigger test runs when test files change - Flag doc staleness when source files change near docs Use `/watch scan` for an immediate scan. Use `/watch stop` to halt.
/watch stop
- Read
. If it doesn't exist or.planning/watch-state.json
is notstatus
: "No watch is active.""watching" - Delete the cron schedule:
If the cron ID is missing or deletion fails, continue gracefully.CronDelete: {cronId} - Update state file:
Preserve all other fields (stats, lastScanCommit, etc.).{ "status": "stopped", "cronId": null } - Output:
Watch stopped. Scans completed: {stats.scansRun} Markers found: {stats.markersFound} Intake items created: {stats.intakeItemsCreated} Skills dispatched: {stats.skillsDispatched}
/watch status
- Read
. If it doesn't exist: "No watch configured. Use.planning/watch-state.json
to begin."/watch start - Output:
Watch: {status} Last scan: {lastScanTime or "never"} Last commit: {lastScanCommit, first 7 chars} Interval: {interval} Pending actions: {pendingActions.length} Stats: Scans run: {stats.scansRun} Markers found: {stats.markersFound} Intake items: {stats.intakeItemsCreated} Skills dispatched: {stats.skillsDispatched} - If
is non-empty, list each:pendingActionsPending: [{action}] {file}:{line} -- {description}
/watch scan
This is the core detection and dispatch loop. Runs on every poll tick or when invoked manually.
Step 1: Load state
- Read
.planning/watch-state.json - If it doesn't exist: create a default state with
fromlastScanCommit
andgit rev-parse HEAD
. This allowsstatus: "watching"
to work as a standalone one-shot without/watch scan
./watch start
Step 2: Detect changed files
Git mode (primary):
- Run
to get files changed since the last scangit diff --name-only {lastScanCommit} HEAD - Also run
(unstaged) andgit diff --name-only
(staged) to catch uncommitted workgit diff --name-only --cached - Merge and deduplicate all three lists
- Filter out files matching
(git diff handles this automatically for committed changes; for unstaged, use.gitignore
to identify ignored files and exclude them)git ls-files --others --ignored --exclude-standard
Fallback mode (no git):
- Walk the working directory using
find . -newer {timestamp_file} -type f - Exclude
,node_modules/
,.git/
,.planning/
,dist/build/ - This is less precise but functional for non-git projects
If no files changed: update
lastScanTime and stats.scansRun, exit early.
Step 3: Scan for marker comments
For each changed file, read its contents and search for marker patterns:
| Pattern | Languages |
|---|---|
| JS, TS, Go, Rust, C, Java |
| Python, Shell, YAML, Ruby |
| CSS, multi-line C-style |
| HTML, Markdown |
Extract from each match:
: the first word afteraction
(e.g.,@citadel:
,review
,test
)fix
: everything after the action worddescription
: the file pathfile
: the line number where the marker was foundline
Action-to-skill mapping:
| Action | Skill | Description |
|---|---|---|
| | Request a code review of this file or section |
| | Generate tests for this code |
| | Investigate and fix a bug described in the marker |
| | Generate or update documentation |
| | Refactor the marked code |
| intake item | Add to intake queue for batch processing |
Unknown actions are treated as intake items with the action preserved as metadata.
Deduplication: Compare each marker against
processedMarkers in the state
file (stored as "{file}:{line}:{action}" strings). Skip markers that have
already been processed. This prevents re-dispatching the same marker on every
scan. A marker is removed from processedMarkers when the file is modified
again (the line content changed).
Step 4: Classify unmarked changes
For changed files without markers, classify by file type and location:
| File pattern | Auto-action |
|---|---|
, , | Queue: "run tests" intake item |
in or project root | Queue: "doc staleness check" intake item |
, | Queue: "changed source" intake item (informational) |
, | Queue: "config change" intake item (high priority) |
Step 5: Dispatch markers
For each new (non-duplicate) marker:
- Route through
with context:/do/do {action} in {file} at line {line}: {description} - Log the dispatch to the state file
- Add to
processedMarkers - Increment
stats.skillsDispatched
Batch limit: Dispatch at most 5 marker actions per scan. If more exist, queue the remainder in
pendingActions for the next scan. This prevents a
single scan from consuming the entire session context.
Step 6: Write intake items
For each classified change (unmarked files + overflow markers), write an intake item to
.planning/intake/:
Filename:
watch-{timestamp}-{index}.md
--- source: watch priority: {normal|high} created: {ISO timestamp} --- # {brief description} File: {file path} Change type: {new|modified|deleted} Classification: {test change|doc change|source change|config change|marker overflow} {If marker: Action: {action}, Description: {description}} Detected by /watch scan at {ISO timestamp}.
Deduplication: Before writing, check if an intake item already exists for this file with the same classification (glob
.planning/intake/watch-* and
grep for the file path). Skip if a duplicate exists.
Increment
stats.intakeItemsCreated for each new item written.
Step 7: Update state
Update
.planning/watch-state.json:
:lastScanCommit
(current HEAD)git rev-parse HEAD
: current ISO timestamplastScanTime
: incrementstats.scansRun
: add count of new markers found this scanstats.markersFound
: any overflow actions not dispatched this scanpendingActions
: append newly processed markersprocessedMarkers
Step 8: Report
If running interactively (manual
/watch scan), output:
Scan complete. Files changed: {N} Markers found: {new markers} ({total processed} total) Actions dispatched: {N} (batch limit: 5) Intake items: {N} written to .planning/intake/ Pending actions: {N} (will dispatch on next scan)
If running from a cron poll, output nothing (silent operation).
Integration Points
- Intake pipeline: Writes items to
for consumption by.planning/intake/
. Items include file context and classification metadata./autopilot - Intent router: Routes marker actions through
, which handles skill dispatch. Watch never invokes skills directly./do - Daemon:
can start a watch alongside a campaign. The watch feeds intake items that the daemon's campaign can consume./daemon - Session-start hook: The
hook can trigger a scan on session start ifinit-project.js
has.planning/watch-state.json
. This catches changes made between sessions.status: "watching"
Fringe Cases
does not exist:
Create .planning/
.planning/ and .planning/intake/ on first scan. Do not require
/do setup -- watch should be lightweight enough to bootstrap its own state
directory.
Not a git repo: Fall back to timestamp-based change detection. Warn on first scan: "Not a git repo. Using file modification times for change detection. This is less precise and does not respect .gitignore automatically."
No files changed since last scan: Update stats and exit silently. This is the normal case for most polls.
Marker comment has an unknown action: Treat as an intake item with the raw action preserved in metadata. Do not error -- unknown actions may be handled by custom skills the user has installed.
File was deleted between scans: Skip marker scanning for deleted files. Write an intake item noting the deletion if the file was previously tracked.
Very large diff (100+ files): Cap marker scanning at the first 50 changed files per scan. Queue the rest for the next scan. Log: "Large changeset detected ({N} files). Scanning first 50 this cycle."
Binary files in the diff: Skip binary files during marker scanning. Detect via
git diff --numstat
(binary files show - for additions/deletions).
watch-state.json is corrupted or missing required fields: Reset to defaults. Preserve
processedMarkers if readable to avoid
re-dispatching old markers. Log: "Watch state was corrupted. Reset to defaults."
CronCreate not available: Warn: "CronCreate is not available.
/watch start requires session-scoped
scheduling. Use /watch scan for manual one-shot scans instead."
Multiple scans overlap (slow scan + fast interval): The cron interval should be longer than scan duration. If a scan takes longer than expected, log a warning: "Scan took {N}s (interval is {M}m). Consider increasing the interval." The state file's
lastScanTime acts as a soft
lock -- if lastScanTime is within the last 60 seconds, skip the scan.
Marker removed from file: On each scan, check if previously processed markers still exist at their recorded file:line location. If the marker was removed (user addressed the action), remove it from
processedMarkers. This keeps the processed list
from growing unbounded.
Quality Gates
- Scan must complete in under 10 seconds for repos up to 100K lines
- Must not create duplicate intake items for the same file and classification
- Must not re-dispatch markers that have already been processed
- Must respect
(automatic in git mode, manual exclusion list in fallback mode).gitignore - Batch limit of 5 dispatches per scan must be enforced -- never consume the full session context on a single scan
- State file must be updated atomically at the end of each scan, not incrementally during the scan (prevents partial state on crash)
- Must work on Windows, macOS, and Linux (Node.js fs + git CLI, no platform-specific filesystem watchers)
- CronCreate failure must not leave watch in an inconsistent state
Exit Protocol
After /watch start
:
/watch startOutput the confirmation block. No HANDOFF -- the watch runs in the background.
After /watch stop
:
/watch stopOutput the stop summary with lifetime stats.
After /watch scan
(manual):
/watch scanOutput the scan report with counts.
After /watch scan
(cron):
/watch scanSilent. Updates state file only.
After /watch status
:
/watch statusOutput current state. Wait for next command.
On error:
Output a clear message with fix. Never leave cron running if state is inconsistent -- clean up on error.
---HANDOFF--- - Built: skills/watch/SKILL.md -- file sentinel skill with poll-based change detection - Commands: start, stop, status, scan with git diff against stored commit hash - Detection: marker comments (@citadel: action) in 4 comment styles + file classification - Dispatch: markers route through /do (batch limit 5), unmarked changes become intake items - Integration: feeds .planning/intake/ for /autopilot, bridges with /daemon and session-start hook ---