Skip to content

Commit 634fb34

Browse files
JonyanDunhclaude
andauthored
feat: rewrite in Node.js for cross-platform support (v1.1.0) (#6)
* feat: rewrite in Node.js for cross-platform support (v1.1.0) Complete rewrite of the Watchdog plugin from Bash + jq + POSIX coreutils to Node.js. Zero shell dependencies — runs natively on Linux, macOS, and native Windows (PowerShell / cmd) with only `node` 18+ required. No more WSL2 requirement for Windows users, no more jq install, no more "bash in PATH" gotcha. ## What changed - **Three thin entry points** (`hooks/stop-hook.js`, `scripts/setup-watchdog.js`, `scripts/stop-watchdog.js`) that all consume shared logic from six `lib/` modules: - `lib/constants.js` — state path pattern, marker tokens, prompt templates - `lib/log.js` — stderr diagnostics - `lib/stdin.js` — cross-platform sync stdin reader - `lib/state.js` — atomic state file lifecycle - `lib/transcript.js` — JSONL parser + current-turn tool extraction - `lib/judge.js` — headless Haiku subprocess + verdict parser - **738 lines** of production JavaScript replacing the combined 287 + 140 + 36 = 463 lines of bash — the extra lines buy full modularization, cross-platform support, and a real test suite. - **53 automated tests** using Node's built-in `node:test` runner. No external test dependencies. The suite exercises: - JSONL transcript parsing (real vs tool_result user turn boundaries, tool_use extraction, malformed-line resilience) - State file lifecycle (atomic writes, merge updates, validation, per-session path keying, parent dir creation) - Verdict parser (FILE_CHANGES substring trap, ambiguous, empty, multi-token) - End-to-end setup-watchdog.js subprocess invocations - End-to-end stop-watchdog.js subprocess invocations - End-to-end stop-hook.js including the owner_session_id recursion guard, ownership claim, max iterations cap, missing transcript, and pure-text turn branches - **CI rewritten** to drop shellcheck (no shell scripts left) and run `node --test` on a 3x3 matrix: `ubuntu-latest`, `macos-latest`, `windows-latest` × Node 18 / 20 / 22. Also dropped `jq` from the jsonlint job in favor of `node -e JSON.parse`. - **All 7 READMEs updated** (English + zh / ja / ko / es / vi / pt) via 6 parallel subagent translations. New Requirements section drops bash/jq, adds node 18+. New Install dependencies subsection has native Windows install commands. Platform support matrix now shows Linux / macOS / Windows all ✅ tested in CI. - **NOTICE updated** with the Node.js rewrite as a new modification entry and the new file list. - **CONTRIBUTING updated** to replace shellcheck references with `node --test` instructions and a description of the test suite. - **plugin.json + marketplace.json bumped to 1.1.0**. Same semantics as 1.0.x: Haiku-classifier convergence detection, tool-call exit precondition, owner_session_id recursion guard, per-session state keyed by TERM_SESSION_ID. The state file format and slash command surface are unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci,docs): use explicit file list for node --test (Node 18/20 glob) Node's --test runner only gained glob pattern support in v21, so on Node 18 and 20 'test/*.test.js' is treated as a literal filename and CI fails with "Could not find 'test/*.test.js'". Fix the CI workflow to list each test file explicitly. Also update all 7 READMEs + CONTRIBUTING.md to show both forms — the Node 22+ glob syntax and the Node 18/20 unquoted (shell- expanded) form. Plus fix a stale "shellcheck → node --test" transition comment in the Plugin Layout tree across all languages to describe the actual CI jobs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 81f663e commit 634fb34

37 files changed

Lines changed: 2134 additions & 735 deletions

.claude-plugin/marketplace.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3-
"name": "claude-code-watchdog",
4-
"description": "Watchdog — watches the agent and re-feeds the same prompt until the task genuinely stops producing file edits. Derived from Anthropic's ralph-loop, Apache 2.0.",
3+
"name": "claude-code-watchdog-nodejs",
4+
"description": "Watchdog (Node.js rewrite) — cross-platform self-referential Claude Code loop with zero bash/jq dependencies. Runs natively on Linux, macOS, and Windows. Apache 2.0, derived from ralph-loop.",
55
"owner": {
66
"name": "Jonyan Dunh",
77
"email": "jonyandunh@outlook.com"
88
},
99
"plugins": [
1010
{
1111
"name": "watchdog",
12-
"description": "A self-referential loop for Claude Code that re-feeds the user's prompt until the task truly stops producing file edits. Uses a headless Haiku classifier to judge convergence, requires the agent to actually call tools before exit (no pure-text 'done' claims), and is hidden from the agent so it cannot cheat. Per-session state keyed by TERM_SESSION_ID. Apache 2.0, derived from ralph-loop.",
13-
"version": "1.0.3",
12+
"description": "Self-referential loop for Claude Code that re-feeds the user's prompt until the task truly stops producing file edits. Cross-platform Node.js implementation with no bash / jq dependencies. Uses a headless Haiku classifier to judge convergence, requires the agent to actually call tools before exit (no pure-text 'done' claims), and is hidden from the agent so it cannot cheat. Per-session state keyed by TERM_SESSION_ID. Apache 2.0, derived from ralph-loop.",
13+
"version": "1.1.0",
1414
"author": {
1515
"name": "Jonyan Dunh",
1616
"email": "jonyandunh@outlook.com"
@@ -26,9 +26,11 @@
2626
"self-referential",
2727
"iteration",
2828
"convergence",
29-
"ralph-loop"
29+
"ralph-loop",
30+
"nodejs",
31+
"cross-platform"
3032
]
3133
}
3234
],
33-
"version": "1.0.3"
35+
"version": "1.1.0"
3436
}

.claude-plugin/plugin.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "watchdog",
3-
"version": "1.0.3",
4-
"description": "Continuous self-referential AI watchdog for interactive iterative development. Runs Claude in a while-true loop with the same prompt until the task stops producing file edits.",
3+
"version": "1.1.0",
4+
"description": "Self-referential loop for Claude Code. Re-feeds the same prompt after every turn until files actually stop changing. Cross-platform Node.js implementation — no bash / jq dependencies, runs natively on Linux, macOS, and Windows.",
55
"author": {
66
"name": "Jonyan Dunh",
77
"email": "jonyandunh@outlook.com"
@@ -17,6 +17,8 @@
1717
"self-referential",
1818
"iteration",
1919
"convergence",
20-
"ralph-loop"
20+
"ralph-loop",
21+
"nodejs",
22+
"cross-platform"
2123
]
2224
}

.github/workflows/ci.yml

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,59 @@ permissions:
1010
contents: read
1111

1212
jobs:
13-
shellcheck:
14-
name: ShellCheck shell scripts
15-
runs-on: ubuntu-latest
13+
nodejs-tests:
14+
name: Node.js tests (${{ matrix.os }}, node ${{ matrix.node }})
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
os: [ubuntu-latest, macos-latest, windows-latest]
19+
node: ['18', '20', '22']
20+
runs-on: ${{ matrix.os }}
1621
steps:
1722
- uses: actions/checkout@v4
18-
- name: Run shellcheck
19-
uses: ludeeus/action-shellcheck@master
23+
- name: Use Node.js ${{ matrix.node }}
24+
uses: actions/setup-node@v4
2025
with:
21-
scandir: "."
22-
severity: warning
23-
env:
24-
SHELLCHECK_OPTS: -e SC2317
26+
node-version: ${{ matrix.node }}
27+
- name: Syntax-check all JavaScript files
28+
shell: bash
29+
run: |
30+
set -e
31+
find . -type f -name '*.js' ! -path './.git/*' -print0 | while IFS= read -r -d '' f; do
32+
node --check "$f"
33+
done
34+
- name: Run the test suite
35+
shell: bash
36+
# Explicit file list — Node's --test glob support only landed in
37+
# v21+. Listing files keeps CI working on Node 18 and 20 (which
38+
# treat 'test/*.test.js' as a literal path).
39+
run: |
40+
node --test \
41+
test/transcript.test.js \
42+
test/state.test.js \
43+
test/judge.test.js \
44+
test/setup.test.js \
45+
test/stop-watchdog.test.js \
46+
test/stop-hook.test.js
2547
2648
jsonlint:
2749
name: Lint JSON manifests
2850
runs-on: ubuntu-latest
2951
steps:
3052
- uses: actions/checkout@v4
31-
- name: Install jq
32-
run: sudo apt-get update && sudo apt-get install -y jq
33-
- name: Validate all JSON files
53+
- name: Validate all JSON files with Node
3454
run: |
3555
set -e
3656
fail=0
3757
while IFS= read -r f; do
38-
if ! jq empty "$f" 2>/tmp/jq-err; then
58+
if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$f" 2>/tmp/json-err; then
3959
echo "::error file=$f::Invalid JSON"
40-
cat /tmp/jq-err
60+
cat /tmp/json-err
4161
fail=1
4262
else
4363
echo "ok: $f"
4464
fi
45-
done < <(find . -type f -name '*.json' ! -path './.git/*')
65+
done < <(find . -type f -name '*.json' ! -path './.git/*' ! -path './node_modules/*')
4666
exit $fail
4767
4868
markdownlint:

CONTRIBUTING.md

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,24 @@ Then `/reload-plugins` inside Claude Code.
4343
claude-code-watchdog/
4444
├── .claude-plugin/ # marketplace + plugin manifests
4545
├── commands/ # slash command definitions (markdown with frontmatter)
46-
├── hooks/ # Stop hook registration + stop-hook.sh (core loop logic)
47-
├── scripts/ # setup-watchdog.sh / stop-watchdog.sh (bash state management)
46+
├── hooks/ # Stop hook registration + stop-hook.js (core loop logic)
47+
├── scripts/ # setup-watchdog.js / stop-watchdog.js entry points
48+
├── lib/ # shared modules (state, transcript, judge, log, stdin, constants)
49+
├── test/ # node:test unit + integration tests with fixtures
4850
├── LICENSE # Apache 2.0
4951
└── NOTICE # Attribution to ralph-loop (required by Apache 2.0 § 4)
5052
```
5153

52-
The heart of the plugin is `hooks/stop-hook.sh`. Read that first before changing anything.
54+
The heart of the plugin is `hooks/stop-hook.js`. Read that first before changing anything — it consumes everything under `lib/`.
5355

5456
## Before you open a PR
5557

56-
1. **Shell scripts must pass `shellcheck`.** CI runs it on every `.sh` file. Install locally (`apt install shellcheck`, `brew install shellcheck`) and fix warnings before pushing.
57-
2. **Line endings must be LF.** The `.gitattributes` file enforces this — do not override.
58-
3. **Do not skip the `NOTICE` file when making derivative changes.** If your change affects how we differ from `ralph-loop`, update the "modifications" list in `NOTICE`.
59-
4. **Do not commit debug logging.** Remove any `WATCHDOG_DEBUG_LOG=...` scaffolding before the PR.
60-
5. **One logical change per PR.** Mixing a bugfix with a refactor makes review harder.
58+
1. **All JavaScript must parse under `node --check`.** CI runs it on every `.js` file. Broken syntax will fail the build.
59+
2. **All tests must pass.** Run `node --test 'test/*.test.js'` locally before pushing. CI runs the full suite on `ubuntu-latest`, `macos-latest`, and `windows-latest` across Node 18 / 20 / 22 — any regression on any matrix entry blocks merge.
60+
3. **Line endings must be LF.** The `.gitattributes` file enforces this — do not override.
61+
4. **Do not skip the `NOTICE` file when making derivative changes.** If your change affects how we differ from `ralph-loop`, update the "modifications" list in `NOTICE`.
62+
5. **Do not commit debug logging.** Remove any `console.log` scaffolding before the PR — `lib/log.js` is the only sanctioned stderr output surface.
63+
6. **One logical change per PR.** Mixing a bugfix with a refactor makes review harder.
6164

6265
## Commit messages
6366

@@ -78,15 +81,40 @@ Common types:
7881

7982
## Testing your change
8083

81-
There is no automated test suite (the project is small and the interesting behavior requires a live Claude Code session). Manual verification steps:
84+
Watchdog ships with 53 automated tests using Node's built-in `node:test` runner — no external test dependencies. Run them from the repo root:
85+
86+
```bash
87+
# Node 22+: glob pattern
88+
node --test 'test/*.test.js'
89+
90+
# Node 18/20: shell expansion (quotes removed) or explicit file list
91+
node --test test/*.test.js
92+
```
93+
94+
Target an individual file:
95+
96+
```bash
97+
node --test test/transcript.test.js
98+
```
99+
100+
The suite covers:
101+
102+
- **`test/transcript.test.js`** — JSONL parser, real-vs-tool_result user turn detection, tool_use extraction
103+
- **`test/state.test.js`** — atomic state file writes, merge updates, validation, per-session path keying
104+
- **`test/judge.test.js`** — verdict parser (FILE_CHANGES substring trap, ambiguous, empty, multi-token)
105+
- **`test/setup.test.js`** — E2E subprocess tests for `scripts/setup-watchdog.js`
106+
- **`test/stop-watchdog.test.js`** — E2E subprocess tests for `scripts/stop-watchdog.js`
107+
- **`test/stop-hook.test.js`** — E2E subprocess tests for `hooks/stop-hook.js` including the recursion guard, ownership claim, max iterations cap, missing transcript, and pure-text turn branches
108+
109+
In addition to the unit/integration suite, you should **also** manually verify your change in a live Claude Code session:
82110

83111
1. Clear any stale state: `rm -f .claude/watchdog.*.local.json`
84112
2. Load the plugin locally (see Quick start above)
85113
3. Run a small iterative task:
86114
```
87115
/watchdog:start "Create tmp/test.md with 'hello world'" --max-iterations 5
88116
```
89-
4. Verify the loop converges (file created, Haiku judges NO_FILE_CHANGES on the next turn, loop exits)
117+
4. Verify the loop converges (file created, Haiku judges `NO_FILE_CHANGES` on the next turn, loop exits)
90118
5. Verify `/watchdog:stop` cleanly removes the state file
91119

92120
## Reporting bugs

NOTICE

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ introduces the following substantive modifications:
4343
- Introduced per-session state files keyed by TERM_SESSION_ID so
4444
concurrent watchdogs in different terminal tabs do not clobber
4545
each other's state. The state file format was also switched from
46-
markdown-with-YAML-frontmatter to JSON, parsed with jq.
46+
markdown-with-YAML-frontmatter to JSON.
4747

4848
- Appended an English verification reminder to every re-fed prompt,
4949
instructing the agent to re-run the verification with real tool
@@ -54,12 +54,38 @@ introduces the following substantive modifications:
5454
mistaken for turn boundaries, causing the current turn's tool_use
5555
blocks to be excluded from the classification prompt.
5656

57+
- Added an owner_session_id recursion guard. The headless Haiku
58+
classifier we spawn is itself a Claude Code session, so its own
59+
Stop hook fires when it ends. TERM_SESSION_ID is inherited by
60+
subprocesses, so the recursive invocation would find and clobber
61+
the main session's state file. The fix records the per-invocation
62+
Claude session_id (from the hook's stdin JSON, which is NOT
63+
inherited) as owner_session_id on first fire, and recursive fires
64+
with a different session_id bail out without touching state.
65+
66+
- Rewrote the entire plugin from Bash + jq + POSIX coreutils to
67+
Node.js (v1.1.0). The new implementation has zero shell or POSIX
68+
dependencies — it runs natively on Linux, macOS, and native
69+
Windows (PowerShell / cmd) with only `node` 18+ required. Shared
70+
logic is extracted into reusable modules under lib/ (constants,
71+
state, transcript, judge, log, stdin), consumed by three thin
72+
entry points (setup-watchdog.js, stop-watchdog.js, stop-hook.js).
73+
The port is covered by 53 tests using Node's built-in node:test
74+
runner (no external test dependencies).
75+
5776
Files significantly modified from their ralph-loop originals include:
5877

59-
- hooks/stop-hook.sh
60-
- scripts/setup-watchdog.sh (originally scripts/setup-ralph-loop.sh)
61-
- scripts/stop-watchdog.sh (originally scripts/cancel-ralph.sh)
78+
- hooks/stop-hook.js (was hooks/stop-hook.sh in ralph-loop
79+
and Watchdog 1.0.x)
80+
- scripts/setup-watchdog.js (originally scripts/setup-ralph-loop.sh)
81+
- scripts/stop-watchdog.js (originally scripts/cancel-ralph.sh)
6282
- commands/start.md (originally commands/ralph-loop.md)
6383
- commands/stop.md (originally commands/cancel-ralph.md)
84+
- lib/constants.js (new in Watchdog 1.1.0)
85+
- lib/state.js (new in Watchdog 1.1.0)
86+
- lib/transcript.js (new in Watchdog 1.1.0)
87+
- lib/judge.js (new in Watchdog 1.1.0)
88+
- lib/log.js (new in Watchdog 1.1.0)
89+
- lib/stdin.js (new in Watchdog 1.1.0)
6490

6591
See each file's header comment for a per-file attribution notice.

0 commit comments

Comments
 (0)