@humwick/runner 0.0.0-stage → 0.130.1

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 (293) hide show
  1. package/LICENSE +167 -0
  2. package/agent-bin/agent-relay-browser +10 -0
  3. package/agent-bin/humwick +10 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
  5. package/hooks/fff-search-redirect/hooks.json +13 -0
  6. package/hooks/rg-r-guard/hooks.json +18 -0
  7. package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
  8. package/hooks/web-tools-redirect/hooks.json +13 -0
  9. package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
  10. package/node_modules/@humwick/offload/LICENSE +167 -0
  11. package/node_modules/@humwick/offload/README.md +57 -0
  12. package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
  13. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
  14. package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
  15. package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
  16. package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
  17. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
  18. package/node_modules/@humwick/offload/dist/envelope.js +80 -0
  19. package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
  20. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
  21. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
  22. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
  23. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
  24. package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
  25. package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
  26. package/node_modules/@humwick/offload/dist/index.js +5 -0
  27. package/node_modules/@humwick/offload/dist/index.js.map +1 -0
  28. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
  29. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
  30. package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
  31. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
  32. package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
  33. package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
  34. package/node_modules/@humwick/offload/dist/types.js +176 -0
  35. package/node_modules/@humwick/offload/dist/types.js.map +1 -0
  36. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
  37. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
  38. package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
  39. package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
  40. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
  41. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
  42. package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
  43. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
  44. package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
  45. package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
  46. package/node_modules/@humwick/offload/dist/worker.js +1 -0
  47. package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
  48. package/node_modules/@humwick/offload/package.json +42 -0
  49. package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
  50. package/node_modules/@humwick/offload/src/envelope.ts +92 -0
  51. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
  52. package/node_modules/@humwick/offload/src/index.ts +4 -0
  53. package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
  54. package/node_modules/@humwick/offload/src/types.ts +235 -0
  55. package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
  56. package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
  57. package/node_modules/@humwick/offload/src/worker.ts +5 -0
  58. package/package.json +47 -4
  59. package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
  60. package/plugins/claude/.claude-plugin/plugin.json +8 -0
  61. package/plugins/claude/claude-statusline-probe.cjs +144 -0
  62. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
  63. package/plugins/claude/hooks/gate-run-guard.sh +9 -0
  64. package/plugins/claude/hooks/gate-run-guard.ts +237 -0
  65. package/plugins/claude/hooks/hooks.json +158 -0
  66. package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
  67. package/plugins/claude/hooks/kill-guard.sh +341 -0
  68. package/plugins/claude/hooks/kill-guard.ts +337 -0
  69. package/plugins/claude/hooks/permission-request.sh +22 -0
  70. package/plugins/claude/hooks/post-compact.sh +10 -0
  71. package/plugins/claude/hooks/post-tool-use.sh +8 -0
  72. package/plugins/claude/hooks/pre-compact.sh +12 -0
  73. package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
  74. package/plugins/claude/hooks/relay-status.sh +373 -0
  75. package/plugins/claude/hooks/session-end.sh +26 -0
  76. package/plugins/claude/hooks/session-start.sh +31 -0
  77. package/plugins/claude/hooks/shell-trust.ts +28 -0
  78. package/plugins/claude/hooks/stop-failure.sh +47 -0
  79. package/plugins/claude/hooks/stop.sh +48 -0
  80. package/plugins/claude/hooks/subagent-guard.cjs +26 -0
  81. package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
  82. package/plugins/claude/hooks/subagent-start.sh +13 -0
  83. package/plugins/claude/hooks/subagent-stop.sh +19 -0
  84. package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
  85. package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
  86. package/plugins/claude/monitors/monitors.json +7 -0
  87. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
  88. package/plugins/claude/monitors/relay-monitor.ts +58 -0
  89. package/plugins/claude/skills/disconnect/SKILL.md +16 -0
  90. package/plugins/claude/skills/label/SKILL.md +23 -0
  91. package/plugins/claude/skills/message/SKILL.md +40 -0
  92. package/plugins/claude/skills/pair/SKILL.md +30 -0
  93. package/plugins/claude/skills/react/SKILL.md +18 -0
  94. package/plugins/claude/skills/read-message/SKILL.md +24 -0
  95. package/plugins/claude/skills/reply/SKILL.md +44 -0
  96. package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
  97. package/plugins/claude/skills/slice-first/SKILL.md +32 -0
  98. package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
  99. package/plugins/claude/skills/status/SKILL.md +16 -0
  100. package/plugins/claude/skills/tags/SKILL.md +25 -0
  101. package/plugins/claude/skills/task/SKILL.md +28 -0
  102. package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
  103. package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
  104. package/plugins/codex/skills/disconnect/SKILL.md +15 -0
  105. package/plugins/codex/skills/guide/SKILL.md +15 -0
  106. package/plugins/codex/skills/label/SKILL.md +22 -0
  107. package/plugins/codex/skills/message/SKILL.md +40 -0
  108. package/plugins/codex/skills/pair/SKILL.md +29 -0
  109. package/plugins/codex/skills/react/SKILL.md +17 -0
  110. package/plugins/codex/skills/read-message/SKILL.md +23 -0
  111. package/plugins/codex/skills/reply/SKILL.md +44 -0
  112. package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
  113. package/plugins/codex/skills/slice-first/SKILL.md +32 -0
  114. package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
  115. package/plugins/codex/skills/status/SKILL.md +15 -0
  116. package/plugins/codex/skills/tags/SKILL.md +24 -0
  117. package/plugins/codex/skills/task/SKILL.md +28 -0
  118. package/src/adapter.ts +1061 -0
  119. package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
  120. package/src/adapters/claude-api.ts +243 -0
  121. package/src/adapters/claude-context-command.ts +16 -0
  122. package/src/adapters/claude-credentials.ts +22 -0
  123. package/src/adapters/claude-delivery-lanes.ts +28 -0
  124. package/src/adapters/claude-delivery.ts +166 -0
  125. package/src/adapters/claude-effort.ts +94 -0
  126. package/src/adapters/claude-exit.ts +290 -0
  127. package/src/adapters/claude-headless-mirror.ts +88 -0
  128. package/src/adapters/claude-headless-refusal.ts +51 -0
  129. package/src/adapters/claude-headless.ts +617 -0
  130. package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
  131. package/src/adapters/claude-permission.ts +243 -0
  132. package/src/adapters/claude-prompt-gates.ts +269 -0
  133. package/src/adapters/claude-prompt-hook.ts +145 -0
  134. package/src/adapters/claude-session-capture.ts +293 -0
  135. package/src/adapters/claude-session-probe.ts +414 -0
  136. package/src/adapters/claude-shutdown.ts +190 -0
  137. package/src/adapters/claude-status-detectors.ts +426 -0
  138. package/src/adapters/claude-subagents.ts +93 -0
  139. package/src/adapters/claude-submit-confirmation.ts +287 -0
  140. package/src/adapters/claude-tmux.ts +548 -0
  141. package/src/adapters/claude-token-usage-capture.ts +253 -0
  142. package/src/adapters/claude-tool-policy.ts +165 -0
  143. package/src/adapters/claude-transcript-tail.ts +193 -0
  144. package/src/adapters/claude-transcript.ts +812 -0
  145. package/src/adapters/claude.ts +1097 -0
  146. package/src/adapters/codex-agent-message-capture.ts +189 -0
  147. package/src/adapters/codex-app-server.ts +70 -0
  148. package/src/adapters/codex-client.ts +362 -0
  149. package/src/adapters/codex-effective-config.ts +87 -0
  150. package/src/adapters/codex-launch-config.ts +153 -0
  151. package/src/adapters/codex-rollout-context.ts +272 -0
  152. package/src/adapters/codex-skill-discovery.ts +18 -0
  153. package/src/adapters/codex-token-usage-capture.ts +163 -0
  154. package/src/adapters/codex-tool-output.ts +53 -0
  155. package/src/adapters/codex-turn-outcome.ts +27 -0
  156. package/src/adapters/codex-version.ts +64 -0
  157. package/src/adapters/codex.ts +1442 -0
  158. package/src/adapters/incremental-file.ts +110 -0
  159. package/src/adapters/opencode-api.ts +71 -0
  160. package/src/adapters/opencode-context.ts +277 -0
  161. package/src/adapters/opencode-model-registry.ts +126 -0
  162. package/src/adapters/opencode-provider.ts +169 -0
  163. package/src/adapters/opencode-server-retry.ts +50 -0
  164. package/src/adapters/opencode-soft-steer.ts +302 -0
  165. package/src/adapters/opencode-spawn.ts +145 -0
  166. package/src/adapters/opencode-subscribe.ts +62 -0
  167. package/src/adapters/opencode-token-usage-capture.ts +187 -0
  168. package/src/adapters/opencode-tool-events.ts +302 -0
  169. package/src/adapters/opencode-tools-config.ts +17 -0
  170. package/src/adapters/opencode.ts +2264 -0
  171. package/src/adapters/project-shell.ts +80 -0
  172. package/src/adapters/provider-launch-config.ts +21 -0
  173. package/src/adapters/provider-quota-harvest.ts +127 -0
  174. package/src/adapters/service-access.ts +153 -0
  175. package/src/adapters/tmux-offload.ts +207 -0
  176. package/src/adapters/tool-summary.ts +214 -0
  177. package/src/agent-redact.ts +75 -0
  178. package/src/agent-runner/commands/dispatch.ts +185 -0
  179. package/src/agent-runner/commands/lifecycle.ts +84 -0
  180. package/src/agent-runner/commands/prompt.ts +284 -0
  181. package/src/agent-runner/credentials.ts +208 -0
  182. package/src/agent-runner/http-liveness.ts +85 -0
  183. package/src/agent-runner/lifecycle-milestones.ts +207 -0
  184. package/src/agent-runner/lifecycle-phase.ts +80 -0
  185. package/src/agent-runner/registration.ts +163 -0
  186. package/src/agent-runner/runner-context.ts +158 -0
  187. package/src/agent-runner/session-mirror.ts +278 -0
  188. package/src/agent-runner/task-claims.ts +116 -0
  189. package/src/agent-runner/token-usage-coverage.ts +54 -0
  190. package/src/agent-runner/turn-report.ts +269 -0
  191. package/src/api-budget-accounting.ts +91 -0
  192. package/src/area-memory.ts +255 -0
  193. package/src/artifact-path-materialize.ts +52 -0
  194. package/src/async-guard.ts +35 -0
  195. package/src/attachment-cache-sweep.ts +31 -0
  196. package/src/attachment-cache.ts +195 -0
  197. package/src/background-script-reconcile.ts +106 -0
  198. package/src/bounded-log.ts +59 -0
  199. package/src/bounded-spawn-sync.ts +18 -0
  200. package/src/busy-reconciler.ts +282 -0
  201. package/src/claim-tracker.ts +140 -0
  202. package/src/claude-settings.ts +167 -0
  203. package/src/claude-workspace-trust.ts +289 -0
  204. package/src/config.ts +547 -0
  205. package/src/context-boundary.ts +101 -0
  206. package/src/context-command.ts +151 -0
  207. package/src/context-probe-state.ts +6 -0
  208. package/src/continuation-archive.ts +179 -0
  209. package/src/control-server.ts +730 -0
  210. package/src/delivery-policy.ts +9 -0
  211. package/src/delivery-result.ts +24 -0
  212. package/src/effort-command.ts +125 -0
  213. package/src/empty-turn.ts +816 -0
  214. package/src/free-port.ts +25 -0
  215. package/src/fs-offload.ts +26 -0
  216. package/src/gate-run-config.ts +35 -0
  217. package/src/gate-run-lease-probe.ts +38 -0
  218. package/src/host-asset-plan.ts +450 -0
  219. package/src/host-claude-config-worker.ts +34 -0
  220. package/src/host-sources.ts +40 -0
  221. package/src/inbox-carrier.ts +7 -0
  222. package/src/index.ts +386 -0
  223. package/src/launch-assembly.ts +955 -0
  224. package/src/liveness.ts +105 -0
  225. package/src/logger.ts +108 -0
  226. package/src/mcp-outbox.ts +204 -0
  227. package/src/mcp-preflight.ts +86 -0
  228. package/src/mcp-source-selection.ts +54 -0
  229. package/src/mcp-tool-deferral.ts +35 -0
  230. package/src/mcp-vault-env.ts +25 -0
  231. package/src/message-body-config/config.ts +16 -0
  232. package/src/message-body-config.ts +1 -0
  233. package/src/mid-turn-delivery.ts +43 -0
  234. package/src/native-memory-lever.ts +329 -0
  235. package/src/native-self-resume.ts +244 -0
  236. package/src/outbox.ts +453 -0
  237. package/src/park-post.ts +17 -0
  238. package/src/pending-permission-store.ts +148 -0
  239. package/src/process-meta.ts +9 -0
  240. package/src/profile-home.ts +419 -0
  241. package/src/profile-projection.ts +411 -0
  242. package/src/project-hooks.ts +65 -0
  243. package/src/project-instructions.ts +119 -0
  244. package/src/provider-double.ts +59 -0
  245. package/src/provider-hold.ts +228 -0
  246. package/src/provider-home-marker.ts +3 -0
  247. package/src/provider-home-tombstone.ts +4 -0
  248. package/src/provider-input-queue.ts +134 -0
  249. package/src/provider-integration.ts +93 -0
  250. package/src/provider-processes.ts +17 -0
  251. package/src/provider-submit-failure.ts +15 -0
  252. package/src/providers.ts +35 -0
  253. package/src/provisioning.ts +703 -0
  254. package/src/question-decision.ts +18 -0
  255. package/src/quota.ts +22 -0
  256. package/src/rate-limit.ts +351 -0
  257. package/src/refused-prompt-store.ts +36 -0
  258. package/src/relay-injection-events.ts +161 -0
  259. package/src/relay-instructions.ts +107 -0
  260. package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
  261. package/src/relay-mcp-proxy.ts +704 -0
  262. package/src/relay-mcp.ts +583 -0
  263. package/src/reply-obligation-cache.ts +211 -0
  264. package/src/response-capture-report.ts +111 -0
  265. package/src/response-routing.ts +6 -0
  266. package/src/runner-core.ts +2955 -0
  267. package/src/runner-exit.ts +12 -0
  268. package/src/runner-helpers.ts +582 -0
  269. package/src/runner-insights.ts +105 -0
  270. package/src/runner-mcp-proxy.ts +46 -0
  271. package/src/runner-spawn-config.ts +68 -0
  272. package/src/runner-stale-scratch-sweep.ts +21 -0
  273. package/src/runner.ts +9 -0
  274. package/src/runtime-token-file.ts +45 -0
  275. package/src/runtime-token-remint.ts +56 -0
  276. package/src/service-access.ts +88 -0
  277. package/src/session-destroy.ts +27 -0
  278. package/src/session-insights.ts +118 -0
  279. package/src/session-scratch.ts +296 -0
  280. package/src/spawn-input.ts +147 -0
  281. package/src/startup-config-fetch.ts +48 -0
  282. package/src/subreaper.ts +113 -0
  283. package/src/template-resolver.ts +29 -0
  284. package/src/test-run-dedup.ts +435 -0
  285. package/src/transcript-context.ts +99 -0
  286. package/src/transport-errors.ts +48 -0
  287. package/src/turn-opener.ts +55 -0
  288. package/src/turn-reconcile-event.ts +88 -0
  289. package/src/vault-proxy-hop.ts +99 -0
  290. package/src/version.ts +44 -0
  291. package/src/web-tool-call-proxy.ts +128 -0
  292. package/src/web-tools.ts +59 -0
  293. package/README.md +0 -3
@@ -0,0 +1,160 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const crypto = require('node:crypto');
5
+ const { isRecord, isPathWithinBase } = require('@humwick/sdk');
6
+
7
+ function stat(file) {
8
+ try { return fs.statSync(file); }
9
+ catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return; throw error; }
10
+ }
11
+ function json(file) {
12
+ if (!stat(file)) return {};
13
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
14
+ if (!isRecord(value)) throw new Error(`Expected object in ${file}`);
15
+ return value;
16
+ }
17
+ function ancestors(directory) {
18
+ const dirs = [];
19
+ for (let dir = path.resolve(directory); ; dir = path.dirname(dir)) {
20
+ dirs.push(dir);
21
+ if (path.dirname(dir) === dir) return dirs;
22
+ }
23
+ }
24
+
25
+ // Cache the byte cursor beside the native transcript; resumed sessions retain discovery history.
26
+ // Only complete JSONL rows advance the cursor. Concurrent readers may repeat work, never skip it.
27
+ async function discoveredDirectories(payload) {
28
+ if (!payload.transcript_path || !stat(payload.transcript_path)) return [];
29
+ const transcript = payload.transcript_path, info = fs.statSync(transcript);
30
+ const cachePath = `${transcript}.relay-skill-dirs.json`;
31
+ let state = json(cachePath);
32
+ if (state.ino !== info.ino || state.offset > info.size) state.offset = 0;
33
+ const dirs = new Set(state.directories || []);
34
+ const start = state.offset || 0;
35
+ if (info.size - start > 64 * 1024 * 1024) throw new Error('skill discovery transcript exceeds incremental read budget');
36
+ let offset = start, pending = Buffer.alloc(0);
37
+ if (info.size > start) for await (const chunk of fs.createReadStream(transcript, { start, end: info.size - 1 })) {
38
+ pending = Buffer.concat([pending, chunk]);
39
+ let end;
40
+ while ((end = pending.indexOf(10)) >= 0) {
41
+ const line = pending.subarray(0, end).toString('utf8');
42
+ offset += end + 1; pending = pending.subarray(end + 1);
43
+ if (!line.trim()) continue;
44
+ const row = JSON.parse(line);
45
+ if (row.cwd) dirs.add(path.resolve(row.cwd));
46
+ for (const block of Array.isArray(row.message?.content) ? row.message.content : []) {
47
+ if (block.type !== 'tool_use' || !['Read', 'Edit', 'Write', 'NotebookEdit', 'Glob', 'Grep'].includes(block.name)) continue;
48
+ const input = block.input || {}, file = input.file_path || input.notebook_path || input.path;
49
+ if (typeof file === 'string') dirs.add(['Glob', 'Grep'].includes(block.name) ? path.resolve(row.cwd || payload.cwd, file) : path.dirname(path.resolve(row.cwd || payload.cwd, file)));
50
+ }
51
+ if (dirs.size > 4096) throw new Error('skill discovery directory budget exceeded');
52
+ }
53
+ if (pending.length > 8 * 1024 * 1024) throw new Error('skill discovery transcript row too large');
54
+ }
55
+ const temporary = `${cachePath}.${process.pid}`;
56
+ try { fs.writeFileSync(temporary, JSON.stringify({ ino: info.ino, offset, directories: [...dirs] })); fs.renameSync(temporary, cachePath); }
57
+ finally { fs.rmSync(temporary, { force: true }); }
58
+ return [...dirs];
59
+ }
60
+
61
+ exports.skillDenial = async function skillDenial(payload, name) {
62
+ if (typeof name !== 'string' || !name.trim()) return 'cannot resolve an unnamed skill';
63
+ const launch = skillSources();
64
+ const cwd = path.resolve(payload.cwd || process.cwd()), initialCwd = launch.cwd || cwd;
65
+ const sources = launch.sources || ['user', 'project', 'local'];
66
+ const home = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
67
+ const roots = new Set(), settings = [];
68
+ if (sources.includes('user')) { roots.add(home); settings.push(json(path.join(home, 'settings.json'))); }
69
+ const directories = [cwd, initialCwd, ...(launch.directories || []).map((dir) => path.resolve(initialCwd, dir)), ...await discoveredDirectories(payload)];
70
+ for (const directory of directories) for (const parent of ancestors(directory)) {
71
+ const root = path.join(parent, '.claude');
72
+ if (sources.includes('project') || sources.includes('local')) roots.add(root);
73
+ }
74
+ for (const root of [...roots].reverse()) {
75
+ if (sources.includes('project')) settings.push(json(path.join(root, 'settings.json')));
76
+ if (sources.includes('local')) settings.push(json(path.join(root, 'settings.local.json')));
77
+ }
78
+ settings.push(launch.settings || {});
79
+ const enabled = Object.assign({}, ...settings.map((setting) => setting.enabledPlugins || {}));
80
+ const overrides = Object.assign({}, ...settings.map((setting) => setting.skillOverrides || {}));
81
+ if (overrides[name] === 'off') return `skill ${name} is disabled`;
82
+ const plugins = new Set((launch.plugins || []).map((dir) => path.resolve(initialCwd, dir)));
83
+ const installed = json(path.join(home, 'plugins/installed_plugins.json')).plugins;
84
+ if (isRecord(installed)) for (const [id, entries] of Object.entries(installed)) {
85
+ if (enabled[id] !== true || !Array.isArray(entries)) continue;
86
+ for (const entry of entries) if (typeof entry.installPath === 'string' && (!entry.projectPath || isPathWithinBase(cwd, entry.projectPath))) plugins.add(path.resolve(home, entry.installPath));
87
+ }
88
+ const candidates = new Map(), visited = new Set();
89
+ let managed = false;
90
+ const header = Buffer.allocUnsafe(64 * 1024);
91
+ function inspectText(text, identity, fallbackName, prefix, overrideModel, size = text.length) {
92
+ let fields = {};
93
+ if (/^\uFEFF?---[^\S\r\n]*\r?\n/.test(text)) {
94
+ const match = /^\uFEFF?---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---(?:\s|$)/.exec(text);
95
+ if (!match) throw new Error(`Incomplete skill frontmatter in ${identity}`);
96
+ fields = Bun.YAML.parse(match[1]) || {};
97
+ if (!isRecord(fields)) throw new Error(`Invalid skill frontmatter in ${identity}`);
98
+ }
99
+ const resolvedName = typeof fields.name === 'string' ? fields.name : fallbackName;
100
+ const aliases = prefix.endsWith(':') ? [prefix + resolvedName, prefix + fallbackName] : [resolvedName, fallbackName, ...(prefix ? [prefix + ':' + resolvedName, prefix + ':' + fallbackName] : [])];
101
+ if (!aliases.includes(name)) return;
102
+ candidates.set(JSON.stringify([identity, overrideModel]), { ...fields, model: overrideModel ?? fields.model, digest: crypto.createHash('sha256').update(text).update(String(size)).digest('hex'), managed });
103
+ }
104
+ function inspect(file, commands, prefix = '', overrideName, overrideModel) {
105
+ const info = stat(file);
106
+ if (!info) return;
107
+ const canonical = fs.realpathSync(file), identity = JSON.stringify([canonical, commands, prefix, overrideName, overrideModel]);
108
+ if (visited.has(identity)) return;
109
+ visited.add(identity);
110
+ if (visited.size > 4096) throw new Error('skill resolution traversal budget exceeded');
111
+ if (info.isDirectory()) {
112
+ if (!commands && stat(path.join(file, 'SKILL.md'))) inspect(path.join(file, 'SKILL.md'), false, prefix);
113
+ else for (const entry of fs.readdirSync(file)) inspect(path.join(file, entry), commands, prefix);
114
+ } else if (info.isFile() && (commands ? file.endsWith('.md') : path.basename(file) === 'SKILL.md')) {
115
+ const fd = fs.openSync(file, 'r');
116
+ try { inspectText(header.subarray(0, fs.readSync(fd, header, 0, header.length, 0)).toString('utf8'), canonical, overrideName || (commands ? path.basename(file, '.md') : path.basename(path.dirname(file))), prefix, overrideModel, info.size); }
117
+ finally { fs.closeSync(fd); }
118
+ }
119
+ }
120
+ for (const root of roots) {
121
+ const prefix = root === home ? '' : path.relative(initialCwd, path.dirname(root)).split(path.sep).join('/');
122
+ managed = root === home;
123
+ inspect(path.join(root, 'skills'), false, prefix); inspect(path.join(root, 'commands'), true, prefix);
124
+ managed = false;
125
+ }
126
+ for (const plugin of plugins) {
127
+ const manifest = json(path.join(plugin, '.claude-plugin/plugin.json'));
128
+ if (typeof manifest.name !== 'string') throw new Error(`Cannot establish plugin namespace in ${plugin}`);
129
+ const prefix = `${manifest.name}:`;
130
+ inspect(path.join(plugin, 'skills'), false, prefix);
131
+ if (!stat(path.join(plugin, 'skills')) && manifest.skills === undefined) inspect(path.join(plugin, 'SKILL.md'), false, prefix);
132
+ if (manifest.commands === undefined) inspect(path.join(plugin, 'commands'), true, prefix);
133
+ for (const key of ['skills', 'commands']) {
134
+ const paths = manifest[key];
135
+ for (const file of typeof paths === 'string' ? [paths] : Array.isArray(paths) ? paths : []) {
136
+ if (typeof file !== 'string') throw new Error('Unsupported plugin skill path');
137
+ const absolute = path.resolve(plugin, file);
138
+ if (!isPathWithinBase(absolute, plugin)) throw new Error('Plugin skill path escapes its root');
139
+ inspect(absolute, key === 'commands', prefix);
140
+ }
141
+ }
142
+ if (isRecord(manifest.commands)) for (const [command, definition] of Object.entries(manifest.commands)) {
143
+ if (!isRecord(definition)) throw new Error('Unsupported plugin command');
144
+ if (typeof definition.source === 'string') inspect(path.resolve(plugin, definition.source), true, prefix, command, definition.model);
145
+ if (typeof definition.content === 'string') inspectText(definition.content, `${plugin}:${command}`, command, prefix, definition.model);
146
+ }
147
+ }
148
+ // #3706: the role-provisioned copy in the managed home and its identical repo source are one skill; differing same-name skills stay ambiguous.
149
+ const sourced = new Set([...candidates.values()].filter((entry) => !entry.managed).map((entry) => entry.digest));
150
+ for (const [key, entry] of candidates) if (entry.managed && sourced.has(entry.digest)) candidates.delete(key);
151
+ if (candidates.size !== 1) return `skill ${name} ${candidates.size ? 'resolves ambiguously' : 'cannot be resolved'}; refusing unverified execution`;
152
+ const skill = [...candidates.values()][0];
153
+ if (typeof skill.context === 'string' && skill.context.trim() === 'fork' && harnessSubagentsDisabled()) return 'Harness sub-agents are disabled. Start a Relay workflow or ask your coordinator';
154
+ if (typeof skill.context === 'string' && skill.context.trim() === 'fork' && (typeof skill.model !== 'string' || !skill.model.trim() || skill.model.trim() === 'inherit')) return `fork skill ${name} requires an explicit model under capped profiles`;
155
+ };
156
+
157
+ // #3470: standalone CJS config adapters; this file is shipped beside subagent-guard.cjs.
158
+ function skillSources() { return JSON.parse(process.env.AGENT_RELAY_CLAUDE_SKILL_SOURCES || '{}'); }
159
+ function harnessSubagentsDisabled() { return process.env.AGENT_RELAY_HARNESS_SUBAGENTS === '0'; }
160
+ exports.harnessSubagentsDisabled = harnessSubagentsDisabled;
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
5
+ # shellcheck source=/dev/null
6
+ source "${PLUGIN_ROOT}/hooks/relay-status.sh"
7
+ relay_install_hook_guard subagent-start
8
+
9
+ payload="$(cat || true)"
10
+ agent_id="$(relay_json_string_field agent_id "$payload")"
11
+ agent_type="$(relay_json_string_field agent_type "$payload")"
12
+
13
+ relay_post_status busy subagent "${agent_id:-subagent}" "$agent_type" "$agent_type"
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
5
+ # shellcheck source=/dev/null
6
+ source "${PLUGIN_ROOT}/hooks/relay-status.sh"
7
+ relay_install_hook_guard subagent-usage
8
+
9
+ payload="$(cat || true)"
10
+ agent_id="$(relay_json_string_field agent_id "$payload")"
11
+ agent_type="$(relay_json_string_field agent_type "$payload")"
12
+
13
+ if [ -n "${AGENT_RELAY_RUNNER_PORT:-}" ] && [ -n "$(relay_json_string_field agent_transcript_path "$payload")" ]; then
14
+ printf '%s' "$payload" | relay_post_json "$AGENT_RELAY_RUNNER_PORT" /subagent-usage 4 >/dev/null
15
+ fi
16
+
17
+ if [ "${AGENT_RELAY_CLAUDE_TRANSPORT:-}" != "stream-json" ]; then
18
+ relay_post_status idle subagent "${agent_id:-subagent}" "$agent_type" "$agent_type"
19
+ fi
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/relay-status.sh"
4
+ relay_install_hook_guard user-prompt-submit
5
+ fired_at="$(relay_now_ms)"
6
+ payload="$(cat || true)"
7
+ # prompt_id (#1086): Claude's own native turn identifier for the prompt starting
8
+ # now — the same UUID the Stop hook reports and the transcript stamps on every
9
+ # entry of this turn. Passed as the busy report's `id` so the runner adopts it as
10
+ # currentTurnId instead of minting a random UUID (runner-core.ts setProviderStatus).
11
+ prompt_id="$(relay_json_string_field prompt_id "$payload")"
12
+ relay_post_status busy "" "$prompt_id"
13
+ # Mirror a terminal/TUI-typed prompt into the dashboard chat and start reasoning
14
+ # tailing for this turn. No-op for prompts the runner injected (chat box / relay).
15
+ relay_post_user_prompt "$payload" "$fired_at"
16
+ # Re-surface the request-review reminder each turn while there is unmerged
17
+ # committed work — so a long session can't "forget" to land it. Silent otherwise.
18
+ relay_emit_additional_context UserPromptSubmit "$(relay_review_reminder_text || true)"
@@ -0,0 +1,120 @@
1
+ // #2993: a FOREGROUND wait (bare long `sleep`, or an until/while loop that only sleeps between checks) holds the
2
+ // turn; the wait belongs in one detached relay_run. STRICT ALLOWLIST: the command must be exactly
3
+ // setup* ( sleep D | (until|while) CHECK; do (sleep D | echo|printf|date|true)+; done ) rest*
4
+ // Anything else (loops doing work, `for`, nesting, `$(...)`, background `&`, unknown durations) is not matched,
5
+ // so every parse doubt fails open to "allow".
6
+ import { lexCommandLine, unwrapGroupCommand } from "@humwick/sdk/gate-run-classify";
7
+ import { CONTROL_KEYWORDS, trustedFlatCommand } from "./shell-trust";
8
+
9
+ const MIN_BARE_SLEEP_SECONDS = 30;
10
+ // Loops re-check, so even short sleeps accumulate; below this it is a startup/readiness blip (`until nc -z …; sleep 1`).
11
+ const MIN_LOOP_SLEEP_SECONDS = 5;
12
+ const MAX_UNWRAP_DEPTH = 2;
13
+ const SETUP = new Set(["cd", "pushd", "export", "echo", "printf", "date", "true", ":"]);
14
+ const BODY_FILLER = new Set(["echo", "printf", "date", "true", ":"]);
15
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
16
+ const DURATION = /^(\d+(?:\.\d+)?)([smhd]?)$/;
17
+ const UNIT_SECONDS: Record<string, number> = { "": 1, s: 1, m: 60, h: 3600, d: 86400 };
18
+ // A lone `&` backgrounds the wait: it does not hold the turn. Also catches `&` inside quotes, which fails open too.
19
+ // Not every wait-shaped text runs: `a || b` may skip b, and `<` feeds stdin (a reader loop, not a wait).
20
+ const UNSURE_OPERATORS = /\|\||</;
21
+ const SHELL_BINARIES = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
22
+ // Only these flag bundles are known to EXECUTE the -c script; `-n`, `--help`, `-o x` and anything unknown do not qualify.
23
+ const EXECUTING_FLAGS = /^-[leuxi]+$/;
24
+ const EXECUTING_C_FLAGS = /^-[leuxi]*c$/;
25
+ const READERS = new Set(["read", "mapfile", "readarray"]);
26
+ const BACKGROUND_AMP = /(?<![&>])&(?![&>])/;
27
+ const LOOP_KEYWORDS = CONTROL_KEYWORDS.filter((word) => word !== "while" && word !== "until");
28
+ const STRUCTURE_WORDS = new Set([...CONTROL_KEYWORDS, "do", "done", "{", "}", "(", ")", "[["]);
29
+
30
+ /** Seconds of a plain `sleep D…` segment (`sleep 1m 30s` sums), or null when it is not one or any arg is not a literal. */
31
+ function sleepSeconds(words: string[]): number | null {
32
+ if (words[0] !== "sleep" || words.length < 2) return null;
33
+ let total = 0;
34
+ for (const arg of words.slice(1)) {
35
+ const match = DURATION.exec(arg);
36
+ if (!match) return null;
37
+ total += Number(match[1]) * UNIT_SECONDS[match[2]!]!;
38
+ }
39
+ return total;
40
+ }
41
+
42
+ const isSetup = (words: string[]): boolean => SETUP.has(words[0]!) || words.every((word) => ASSIGNMENT.test(word));
43
+ // The first word of a "rest" segment must not open/close structure we did not parse.
44
+ const isPlainRest = (words: string[]): boolean => !STRUCTURE_WORDS.has(words[0]!);
45
+
46
+ /** True when the segments are one allowlisted wait. */
47
+ function isForegroundWait(segments: string[][]): boolean {
48
+ let i = 0;
49
+ while (i < segments.length && isSetup(segments[i]!)) i += 1;
50
+ const head = segments[i];
51
+ if (!head) return false;
52
+ let next: number;
53
+ if (head[0] === "sleep") {
54
+ const seconds = sleepSeconds(head);
55
+ if (seconds === null || seconds < MIN_BARE_SLEEP_SECONDS) return false;
56
+ next = i + 1;
57
+ } else if (head[0] === "until" || head[0] === "while") {
58
+ const checkStart = head.slice(1);
59
+ // `while [IFS= ]read …` consumes stdin; an empty check is malformed.
60
+ const checkCommand = checkStart.find((word) => !ASSIGNMENT.test(word));
61
+ if (!checkCommand || READERS.has(checkCommand)) return false;
62
+ let j = i + 1;
63
+ while (j < segments.length && segments[j]![0] !== "do") {
64
+ if (STRUCTURE_WORDS.has(segments[j]![0]!)) return false;
65
+ j += 1;
66
+ }
67
+ if (j >= segments.length) return false;
68
+ // `do` may carry the first body command (`do sleep 60`) or stand alone (multi-line form).
69
+ let sleeps = false;
70
+ let first = true;
71
+ for (; j < segments.length; j += 1) {
72
+ const words = first ? segments[j]!.slice(1) : segments[j]!;
73
+ first = false;
74
+ if (words.length === 1 && words[0] === "done") break;
75
+ if (words.length === 0) continue;
76
+ const seconds = sleepSeconds(words);
77
+ if (seconds !== null) {
78
+ if (seconds === 0) return false;
79
+ if (seconds >= MIN_LOOP_SLEEP_SECONDS) sleeps = true;
80
+ } else if (!BODY_FILLER.has(words[0]!)) {
81
+ return false;
82
+ }
83
+ }
84
+ if (j >= segments.length || !sleeps) return false;
85
+ next = j + 1;
86
+ } else {
87
+ return false;
88
+ }
89
+ return segments.slice(next).every(isPlainRest);
90
+ }
91
+
92
+ /** The script of `<shell> [-l|-e|-u|-x|-i…] -c '<script>'` — only an exact, known-executing wrapper; else undefined. */
93
+ function executingShellScript(candidate: string): string | undefined {
94
+ const segments = lexCommandLine(candidate);
95
+ const words = segments[0] ?? [];
96
+ if (!SHELL_BINARIES.has((words[0] ?? "").split("/").pop()!.toLowerCase())) return undefined;
97
+ let i = 1;
98
+ while (i < words.length && EXECUTING_FLAGS.test(words[i]!)) i += 1;
99
+ if (words.length !== i + 2 || !EXECUTING_C_FLAGS.test(words[i]!)) return undefined;
100
+ return words[i + 1];
101
+ }
102
+
103
+ const TIMEOUT_PREFIX = /^\s*timeout\s+(?:-\S+\s+)*\S+\s+/;
104
+
105
+ /** The deny reason for a foreground wait, or null (allow). `command` is the raw tool command text. */
106
+ export function waitLoopDenial(command: string): string | null {
107
+ const flat = trustedFlatCommand(command, LOOP_KEYWORDS);
108
+ if (flat === null || BACKGROUND_AMP.test(flat) || UNSURE_OPERATORS.test(flat)) return null;
109
+ let candidate: string | undefined = flat;
110
+ for (let depth = 0; depth <= MAX_UNWRAP_DEPTH && candidate !== undefined; depth += 1) {
111
+ candidate = candidate.replace(TIMEOUT_PREFIX, ""); // `timeout 600 <cmd>` is transparent
112
+ if (isForegroundWait(lexCommandLine(candidate))) {
113
+ return "Do not hold the turn on a foreground wait. Run it with the relay_run MCP tool (detach: true, waitMs: 0) as ONE poll loop with a deadline "
114
+ + "(e.g. `end=$((SECONDS+1800)); until <check>; do [ $SECONDS -ge $end ] && exit 1; sleep 30; done`), then end your turn with no text; "
115
+ + `Relay wakes you once when it finishes. Command: ${command}`;
116
+ }
117
+ candidate = executingShellScript(candidate) ?? unwrapGroupCommand(candidate);
118
+ }
119
+ return null;
120
+ }
@@ -0,0 +1,7 @@
1
+ [
2
+ {
3
+ "name": "agent-relay-runner-monitor",
4
+ "command": "bun run \"${CLAUDE_PLUGIN_ROOT}/monitors/relay-monitor.ts\"",
5
+ "description": "Agent Relay runner message delivery pipe"
6
+ }
7
+ ]