@indigoai-us/hq-cli 5.108.1 → 5.108.2

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 (106) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/assets/mesh-daemon/README.md +23 -0
  3. package/assets/mesh-daemon/launchd.plist.template +33 -0
  4. package/assets/mesh-daemon/systemd.system.service.template +27 -0
  5. package/assets/mesh-daemon/systemd.user.service.template +18 -0
  6. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
  7. package/dist/commands/core.js +5 -4
  8. package/dist/commands/mesh.d.ts +15 -0
  9. package/dist/commands/mesh.js +732 -4
  10. package/dist/commands/search.d.ts +23 -0
  11. package/dist/commands/search.js +42 -1
  12. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  13. package/dist/lib/doctor/checks/work-context.js +195 -0
  14. package/dist/lib/doctor/registry.js +4 -0
  15. package/dist/lib/mesh/api.d.ts +18 -0
  16. package/dist/lib/mesh/api.js +55 -0
  17. package/dist/lib/mesh/cache.js +35 -4
  18. package/dist/lib/mesh/client.d.ts +118 -0
  19. package/dist/lib/mesh/client.js +268 -0
  20. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  21. package/dist/lib/mesh/live/backoff.js +20 -0
  22. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  23. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  24. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  25. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  26. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  27. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  28. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  29. package/dist/lib/mesh/live/daemon/index.js +14 -0
  30. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  31. package/dist/lib/mesh/live/daemon/install.js +330 -0
  32. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  33. package/dist/lib/mesh/live/daemon/log.js +79 -0
  34. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  35. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  36. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  37. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  38. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  39. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  40. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  41. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  42. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  43. package/dist/lib/mesh/live/daemon/run.js +400 -0
  44. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  45. package/dist/lib/mesh/live/daemon/state.js +57 -0
  46. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  47. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  48. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  49. package/dist/lib/mesh/live/enqueue.js +100 -0
  50. package/dist/lib/mesh/live/flush.d.ts +56 -0
  51. package/dist/lib/mesh/live/flush.js +360 -0
  52. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  53. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  54. package/dist/lib/mesh/live/index.d.ts +22 -0
  55. package/dist/lib/mesh/live/index.js +16 -0
  56. package/dist/lib/mesh/live/paths.d.ts +13 -0
  57. package/dist/lib/mesh/live/paths.js +30 -0
  58. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  59. package/dist/lib/mesh/live/session-events-client.js +120 -0
  60. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  61. package/dist/lib/mesh/live/session-identity.js +91 -0
  62. package/dist/lib/mesh/live/spool.d.ts +52 -0
  63. package/dist/lib/mesh/live/spool.js +193 -0
  64. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  65. package/dist/lib/mesh/live/ulid.js +39 -0
  66. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  67. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  68. package/dist/lib/search-index/index.d.ts +45 -0
  69. package/dist/lib/search-index/index.js +76 -0
  70. package/dist/lib/work-context/atomic.d.ts +18 -0
  71. package/dist/lib/work-context/atomic.js +132 -0
  72. package/dist/lib/work-context/company.d.ts +103 -0
  73. package/dist/lib/work-context/company.js +274 -0
  74. package/dist/lib/work-context/config.d.ts +59 -0
  75. package/dist/lib/work-context/config.js +172 -0
  76. package/dist/lib/work-context/contract.d.ts +81 -0
  77. package/dist/lib/work-context/contract.js +65 -0
  78. package/dist/lib/work-context/errors.d.ts +27 -0
  79. package/dist/lib/work-context/errors.js +46 -0
  80. package/dist/lib/work-context/index.d.ts +23 -0
  81. package/dist/lib/work-context/index.js +20 -0
  82. package/dist/lib/work-context/migrate.d.ts +84 -0
  83. package/dist/lib/work-context/migrate.js +287 -0
  84. package/dist/lib/work-context/organize.d.ts +130 -0
  85. package/dist/lib/work-context/organize.js +471 -0
  86. package/dist/lib/work-context/outbox.d.ts +97 -0
  87. package/dist/lib/work-context/outbox.js +357 -0
  88. package/dist/lib/work-context/paths.d.ts +14 -0
  89. package/dist/lib/work-context/paths.js +39 -0
  90. package/dist/lib/work-context/project.d.ts +78 -0
  91. package/dist/lib/work-context/project.js +247 -0
  92. package/dist/lib/work-context/reconcile.d.ts +85 -0
  93. package/dist/lib/work-context/reconcile.js +651 -0
  94. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  95. package/dist/lib/work-context/repo-remote.js +200 -0
  96. package/dist/lib/work-context/state.d.ts +112 -0
  97. package/dist/lib/work-context/state.js +162 -0
  98. package/dist/main.js +40 -3
  99. package/dist/utils/cognito-session.d.ts +85 -9
  100. package/dist/utils/cognito-session.js +216 -9
  101. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  102. package/dist/utils/qmd-model-download-error.js +114 -0
  103. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  104. package/dist/utils/qmd-query-document-error.js +117 -0
  105. package/dist/utils/sentry-fingerprint.js +2 -0
  106. package/package.json +5 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Flush spool + held session events (US-008).
3
+ *
4
+ * Claim-by-rename → group by sessionId → read work-context state →
5
+ * drop untracked / hold unverified / strip local-only → POST batches of ≤100
6
+ * with bounded full-jitter retries. Never touches sessions-bind.json.
7
+ *
8
+ * Orphan `*.claimed` files from a prior crash are recovered first.
9
+ * 2xx responses are accounted per eventId (accepted / duplicates / rejected).
10
+ */
11
+ import { type RandomFn, type SleepFn } from "./backoff.js";
12
+ import { type SessionEventsPoster } from "./session-events-client.js";
13
+ export declare const HELD_TTL_MS: number;
14
+ export type HeldReason = "STATE_ABSENT" | "NEEDS_COMPANY" | "COMPANY_CONFLICT" | "HELD_EXPIRED";
15
+ export interface HeldLineRecord {
16
+ /** Original spool event fields. */
17
+ event: Record<string, unknown>;
18
+ heldReason: HeldReason;
19
+ heldAt: string;
20
+ }
21
+ export interface FlushDeps {
22
+ workMeshRoot: string;
23
+ workContextRoot: string;
24
+ poster: SessionEventsPoster;
25
+ now?: () => Date;
26
+ sleep?: SleepFn;
27
+ random?: RandomFn;
28
+ maxAttempts?: number;
29
+ }
30
+ export interface FlushSummary {
31
+ claimed: number;
32
+ posted: number;
33
+ held: number;
34
+ dropped: number;
35
+ deadLettered: number;
36
+ restored: number;
37
+ batches: number;
38
+ }
39
+ /**
40
+ * Flush spool + held. Safe to call concurrently with enqueue (claim-by-rename).
41
+ * Recovers orphan `*.claimed` files first; restores unprocessed lines on throw.
42
+ */
43
+ export declare function flushSessionEvents(deps: FlushDeps): Promise<FlushSummary>;
44
+ /**
45
+ * Assert flush never opens the machine-global bind cache.
46
+ * Used by tests; production flush has no reference to this path.
47
+ */
48
+ export declare function sessionsBindPath(home: string): string;
49
+ export declare function assertNoBindCacheAccess(root: string): void;
50
+ /** Convenience: build FlushDeps from env/home with a poster. */
51
+ export declare function defaultFlushRoots(env?: NodeJS.ProcessEnv): {
52
+ workMeshRoot: string;
53
+ workContextRoot: string;
54
+ };
55
+ export declare function bindCacheExists(meshRoot: string): boolean;
56
+ //# sourceMappingURL=flush.d.ts.map
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Flush spool + held session events (US-008).
3
+ *
4
+ * Claim-by-rename → group by sessionId → read work-context state →
5
+ * drop untracked / hold unverified / strip local-only → POST batches of ≤100
6
+ * with bounded full-jitter retries. Never touches sessions-bind.json.
7
+ *
8
+ * Orphan `*.claimed` files from a prior crash are recovered first.
9
+ * 2xx responses are accounted per eventId (accepted / duplicates / rejected).
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { readSessionState } from "../../work-context/state.js";
14
+ import { workContextRoot } from "../../work-context/paths.js";
15
+ import { defaultSleep, FLUSH_MAX_ATTEMPTS, fullJitterDelayMs, } from "./backoff.js";
16
+ import { LOCAL_ONLY_FIELDS, stripLocalOnlyFields } from "./format-spool-line.js";
17
+ import { SESSION_EVENTS_BATCH_MAX, WRITABLE_CONTEXT_STATUSES, parseSessionEventsBatchAck, } from "./session-events-client.js";
18
+ import { appendDeadLetterLine, appendDroppedReceipt, appendHeldLine, appendSpoolLine, claimHeld, claimSpool, recoverOrphanClaims, removeClaimFile, } from "./spool.js";
19
+ import { workMeshRoot } from "./paths.js";
20
+ export const HELD_TTL_MS = 7 * 24 * 60 * 60 * 1000;
21
+ function parseLine(line) {
22
+ try {
23
+ const v = JSON.parse(line);
24
+ if (!v || typeof v !== "object" || Array.isArray(v))
25
+ return null;
26
+ return v;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function sessionIdOf(obj) {
33
+ return typeof obj.sessionId === "string" && obj.sessionId.trim()
34
+ ? obj.sessionId.trim()
35
+ : null;
36
+ }
37
+ function eventIdOf(obj) {
38
+ return typeof obj.eventId === "string" && obj.eventId.trim()
39
+ ? obj.eventId.trim()
40
+ : null;
41
+ }
42
+ /** Peel held envelope if present; return event + prior heldAt. */
43
+ function unwrapHeld(obj) {
44
+ if (obj.event &&
45
+ typeof obj.event === "object" &&
46
+ !Array.isArray(obj.event) &&
47
+ typeof obj.heldReason === "string") {
48
+ return {
49
+ event: obj.event,
50
+ heldAt: typeof obj.heldAt === "string" ? obj.heldAt : undefined,
51
+ heldReason: obj.heldReason,
52
+ };
53
+ }
54
+ return { event: obj };
55
+ }
56
+ function heldEnvelope(event, heldReason, heldAt) {
57
+ return JSON.stringify({ event, heldReason, heldAt });
58
+ }
59
+ function deadLetterEnvelope(event, reason, responseCode, at) {
60
+ return JSON.stringify({ event, reason, responseCode, at });
61
+ }
62
+ function groupBySession(events) {
63
+ const map = new Map();
64
+ for (const item of events) {
65
+ const sid = sessionIdOf(item.event);
66
+ if (!sid) {
67
+ // Malformed: dead-letter immediately by caller — skip grouping key.
68
+ continue;
69
+ }
70
+ const list = map.get(sid) ?? [];
71
+ list.push(item);
72
+ map.set(sid, list);
73
+ }
74
+ return map;
75
+ }
76
+ async function postBatchWithRetries(poster, body, deps) {
77
+ let lastStatus = 0;
78
+ let lastBody = {};
79
+ for (let attempt = 0; attempt < deps.maxAttempts; attempt += 1) {
80
+ const res = await poster(body);
81
+ lastStatus = res.status;
82
+ lastBody = res.body;
83
+ if (res.ok)
84
+ return { ok: true, body: res.body };
85
+ if (!res.retryable) {
86
+ return { ok: false, retryable: false, status: res.status, body: res.body };
87
+ }
88
+ if (attempt + 1 >= deps.maxAttempts)
89
+ break;
90
+ const delay = fullJitterDelayMs(attempt, { random: deps.random });
91
+ await deps.sleep(delay);
92
+ }
93
+ return { ok: false, retryable: true, status: lastStatus, body: lastBody };
94
+ }
95
+ /**
96
+ * Flush spool + held. Safe to call concurrently with enqueue (claim-by-rename).
97
+ * Recovers orphan `*.claimed` files first; restores unprocessed lines on throw.
98
+ */
99
+ export async function flushSessionEvents(deps) {
100
+ const now = deps.now ?? (() => new Date());
101
+ const sleep = deps.sleep ?? defaultSleep;
102
+ const random = deps.random ?? Math.random;
103
+ const maxAttempts = deps.maxAttempts ?? FLUSH_MAX_ATTEMPTS;
104
+ const ts = `${now().getTime()}`;
105
+ const orphans = recoverOrphanClaims(deps.workMeshRoot);
106
+ const spoolClaim = claimSpool(deps.workMeshRoot, ts);
107
+ const heldClaim = claimHeld(deps.workMeshRoot, `${ts}.held`);
108
+ const claimPaths = [
109
+ ...orphans.claimPaths,
110
+ spoolClaim.claimPath,
111
+ heldClaim.claimPath,
112
+ ].filter((p) => Boolean(p));
113
+ const summary = {
114
+ claimed: 0,
115
+ posted: 0,
116
+ held: 0,
117
+ dropped: 0,
118
+ deadLettered: 0,
119
+ restored: 0,
120
+ batches: 0,
121
+ };
122
+ /** Original events still awaiting disposition (restored on throw). */
123
+ const pendingRestore = [];
124
+ const collected = [];
125
+ try {
126
+ for (const line of [...orphans.lines, ...spoolClaim.lines, ...heldClaim.lines]) {
127
+ summary.claimed += 1;
128
+ const parsed = parseLine(line);
129
+ if (!parsed) {
130
+ appendDeadLetterLine(deadLetterEnvelope({ raw: line.slice(0, 200) }, "SCHEMA_REJECTED", "parse_error", now().toISOString()), deps.workMeshRoot);
131
+ summary.deadLettered += 1;
132
+ continue;
133
+ }
134
+ const { event, heldAt } = unwrapHeld(parsed);
135
+ if (!sessionIdOf(event)) {
136
+ appendDeadLetterLine(deadLetterEnvelope(event, "SCHEMA_REJECTED", "missing_sessionId", now().toISOString()), deps.workMeshRoot);
137
+ summary.deadLettered += 1;
138
+ continue;
139
+ }
140
+ // Expire held lines older than 7 days.
141
+ if (heldAt) {
142
+ const age = now().getTime() - Date.parse(heldAt);
143
+ if (Number.isFinite(age) && age > HELD_TTL_MS) {
144
+ appendDeadLetterLine(deadLetterEnvelope(event, "HELD_EXPIRED", "held_ttl", now().toISOString()), deps.workMeshRoot);
145
+ summary.deadLettered += 1;
146
+ continue;
147
+ }
148
+ }
149
+ pendingRestore.push(event);
150
+ collected.push({ event, heldAt });
151
+ }
152
+ const bySession = groupBySession(collected);
153
+ const markDisposed = (event) => {
154
+ const idx = pendingRestore.indexOf(event);
155
+ if (idx >= 0)
156
+ pendingRestore.splice(idx, 1);
157
+ };
158
+ for (const [sessionId, items] of bySession) {
159
+ const state = readSessionState(sessionId, deps.workContextRoot);
160
+ const decision = classifySession(state);
161
+ if (decision.action === "drop") {
162
+ appendDroppedReceipt({
163
+ v: 1,
164
+ sessionId,
165
+ dropped: items.length,
166
+ reason: "untracked",
167
+ at: now().toISOString(),
168
+ }, deps.workMeshRoot);
169
+ summary.dropped += items.length;
170
+ for (const item of items)
171
+ markDisposed(item.event);
172
+ continue;
173
+ }
174
+ if (decision.action === "hold") {
175
+ const heldAt = now().toISOString();
176
+ for (const item of items) {
177
+ appendHeldLine(heldEnvelope(item.event, decision.heldReason, item.heldAt ?? heldAt), deps.workMeshRoot);
178
+ summary.held += 1;
179
+ markDisposed(item.event);
180
+ }
181
+ continue;
182
+ }
183
+ // Post path: strip local-only, batch ≤100, retry.
184
+ const stripped = items.map((item) => stripLocalOnlyFields(item.event));
185
+ for (let i = 0; i < stripped.length; i += SESSION_EVENTS_BATCH_MAX) {
186
+ const chunk = stripped.slice(i, i + SESSION_EVENTS_BATCH_MAX);
187
+ const originalChunk = items.slice(i, i + SESSION_EVENTS_BATCH_MAX);
188
+ const body = {
189
+ contractVersion: 1,
190
+ companyUid: decision.companyUid,
191
+ sessionId,
192
+ contextStatus: decision.contextStatus,
193
+ events: chunk,
194
+ };
195
+ if (decision.bindingEpisodeId)
196
+ body.bindingEpisodeId = decision.bindingEpisodeId;
197
+ if (decision.projectId)
198
+ body.projectId = decision.projectId;
199
+ if (decision.taskId)
200
+ body.taskId = decision.taskId;
201
+ summary.batches += 1;
202
+ const result = await postBatchWithRetries(deps.poster, body, {
203
+ maxAttempts,
204
+ sleep,
205
+ random,
206
+ });
207
+ if (result.ok) {
208
+ accountBatchSuccess(result.body, originalChunk, deps.workMeshRoot, now, summary, markDisposed);
209
+ continue;
210
+ }
211
+ if (!result.retryable) {
212
+ const at = now().toISOString();
213
+ for (const item of originalChunk) {
214
+ appendDeadLetterLine(deadLetterEnvelope(item.event, `HTTP_${result.status}`, result.status, at), deps.workMeshRoot);
215
+ summary.deadLettered += 1;
216
+ markDisposed(item.event);
217
+ }
218
+ continue;
219
+ }
220
+ // Retry exhaustion (5xx / network): restore to spool intact.
221
+ for (const item of originalChunk) {
222
+ const line = JSON.stringify(canonicalizeForRestore(item.event));
223
+ appendSpoolLine(line, deps.workMeshRoot);
224
+ summary.restored += 1;
225
+ markDisposed(item.event);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ finally {
231
+ // Crash / throw mid-flush: put unprocessed events back on the spool.
232
+ for (const event of pendingRestore) {
233
+ try {
234
+ appendSpoolLine(JSON.stringify(canonicalizeForRestore(event)), deps.workMeshRoot);
235
+ summary.restored += 1;
236
+ }
237
+ catch {
238
+ /* best-effort; orphan claim scan will retry next flush */
239
+ }
240
+ }
241
+ pendingRestore.length = 0;
242
+ for (const p of claimPaths) {
243
+ removeClaimFile(p);
244
+ }
245
+ }
246
+ return summary;
247
+ }
248
+ /**
249
+ * Account a 2xx batch: dead-letter rejected eventIds with their codes;
250
+ * count accepted+duplicates as posted; restore any eventId not accounted for.
251
+ */
252
+ function accountBatchSuccess(body, originalChunk, meshRoot, now, summary, markDisposed) {
253
+ const ack = parseSessionEventsBatchAck(body);
254
+ if (!ack) {
255
+ // Unparseable 2xx: retain everything rather than pretend posted.
256
+ for (const item of originalChunk) {
257
+ appendSpoolLine(JSON.stringify(canonicalizeForRestore(item.event)), meshRoot);
258
+ summary.restored += 1;
259
+ markDisposed(item.event);
260
+ }
261
+ return;
262
+ }
263
+ const byId = new Map();
264
+ for (const item of originalChunk) {
265
+ const id = eventIdOf(item.event);
266
+ if (id)
267
+ byId.set(id, item);
268
+ }
269
+ const rejectedIds = new Set();
270
+ const at = now().toISOString();
271
+ for (const row of ack.rejected) {
272
+ const item = byId.get(row.eventId);
273
+ if (!item)
274
+ continue;
275
+ appendDeadLetterLine(deadLetterEnvelope(item.event, row.code, row.code, at), meshRoot);
276
+ summary.deadLettered += 1;
277
+ rejectedIds.add(row.eventId);
278
+ markDisposed(item.event);
279
+ }
280
+ const remaining = originalChunk.filter((item) => {
281
+ const id = eventIdOf(item.event);
282
+ return !id || !rejectedIds.has(id);
283
+ });
284
+ const accounted = ack.accepted + ack.duplicates;
285
+ if (accounted === remaining.length) {
286
+ for (const item of remaining) {
287
+ summary.posted += 1;
288
+ markDisposed(item.event);
289
+ }
290
+ return;
291
+ }
292
+ // Under-accounted (or over): retain every remaining event rather than delete.
293
+ for (const item of remaining) {
294
+ appendSpoolLine(JSON.stringify(canonicalizeForRestore(item.event)), meshRoot);
295
+ summary.restored += 1;
296
+ markDisposed(item.event);
297
+ }
298
+ }
299
+ function canonicalizeForRestore(event) {
300
+ // Preserve known fields; drop held envelope keys if any leaked.
301
+ const out = {};
302
+ for (const [k, v] of Object.entries(event)) {
303
+ if (k === "heldReason" || k === "heldAt" || k === "event")
304
+ continue;
305
+ out[k] = v;
306
+ }
307
+ return out;
308
+ }
309
+ function classifySession(state) {
310
+ if (!state) {
311
+ return { action: "hold", heldReason: "STATE_ABSENT" };
312
+ }
313
+ if (state.contextStatus === "untracked") {
314
+ return { action: "drop" };
315
+ }
316
+ if (state.contextStatus === "needs_company") {
317
+ return { action: "hold", heldReason: "NEEDS_COMPANY" };
318
+ }
319
+ if (state.contextStatus === "company_conflict") {
320
+ return { action: "hold", heldReason: "COMPANY_CONFLICT" };
321
+ }
322
+ if (!WRITABLE_CONTEXT_STATUSES.has(state.contextStatus)) {
323
+ return { action: "hold", heldReason: "STATE_ABSENT" };
324
+ }
325
+ if (!state.companyUid) {
326
+ return { action: "hold", heldReason: "NEEDS_COMPANY" };
327
+ }
328
+ return {
329
+ action: "post",
330
+ companyUid: state.companyUid,
331
+ contextStatus: state.contextStatus,
332
+ bindingEpisodeId: state.bindingEpisodeId,
333
+ projectId: state.projectId,
334
+ taskId: state.taskId,
335
+ };
336
+ }
337
+ /**
338
+ * Assert flush never opens the machine-global bind cache.
339
+ * Used by tests; production flush has no reference to this path.
340
+ */
341
+ export function sessionsBindPath(home) {
342
+ return path.join(home, ".hq", "work-mesh", "cache", "sessions-bind.json");
343
+ }
344
+ export function assertNoBindCacheAccess(root) {
345
+ const bind = path.join(root, "cache", "sessions-bind.json");
346
+ // Touch-detection helper for tests: we simply ensure we never create it.
347
+ void bind;
348
+ void LOCAL_ONLY_FIELDS;
349
+ }
350
+ /** Convenience: build FlushDeps from env/home with a poster. */
351
+ export function defaultFlushRoots(env = process.env) {
352
+ return {
353
+ workMeshRoot: workMeshRoot(undefined, env),
354
+ workContextRoot: workContextRoot(undefined, env),
355
+ };
356
+ }
357
+ export function bindCacheExists(meshRoot) {
358
+ return fs.existsSync(path.join(meshRoot, "cache", "sessions-bind.json"));
359
+ }
360
+ //# sourceMappingURL=flush.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Canonical spool-line JSON. Node and the POSIX shell writer must emit
3
+ * byte-identical lines for the same inputs (fixed key order, compact JSON).
4
+ */
5
+ export declare const LOCAL_ONLY_FIELDS: readonly ["cwd", "hqRoot", "companySlug", "project", "task", "toolWrites"];
6
+ export type LocalOnlyField = (typeof LOCAL_ONLY_FIELDS)[number];
7
+ /** Network + local key order matching work-mesh-enqueue.sh. */
8
+ export declare const SPOOL_KEY_ORDER: readonly ["v", "eventId", "kind", "sessionId", "harness", "adapterVersion", "runtimeVersion", "source", "at", "seq", "taskId", "status", "reason", "summary", "cwd", "hqRoot", "companySlug", "project", "task", "toolWrites"];
9
+ export type SessionEventKind = "session_start" | "turn_start" | "turn_end" | "session_end" | "task_status" | "blocked" | "note";
10
+ export type SessionEventHarness = "claude-code" | "claude-desktop" | "codex" | "grok" | "hq-sessions" | "agent-box";
11
+ export type SessionEventSource = "hooks" | "transcript";
12
+ export interface SpoolEventInput {
13
+ v?: 1;
14
+ eventId: string;
15
+ kind: SessionEventKind;
16
+ sessionId: string;
17
+ harness: SessionEventHarness;
18
+ adapterVersion: string;
19
+ runtimeVersion?: string;
20
+ source?: SessionEventSource;
21
+ at: string;
22
+ seq: number;
23
+ taskId?: string;
24
+ status?: "queued" | "in_progress" | "review" | "done";
25
+ reason?: string;
26
+ summary?: string;
27
+ cwd?: string;
28
+ hqRoot?: string;
29
+ companySlug?: string;
30
+ project?: string;
31
+ task?: string;
32
+ toolWrites?: number;
33
+ }
34
+ /**
35
+ * Build a compact JSON object in SPOOL_KEY_ORDER, omitting absent optional
36
+ * strings. `toolWrites` is included when it is a finite number >= 0 (including 0).
37
+ */
38
+ export declare function buildOrderedSpoolObject(input: SpoolEventInput): Record<string, string | number>;
39
+ /** Compact JSON line body (no trailing newline). */
40
+ export declare function formatSpoolLine(input: SpoolEventInput): string;
41
+ export declare function stripLocalOnlyFields(event: Record<string, unknown>): Record<string, unknown>;
42
+ //# sourceMappingURL=format-spool-line.d.ts.map
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Canonical spool-line JSON. Node and the POSIX shell writer must emit
3
+ * byte-identical lines for the same inputs (fixed key order, compact JSON).
4
+ */
5
+ export const LOCAL_ONLY_FIELDS = [
6
+ "cwd",
7
+ "hqRoot",
8
+ "companySlug",
9
+ "project",
10
+ "task",
11
+ "toolWrites",
12
+ ];
13
+ /** Network + local key order matching work-mesh-enqueue.sh. */
14
+ export const SPOOL_KEY_ORDER = [
15
+ "v",
16
+ "eventId",
17
+ "kind",
18
+ "sessionId",
19
+ "harness",
20
+ "adapterVersion",
21
+ "runtimeVersion",
22
+ "source",
23
+ "at",
24
+ "seq",
25
+ "taskId",
26
+ "status",
27
+ "reason",
28
+ "summary",
29
+ "cwd",
30
+ "hqRoot",
31
+ "companySlug",
32
+ "project",
33
+ "task",
34
+ "toolWrites",
35
+ ];
36
+ function includeString(value) {
37
+ return typeof value === "string" && value.length > 0;
38
+ }
39
+ /**
40
+ * Build a compact JSON object in SPOOL_KEY_ORDER, omitting absent optional
41
+ * strings. `toolWrites` is included when it is a finite number >= 0 (including 0).
42
+ */
43
+ export function buildOrderedSpoolObject(input) {
44
+ const out = {
45
+ v: 1,
46
+ eventId: input.eventId,
47
+ kind: input.kind,
48
+ sessionId: input.sessionId,
49
+ harness: input.harness,
50
+ adapterVersion: input.adapterVersion,
51
+ };
52
+ if (includeString(input.runtimeVersion))
53
+ out.runtimeVersion = input.runtimeVersion;
54
+ if (includeString(input.source))
55
+ out.source = input.source;
56
+ out.at = input.at;
57
+ out.seq = input.seq;
58
+ if (includeString(input.taskId))
59
+ out.taskId = input.taskId;
60
+ if (includeString(input.status))
61
+ out.status = input.status;
62
+ if (includeString(input.reason))
63
+ out.reason = input.reason;
64
+ if (includeString(input.summary))
65
+ out.summary = input.summary;
66
+ if (includeString(input.cwd))
67
+ out.cwd = input.cwd;
68
+ if (includeString(input.hqRoot))
69
+ out.hqRoot = input.hqRoot;
70
+ if (includeString(input.companySlug))
71
+ out.companySlug = input.companySlug;
72
+ if (includeString(input.project))
73
+ out.project = input.project;
74
+ if (includeString(input.task))
75
+ out.task = input.task;
76
+ if (typeof input.toolWrites === "number" &&
77
+ Number.isFinite(input.toolWrites) &&
78
+ input.toolWrites >= 0) {
79
+ out.toolWrites = Math.floor(input.toolWrites);
80
+ }
81
+ return out;
82
+ }
83
+ /** Compact JSON line body (no trailing newline). */
84
+ export function formatSpoolLine(input) {
85
+ const ordered = buildOrderedSpoolObject(input);
86
+ // Re-insert in canonical order so JSON.stringify key order is stable.
87
+ const canonical = {};
88
+ for (const key of SPOOL_KEY_ORDER) {
89
+ if (Object.prototype.hasOwnProperty.call(ordered, key)) {
90
+ canonical[key] = ordered[key];
91
+ }
92
+ }
93
+ return JSON.stringify(canonical);
94
+ }
95
+ export function stripLocalOnlyFields(event) {
96
+ const out = {};
97
+ for (const key of SPOOL_KEY_ORDER) {
98
+ if (LOCAL_ONLY_FIELDS.includes(key))
99
+ continue;
100
+ if (Object.prototype.hasOwnProperty.call(event, key) && event[key] !== undefined) {
101
+ out[key] = event[key];
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ //# sourceMappingURL=format-spool-line.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Work Mesh Live client-side contract package (hq-cli).
3
+ * Schema is a verbatim copy of hq-pro's session-event.schema.json.
4
+ */
5
+ export { getSessionEventValidator, PROHIBITED_CONTENT_CLASSES, PROHIBITED_FIELD_BY_CLASS, redactValidationMessage, resolveHqProSchemaPath, SESSION_EVENT_SCHEMA_PATH, SESSION_EVENT_SCHEMA_SHA256, sha256File, sha256SessionEventSchema, validateSessionEvent, } from "./validate-session-event.js";
6
+ export type { ProhibitedContentClass, SessionEventValidationResult, } from "./validate-session-event.js";
7
+ export { InvalidSessionIdentityError, SESSION_ID_ENV_KEYS, isValidSessionId, normalizeSessionIdentity, sessionIdFromEnv, } from "./session-identity.js";
8
+ export type { NormalizeSessionIdentityInput, NormalizedSessionIdentity, } from "./session-identity.js";
9
+ export { LOCAL_ONLY_FIELDS, SPOOL_KEY_ORDER, buildOrderedSpoolObject, formatSpoolLine, stripLocalOnlyFields, } from "./format-spool-line.js";
10
+ export type { LocalOnlyField, SessionEventHarness, SessionEventKind, SessionEventSource, SpoolEventInput, } from "./format-spool-line.js";
11
+ export { encodeCrockford, generateUlid, isUlid } from "./ulid.js";
12
+ export { workMeshClaimPath, workMeshDeadLetterPath, workMeshDroppedReceiptPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "./paths.js";
13
+ export { SPOOL_LINE_MAX_BYTES, SpoolWriteError, appendDeadLetterLine, appendDroppedReceipt, appendHeldLine, appendJsonlLine, appendSpoolLine, claimHeld, claimJsonlByRename, claimSpool, countJsonlLines, deadLetterNonEmpty, ensureSpoolFile, removeClaimFile, } from "./spool.js";
14
+ export { CLI_KIND_TO_SCHEMA, EnqueueValidationError, enqueueSessionEvent, resolveEnqueueSessionId, } from "./enqueue.js";
15
+ export type { EnqueueOptions, EnqueueResult } from "./enqueue.js";
16
+ export { HELD_TTL_MS, assertNoBindCacheAccess, bindCacheExists, defaultFlushRoots, flushSessionEvents, sessionsBindPath, } from "./flush.js";
17
+ export type { FlushDeps, FlushSummary, HeldReason } from "./flush.js";
18
+ export { BACKOFF_BASE_MS, BACKOFF_CAP_MS, FLUSH_MAX_ATTEMPTS, defaultSleep, fullJitterDelayMs, } from "./backoff.js";
19
+ export { SESSION_EVENTS_BATCH_MAX, SESSION_EVENTS_PATH, WRITABLE_CONTEXT_STATUSES, classifySessionEventsResponse, createSessionEventsPoster, resolveVaultApiBase, } from "./session-events-client.js";
20
+ export type { SessionEventsBatchBody, SessionEventsPostResult, SessionEventsPoster, } from "./session-events-client.js";
21
+ export * from "./daemon/index.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Work Mesh Live client-side contract package (hq-cli).
3
+ * Schema is a verbatim copy of hq-pro's session-event.schema.json.
4
+ */
5
+ export { getSessionEventValidator, PROHIBITED_CONTENT_CLASSES, PROHIBITED_FIELD_BY_CLASS, redactValidationMessage, resolveHqProSchemaPath, SESSION_EVENT_SCHEMA_PATH, SESSION_EVENT_SCHEMA_SHA256, sha256File, sha256SessionEventSchema, validateSessionEvent, } from "./validate-session-event.js";
6
+ export { InvalidSessionIdentityError, SESSION_ID_ENV_KEYS, isValidSessionId, normalizeSessionIdentity, sessionIdFromEnv, } from "./session-identity.js";
7
+ export { LOCAL_ONLY_FIELDS, SPOOL_KEY_ORDER, buildOrderedSpoolObject, formatSpoolLine, stripLocalOnlyFields, } from "./format-spool-line.js";
8
+ export { encodeCrockford, generateUlid, isUlid } from "./ulid.js";
9
+ export { workMeshClaimPath, workMeshDeadLetterPath, workMeshDroppedReceiptPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "./paths.js";
10
+ export { SPOOL_LINE_MAX_BYTES, SpoolWriteError, appendDeadLetterLine, appendDroppedReceipt, appendHeldLine, appendJsonlLine, appendSpoolLine, claimHeld, claimJsonlByRename, claimSpool, countJsonlLines, deadLetterNonEmpty, ensureSpoolFile, removeClaimFile, } from "./spool.js";
11
+ export { CLI_KIND_TO_SCHEMA, EnqueueValidationError, enqueueSessionEvent, resolveEnqueueSessionId, } from "./enqueue.js";
12
+ export { HELD_TTL_MS, assertNoBindCacheAccess, bindCacheExists, defaultFlushRoots, flushSessionEvents, sessionsBindPath, } from "./flush.js";
13
+ export { BACKOFF_BASE_MS, BACKOFF_CAP_MS, FLUSH_MAX_ATTEMPTS, defaultSleep, fullJitterDelayMs, } from "./backoff.js";
14
+ export { SESSION_EVENTS_BATCH_MAX, SESSION_EVENTS_PATH, WRITABLE_CONTEXT_STATUSES, classifySessionEventsResponse, createSessionEventsPoster, resolveVaultApiBase, } from "./session-events-client.js";
15
+ export * from "./daemon/index.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Local Work Mesh Live paths under ~/.hq/work-mesh/.
3
+ * Injectable root for tests (never touch the real HOME in unit tests).
4
+ */
5
+ export declare const WORK_MESH_DIRNAME = "work-mesh";
6
+ export declare function workMeshRoot(home?: string, env?: NodeJS.ProcessEnv): string;
7
+ export declare function workMeshSpoolPath(root?: string): string;
8
+ export declare function workMeshHeldPath(root?: string): string;
9
+ export declare function workMeshDeadLetterPath(root?: string): string;
10
+ export declare function workMeshDroppedReceiptPath(root?: string): string;
11
+ /** Claim path: spool.<ts>.claimed (never reuse spool.jsonl for claim). */
12
+ export declare function workMeshClaimPath(kind: "spool" | "held", ts: string, root?: string): string;
13
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Local Work Mesh Live paths under ~/.hq/work-mesh/.
3
+ * Injectable root for tests (never touch the real HOME in unit tests).
4
+ */
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ export const WORK_MESH_DIRNAME = "work-mesh";
8
+ export function workMeshRoot(home = os.homedir(), env = process.env) {
9
+ const override = env.HQ_WORK_MESH_ROOT?.trim();
10
+ if (override)
11
+ return override;
12
+ return path.join(home, ".hq", WORK_MESH_DIRNAME);
13
+ }
14
+ export function workMeshSpoolPath(root) {
15
+ return path.join(root ?? workMeshRoot(), "spool.jsonl");
16
+ }
17
+ export function workMeshHeldPath(root) {
18
+ return path.join(root ?? workMeshRoot(), "held.jsonl");
19
+ }
20
+ export function workMeshDeadLetterPath(root) {
21
+ return path.join(root ?? workMeshRoot(), "dead-letter.jsonl");
22
+ }
23
+ export function workMeshDroppedReceiptPath(root) {
24
+ return path.join(root ?? workMeshRoot(), "dropped-receipts.jsonl");
25
+ }
26
+ /** Claim path: spool.<ts>.claimed (never reuse spool.jsonl for claim). */
27
+ export function workMeshClaimPath(kind, ts, root) {
28
+ return path.join(root ?? workMeshRoot(), `${kind}.${ts}.claimed`);
29
+ }
30
+ //# sourceMappingURL=paths.js.map