@decibelsystems/tools 3.0.0 → 3.1.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 (253) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/agentCommands.d.ts +0 -1
  3. package/dist/agentCommands.d.ts.map +1 -1
  4. package/dist/agentCommands.js +19 -8
  5. package/dist/agentCommands.js.map +1 -1
  6. package/dist/agentPresence.d.ts +5 -3
  7. package/dist/agentPresence.d.ts.map +1 -1
  8. package/dist/agentPresence.js +20 -29
  9. package/dist/agentPresence.js.map +1 -1
  10. package/dist/architectAdrs.d.ts +13 -1
  11. package/dist/architectAdrs.d.ts.map +1 -1
  12. package/dist/architectAdrs.js +83 -1
  13. package/dist/architectAdrs.js.map +1 -1
  14. package/dist/artifacts.d.ts +60 -0
  15. package/dist/artifacts.d.ts.map +1 -0
  16. package/dist/artifacts.js +208 -0
  17. package/dist/artifacts.js.map +1 -0
  18. package/dist/asks.d.ts +93 -0
  19. package/dist/asks.d.ts.map +1 -0
  20. package/dist/asks.js +383 -0
  21. package/dist/asks.js.map +1 -0
  22. package/dist/chat.d.ts +47 -0
  23. package/dist/chat.d.ts.map +1 -0
  24. package/dist/chat.js +288 -0
  25. package/dist/chat.js.map +1 -0
  26. package/dist/daemon.d.ts +14 -3
  27. package/dist/daemon.d.ts.map +1 -1
  28. package/dist/daemon.js +44 -14
  29. package/dist/daemon.js.map +1 -1
  30. package/dist/daemonConfig.d.ts +22 -0
  31. package/dist/daemonConfig.d.ts.map +1 -1
  32. package/dist/daemonConfig.js +11 -0
  33. package/dist/daemonConfig.js.map +1 -1
  34. package/dist/domain/issue.d.ts +19 -15
  35. package/dist/domain/issue.d.ts.map +1 -1
  36. package/dist/domain/issue.js +4 -32
  37. package/dist/domain/issue.js.map +1 -1
  38. package/dist/domain/issueCodec.d.ts.map +1 -1
  39. package/dist/domain/issueCodec.js +10 -0
  40. package/dist/domain/issueCodec.js.map +1 -1
  41. package/dist/domain/issueRepository.d.ts +35 -2
  42. package/dist/domain/issueRepository.d.ts.map +1 -1
  43. package/dist/domain/issueRepository.js +40 -1
  44. package/dist/domain/issueRepository.js.map +1 -1
  45. package/dist/domain/uid.d.ts +16 -0
  46. package/dist/domain/uid.d.ts.map +1 -0
  47. package/dist/domain/uid.js +43 -0
  48. package/dist/domain/uid.js.map +1 -0
  49. package/dist/facades/definitions.d.ts.map +1 -1
  50. package/dist/facades/definitions.js +46 -3
  51. package/dist/facades/definitions.js.map +1 -1
  52. package/dist/hq/defineAgent.d.ts.map +1 -1
  53. package/dist/hq/defineAgent.js +7 -11
  54. package/dist/hq/defineAgent.js.map +1 -1
  55. package/dist/httpServer.d.ts.map +1 -1
  56. package/dist/httpServer.js +201 -48
  57. package/dist/httpServer.js.map +1 -1
  58. package/dist/identity.d.ts +123 -0
  59. package/dist/identity.d.ts.map +1 -0
  60. package/dist/identity.js +312 -0
  61. package/dist/identity.js.map +1 -0
  62. package/dist/kernel.d.ts.map +1 -1
  63. package/dist/kernel.js +24 -1
  64. package/dist/kernel.js.map +1 -1
  65. package/dist/killSwitch.d.ts +117 -0
  66. package/dist/killSwitch.d.ts.map +1 -0
  67. package/dist/killSwitch.js +161 -0
  68. package/dist/killSwitch.js.map +1 -0
  69. package/dist/lib/atomicWrite.d.ts +23 -0
  70. package/dist/lib/atomicWrite.d.ts.map +1 -1
  71. package/dist/lib/atomicWrite.js +84 -0
  72. package/dist/lib/atomicWrite.js.map +1 -1
  73. package/dist/lib/recordIdAllocator.d.ts +21 -5
  74. package/dist/lib/recordIdAllocator.d.ts.map +1 -1
  75. package/dist/lib/recordIdAllocator.js +25 -7
  76. package/dist/lib/recordIdAllocator.js.map +1 -1
  77. package/dist/orgId.d.ts +23 -0
  78. package/dist/orgId.d.ts.map +1 -0
  79. package/dist/orgId.js +73 -0
  80. package/dist/orgId.js.map +1 -0
  81. package/dist/pairCli.d.ts +2 -0
  82. package/dist/pairCli.d.ts.map +1 -0
  83. package/dist/pairCli.js +97 -0
  84. package/dist/pairCli.js.map +1 -0
  85. package/dist/peerBroker.d.ts +40 -0
  86. package/dist/peerBroker.d.ts.map +1 -0
  87. package/dist/peerBroker.js +70 -0
  88. package/dist/peerBroker.js.map +1 -0
  89. package/dist/peerBrokerSupervisor.d.ts +21 -0
  90. package/dist/peerBrokerSupervisor.d.ts.map +1 -0
  91. package/dist/peerBrokerSupervisor.js +131 -0
  92. package/dist/peerBrokerSupervisor.js.map +1 -0
  93. package/dist/projectPaths.d.ts.map +1 -1
  94. package/dist/projectPaths.js +1 -19
  95. package/dist/projectPaths.js.map +1 -1
  96. package/dist/projectRegistry.d.ts +22 -0
  97. package/dist/projectRegistry.d.ts.map +1 -1
  98. package/dist/projectRegistry.js +58 -2
  99. package/dist/projectRegistry.js.map +1 -1
  100. package/dist/pty/boundary.d.ts +39 -0
  101. package/dist/pty/boundary.d.ts.map +1 -0
  102. package/dist/pty/boundary.js +100 -0
  103. package/dist/pty/boundary.js.map +1 -0
  104. package/dist/pty/errors.d.ts +6 -0
  105. package/dist/pty/errors.d.ts.map +1 -0
  106. package/dist/pty/errors.js +12 -0
  107. package/dist/pty/errors.js.map +1 -0
  108. package/dist/pty/fleet.d.ts +159 -0
  109. package/dist/pty/fleet.d.ts.map +1 -0
  110. package/dist/pty/fleet.js +543 -0
  111. package/dist/pty/fleet.js.map +1 -0
  112. package/dist/pty/launchProfile.d.ts +45 -0
  113. package/dist/pty/launchProfile.d.ts.map +1 -0
  114. package/dist/pty/launchProfile.js +149 -0
  115. package/dist/pty/launchProfile.js.map +1 -0
  116. package/dist/pty/openTerminal.d.ts +48 -0
  117. package/dist/pty/openTerminal.d.ts.map +1 -0
  118. package/dist/pty/openTerminal.js +185 -0
  119. package/dist/pty/openTerminal.js.map +1 -0
  120. package/dist/pty/pairDialog.d.ts +16 -0
  121. package/dist/pty/pairDialog.d.ts.map +1 -0
  122. package/dist/pty/pairDialog.js +121 -0
  123. package/dist/pty/pairDialog.js.map +1 -0
  124. package/dist/pty/pairing.d.ts +86 -0
  125. package/dist/pty/pairing.d.ts.map +1 -0
  126. package/dist/pty/pairing.js +351 -0
  127. package/dist/pty/pairing.js.map +1 -0
  128. package/dist/pty/routes.d.ts +49 -0
  129. package/dist/pty/routes.d.ts.map +1 -0
  130. package/dist/pty/routes.js +624 -0
  131. package/dist/pty/routes.js.map +1 -0
  132. package/dist/pty/sessions.d.ts +167 -0
  133. package/dist/pty/sessions.d.ts.map +1 -0
  134. package/dist/pty/sessions.js +513 -0
  135. package/dist/pty/sessions.js.map +1 -0
  136. package/dist/pty/tmux.d.ts +109 -0
  137. package/dist/pty/tmux.d.ts.map +1 -0
  138. package/dist/pty/tmux.js +271 -0
  139. package/dist/pty/tmux.js.map +1 -0
  140. package/dist/roster.d.ts +37 -0
  141. package/dist/roster.d.ts.map +1 -0
  142. package/dist/roster.js +126 -0
  143. package/dist/roster.js.map +1 -0
  144. package/dist/runtime/daemonMeta.d.ts +53 -0
  145. package/dist/runtime/daemonMeta.d.ts.map +1 -0
  146. package/dist/runtime/daemonMeta.js +121 -0
  147. package/dist/runtime/daemonMeta.js.map +1 -0
  148. package/dist/runtime/ensureRuntime.d.ts.map +1 -1
  149. package/dist/runtime/ensureRuntime.js +5 -10
  150. package/dist/runtime/ensureRuntime.js.map +1 -1
  151. package/dist/sentinelIssues.d.ts +16 -0
  152. package/dist/sentinelIssues.d.ts.map +1 -1
  153. package/dist/sentinelIssues.js +42 -1
  154. package/dist/sentinelIssues.js.map +1 -1
  155. package/dist/server.js +66 -5
  156. package/dist/server.js.map +1 -1
  157. package/dist/toolConfig.d.ts.map +1 -1
  158. package/dist/toolConfig.js +12 -0
  159. package/dist/toolConfig.js.map +1 -1
  160. package/dist/tools/agentic/agentQueue.d.ts.map +1 -1
  161. package/dist/tools/agentic/agentQueue.js +4 -0
  162. package/dist/tools/agentic/agentQueue.js.map +1 -1
  163. package/dist/tools/architect/index.d.ts +1 -0
  164. package/dist/tools/architect/index.d.ts.map +1 -1
  165. package/dist/tools/architect/index.js +45 -1
  166. package/dist/tools/architect/index.js.map +1 -1
  167. package/dist/tools/architect.d.ts.map +1 -1
  168. package/dist/tools/architect.js +59 -8
  169. package/dist/tools/architect.js.map +1 -1
  170. package/dist/tools/artifacts.d.ts +5 -0
  171. package/dist/tools/artifacts.d.ts.map +1 -0
  172. package/dist/tools/artifacts.js +89 -0
  173. package/dist/tools/artifacts.js.map +1 -0
  174. package/dist/tools/asks.d.ts +6 -0
  175. package/dist/tools/asks.d.ts.map +1 -0
  176. package/dist/tools/asks.js +104 -0
  177. package/dist/tools/asks.js.map +1 -0
  178. package/dist/tools/coordinator/index.d.ts +1 -0
  179. package/dist/tools/coordinator/index.d.ts.map +1 -1
  180. package/dist/tools/coordinator/index.js +42 -2
  181. package/dist/tools/coordinator/index.js.map +1 -1
  182. package/dist/tools/data-inspector.d.ts +1 -1
  183. package/dist/tools/data-inspector.d.ts.map +1 -1
  184. package/dist/tools/data-inspector.js +1 -1
  185. package/dist/tools/data-inspector.js.map +1 -1
  186. package/dist/tools/dojo/index.d.ts.map +1 -1
  187. package/dist/tools/dojo/index.js +52 -3
  188. package/dist/tools/dojo/index.js.map +1 -1
  189. package/dist/tools/dojo.d.ts +62 -1
  190. package/dist/tools/dojo.d.ts.map +1 -1
  191. package/dist/tools/dojo.js +84 -1
  192. package/dist/tools/dojo.js.map +1 -1
  193. package/dist/tools/guardian.d.ts +7 -0
  194. package/dist/tools/guardian.d.ts.map +1 -1
  195. package/dist/tools/guardian.js +146 -9
  196. package/dist/tools/guardian.js.map +1 -1
  197. package/dist/tools/index.d.ts.map +1 -1
  198. package/dist/tools/index.js +6 -0
  199. package/dist/tools/index.js.map +1 -1
  200. package/dist/tools/killswitch/index.d.ts +13 -0
  201. package/dist/tools/killswitch/index.d.ts.map +1 -0
  202. package/dist/tools/killswitch/index.js +138 -0
  203. package/dist/tools/killswitch/index.js.map +1 -0
  204. package/dist/tools/oracle/index.d.ts.map +1 -1
  205. package/dist/tools/oracle/index.js +8 -0
  206. package/dist/tools/oracle/index.js.map +1 -1
  207. package/dist/tools/oracle.d.ts +7 -0
  208. package/dist/tools/oracle.d.ts.map +1 -1
  209. package/dist/tools/oracle.js +22 -6
  210. package/dist/tools/oracle.js.map +1 -1
  211. package/dist/tools/provenance.d.ts +11 -2
  212. package/dist/tools/provenance.d.ts.map +1 -1
  213. package/dist/tools/provenance.js +26 -24
  214. package/dist/tools/provenance.js.map +1 -1
  215. package/dist/tools/registry/index.d.ts.map +1 -1
  216. package/dist/tools/registry/index.js +24 -0
  217. package/dist/tools/registry/index.js.map +1 -1
  218. package/dist/tools/sentinel/index.d.ts.map +1 -1
  219. package/dist/tools/sentinel/index.js +54 -2
  220. package/dist/tools/sentinel/index.js.map +1 -1
  221. package/dist/tools/sentinel.d.ts +50 -0
  222. package/dist/tools/sentinel.d.ts.map +1 -1
  223. package/dist/tools/sentinel.js +72 -7
  224. package/dist/tools/sentinel.js.map +1 -1
  225. package/dist/tools/shared/runTracker.d.ts +2 -0
  226. package/dist/tools/shared/runTracker.d.ts.map +1 -1
  227. package/dist/tools/shared/runTracker.js +38 -0
  228. package/dist/tools/shared/runTracker.js.map +1 -1
  229. package/dist/tools/swarm.d.ts +3 -1
  230. package/dist/tools/swarm.d.ts.map +1 -1
  231. package/dist/tools/swarm.js +6 -12
  232. package/dist/tools/swarm.js.map +1 -1
  233. package/dist/tools/voice/index.d.ts +1 -0
  234. package/dist/tools/voice/index.d.ts.map +1 -1
  235. package/dist/tools/voice/index.js +33 -2
  236. package/dist/tools/voice/index.js.map +1 -1
  237. package/dist/tools/voice.d.ts +48 -1
  238. package/dist/tools/voice.d.ts.map +1 -1
  239. package/dist/tools/voice.js +63 -3
  240. package/dist/tools/voice.js.map +1 -1
  241. package/dist/transports/bridge.d.ts.map +1 -1
  242. package/dist/transports/bridge.js +2 -1
  243. package/dist/transports/bridge.js.map +1 -1
  244. package/dist/transports/mcp.d.ts.map +1 -1
  245. package/dist/transports/mcp.js +2 -1
  246. package/dist/transports/mcp.js.map +1 -1
  247. package/dist/version.d.ts +5 -0
  248. package/dist/version.d.ts.map +1 -0
  249. package/dist/version.js +23 -0
  250. package/dist/version.js.map +1 -0
  251. package/package.json +13 -2
  252. package/templates/AGENT.md +3 -0
  253. package/templates/CLAUDE-decibel.md +11 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../../src/pty/tmux.ts"],"names":[],"mappings":"AAmBA,qFAAqF;AACrF,eAAO,MAAM,cAAc,SAAS,CAAC;AAOrC,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,CAAC,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAcvD;AAED,6EAA6E;AAC7E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,QAGmF,CAAC;AAE7G,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAElD;AAwBD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAE/E;AAED,wBAAgB,QAAQ,CAAC,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAetF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAIlF;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAM9F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,YAAY,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,SAAM,GAAG,MAAM,GAAG,SAAS,CAI/E;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,IAAI,EAAE,OAAO,CAAC;CACf;AAgBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKlD;AAgBD,yGAAyG;AACzG,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAG7E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAgB3D;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAY/D;AAED,oEAAoE;AACpE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAEhF"}
@@ -0,0 +1,271 @@
1
+ // ============================================================================
2
+ // tmux — the substrate hosted sessions live in (ADR-0012 / decibel-hq EPIC-0015)
3
+ // ============================================================================
4
+ // Every call is execFile with an argv array, never a shell string, so a session
5
+ // name or label can never become a command. Session targets use tmux's `=name`
6
+ // exact-match form: a bare `-t name` is a PREFIX match, so `-t dcb-a` would
7
+ // happily hit `dcb-abc`.
8
+ //
9
+ // Session metadata (project, branch, issue label) is stored ON the tmux session
10
+ // as user options (@dcb_*), not in a daemon-side file. It then lives exactly as
11
+ // long as the session does and survives a daemon restart with nothing to
12
+ // reconcile.
13
+ // ============================================================================
14
+ import { execFile } from 'child_process';
15
+ import { existsSync } from 'fs';
16
+ import { log } from '../config.js';
17
+ import { SessionError } from './errors.js';
18
+ /** Prefix for sessions the daemon spawned. Anything else is adopted, never owned. */
19
+ export const SPAWNED_PREFIX = 'dcb-';
20
+ /**
21
+ * launchd does not give the daemon a login PATH, so `tmux` alone may not
22
+ * resolve. Probe the usual install locations first.
23
+ */
24
+ let tmuxPath = null;
25
+ export function resolveTmux() {
26
+ if (tmuxPath)
27
+ return tmuxPath;
28
+ for (const p of ['/opt/homebrew/bin/tmux', '/usr/local/bin/tmux', '/usr/bin/tmux']) {
29
+ if (existsSync(p))
30
+ return (tmuxPath = p);
31
+ }
32
+ return (tmuxPath = 'tmux');
33
+ }
34
+ /** Test seam: forget the cached binary path. */
35
+ export function _resetTmuxPath(p = null) {
36
+ tmuxPath = p;
37
+ }
38
+ export const OWNED_SOCKET_DEFAULT = 'decibel';
39
+ export function socketArgs(server) {
40
+ // -f /dev/null: the owned server must not read ~/.tmux.conf. HQ's pane needs
41
+ // predictable behaviour — no custom prefix, and no `mouse on`, whose mouse
42
+ // capture breaks xterm.js selection. (-f only matters when the server starts;
43
+ // passing it on every call is harmless.)
44
+ //
45
+ // -u on every call: launchd gives the daemon no locale, and a tmux client
46
+ // without one is treated as non-UTF-8 — every non-ASCII glyph Claude draws
47
+ // (❯, ⏵⏵, the logo) arrived in HQ as '_'. cleanEnv also supplies a UTF-8
48
+ // LANG/LC_CTYPE, so a server started by the daemon is UTF-8 too. Found by
49
+ // decibel-hq on its first live pass.
50
+ if (server === 'owned')
51
+ return ['-u', '-L', process.env.DECIBEL_TMUX_SOCKET || OWNED_SOCKET_DEFAULT, '-f', '/dev/null'];
52
+ const user = process.env.DECIBEL_TMUX_USER_SOCKET;
53
+ return user ? ['-u', '-L', user] : ['-u'];
54
+ }
55
+ /** Which server a session key lives on: the prefix is the ownership mark. */
56
+ /**
57
+ * Terminal title for clients of the owned server (set-titles): the repo, plus
58
+ * the issue label, or else a branch other than main/master. Built from the
59
+ * @dcb_* options the daemon sets at spawn, which cleanLabel has already
60
+ * stripped of control characters. tmux sends it as OSC 2 to every attached
61
+ * client, so a Ghostty tab is titled without any title text in its command
62
+ * (ISS-0210).
63
+ */
64
+ export const TITLE_FORMAT = '#{?@dcb_project,#{@dcb_project},#{session_name}}' +
65
+ '#{?@dcb_issue, · #{@dcb_issue},' +
66
+ '#{?#{&&:#{@dcb_branch},#{&&:#{!=:#{@dcb_branch},main},#{!=:#{@dcb_branch},master}}}, · #{@dcb_branch},}}';
67
+ export function serverFor(name) {
68
+ return name.startsWith(SPAWNED_PREFIX) ? 'owned' : 'user';
69
+ }
70
+ /**
71
+ * The environment every tmux client and spawned claude gets. TMUX is removed so
72
+ * a daemon started inside tmux does not make nested-client calls. CLAUDECODE,
73
+ * CLAUDE_CODE_* and CLAUDE_PID are removed because claude refuses to start when
74
+ * it sees them (the nested-session guard): a daemon ever launched from inside a
75
+ * Claude Code session would otherwise start an owned server whose every session
76
+ * dies instantly. Found by decibel-hq against its mock of this contract.
77
+ */
78
+ /**
79
+ * The daemon's own environment, which a spawned agent must not inherit
80
+ * (ISS-0225). launchd starts the daemon with its env file loaded and
81
+ * NODE_ENV=production, and before this every hosted Claude session held them
82
+ * all, including SUPABASE_SERVICE_KEY, which bypasses RLS on Core. Only the
83
+ * daemon writes with service_role; an agent and anything it runs must not.
84
+ * NODE_ENV=production also makes npm/pnpm skip and prune devDependencies.
85
+ *
86
+ * Matched by name: credentials by suffix, plus the daemon-only config.
87
+ * ANTHROPIC_* is kept, so Claude Code's own auth is untouched.
88
+ */
89
+ const DAEMON_ONLY = new Set(['NODE_ENV', 'SUPABASE_URL', 'SUPABASE_ANON_KEY', 'SUPABASE_SERVICE_KEY', 'SUPABASE_SERVICE_ROLE_KEY', 'DECIBEL_ORG_ID']);
90
+ const CREDENTIAL_RE = /(_KEY|_TOKEN|_SECRET|PASSWORD|_DATABASE_URL|_DSN)$/i;
91
+ export function isDaemonOnlyVar(name) {
92
+ if (name.startsWith('ANTHROPIC_'))
93
+ return false;
94
+ return DAEMON_ONLY.has(name) || CREDENTIAL_RE.test(name);
95
+ }
96
+ /** Names in `base` that cleanEnv drops for being the daemon's. Names only, never values. */
97
+ export function daemonOnlyNames(base = process.env) {
98
+ return Object.keys(base).filter((k) => base[k] !== undefined && isDaemonOnlyVar(k)).sort();
99
+ }
100
+ export function cleanEnv(base = process.env) {
101
+ const env = {};
102
+ for (const [k, v] of Object.entries(base)) {
103
+ if (v === undefined)
104
+ continue;
105
+ if (k === 'TMUX' || k === 'TMUX_PANE' || k === 'CLAUDECODE' || k === 'CLAUDE_PID' || k.startsWith('CLAUDE_CODE_'))
106
+ continue;
107
+ if (isDaemonOnlyVar(k))
108
+ continue;
109
+ env[k] = v;
110
+ }
111
+ // No locale at all (launchd) means tmux and claude both fall back to ASCII.
112
+ // Supply UTF-8 only when nothing is set, so a user's own choice stands.
113
+ if (!env.LANG && !env.LC_ALL && !env.LC_CTYPE) {
114
+ env.LANG = 'en_US.UTF-8';
115
+ env.LC_CTYPE = 'en_US.UTF-8';
116
+ }
117
+ return env;
118
+ }
119
+ /**
120
+ * tmux keeps the environment its server started with as the GLOBAL
121
+ * environment and copies it into every new session. A server started before
122
+ * ISS-0225 already holds the daemon's variables, so cleaning the client env
123
+ * is not enough: unset them there too. Applied before every spawn and at
124
+ * daemon start.
125
+ */
126
+ export function unsetDaemonEnvArgs(base = process.env) {
127
+ const out = [];
128
+ for (const name of daemonOnlyNames(base))
129
+ out.push(';', 'set-environment', '-g', '-u', name);
130
+ return out.slice(1);
131
+ }
132
+ export function tmux(server, args, timeoutMs = 5000) {
133
+ return new Promise((resolve) => {
134
+ execFile(resolveTmux(), [...socketArgs(server), ...args], { timeout: timeoutMs, env: cleanEnv() }, (err, stdout, stderr) => {
135
+ resolve({ ok: !err, stdout: String(stdout), stderr: String(stderr) || (err ? err.message : '') });
136
+ });
137
+ });
138
+ }
139
+ /**
140
+ * Exact-match session target, for commands that take a SESSION target:
141
+ * has-session, kill-session, attach-session.
142
+ */
143
+ export function sessionTarget(name) {
144
+ return `=${name}`;
145
+ }
146
+ /**
147
+ * Exact-match target for everything that parses -t as a pane or window:
148
+ * set-option, show-options, send-keys, respawn-pane. tmux 3.5a rejects a bare
149
+ * `=name` there with "no such session"; the trailing colon is required
150
+ * (verified 2026-09-25, and hit independently by decibel-hq's mock).
151
+ */
152
+ export function paneTarget(name) {
153
+ return `=${name}:`;
154
+ }
155
+ /**
156
+ * Field separator for -F output. PRINTABLE on purpose: with no LANG/LC_* in
157
+ * the environment — which is exactly the launchd daemon's environment — tmux
158
+ * rewrites a TAB in format output as '_', every field collapsed into the
159
+ * session name, and no spawned session ever matched its project. The tests
160
+ * passed because the shell running them had a locale. Pinned by a test that
161
+ * lists with the locale removed.
162
+ */
163
+ export const FIELD_SEP = '|:dcb:|';
164
+ /**
165
+ * Newlines and the field separator would corrupt list output, and a label has
166
+ * no business being long. Applied to every value written into a tmux option.
167
+ */
168
+ export function cleanLabel(v, max = 200) {
169
+ if (v === undefined || v === null)
170
+ return undefined;
171
+ const cleaned = String(v).split(FIELD_SEP).join(' ').replace(/[\t\r\n]/g, ' ').trim().slice(0, max);
172
+ return cleaned || undefined;
173
+ }
174
+ const LIST_FORMAT = [
175
+ '#{session_name}',
176
+ '#{@dcb_project}',
177
+ '#{@dcb_branch}',
178
+ '#{@dcb_issue}',
179
+ '#{@dcb_uuid}',
180
+ '#{session_created}',
181
+ '#{pane_current_path}',
182
+ '#{pane_dead}',
183
+ // #{pane_current_path} is empty once the pane is dead, so spawned sessions
184
+ // also record their cwd as an option (found by decibel-hq's mock).
185
+ '#{@dcb_cwd}',
186
+ ].join(FIELD_SEP);
187
+ /**
188
+ * "No server" is a normal answer; "could not ask" is not. A missing socket
189
+ * means no server, so no sessions. Any other failure — a server that exists
190
+ * but did not answer — must NOT read as an empty list, because HQ would render
191
+ * a live session as gone. It gets one retry and then throws. (A spawned session
192
+ * once went unlisted for minutes while its server was plainly up; the old check
193
+ * swallowed every "error connecting to" as "no server". Cause unverified.)
194
+ */
195
+ export function isNoServer(stderr) {
196
+ // A stale socket file (the server died without removing it) refuses the
197
+ // connection; that is also "no server".
198
+ return /no server running/.test(stderr) ||
199
+ /error connecting to .*\((No such file or directory|Connection refused)\)/.test(stderr);
200
+ }
201
+ async function tmuxRead(server, args) {
202
+ for (let attempt = 0; attempt < 2; attempt++) {
203
+ const r = await tmux(server, args);
204
+ if (r.ok)
205
+ return r;
206
+ if (isNoServer(r.stderr))
207
+ return null;
208
+ log(`Sessions: tmux ${args[0]} (${server}) failed${attempt ? '' : ', retrying'}: ${r.stderr.trim() || 'no stderr'}`);
209
+ if (!attempt)
210
+ await new Promise((res) => setTimeout(res, 250));
211
+ else {
212
+ throw new SessionError(503, 'TMUX_UNAVAILABLE', `The ${server} tmux server did not answer: ${r.stderr.trim() || 'no stderr'}`);
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ /** Every session on one server. Empty when that server is not running; throws when it cannot be read. */
218
+ export async function listSessions(server) {
219
+ const r = await tmuxRead(server, ['list-sessions', '-F', LIST_FORMAT]);
220
+ return r ? parseSessions(r.stdout) : [];
221
+ }
222
+ export function parseSessions(stdout) {
223
+ const out = [];
224
+ for (const line of stdout.split('\n')) {
225
+ if (!line.trim())
226
+ continue;
227
+ const [name, project, branch, issue, uuid, created, paneCwd, dead, dcbCwd] = line.split(FIELD_SEP);
228
+ const cwd = dcbCwd || paneCwd;
229
+ const s = { name, dead: dead === '1' };
230
+ if (uuid)
231
+ s.claude_session_id = uuid;
232
+ if (project)
233
+ s.project_id = project;
234
+ if (branch)
235
+ s.branch = branch;
236
+ if (issue)
237
+ s.issue_label = issue;
238
+ if (created && /^\d+$/.test(created))
239
+ s.created = new Date(Number(created) * 1000).toISOString();
240
+ if (cwd)
241
+ s.cwd = cwd;
242
+ out.push(s);
243
+ }
244
+ return out;
245
+ }
246
+ /**
247
+ * pane tty (without /dev/) → session name, across BOTH servers, for matching
248
+ * claude-peers ttys. A user-server session carrying the dcb- prefix is skipped:
249
+ * the prefix means "owned", and honouring it on the wrong server would route
250
+ * attach and kill to a session the daemon does not own.
251
+ */
252
+ export async function paneTtyMap() {
253
+ const m = new Map();
254
+ for (const server of ['owned', 'user']) {
255
+ const r = await tmuxRead(server, ['list-panes', '-a', '-F', `#{pane_tty}${FIELD_SEP}#{session_name}`]);
256
+ if (!r)
257
+ continue;
258
+ for (const line of r.stdout.split('\n')) {
259
+ const [tty, name] = line.split(FIELD_SEP);
260
+ if (!tty || !name || serverFor(name) !== server)
261
+ continue;
262
+ m.set(tty.replace(/^\/dev\//, ''), name);
263
+ }
264
+ }
265
+ return m;
266
+ }
267
+ /** Find a session by key on the server its key says it lives on. */
268
+ export async function findSession(name) {
269
+ return (await listSessions(serverFor(name))).find((s) => s.name === name);
270
+ }
271
+ //# sourceMappingURL=tmux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux.js","sourceRoot":"","sources":["../../src/pty/tmux.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ export interface ScopeFilter {
2
+ assignee?: unknown;
3
+ team?: unknown;
4
+ }
5
+ export interface ScopeTerm {
6
+ requested: string;
7
+ /** Every id a record may carry to match this filter. */
8
+ match: string[];
9
+ }
10
+ export interface ResolvedScope {
11
+ assignee?: ScopeTerm;
12
+ team?: ScopeTerm;
13
+ /** True when the roster answered and aliases were applied. */
14
+ expanded: boolean;
15
+ /** Present whenever the result may be short, and why. */
16
+ note?: string;
17
+ }
18
+ export interface RosterRow {
19
+ id: string;
20
+ kind: string;
21
+ canonical_id: string | null;
22
+ aliases: string[] | null;
23
+ }
24
+ export type RosterResolver = (ids: string[]) => Promise<RosterRow[]>;
25
+ /** Test seam. */
26
+ export declare function _setRosterResolver(fn: RosterResolver | null): void;
27
+ /**
28
+ * Resolve a scope filter. Returns null when no filter was given, so an
29
+ * unscoped call keeps exactly its current behaviour.
30
+ */
31
+ export declare function resolveScope(f: ScopeFilter): Promise<ResolvedScope | null>;
32
+ /** Does a record fall inside the scope? Both terms must hold when both are set. */
33
+ export declare function inScope(record: {
34
+ assignee?: string;
35
+ team?: string;
36
+ }, scope: ResolvedScope): boolean;
37
+ //# sourceMappingURL=roster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roster.d.ts","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8DAA8D;IAC9D,QAAQ,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;AAkBrE,iBAAiB;AACjB,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAGlE;AA+BD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAoChF;AAED,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAInG"}
package/dist/roster.js ADDED
@@ -0,0 +1,126 @@
1
+ // ============================================================================
2
+ // Roster resolution for scoped reads (EPIC-0040 slice 3)
3
+ // ============================================================================
4
+ // A filter such as assignee=user:<ben> has to match every id that means Ben:
5
+ // the canonical user:<uuid> AND every human:<name> claimed to that account.
6
+ // Records are never rewritten to upgrade a human: to a user:, so without this
7
+ // expansion a filter by user:<uuid> misses every record written before the
8
+ // claim (44 human:ben records in this repo alone) and returns a confidently
9
+ // short list.
10
+ //
11
+ // HQ owns the mapping: hq.resolve_ids(p_org, p_ids) returns, per input id, its
12
+ // canonical_id and aliases. The daemon turns one filter id into an IN-list:
13
+ // {requested} ∪ {canonical_id} ∪ aliases.
14
+ //
15
+ // Offline degradation, agreed with decibel-hq: when the roster is unreachable,
16
+ // match the literal id only and SAY SO in the result ("not expanded"). Never a
17
+ // guess, never a quiet omission.
18
+ // ============================================================================
19
+ import { assertActor, assertTeam, IdentityError } from './identity.js';
20
+ import { requireOrgId } from './orgId.js';
21
+ import { getSupabaseServiceClient, isSupabaseConfigured } from './lib/supabase.js';
22
+ const CACHE_MS = 5 * 60_000;
23
+ const TIMEOUT_MS = 3_000;
24
+ const cache = new Map();
25
+ async function supabaseResolver(ids) {
26
+ if (!isSupabaseConfigured())
27
+ throw new Error('Supabase is not configured on this process');
28
+ const client = getSupabaseServiceClient();
29
+ const call = client.schema('hq').rpc('resolve_ids', { p_org: requireOrgId(), p_ids: ids });
30
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`no answer in ${TIMEOUT_MS}ms`)), TIMEOUT_MS));
31
+ const { data, error } = (await Promise.race([call, timeout]));
32
+ if (error)
33
+ throw new Error(error.message);
34
+ if (!Array.isArray(data))
35
+ throw new Error('resolve_ids returned no rows');
36
+ return data;
37
+ }
38
+ let resolver = supabaseResolver;
39
+ /** Test seam. */
40
+ export function _setRosterResolver(fn) {
41
+ resolver = fn ?? supabaseResolver;
42
+ cache.clear();
43
+ }
44
+ /** Validate a filter's SHAPE. Membership is HQ's business, never checked here. */
45
+ function parseFilter(f) {
46
+ const out = {};
47
+ try {
48
+ if (f.assignee !== undefined && f.assignee !== '') {
49
+ if (typeof f.assignee !== 'string')
50
+ throw new IdentityError('assignee must be a string');
51
+ assertActor(f.assignee);
52
+ out.assignee = f.assignee;
53
+ }
54
+ if (f.team !== undefined && f.team !== '') {
55
+ if (typeof f.team !== 'string')
56
+ throw new IdentityError('team must be a string');
57
+ assertTeam(f.team);
58
+ out.team = f.team;
59
+ }
60
+ }
61
+ catch (err) {
62
+ throw new Error(`Invalid scope filter: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ return out;
65
+ }
66
+ function termFrom(requested, row) {
67
+ const ids = new Set([requested]);
68
+ if (row && row.kind !== 'unresolved') {
69
+ if (row.canonical_id)
70
+ ids.add(row.canonical_id);
71
+ for (const a of row.aliases ?? [])
72
+ ids.add(a);
73
+ }
74
+ return { requested, match: [...ids] };
75
+ }
76
+ /**
77
+ * Resolve a scope filter. Returns null when no filter was given, so an
78
+ * unscoped call keeps exactly its current behaviour.
79
+ */
80
+ export async function resolveScope(f) {
81
+ const parsed = parseFilter(f);
82
+ const wanted = [parsed.assignee, parsed.team].filter((x) => !!x);
83
+ if (!wanted.length)
84
+ return null;
85
+ const now = Date.now();
86
+ const rows = new Map();
87
+ const missing = wanted.filter((id) => {
88
+ const hit = cache.get(id);
89
+ if (hit && now - hit.at < CACHE_MS) {
90
+ rows.set(id, hit.row);
91
+ return false;
92
+ }
93
+ return true;
94
+ });
95
+ let failure;
96
+ if (missing.length) {
97
+ try {
98
+ const answered = await resolver(missing);
99
+ for (const row of answered) {
100
+ rows.set(row.id, row);
101
+ cache.set(row.id, { row, at: now });
102
+ }
103
+ }
104
+ catch (err) {
105
+ failure = err instanceof Error ? err.message : String(err);
106
+ }
107
+ }
108
+ const scope = { expanded: !failure };
109
+ if (parsed.assignee)
110
+ scope.assignee = termFrom(parsed.assignee, rows.get(parsed.assignee));
111
+ if (parsed.team)
112
+ scope.team = termFrom(parsed.team, rows.get(parsed.team));
113
+ if (failure) {
114
+ scope.note = `Roster unreachable (${failure}): matched the literal id only; local identities were not expanded, so this list may be short.`;
115
+ }
116
+ return scope;
117
+ }
118
+ /** Does a record fall inside the scope? Both terms must hold when both are set. */
119
+ export function inScope(record, scope) {
120
+ if (scope.assignee && !(record.assignee && scope.assignee.match.includes(record.assignee)))
121
+ return false;
122
+ if (scope.team && !(record.team && scope.team.match.includes(record.team)))
123
+ return false;
124
+ return true;
125
+ }
126
+ //# sourceMappingURL=roster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roster.js","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Resolved per call rather than captured at import, the way daemonConfig does:
3
+ * os.homedir() tracks $HOME, and a module-level constant freezes whatever it
4
+ * was when the file was first loaded — which makes this unverifiable in a test
5
+ * and wrong in any process that changes HOME.
6
+ */
7
+ export declare function decibelHome(): string;
8
+ /**
9
+ * DECIBEL_DAEMON_SIDECAR=<dir> marks a second daemon run beside the real one
10
+ * (a branch build on another port, decibel-hq ADR-0018 testing). Its pid,
11
+ * meta and logs live in <dir>, so it never takes over the machine's
12
+ * advertisement, and server.ts skips the machine singletons (peer broker,
13
+ * presence, HQ command dispatch, chat). Config, env and projects stay shared.
14
+ */
15
+ export declare function sidecarDir(): string | null;
16
+ /** Where this daemon's pid, meta and logs live: the sidecar dir, else ~/.decibel. */
17
+ export declare function daemonStateDir(): string;
18
+ export declare function metaPath(): string;
19
+ /** The port the daemon binds when nothing says otherwise. */
20
+ export declare const DEFAULT_DAEMON_PORT = 4888;
21
+ export interface DaemonMeta {
22
+ started_at: string;
23
+ crash_count: number;
24
+ port?: number;
25
+ pid?: number;
26
+ }
27
+ /**
28
+ * Is this pid a process that currently exists?
29
+ *
30
+ * Signal 0 performs the permission and existence checks without delivering
31
+ * anything. EPERM means the process is there and owned by someone else, which
32
+ * is still alive — only ESRCH means gone.
33
+ */
34
+ export declare function isPidAlive(pid: number | undefined): boolean;
35
+ export declare function readDaemonMeta(): DaemonMeta | null;
36
+ /**
37
+ * The advertised port, or undefined when there is no usable advertisement.
38
+ *
39
+ * An entry whose pid is dead is treated as absent rather than followed. That is
40
+ * the whole point: a stale port is worse than no port, because callers fall
41
+ * back to the default when there is nothing to read and dial a corpse when
42
+ * there is.
43
+ */
44
+ export declare function advertisedPort(): number | undefined;
45
+ /** Discovery order shared by every client: explicit > env > meta > default. */
46
+ export declare function resolveDaemonPort(explicit?: number): number;
47
+ /**
48
+ * True when someone else is currently advertising — a live pid that is not us.
49
+ * Taking discovery from a running daemon is how a second instance silently
50
+ * breaks the first one's clients, so callers refuse rather than overwrite.
51
+ */
52
+ export declare function heldByAnotherProcess(): DaemonMeta | null;
53
+ //# sourceMappingURL=daemonMeta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemonMeta.d.ts","sourceRoot":"","sources":["../../src/runtime/daemonMeta.ts"],"names":[],"mappings":"AA6BA;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAG1C;AAED,qFAAqF;AACrF,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQ3D;AAED,wBAAgB,cAAc,IAAI,UAAU,GAAG,IAAI,CAOlD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAKnD;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,GAAG,IAAI,CAIxD"}
@@ -0,0 +1,121 @@
1
+ // ============================================================================
2
+ // daemon.meta — machine-global daemon discovery
3
+ // ============================================================================
4
+ //
5
+ // ~/.decibel/daemon.meta is how every client on the machine finds the daemon:
6
+ // the session-init hook, the issue-close hook, the HQ client, ensureRuntime,
7
+ // the CLI. One small JSON file that three modules used to parse for themselves.
8
+ //
9
+ // ISS-0179. Two defects, and the second is the worse one:
10
+ //
11
+ // 1. ANY `--http` run advertised itself here, not just `--daemon`. Starting a
12
+ // throwaway server on another port — a test, a second checkout, a port
13
+ // conflict, a demo — silently redirected every hook on the machine to it.
14
+ //
15
+ // 2. Nothing removed a stale entry and no reader checked one. When that
16
+ // throwaway process died, the entry stayed, and every hook kept dialling a
17
+ // port with nothing behind it. The failure was silent at every layer: curl
18
+ // got connection-refused, the hook read empty output as "nothing to do",
19
+ // and the user saw a successful commit.
20
+ //
21
+ // The entry records the pid that wrote it, which is enough to tell a live
22
+ // advertisement from a dead one. That check lives here, once, because the
23
+ // reason it was missing is that it would have had to be written three times.
24
+ // ============================================================================
25
+ import fs from 'fs';
26
+ import path from 'path';
27
+ import os from 'os';
28
+ /**
29
+ * Resolved per call rather than captured at import, the way daemonConfig does:
30
+ * os.homedir() tracks $HOME, and a module-level constant freezes whatever it
31
+ * was when the file was first loaded — which makes this unverifiable in a test
32
+ * and wrong in any process that changes HOME.
33
+ */
34
+ export function decibelHome() {
35
+ return path.join(os.homedir(), '.decibel');
36
+ }
37
+ /**
38
+ * DECIBEL_DAEMON_SIDECAR=<dir> marks a second daemon run beside the real one
39
+ * (a branch build on another port, decibel-hq ADR-0018 testing). Its pid,
40
+ * meta and logs live in <dir>, so it never takes over the machine's
41
+ * advertisement, and server.ts skips the machine singletons (peer broker,
42
+ * presence, HQ command dispatch, chat). Config, env and projects stay shared.
43
+ */
44
+ export function sidecarDir() {
45
+ const dir = process.env.DECIBEL_DAEMON_SIDECAR;
46
+ return dir ? path.resolve(dir) : null;
47
+ }
48
+ /** Where this daemon's pid, meta and logs live: the sidecar dir, else ~/.decibel. */
49
+ export function daemonStateDir() {
50
+ return sidecarDir() ?? decibelHome();
51
+ }
52
+ export function metaPath() {
53
+ return path.join(daemonStateDir(), 'daemon.meta');
54
+ }
55
+ /** The port the daemon binds when nothing says otherwise. */
56
+ export const DEFAULT_DAEMON_PORT = 4888;
57
+ /**
58
+ * Is this pid a process that currently exists?
59
+ *
60
+ * Signal 0 performs the permission and existence checks without delivering
61
+ * anything. EPERM means the process is there and owned by someone else, which
62
+ * is still alive — only ESRCH means gone.
63
+ */
64
+ export function isPidAlive(pid) {
65
+ if (!Number.isInteger(pid) || pid <= 0)
66
+ return false;
67
+ try {
68
+ process.kill(pid, 0);
69
+ return true;
70
+ }
71
+ catch (err) {
72
+ return err.code === 'EPERM';
73
+ }
74
+ }
75
+ export function readDaemonMeta() {
76
+ try {
77
+ const parsed = JSON.parse(fs.readFileSync(metaPath(), 'utf-8'));
78
+ return parsed && typeof parsed === 'object' ? parsed : null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * The advertised port, or undefined when there is no usable advertisement.
86
+ *
87
+ * An entry whose pid is dead is treated as absent rather than followed. That is
88
+ * the whole point: a stale port is worse than no port, because callers fall
89
+ * back to the default when there is nothing to read and dial a corpse when
90
+ * there is.
91
+ */
92
+ export function advertisedPort() {
93
+ const meta = readDaemonMeta();
94
+ if (!meta || !Number.isInteger(meta.port))
95
+ return undefined;
96
+ if (meta.pid !== undefined && !isPidAlive(meta.pid))
97
+ return undefined;
98
+ return meta.port;
99
+ }
100
+ /** Discovery order shared by every client: explicit > env > meta > default. */
101
+ export function resolveDaemonPort(explicit) {
102
+ if (Number.isInteger(explicit) && explicit > 0 && explicit < 65536) {
103
+ return explicit;
104
+ }
105
+ const fromEnv = Number(process.env.DECIBEL_DAEMON_PORT);
106
+ if (Number.isInteger(fromEnv) && fromEnv > 0 && fromEnv < 65536)
107
+ return fromEnv;
108
+ return advertisedPort() ?? DEFAULT_DAEMON_PORT;
109
+ }
110
+ /**
111
+ * True when someone else is currently advertising — a live pid that is not us.
112
+ * Taking discovery from a running daemon is how a second instance silently
113
+ * breaks the first one's clients, so callers refuse rather than overwrite.
114
+ */
115
+ export function heldByAnotherProcess() {
116
+ const meta = readDaemonMeta();
117
+ if (!meta || meta.pid === undefined || meta.pid === process.pid)
118
+ return null;
119
+ return isPidAlive(meta.pid) ? meta : null;
120
+ }
121
+ //# sourceMappingURL=daemonMeta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemonMeta.js","sourceRoot":"","sources":["../../src/runtime/daemonMeta.ts"],"names":[],"mappings":"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"}