aicodeman 1.33.2 → 1.34.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 (214) hide show
  1. package/dist/config/cli-registry/schema.d.ts +18 -0
  2. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  3. package/dist/config/cli-registry/schema.js +27 -0
  4. package/dist/config/cli-registry/schema.js.map +1 -1
  5. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  6. package/dist/config/cli-registry/stock.js +35 -0
  7. package/dist/config/cli-registry/stock.js.map +1 -1
  8. package/dist/config/cli-registry/types.d.ts +33 -0
  9. package/dist/config/cli-registry/types.d.ts.map +1 -1
  10. package/dist/hooks-config.d.ts.map +1 -1
  11. package/dist/hooks-config.js +5 -1
  12. package/dist/hooks-config.js.map +1 -1
  13. package/dist/mcp-sync.d.ts +105 -0
  14. package/dist/mcp-sync.d.ts.map +1 -0
  15. package/dist/mcp-sync.js +619 -0
  16. package/dist/mcp-sync.js.map +1 -0
  17. package/dist/mux-interface.d.ts +4 -0
  18. package/dist/mux-interface.d.ts.map +1 -1
  19. package/dist/session-cli-builder.d.ts +20 -1
  20. package/dist/session-cli-builder.d.ts.map +1 -1
  21. package/dist/session-cli-builder.js +33 -3
  22. package/dist/session-cli-builder.js.map +1 -1
  23. package/dist/session-cli-registry-bridge.d.ts +2 -0
  24. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  25. package/dist/session-cli-registry-bridge.js +15 -8
  26. package/dist/session-cli-registry-bridge.js.map +1 -1
  27. package/dist/session.d.ts +19 -0
  28. package/dist/session.d.ts.map +1 -1
  29. package/dist/session.js +29 -2
  30. package/dist/session.js.map +1 -1
  31. package/dist/tmux-manager.d.ts +2 -0
  32. package/dist/tmux-manager.d.ts.map +1 -1
  33. package/dist/tmux-manager.js +4 -2
  34. package/dist/tmux-manager.js.map +1 -1
  35. package/dist/types/index.d.ts +3 -1
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/index.js +3 -1
  38. package/dist/types/index.js.map +1 -1
  39. package/dist/types/mcp-sync.d.ts +40 -0
  40. package/dist/types/mcp-sync.d.ts.map +1 -0
  41. package/dist/types/mcp-sync.js +8 -0
  42. package/dist/types/mcp-sync.js.map +1 -0
  43. package/dist/types/push.d.ts +37 -2
  44. package/dist/types/push.d.ts.map +1 -1
  45. package/dist/types/push.js +10 -3
  46. package/dist/types/push.js.map +1 -1
  47. package/dist/types/session.d.ts +34 -1
  48. package/dist/types/session.d.ts.map +1 -1
  49. package/dist/types/session.js +25 -1
  50. package/dist/types/session.js.map +1 -1
  51. package/dist/web/public/admin-ui.js.gz +0 -0
  52. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  53. package/dist/web/public/app.cf0db8ce.js +43 -0
  54. package/dist/web/public/app.cf0db8ce.js.br +0 -0
  55. package/dist/web/public/app.cf0db8ce.js.gz +0 -0
  56. package/dist/web/public/approvals-ui.js.gz +0 -0
  57. package/dist/web/public/{constants.cc927eb8.js → constants.d95fade9.js} +19 -2
  58. package/dist/web/public/constants.d95fade9.js.br +0 -0
  59. package/dist/web/public/constants.d95fade9.js.gz +0 -0
  60. package/dist/web/public/cron-ui.js.gz +0 -0
  61. package/dist/web/public/entrance-animations.js.gz +0 -0
  62. package/dist/web/public/home-sessions.js.gz +0 -0
  63. package/dist/web/public/host-wake-ui.js.gz +0 -0
  64. package/dist/web/public/i18n.398b8331.js +1 -0
  65. package/dist/web/public/i18n.398b8331.js.br +0 -0
  66. package/dist/web/public/i18n.398b8331.js.gz +0 -0
  67. package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
  68. package/dist/web/public/index.html +125 -14
  69. package/dist/web/public/index.html.br +0 -0
  70. package/dist/web/public/index.html.gz +0 -0
  71. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  72. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  73. package/dist/web/public/mobile-handlers.23a85db1.js.gz +0 -0
  74. package/dist/web/public/mobile-ime-preview.2dbaf262.js +1 -0
  75. package/dist/web/public/mobile-ime-preview.2dbaf262.js.br +0 -0
  76. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  77. package/dist/web/public/mobile-overview.js.gz +0 -0
  78. package/dist/web/public/mobile.1aa4952d.css.gz +0 -0
  79. package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
  80. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  81. package/dist/web/public/{panels-ui.67646701.js → panels-ui.6a5898d9.js} +69 -67
  82. package/dist/web/public/panels-ui.6a5898d9.js.br +0 -0
  83. package/dist/web/public/panels-ui.6a5898d9.js.gz +0 -0
  84. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  85. package/dist/web/public/{ralph-wizard.13a1831e.js → ralph-wizard.b6c0dd19.js} +2 -0
  86. package/dist/web/public/ralph-wizard.b6c0dd19.js.br +0 -0
  87. package/dist/web/public/{ralph-wizard.13a1831e.js.gz → ralph-wizard.b6c0dd19.js.gz} +0 -0
  88. package/dist/web/public/readmymind-ui.js.gz +0 -0
  89. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  90. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  91. package/dist/web/public/sanitize-html.e7147e76.js +1 -0
  92. package/dist/web/public/sanitize-html.e7147e76.js.br +0 -0
  93. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  94. package/dist/web/public/session-lineage.js +27 -11
  95. package/dist/web/public/session-lineage.js.br +0 -0
  96. package/dist/web/public/session-lineage.js.gz +0 -0
  97. package/dist/web/public/{session-ui.7575a231.js → session-ui.bf66c13b.js} +4 -4
  98. package/dist/web/public/session-ui.bf66c13b.js.br +0 -0
  99. package/dist/web/public/session-ui.bf66c13b.js.gz +0 -0
  100. package/dist/web/public/{settings-ui.c68e7f59.js → settings-ui.331d3b1d.js} +12 -10
  101. package/dist/web/public/settings-ui.331d3b1d.js.br +0 -0
  102. package/dist/web/public/settings-ui.331d3b1d.js.gz +0 -0
  103. package/dist/web/public/styles.8c485d26.css +1 -0
  104. package/dist/web/public/styles.8c485d26.css.br +0 -0
  105. package/dist/web/public/styles.8c485d26.css.gz +0 -0
  106. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  107. package/dist/web/public/sw.js +2 -2
  108. package/dist/web/public/sw.js.br +0 -0
  109. package/dist/web/public/sw.js.gz +0 -0
  110. package/dist/web/public/tab-layout-browser.4e0ed50c.js +1 -0
  111. package/dist/web/public/tab-layout-browser.4e0ed50c.js.br +0 -0
  112. package/dist/web/public/tab-layout-browser.4e0ed50c.js.gz +0 -0
  113. package/dist/web/public/tab-rail-resize.dcdc5772.js.gz +0 -0
  114. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  115. package/dist/web/public/terminal-split.js +248 -15
  116. package/dist/web/public/terminal-split.js.br +0 -0
  117. package/dist/web/public/terminal-split.js.gz +0 -0
  118. package/dist/web/public/terminal-ui.a4535311.js +2 -0
  119. package/dist/web/public/terminal-ui.a4535311.js.br +0 -0
  120. package/dist/web/public/terminal-ui.a4535311.js.gz +0 -0
  121. package/dist/web/public/ultracode-panel.js.gz +0 -0
  122. package/dist/web/public/ultracode-windows.js.gz +0 -0
  123. package/dist/web/public/upload.html.gz +0 -0
  124. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  125. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  126. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  127. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  128. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  129. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  130. package/dist/web/public/vendor/{xterm-predictive-echo.bd6882b8.js → xterm-predictive-echo.ae5e3d5d.js} +1 -1
  131. package/dist/web/public/vendor/{xterm-predictive-echo.bd6882b8.js.br → xterm-predictive-echo.ae5e3d5d.js.br} +0 -0
  132. package/dist/web/public/vendor/{xterm-predictive-echo.bd6882b8.js.gz → xterm-predictive-echo.ae5e3d5d.js.gz} +0 -0
  133. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +4 -0
  134. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
  135. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
  136. package/dist/web/public/vendor/xterm.css.gz +0 -0
  137. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  138. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  139. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  140. package/dist/web/public/webview-tabs.js +36 -14
  141. package/dist/web/public/webview-tabs.js.br +0 -0
  142. package/dist/web/public/webview-tabs.js.gz +0 -0
  143. package/dist/web/reboot-restore-registry.d.ts +2 -1
  144. package/dist/web/reboot-restore-registry.d.ts.map +1 -1
  145. package/dist/web/reboot-restore-registry.js +2 -1
  146. package/dist/web/reboot-restore-registry.js.map +1 -1
  147. package/dist/web/routes/file-routes.d.ts.map +1 -1
  148. package/dist/web/routes/file-routes.js +2 -1
  149. package/dist/web/routes/file-routes.js.map +1 -1
  150. package/dist/web/routes/index.d.ts +2 -0
  151. package/dist/web/routes/index.d.ts.map +1 -1
  152. package/dist/web/routes/index.js +2 -0
  153. package/dist/web/routes/index.js.map +1 -1
  154. package/dist/web/routes/mcp-sync-routes.d.ts +29 -0
  155. package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -0
  156. package/dist/web/routes/mcp-sync-routes.js +83 -0
  157. package/dist/web/routes/mcp-sync-routes.js.map +1 -0
  158. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  159. package/dist/web/routes/ralph-routes.js +2 -1
  160. package/dist/web/routes/ralph-routes.js.map +1 -1
  161. package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
  162. package/dist/web/routes/reboot-restore-routes.js +2 -0
  163. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  164. package/dist/web/routes/session-routes.d.ts.map +1 -1
  165. package/dist/web/routes/session-routes.js +40 -17
  166. package/dist/web/routes/session-routes.js.map +1 -1
  167. package/dist/web/routes/webhook-routes.d.ts +21 -0
  168. package/dist/web/routes/webhook-routes.d.ts.map +1 -0
  169. package/dist/web/routes/webhook-routes.js +87 -0
  170. package/dist/web/routes/webhook-routes.js.map +1 -0
  171. package/dist/web/schemas.d.ts +47 -2
  172. package/dist/web/schemas.d.ts.map +1 -1
  173. package/dist/web/schemas.js +64 -0
  174. package/dist/web/schemas.js.map +1 -1
  175. package/dist/web/server.d.ts +4 -0
  176. package/dist/web/server.d.ts.map +1 -1
  177. package/dist/web/server.js +55 -25
  178. package/dist/web/server.js.map +1 -1
  179. package/dist/webhook-notify.d.ts +71 -0
  180. package/dist/webhook-notify.d.ts.map +1 -0
  181. package/dist/webhook-notify.js +282 -0
  182. package/dist/webhook-notify.js.map +1 -0
  183. package/package.json +2 -1
  184. package/skills/codeman/SKILL.md +26 -8
  185. package/skills/codeman/preamble.sh +16 -4
  186. package/skills/codeman/reference/endpoints.md +11 -3
  187. package/skills/codeman/reference/recipes.md +1 -1
  188. package/dist/web/public/app.91131d2c.js +0 -43
  189. package/dist/web/public/app.91131d2c.js.br +0 -0
  190. package/dist/web/public/app.91131d2c.js.gz +0 -0
  191. package/dist/web/public/constants.cc927eb8.js.br +0 -0
  192. package/dist/web/public/constants.cc927eb8.js.gz +0 -0
  193. package/dist/web/public/i18n.f094d01f.js +0 -1
  194. package/dist/web/public/i18n.f094d01f.js.br +0 -0
  195. package/dist/web/public/i18n.f094d01f.js.gz +0 -0
  196. package/dist/web/public/panels-ui.67646701.js.br +0 -0
  197. package/dist/web/public/panels-ui.67646701.js.gz +0 -0
  198. package/dist/web/public/ralph-wizard.13a1831e.js.br +0 -0
  199. package/dist/web/public/sanitize-html.bc7078d6.js +0 -1
  200. package/dist/web/public/sanitize-html.bc7078d6.js.br +0 -0
  201. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  202. package/dist/web/public/session-ui.7575a231.js.br +0 -0
  203. package/dist/web/public/session-ui.7575a231.js.gz +0 -0
  204. package/dist/web/public/settings-ui.c68e7f59.js.br +0 -0
  205. package/dist/web/public/settings-ui.c68e7f59.js.gz +0 -0
  206. package/dist/web/public/styles.3fe3312d.css +0 -1
  207. package/dist/web/public/styles.3fe3312d.css.br +0 -0
  208. package/dist/web/public/styles.3fe3312d.css.gz +0 -0
  209. package/dist/web/public/terminal-ui.d5223c0e.js +0 -2
  210. package/dist/web/public/terminal-ui.d5223c0e.js.br +0 -0
  211. package/dist/web/public/terminal-ui.d5223c0e.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js +0 -4
  213. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.br +0 -0
  214. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview Webhook notifications (ntfy, Slack, Discord, generic JSON) for the events that
3
+ * already trigger Web Push, so a headless server can reach a phone without a browser tab or a
4
+ * push subscription.
5
+ *
6
+ * Split in three, so the parts that matter are testable without a network:
7
+ * - pure: `webhookUrlProblem`, `maskWebhookUrl`, `shouldSendWebhook`, `buildWebhookRequest`
8
+ * - store: `~/.codeman/webhook.json`, written 0600 via tmp+rename (the URL is a bearer secret:
9
+ * anyone holding a Slack/Discord webhook URL can post as it)
10
+ * - IO: `sendWebhook` (injected fetch) and `WebhookNotifier` (dedupe, in-flight cap, last result)
11
+ *
12
+ * Rules the code keeps and the tests pin:
13
+ * - The URL is configured only through the admin-only `/api/webhook` routes and kept OUT of
14
+ * `settings.json`, which every logged-in user can read through `GET /api/settings`.
15
+ * - Delivery goes through `webviewFetch`: link-local and cloud-metadata targets are refused on
16
+ * the RESOLVED address at connect time, redirects are not followed, and the call is bounded
17
+ * by a timeout. Loopback and LAN stay allowed on purpose (a local ntfy is the feature).
18
+ * - The URL never appears in a log line, a result, or an error message.
19
+ * - Session names and error text are user/agent-controlled, so they cannot ping a channel:
20
+ * Discord gets `allowed_mentions: { parse: [] }` and Slack control characters are escaped.
21
+ *
22
+ * @module webhook-notify
23
+ */
24
+ import { type WebhookConfig, type WebhookKind, type WebhookResult, type WebhookUrgency } from './types/push.js';
25
+ export declare const DEFAULT_WEBHOOK_CONFIG: WebhookConfig;
26
+ export interface WebhookMessage {
27
+ event: string;
28
+ title: string;
29
+ body: string;
30
+ urgency: WebhookUrgency;
31
+ sessionId?: string;
32
+ sessionName?: string;
33
+ /** The Codeman instance's window title, so several machines are told apart. */
34
+ host?: string;
35
+ }
36
+ /** Why `raw` cannot be a webhook URL, or null. Used at save time; delivery re-checks the resolved address. */
37
+ export declare function webhookUrlProblem(raw: string): string | null;
38
+ /** Scheme + host only: the path and query of a webhook URL are the secret. */
39
+ export declare function maskWebhookUrl(raw: string): string;
40
+ export declare function shouldSendWebhook(cfg: WebhookConfig, urgency: WebhookUrgency): boolean;
41
+ export interface WebhookRequest {
42
+ method: 'POST';
43
+ headers: Record<string, string>;
44
+ body: string;
45
+ }
46
+ export declare function buildWebhookRequest(kind: WebhookKind, msg: WebhookMessage, now?: Date): WebhookRequest;
47
+ export declare function webhookConfigPath(configDir: string): string;
48
+ export declare function readWebhookConfig(configDir: string): Promise<WebhookConfig>;
49
+ /** 0600 via tmp+rename: `mode` on writeFile only applies to a file being created. */
50
+ export declare function writeWebhookConfig(configDir: string, cfg: WebhookConfig): Promise<void>;
51
+ export type WebhookFetch = (target: URL, init: RequestInit) => Promise<Response>;
52
+ export declare function sendWebhook(cfg: Pick<WebhookConfig, 'kind' | 'url'>, msg: WebhookMessage, fetchImpl: WebhookFetch): Promise<WebhookResult>;
53
+ /**
54
+ * Sends the notifications the server decides on. Fire-and-forget by design (a slow webhook must
55
+ * never delay Web Push or a request), so it dedupes, caps what is in flight, and remembers only
56
+ * the last result for the Settings status line.
57
+ */
58
+ export declare class WebhookNotifier {
59
+ private readonly load;
60
+ private readonly fetchImpl;
61
+ private readonly now;
62
+ private lastSent;
63
+ private inFlight;
64
+ private last;
65
+ constructor(load: () => Promise<WebhookConfig>, fetchImpl: WebhookFetch, now?: () => number);
66
+ get lastResult(): WebhookResult | null;
67
+ notify(msg: WebhookMessage): Promise<void>;
68
+ /** A deliberate test send: bypasses `enabled`, scope and dedupe, and records the result. */
69
+ sendTest(cfg: Pick<WebhookConfig, 'kind' | 'url'>, host?: string): Promise<WebhookResult>;
70
+ }
71
+ //# sourceMappingURL=webhook-notify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-notify.d.ts","sourceRoot":"","sources":["../src/webhook-notify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAUzB,eAAO,MAAM,sBAAsB,EAAE,aAA6E,CAAC;AAEnH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa5D;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAGtF;AAoBD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,GAAE,IAAiB,GAAG,cAAc,CAgDlH;AAMD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAgBD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAMjF;AAED,qFAAqF;AACrF,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAW7F;AAMD,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAgBjF,wBAAsB,WAAW,CAC/B,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,KAAK,CAAC,EACxC,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,YAAY,GACtB,OAAO,CAAC,aAAa,CAAC,CAuBxB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAMxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,IAAI,CAA8B;gBAGvB,IAAI,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,EAClC,SAAS,EAAE,YAAY,EACvB,GAAG,GAAE,MAAM,MAAiB;IAG/C,IAAI,UAAU,IAAI,aAAa,GAAG,IAAI,CAErC;IAEK,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBhD,4FAA4F;IACtF,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;CAehG"}
@@ -0,0 +1,282 @@
1
+ /**
2
+ * @fileoverview Webhook notifications (ntfy, Slack, Discord, generic JSON) for the events that
3
+ * already trigger Web Push, so a headless server can reach a phone without a browser tab or a
4
+ * push subscription.
5
+ *
6
+ * Split in three, so the parts that matter are testable without a network:
7
+ * - pure: `webhookUrlProblem`, `maskWebhookUrl`, `shouldSendWebhook`, `buildWebhookRequest`
8
+ * - store: `~/.codeman/webhook.json`, written 0600 via tmp+rename (the URL is a bearer secret:
9
+ * anyone holding a Slack/Discord webhook URL can post as it)
10
+ * - IO: `sendWebhook` (injected fetch) and `WebhookNotifier` (dedupe, in-flight cap, last result)
11
+ *
12
+ * Rules the code keeps and the tests pin:
13
+ * - The URL is configured only through the admin-only `/api/webhook` routes and kept OUT of
14
+ * `settings.json`, which every logged-in user can read through `GET /api/settings`.
15
+ * - Delivery goes through `webviewFetch`: link-local and cloud-metadata targets are refused on
16
+ * the RESOLVED address at connect time, redirects are not followed, and the call is bounded
17
+ * by a timeout. Loopback and LAN stay allowed on purpose (a local ntfy is the feature).
18
+ * - The URL never appears in a log line, a result, or an error message.
19
+ * - Session names and error text are user/agent-controlled, so they cannot ping a channel:
20
+ * Discord gets `allowed_mentions: { parse: [] }` and Slack control characters are escaped.
21
+ *
22
+ * @module webhook-notify
23
+ */
24
+ import { existsSync, mkdirSync } from 'node:fs';
25
+ import fs from 'node:fs/promises';
26
+ import { join } from 'node:path';
27
+ import { blockedWebviewHostReason } from './web/webview-egress-policy.js';
28
+ import { isEgressBlockedError } from './web/webview-egress.js';
29
+ import { WEBHOOK_KINDS, WEBHOOK_SCOPES, } from './types/push.js';
30
+ const WEBHOOK_FILE = 'webhook.json';
31
+ const MAX_URL_LENGTH = 2048;
32
+ const SEND_TIMEOUT_MS = 5000;
33
+ const MAX_BODY_CHARS = 500;
34
+ /** Same event + session within this window is sent once: a flapping prompt must not flood a channel. */
35
+ const DEDUPE_WINDOW_MS = 3000;
36
+ const MAX_IN_FLIGHT = 5;
37
+ export const DEFAULT_WEBHOOK_CONFIG = { enabled: false, kind: 'ntfy', url: '', scope: 'attention' };
38
+ // ---------------------------------------------------------------------------
39
+ // Pure
40
+ // ---------------------------------------------------------------------------
41
+ /** Why `raw` cannot be a webhook URL, or null. Used at save time; delivery re-checks the resolved address. */
42
+ export function webhookUrlProblem(raw) {
43
+ if (raw.length > MAX_URL_LENGTH)
44
+ return 'URL is too long';
45
+ let url;
46
+ try {
47
+ url = new URL(raw);
48
+ }
49
+ catch {
50
+ return 'Not a valid URL';
51
+ }
52
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
53
+ return 'Only http and https URLs are allowed';
54
+ if (url.username || url.password)
55
+ return 'Put credentials in the path or a header-less token, not user:password@';
56
+ const blocked = blockedWebviewHostReason(url.hostname);
57
+ if (blocked)
58
+ return `Refused: ${blocked}`;
59
+ return null;
60
+ }
61
+ /** Scheme + host only: the path and query of a webhook URL are the secret. */
62
+ export function maskWebhookUrl(raw) {
63
+ try {
64
+ const url = new URL(raw);
65
+ return `${url.protocol}//${url.host}/•••`;
66
+ }
67
+ catch {
68
+ return '';
69
+ }
70
+ }
71
+ export function shouldSendWebhook(cfg, urgency) {
72
+ if (!cfg.enabled || !cfg.url)
73
+ return false;
74
+ return cfg.scope === 'all' || urgency !== 'info';
75
+ }
76
+ const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
77
+ /** A header value must be single-line printable ASCII; anything else goes out RFC 2047 encoded. */
78
+ function headerSafe(value) {
79
+ const oneLine = value.replace(/[\r\n]+/g, ' ').trim();
80
+ return /^[\x20-\x7e]*$/.test(oneLine) ? oneLine : `=?UTF-8?B?${Buffer.from(oneLine, 'utf8').toString('base64')}?=`;
81
+ }
82
+ /** Slack parses `<!channel>`, `<@U123>` and `<url|text>`; escaping the three control characters turns them to text. */
83
+ const slackEscape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
84
+ const NTFY_PRIORITY = { critical: '5', warning: '4', info: '3' };
85
+ const NTFY_TAGS = {
86
+ critical: 'rotating_light',
87
+ warning: 'bell',
88
+ info: 'white_check_mark',
89
+ };
90
+ export function buildWebhookRequest(kind, msg, now = new Date()) {
91
+ const title = clip(msg.title, 120);
92
+ const body = clip(msg.body, MAX_BODY_CHARS);
93
+ const prefix = msg.host ? `${clip(msg.host, 60)}: ` : '';
94
+ switch (kind) {
95
+ case 'ntfy':
96
+ return {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'text/plain; charset=utf-8',
100
+ Title: headerSafe(`${prefix}${title}`),
101
+ Priority: NTFY_PRIORITY[msg.urgency],
102
+ Tags: NTFY_TAGS[msg.urgency],
103
+ },
104
+ body: body || title,
105
+ };
106
+ case 'slack':
107
+ return {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ text: `*${slackEscape(`${prefix}${title}`)}*${body ? `\n${slackEscape(body)}` : ''}` }),
111
+ };
112
+ case 'discord':
113
+ return {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({
117
+ content: clip(`**${prefix}${title}**${body ? `\n${body}` : ''}`, 1900),
118
+ // Agent output and session names are not trusted to @everyone a channel.
119
+ allowed_mentions: { parse: [] },
120
+ }),
121
+ };
122
+ case 'generic':
123
+ return {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({
127
+ event: msg.event,
128
+ title,
129
+ body,
130
+ urgency: msg.urgency,
131
+ sessionId: msg.sessionId ?? null,
132
+ sessionName: msg.sessionName ?? null,
133
+ host: msg.host ?? null,
134
+ at: now.toISOString(),
135
+ }),
136
+ };
137
+ }
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Store
141
+ // ---------------------------------------------------------------------------
142
+ export function webhookConfigPath(configDir) {
143
+ return join(configDir, WEBHOOK_FILE);
144
+ }
145
+ function coerce(raw) {
146
+ const r = (typeof raw === 'object' && raw !== null ? raw : {});
147
+ return {
148
+ enabled: r.enabled === true,
149
+ kind: WEBHOOK_KINDS.includes(r.kind)
150
+ ? r.kind
151
+ : DEFAULT_WEBHOOK_CONFIG.kind,
152
+ url: typeof r.url === 'string' ? r.url : '',
153
+ scope: WEBHOOK_SCOPES.includes(r.scope)
154
+ ? r.scope
155
+ : DEFAULT_WEBHOOK_CONFIG.scope,
156
+ };
157
+ }
158
+ export async function readWebhookConfig(configDir) {
159
+ try {
160
+ return coerce(JSON.parse(await fs.readFile(webhookConfigPath(configDir), 'utf-8')));
161
+ }
162
+ catch {
163
+ return { ...DEFAULT_WEBHOOK_CONFIG };
164
+ }
165
+ }
166
+ /** 0600 via tmp+rename: `mode` on writeFile only applies to a file being created. */
167
+ export async function writeWebhookConfig(configDir, cfg) {
168
+ if (!existsSync(configDir))
169
+ mkdirSync(configDir, { recursive: true });
170
+ const target = webhookConfigPath(configDir);
171
+ const tmp = `${target}.${process.pid}.tmp`;
172
+ try {
173
+ await fs.writeFile(tmp, JSON.stringify(coerce(cfg), null, 2), { mode: 0o600 });
174
+ await fs.rename(tmp, target);
175
+ }
176
+ catch (err) {
177
+ await fs.unlink(tmp).catch(() => undefined);
178
+ throw err;
179
+ }
180
+ }
181
+ /**
182
+ * What went wrong, without the URL: blocked / timed out / refused / an HTTP status. An egress
183
+ * refusal is recognised by its `CODEMAN_EGRESS_BLOCKED` code anywhere in the cause chain (undici
184
+ * wraps the lookup's error as `TypeError('fetch failed', { cause })`), never by message text.
185
+ */
186
+ function describeError(err) {
187
+ const e = err;
188
+ if (e?.name === 'TimeoutError' || e?.name === 'AbortError')
189
+ return 'Timed out';
190
+ if (isEgressBlockedError(err))
191
+ return 'Refused: target is a link-local or cloud-metadata address';
192
+ if (e?.cause?.code === 'ENOTFOUND')
193
+ return 'Host not found';
194
+ if (e?.cause?.code === 'ECONNREFUSED')
195
+ return 'Connection refused';
196
+ return 'Network error';
197
+ }
198
+ export async function sendWebhook(cfg, msg, fetchImpl) {
199
+ const at = Date.now();
200
+ const problem = webhookUrlProblem(cfg.url);
201
+ if (problem)
202
+ return { ok: false, error: problem, at };
203
+ const req = buildWebhookRequest(cfg.kind, msg);
204
+ try {
205
+ const res = await fetchImpl(new URL(cfg.url), {
206
+ method: req.method,
207
+ headers: req.headers,
208
+ body: req.body,
209
+ redirect: 'manual',
210
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
211
+ });
212
+ void res.body?.cancel().catch(() => undefined);
213
+ if (res.status >= 300 && res.status < 400) {
214
+ return { ok: false, status: res.status, error: 'The URL redirects; use the final URL', at };
215
+ }
216
+ return res.ok
217
+ ? { ok: true, status: res.status, at }
218
+ : { ok: false, status: res.status, error: `HTTP ${res.status}`, at };
219
+ }
220
+ catch (err) {
221
+ return { ok: false, error: describeError(err), at };
222
+ }
223
+ }
224
+ /**
225
+ * Sends the notifications the server decides on. Fire-and-forget by design (a slow webhook must
226
+ * never delay Web Push or a request), so it dedupes, caps what is in flight, and remembers only
227
+ * the last result for the Settings status line.
228
+ */
229
+ export class WebhookNotifier {
230
+ load;
231
+ fetchImpl;
232
+ now;
233
+ lastSent = new Map();
234
+ inFlight = 0;
235
+ last = null;
236
+ constructor(load, fetchImpl, now = Date.now) {
237
+ this.load = load;
238
+ this.fetchImpl = fetchImpl;
239
+ this.now = now;
240
+ }
241
+ get lastResult() {
242
+ return this.last;
243
+ }
244
+ async notify(msg) {
245
+ const cfg = await this.load();
246
+ if (!shouldSendWebhook(cfg, msg.urgency))
247
+ return;
248
+ const key = `${msg.event}:${msg.sessionId ?? ''}`;
249
+ const t = this.now();
250
+ const prev = this.lastSent.get(key);
251
+ if (prev !== undefined && t - prev < DEDUPE_WINDOW_MS)
252
+ return;
253
+ if (this.inFlight >= MAX_IN_FLIGHT)
254
+ return;
255
+ this.lastSent.set(key, t);
256
+ if (this.lastSent.size > 256) {
257
+ for (const [k, v] of this.lastSent)
258
+ if (t - v > DEDUPE_WINDOW_MS)
259
+ this.lastSent.delete(k);
260
+ }
261
+ this.inFlight++;
262
+ try {
263
+ this.last = await sendWebhook(cfg, msg, this.fetchImpl);
264
+ }
265
+ finally {
266
+ this.inFlight--;
267
+ }
268
+ }
269
+ /** A deliberate test send: bypasses `enabled`, scope and dedupe, and records the result. */
270
+ async sendTest(cfg, host) {
271
+ const result = await sendWebhook(cfg, {
272
+ event: 'webhook:test',
273
+ title: 'Codeman test notification',
274
+ body: 'If you can read this, webhook notifications are working.',
275
+ urgency: 'info',
276
+ host,
277
+ }, this.fetchImpl);
278
+ this.last = result;
279
+ return result;
280
+ }
281
+ }
282
+ //# sourceMappingURL=webhook-notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-notify.js","sourceRoot":"","sources":["../src/webhook-notify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeman",
3
- "version": "1.33.2",
3
+ "version": "1.34.0",
4
4
  "description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -105,6 +105,7 @@
105
105
  "jpeg-js": "^0.4.4",
106
106
  "node-pty": "^1.1.0",
107
107
  "qrcode": "^1.5.4",
108
+ "smol-toml": "^1.9.0",
108
109
  "undici": "^6.28.0",
109
110
  "uuid": "^14.0.0",
110
111
  "web-push": "^3.6.7",
@@ -47,7 +47,7 @@ later call opens with, and your first REAL call performs them anyway:
47
47
 
48
48
  ```bash
49
49
  . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
50
- [ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
50
+ [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
51
51
  ```
52
52
 
53
53
  ⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
@@ -75,8 +75,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
75
75
  mkdir -p "$(dirname "$PRE")"
76
76
  # Rewrite unless the file already ends with THIS version's stamp, so a stale or a
77
77
  # half-written file self-heals here instead of costing you a round trip to rm it.
78
- grep -qs '^CODEMAN_PREAMBLE=1.30.1$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
79
- # ---- Codeman agent preamble 1.30.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
78
+ grep -qs '^CODEMAN_PREAMBLE=1.33.4$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
79
+ # ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
80
80
  API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
81
81
  SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
82
82
  # Credentials, cheapest first. Your session has usually INHERITED the server's
@@ -196,6 +196,11 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
196
196
  # rather than handed back, because a worker that never drew its composer would eat the
197
197
  # task prompt with its trust dialog. There is deliberately no pid poll: wait-output
198
198
  # already blocks until the composer draws, and pid!=null proved startup, never readiness.
199
+ # CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
200
+ # it is set Claude Code's advisor tool: a stronger model the worker consults before
201
+ # committing to an approach, on a recurring error and before declaring the task done.
202
+ # Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
203
+ # advisor that ranks below the worker's model is accepted but never attached by claude.
199
204
  spawn_worker() {
200
205
  local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
201
206
  # parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
@@ -207,12 +212,19 @@ spawn_worker() {
207
212
  # server clamps this back to `workspace-write` for an owner without the grant.
208
213
  # Spawn by hand (§5.1) when you want a worker that asks.
209
214
  q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
210
- -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
215
+ -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
211
216
  '{caseName:$n,mode:$m,parentSessionId:$p}
212
- + (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
217
+ + (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
218
+ + (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
213
219
  sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
214
220
  # NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
215
221
  [ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
222
+ # A server without advisor support DROPS the field instead of refusing it, so read it
223
+ # back: a worker silently missing the advisor it was asked for is worth one line.
224
+ if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
225
+ [ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
226
+ echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
227
+ fi
216
228
  if [ "$mode" = deepseek ]; then
217
229
  # The one non-claude mode with REAL end-of-turn signals: its TUI reports
218
230
  # idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
@@ -372,10 +384,10 @@ last_text() {
372
384
  # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
373
385
  # bare on purpose: the write condition above anchors on it with $, so an inline comment
374
386
  # here would fail that match and rewrite this file on every single bootstrap.
375
- CODEMAN_PREAMBLE=1.30.1
387
+ CODEMAN_PREAMBLE=1.33.4
376
388
  PREAMBLE
377
389
  )
378
- . "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
390
+ . "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
379
391
  ```
380
392
 
381
393
  Every later Bash call that touches the API starts with the same two loader lines from
@@ -426,7 +438,7 @@ and no per-call body to hand-build.
426
438
 
427
439
  ```bash
428
440
  . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
429
- [ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
441
+ [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
430
442
  N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
431
443
  # (a name may carry a mode: `beta:deepseek`, see below)
432
444
  T=('reply with one line: the absolute path of your working directory'
@@ -493,6 +505,12 @@ Four things this block leans on, each one link away, no detour needed to run it:
493
505
  `sendwait` reads the composer and keeps pressing Enter until the prompt has left it.
494
506
  All three are reasons to let `sendwait` build the call rather than hand-rolling it.
495
507
  - Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
508
+ - For long or high-stakes worker tasks, `CODEMAN_WORKER_ADVISOR=opus spawn_workers "${N[@]}"`
509
+ (`fable`, `opus` or `sonnet`) gives each claude worker Claude Code's advisor tool: a
510
+ stronger model it consults before committing to an approach, on a recurring error and
511
+ before declaring the task done. Advisor calls bill extra tokens, and an advisor ranked
512
+ below the worker's own model is never attached (on an Opus worker only `opus` and
513
+ `fable` do anything).
496
514
  - Deleting the sessions does **not** remove the case directories. They are marked as
497
515
  agent-created, so `GET /api/v1/cases/agent-created` lists them for cleanup: §5.14.
498
516
 
@@ -1,4 +1,4 @@
1
- # ---- Codeman agent preamble 1.30.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
1
+ # ---- Codeman agent preamble 1.33.4 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
2
2
  API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
3
3
  SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
4
4
  # Credentials, cheapest first. Your session has usually INHERITED the server's
@@ -118,6 +118,11 @@ _accept_trust() { # <sid> -> 0 once it has answered the dialog, 1 if it could n
118
118
  # rather than handed back, because a worker that never drew its composer would eat the
119
119
  # task prompt with its trust dialog. There is deliberately no pid poll: wait-output
120
120
  # already blocks until the composer draws, and pid!=null proved startup, never readiness.
121
+ # CODEMAN_WORKER_ADVISOR=opus (or fable / sonnet) gives every CLAUDE worker spawned while
122
+ # it is set Claude Code's advisor tool: a stronger model the worker consults before
123
+ # committing to an approach, on a recurring error and before declaring the task done.
124
+ # Other modes ignore it. A value the server refuses fails the spawn (INVALID_INPUT); an
125
+ # advisor that ranks below the worker's model is accepted but never attached by claude.
121
126
  spawn_worker() {
122
127
  local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
123
128
  # parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
@@ -129,12 +134,19 @@ spawn_worker() {
129
134
  # server clamps this back to `workspace-write` for an owner without the grant.
130
135
  # Spawn by hand (§5.1) when you want a worker that asks.
131
136
  q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
132
- -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
137
+ -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" --arg a "${CODEMAN_WORKER_ADVISOR:-}" \
133
138
  '{caseName:$n,mode:$m,parentSessionId:$p}
134
- + (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
139
+ + (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)
140
+ + (if $m == "claude" and $a != "" then {advisorModel:$a} else {} end)')")
135
141
  sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
136
142
  # NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
137
143
  [ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
144
+ # A server without advisor support DROPS the field instead of refusing it, so read it
145
+ # back: a worker silently missing the advisor it was asked for is worth one line.
146
+ if [ "$mode" = claude ] && [ -n "${CODEMAN_WORKER_ADVISOR:-}" ] &&
147
+ [ "$("${CURL[@]}" "$API/api/v1/sessions/$sid" | jq -r '.data.advisorModel // empty')" != "$CODEMAN_WORKER_ADVISOR" ]; then
148
+ echo "worker $sid: this Codeman server ignored CODEMAN_WORKER_ADVISOR (no advisor support); it runs without one" >&2
149
+ fi
138
150
  if [ "$mode" = deepseek ]; then
139
151
  # The one non-claude mode with REAL end-of-turn signals: its TUI reports
140
152
  # idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
@@ -294,4 +306,4 @@ last_text() {
294
306
  # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
295
307
  # bare on purpose: the write condition above anchors on it with $, so an inline comment
296
308
  # here would fail that match and rewrite this file on every single bootstrap.
297
- CODEMAN_PREAMBLE=1.30.1
309
+ CODEMAN_PREAMBLE=1.33.4
@@ -345,6 +345,13 @@ ESC=$(printf '\033')
345
345
  `.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
346
346
  on the user's disk) if missing, do not retry it in a loop, and remember the name.
347
347
 
348
+ A claude worker also takes `"advisorModel":"opus"` (`fable`, `opus`, `sonnet` or a full
349
+ model id): Claude Code's advisor tool, a stronger model the worker consults before
350
+ committing to an approach, on a recurring error and before declaring the task done. It is
351
+ a soft default the worker can change with `/advisor`. Remote and docker cases refuse it
352
+ (400), as they refuse `effort`. `spawn_worker` and `spawn_workers` send it for you when
353
+ `CODEMAN_WORKER_ADVISOR` is set.
354
+
348
355
  ⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
349
356
  `OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
350
357
  the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
@@ -384,17 +391,18 @@ every claude create path installs them, so a linked case and a raw path both get
384
391
 
385
392
  **The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
386
393
  a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
387
- `envOverrides`). Three differences that break copied code:
394
+ `advisorModel`, `envOverrides`, and for claude a per-session `model` passed as `--model`). Three
395
+ differences that break copied code:
388
396
 
389
397
  - The id is at **`.data.session.id`**, not quick-start's `.data.sessionId`
390
- (`session-routes.ts:878` returns `{ session: lightState }`).
398
+ (the `POST /api/sessions` handler in `session-routes.ts` returns `{ session: lightState }`).
391
399
  - **It spawns no PTY.** The session exists with `pid:null` and nothing running, so
392
400
  `wait?until=exit` answers `exit` immediately. Follow it with
393
401
  `POST /api/v1/sessions/:id/interactive` (claude and the other agent CLIs) or
394
402
  `POST /api/v1/sessions/:id/shell` (shell mode) to actually start the worker.
395
403
  - Its capacity failure is **`OPERATION_FAILED` (422)**, not quick-start's
396
404
  `SESSION_BUSY` (409), from the same global-50 / per-user-25 caps
397
- (`session-routes.ts:648`).
405
+ (`sessionCapacityMessage()` in `route-helpers.ts`).
398
406
 
399
407
  ⚠️ `POST .../interactive` accepts `{"clearBreaker":true}`, which resets the **PTY-exit
400
408
  circuit breaker**. That breaker exists to stop a session that keeps crashing on spawn
@@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version
21
21
 
22
22
  ```bash
23
23
  . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
24
- [ "${CODEMAN_PREAMBLE:-}" = 1.30.1 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
24
+ [ "${CODEMAN_PREAMBLE:-}" = 1.33.4 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
25
25
  ```
26
26
 
27
27
  Do **not** re-paste the preamble body into each call. Sourcing it is what retires the