claws-code 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/commands/claws-auto.md +90 -0
  2. package/.claude/commands/claws-bin.md +28 -0
  3. package/.claude/commands/claws-cleanup.md +28 -0
  4. package/.claude/commands/claws-do.md +82 -0
  5. package/.claude/commands/claws-fix.md +40 -0
  6. package/.claude/commands/claws-goal.md +111 -0
  7. package/.claude/commands/claws-help.md +54 -0
  8. package/.claude/commands/claws-plan.md +103 -0
  9. package/.claude/commands/claws-report.md +29 -0
  10. package/.claude/commands/claws-status.md +37 -0
  11. package/.claude/commands/claws-update.md +32 -0
  12. package/.claude/commands/claws.md +64 -0
  13. package/.claude/rules/claws-default-behavior.md +76 -0
  14. package/.claude/settings.json +112 -0
  15. package/.claude/settings.local.json +19 -0
  16. package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
  17. package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
  18. package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
  19. package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
  20. package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
  21. package/CHANGELOG.md +1949 -0
  22. package/LICENSE +21 -0
  23. package/README.md +420 -0
  24. package/bin/cli.js +84 -0
  25. package/cli.js +223 -0
  26. package/docs/ARCHITECTURE.md +511 -0
  27. package/docs/event-protocol.md +588 -0
  28. package/docs/features.md +562 -0
  29. package/docs/guide.md +891 -0
  30. package/docs/index.html +716 -0
  31. package/docs/protocol.md +323 -0
  32. package/extension/.vscodeignore +15 -0
  33. package/extension/CHANGELOG.md +1906 -0
  34. package/extension/LICENSE +21 -0
  35. package/extension/README.md +137 -0
  36. package/extension/docs/features.md +424 -0
  37. package/extension/docs/protocol.md +197 -0
  38. package/extension/esbuild.mjs +25 -0
  39. package/extension/icon.png +0 -0
  40. package/extension/native/.metadata.json +10 -0
  41. package/extension/native/node-pty/LICENSE +69 -0
  42. package/extension/native/node-pty/README.md +165 -0
  43. package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
  44. package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
  45. package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
  46. package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
  47. package/extension/native/node-pty/lib/index.js +52 -0
  48. package/extension/native/node-pty/lib/index.js.map +1 -0
  49. package/extension/native/node-pty/lib/interfaces.js +7 -0
  50. package/extension/native/node-pty/lib/interfaces.js.map +1 -0
  51. package/extension/native/node-pty/lib/shared/conout.js +11 -0
  52. package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
  53. package/extension/native/node-pty/lib/terminal.js +190 -0
  54. package/extension/native/node-pty/lib/terminal.js.map +1 -0
  55. package/extension/native/node-pty/lib/types.js +7 -0
  56. package/extension/native/node-pty/lib/types.js.map +1 -0
  57. package/extension/native/node-pty/lib/unixTerminal.js +346 -0
  58. package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
  59. package/extension/native/node-pty/lib/utils.js +39 -0
  60. package/extension/native/node-pty/lib/utils.js.map +1 -0
  61. package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
  62. package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
  63. package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
  64. package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
  65. package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
  66. package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
  67. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  68. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
  69. package/extension/native/node-pty/package.json +64 -0
  70. package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  71. package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  72. package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  73. package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  74. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  75. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  76. package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  77. package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  78. package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
  79. package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
  80. package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
  81. package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  82. package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  83. package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  84. package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  85. package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
  86. package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
  87. package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
  88. package/extension/package-lock.json +605 -0
  89. package/extension/package.json +343 -0
  90. package/extension/scripts/bundle-native.mjs +104 -0
  91. package/extension/scripts/deploy-dev.mjs +60 -0
  92. package/extension/src/ansi-strip.ts +52 -0
  93. package/extension/src/backends/vscode/claws-pty.ts +483 -0
  94. package/extension/src/backends/vscode/status-bar.ts +99 -0
  95. package/extension/src/backends/vscode/vscode-backend.ts +282 -0
  96. package/extension/src/capture-store.ts +125 -0
  97. package/extension/src/event-log.ts +629 -0
  98. package/extension/src/event-schemas.ts +478 -0
  99. package/extension/src/extension.js +492 -0
  100. package/extension/src/extension.ts +873 -0
  101. package/extension/src/lifecycle-engine.ts +60 -0
  102. package/extension/src/lifecycle-rules.ts +171 -0
  103. package/extension/src/lifecycle-store.ts +506 -0
  104. package/extension/src/peer-registry.ts +176 -0
  105. package/extension/src/pipeline-registry.ts +82 -0
  106. package/extension/src/platform.ts +64 -0
  107. package/extension/src/protocol.ts +532 -0
  108. package/extension/src/server-config.ts +98 -0
  109. package/extension/src/server.ts +2210 -0
  110. package/extension/src/task-registry.ts +51 -0
  111. package/extension/src/terminal-backend.ts +211 -0
  112. package/extension/src/terminal-manager.ts +395 -0
  113. package/extension/src/topic-registry.ts +70 -0
  114. package/extension/src/topic-utils.ts +46 -0
  115. package/extension/src/transport.ts +45 -0
  116. package/extension/src/uninstall-cleanup.ts +232 -0
  117. package/extension/src/wave-registry.ts +314 -0
  118. package/extension/src/websocket-transport.ts +153 -0
  119. package/extension/tsconfig.json +23 -0
  120. package/lib/capabilities.js +145 -0
  121. package/lib/dry-run.js +43 -0
  122. package/lib/install.js +1018 -0
  123. package/lib/mcp-setup.js +92 -0
  124. package/lib/platform.js +240 -0
  125. package/lib/preflight.js +152 -0
  126. package/lib/shell-hook.js +343 -0
  127. package/lib/uninstall.js +162 -0
  128. package/lib/verify.js +166 -0
  129. package/mcp_server.js +3529 -0
  130. package/package.json +48 -0
  131. package/rules/claws-default-behavior.md +72 -0
  132. package/scripts/_helpers/atomic-file.mjs +137 -0
  133. package/scripts/_helpers/fix-repair.js +64 -0
  134. package/scripts/_helpers/json-safe.mjs +218 -0
  135. package/scripts/bump-version.sh +84 -0
  136. package/scripts/codegen/gen-docs.mjs +61 -0
  137. package/scripts/codegen/gen-json-schema.mjs +62 -0
  138. package/scripts/codegen/gen-mcp-tools.mjs +358 -0
  139. package/scripts/codegen/gen-types.mjs +172 -0
  140. package/scripts/codegen/index.mjs +42 -0
  141. package/scripts/dev-hooks/check-extension-dirs.js +77 -0
  142. package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
  143. package/scripts/dev-hooks/check-stale-main.js +55 -0
  144. package/scripts/dev-hooks/check-tag-pushed.js +51 -0
  145. package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
  146. package/scripts/dev-vsix-install.sh +60 -0
  147. package/scripts/fix.sh +702 -0
  148. package/scripts/gen-client-types.mjs +81 -0
  149. package/scripts/git-hooks/pre-commit +31 -0
  150. package/scripts/hooks/lifecycle-state.js +61 -0
  151. package/scripts/hooks/package.json +4 -0
  152. package/scripts/hooks/post-tool-use-claws.js +292 -0
  153. package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
  154. package/scripts/hooks/pre-tool-use-claws.js +206 -0
  155. package/scripts/hooks/session-start-claws.js +97 -0
  156. package/scripts/hooks/stop-claws.js +88 -0
  157. package/scripts/inject-claude-md.js +205 -0
  158. package/scripts/inject-dev-hooks.js +96 -0
  159. package/scripts/inject-global-claude-md.js +140 -0
  160. package/scripts/inject-settings-hooks.js +370 -0
  161. package/scripts/install.ps1 +146 -0
  162. package/scripts/install.sh +1729 -0
  163. package/scripts/monitor-arm-watch.js +155 -0
  164. package/scripts/rebuild-node-pty.sh +245 -0
  165. package/scripts/report.sh +232 -0
  166. package/scripts/shell-hook.fish +164 -0
  167. package/scripts/shell-hook.ps1 +33 -0
  168. package/scripts/shell-hook.sh +232 -0
  169. package/scripts/stream-events.js +399 -0
  170. package/scripts/terminal-wrapper.sh +36 -0
  171. package/scripts/test-enforcement.sh +132 -0
  172. package/scripts/test-install.sh +174 -0
  173. package/scripts/test-installer-parity.sh +135 -0
  174. package/scripts/test-template-enforcement.sh +76 -0
  175. package/scripts/uninstall.sh +143 -0
  176. package/scripts/update.sh +337 -0
  177. package/scripts/verify-release.sh +323 -0
  178. package/scripts/verify-wrapped.sh +194 -0
  179. package/templates/CLAUDE.global.md +135 -0
  180. package/templates/CLAUDE.project.md +37 -0
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+ // Claws stream-events sidecar.
3
+ //
4
+ // Modes:
5
+ // Default (no --wait): holds ONE persistent connection, registers as a claws/2 peer,
6
+ // subscribes to a topic pattern, and prints every server-push frame to stdout as
7
+ // a single line of JSON. Designed to be spawned via Bash run_in_background and
8
+ // consumed by the Monitor tool — each push frame becomes one notification.
9
+ // --wait <uuid>: connects, sends hello, subscribes to system.worker.completed and
10
+ // system.terminal.closed with fromCursor='0000:0' (atomic historical replay + live),
11
+ // exits 0 when a push frame with matching correlation_id arrives, 2 on socket close
12
+ // before match, 3 on timeout. No awk/grep — server-side topic filter only.
13
+ //
14
+ // --wait flags:
15
+ // --keep-alive-on <termId> activates in-process rearm. When the inner timer fires,
16
+ // run a 3-check decision: (1) system.worker.completed in
17
+ // events.log? → exit 0. (2) terminal closed/terminated in
18
+ // events.log? → exit 0. (3) eventsSeen(termId, staleMs)?
19
+ // → rearm. Otherwise → exit 2 (truly stuck).
20
+ // --stale-threshold <ms> liveness window for eventsSeen check (default 120000).
21
+ // --rearm-cycle <ms> interval between rearm checks (default = --timeout-ms).
22
+ //
23
+ // Env:
24
+ // CLAWS_SOCKET override socket path (both modes; used by tests)
25
+ // CLAWS_TOPIC subscribe pattern (default '**') [default mode only]
26
+ // CLAWS_PEER_NAME peer label (default 'orchestrator-stream') [default mode only]
27
+ // CLAWS_ROLE 'orchestrator' | 'worker' | 'observer' (default 'observer') [default mode only]
28
+ // CLAWS_DEBUG '1' or 'true' → emit one structured JSON line to stderr per decision
29
+ // point in the rearm loop (Check 1, Check 2, Check 3, rearm, exit).
30
+ // Additive only — does NOT change exit codes or default stdout output.
31
+ //
32
+ // Default-mode output lines (stdout, one JSON per line):
33
+ // {"type":"sidecar.connected", "socket":"...", "ts":"..."}
34
+ // {"type":"sidecar.hello.ack", "peerId":"p7", ...}
35
+ // {"type":"sidecar.subscribed", "topic":"**", "subscriptionId":"s3", ...}
36
+ // {"type":"event", "push":"message", "topic":"...", "from":"...", "payload":{...}}
37
+ // {"type":"sidecar.error", "error":"..."}
38
+ // {"type":"sidecar.closed", "ts":"..."}
39
+ //
40
+ // Wait-mode output on match (stdout, exactly one JSON line):
41
+ // {"topic":"system.terminal.closed","payload":{...}}
42
+
43
+ 'use strict';
44
+ const net = require('net');
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+
48
+ // Walk up from startDir looking for a .claws/ directory (win32 workspace detection).
49
+ function _findWorkspaceRootWin(startDir) {
50
+ let dir = startDir;
51
+ while (dir) {
52
+ try { if (fs.statSync(path.join(dir, '.claws')).isDirectory()) return dir; } catch {}
53
+ const parent = path.dirname(dir);
54
+ if (parent === dir) break;
55
+ dir = parent;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function findSocket() {
61
+ if (process.env.CLAWS_SOCKET) return process.env.CLAWS_SOCKET;
62
+
63
+ if (process.platform === 'win32') {
64
+ // Named pipes are kernel objects — derive name from workspace root hash.
65
+ // Same algorithm as extension/src/transport.ts getServerEndpoint().
66
+ const workspaceRoot = process.env.CLAWS_WORKSPACE_ROOT
67
+ || _findWorkspaceRootWin(process.cwd())
68
+ || _findWorkspaceRootWin(path.dirname(__dirname))
69
+ || process.cwd();
70
+ const hash = require('crypto')
71
+ .createHash('sha256')
72
+ .update(workspaceRoot.toLowerCase())
73
+ .digest('hex')
74
+ .slice(0, 8);
75
+ return `\\\\.\\pipe\\claws-${hash}`;
76
+ }
77
+
78
+ // Mac / Linux: walk up the directory tree looking for .claws/claws.sock.
79
+ for (const start of [process.cwd(), __dirname]) {
80
+ let dir = start;
81
+ while (dir && dir !== '/' && dir !== path.dirname(dir)) {
82
+ const c = path.join(dir, '.claws', 'claws.sock');
83
+ try { if (fs.statSync(c).isSocket()) return c; } catch { /* */ }
84
+ dir = path.dirname(dir);
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ function emit(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
91
+
92
+ // ── Arg parsing ───────────────────────────────────────────────────────────────
93
+ const _argv = process.argv.slice(2);
94
+ let _waitCorrId = null;
95
+ let _waitFlagSeen = false;
96
+ let _waitTimeoutMs = 600000;
97
+ let _hasAutoSidecar = false;
98
+ let _keepAliveTermId = null;
99
+ let _staleThresholdMs = 120000;
100
+ let _rearmCycleMs = null;
101
+
102
+ for (let i = 0; i < _argv.length; i++) {
103
+ if (_argv[i] === '--auto-sidecar') {
104
+ _hasAutoSidecar = true;
105
+ } else if (_argv[i] === '--wait') {
106
+ _waitFlagSeen = true;
107
+ i++;
108
+ _waitCorrId = (_argv[i] !== undefined) ? _argv[i] : null;
109
+ } else if (_argv[i] === '--timeout-ms') {
110
+ i++;
111
+ _waitTimeoutMs = (_argv[i] !== undefined) ? Number(_argv[i]) : NaN;
112
+ } else if (_argv[i] === '--keep-alive-on') {
113
+ i++;
114
+ _keepAliveTermId = (_argv[i] !== undefined) ? String(_argv[i]) : null;
115
+ } else if (_argv[i] === '--stale-threshold') {
116
+ i++;
117
+ _staleThresholdMs = (_argv[i] !== undefined) ? Number(_argv[i]) : NaN;
118
+ } else if (_argv[i] === '--rearm-cycle') {
119
+ i++;
120
+ _rearmCycleMs = (_argv[i] !== undefined) ? Number(_argv[i]) : NaN;
121
+ }
122
+ }
123
+
124
+ if (_rearmCycleMs === null) _rearmCycleMs = _waitTimeoutMs;
125
+
126
+ if (_hasAutoSidecar && _waitFlagSeen) {
127
+ process.stderr.write('stream-events.js: --wait and --auto-sidecar are mutually exclusive\n');
128
+ process.exit(1);
129
+ }
130
+
131
+ // ── Wait mode ─────────────────────────────────────────────────────────────────
132
+ if (_waitFlagSeen) {
133
+ if (_waitCorrId === null || !/^[0-9a-f-]{36}$/.test(_waitCorrId)) {
134
+ process.stderr.write('stream-events.js: --wait requires a valid UUID (36 hex/dash chars)\n');
135
+ process.exit(1);
136
+ }
137
+ if (!Number.isInteger(_waitTimeoutMs) || _waitTimeoutMs <= 0) {
138
+ process.stderr.write('stream-events.js: --timeout-ms must be a positive integer\n');
139
+ process.exit(1);
140
+ }
141
+
142
+ const _wSockPath = findSocket();
143
+ if (!_wSockPath) {
144
+ process.stderr.write('stream-events.js --wait: cannot connect to claws.sock\n');
145
+ process.exit(1);
146
+ }
147
+
148
+ let _wBuf = '';
149
+ let _wMatched = false;
150
+ const _wCorrId = _waitCorrId;
151
+
152
+ const _debug = process.env.CLAWS_DEBUG === '1' || process.env.CLAWS_DEBUG === 'true';
153
+ function dbg(obj) { if (_debug) process.stderr.write(JSON.stringify(obj) + '\n'); }
154
+
155
+ // ── Helper: scan events.log for a matching completed/terminated event ──────────
156
+ // Returns true if events.log contains a line matching topic + (corrId or terminal_id).
157
+ function eventsLogContains({ topic, corrId, terminal_id }) {
158
+ try {
159
+ const sockDir = path.dirname(_wSockPath);
160
+ const evLog = path.join(sockDir, 'events.log');
161
+ if (!fs.existsSync(evLog)) return false;
162
+ const stat = fs.statSync(evLog);
163
+ const readSz = Math.min(stat.size, 512 * 1024);
164
+ const fd = fs.openSync(evLog, 'r');
165
+ const buf = Buffer.alloc(readSz);
166
+ fs.readSync(fd, buf, 0, readSz, Math.max(0, stat.size - readSz));
167
+ fs.closeSync(fd);
168
+ const topicStr = `"topic":"${topic}"`;
169
+ const corrStr = corrId ? `"correlation_id":"${corrId}"` : null;
170
+ const termStr = terminal_id ? `"terminal_id":"${terminal_id}"` : null;
171
+ for (const line of buf.toString('utf8').split('\n').reverse()) {
172
+ if (!line.includes(topicStr)) continue;
173
+ if (corrStr && line.includes(corrStr)) return true;
174
+ if (termStr && line.includes(termStr)) return true;
175
+ }
176
+ } catch { /* swallow */ }
177
+ return false;
178
+ }
179
+
180
+ // ── Helper: scan events.log for any recent event belonging to termId ──────────
181
+ // Checks both snake_case ("terminal_id") and camelCase ("terminalId") fields
182
+ // so it catches system.worker.* (snake_case) and vehicle.* (camelCase) events.
183
+ // Returns true if any matching line has sentAt (Unix ms) within withinMs of now.
184
+ function eventsSeen(termId, withinMs) {
185
+ try {
186
+ const sockDir = path.dirname(_wSockPath);
187
+ const evLog = path.join(sockDir, 'events.log');
188
+ if (!fs.existsSync(evLog)) return false;
189
+ const stat = fs.statSync(evLog);
190
+ const readSz = Math.min(stat.size, 512 * 1024);
191
+ const fd = fs.openSync(evLog, 'r');
192
+ const buf = Buffer.alloc(readSz);
193
+ fs.readSync(fd, buf, 0, readSz, Math.max(0, stat.size - readSz));
194
+ fs.closeSync(fd);
195
+ const now = Date.now();
196
+ const matchA = `"terminal_id":"${termId}"`;
197
+ const matchB = `"terminalId":"${termId}"`;
198
+ for (const line of buf.toString('utf8').split('\n').reverse()) {
199
+ if (!line.includes(matchA) && !line.includes(matchB)) continue;
200
+ try {
201
+ const ev = JSON.parse(line);
202
+ if (typeof ev.sentAt === 'number' && (now - ev.sentAt) < withinMs) return true;
203
+ } catch { /* malformed line — skip */ }
204
+ }
205
+ } catch { /* swallow */ }
206
+ return false;
207
+ }
208
+
209
+ // ── Rearm decision loop (fires when inner timer expires) ──────────────────────
210
+ function rearmDecisionLoop() {
211
+ const _now = Date.now();
212
+
213
+ // Check 1: completion event already persisted in events.log? (race: Monitor armed after done)
214
+ const _c1Matched = eventsLogContains({ topic: 'system.worker.completed', corrId: _wCorrId });
215
+ dbg({ check: 1, event: 'completion-scan', corrId: _wCorrId, matched: _c1Matched, matchedAt: _c1Matched ? _now : null, now: _now });
216
+ if (_c1Matched) {
217
+ process.stderr.write(`stream-events.js --wait: matched (raced) — system.worker.completed in events.log\n`);
218
+ dbg({ event: 'exit', code: 0, reason: 'check1-completed', corrId: _wCorrId, now: _now });
219
+ clearTimeout(_wTimer);
220
+ try { _wSock.destroy(); } catch {}
221
+ process.exit(0);
222
+ }
223
+
224
+ // Check 2: termination — corrId-only matching for both system.terminal.closed and
225
+ // system.worker.terminated. terminal_id is session-local: VS Code resets the
226
+ // counter on extension reload and recycles integers as terminals open/close.
227
+ // events.log is globally append-only, so any prior-session terminated event
228
+ // with the same numeric terminal_id would false-positive here. correlation_id
229
+ // is a UUID — globally unique, collision-free across sessions and reloads.
230
+ if (_keepAliveTermId) {
231
+ const closedByCorrId = eventsLogContains({ topic: 'system.terminal.closed', corrId: _wCorrId });
232
+ const terminatedByCorrId = eventsLogContains({ topic: 'system.worker.terminated', corrId: _wCorrId });
233
+ dbg({ check: 2, event: 'termination-scan', corrId: _wCorrId, closedByCorrId, terminatedByCorrId, now: _now });
234
+ if (closedByCorrId || terminatedByCorrId) {
235
+ const which = closedByCorrId ? 'system.terminal.closed(corrId)' : 'system.worker.terminated(corrId)';
236
+ process.stderr.write(`stream-events.js --wait: matched (raced) — ${which} in events.log\n`);
237
+ dbg({ event: 'exit', code: 0, reason: 'check2-termination', corrId: _wCorrId, now: _now });
238
+ clearTimeout(_wTimer);
239
+ try { _wSock.destroy(); } catch {}
240
+ process.exit(0);
241
+ }
242
+ }
243
+
244
+ // Check 3: events.log recency scan — is the terminal still producing bus events?
245
+ if (_keepAliveTermId) {
246
+ const alive = eventsSeen(_keepAliveTermId, _staleThresholdMs);
247
+ dbg({ check: 3, event: 'liveness-scan', termId: _keepAliveTermId, alive, staleThresholdMs: _staleThresholdMs, now: _now });
248
+ if (alive) {
249
+ dbg({ event: 'rearm', reason: 'alive', now: _now });
250
+ process.stderr.write(`stream-events.js --wait: rearming — terminal ${_keepAliveTermId} active in events.log within ${_staleThresholdMs}ms\n`);
251
+ _wTimer = setTimeout(rearmDecisionLoop, _rearmCycleMs);
252
+ return;
253
+ }
254
+ dbg({ event: 'exit', code: 2, reason: 'no-events-for-terminal', corrId: _wCorrId, now: _now });
255
+ process.stderr.write(`stream-events.js --wait: exit stuck — no events for terminal ${_keepAliveTermId} within ${_staleThresholdMs}ms\n`);
256
+ try { _wSock.destroy(); } catch {}
257
+ process.exit(2);
258
+ }
259
+
260
+ // No --keep-alive-on provided: original timeout behavior (backwards compat)
261
+ dbg({ event: 'rearm', reason: 'no-keep-alive', now: _now });
262
+ process.stderr.write(`stream-events.js --wait: timeout waiting for close event (correlation_id=${_wCorrId})\n`);
263
+ process.exit(3);
264
+ }
265
+
266
+ let _wTimer = setTimeout(rearmDecisionLoop, _keepAliveTermId ? _rearmCycleMs : _waitTimeoutMs);
267
+
268
+ const _wSock = net.createConnection(_wSockPath);
269
+
270
+ _wSock.on('error', (e) => {
271
+ clearTimeout(_wTimer);
272
+ if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {
273
+ process.stderr.write('stream-events.js --wait: cannot connect to claws.sock\n');
274
+ process.exit(1);
275
+ }
276
+ process.stderr.write('stream-events.js --wait: socket closed before close event\n');
277
+ process.exit(2);
278
+ });
279
+
280
+ _wSock.on('connect', () => {
281
+ // Bug-13 observability: log hello-send time so arm-race investigations can compare
282
+ // this timestamp against the L2 30s window start (spawn time in mcp_server.js).
283
+ process.stderr.write(`stream-events.js --wait: hello sent | corrId=${_wCorrId} | t=${new Date().toISOString()}\n`);
284
+ _wSock.write(JSON.stringify({ id: 1, cmd: 'hello', protocol: 'claws/2', role: 'observer', peerName: 'wait-mode', monitorCorrelationId: _wCorrId }) + '\n');
285
+ });
286
+
287
+ _wSock.on('data', (d) => {
288
+ _wBuf += d.toString('utf8');
289
+ let nl;
290
+ while ((nl = _wBuf.indexOf('\n')) !== -1) {
291
+ const line = _wBuf.slice(0, nl);
292
+ _wBuf = _wBuf.slice(nl + 1);
293
+ if (!line.trim()) continue;
294
+ let msg;
295
+ try { msg = JSON.parse(line); }
296
+ catch (_) {
297
+ process.stderr.write(`stream-events.js --wait: malformed event: ${line.slice(0, 200)}\n`);
298
+ continue;
299
+ }
300
+
301
+ const rid = msg.rid != null ? msg.rid : msg.id;
302
+
303
+ if (rid === 1 && msg.peerId) {
304
+ // Hello ack — subscribe to both terminal-state topics with full historical replay.
305
+ // fromCursor:'0000:0' makes the server replay matching events from the event log
306
+ // before delivering live pushes, closing the subscribe-before-drain race gap.
307
+ _wSock.write(JSON.stringify({ id: 2, cmd: 'subscribe', topic: 'system.worker.completed', fromCursor: '0000:0' }) + '\n');
308
+ _wSock.write(JSON.stringify({ id: 3, cmd: 'subscribe', topic: 'system.terminal.closed', fromCursor: '0000:0' }) + '\n');
309
+ continue;
310
+ }
311
+ if (rid === 2 || rid === 3) continue; // subscribe acks — no action needed
312
+
313
+ // Push frames (both replayed historical and live events arrive the same way)
314
+ if (msg.push === 'message' &&
315
+ (msg.topic === 'system.worker.completed' || msg.topic === 'system.terminal.closed') &&
316
+ msg.payload != null && msg.payload.correlation_id === _wCorrId) {
317
+ _wMatched = true;
318
+ process.stdout.write(JSON.stringify({ topic: msg.topic, payload: msg.payload }) + '\n');
319
+ clearTimeout(_wTimer);
320
+ _wSock.destroy();
321
+ process.exit(0);
322
+ }
323
+ }
324
+ });
325
+
326
+ _wSock.on('close', () => {
327
+ if (!_wMatched) {
328
+ clearTimeout(_wTimer);
329
+ process.stderr.write('stream-events.js --wait: socket closed before close event\n');
330
+ process.exit(2);
331
+ }
332
+ });
333
+
334
+ process.on('SIGTERM', () => { clearTimeout(_wTimer); try { _wSock.destroy(); } catch {} process.exit(143); });
335
+ process.on('SIGINT', () => { clearTimeout(_wTimer); try { _wSock.destroy(); } catch {} process.exit(130); });
336
+ process.stdout.on('error', (e) => { if (e.code === 'EPIPE') { clearTimeout(_wTimer); process.exit(141); } });
337
+
338
+ } else {
339
+ // ── Default mode (unchanged) ──────────────────────────────────────────────────
340
+ const SOCK = findSocket();
341
+ const TOPIC = process.env.CLAWS_TOPIC || '**';
342
+ const PEER_NAME = process.env.CLAWS_PEER_NAME || 'orchestrator-stream';
343
+ const ROLE = process.env.CLAWS_ROLE || 'observer';
344
+
345
+ if (!SOCK) {
346
+ emit({ type: 'sidecar.error', error: 'no .claws/claws.sock found' });
347
+ process.exit(1);
348
+ }
349
+
350
+ let buf = '';
351
+ const sock = net.createConnection(SOCK);
352
+
353
+ sock.on('connect', () => {
354
+ emit({ type: 'sidecar.connected', socket: SOCK, role: ROLE, ts: new Date().toISOString() });
355
+ sock.write(JSON.stringify({ id: 1, cmd: 'hello', protocol: 'claws/2', role: ROLE, peerName: PEER_NAME }) + '\n');
356
+ });
357
+
358
+ sock.on('data', (d) => {
359
+ buf += d.toString('utf8');
360
+ let nl;
361
+ while ((nl = buf.indexOf('\n')) !== -1) {
362
+ const line = buf.slice(0, nl);
363
+ buf = buf.slice(nl + 1);
364
+ if (!line.trim()) continue;
365
+ let msg;
366
+ try { msg = JSON.parse(line); }
367
+ catch (e) { emit({ type: 'sidecar.parse-error', line, error: e.message }); continue; }
368
+
369
+ if (msg.rid === 1 && msg.peerId) {
370
+ emit({ type: 'sidecar.hello.ack', peerId: msg.peerId, ts: new Date().toISOString() });
371
+ sock.write(JSON.stringify({ id: 2, cmd: 'subscribe', topic: TOPIC }) + '\n');
372
+ continue;
373
+ }
374
+ if (msg.rid === 2) {
375
+ emit({ type: 'sidecar.subscribed', topic: TOPIC, subscriptionId: msg.subscriptionId, ok: msg.ok, ts: new Date().toISOString() });
376
+ continue;
377
+ }
378
+ if (msg.push) {
379
+ emit({ type: 'event', ...msg, recvTs: new Date().toISOString() });
380
+ continue;
381
+ }
382
+ emit({ type: 'sidecar.unknown', msg });
383
+ }
384
+ });
385
+
386
+ sock.on('error', (e) => {
387
+ emit({ type: 'sidecar.error', error: e.message });
388
+ process.exit(1);
389
+ });
390
+
391
+ sock.on('close', () => {
392
+ emit({ type: 'sidecar.closed', ts: new Date().toISOString() });
393
+ process.exit(0);
394
+ });
395
+
396
+ const shutdown = () => { try { sock.end(); } catch {} process.exit(0); };
397
+ process.on('SIGTERM', shutdown);
398
+ process.on('SIGINT', shutdown);
399
+ }
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # Claws terminal wrapper.
3
+ # Exec-replaces itself with script(1) so every byte that flows through the
4
+ # pty is logged to CLAWS_TERM_LOG. The Claws extension (or any orchestrator)
5
+ # tails that log to read what happened in this terminal — including TUI
6
+ # sessions like claude, vim, less, top that are opaque to shell integration.
7
+ #
8
+ # Set CLAWS_TERM_LOG in the extension's createTerminal env. Fallback
9
+ # derives a path from the PID so the wrapper never fails silently.
10
+
11
+ set -e
12
+
13
+ if [ -z "${CLAWS_TERM_LOG:-}" ]; then
14
+ CLAWS_TERM_LOG="${PWD}/.claws/terminals/claws-$$.log"
15
+ fi
16
+
17
+ mkdir -p "$(dirname "$CLAWS_TERM_LOG")"
18
+ : > "$CLAWS_TERM_LOG"
19
+
20
+ # Mark ourselves as a wrapped session so any process inside can detect it.
21
+ export CLAWS_WRAPPED=1
22
+
23
+ SHELL_BIN="${SHELL:-/bin/zsh}"
24
+
25
+ # Do NOT use script's -F flag. -F flushes after every write, which splits
26
+ # Ink-based TUI renderers (Claude Code, etc.) into corrupted partial frames.
27
+ # Default buffering produces a clean terminal at the cost of ~1-2s log delay.
28
+
29
+ # Detect BSD (macOS) vs GNU/Linux script(1) — different argument order.
30
+ if script --version 2>&1 | grep -qi "util-linux"; then
31
+ # Linux: script -q -c "command" outfile
32
+ exec script -q -c "$SHELL_BIN -il" "$CLAWS_TERM_LOG"
33
+ else
34
+ # macOS/BSD: script -q outfile command
35
+ exec script -q "$CLAWS_TERM_LOG" "$SHELL_BIN" -il
36
+ fi
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # Integration test for Claws behavioral injection enforcement pipeline.
3
+ # Tests all four stages: project CLAUDE.md, global CLAUDE.md, hooks, session-start.
4
+ # Usage: bash scripts/test-enforcement.sh
5
+ # Exit 0 = all tests pass. Exit 1 = one or more tests failed.
6
+
7
+ set -eo pipefail
8
+ INSTALL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
9
+ PASS=0; FAIL=0
10
+
11
+ pass() { printf " \033[32m✓\033[0m %s\n" "$*"; PASS=$((PASS+1)); }
12
+ fail() { printf " \033[31m✗\033[0m %s\n" "$*"; FAIL=$((FAIL+1)); }
13
+ header() { printf "\n\033[1m── %s ──\033[0m\n" "$*"; }
14
+
15
+ TMPDIR_TEST="$(mktemp -d)"
16
+ trap 'rm -rf "$TMPDIR_TEST"' EXIT
17
+
18
+ # ── Test 1: inject-claude-md.js writes imperative content ─────────────────
19
+ header "Test 1: inject-claude-md.js"
20
+ TEST_PROJECT="$TMPDIR_TEST/test-project"
21
+ mkdir -p "$TEST_PROJECT"
22
+ node "$INSTALL_DIR/scripts/inject-claude-md.js" "$TEST_PROJECT" >/dev/null 2>&1
23
+
24
+ if [ -f "$TEST_PROJECT/CLAUDE.md" ]; then pass "CLAUDE.md created"; else fail "CLAUDE.md not created"; fi
25
+
26
+ if grep -q "MUST\|MANDATORY\|ALWAYS\|NEVER" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null; then
27
+ pass "CLAUDE.md contains imperative language (MUST/ALWAYS/NEVER)"
28
+ else
29
+ fail "CLAUDE.md missing imperative language — advisory content only"
30
+ fi
31
+
32
+ if grep -q "CLAWS:BEGIN" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null; then
33
+ pass "CLAUDE.md has CLAWS:BEGIN sentinel"
34
+ else
35
+ fail "CLAUDE.md missing CLAWS:BEGIN sentinel"
36
+ fi
37
+
38
+ if grep -q "claws_create\|claws_send" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null; then
39
+ pass "CLAUDE.md lists MCP tools"
40
+ else
41
+ fail "CLAUDE.md missing MCP tool list"
42
+ fi
43
+
44
+ # Idempotency: run again and verify no duplicate sentinel
45
+ node "$INSTALL_DIR/scripts/inject-claude-md.js" "$TEST_PROJECT" >/dev/null 2>&1
46
+ SENTINEL_COUNT=$(grep -c "CLAWS:BEGIN" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null || echo 0)
47
+ if [ "$SENTINEL_COUNT" -eq 1 ]; then
48
+ pass "inject-claude-md.js is idempotent (sentinel count=1 after 2 runs)"
49
+ else
50
+ fail "inject-claude-md.js not idempotent (sentinel count=$SENTINEL_COUNT after 2 runs)"
51
+ fi
52
+
53
+ # ── Test 2: inject-global-claude-md.js dry-run ────────────────────────────
54
+ header "Test 2: inject-global-claude-md.js"
55
+ DRY_OUT=$(node "$INSTALL_DIR/scripts/inject-global-claude-md.js" --dry-run 2>&1)
56
+
57
+ if echo "$DRY_OUT" | grep -q "CLAWS-GLOBAL:BEGIN v1"; then
58
+ pass "inject-global-claude-md.js dry-run emits CLAWS-GLOBAL:BEGIN v1"
59
+ else
60
+ fail "inject-global-claude-md.js dry-run missing CLAWS-GLOBAL:BEGIN v1"
61
+ fi
62
+
63
+ if echo "$DRY_OUT" | grep -q "MUST\|ALWAYS\|NEVER"; then
64
+ pass "inject-global-claude-md.js dry-run contains imperative language"
65
+ else
66
+ fail "inject-global-claude-md.js dry-run missing imperative language"
67
+ fi
68
+
69
+ # ── Test 3: inject-settings-hooks.js dry-run ──────────────────────────────
70
+ header "Test 3: inject-settings-hooks.js"
71
+ HOOKS_DRY=$(node "$INSTALL_DIR/scripts/inject-settings-hooks.js" "$INSTALL_DIR/.claws-bin" --dry-run 2>&1)
72
+
73
+ if echo "$HOOKS_DRY" | grep -q "SessionStart"; then
74
+ pass "inject-settings-hooks.js dry-run includes SessionStart hook"
75
+ else
76
+ fail "inject-settings-hooks.js dry-run missing SessionStart hook"
77
+ fi
78
+
79
+ if echo "$HOOKS_DRY" | grep -q "PreToolUse\|Bash"; then
80
+ pass "inject-settings-hooks.js dry-run includes PreToolUse hook"
81
+ else
82
+ fail "inject-settings-hooks.js dry-run missing PreToolUse hook"
83
+ fi
84
+
85
+ if echo "$HOOKS_DRY" | grep -q '"_source".*claws\|claws.*_source'; then
86
+ pass "inject-settings-hooks.js tags hooks with _source:claws"
87
+ else
88
+ fail "inject-settings-hooks.js missing _source:claws tag"
89
+ fi
90
+
91
+ # ── Test 4: session-start-claws.js emits lifecycle reminder ───────────────
92
+ header "Test 4: session-start-claws.js"
93
+ FAKE_PROJECT="$TMPDIR_TEST/fake-claws-project"
94
+ mkdir -p "$FAKE_PROJECT/.claws"
95
+ touch "$FAKE_PROJECT/.claws/claws.sock"
96
+
97
+ HOOK_OUT=$(echo "{\"cwd\":\"$FAKE_PROJECT\"}" | node "$INSTALL_DIR/.claws-bin/hooks/session-start-claws.js" 2>&1)
98
+
99
+ if echo "$HOOK_OUT" | grep -q "MANDATORY"; then
100
+ pass "session-start-claws.js emits MANDATORY reminder when socket present"
101
+ else
102
+ fail "session-start-claws.js missing MANDATORY reminder"
103
+ fi
104
+
105
+ if echo "$HOOK_OUT" | grep -q "claws_create\|boot sequence"; then
106
+ pass "session-start-claws.js includes boot sequence or claws_create"
107
+ else
108
+ fail "session-start-claws.js missing boot sequence reference"
109
+ fi
110
+
111
+ NO_SOCK_OUT=$(echo "{\"cwd\":\"$TMPDIR_TEST\"}" | node "$INSTALL_DIR/.claws-bin/hooks/session-start-claws.js" 2>&1)
112
+ if [ -z "$NO_SOCK_OUT" ]; then
113
+ pass "session-start-claws.js silent when no socket present"
114
+ else
115
+ fail "session-start-claws.js emitted output when no socket (should be silent)"
116
+ fi
117
+
118
+ # ── Test 5: hook scripts have correct exit codes ───────────────────────────
119
+ header "Test 5: hook exit codes"
120
+ echo '{}' | node "$INSTALL_DIR/.claws-bin/hooks/pre-tool-use-claws.js" >/dev/null 2>&1 && pass "pre-tool-use-claws.js exits 0 on empty input" || fail "pre-tool-use-claws.js non-zero exit on empty input"
121
+ echo '{}' | node "$INSTALL_DIR/.claws-bin/hooks/stop-claws.js" >/dev/null 2>&1 && pass "stop-claws.js exits 0 on empty input" || fail "stop-claws.js non-zero exit on empty input"
122
+
123
+ # ── Summary ────────────────────────────────────────────────────────────────
124
+ printf "\n\033[1m── Results ──\033[0m\n"
125
+ printf " Passed: \033[32m%d\033[0m Failed: \033[31m%d\033[0m Total: %d\n" "$PASS" "$FAIL" "$((PASS+FAIL))"
126
+
127
+ if [ "$FAIL" -gt 0 ]; then
128
+ printf "\n\033[31mSome tests failed.\033[0m\n"
129
+ exit 1
130
+ else
131
+ printf "\n\033[32mAll tests passed.\033[0m\n"
132
+ fi