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,206 @@
1
+ #!/usr/bin/env node
2
+ // Claws PreToolUse hook — Bash long-running enforcement.
3
+ //
4
+ // Default mode: long-running Bash patterns hard-block via exit 2 + stderr.
5
+ // STRICT=1: deny via permissionDecision JSON (cleaner inline reason).
6
+ //
7
+ // CLAWS_STRICT=1 (env var, set in user shell or settings.json env block):
8
+ // long-running Bash patterns return permissionDecision:"deny" via the
9
+ // PreToolUse hookSpecificOutput. Claude Code blocks the tool call and shows
10
+ // the reason — which tells the model exactly what to use instead.
11
+ //
12
+ // Lifecycle gate moved server-side in v0.6.5 — removed from this hook.
13
+ //
14
+ // SAFETY CONTRACT (v0.7.3): this hook MUST NEVER block, crash, or exit
15
+ // non-zero EXCEPT when intentionally denying via permissionDecision. Any
16
+ // internal error → silent exit 0. Avoids the "non-blocking status code"
17
+ // error spam that breaks user workflows.
18
+ 'use strict';
19
+
20
+ // M-24: gate error handlers on CLAWS_DEBUG — when CLAWS_DEBUG=1, errors
21
+ // propagate visibly for debugging instead of being silently swallowed.
22
+ if (!process.env.CLAWS_DEBUG) {
23
+ process.on('uncaughtException', () => { try { process.exit(0); } catch {} });
24
+ process.on('unhandledRejection', () => { try { process.exit(0); } catch {} });
25
+ }
26
+
27
+ // M-13: 5-second self-kill safety timer — hook can never hang the parent process.
28
+ setTimeout(() => { process.exit(0); }, 5000).unref();
29
+
30
+ let input = '';
31
+ // M-13: single try block for both 'data' and 'end' — fail together or not at all.
32
+ try {
33
+ process.stdin.on('data', d => { input += d; });
34
+ process.stdin.on('end', () => {
35
+ try {
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+
39
+ // Patterns that almost always indicate orchestration work — long-running
40
+ // servers, watchers, or background processes — where a wrapped Claws
41
+ // terminal is strictly better than a fire-and-forget Bash call.
42
+ // Conservative list: every entry should be unambiguously long-running
43
+ // so strict mode never blocks ordinary commands like `ls`, `git status`,
44
+ // or one-shot builds.
45
+ const LONG_RUNNING_PATTERNS = [
46
+ /\bnpm (run )?(start|dev|serve|watch)\b/i,
47
+ /\byarn (start|dev|serve|watch)\b/i,
48
+ /\bpnpm (run )?(start|dev|serve|watch)\b/i,
49
+ /\bbun (run )?(start|dev|serve|watch)\b/i,
50
+ /\bnode\b.*\bserver\b/i,
51
+ /\bpython\b.*\bserver\b/i,
52
+ /\b(uvicorn|gunicorn|hypercorn)\b/i,
53
+ /\bflask run\b/i,
54
+ /\brails (server|s)\b/i,
55
+ /\bcargo (watch|run)\b/i,
56
+ /\bgo run\b/i,
57
+ /\bmake (run|serve|start|dev)\b/i,
58
+ /\bnodemon\b/i,
59
+ /\bnohup\b/i,
60
+ // Always-block patterns: interactive/long-lived by nature
61
+ /\bclaude(?!\s+-p\b)(?!\s+--print\b)(\s|$)/, // claude without -p/--print (interactive TUI)
62
+ /\bpython\s+-m\b/i, // python -m <module> (often servers/long-running)
63
+ /\bvite\b(?!\s+build\b)/i, // vite dev server (not vite build)
64
+ /\bwebpack(-dev-server|.*--watch)\b/i, // webpack-dev-server or webpack --watch
65
+ /\bnode\b.*\bserver\.js\b/i, // node server.js
66
+ ];
67
+
68
+ // BUG-16: argv[0] allowlist — LONG_RUNNING_PATTERNS only run when the
69
+ // command name (first token, basename) is a known long-running candidate.
70
+ // Prevents false positives when keywords appear in arguments, not commands
71
+ // (e.g. `grep "node server"` or `cat node-server-config.json`).
72
+ const LONG_RUNNING_ARGV0 = new Set([
73
+ 'npm', 'yarn', 'pnpm', 'bun', 'bunx', 'npx',
74
+ 'node', 'python', 'python3',
75
+ 'uvicorn', 'gunicorn', 'hypercorn', 'flask',
76
+ 'rails', 'cargo', 'go', 'make',
77
+ 'nodemon', 'nohup', 'claude',
78
+ 'vite', 'webpack', 'webpack-dev-server',
79
+ ]);
80
+
81
+ const STRICT = process.env.CLAWS_STRICT === '1';
82
+
83
+ let data = {};
84
+ try { data = JSON.parse(input); } catch { process.exit(0); return; }
85
+
86
+ const toolName = data.tool_name || '';
87
+ const cwd = data.cwd || process.cwd();
88
+
89
+ // Only act if Claws socket is present in the cwd
90
+ const socketPath = path.join(cwd, '.claws', 'claws.sock');
91
+ if (!fs.existsSync(socketPath)) { process.exit(0); return; }
92
+
93
+ // --- Edit/Write guard: block direct edits to mcp_server.js from orchestrator ---
94
+ if (toolName === 'Edit' || toolName === 'Write') {
95
+ const filePath = (data.tool_input && data.tool_input.file_path) || '';
96
+ if (/mcp_server\.js$/.test(filePath)) {
97
+ // BUG-27: workers spawned by claws_worker/fleet/dispatch_subworker carry
98
+ // CLAWS_WORKER=1 — allow them to edit mcp_server.js directly.
99
+ if (process.env.CLAWS_WORKER === '1') { process.exit(0); return; }
100
+ try {
101
+ process.stderr.write(
102
+ `[claws] Direct edits to mcp_server.js from orchestrator are forbidden.\n` +
103
+ ` Dispatch a worker via claws_worker instead.\n`
104
+ );
105
+ } catch {}
106
+ process.exit(2);
107
+ return;
108
+ }
109
+ }
110
+
111
+ // --- MCP spawn-class gate: enforce Monitor arm after grace period ---
112
+ // claws_create / claws_worker / claws_fleet / claws_dispatch_subworker all
113
+ // spawn terminals; requiring an active Monitor prevents silent orphans.
114
+ // BUG-28: 5 s grace window (was 60 s) — tight enough to catch any spawn call
115
+ // after the SessionStart reminder has been processed. Grace state lives in
116
+ // /tmp keyed by a hash of cwd so it survives hook restarts within a session.
117
+ const SPAWN_CLASS = /^mcp__claws__(claws_create|claws_worker|claws_fleet|claws_dispatch_subworker)$/;
118
+ if (SPAWN_CLASS.test(toolName)) {
119
+ const cwdKey = Buffer.from(cwd).toString('base64').replace(/[+/=]/g, '_').slice(0, 12);
120
+ const graceFile = `/tmp/claws-pretooluse-grace-${cwdKey}`;
121
+ let enforceNow = false;
122
+ try {
123
+ if (!fs.existsSync(graceFile)) {
124
+ fs.writeFileSync(graceFile, String(Date.now()), 'utf8');
125
+ } else {
126
+ const ts = parseInt(fs.readFileSync(graceFile, 'utf8').trim(), 10);
127
+ enforceNow = (Date.now() - ts) > 5000;
128
+ }
129
+ } catch { /* grace check failure → never block */ }
130
+
131
+ if (enforceNow) {
132
+ const { spawnSync } = require('child_process');
133
+ // Wave C (Task #63): accept canonical bus-stream sidecar as satisfier.
134
+ // stream-events.js is auto-spawned by SessionStart and never goes idle
135
+ // (constant heartbeat traffic) so it is never SIGURG'd. tail -F is kept
136
+ // as a deprecated fallback for one release. See ARCHITECTURE.md P9 + A1.
137
+ const pgSidecar = spawnSync('pgrep', ['-f', 'stream-events\\.js'], { stdio: 'ignore' });
138
+ const pgTail = spawnSync('pgrep', ['-f', 'tail.*\\.claws/events\\.log'], { stdio: 'ignore' });
139
+ if (pgSidecar.status !== 0 && pgTail.status !== 0) {
140
+ try {
141
+ process.stdout.write(JSON.stringify({
142
+ hookSpecificOutput: {
143
+ hookEventName: 'PreToolUse',
144
+ permissionDecision: 'deny',
145
+ permissionDecisionReason:
146
+ `Monitor not armed. The canonical satisfier is the stream-events.js sidecar ` +
147
+ `(auto-spawned by SessionStart hook). If missing, arm it with: ` +
148
+ `Monitor(command="node <claws>/scripts/stream-events.js | grep --line-buffered ...", ...). ` +
149
+ `(Per ARCHITECTURE.md P9 — tail -F is deprecated as Monitor satisfier.)`,
150
+ },
151
+ }) + '\n');
152
+ } catch {}
153
+ process.exit(0);
154
+ return;
155
+ }
156
+ }
157
+ }
158
+
159
+ // --- Bash long-running guard ---
160
+ if (toolName === 'Bash') {
161
+ const cmd = (data.tool_input && data.tool_input.command) || '';
162
+ // BUG-16: extract argv[0] (basename of first token) and gate pattern
163
+ // matching on the argv0 allowlist — prevents false positives from
164
+ // keywords appearing inside arguments rather than as the command name.
165
+ const argv0 = path.basename((cmd.trim().split(/\s+/)[0] || ''));
166
+ const match = LONG_RUNNING_ARGV0.has(argv0) && LONG_RUNNING_PATTERNS.find(p => p.test(cmd));
167
+ if (match) {
168
+ const msg = [
169
+ `[claws] Bash command matches a long-running pattern.`,
170
+ `Run this in a visible, monitorable Claws terminal instead:`,
171
+ ``,
172
+ ` 1. claws_create({ name: "<slug>", wrapped: true })`,
173
+ ` 2. claws_send({ id: <N>, text: ${JSON.stringify(cmd)} })`,
174
+ ` 3. claws_read_log({ id: <N>, lines: 50 }) — to observe`,
175
+ ` 4. claws_close({ id: <N> }) — when done`,
176
+ ``,
177
+ `Or use claws_worker for AI-driven tasks.`,
178
+ ].join('\n');
179
+ if (STRICT) {
180
+ // STRICT mode: deny via permissionDecision JSON on stdout (M-16)
181
+ try {
182
+ process.stdout.write(JSON.stringify({
183
+ hookSpecificOutput: {
184
+ hookEventName: 'PreToolUse',
185
+ permissionDecision: 'deny',
186
+ permissionDecisionReason: msg,
187
+ },
188
+ }) + '\n');
189
+ } catch {}
190
+ process.exit(0);
191
+ return;
192
+ }
193
+ // Default: hard-block via exit 2 + stderr
194
+ try { process.stderr.write(msg + '\n'); } catch {}
195
+ process.exit(2);
196
+ return;
197
+ }
198
+ }
199
+ process.exit(0);
200
+ } catch {
201
+ process.exit(0);
202
+ }
203
+ });
204
+ } catch {
205
+ process.exit(0);
206
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // Claws SessionStart hook — detects Claws socket and emits lifecycle reminder.
3
+ //
4
+ // SAFETY CONTRACT: this hook MUST NEVER block, crash, or exit non-zero.
5
+ // Any internal error → silent exit 0. Hooks are advisory; failing loud
6
+ // blocks the user's tool calls and creates the very "hook errors" we're
7
+ // trying to eliminate. v0.7.3 hardening.
8
+ 'use strict';
9
+
10
+ // M-24: gate error handlers on CLAWS_DEBUG — when CLAWS_DEBUG=1, errors
11
+ // propagate visibly for debugging instead of being silently swallowed.
12
+ if (!process.env.CLAWS_DEBUG) {
13
+ process.on('uncaughtException', () => { try { process.exit(0); } catch {} });
14
+ process.on('unhandledRejection', () => { try { process.exit(0); } catch {} });
15
+ }
16
+
17
+ // M-13: 5-second self-kill safety timer — hook can never hang the parent process.
18
+ // .unref() so the timer doesn't prevent normal early exit.
19
+ setTimeout(() => { process.exit(0); }, 5000).unref();
20
+
21
+ let input = '';
22
+ // M-13: single try block wrapping both 'data' and 'end' listeners — if either
23
+ // registration throws (pathological stdin state), both fail together cleanly.
24
+ try {
25
+ process.stdin.on('data', d => { input += d; });
26
+ process.stdin.on('end', () => {
27
+ try {
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+
31
+ let cwd = process.cwd();
32
+ try {
33
+ const parsed = JSON.parse(input);
34
+ if (parsed.cwd) cwd = parsed.cwd;
35
+ } catch { /* use process.cwd() */ }
36
+
37
+ const socketPath = path.join(cwd, '.claws', 'claws.sock');
38
+ if (!fs.existsSync(socketPath)) { process.exit(0); return; }
39
+
40
+ // events.log: bus push events from the sidecar are piped here so the
41
+ // orchestrator can arm a Monitor on it and receive real-time notifications.
42
+ const logPath = path.join(cwd, '.claws', 'events.log');
43
+
44
+ // Spawn stream-events.js sidecar daemon if not already running (idempotent).
45
+ // SIM2B-P2b: include socket path in pgrep pattern so per-project sidecars are
46
+ // distinct — a stale sidecar from a different project won't block this session.
47
+ // pgrep exit 0 = found (running), non-zero = not found.
48
+ try {
49
+ const { spawnSync, spawn } = require('child_process');
50
+ const sidecarPath = path.join(__dirname, '..', 'stream-events.js');
51
+ if (fs.existsSync(sidecarPath)) {
52
+ // Escape regex metacharacters in the socket path for the pgrep pattern.
53
+ const escapedSocket = socketPath.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
54
+ const pg = spawnSync('pgrep', ['-f', `stream-events\\.js.*--auto-sidecar.*${escapedSocket}`], { stdio: 'ignore' });
55
+ if (pg.status !== 0) {
56
+ // Pre-create events.log so 'tail -F' has a file to watch immediately
57
+ // (sidecar writes the first event only after the first publish arrives).
58
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
59
+ try { fs.writeFileSync(logPath, '', { flag: 'a' }); } catch {}
60
+ let logFd = null;
61
+ try { logFd = fs.openSync(logPath, 'a'); } catch {}
62
+ const sidecar = spawn(process.execPath, [sidecarPath, '--auto-sidecar', socketPath], {
63
+ detached: true,
64
+ stdio: ['ignore', logFd !== null ? logFd : 'ignore', 'ignore'],
65
+ cwd,
66
+ });
67
+ sidecar.unref();
68
+ // Close the fd in the parent — the child has its own copy after fork.
69
+ if (logFd !== null) { try { fs.closeSync(logFd); } catch {} }
70
+ }
71
+ }
72
+ } catch { /* sidecar spawn failure must never block the hook */ }
73
+
74
+ // BUG-29: lifecycle-bound Monitor description — include session start timestamp
75
+ // so stale Monitors from prior sessions are visually distinguishable.
76
+ // Format: "claws bus | sess=<ISO-hour>" — add plan slug once lifecycle plan is active.
77
+ const sessTs = new Date().toISOString().slice(0, 13); // "2026-05-02T04"
78
+
79
+ const reminder = [
80
+ '## Claws active in this project',
81
+ '',
82
+ `Bus events stream to ${logPath}. Server enforces lifecycle (sess=${sessTs}).`,
83
+ '',
84
+ 'Spawn-class MCP tools (claws_create / claws_worker / claws_fleet / claws_dispatch_subworker)',
85
+ 'require an active Monitor on the bus stream — server refuses spawn without it.',
86
+ 'See docs/ENFORCEMENT.md for the architectural contract.',
87
+ ].join('\n');
88
+
89
+ try { process.stdout.write(JSON.stringify({ type: 'system', content: reminder }) + '\n'); } catch {}
90
+ process.exit(0);
91
+ } catch {
92
+ process.exit(0);
93
+ }
94
+ });
95
+ } catch {
96
+ process.exit(0);
97
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // Claws Stop hook — enforce CLEANUP and REFLECT before session ends.
3
+ //
4
+ // SAFETY CONTRACT (v0.7.3): never crash, never block, never exit non-zero.
5
+ // Hooks are advisory; failure to load lifecycle-state.js (e.g. file missing
6
+ // after partial install) must not surface as "hook error" in Claude Code.
7
+ 'use strict';
8
+
9
+ // M-24: gate error handlers on CLAWS_DEBUG — when CLAWS_DEBUG=1, errors
10
+ // propagate visibly for debugging instead of being silently swallowed.
11
+ if (!process.env.CLAWS_DEBUG) {
12
+ process.on('uncaughtException', () => { try { process.exit(0); } catch {} });
13
+ process.on('unhandledRejection', () => { try { process.exit(0); } catch {} });
14
+ }
15
+
16
+ // M-13: 5-second self-kill safety timer — hook can never hang the parent process.
17
+ setTimeout(() => { process.exit(0); }, 5000).unref();
18
+
19
+ let input = '';
20
+ // M-13: single try block for both 'data' and 'end' — fail together or not at all.
21
+ try {
22
+ process.stdin.on('data', d => { input += d; });
23
+ process.stdin.on('end', () => {
24
+ try {
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ // LH-9: lifecycle-state is no longer read by the Stop hook. The
28
+ // extension's TTL watchdog + reconcile-on-boot are the deterministic
29
+ // close mechanism. This hook is now responsible only for cleaning up
30
+ // sidecar/tail processes and the pre-tool-use grace file.
31
+
32
+ let cwd = process.cwd();
33
+ try {
34
+ const parsed = JSON.parse(input);
35
+ if (parsed.cwd) cwd = parsed.cwd;
36
+ } catch { /* use process.cwd() */ }
37
+
38
+ const socketPath = path.join(cwd, '.claws', 'claws.sock');
39
+ if (!fs.existsSync(socketPath)) { process.exit(0); return; }
40
+
41
+ // Kill stream-events.js sidecar daemon spawned by session-start-claws.js
42
+ try {
43
+ const { spawnSync } = require('child_process');
44
+ const pg = spawnSync('pgrep', ['-f', 'stream-events\\.js.*--auto-sidecar'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
45
+ if (pg.status === 0 && pg.stdout) {
46
+ const pids = pg.stdout.trim().split('\n').filter(Boolean);
47
+ for (const pid of pids) {
48
+ try { spawnSync('kill', ['-TERM', pid.trim()], { stdio: 'ignore' }); } catch {}
49
+ }
50
+ }
51
+ } catch { /* sidecar kill failure must never block the hook */ }
52
+
53
+ // Kill orphan tail -F processes monitoring events.log spawned this session.
54
+ try {
55
+ const { spawnSync } = require('child_process');
56
+ const pg2 = spawnSync('pgrep', ['-f', 'tail.*\\.claws/events\\.log'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
57
+ if (pg2.status === 0 && pg2.stdout) {
58
+ const pids = pg2.stdout.trim().split('\n').filter(Boolean);
59
+ for (const pid of pids) {
60
+ try { spawnSync('kill', ['-TERM', pid.trim()], { stdio: 'ignore' }); } catch {}
61
+ }
62
+ }
63
+ } catch { /* tail kill failure must never block the hook */ }
64
+
65
+ // Remove the pre-tool-use grace file so the next session starts with a
66
+ // fresh 60 s window and the Monitor-arm enforcement resets cleanly.
67
+ try {
68
+ const cwdKey = Buffer.from(cwd).toString('base64').replace(/[+/=]/g, '_').slice(0, 12);
69
+ const graceFile = `/tmp/claws-pretooluse-grace-${cwdKey}`;
70
+ if (fs.existsSync(graceFile)) fs.unlinkSync(graceFile);
71
+ } catch { /* grace file removal must never block the hook */ }
72
+
73
+ // LH-9: force-close removed. The Stop hook fires at the end of every
74
+ // assistant turn (Anthropic semantics) — not at session shutdown — so
75
+ // closing detached workers here killed long-running missions between
76
+ // turns. Worker lifetime is now governed by the TTL watchdog inside
77
+ // the extension (default 10min idle, 4h hard ceiling), and stale
78
+ // entries in lifecycle-state.json self-heal via reconcile-on-boot in
79
+ // ClawsServer's constructor. The hook keeps only the sidecar/tail
80
+ // cleanup above.
81
+ process.exit(0);
82
+ } catch {
83
+ process.exit(0);
84
+ }
85
+ });
86
+ } catch {
87
+ process.exit(0);
88
+ }
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ // Inject the dynamic Claws block into a project's CLAUDE.md.
3
+ // Usage: node inject-claude-md.js <project-root>
4
+ //
5
+ // Behavior:
6
+ // 1. Migrates legacy v0.1–v0.3 "## CLAWS — Terminal Orchestration Active"
7
+ // section if present.
8
+ // 2. Inserts or replaces the fenced block:
9
+ // <!-- CLAWS:BEGIN [v<X.Y.Z>] --> ... <!-- CLAWS:END [v<X.Y.Z>] -->
10
+ // Sentinel match is regex-based so any prior version is cleanly replaced.
11
+ // 3. Tool list, lifecycle phase list, and version are derived from code at
12
+ // inject time — see readToolList(), readPhases(), readVersion(). This makes
13
+ // drift between the server and the user-facing CLAUDE.md structurally
14
+ // impossible.
15
+ // 4. If CLAUDE.md doesn't exist, creates a minimal stub with a placeholder for
16
+ // project-specific context above the block.
17
+ // 5. Preserves every non-Claws line of the file byte-for-byte.
18
+
19
+ 'use strict';
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ // M-27: atomic write helper (tmp + renameSync) — mirrors atomic-file.mjs writeAtomic.
24
+ function writeAtomic(filePath, content) {
25
+ const tmp = filePath + '.claws-tmp.' + process.pid + '-' + (++writeAtomic._nonce);
26
+ let _fd;
27
+ try {
28
+ _fd = fs.openSync(tmp, 'w', 0o644);
29
+ fs.writeSync(_fd, content);
30
+ fs.fsyncSync(_fd);
31
+ fs.closeSync(_fd);
32
+ _fd = null;
33
+ fs.renameSync(tmp, filePath);
34
+ } catch (err) {
35
+ if (_fd != null) { try { fs.closeSync(_fd); } catch { /* ignore */ } }
36
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
37
+ throw err;
38
+ }
39
+ }
40
+ writeAtomic._nonce = 0;
41
+
42
+ const TARGET = process.argv[2];
43
+ if (!TARGET) {
44
+ console.error('usage: inject-claude-md.js <project-root>');
45
+ process.exit(2);
46
+ }
47
+
48
+ const REPO_ROOT = path.join(__dirname, '..');
49
+ const CLAUDE_MD = path.join(TARGET, 'CLAUDE.md');
50
+ const CMD_DIR = path.join(TARGET, '.claude', 'commands');
51
+
52
+ // ── Source-of-truth readers ────────────────────────────────────────────────
53
+ // Tool list: parse mcp_server.js dispatch handlers. Pattern: `name === 'claws_xxx'`.
54
+ // One unique entry per tool. Sorted alphabetically for stable diffs.
55
+ function readToolList() {
56
+ const mcpPath = path.join(REPO_ROOT, 'mcp_server.js');
57
+ try {
58
+ const src = fs.readFileSync(mcpPath, 'utf8');
59
+ const re = /name === '(claws_[a-z_]+)'/g;
60
+ const set = new Set();
61
+ let m;
62
+ while ((m = re.exec(src)) !== null) set.add(m[1]);
63
+ return Array.from(set).sort();
64
+ } catch (err) {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ // Phase enum: parse extension/src/lifecycle-store.ts `export type Phase = ...`.
70
+ // Returned in declaration order (which is also lifecycle order).
71
+ function readPhases() {
72
+ const ltPath = path.join(REPO_ROOT, 'extension', 'src', 'lifecycle-store.ts');
73
+ try {
74
+ const src = fs.readFileSync(ltPath, 'utf8');
75
+ const m = src.match(/export type Phase\s*=([^;]+);/);
76
+ if (!m) return [];
77
+ return Array.from(m[1].matchAll(/'([A-Z][A-Z0-9-]+)'/g)).map((x) => x[1]);
78
+ } catch (err) {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ function readVersion() {
84
+ try {
85
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
86
+ return pkg.version || 'unknown';
87
+ } catch (err) {
88
+ return 'unknown';
89
+ }
90
+ }
91
+
92
+ const TOOLS = readToolList();
93
+ const PHASES = readPhases();
94
+ const VERSION = readVersion();
95
+
96
+ // Versioned sentinels — emitted with current version, matched with regex
97
+ // so prior-version blocks are replaced cleanly on upgrade.
98
+ const BEGIN_LITERAL = `<!-- CLAWS:BEGIN v${VERSION} -->`;
99
+ const END_LITERAL = `<!-- CLAWS:END v${VERSION} -->`;
100
+ const BEGIN_RE = /<!-- CLAWS:BEGIN(?: v[\d.]+(?:-[\w.]+)?)? -->/;
101
+ const END_RE = /<!-- CLAWS:END(?: v[\d.]+(?:-[\w.]+)?)? -->/;
102
+
103
+ let cmds = [];
104
+ try {
105
+ cmds = fs.readdirSync(CMD_DIR)
106
+ .filter((f) => f.startsWith('claws') && f.endsWith('.md'))
107
+ .map((f) => '/' + f.replace(/\.md$/, ''))
108
+ .sort();
109
+ } catch { /* ignore */ }
110
+
111
+ function buildBlock(target, cmds) {
112
+ const tpl = path.join(REPO_ROOT, 'templates', 'CLAUDE.project.md');
113
+ try {
114
+ const raw = fs.readFileSync(tpl, 'utf8');
115
+ const toList = (arr) => arr.map((t) => '`' + t + '`').join(', ');
116
+ const phaseChain = PHASES.length ? PHASES.join(' → ') : '(unavailable)';
117
+ return raw
118
+ .trimEnd()
119
+ // Replace literal BEGIN/END sentinels in template with versioned ones.
120
+ .replace(/<!-- CLAWS:BEGIN(?: v[\d.]+(?:-[\w.]+)?)? -->/g, BEGIN_LITERAL)
121
+ .replace(/<!-- CLAWS:END(?: v[\d.]+(?:-[\w.]+)?)? -->/g, END_LITERAL)
122
+ .replace(/\{PROJECT_NAME\}/g, path.basename(target))
123
+ .replace(/\{SOCKET_PATH\}/g, '.claws/claws.sock')
124
+ .replace(/\{VERSION\}/g, VERSION)
125
+ .replace(/\{TOOLS_COUNT\}/g, String(TOOLS.length))
126
+ .replace(/\{TOOLS_LIST\}/g, TOOLS.length ? toList(TOOLS) : '_(no tools detected — mcp_server.js missing?)_')
127
+ .replace(/\{LIFECYCLE_PHASES\}/g, phaseChain)
128
+ .replace(/\{CMDS_COUNT\}/g, String(cmds.length))
129
+ .replace(/\{CMDS_LIST\}/g, cmds.length ? toList(cmds) : '_(none installed)_');
130
+ } catch (err) {
131
+ return [
132
+ BEGIN_LITERAL,
133
+ '<!-- ERROR: templates/CLAUDE.project.md not found: ' + err.message + ' -->',
134
+ '## Claws — Terminal Orchestration (MANDATORY)',
135
+ '',
136
+ 'You are a Claws orchestrator. Use claws_create + claws_send for long-lived processes.',
137
+ 'Always close every terminal you create. Never touch terminals you did not create.',
138
+ END_LITERAL,
139
+ ].join('\n');
140
+ }
141
+ }
142
+
143
+ const block = buildBlock(TARGET, cmds);
144
+
145
+ let md = '';
146
+ let existed = false;
147
+ try { md = fs.readFileSync(CLAUDE_MD, 'utf8'); existed = true; } catch { /* ignore */ }
148
+
149
+ // ── Migrate legacy v0.1–v0.3 section ─────────────────────────────────────
150
+ let migrated = false;
151
+ const legacyStart = md.indexOf('## CLAWS — Terminal Orchestration Active');
152
+ if (legacyStart !== -1) {
153
+ const rest = md.slice(legacyStart);
154
+ const legacyEndPhrase = 'Type `/claws-help` for the full prompt guide.';
155
+ const phraseIdx = rest.indexOf(legacyEndPhrase);
156
+ let legacyEndAbs;
157
+ if (phraseIdx !== -1) {
158
+ const after = legacyStart + phraseIdx + legacyEndPhrase.length;
159
+ const nlAfter = md.indexOf('\n', after);
160
+ legacyEndAbs = nlAfter === -1 ? md.length : nlAfter + 1;
161
+ } else {
162
+ const lines = rest.split('\n');
163
+ let consumed = lines[0].length + 1;
164
+ for (let i = 1; i < lines.length; i++) {
165
+ if (lines[i].startsWith('## ')) {
166
+ legacyEndAbs = legacyStart + consumed;
167
+ break;
168
+ }
169
+ consumed += lines[i].length + 1;
170
+ }
171
+ if (legacyEndAbs === undefined) legacyEndAbs = md.length;
172
+ }
173
+ let trimStart = legacyStart;
174
+ if (trimStart >= 2 && md.slice(trimStart - 2, trimStart) === '\n\n') {
175
+ trimStart -= 1;
176
+ }
177
+ md = md.slice(0, trimStart) + md.slice(legacyEndAbs);
178
+ migrated = true;
179
+ }
180
+
181
+ // ── Insert or replace fenced block (regex match — handles any prior version) ──
182
+ const beginMatch = md.match(BEGIN_RE);
183
+ const endMatch = md.match(END_RE);
184
+
185
+ let next;
186
+ if (beginMatch && endMatch && endMatch.index > beginMatch.index) {
187
+ next = md.slice(0, beginMatch.index) + block + md.slice(endMatch.index + endMatch[0].length);
188
+ } else if (existed) {
189
+ const sep = md.endsWith('\n\n') ? '' : md.endsWith('\n') ? '\n' : '\n\n';
190
+ next = md + sep + block + '\n';
191
+ } else {
192
+ next = '# Project\n\n<!-- Add your project-specific Claude Code context above this line -->\n\n' + block + '\n';
193
+ }
194
+
195
+ let orig = '';
196
+ try { orig = fs.readFileSync(CLAUDE_MD, 'utf8'); } catch { /* ignore */ }
197
+
198
+ if (next !== orig) {
199
+ writeAtomic(CLAUDE_MD, next);
200
+ const prefix = migrated ? 'legacy section migrated; ' : '';
201
+ const action = beginMatch ? 'Claws block updated' : 'Claws block inserted';
202
+ console.log(`CLAUDE.md ${prefix}${existed ? action : 'created with Claws block'} (v${VERSION}, ${TOOLS.length} tools, ${PHASES.length} phases)`);
203
+ } else {
204
+ console.log(`CLAUDE.md already has the current Claws block (v${VERSION})`);
205
+ }