@indigoai-us/hq-cli 5.108.0 → 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 (108) hide show
  1. package/CHANGELOG.md +79 -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/cloud.d.ts +6 -0
  8. package/dist/commands/cloud.js +26 -3
  9. package/dist/commands/core.js +5 -4
  10. package/dist/commands/mesh.d.ts +15 -0
  11. package/dist/commands/mesh.js +732 -4
  12. package/dist/commands/search.d.ts +23 -0
  13. package/dist/commands/search.js +42 -1
  14. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  15. package/dist/lib/doctor/checks/work-context.js +195 -0
  16. package/dist/lib/doctor/registry.js +4 -0
  17. package/dist/lib/mesh/api.d.ts +18 -0
  18. package/dist/lib/mesh/api.js +55 -0
  19. package/dist/lib/mesh/cache.js +35 -4
  20. package/dist/lib/mesh/client.d.ts +118 -0
  21. package/dist/lib/mesh/client.js +268 -0
  22. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  23. package/dist/lib/mesh/live/backoff.js +20 -0
  24. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  25. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  26. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  27. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  28. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  29. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  30. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  31. package/dist/lib/mesh/live/daemon/index.js +14 -0
  32. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  33. package/dist/lib/mesh/live/daemon/install.js +330 -0
  34. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  35. package/dist/lib/mesh/live/daemon/log.js +79 -0
  36. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  37. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  38. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  39. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  40. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  41. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  42. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  43. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  44. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  45. package/dist/lib/mesh/live/daemon/run.js +400 -0
  46. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  47. package/dist/lib/mesh/live/daemon/state.js +57 -0
  48. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  49. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  50. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  51. package/dist/lib/mesh/live/enqueue.js +100 -0
  52. package/dist/lib/mesh/live/flush.d.ts +56 -0
  53. package/dist/lib/mesh/live/flush.js +360 -0
  54. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  55. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  56. package/dist/lib/mesh/live/index.d.ts +22 -0
  57. package/dist/lib/mesh/live/index.js +16 -0
  58. package/dist/lib/mesh/live/paths.d.ts +13 -0
  59. package/dist/lib/mesh/live/paths.js +30 -0
  60. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  61. package/dist/lib/mesh/live/session-events-client.js +120 -0
  62. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  63. package/dist/lib/mesh/live/session-identity.js +91 -0
  64. package/dist/lib/mesh/live/spool.d.ts +52 -0
  65. package/dist/lib/mesh/live/spool.js +193 -0
  66. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  67. package/dist/lib/mesh/live/ulid.js +39 -0
  68. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  69. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  70. package/dist/lib/search-index/index.d.ts +45 -0
  71. package/dist/lib/search-index/index.js +76 -0
  72. package/dist/lib/work-context/atomic.d.ts +18 -0
  73. package/dist/lib/work-context/atomic.js +132 -0
  74. package/dist/lib/work-context/company.d.ts +103 -0
  75. package/dist/lib/work-context/company.js +274 -0
  76. package/dist/lib/work-context/config.d.ts +59 -0
  77. package/dist/lib/work-context/config.js +172 -0
  78. package/dist/lib/work-context/contract.d.ts +81 -0
  79. package/dist/lib/work-context/contract.js +65 -0
  80. package/dist/lib/work-context/errors.d.ts +27 -0
  81. package/dist/lib/work-context/errors.js +46 -0
  82. package/dist/lib/work-context/index.d.ts +23 -0
  83. package/dist/lib/work-context/index.js +20 -0
  84. package/dist/lib/work-context/migrate.d.ts +84 -0
  85. package/dist/lib/work-context/migrate.js +287 -0
  86. package/dist/lib/work-context/organize.d.ts +130 -0
  87. package/dist/lib/work-context/organize.js +471 -0
  88. package/dist/lib/work-context/outbox.d.ts +97 -0
  89. package/dist/lib/work-context/outbox.js +357 -0
  90. package/dist/lib/work-context/paths.d.ts +14 -0
  91. package/dist/lib/work-context/paths.js +39 -0
  92. package/dist/lib/work-context/project.d.ts +78 -0
  93. package/dist/lib/work-context/project.js +247 -0
  94. package/dist/lib/work-context/reconcile.d.ts +85 -0
  95. package/dist/lib/work-context/reconcile.js +651 -0
  96. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  97. package/dist/lib/work-context/repo-remote.js +200 -0
  98. package/dist/lib/work-context/state.d.ts +112 -0
  99. package/dist/lib/work-context/state.js +162 -0
  100. package/dist/main.js +40 -3
  101. package/dist/utils/cognito-session.d.ts +85 -9
  102. package/dist/utils/cognito-session.js +216 -9
  103. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  104. package/dist/utils/qmd-model-download-error.js +114 -0
  105. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  106. package/dist/utils/qmd-query-document-error.js +117 -0
  107. package/dist/utils/sentry-fingerprint.js +2 -0
  108. package/package.json +5 -1
@@ -0,0 +1,471 @@
1
+ /**
2
+ * hq mesh context organize — one-time decisions and US-005B submit (US-007B).
3
+ *
4
+ * CLI only presents server candidates and submits structured decisions.
5
+ * Ranking is server-owned. Success is reported only from the server receipt.
6
+ * Cancel / timeout / incapable surface leaves the session unresolved; the CLI
7
+ * never invents a project or adhoc target and never repeats the ask.
8
+ */
9
+ import * as crypto from "node:crypto";
10
+ import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
11
+ import { NotTrackingError } from "./errors.js";
12
+ import { shouldAskAfter } from "./project.js";
13
+ import { mergeLocalSessionFields, readSessionState, writeSessionState, } from "./state.js";
14
+ /** Stable decision id for a session + expiry window. */
15
+ export function stableDecisionId(sessionId, decisionExpiresAt) {
16
+ const h = crypto
17
+ .createHash("sha256")
18
+ .update(`dec:v1:${sessionId}:${decisionExpiresAt}`)
19
+ .digest("hex")
20
+ .slice(0, 24);
21
+ return `dec_${h}`;
22
+ }
23
+ export function digestOrganizePayload(parts) {
24
+ const canonical = JSON.stringify({
25
+ v: 1,
26
+ sessionId: parts.sessionId,
27
+ companyUid: parts.companyUid,
28
+ decision: parts.decision,
29
+ expectedVersion: parts.expectedVersion,
30
+ });
31
+ return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 64);
32
+ }
33
+ export function stableOrganizeOperationId(sessionId, decision) {
34
+ const h = crypto
35
+ .createHash("sha256")
36
+ .update(`organize:v1:${sessionId}:${decision.kind}:${decision.projectId ?? ""}:${decision.taskId ?? ""}:${decision.title ?? ""}`)
37
+ .digest("hex")
38
+ .slice(0, 32);
39
+ return `op_${h}`;
40
+ }
41
+ /**
42
+ * Build a pending decision from server candidates. Soft evidence ranks on the
43
+ * server; the CLI never auto-binds from this list.
44
+ */
45
+ export function decisionFromCandidates(sessionId, response) {
46
+ const options = response.candidates.map((c) => ({
47
+ optionId: c.candidateId,
48
+ label: c.taskId
49
+ ? `${c.projectName} / ${c.title}`
50
+ : c.projectName || c.title || c.projectId,
51
+ kind: "bind_existing",
52
+ projectId: c.projectId,
53
+ ...(c.taskId ? { taskId: c.taskId } : {}),
54
+ }));
55
+ options.push({
56
+ optionId: "opt_create_project",
57
+ label: "Create new project",
58
+ kind: "create_project",
59
+ });
60
+ options.push({
61
+ optionId: "opt_untracked",
62
+ label: "Leave untracked (local only)",
63
+ kind: "untracked",
64
+ });
65
+ return {
66
+ decisionId: stableDecisionId(sessionId, response.decisionExpiresAt),
67
+ options,
68
+ expiresAt: response.decisionExpiresAt,
69
+ askAfter: true,
70
+ };
71
+ }
72
+ /**
73
+ * Evaluate work-signal threshold and, when crossed for the first time, attach
74
+ * a one-time decision (optionally from server candidates). Never invents a
75
+ * project. If a decision already exists, it is left untouched (ask-once).
76
+ */
77
+ export async function prepareOrganizeDecision(input) {
78
+ const nowFn = input.now ?? (() => new Date());
79
+ const state = readSessionState(input.sessionId, input.root);
80
+ if (!state) {
81
+ return {
82
+ sessionId: input.sessionId,
83
+ contextStatus: "unresolved",
84
+ pending: false,
85
+ decision: null,
86
+ reason: "no_state",
87
+ };
88
+ }
89
+ if (state.contextStatus === "bound") {
90
+ return {
91
+ sessionId: input.sessionId,
92
+ contextStatus: state.contextStatus,
93
+ pending: false,
94
+ decision: state.decision ?? null,
95
+ reason: "already_bound",
96
+ };
97
+ }
98
+ if (state.contextStatus === "untracked") {
99
+ return {
100
+ sessionId: input.sessionId,
101
+ contextStatus: state.contextStatus,
102
+ pending: false,
103
+ decision: null,
104
+ reason: "untracked",
105
+ };
106
+ }
107
+ if (state.contextStatus === "needs_company" ||
108
+ state.contextStatus === "company_conflict") {
109
+ return {
110
+ sessionId: input.sessionId,
111
+ contextStatus: state.contextStatus,
112
+ pending: Boolean(state.decision),
113
+ decision: state.decision ?? null,
114
+ reason: state.contextStatus,
115
+ };
116
+ }
117
+ // Ask-once: an existing decision is never replaced or re-armed.
118
+ if (state.decision?.decisionId) {
119
+ return {
120
+ sessionId: input.sessionId,
121
+ contextStatus: state.contextStatus,
122
+ pending: true,
123
+ decision: state.decision,
124
+ reason: state.decision.askAfter ? "pending_ask" : "ask_already_settled",
125
+ };
126
+ }
127
+ const ready = shouldAskAfter({
128
+ toolWrites: state.toolWrites,
129
+ startedAt: state.startedAt,
130
+ hasTurn: state.hasTurn,
131
+ now: nowFn(),
132
+ });
133
+ if (!ready) {
134
+ return {
135
+ sessionId: input.sessionId,
136
+ contextStatus: state.contextStatus,
137
+ pending: false,
138
+ decision: null,
139
+ reason: "below_work_signal_threshold",
140
+ };
141
+ }
142
+ if (!state.companyUid || input.offline || !input.fetchCandidates) {
143
+ // Threshold crossed but no network — record a local placeholder so we
144
+ // do not invent a project; askAfter stays false until candidates arrive.
145
+ return {
146
+ sessionId: input.sessionId,
147
+ contextStatus: state.contextStatus,
148
+ pending: false,
149
+ decision: null,
150
+ reason: "candidates_unavailable",
151
+ };
152
+ }
153
+ const response = await input.fetchCandidates({
154
+ sessionId: input.sessionId,
155
+ companyUid: state.companyUid,
156
+ });
157
+ const decision = decisionFromCandidates(input.sessionId, response);
158
+ const updatedAt = nowFn().toISOString();
159
+ const next = mergeLocalSessionFields({
160
+ ...state,
161
+ decision,
162
+ contextStatus: state.contextStatus === "needs_task" ? "needs_task" : "needs_project",
163
+ updatedAt,
164
+ }, state);
165
+ writeSessionState(next, input.root);
166
+ return {
167
+ sessionId: input.sessionId,
168
+ contextStatus: next.contextStatus,
169
+ pending: true,
170
+ decision,
171
+ reason: "ask_prepared",
172
+ };
173
+ }
174
+ export function formatOrganizeList(result) {
175
+ const lines = [
176
+ `session ${result.sessionId} status=${result.contextStatus}` +
177
+ (result.reason ? ` (${result.reason})` : ""),
178
+ ];
179
+ if (!result.decision) {
180
+ lines.push("No pending decision.");
181
+ return lines;
182
+ }
183
+ lines.push(`decisionId=${result.decision.decisionId}`);
184
+ if (result.decision.expiresAt) {
185
+ lines.push(`expiresAt=${result.decision.expiresAt}`);
186
+ }
187
+ if (result.decision.correctionPath) {
188
+ lines.push(`correctionPath=${result.decision.correctionPath}`);
189
+ }
190
+ lines.push(`askAfter=${result.decision.askAfter === true}`);
191
+ for (const opt of result.decision.options) {
192
+ const bits = [` ${opt.optionId}`];
193
+ if (opt.label)
194
+ bits.push(opt.label);
195
+ if (opt.kind)
196
+ bits.push(`kind=${opt.kind}`);
197
+ if (opt.projectId)
198
+ bits.push(`project=${opt.projectId}`);
199
+ if (opt.taskId)
200
+ bits.push(`task=${opt.taskId}`);
201
+ lines.push(bits.join(" "));
202
+ }
203
+ return lines;
204
+ }
205
+ /**
206
+ * Mark that the ask was cancelled / timed out / surface incapable.
207
+ * Leaves the session unresolved and visible; clears askAfter so the CLI
208
+ * never repeats the ask; never invents a project.
209
+ */
210
+ export function settleOrganizeAskWithoutBind(input) {
211
+ const state = readSessionState(input.sessionId, input.root);
212
+ if (!state)
213
+ return null;
214
+ if (state.contextStatus === "bound" || state.contextStatus === "untracked") {
215
+ return state;
216
+ }
217
+ const now = (input.now ?? (() => new Date()))().toISOString();
218
+ const decision = state.decision
219
+ ? {
220
+ ...state.decision,
221
+ askAfter: false,
222
+ }
223
+ : {
224
+ decisionId: `settled_${input.sessionId}`,
225
+ options: [],
226
+ askAfter: false,
227
+ };
228
+ const next = mergeLocalSessionFields({
229
+ ...state,
230
+ contextStatus: state.contextStatus === "needs_company" ||
231
+ state.contextStatus === "company_conflict"
232
+ ? state.contextStatus
233
+ : "unresolved",
234
+ decision,
235
+ // Drop any speculative project/task — never invent.
236
+ projectId: undefined,
237
+ taskId: undefined,
238
+ updatedAt: now,
239
+ }, state);
240
+ // Explicitly clear project/task on disk projection.
241
+ const written = {
242
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
243
+ sessionId: next.sessionId,
244
+ contextStatus: next.contextStatus,
245
+ updatedAt: now,
246
+ decision,
247
+ };
248
+ if (next.companyUid)
249
+ written.companyUid = next.companyUid;
250
+ if (next.companySlug)
251
+ written.companySlug = next.companySlug;
252
+ if (typeof next.toolWrites === "number")
253
+ written.toolWrites = next.toolWrites;
254
+ if (next.startedAt)
255
+ written.startedAt = next.startedAt;
256
+ if (next.hasTurn)
257
+ written.hasTurn = true;
258
+ if (next.source)
259
+ written.source = next.source;
260
+ writeSessionState(written, input.root);
261
+ return written;
262
+ }
263
+ /**
264
+ * Submit an organize decision through US-005B. Success only from receipt.
265
+ */
266
+ export async function submitOrganizeDecision(input) {
267
+ const state = readSessionState(input.sessionId, input.root);
268
+ if (!state) {
269
+ return {
270
+ ok: false,
271
+ errorCode: "NO_STATE",
272
+ message: `No work-context state for session ${input.sessionId}`,
273
+ };
274
+ }
275
+ if (input.untracked) {
276
+ const now = (input.now ?? (() => new Date()))().toISOString();
277
+ writeSessionState({
278
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
279
+ sessionId: input.sessionId,
280
+ contextStatus: "untracked",
281
+ untrackedReceipt: { origin: "user", at: now, noBackfill: true },
282
+ updatedAt: now,
283
+ ...(typeof state.toolWrites === "number"
284
+ ? { toolWrites: state.toolWrites }
285
+ : {}),
286
+ ...(state.startedAt ? { startedAt: state.startedAt } : {}),
287
+ }, input.root);
288
+ return { ok: true, localUntracked: true };
289
+ }
290
+ let decision;
291
+ let decisionExpiresAt;
292
+ if (input.createProjectTitle?.trim()) {
293
+ decision = {
294
+ kind: "create_project",
295
+ title: input.createProjectTitle.trim(),
296
+ };
297
+ }
298
+ else if (input.createTaskTitle?.trim()) {
299
+ const projectId = state.projectId ||
300
+ state.decision?.options.find((o) => o.projectId)?.projectId;
301
+ if (!projectId) {
302
+ return {
303
+ ok: false,
304
+ errorCode: "PROJECT_REQUIRED",
305
+ message: "create-task requires an existing projectId on the session",
306
+ };
307
+ }
308
+ decision = {
309
+ kind: "create_task",
310
+ projectId,
311
+ title: input.createTaskTitle.trim(),
312
+ };
313
+ }
314
+ else if (input.decisionId && input.optionId) {
315
+ const pending = state.decision;
316
+ if (!pending || pending.decisionId !== input.decisionId) {
317
+ return {
318
+ ok: false,
319
+ errorCode: "DECISION_MISMATCH",
320
+ message: "decisionId does not match the pending local decision",
321
+ };
322
+ }
323
+ decisionExpiresAt = pending.expiresAt;
324
+ const opt = pending.options.find((o) => o.optionId === input.optionId);
325
+ if (!opt) {
326
+ return {
327
+ ok: false,
328
+ errorCode: "OPTION_NOT_FOUND",
329
+ message: `Unknown optionId ${input.optionId}`,
330
+ };
331
+ }
332
+ if (opt.kind === "untracked") {
333
+ return submitOrganizeDecision({
334
+ ...input,
335
+ untracked: true,
336
+ decisionId: undefined,
337
+ optionId: undefined,
338
+ });
339
+ }
340
+ if (opt.kind === "create_project") {
341
+ return {
342
+ ok: false,
343
+ errorCode: "TITLE_REQUIRED",
344
+ message: "Use --create-project <title> to create a project (option alone has no title)",
345
+ };
346
+ }
347
+ if (opt.kind === "create_task") {
348
+ return {
349
+ ok: false,
350
+ errorCode: "TITLE_REQUIRED",
351
+ message: "Use --create-task <title> to create a task (option alone has no title)",
352
+ };
353
+ }
354
+ if (!opt.projectId) {
355
+ return {
356
+ ok: false,
357
+ errorCode: "OPTION_INCOMPLETE",
358
+ message: "Selected option has no projectId",
359
+ };
360
+ }
361
+ decision = {
362
+ kind: "bind_existing",
363
+ projectId: opt.projectId,
364
+ ...(opt.taskId ? { taskId: opt.taskId } : {}),
365
+ };
366
+ }
367
+ else {
368
+ return {
369
+ ok: false,
370
+ errorCode: "INVALID_ARGS",
371
+ message: "Provide --decision + --option, --create-project, --create-task, or --untracked",
372
+ };
373
+ }
374
+ if (decision === undefined) {
375
+ return { ok: false, errorCode: "INVALID_ARGS", message: "No decision" };
376
+ }
377
+ if (!state.companyUid) {
378
+ return {
379
+ ok: false,
380
+ errorCode: "COMPANY_REQUIRED",
381
+ message: "Session has no verified companyUid; cannot organize on server",
382
+ };
383
+ }
384
+ if (!input.postOrganize) {
385
+ return {
386
+ ok: false,
387
+ errorCode: "OFFLINE",
388
+ message: "No organize transport; server receipt required for success",
389
+ };
390
+ }
391
+ // Stale decision → typed error (caller may refresh candidates).
392
+ if (decisionExpiresAt) {
393
+ const exp = Date.parse(decisionExpiresAt);
394
+ if (Number.isFinite(exp) && Date.now() >= exp) {
395
+ return {
396
+ ok: false,
397
+ errorCode: "DECISION_EXPIRED",
398
+ message: "organization decision has expired; refresh candidates",
399
+ };
400
+ }
401
+ }
402
+ const expectedVersion = 0;
403
+ const operationId = stableOrganizeOperationId(input.sessionId, decision);
404
+ const digest = digestOrganizePayload({
405
+ sessionId: input.sessionId,
406
+ companyUid: state.companyUid,
407
+ decision,
408
+ expectedVersion,
409
+ });
410
+ let receipt;
411
+ try {
412
+ receipt = await input.postOrganize({
413
+ sessionId: input.sessionId,
414
+ companyUid: state.companyUid,
415
+ operationId,
416
+ digest,
417
+ decision,
418
+ expectedVersion,
419
+ ...(decisionExpiresAt ? { decisionExpiresAt } : {}),
420
+ });
421
+ }
422
+ catch (err) {
423
+ const message = err instanceof Error ? err.message : String(err);
424
+ if (/DECISION_EXPIRED|409/.test(message)) {
425
+ return { ok: false, errorCode: "DECISION_EXPIRED", message };
426
+ }
427
+ if (/WORK_TARGET_UNAVAILABLE|404/.test(message)) {
428
+ return { ok: false, errorCode: "WORK_TARGET_UNAVAILABLE", message };
429
+ }
430
+ return { ok: false, errorCode: "ORGANIZE_FAILED", message };
431
+ }
432
+ // Success only from receipt.
433
+ if (!receipt?.receipt?.operationId ||
434
+ receipt.contextStatus !== "bound" ||
435
+ !receipt.projectId ||
436
+ !receipt.bindingEpisodeId) {
437
+ return {
438
+ ok: false,
439
+ errorCode: "RECEIPT_INCOMPLETE",
440
+ message: "Server response missing authoritative binding receipt",
441
+ };
442
+ }
443
+ const now = (input.now ?? (() => new Date()))().toISOString();
444
+ try {
445
+ writeSessionState(mergeLocalSessionFields({
446
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
447
+ sessionId: input.sessionId,
448
+ companyUid: receipt.companyUid || state.companyUid,
449
+ companySlug: state.companySlug,
450
+ contextStatus: "bound",
451
+ projectId: receipt.projectId,
452
+ ...(receipt.taskId ? { taskId: receipt.taskId } : {}),
453
+ bindingEpisodeId: receipt.bindingEpisodeId,
454
+ updatedAt: now,
455
+ // Clear ask — decision resolved.
456
+ decision: undefined,
457
+ }, state), input.root);
458
+ }
459
+ catch (err) {
460
+ if (err instanceof NotTrackingError) {
461
+ return {
462
+ ok: false,
463
+ errorCode: "NOT_TRACKING",
464
+ message: err.message,
465
+ };
466
+ }
467
+ throw err;
468
+ }
469
+ return { ok: true, receipt };
470
+ }
471
+ //# sourceMappingURL=organize.js.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Metadata-only durable outbox under ~/.hq/work-context/outbox/<operationId>.json.
3
+ * Written before any network I/O. FIFO per session; bounded-parallel across sessions.
4
+ */
5
+ import type { DeliveryState } from "./contract.js";
6
+ import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
7
+ /** Fields allowed on a durable outbox operation (privacy allowlist). */
8
+ export declare const OUTBOX_ALLOWLIST: readonly ["contractVersion", "operationId", "clientOperationId", "sessionId", "companyUid", "companySlug", "projectId", "taskId", "kind", "digest", "delivery", "createdAt", "updatedAt", "attemptCount", "lastErrorCode", "receiptId", "destinationCompanyUid", "destinationCompanySlug"];
9
+ export type OutboxKind = "register" | "reconcile" | "migrate";
10
+ export interface OutboxOperation {
11
+ contractVersion: typeof WORK_CONTEXT_CONTRACT_VERSION;
12
+ operationId: string;
13
+ clientOperationId: string;
14
+ sessionId: string;
15
+ companyUid?: string;
16
+ companySlug?: string;
17
+ projectId?: string;
18
+ taskId?: string;
19
+ kind: OutboxKind;
20
+ digest: string;
21
+ delivery: DeliveryState;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ attemptCount: number;
25
+ lastErrorCode?: string;
26
+ receiptId?: string;
27
+ /** Destination company for kind=migrate (source is companyUid). */
28
+ destinationCompanyUid?: string;
29
+ destinationCompanySlug?: string;
30
+ }
31
+ export interface OutboxEnqueueInput {
32
+ clientOperationId: string;
33
+ sessionId: string;
34
+ companyUid?: string;
35
+ companySlug?: string;
36
+ projectId?: string;
37
+ taskId?: string;
38
+ kind?: OutboxKind;
39
+ destinationCompanyUid?: string;
40
+ destinationCompanySlug?: string;
41
+ now?: () => Date;
42
+ }
43
+ export declare function stableOperationId(clientOperationId: string, sessionId: string): string;
44
+ export declare function digestOperation(parts: {
45
+ sessionId: string;
46
+ clientOperationId: string;
47
+ companyUid?: string;
48
+ companySlug?: string;
49
+ projectId?: string;
50
+ taskId?: string;
51
+ kind: OutboxKind;
52
+ destinationCompanyUid?: string;
53
+ destinationCompanySlug?: string;
54
+ }): string;
55
+ /**
56
+ * Atomically enqueue (or idempotently return) an outbox operation.
57
+ * Disk/permission/lock failure → NotTrackingError (no network-only send).
58
+ */
59
+ export declare function enqueueOutbox(input: OutboxEnqueueInput, root: string): OutboxOperation;
60
+ export declare function readOutboxOperation(operationId: string, root: string): OutboxOperation | null;
61
+ export declare function updateOutboxOperation(op: OutboxOperation, root: string): void;
62
+ export declare function markOutboxAcked(operationId: string, root: string, receiptId: string, now?: () => Date): OutboxOperation | null;
63
+ export declare function markOutboxQueued(operationId: string, root: string, errorCode: string, now?: () => Date): OutboxOperation | null;
64
+ export declare function markOutboxQuarantined(operationId: string, root: string, errorCode: string, now?: () => Date): OutboxOperation | null;
65
+ export declare function listOutboxOperations(root: string): OutboxOperation[];
66
+ export interface OutboxStats {
67
+ depth: number;
68
+ quarantined: number;
69
+ queued: number;
70
+ acked: number;
71
+ }
72
+ export declare function outboxStats(root: string): OutboxStats;
73
+ export type DeliverFn = (op: OutboxOperation) => Promise<{
74
+ ok: true;
75
+ receiptId: string;
76
+ } | {
77
+ ok: false;
78
+ retryable: boolean;
79
+ code: string;
80
+ }>;
81
+ /**
82
+ * Replay queued (and recover lost-receipt) operations.
83
+ * FIFO per sessionId; bounded parallelism across sessions.
84
+ */
85
+ export declare function replayOutbox(root: string, deliver: DeliverFn, opts?: {
86
+ parallel?: number;
87
+ now?: () => Date;
88
+ }): Promise<{
89
+ delivered: number;
90
+ queued: number;
91
+ quarantined: number;
92
+ }>;
93
+ /** Remove acked ops older than retention (optional GC; not required by AC). */
94
+ export declare function removeAckedOutbox(root: string, olderThanMs: number, now?: number): number;
95
+ export declare function outboxFileExists(operationId: string, root: string): boolean;
96
+ export declare function quarantineCorruptOutboxFile(filePath: string, root: string): void;
97
+ //# sourceMappingURL=outbox.d.ts.map