@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,357 @@
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 * as crypto from "node:crypto";
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
9
+ import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
10
+ import { NotTrackingError } from "./errors.js";
11
+ import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
12
+ /** Fields allowed on a durable outbox operation (privacy allowlist). */
13
+ export const OUTBOX_ALLOWLIST = [
14
+ "contractVersion",
15
+ "operationId",
16
+ "clientOperationId",
17
+ "sessionId",
18
+ "companyUid",
19
+ "companySlug",
20
+ "projectId",
21
+ "taskId",
22
+ "kind",
23
+ "digest",
24
+ "delivery",
25
+ "createdAt",
26
+ "updatedAt",
27
+ "attemptCount",
28
+ "lastErrorCode",
29
+ "receiptId",
30
+ "destinationCompanyUid",
31
+ "destinationCompanySlug",
32
+ ];
33
+ const DEFAULT_PARALLEL = 4;
34
+ export function stableOperationId(clientOperationId, sessionId) {
35
+ const h = crypto
36
+ .createHash("sha256")
37
+ .update(`v1:${sessionId}:${clientOperationId}`)
38
+ .digest("hex")
39
+ .slice(0, 32);
40
+ return `op_${h}`;
41
+ }
42
+ export function digestOperation(parts) {
43
+ const canonical = JSON.stringify({
44
+ v: 1,
45
+ sessionId: parts.sessionId,
46
+ clientOperationId: parts.clientOperationId,
47
+ companyUid: parts.companyUid ?? null,
48
+ companySlug: parts.companySlug ?? null,
49
+ projectId: parts.projectId ?? null,
50
+ taskId: parts.taskId ?? null,
51
+ kind: parts.kind,
52
+ destinationCompanyUid: parts.destinationCompanyUid ?? null,
53
+ destinationCompanySlug: parts.destinationCompanySlug ?? null,
54
+ });
55
+ return crypto.createHash("sha256").update(canonical).digest("hex");
56
+ }
57
+ function projectOutbox(op) {
58
+ const out = {
59
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
60
+ operationId: op.operationId,
61
+ clientOperationId: op.clientOperationId,
62
+ sessionId: op.sessionId,
63
+ kind: op.kind,
64
+ digest: op.digest,
65
+ delivery: op.delivery,
66
+ createdAt: op.createdAt,
67
+ updatedAt: op.updatedAt,
68
+ attemptCount: op.attemptCount,
69
+ };
70
+ if (op.companyUid)
71
+ out.companyUid = op.companyUid;
72
+ if (op.companySlug)
73
+ out.companySlug = op.companySlug;
74
+ if (op.projectId)
75
+ out.projectId = op.projectId;
76
+ if (op.taskId)
77
+ out.taskId = op.taskId;
78
+ if (op.lastErrorCode)
79
+ out.lastErrorCode = op.lastErrorCode;
80
+ if (op.receiptId)
81
+ out.receiptId = op.receiptId;
82
+ if (op.destinationCompanyUid) {
83
+ out.destinationCompanyUid = op.destinationCompanyUid;
84
+ }
85
+ if (op.destinationCompanySlug) {
86
+ out.destinationCompanySlug = op.destinationCompanySlug;
87
+ }
88
+ return out;
89
+ }
90
+ /**
91
+ * Atomically enqueue (or idempotently return) an outbox operation.
92
+ * Disk/permission/lock failure → NotTrackingError (no network-only send).
93
+ */
94
+ export function enqueueOutbox(input, root) {
95
+ if (!isSafeWorkContextSegment(input.sessionId)) {
96
+ throw new NotTrackingError(`Unsafe session id: ${input.sessionId}`);
97
+ }
98
+ if (!isSafeWorkContextSegment(input.clientOperationId) &&
99
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(input.clientOperationId)) {
100
+ throw new NotTrackingError(`Unsafe clientOperationId: ${input.clientOperationId}`);
101
+ }
102
+ const kind = input.kind ?? "register";
103
+ const operationId = stableOperationId(input.clientOperationId, input.sessionId);
104
+ if (!isSafeWorkContextSegment(operationId)) {
105
+ throw new NotTrackingError(`Unsafe operationId: ${operationId}`);
106
+ }
107
+ const digest = digestOperation({
108
+ sessionId: input.sessionId,
109
+ clientOperationId: input.clientOperationId,
110
+ companyUid: input.companyUid,
111
+ companySlug: input.companySlug,
112
+ projectId: input.projectId,
113
+ taskId: input.taskId,
114
+ kind,
115
+ destinationCompanyUid: input.destinationCompanyUid,
116
+ destinationCompanySlug: input.destinationCompanySlug,
117
+ });
118
+ const now = (input.now ?? (() => new Date()))().toISOString();
119
+ const existing = readOutboxOperation(operationId, root);
120
+ if (existing) {
121
+ if (existing.digest !== digest) {
122
+ throw new NotTrackingError(`Idempotency conflict for ${operationId}`, "IdempotencyConflictError");
123
+ }
124
+ return existing;
125
+ }
126
+ const op = projectOutbox({
127
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
128
+ operationId,
129
+ clientOperationId: input.clientOperationId,
130
+ sessionId: input.sessionId,
131
+ companyUid: input.companyUid,
132
+ companySlug: input.companySlug,
133
+ projectId: input.projectId,
134
+ taskId: input.taskId,
135
+ kind,
136
+ digest,
137
+ delivery: "queued",
138
+ createdAt: now,
139
+ updatedAt: now,
140
+ attemptCount: 0,
141
+ destinationCompanyUid: input.destinationCompanyUid,
142
+ destinationCompanySlug: input.destinationCompanySlug,
143
+ });
144
+ try {
145
+ ensureOwnerDir(workContextOutboxDir(root));
146
+ // Exclusive publish: linkSync fails with EEXIST if dest appeared (rename
147
+ // would clobber a conflicting digest from a concurrent writer).
148
+ const dest = workContextOutboxPath(operationId, root);
149
+ const tmp = `${dest}.tmp.${process.pid}.${Date.now()}`;
150
+ fs.writeFileSync(tmp, `${JSON.stringify(op, null, 2)}\n`, { flag: "wx", mode: 0o600 });
151
+ try {
152
+ fs.linkSync(tmp, dest);
153
+ try {
154
+ fs.unlinkSync(tmp);
155
+ }
156
+ catch {
157
+ /* ignore */
158
+ }
159
+ }
160
+ catch (linkErr) {
161
+ try {
162
+ fs.unlinkSync(tmp);
163
+ }
164
+ catch {
165
+ /* ignore */
166
+ }
167
+ const code = linkErr?.code;
168
+ if (code === "EEXIST") {
169
+ const raced = readOutboxOperation(operationId, root);
170
+ if (raced) {
171
+ if (raced.digest !== digest) {
172
+ throw new NotTrackingError(`Idempotency conflict for ${operationId}`, "IdempotencyConflictError");
173
+ }
174
+ return raced;
175
+ }
176
+ }
177
+ throw linkErr;
178
+ }
179
+ try {
180
+ fs.chmodSync(dest, 0o600);
181
+ }
182
+ catch {
183
+ /* ignore */
184
+ }
185
+ return op;
186
+ }
187
+ catch (err) {
188
+ if (err instanceof NotTrackingError)
189
+ throw err;
190
+ throw new NotTrackingError(`Outbox persistence failed: ${err instanceof Error ? err.message : String(err)}`, "OutboxPersistenceError");
191
+ }
192
+ }
193
+ export function readOutboxOperation(operationId, root) {
194
+ if (!isSafeWorkContextSegment(operationId))
195
+ return null;
196
+ const filePath = workContextOutboxPath(operationId, root);
197
+ if (!fs.existsSync(filePath))
198
+ return null;
199
+ try {
200
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ export function updateOutboxOperation(op, root) {
207
+ atomicWriteJson(workContextOutboxPath(op.operationId, root), projectOutbox(op));
208
+ }
209
+ export function markOutboxAcked(operationId, root, receiptId, now = () => new Date()) {
210
+ const op = readOutboxOperation(operationId, root);
211
+ if (!op)
212
+ return null;
213
+ op.delivery = "acked";
214
+ op.receiptId = receiptId;
215
+ op.updatedAt = now().toISOString();
216
+ updateOutboxOperation(op, root);
217
+ return op;
218
+ }
219
+ export function markOutboxQueued(operationId, root, errorCode, now = () => new Date()) {
220
+ const op = readOutboxOperation(operationId, root);
221
+ if (!op)
222
+ return null;
223
+ op.delivery = "queued";
224
+ op.lastErrorCode = errorCode;
225
+ op.attemptCount += 1;
226
+ op.updatedAt = now().toISOString();
227
+ updateOutboxOperation(op, root);
228
+ return op;
229
+ }
230
+ export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date()) {
231
+ const op = readOutboxOperation(operationId, root);
232
+ if (!op)
233
+ return null;
234
+ op.delivery = "quarantined";
235
+ op.lastErrorCode = errorCode;
236
+ op.attemptCount += 1;
237
+ op.updatedAt = now().toISOString();
238
+ updateOutboxOperation(op, root);
239
+ return op;
240
+ }
241
+ export function listOutboxOperations(root) {
242
+ const dir = workContextOutboxDir(root);
243
+ if (!fs.existsSync(dir))
244
+ return [];
245
+ const ops = [];
246
+ for (const name of fs.readdirSync(dir)) {
247
+ if (!name.endsWith(".json") || name.startsWith("."))
248
+ continue;
249
+ const op = readOutboxOperation(name.replace(/\.json$/, ""), root);
250
+ if (op)
251
+ ops.push(op);
252
+ }
253
+ return ops.sort((a, b) => {
254
+ const byTime = a.createdAt.localeCompare(b.createdAt);
255
+ if (byTime !== 0)
256
+ return byTime;
257
+ return a.clientOperationId.localeCompare(b.clientOperationId);
258
+ });
259
+ }
260
+ export function outboxStats(root) {
261
+ const ops = listOutboxOperations(root);
262
+ return {
263
+ depth: ops.filter((o) => o.delivery === "queued").length,
264
+ quarantined: ops.filter((o) => o.delivery === "quarantined").length,
265
+ queued: ops.filter((o) => o.delivery === "queued").length,
266
+ acked: ops.filter((o) => o.delivery === "acked").length,
267
+ };
268
+ }
269
+ /**
270
+ * Replay queued (and recover lost-receipt) operations.
271
+ * FIFO per sessionId; bounded parallelism across sessions.
272
+ */
273
+ export async function replayOutbox(root, deliver, opts = {}) {
274
+ const parallel = opts.parallel ?? DEFAULT_PARALLEL;
275
+ const now = opts.now ?? (() => new Date());
276
+ const pending = listOutboxOperations(root).filter((o) => o.delivery === "queued" || (o.delivery === "acked" && !o.receiptId));
277
+ // Group by session, preserve FIFO within each.
278
+ const bySession = new Map();
279
+ for (const op of pending) {
280
+ const list = bySession.get(op.sessionId) ?? [];
281
+ list.push(op);
282
+ bySession.set(op.sessionId, list);
283
+ }
284
+ let delivered = 0;
285
+ let queued = 0;
286
+ let quarantined = 0;
287
+ const sessions = [...bySession.keys()];
288
+ let cursor = 0;
289
+ async function runSession(sessionId) {
290
+ const ops = bySession.get(sessionId) ?? [];
291
+ for (const op of ops) {
292
+ // Lost-receipt recovery: if already acked with receipt, skip; if acked
293
+ // without receiptId, re-deliver via same operationId (idempotent server).
294
+ const result = await deliver(op);
295
+ if (result.ok) {
296
+ markOutboxAcked(op.operationId, root, result.receiptId, now);
297
+ delivered += 1;
298
+ }
299
+ else if (result.retryable) {
300
+ markOutboxQueued(op.operationId, root, result.code, now);
301
+ queued += 1;
302
+ // Stop this session's FIFO on transient failure.
303
+ break;
304
+ }
305
+ else {
306
+ markOutboxQuarantined(op.operationId, root, result.code, now);
307
+ quarantined += 1;
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ async function worker() {
313
+ while (cursor < sessions.length) {
314
+ const idx = cursor;
315
+ cursor += 1;
316
+ const sid = sessions[idx];
317
+ if (sid)
318
+ await runSession(sid);
319
+ }
320
+ }
321
+ const workers = Array.from({ length: Math.min(parallel, sessions.length) }, () => worker());
322
+ await Promise.all(workers);
323
+ return { delivered, queued, quarantined };
324
+ }
325
+ /** Remove acked ops older than retention (optional GC; not required by AC). */
326
+ export function removeAckedOutbox(root, olderThanMs, now = Date.now()) {
327
+ let removed = 0;
328
+ for (const op of listOutboxOperations(root)) {
329
+ if (op.delivery !== "acked" || !op.receiptId)
330
+ continue;
331
+ const age = now - Date.parse(op.updatedAt);
332
+ if (Number.isFinite(age) && age > olderThanMs) {
333
+ try {
334
+ fs.unlinkSync(workContextOutboxPath(op.operationId, root));
335
+ removed += 1;
336
+ }
337
+ catch {
338
+ /* ignore */
339
+ }
340
+ }
341
+ }
342
+ return removed;
343
+ }
344
+ export function outboxFileExists(operationId, root) {
345
+ return fs.existsSync(workContextOutboxPath(operationId, root));
346
+ }
347
+ export function quarantineCorruptOutboxFile(filePath, root) {
348
+ const base = path.basename(filePath);
349
+ const dest = path.join(workContextOutboxDir(root), `.quarantine.${base}`);
350
+ try {
351
+ fs.renameSync(filePath, dest);
352
+ }
353
+ catch {
354
+ /* ignore */
355
+ }
356
+ }
357
+ //# sourceMappingURL=outbox.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Local Work Context paths under ~/.hq/work-context/.
3
+ * All roots are injectable for tests (never touch the real HOME in unit tests).
4
+ */
5
+ export declare const WORK_CONTEXT_DIRNAME = "work-context";
6
+ export declare function workContextRoot(home?: string, env?: NodeJS.ProcessEnv): string;
7
+ export declare function workContextConfigPath(root?: string): string;
8
+ export declare function workContextSessionsDir(root?: string): string;
9
+ export declare function workContextSessionPath(sessionId: string, root?: string): string;
10
+ export declare function workContextOutboxDir(root?: string): string;
11
+ export declare function workContextOutboxPath(operationId: string, root?: string): string;
12
+ /** Safe path segment for session/operation ids (no traversal). */
13
+ export declare function isSafeWorkContextSegment(value: string): boolean;
14
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Local Work Context paths under ~/.hq/work-context/.
3
+ * All roots are injectable 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_CONTEXT_DIRNAME = "work-context";
8
+ export function workContextRoot(home = os.homedir(), env = process.env) {
9
+ const override = env.HQ_WORK_CONTEXT_ROOT?.trim();
10
+ if (override)
11
+ return override;
12
+ return path.join(home, ".hq", WORK_CONTEXT_DIRNAME);
13
+ }
14
+ export function workContextConfigPath(root) {
15
+ return path.join(root ?? workContextRoot(), "config.json");
16
+ }
17
+ export function workContextSessionsDir(root) {
18
+ return path.join(root ?? workContextRoot(), "sessions");
19
+ }
20
+ export function workContextSessionPath(sessionId, root) {
21
+ return path.join(workContextSessionsDir(root), `${sessionId}.json`);
22
+ }
23
+ export function workContextOutboxDir(root) {
24
+ return path.join(root ?? workContextRoot(), "outbox");
25
+ }
26
+ export function workContextOutboxPath(operationId, root) {
27
+ return path.join(workContextOutboxDir(root), `${operationId}.json`);
28
+ }
29
+ /** Safe path segment for session/operation ids (no traversal). */
30
+ export function isSafeWorkContextSegment(value) {
31
+ const trimmed = value.trim();
32
+ return Boolean(trimmed &&
33
+ trimmed.length <= 128 &&
34
+ !trimmed.includes("..") &&
35
+ !trimmed.includes("/") &&
36
+ !trimmed.includes("\\") &&
37
+ /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed));
38
+ }
39
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Project/task resolution for work-context (US-007B).
3
+ *
4
+ * Order:
5
+ * 1. existing active binding (state bound / needs_task with projectId)
6
+ * 2. trusted explicit (HQ_SPAWN_PROJECT/HQ_SPAWN_TASK, observation, meta.yaml)
7
+ * 3. exactly one deterministic mapping (cwd under companies/{slug}/projects/{project}/
8
+ * or a registry resource that names exactly one project for the repo)
9
+ * Soft evidence never auto-binds — candidates are server-owned (GET .../candidates).
10
+ */
11
+ import { type TrustedExplicitContext } from "./contract.js";
12
+ import type { SessionStateFile } from "./state.js";
13
+ export type ProjectResolutionSource = "existing_binding" | "trusted_explicit" | "deterministic_cwd" | "deterministic_registry";
14
+ export interface ResolvedProjectTask {
15
+ projectId?: string;
16
+ taskId?: string;
17
+ source: ProjectResolutionSource;
18
+ /** True when project+task are both present (trusted or existing bound). */
19
+ complete: boolean;
20
+ }
21
+ export interface ProjectResolveInput {
22
+ sessionId: string;
23
+ env?: NodeJS.ProcessEnv;
24
+ trusted?: TrustedExplicitContext & {
25
+ companySlug?: string;
26
+ projectId?: string;
27
+ taskId?: string;
28
+ storyId?: string;
29
+ };
30
+ cwd?: string;
31
+ hqRoot?: string;
32
+ existingState?: SessionStateFile | null;
33
+ /** Injectable meta project/task (tests). */
34
+ metaProjectId?: string | null;
35
+ metaTaskId?: string | null;
36
+ /** Injectable registry project id (tests). */
37
+ registryProjectId?: string | null;
38
+ }
39
+ /**
40
+ * Read project / task from workspace/sessions/<sid>/meta.yaml (best-effort).
41
+ * Keys written by startwork, execute-task, run-project, plan, brainstorm:
42
+ * project | project_id | projectId
43
+ * task | task_id | taskId | story_id | storyId
44
+ */
45
+ export declare function readMetaProjectTask(sessionId: string, hqRoot: string | undefined): {
46
+ projectId?: string;
47
+ taskId?: string;
48
+ };
49
+ /**
50
+ * Look up a company registry resource that names exactly one project for the
51
+ * enclosing git remote. Scans companies/{slug}/registry/resources/*.yaml for
52
+ * type: repo entries whose repo_url matches and that carry a `project:` field.
53
+ * Zero or multiple distinct projects → null (never guess).
54
+ */
55
+ export declare function projectIdFromRepoRegistry(input: {
56
+ cwd?: string;
57
+ hqRoot?: string;
58
+ companySlug?: string;
59
+ }): string | null;
60
+ /**
61
+ * Resolve project and task. Never invents an adhoc target. Soft evidence is
62
+ * out of scope here (server candidates only).
63
+ */
64
+ export declare function resolveProjectTask(input: ProjectResolveInput): ResolvedProjectTask | null;
65
+ /** Work-signal thresholds for one-time clarification (US-007B). */
66
+ export declare const TOOL_WRITES_ASK_THRESHOLD = 3;
67
+ export declare const SESSION_AGE_ASK_MS: number;
68
+ /**
69
+ * Whether the session has crossed the work-signal threshold that unlocks a
70
+ * one-time clarification. Soft — never auto-binds.
71
+ */
72
+ export declare function shouldAskAfter(input: {
73
+ toolWrites?: number;
74
+ startedAt?: string;
75
+ hasTurn?: boolean;
76
+ now?: Date;
77
+ }): boolean;
78
+ //# sourceMappingURL=project.d.ts.map