@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,161 @@
1
+ /**
2
+ * The kill switch — one durable STOP for unattended work (ISS-0178).
3
+ *
4
+ * WHAT IT STOPS, AND WHAT IT DELIBERATELY DOES NOT. The metaphor is a machine
5
+ * tool's big red button: it halts the automation, it does not power down the
6
+ * panel. Unattended work — the agentic queue replaying remote writes, the dojo
7
+ * experiment runner spawning a script — refuses while stopped. Interactive
8
+ * calls keep working on purpose, because the first thing anyone does after
9
+ * hitting stop is ask what went wrong, and that takes sentinel, oracle and this
10
+ * module itself.
11
+ *
12
+ * THE SAFETY INVARIANT. You can never stop yourself out of resuming. That holds
13
+ * BY CONSTRUCTION rather than by an exception list: nothing in this file gates
14
+ * itself, and enforcement lives at the call sites of the work being stopped.
15
+ * There is no path where `resume` consults the flag it is trying to clear.
16
+ *
17
+ * WHY A FILE, AND WHY THIS FILE. State lives at ~/.decibel/killswitch.json:
18
+ *
19
+ * - machine-global, because a button on a desk is machine-global. Stopping
20
+ * "the project you happen to be in" is not what a red button means.
21
+ * - a file, so it survives a daemon restart, a crash, and a reboot. A stop
22
+ * that forgets itself is worse than no stop, because it teaches people the
23
+ * switch is unreliable.
24
+ * - plain JSON with no schema tricks, so a process that is not this one — a
25
+ * shell script, a microcontroller, a future physical button — can read and
26
+ * write it without linking anything.
27
+ *
28
+ * WHAT IT IS NOT. It is not a security control. Anything that can write to
29
+ * ~/.decibel can clear it. It is an operational brake for work you started, in
30
+ * the same spirit as the daemon's own PID file.
31
+ */
32
+ import fs from 'fs';
33
+ import path from 'path';
34
+ import os from 'os';
35
+ /** Machine-global, deliberately — see the header. */
36
+ export const KILL_SWITCH_PATH = path.join(os.homedir(), '.decibel', 'killswitch.json');
37
+ const RUNNING = { stopped: false };
38
+ /**
39
+ * Read the current state, treating every failure as RUNNING.
40
+ *
41
+ * FAIL-OPEN IS THE RIGHT CHOICE HERE and it is worth saying why, because the
42
+ * opposite is defensible and wrong for this case. A corrupt or unreadable
43
+ * killswitch file would, under fail-closed, halt all unattended work on the
44
+ * machine with no way to diagnose it except editing the very file that is
45
+ * broken — a brake that jams on is worse than one that does not engage. The
46
+ * stop is an operational convenience, not a safety interlock, and it says so in
47
+ * the header.
48
+ *
49
+ * Note this is the one place the module is deliberately quiet: a missing file
50
+ * is the overwhelmingly common case (nobody has ever pressed stop) and must not
51
+ * be noise.
52
+ */
53
+ export function readKillSwitch() {
54
+ try {
55
+ const raw = fs.readFileSync(KILL_SWITCH_PATH, 'utf-8');
56
+ const parsed = JSON.parse(raw);
57
+ // Anything other than an explicit `true` is running. A file containing
58
+ // `{"stopped":"yes"}` from a well-meaning script must not read as stopped,
59
+ // because a truthy-string check would make the brake depend on spelling.
60
+ return parsed.stopped === true ? parsed : RUNNING;
61
+ }
62
+ catch {
63
+ return RUNNING;
64
+ }
65
+ }
66
+ /** True when unattended work should refuse. */
67
+ export function isStopped() {
68
+ return readKillSwitch().stopped;
69
+ }
70
+ /**
71
+ * Engage the stop. Idempotent: a second press reports the FIRST stop rather
72
+ * than overwriting it, because a button will be pressed twice and the original
73
+ * reason and time are the ones worth keeping.
74
+ *
75
+ * @returns the state now in force, and whether this call is what engaged it
76
+ */
77
+ export function engageStop(opts = {}) {
78
+ const current = readKillSwitch();
79
+ if (current.stopped) {
80
+ return { state: current, already: true };
81
+ }
82
+ const state = {
83
+ stopped: true,
84
+ stopped_at: new Date().toISOString(),
85
+ ...(opts.reason ? { reason: opts.reason } : {}),
86
+ // An unattributed stop is allowed — a physical button cannot type — but it
87
+ // is recorded as unattributed rather than silently attributed to whoever
88
+ // happened to be running.
89
+ actor: opts.actor || 'unattributed',
90
+ ...(opts.source ? { source: opts.source } : {}),
91
+ };
92
+ writeState(state);
93
+ return { state, already: false };
94
+ }
95
+ /**
96
+ * Release the stop. Idempotent, and never gated — see the safety invariant.
97
+ *
98
+ * @returns the state that was cleared, when there was one
99
+ */
100
+ export function releaseStop() {
101
+ const current = readKillSwitch();
102
+ if (!current.stopped)
103
+ return { released: null };
104
+ writeState(RUNNING);
105
+ return { released: current };
106
+ }
107
+ function writeState(state) {
108
+ const dir = path.dirname(KILL_SWITCH_PATH);
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ // Write-then-rename: a physical button may be pressed while something else is
111
+ // reading, and a half-written file must never be observable as either state.
112
+ const tmp = `${KILL_SWITCH_PATH}.${process.pid}.tmp`;
113
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf-8');
114
+ fs.renameSync(tmp, KILL_SWITCH_PATH);
115
+ }
116
+ /**
117
+ * Thrown at the gate. Carries the stop's own reason and time so the refusal can
118
+ * say WHY work stopped rather than merely that it did — the difference between
119
+ * a legible brake and a mystery.
120
+ */
121
+ export class KillSwitchEngagedError extends Error {
122
+ code = 'KILL_SWITCH_ENGAGED';
123
+ state;
124
+ constructor(operation, state) {
125
+ const when = state.stopped_at ? ` at ${state.stopped_at}` : '';
126
+ const who = state.actor && state.actor !== 'unattributed' ? ` by ${state.actor}` : '';
127
+ const why = state.reason ? ` — ${state.reason}` : '';
128
+ super(`Kill switch engaged${when}${who}${why}. Refusing: ${operation}. ` +
129
+ `Release it with killswitch.resume (never blocked), or delete ${KILL_SWITCH_PATH}.`);
130
+ this.name = 'KillSwitchEngagedError';
131
+ this.state = state;
132
+ }
133
+ }
134
+ /**
135
+ * The gate. Call at the top of anything that runs without someone watching.
136
+ *
137
+ * Takes the operation name so the refusal names what it refused — "agentic
138
+ * queue replay" is actionable, "operation failed" is not.
139
+ */
140
+ export function assertRunnable(operation) {
141
+ const state = readKillSwitch();
142
+ if (state.stopped)
143
+ throw new KillSwitchEngagedError(operation, state);
144
+ }
145
+ /**
146
+ * Non-throwing form of {@link assertRunnable}, for call sites whose contract is
147
+ * to RETURN an error rather than throw one.
148
+ *
149
+ * Both forms exist deliberately. `assertRunnable` suits functions that already
150
+ * throw (agentQueueSync throws for unconfigured Supabase, so a throw is in
151
+ * keeping); this suits functions that declare `Promise<Output | SomeError>`,
152
+ * where throwing would be a lie about the signature and would behave
153
+ * differently when called directly than when called through a handler that
154
+ * catches. That divergence between the direct and dispatched path is a bug
155
+ * shape this repo has been bitten by more than once.
156
+ */
157
+ export function checkRunnable(operation) {
158
+ const state = readKillSwitch();
159
+ return state.stopped ? new KillSwitchEngagedError(operation, state) : null;
160
+ }
161
+ //# sourceMappingURL=killSwitch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"killSwitch.js","sourceRoot":"","sources":["../src/killSwitch.ts"],"names":[],"mappings":"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"}
@@ -23,4 +23,27 @@
23
23
  * record does not quietly widen or narrow its mode.
24
24
  */
25
25
  export declare function writeFileAtomic(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>;
26
+ /**
27
+ * Create `filePath` with `content`, atomically AND exclusively (ISS-0195).
28
+ *
29
+ * `writeFile(…, { flag: 'wx' })` is exclusive but not atomic: O_EXCL creates
30
+ * the file empty and the bytes follow, so a SIGKILL in between leaves a 0-byte
31
+ * record under a real id. Temp-and-rename is atomic but not exclusive: rename
32
+ * silently replaces an existing file. `link(2)` is both. It publishes a fully
33
+ * written, fsynced temp under the final name in one step, and fails with
34
+ * EEXIST when the name is taken, exactly like O_EXCL. A kill can only leave
35
+ * the dot-temp behind, which no record scan reads; see sweepAllocationTemps.
36
+ *
37
+ * Where hard links are unavailable (some FAT, exFAT and network mounts), this
38
+ * falls back to the O_EXCL write: still exclusive, and no worse than before.
39
+ */
40
+ export declare function createFileExclusive(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>;
41
+ /** Suffix of createFileExclusive's temp files. */
42
+ export declare const ALLOC_TEMP_SUFFIX = ".alloc.tmp";
43
+ /**
44
+ * Remove createFileExclusive temps in `dir` older than `maxAgeMs`: the debris
45
+ * a killed writer leaves behind. The age floor keeps it from touching a temp
46
+ * a live writer is still filling. Never throws; this is housekeeping.
47
+ */
48
+ export declare function sweepAllocationTemps(dir: string, maxAgeMs?: number): Promise<number>;
26
49
  //# sourceMappingURL=atomicWrite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"atomicWrite.d.ts","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Cf"}
1
+ {"version":3,"file":"atomicWrite.d.ts","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Cf;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,kDAAkD;AAClD,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB/F"}
@@ -70,4 +70,88 @@ export async function writeFileAtomic(filePath, content, encoding = 'utf-8') {
70
70
  throw err;
71
71
  }
72
72
  }
73
+ /** Errors meaning "this filesystem cannot hard-link", not "the target exists". */
74
+ const LINK_UNSUPPORTED = new Set(['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV', 'ENOSYS']);
75
+ /**
76
+ * Create `filePath` with `content`, atomically AND exclusively (ISS-0195).
77
+ *
78
+ * `writeFile(…, { flag: 'wx' })` is exclusive but not atomic: O_EXCL creates
79
+ * the file empty and the bytes follow, so a SIGKILL in between leaves a 0-byte
80
+ * record under a real id. Temp-and-rename is atomic but not exclusive: rename
81
+ * silently replaces an existing file. `link(2)` is both. It publishes a fully
82
+ * written, fsynced temp under the final name in one step, and fails with
83
+ * EEXIST when the name is taken, exactly like O_EXCL. A kill can only leave
84
+ * the dot-temp behind, which no record scan reads; see sweepAllocationTemps.
85
+ *
86
+ * Where hard links are unavailable (some FAT, exFAT and network mounts), this
87
+ * falls back to the O_EXCL write: still exclusive, and no worse than before.
88
+ */
89
+ export async function createFileExclusive(filePath, content, encoding = 'utf-8') {
90
+ const dir = path.dirname(filePath);
91
+ const tmpPath = path.join(dir, `.${path.basename(filePath)}.${randomBytes(6).toString('hex')}${ALLOC_TEMP_SUFFIX}`);
92
+ let handle;
93
+ try {
94
+ handle = await fs.open(tmpPath, 'wx');
95
+ await handle.writeFile(content, encoding);
96
+ await handle.sync();
97
+ await handle.close();
98
+ handle = undefined;
99
+ try {
100
+ await fs.link(tmpPath, filePath);
101
+ }
102
+ catch (err) {
103
+ const code = err.code ?? '';
104
+ if (!LINK_UNSUPPORTED.has(code))
105
+ throw err; // EEXIST included: the caller retries.
106
+ await fs.writeFile(filePath, content, { encoding, flag: 'wx' });
107
+ }
108
+ }
109
+ finally {
110
+ if (handle) {
111
+ try {
112
+ await handle.close();
113
+ }
114
+ catch {
115
+ // Already closed or never opened cleanly.
116
+ }
117
+ }
118
+ try {
119
+ await fs.unlink(tmpPath);
120
+ }
121
+ catch {
122
+ // Nothing to clean up.
123
+ }
124
+ }
125
+ }
126
+ /** Suffix of createFileExclusive's temp files. */
127
+ export const ALLOC_TEMP_SUFFIX = '.alloc.tmp';
128
+ /**
129
+ * Remove createFileExclusive temps in `dir` older than `maxAgeMs`: the debris
130
+ * a killed writer leaves behind. The age floor keeps it from touching a temp
131
+ * a live writer is still filling. Never throws; this is housekeeping.
132
+ */
133
+ export async function sweepAllocationTemps(dir, maxAgeMs = 10 * 60_000) {
134
+ let removed = 0;
135
+ try {
136
+ const now = Date.now();
137
+ for (const name of await fs.readdir(dir)) {
138
+ if (!name.startsWith('.') || !name.endsWith(ALLOC_TEMP_SUFFIX))
139
+ continue;
140
+ const full = path.join(dir, name);
141
+ try {
142
+ if (now - (await fs.stat(full)).mtimeMs > maxAgeMs) {
143
+ await fs.unlink(full);
144
+ removed++;
145
+ }
146
+ }
147
+ catch {
148
+ // Raced with another sweeper, or already gone.
149
+ }
150
+ }
151
+ }
152
+ catch {
153
+ // Directory unreadable or absent: nothing to sweep.
154
+ }
155
+ return removed;
156
+ }
73
157
  //# sourceMappingURL=atomicWrite.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"atomicWrite.js","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,OAAe,EACf,WAA2B,OAAO;IAElC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEpG,qEAAqE;IACrE,IAAI,IAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;IAED,IAAI,MAAoD,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC1C,0EAA0E;QAC1E,yEAAyE;QACzE,yEAAyE;QACzE,YAAY;QACZ,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,SAAS,CAAC;QAEnB,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,0CAA0C;YAC5C,CAAC;QACH,CAAC;QACD,2EAA2E;QAC3E,sEAAsE;QACtE,uDAAuD;QACvD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,uBAAuB;QACzB,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"atomicWrite.js","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"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"}
@@ -4,9 +4,22 @@
4
4
  * The defect this closes: `create_issue` picked the next id by scanning the
5
5
  * issues directory for the current maximum, then wrote the file many awaits
6
6
  * later. Two processes could read the same maximum and both write ISS-0141.
7
- * Four duplicate-id groups exist on disk from exactly this (ISS-0015, ISS-0028,
8
- * ISS-0054, ISS-0112), reported independently by the `machina` peer and
9
- * reproduced locally.
7
+ * Four duplicate-id groups were produced by exactly this and were found on disk
8
+ * (ISS-0015, ISS-0028, ISS-0054, ISS-0112), reported independently by the
9
+ * `machina` peer and reproduced locally. THIS repo's four were cleared: as of
10
+ * 2026-09-19 it holds 200 issues and 38 epics with ZERO duplicate frontmatter
11
+ * ids. ISS-0136 remains OPEN, because it is portfolio-wide and 61 groups still
12
+ * sit in five other projects (frontend_v0.2, senken-trading-agent, machina,
13
+ * decibel-studio, decibel-tools-mobile). Do not read "clean" here as "clean
14
+ * everywhere". The history is kept because it is the evidence
15
+ * this lock exists, not because the state persists — stated in the past tense
16
+ * deliberately, since the present-tense version outlived the fact and was read
17
+ * as current by an outside reader, who repeated it in a document.
18
+ *
19
+ * Verify by parsing frontmatter, not by grepping. `grep "^id: ISS-"` over this
20
+ * store counts documents that QUOTE an id — a report about malformed
21
+ * frontmatter contains the line it is reporting — and that false positive has
22
+ * already cost one confidently wrong correction.
10
23
  *
11
24
  * Two independent defences, because a lock only protects callers that take it:
12
25
  *
@@ -14,9 +27,12 @@
14
27
  * calculation. Serializing only the scan leaves the same race — both
15
28
  * processes still compute 141 before either file lands.
16
29
  *
17
- * 2. The write itself is O_EXCL. If a caller bypasses the lock entirely — an
18
- * old client, a migration script, a future mistake — a colliding write
30
+ * 2. The write itself is exclusive. If a caller bypasses the lock entirely —
31
+ * an old client, a migration script, a future mistake — a colliding write
19
32
  * fails loudly with EEXIST instead of silently overwriting a real issue.
33
+ * It is also atomic (ISS-0195): the record is published by link(2) from a
34
+ * complete, fsynced temp, so a SIGKILL mid-write can no longer leave a
35
+ * 0-byte record under a real id, which plain O_EXCL could.
20
36
  *
21
37
  * Both `createIssue` implementations (src/tools/sentinel.ts and
22
38
  * src/sentinelIssues.ts) allocate from a single numbering space and write
@@ -1 +1 @@
1
- {"version":3,"file":"recordIdAllocator.d.ts","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAcH,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,CAAC;AAgB1C,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAiC5F;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3E;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,EAC1C,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yEACqE;IACrE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GACL,OAAO,CAAC,eAAe,CAAC,CAgC1B;AAED,+DAA+D;AAC/D,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EACzC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,EACxC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,eAAe,CAAC,CAE1B"}
1
+ {"version":3,"file":"recordIdAllocator.d.ts","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAeH,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,CAAC;AAgB1C,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAiC5F;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3E;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,EAC1C,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yEACqE;IACrE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GACL,OAAO,CAAC,eAAe,CAAC,CAiC1B;AAED,+DAA+D;AAC/D,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EACzC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,EACxC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,eAAe,CAAC,CAE1B"}
@@ -4,9 +4,22 @@
4
4
  * The defect this closes: `create_issue` picked the next id by scanning the
5
5
  * issues directory for the current maximum, then wrote the file many awaits
6
6
  * later. Two processes could read the same maximum and both write ISS-0141.
7
- * Four duplicate-id groups exist on disk from exactly this (ISS-0015, ISS-0028,
8
- * ISS-0054, ISS-0112), reported independently by the `machina` peer and
9
- * reproduced locally.
7
+ * Four duplicate-id groups were produced by exactly this and were found on disk
8
+ * (ISS-0015, ISS-0028, ISS-0054, ISS-0112), reported independently by the
9
+ * `machina` peer and reproduced locally. THIS repo's four were cleared: as of
10
+ * 2026-09-19 it holds 200 issues and 38 epics with ZERO duplicate frontmatter
11
+ * ids. ISS-0136 remains OPEN, because it is portfolio-wide and 61 groups still
12
+ * sit in five other projects (frontend_v0.2, senken-trading-agent, machina,
13
+ * decibel-studio, decibel-tools-mobile). Do not read "clean" here as "clean
14
+ * everywhere". The history is kept because it is the evidence
15
+ * this lock exists, not because the state persists — stated in the past tense
16
+ * deliberately, since the present-tense version outlived the fact and was read
17
+ * as current by an outside reader, who repeated it in a document.
18
+ *
19
+ * Verify by parsing frontmatter, not by grepping. `grep "^id: ISS-"` over this
20
+ * store counts documents that QUOTE an id — a report about malformed
21
+ * frontmatter contains the line it is reporting — and that false positive has
22
+ * already cost one confidently wrong correction.
10
23
  *
11
24
  * Two independent defences, because a lock only protects callers that take it:
12
25
  *
@@ -14,9 +27,12 @@
14
27
  * calculation. Serializing only the scan leaves the same race — both
15
28
  * processes still compute 141 before either file lands.
16
29
  *
17
- * 2. The write itself is O_EXCL. If a caller bypasses the lock entirely — an
18
- * old client, a migration script, a future mistake — a colliding write
30
+ * 2. The write itself is exclusive. If a caller bypasses the lock entirely —
31
+ * an old client, a migration script, a future mistake — a colliding write
19
32
  * fails loudly with EEXIST instead of silently overwriting a real issue.
33
+ * It is also atomic (ISS-0195): the record is published by link(2) from a
34
+ * complete, fsynced temp, so a SIGKILL mid-write can no longer leave a
35
+ * 0-byte record under a real id, which plain O_EXCL could.
20
36
  *
21
37
  * Both `createIssue` implementations (src/tools/sentinel.ts and
22
38
  * src/sentinelIssues.ts) allocate from a single numbering space and write
@@ -26,6 +42,7 @@
26
42
  import { promises as fs } from 'fs';
27
43
  import path from 'path';
28
44
  import { withFileLock } from './fileLock.js';
45
+ import { createFileExclusive, sweepAllocationTemps } from './atomicWrite.js';
29
46
  /**
30
47
  * Lock file guarding id allocation, scoped per record directory.
31
48
  *
@@ -118,6 +135,7 @@ export async function allocateAndWriteRecord(dir, prefix, extension, slug, build
118
135
  const maxAttempts = options.maxAttempts ?? 5;
119
136
  const lockPath = path.join(dir, LOCK_FILENAME);
120
137
  return withFileLock(lockPath, async () => {
138
+ await sweepAllocationTemps(dir);
121
139
  let next = (await scanMaxRecordNumber(dir, prefix)) + 1;
122
140
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
123
141
  const id = formatRecordId(prefix, next);
@@ -125,8 +143,8 @@ export async function allocateAndWriteRecord(dir, prefix, extension, slug, build
125
143
  const filePath = path.join(dir, filename);
126
144
  options.validate?.(filePath);
127
145
  try {
128
- // 'wx' is O_EXCL: fails rather than clobbering an existing record.
129
- await fs.writeFile(filePath, buildContent(id), { encoding: 'utf-8', flag: 'wx' });
146
+ // Exclusive (EEXIST rather than clobbering) and atomic (never partial).
147
+ await createFileExclusive(filePath, buildContent(id));
130
148
  return { id, filename, filePath };
131
149
  }
132
150
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"recordIdAllocator.js","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recordIdAllocator.js","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ export declare class OrgIdError extends Error {
2
+ }
3
+ /**
4
+ * The configured org, or null when DECIBEL_ORG_ID is unset or blank.
5
+ * A value that is set but not a uuid throws: a typo must not look like "unset".
6
+ */
7
+ export declare function getOrgId(): string | null;
8
+ /** The configured org, or an actionable error naming the variable. */
9
+ export declare function requireOrgId(): string;
10
+ /** True when this process holds HQ service credentials. */
11
+ export declare function isHqConnected(): boolean;
12
+ /**
13
+ * Startup gate. Throws when HQ credentials are present but no valid org is.
14
+ * Returns the org when connected, or null for a local-only install.
15
+ */
16
+ export declare function assertOrgForHqConnection(): string | null;
17
+ /**
18
+ * For the HQ writers' entry points, which must never throw into the daemon:
19
+ * the org, or null after logging why the feature is off. Reaching null on the
20
+ * daemon path means the startup gate was skipped.
21
+ */
22
+ export declare function orgOrLog(feature: string): string | null;
23
+ //# sourceMappingURL=orgId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgId.d.ts","sourceRoot":"","sources":["../src/orgId.ts"],"names":[],"mappings":"AAqBA,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAUxC;AAED,sEAAsE;AACtE,wBAAgB,YAAY,IAAI,MAAM,CAWrC;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOvD"}
package/dist/orgId.js ADDED
@@ -0,0 +1,73 @@
1
+ // ============================================================================
2
+ // The org this daemon serves: one source (EPIC-0040, ADR-0014)
3
+ // ============================================================================
4
+ // There is no default. This used to fall back to a hardcoded uuid, Ben's org,
5
+ // so an HQ-connected daemon that forgot DECIBEL_ORG_ID silently wrote presence
6
+ // and command settlements into that org under service_role. RLS sits below
7
+ // service_role, so nothing caught it. Once HQ gives every account a personal
8
+ // org (decibel-hq ADR-0016), that fallback becomes a cross-tenant write on
9
+ // every solo install, which is why removing it is step (a) of ADR-0014.
10
+ //
11
+ // The org is required exactly where it is used: the HQ-connected path, i.e.
12
+ // SUPABASE_URL and SUPABASE_SERVICE_KEY are set. A local-only install never
13
+ // reads it. assertOrgForHqConnection() is the daemon's startup gate; the
14
+ // readers call requireOrgId() lazily, never at import time, so a missing
15
+ // variable cannot break boot for a process that would never use it.
16
+ // ============================================================================
17
+ import { log } from './config.js';
18
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ export class OrgIdError extends Error {
20
+ }
21
+ /**
22
+ * The configured org, or null when DECIBEL_ORG_ID is unset or blank.
23
+ * A value that is set but not a uuid throws: a typo must not look like "unset".
24
+ */
25
+ export function getOrgId() {
26
+ const raw = process.env.DECIBEL_ORG_ID?.trim();
27
+ if (!raw)
28
+ return null;
29
+ if (!UUID_RE.test(raw)) {
30
+ throw new OrgIdError('DECIBEL_ORG_ID is set but is not a uuid. It must be the id of your org ' +
31
+ 'in HQ (hq.orgs.id).');
32
+ }
33
+ return raw.toLowerCase();
34
+ }
35
+ /** The configured org, or an actionable error naming the variable. */
36
+ export function requireOrgId() {
37
+ const org = getOrgId();
38
+ if (!org) {
39
+ throw new OrgIdError('DECIBEL_ORG_ID is not set. HQ-connected features (presence, agent commands, ' +
40
+ 'roster resolution) need the id of the org this machine works for. Set ' +
41
+ 'DECIBEL_ORG_ID to your org id from HQ (hq.orgs.id) in the daemon environment. ' +
42
+ 'There is no default, so writes cannot land in someone else\'s org.');
43
+ }
44
+ return org;
45
+ }
46
+ /** True when this process holds HQ service credentials. */
47
+ export function isHqConnected() {
48
+ return Boolean(process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_KEY);
49
+ }
50
+ /**
51
+ * Startup gate. Throws when HQ credentials are present but no valid org is.
52
+ * Returns the org when connected, or null for a local-only install.
53
+ */
54
+ export function assertOrgForHqConnection() {
55
+ if (!isHqConnected())
56
+ return null;
57
+ return requireOrgId();
58
+ }
59
+ /**
60
+ * For the HQ writers' entry points, which must never throw into the daemon:
61
+ * the org, or null after logging why the feature is off. Reaching null on the
62
+ * daemon path means the startup gate was skipped.
63
+ */
64
+ export function orgOrLog(feature) {
65
+ try {
66
+ return requireOrgId();
67
+ }
68
+ catch (e) {
69
+ log(`${feature}: disabled. ${e instanceof Error ? e.message : String(e)}`);
70
+ return null;
71
+ }
72
+ }
73
+ //# sourceMappingURL=orgId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgId.js","sourceRoot":"","sources":["../src/orgId.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+DAA+D;AAC/D,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,2EAA2E;AAC3E,6EAA6E;AAC7E,2EAA2E;AAC3E,wEAAwE;AACxE,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,oEAAoE;AACpE,+EAA+E;AAE/E,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAElF,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG;AAExC;;;GAGG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,UAAU,CAClB,yEAAyE;YACvE,qBAAqB,CACxB,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC;AAC3B,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY;IAC1B,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;IACvB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,UAAU,CAClB,8EAA8E;YAC5E,wEAAwE;YACxE,gFAAgF;YAChF,oEAAoE,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,IAAI,CAAC,aAAa,EAAE;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,IAAI,CAAC;QACH,OAAO,YAAY,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,GAAG,CAAC,GAAG,OAAO,eAAe,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function runPairCli(argv: string[]): Promise<number>;
2
+ //# sourceMappingURL=pairCli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairCli.d.ts","sourceRoot":"","sources":["../src/pairCli.ts"],"names":[],"mappings":"AAwCA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0DhE"}