@indigoai-us/hq-cli 5.312.1 → 5.312.2-beta.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.
- package/CHANGELOG.md +2 -5
- package/dist/command-catalog.generated.d.ts +0 -65
- package/dist/command-catalog.generated.js +0 -80
- package/dist/commands/cloud.js +1 -0
- package/dist/commands/company.js +0 -2
- package/dist/commands/core.js +0 -7
- package/dist/fast-core.js +3 -10
- package/dist/fast-scaffold.d.ts +0 -2
- package/dist/fast-scaffold.js +0 -11
- package/dist/lib/doctor/checks/work-context.js +1 -9
- package/dist/lib/mesh/client.js +5 -23
- package/dist/lib/mesh/live/daemon/doctor.d.ts +0 -2
- package/dist/lib/mesh/live/daemon/doctor.js +1 -11
- package/dist/lib/search-index/embed-lock.d.ts +0 -2
- package/dist/lib/search-index/embed-lock.js +1 -10
- package/dist/lib/work-context/contract.d.ts +0 -3
- package/dist/lib/work-context/contract.schema.json +1 -3
- package/dist/lib/work-context/outbox.d.ts +5 -14
- package/dist/lib/work-context/outbox.js +5 -48
- package/dist/lib/work-context/reconcile.js +5 -26
- package/dist/main.d.ts +0 -20
- package/dist/main.js +1 -8
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +0 -378
- package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +0 -141
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +0 -373
- package/assets/scaffold/core/scripts/register-project.sh +0 -262
- package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +0 -59
- package/dist/commands/company-rename-flag.d.ts +0 -4
- package/dist/commands/company-rename-flag.js +0 -7
- package/dist/commands/company-rename.d.ts +0 -4
- package/dist/commands/company-rename.js +0 -90
- package/dist/commands/core-eval-trigger-native.d.ts +0 -4
- package/dist/commands/core-eval-trigger-native.js +0 -12
- package/dist/commands/core-migrate-policy-triggers-native.d.ts +0 -21
- package/dist/commands/core-migrate-policy-triggers-native.js +0 -378
- package/dist/commands/core-native-hook-helper-commands.d.ts +0 -4
- package/dist/commands/core-native-hook-helper-commands.js +0 -56
- package/dist/commands/core-native-hook-helpers-flag.d.ts +0 -4
- package/dist/commands/core-native-hook-helpers-flag.js +0 -7
- package/dist/commands/core-register-project-native.d.ts +0 -30
- package/dist/commands/core-register-project-native.js +0 -347
- package/dist/commands/core-work-mesh-live-rebind-native.d.ts +0 -18
- package/dist/commands/core-work-mesh-live-rebind-native.js +0 -262
- package/dist/lib/mesh/denial-codes.d.ts +0 -5
- package/dist/lib/mesh/denial-codes.js +0 -14
- package/dist/utils/core-eval-trigger.d.ts +0 -4
- package/dist/utils/core-eval-trigger.js +0 -88
|
@@ -8,7 +8,6 @@ import * as path from "node:path";
|
|
|
8
8
|
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
9
9
|
import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
|
|
10
10
|
import { NotTrackingError } from "./errors.js";
|
|
11
|
-
import { isWorkSessionDenialCode } from "../mesh/denial-codes.js";
|
|
12
11
|
import { readSessionState, stripRegisterReceiptEpisodes, updateSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
|
|
13
12
|
import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
|
|
14
13
|
/** Fields allowed on a durable outbox operation (privacy allowlist). */
|
|
@@ -30,8 +29,6 @@ export const OUTBOX_ALLOWLIST = [
|
|
|
30
29
|
"updatedAt",
|
|
31
30
|
"attemptCount",
|
|
32
31
|
"lastErrorCode",
|
|
33
|
-
"lastErrorHttpStatus",
|
|
34
|
-
"lastServerErrorCode",
|
|
35
32
|
"receiptId",
|
|
36
33
|
"destinationCompanyUid",
|
|
37
34
|
"destinationCompanySlug",
|
|
@@ -104,11 +101,6 @@ function projectOutbox(op) {
|
|
|
104
101
|
out.companyConfidence = op.companyConfidence;
|
|
105
102
|
if (op.lastErrorCode)
|
|
106
103
|
out.lastErrorCode = op.lastErrorCode;
|
|
107
|
-
if (op.lastErrorHttpStatus !== undefined) {
|
|
108
|
-
out.lastErrorHttpStatus = op.lastErrorHttpStatus;
|
|
109
|
-
}
|
|
110
|
-
if (op.lastServerErrorCode)
|
|
111
|
-
out.lastServerErrorCode = op.lastServerErrorCode;
|
|
112
104
|
if (op.receiptId)
|
|
113
105
|
out.receiptId = op.receiptId;
|
|
114
106
|
if (op.destinationCompanyUid) {
|
|
@@ -345,13 +337,12 @@ export function repairAckedRegisterStates(root, sessionIds, now = () => new Date
|
|
|
345
337
|
}
|
|
346
338
|
return repaired;
|
|
347
339
|
}
|
|
348
|
-
export function markOutboxQueued(operationId, root, errorCode, now = () => new Date(), random = Math.random
|
|
340
|
+
export function markOutboxQueued(operationId, root, errorCode, now = () => new Date(), random = Math.random) {
|
|
349
341
|
const op = readOutboxOperation(operationId, root);
|
|
350
342
|
if (!op)
|
|
351
343
|
return null;
|
|
352
344
|
op.delivery = "queued";
|
|
353
345
|
op.lastErrorCode = errorCode;
|
|
354
|
-
applyOutboxErrorDetails(op, details);
|
|
355
346
|
op.attemptCount += 1;
|
|
356
347
|
const at = now();
|
|
357
348
|
op.updatedAt = at.toISOString();
|
|
@@ -360,40 +351,17 @@ export function markOutboxQueued(operationId, root, errorCode, now = () => new D
|
|
|
360
351
|
updateOutboxOperation(op, root);
|
|
361
352
|
return op;
|
|
362
353
|
}
|
|
363
|
-
export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date()
|
|
354
|
+
export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date()) {
|
|
364
355
|
const op = readOutboxOperation(operationId, root);
|
|
365
356
|
if (!op)
|
|
366
357
|
return null;
|
|
367
358
|
op.delivery = "quarantined";
|
|
368
359
|
op.lastErrorCode = errorCode;
|
|
369
|
-
applyOutboxErrorDetails(op, details);
|
|
370
360
|
op.attemptCount += 1;
|
|
371
361
|
op.updatedAt = now().toISOString();
|
|
372
362
|
updateOutboxOperation(op, root);
|
|
373
363
|
return op;
|
|
374
364
|
}
|
|
375
|
-
function applyOutboxErrorDetails(op, details) {
|
|
376
|
-
delete op.lastErrorHttpStatus;
|
|
377
|
-
delete op.lastServerErrorCode;
|
|
378
|
-
const safe = sanitizeOutboxErrorDetails(details);
|
|
379
|
-
if (safe.httpStatus !== undefined)
|
|
380
|
-
op.lastErrorHttpStatus = safe.httpStatus;
|
|
381
|
-
if (safe.serverCode !== undefined)
|
|
382
|
-
op.lastServerErrorCode = safe.serverCode;
|
|
383
|
-
}
|
|
384
|
-
export function sanitizeOutboxErrorDetails(details) {
|
|
385
|
-
const safe = {};
|
|
386
|
-
if (details?.httpStatus !== undefined &&
|
|
387
|
-
Number.isInteger(details.httpStatus) &&
|
|
388
|
-
details.httpStatus >= 100 &&
|
|
389
|
-
details.httpStatus <= 599) {
|
|
390
|
-
safe.httpStatus = details.httpStatus;
|
|
391
|
-
}
|
|
392
|
-
if (isWorkSessionDenialCode(details?.serverCode)) {
|
|
393
|
-
safe.serverCode = details.serverCode;
|
|
394
|
-
}
|
|
395
|
-
return safe;
|
|
396
|
-
}
|
|
397
365
|
/**
|
|
398
366
|
* Requeue quarantined outbox operations so the daemon's replay picks them up
|
|
399
367
|
* again. Quarantine is otherwise terminal: after the work-mesh-live gap 10 fix
|
|
@@ -421,8 +389,6 @@ export function requeueQuarantinedOutbox(root, opts = {}) {
|
|
|
421
389
|
op.delivery = "queued";
|
|
422
390
|
op.attemptCount = 0;
|
|
423
391
|
op.lastErrorCode = undefined;
|
|
424
|
-
op.lastErrorHttpStatus = undefined;
|
|
425
|
-
op.lastServerErrorCode = undefined;
|
|
426
392
|
op.nextAttemptAt = undefined;
|
|
427
393
|
op.updatedAt = now().toISOString();
|
|
428
394
|
updateOutboxOperation(op, root);
|
|
@@ -503,15 +469,6 @@ export function outboxStats(root) {
|
|
|
503
469
|
queued: ops.filter((o) => o.delivery === "queued").length,
|
|
504
470
|
acked: ops.filter((o) => o.delivery === "acked").length,
|
|
505
471
|
lastQuarantineReason: latestQuarantine?.lastErrorCode ?? null,
|
|
506
|
-
lastQuarantineHttpStatus: latestQuarantine &&
|
|
507
|
-
Number.isInteger(latestQuarantine.lastErrorHttpStatus) &&
|
|
508
|
-
latestQuarantine.lastErrorHttpStatus >= 100 &&
|
|
509
|
-
latestQuarantine.lastErrorHttpStatus <= 599
|
|
510
|
-
? latestQuarantine.lastErrorHttpStatus
|
|
511
|
-
: null,
|
|
512
|
-
lastQuarantineServerCode: isWorkSessionDenialCode(latestQuarantine?.lastServerErrorCode)
|
|
513
|
-
? latestQuarantine.lastServerErrorCode
|
|
514
|
-
: null,
|
|
515
472
|
};
|
|
516
473
|
}
|
|
517
474
|
function isOutboxDue(op, nowIso) {
|
|
@@ -594,18 +551,18 @@ export async function replayOutbox(root, deliver, opts = {}) {
|
|
|
594
551
|
}
|
|
595
552
|
else if (result.retryable) {
|
|
596
553
|
if (op.attemptCount + 1 >= OUTBOX_MAX_ATTEMPTS) {
|
|
597
|
-
markOutboxQuarantined(op.operationId, root, `${result.code}_MAX_ATTEMPTS`, now
|
|
554
|
+
markOutboxQuarantined(op.operationId, root, `${result.code}_MAX_ATTEMPTS`, now);
|
|
598
555
|
quarantined += 1;
|
|
599
556
|
}
|
|
600
557
|
else {
|
|
601
|
-
markOutboxQueued(op.operationId, root, result.code, now, random
|
|
558
|
+
markOutboxQueued(op.operationId, root, result.code, now, random);
|
|
602
559
|
queued += 1;
|
|
603
560
|
}
|
|
604
561
|
// Stop this session's FIFO on transient failure.
|
|
605
562
|
break;
|
|
606
563
|
}
|
|
607
564
|
else {
|
|
608
|
-
markOutboxQuarantined(op.operationId, root, result.code, now
|
|
565
|
+
markOutboxQuarantined(op.operationId, root, result.code, now);
|
|
609
566
|
quarantined += 1;
|
|
610
567
|
break;
|
|
611
568
|
}
|
|
@@ -14,7 +14,7 @@ import { refineProjectBinding } from "./bind-best.js";
|
|
|
14
14
|
import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
|
|
15
15
|
import { resolveProjectTask, shouldAskAfter } from "./project.js";
|
|
16
16
|
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
|
|
17
|
-
import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox,
|
|
17
|
+
import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, writeAckedRegisterState, } from "./outbox.js";
|
|
18
18
|
import { mergeLocalSessionFields, isRegisterReceiptEpisodeId, readSessionState, stripRegisterReceiptEpisodes, writeSessionState, } from "./state.js";
|
|
19
19
|
function resultOf(parts) {
|
|
20
20
|
const out = {
|
|
@@ -38,10 +38,6 @@ function resultOf(parts) {
|
|
|
38
38
|
out.bindingEpisodeId = parts.bindingEpisodeId;
|
|
39
39
|
if (parts.errorCode)
|
|
40
40
|
out.errorCode = parts.errorCode;
|
|
41
|
-
if (parts.httpStatus !== undefined)
|
|
42
|
-
out.httpStatus = parts.httpStatus;
|
|
43
|
-
if (parts.serverCode)
|
|
44
|
-
out.serverCode = parts.serverCode;
|
|
45
41
|
if (parts.projectBindReason)
|
|
46
42
|
out.projectBindReason = parts.projectBindReason;
|
|
47
43
|
if (parts.projectCandidates)
|
|
@@ -978,8 +974,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
978
974
|
};
|
|
979
975
|
}
|
|
980
976
|
if (delivered.retryable) {
|
|
981
|
-
|
|
982
|
-
markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn, undefined, details);
|
|
977
|
+
markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn);
|
|
983
978
|
return {
|
|
984
979
|
result: resultOf({
|
|
985
980
|
kind: "queued",
|
|
@@ -991,20 +986,15 @@ export async function reconcileObservation(obs, deps) {
|
|
|
991
986
|
projectId,
|
|
992
987
|
taskId,
|
|
993
988
|
errorCode: delivered.code,
|
|
994
|
-
...details,
|
|
995
989
|
...bindResultFields,
|
|
996
990
|
}),
|
|
997
991
|
exitCode: EXIT_OK,
|
|
998
|
-
diagnostics: [
|
|
999
|
-
...diagnostics,
|
|
1000
|
-
`transient ${delivered.code}${formatDeliveryDetails(details)}; remains queued`,
|
|
1001
|
-
],
|
|
992
|
+
diagnostics: [...diagnostics, `transient ${delivered.code}; remains queued`],
|
|
1002
993
|
statePath,
|
|
1003
994
|
outboxOperationId: outboxOp.operationId,
|
|
1004
995
|
};
|
|
1005
996
|
}
|
|
1006
|
-
|
|
1007
|
-
markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn, details);
|
|
997
|
+
markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn);
|
|
1008
998
|
return {
|
|
1009
999
|
result: resultOf({
|
|
1010
1000
|
kind: "error",
|
|
@@ -1017,24 +1007,13 @@ export async function reconcileObservation(obs, deps) {
|
|
|
1017
1007
|
projectId,
|
|
1018
1008
|
taskId,
|
|
1019
1009
|
errorCode: delivered.code,
|
|
1020
|
-
...details,
|
|
1021
1010
|
...bindResultFields,
|
|
1022
1011
|
}),
|
|
1023
1012
|
exitCode: EXIT_OK,
|
|
1024
|
-
diagnostics: [
|
|
1025
|
-
...diagnostics,
|
|
1026
|
-
`quarantined: ${delivered.code}${formatDeliveryDetails(details)}`,
|
|
1027
|
-
],
|
|
1013
|
+
diagnostics: [...diagnostics, `quarantined: ${delivered.code}`],
|
|
1028
1014
|
statePath,
|
|
1029
1015
|
outboxOperationId: outboxOp.operationId,
|
|
1030
1016
|
};
|
|
1031
1017
|
}
|
|
1032
|
-
function formatDeliveryDetails(details) {
|
|
1033
|
-
const parts = [
|
|
1034
|
-
details.httpStatus !== undefined ? `HTTP ${details.httpStatus}` : "",
|
|
1035
|
-
details.serverCode ?? "",
|
|
1036
|
-
].filter(Boolean);
|
|
1037
|
-
return parts.length ? ` (${parts.join(" ")})` : "";
|
|
1038
|
-
}
|
|
1039
1018
|
export { replayOutbox };
|
|
1040
1019
|
//# sourceMappingURL=reconcile.js.map
|
package/dist/main.d.ts
CHANGED
|
@@ -12,25 +12,6 @@ import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
|
12
12
|
* finalization; command execution itself does not consume this allowance.
|
|
13
13
|
*/
|
|
14
14
|
export declare const FOREGROUND_NETWORK_BUDGET_MS = 1500;
|
|
15
|
-
/**
|
|
16
|
-
* Shared accounting for awaited foreground network work. Each lifecycle phase
|
|
17
|
-
* debits only the bounded await it performs; ordinary command runtime leaves
|
|
18
|
-
* the allowance untouched. Earlier phases reserve the finalization floor, so
|
|
19
|
-
* error flushing cannot disappear after a long command.
|
|
20
|
-
*/
|
|
21
|
-
declare class ForegroundNetworkBudget {
|
|
22
|
-
private readonly budgetMs;
|
|
23
|
-
private readonly now;
|
|
24
|
-
private consumedMs;
|
|
25
|
-
constructor(budgetMs?: number, now?: () => number);
|
|
26
|
-
timeoutFor(phaseMaximumMs: number, reservedMs?: number): number;
|
|
27
|
-
waitFor<T>(phaseMaximumMs: number, reservedMs: number, work: (timeoutMs: number) => Promise<T>): Promise<T | undefined>;
|
|
28
|
-
}
|
|
29
|
-
/** Create a shared invocation budget; exported so the time accounting is directly testable. */
|
|
30
|
-
export declare function createForegroundNetworkBudget(options?: {
|
|
31
|
-
budgetMs?: number;
|
|
32
|
-
now?: () => number;
|
|
33
|
-
}): ForegroundNetworkBudget;
|
|
34
15
|
export type StreamErrorDependencies = {
|
|
35
16
|
stderr: Pick<typeof process.stderr, "write">;
|
|
36
17
|
exit: (code: number) => void;
|
|
@@ -49,5 +30,4 @@ export type TopLevelErrorDependencies = {
|
|
|
49
30
|
};
|
|
50
31
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
51
32
|
export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): Promise<void>;
|
|
52
|
-
export {};
|
|
53
33
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -108,13 +108,6 @@ class ForegroundNetworkBudget {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
/** Create a shared invocation budget; exported so the time accounting is directly testable. */
|
|
112
|
-
export function createForegroundNetworkBudget(options = {}) {
|
|
113
|
-
if (options.budgetMs === undefined && options.now === undefined) {
|
|
114
|
-
return new ForegroundNetworkBudget();
|
|
115
|
-
}
|
|
116
|
-
return new ForegroundNetworkBudget(options.budgetMs ?? FOREGROUND_NETWORK_BUDGET_MS, options.now ?? (() => performance.now()));
|
|
117
|
-
}
|
|
118
111
|
let activeForegroundNetworkBudget;
|
|
119
112
|
/**
|
|
120
113
|
* The RUNNING install's own entrypoint (`<pkg>/dist/index.js`), used as the
|
|
@@ -200,7 +193,7 @@ program.hook("preAction", async () => {
|
|
|
200
193
|
});
|
|
201
194
|
});
|
|
202
195
|
export async function runCli() {
|
|
203
|
-
const foregroundNetworkBudget =
|
|
196
|
+
const foregroundNetworkBudget = new ForegroundNetworkBudget();
|
|
204
197
|
activeForegroundNetworkBudget = foregroundNetworkBudget;
|
|
205
198
|
// Login must be usable before any identity or company is available, so do
|
|
206
199
|
// not make a pre-auth feature-flag request on this path. Other commands
|
package/package.json
CHANGED
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hq-core: public
|
|
3
|
-
# work-mesh-live-hook.sh — shared helpers for Work Mesh Live enqueue-only hooks.
|
|
4
|
-
# Sourced by core/hooks/*/3*-work-mesh-*.sh. Never execute directly.
|
|
5
|
-
|
|
6
|
-
# work_mesh_live_disabled
|
|
7
|
-
# Exit 0 (true) when kill switches say these hooks must no-op.
|
|
8
|
-
work_mesh_live_disabled() {
|
|
9
|
-
case "${HQ_WORK_MESH_DISABLED:-}" in
|
|
10
|
-
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
|
11
|
-
esac
|
|
12
|
-
case ",${HQ_DISABLED_HOOKS:-}," in
|
|
13
|
-
*,work-mesh,*|*,work-mesh-live,*|*,\*,*) return 0 ;;
|
|
14
|
-
esac
|
|
15
|
-
return 1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
# work_mesh_live_bootstrap
|
|
19
|
-
# Resolve HQ_ROOT from the calling hook path; source enqueue + session-id.
|
|
20
|
-
# Caller must set _WM_HOOK_DIR to dirname of BASH_SOURCE[0] before calling,
|
|
21
|
-
# OR pass the hook file path as $1.
|
|
22
|
-
work_mesh_live_bootstrap() {
|
|
23
|
-
local hook_file="${1:-${BASH_SOURCE[1]:-}}"
|
|
24
|
-
local hook_dir hq
|
|
25
|
-
hook_dir="$(cd "$(dirname "$hook_file")" 2>/dev/null && pwd)" || return 1
|
|
26
|
-
# core/hooks/<Event>/<file> -> HQ root is ../../..
|
|
27
|
-
hq="${HQ_ROOT:-$(cd "$hook_dir/../../.." 2>/dev/null && pwd)}"
|
|
28
|
-
[ -n "$hq" ] || return 1
|
|
29
|
-
export HQ_ROOT="$hq"
|
|
30
|
-
# shellcheck source=core/scripts/lib/work-mesh-enqueue.sh
|
|
31
|
-
. "$HQ_ROOT/core/scripts/lib/work-mesh-enqueue.sh" 2>/dev/null || return 1
|
|
32
|
-
# shellcheck source=core/scripts/lib/session-id.sh
|
|
33
|
-
. "$HQ_ROOT/core/scripts/lib/session-id.sh" 2>/dev/null || true
|
|
34
|
-
return 0
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# work_mesh_live_session_id <stdin-json>
|
|
38
|
-
# Prefer env (session_id_from_env); else payload session_id / sessionId.
|
|
39
|
-
work_mesh_live_session_id() {
|
|
40
|
-
local input="${1:-}" id=""
|
|
41
|
-
if command -v session_id_from_env >/dev/null 2>&1; then
|
|
42
|
-
id="$(session_id_from_env)"
|
|
43
|
-
fi
|
|
44
|
-
if [ -z "$id" ]; then
|
|
45
|
-
for var in HQ_SESSION_ID CLAUDE_CODE_SESSION_ID CLAUDE_SESSION_ID CODEX_SESSION_ID CODEX_THREAD_ID; do
|
|
46
|
-
id="${!var:-}"
|
|
47
|
-
id="$(printf '%s' "$id" | tr -d '[:space:]')"
|
|
48
|
-
[ -n "$id" ] && break
|
|
49
|
-
done
|
|
50
|
-
fi
|
|
51
|
-
if [ -z "$id" ] && [ -n "$input" ]; then
|
|
52
|
-
# Prefer jq when present; pure-bash fallback for the common keys.
|
|
53
|
-
if command -v jq >/dev/null 2>&1; then
|
|
54
|
-
id="$(printf '%s' "$input" | jq -r '.session_id // .sessionId // .conversation_id // .conversationId // .thread_id // .threadId // empty' 2>/dev/null || true)"
|
|
55
|
-
else
|
|
56
|
-
id="$(printf '%s' "$input" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p; t; s/.*"sessionId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)"
|
|
57
|
-
fi
|
|
58
|
-
id="$(printf '%s' "$id" | tr -d '[:space:]')"
|
|
59
|
-
fi
|
|
60
|
-
if command -v session_id_is_valid >/dev/null 2>&1; then
|
|
61
|
-
session_id_is_valid "$id" || id=""
|
|
62
|
-
else
|
|
63
|
-
case "$id" in
|
|
64
|
-
""|.|..|*[!A-Za-z0-9._-]*) id="" ;;
|
|
65
|
-
esac
|
|
66
|
-
fi
|
|
67
|
-
printf '%s' "$id"
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
# work_mesh_live_harness
|
|
71
|
-
# HQ_HARNESS / HQ_WORK_MESH_HARNESS / HQ_CHECKPOINT_RUNTIME -> contract harness.
|
|
72
|
-
work_mesh_live_harness() {
|
|
73
|
-
local h="${HQ_HARNESS:-${HQ_WORK_MESH_HARNESS:-${HQ_CHECKPOINT_RUNTIME:-claude-code}}}"
|
|
74
|
-
case "$h" in
|
|
75
|
-
claude|claude-code|Claude|ClaudeCode) printf 'claude-code' ;;
|
|
76
|
-
codex|Codex) printf 'codex' ;;
|
|
77
|
-
grok|Grok) printf 'grok' ;;
|
|
78
|
-
hq-sessions|hq_sessions|sessions) printf 'hq-sessions' ;;
|
|
79
|
-
agent-box|agent_box|box) printf 'agent-box' ;;
|
|
80
|
-
*) printf '%s' "$h" ;;
|
|
81
|
-
esac
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# work_mesh_live_adapter_version
|
|
85
|
-
work_mesh_live_adapter_version() {
|
|
86
|
-
if [ -n "${HQ_ADAPTER_CONTRACT_VERSION:-}" ]; then
|
|
87
|
-
printf '%s' "$HQ_ADAPTER_CONTRACT_VERSION"
|
|
88
|
-
return 0
|
|
89
|
-
fi
|
|
90
|
-
if [ -f "${HQ_ROOT:-}/core/scripts/lib/provider-adapter-version.sh" ]; then
|
|
91
|
-
# shellcheck source=core/scripts/lib/provider-adapter-version.sh
|
|
92
|
-
. "$HQ_ROOT/core/scripts/lib/provider-adapter-version.sh" 2>/dev/null || true
|
|
93
|
-
fi
|
|
94
|
-
printf '%s' "${HQ_ADAPTER_CONTRACT_VERSION:-1.0.0}"
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
# work_mesh_live_runtime_version
|
|
98
|
-
work_mesh_live_runtime_version() {
|
|
99
|
-
printf '%s' "${HQ_RUNTIME_VERSION:-${CLAUDE_CODE_VERSION:-${CODEX_VERSION:-${GROK_VERSION:-}}}}"
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
# work_mesh_live_next_seq <session-id>
|
|
103
|
-
# Monotonic seq under ~/.hq/work-mesh/seq/<sid> (or WORK_MESH_SEQ_DIR).
|
|
104
|
-
# Hot path: bash file read, no tr/chmod when the dir already exists.
|
|
105
|
-
work_mesh_live_next_seq() {
|
|
106
|
-
local sid="$1" dir file n
|
|
107
|
-
dir="${WORK_MESH_SEQ_DIR:-${HOME}/.hq/work-mesh/seq}"
|
|
108
|
-
if [ ! -d "$dir" ]; then
|
|
109
|
-
mkdir -p -- "$dir" 2>/dev/null || true
|
|
110
|
-
chmod 700 -- "$dir" 2>/dev/null || true
|
|
111
|
-
fi
|
|
112
|
-
file="$dir/$sid"
|
|
113
|
-
n=0
|
|
114
|
-
if [ -f "$file" ]; then
|
|
115
|
-
n="$(<"$file")"
|
|
116
|
-
n="${n//[[:space:]]/}"
|
|
117
|
-
case "$n" in
|
|
118
|
-
""|*[!0-9]*) n=0 ;;
|
|
119
|
-
esac
|
|
120
|
-
fi
|
|
121
|
-
n=$((n + 1))
|
|
122
|
-
printf '%s\n' "$n" >"$file" 2>/dev/null || return 1
|
|
123
|
-
printf '%s' "$n"
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# work_mesh_live_meta_get <root> <sid> <key>
|
|
127
|
-
work_mesh_live_meta_get() {
|
|
128
|
-
local root="$1" sid="$2" key="$3" meta
|
|
129
|
-
meta="$root/workspace/sessions/$sid/meta.yaml"
|
|
130
|
-
[ -f "$meta" ] || return 0
|
|
131
|
-
awk -v k="$key" '$1==k":"{ sub(/^[^:]+:[[:space:]]*/,""); gsub(/^"|"$/,""); print; exit }' "$meta" 2>/dev/null
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
# work_mesh_live_state_path <sid>
|
|
135
|
-
work_mesh_live_state_path() {
|
|
136
|
-
printf '%s/.hq/work-context/sessions/%s.json' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# work_mesh_live_board_path <sid>
|
|
140
|
-
work_mesh_live_board_path() {
|
|
141
|
-
printf '%s/.hq/work-context/sessions/%s/board.md' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
# work_mesh_live_ask_surfaced_path <sid>
|
|
145
|
-
work_mesh_live_ask_surfaced_path() {
|
|
146
|
-
printf '%s/.hq/work-context/sessions/%s.ask-surfaced' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
# work_mesh_live_pending_decision_path <sid>
|
|
150
|
-
work_mesh_live_pending_decision_path() {
|
|
151
|
-
printf '%s/.hq/work-context/sessions/%s.pending-decision' "${WORK_MESH_HOME:-$HOME}" "$1"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
# work_mesh_live_word_count <text> — count whitespace-separated tokens.
|
|
155
|
-
work_mesh_live_word_count() {
|
|
156
|
-
local t="$1"
|
|
157
|
-
# shellcheck disable=SC2086
|
|
158
|
-
set -- $t
|
|
159
|
-
printf '%s' "$#"
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
# work_mesh_live_is_slash_or_short <prompt>
|
|
163
|
-
# True (0) when slash command or fewer than 3 words.
|
|
164
|
-
work_mesh_live_is_slash_or_short() {
|
|
165
|
-
local p="$1" n
|
|
166
|
-
case "$p" in
|
|
167
|
-
/*) return 0 ;;
|
|
168
|
-
esac
|
|
169
|
-
n="$(work_mesh_live_word_count "$p")"
|
|
170
|
-
[ "$n" -lt 3 ]
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
# work_mesh_live_bump_tool_writes <sid>
|
|
174
|
-
# Atomically increment toolWrites on the state JSON (create stub if absent).
|
|
175
|
-
# Sets REPLY to the new count. Uses temp+mv and chmod 600.
|
|
176
|
-
# Merge-safe: only toolWrites and updatedAt change; every other field the
|
|
177
|
-
# hq-cli reconcile/ack path wrote (companyUid, companySlug, projectId, taskId,
|
|
178
|
-
# startedAt, bindingEpisodeId, decision, contextStatus, ...) is preserved
|
|
179
|
-
# byte-for-byte. Prefers jq; the no-jq fallback edits the two values in place
|
|
180
|
-
# in pure bash (compact or pretty-printed JSON, any key order) and only
|
|
181
|
-
# appends a missing key. contextStatus is set to "unresolved" only on a fresh
|
|
182
|
-
# stub. Bash 3.2 compatible; no sed/awk/GNU flags.
|
|
183
|
-
# Concurrency: hq-cli writes the same path with same-dir tmp + rename and has
|
|
184
|
-
# no lock convention (hq-cli src/lib/work-context/atomic.ts). Two lines of
|
|
185
|
-
# defense against a reconcile write landing between our read and mv:
|
|
186
|
-
# 1. mkdir lock at <path>.lock (Bash 3.2 safe), bounded wait ~200ms, stale
|
|
187
|
-
# lock reclaimed by age (>5s). If the lock cannot be taken, the bump is
|
|
188
|
-
# skipped (return 1) rather than risk a clobber.
|
|
189
|
-
# 2. Re-read immediately before mv; if the file changed since the first
|
|
190
|
-
# read, rebuild from the new body.
|
|
191
|
-
# Test seam: WORK_MESH_BUMP_TEST_SLEEP_AFTER_READ=<secs> sleeps between the
|
|
192
|
-
# first read and the re-read so a test can inject a concurrent writer.
|
|
193
|
-
work_mesh_live_bump_tool_writes() {
|
|
194
|
-
local sid="$1" path dir lock rc
|
|
195
|
-
path="$(work_mesh_live_state_path "$sid")"
|
|
196
|
-
dir="${path%/*}"
|
|
197
|
-
if [ ! -d "$dir" ]; then
|
|
198
|
-
mkdir -p -- "$dir" 2>/dev/null || true
|
|
199
|
-
chmod 700 -- "$dir" 2>/dev/null || true
|
|
200
|
-
fi
|
|
201
|
-
lock="$path.lock"
|
|
202
|
-
_work_mesh_live_lock_acquire "$lock" || return 1
|
|
203
|
-
_work_mesh_live_bump_locked "$sid" "$path"
|
|
204
|
-
rc=$?
|
|
205
|
-
rm -rf -- "$lock" 2>/dev/null || true
|
|
206
|
-
return $rc
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# _work_mesh_live_lock_acquire <lockdir>
|
|
210
|
-
# mkdir lock; ~20 x 10ms bounded wait; reclaim when the holder's stamp is
|
|
211
|
-
# older than 5s (or missing after the full wait). 0 = held, 1 = give up.
|
|
212
|
-
_work_mesh_live_lock_acquire() {
|
|
213
|
-
local lock="$1" i=0 now stamp
|
|
214
|
-
while :; do
|
|
215
|
-
if mkdir -- "$lock" 2>/dev/null; then
|
|
216
|
-
date +%s >"$lock/ts" 2>/dev/null || true
|
|
217
|
-
return 0
|
|
218
|
-
fi
|
|
219
|
-
i=$((i + 1))
|
|
220
|
-
if [ "$i" -ge 20 ]; then
|
|
221
|
-
now="$(date +%s 2>/dev/null || printf '0')"
|
|
222
|
-
stamp="$(cat "$lock/ts" 2>/dev/null || printf '')"
|
|
223
|
-
case "$stamp" in ""|*[!0-9]*) stamp=0 ;; esac
|
|
224
|
-
if [ "$stamp" -eq 0 ] || [ $((now - stamp)) -gt 5 ]; then
|
|
225
|
-
rm -rf -- "$lock" 2>/dev/null || true
|
|
226
|
-
if mkdir -- "$lock" 2>/dev/null; then
|
|
227
|
-
date +%s >"$lock/ts" 2>/dev/null || true
|
|
228
|
-
return 0
|
|
229
|
-
fi
|
|
230
|
-
fi
|
|
231
|
-
return 1
|
|
232
|
-
fi
|
|
233
|
-
sleep 0.01 2>/dev/null || sleep 1
|
|
234
|
-
done
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
# _work_mesh_live_bump_render <sid> <path> <tmp>
|
|
238
|
-
# Read <path> (if any), build the bumped document into <tmp>.
|
|
239
|
-
# Sets _WM_SEEN to the body that was read ("" when absent) and REPLY to the
|
|
240
|
-
# new count. Returns 1 when the file must not be touched.
|
|
241
|
-
_work_mesh_live_bump_render() {
|
|
242
|
-
local sid="$1" path="$2" tmp="$3" cur="" next ts body="" rest have_jq=0 written=0
|
|
243
|
-
_WM_SEEN=""
|
|
244
|
-
command -v jq >/dev/null 2>&1 && have_jq=1
|
|
245
|
-
if [ -f "$path" ]; then
|
|
246
|
-
body="$(<"$path")"
|
|
247
|
-
_WM_SEEN="$body"
|
|
248
|
-
case "$body" in
|
|
249
|
-
*"\"toolWrites\""*)
|
|
250
|
-
rest="${body#*\"toolWrites\"}"; rest="${rest#*:}"
|
|
251
|
-
while [ "${rest#"${rest%%[![:space:]]*}"}" != "$rest" ]; do rest="${rest#?}"; done
|
|
252
|
-
cur="${rest%%[!0-9]*}"
|
|
253
|
-
;;
|
|
254
|
-
esac
|
|
255
|
-
fi
|
|
256
|
-
case "$cur" in ""|*[!0-9]*) cur=0 ;; esac
|
|
257
|
-
next=$((cur + 1))
|
|
258
|
-
ts=""
|
|
259
|
-
# %(...)T needs Bash 4.2+; macOS Bash 3.2 falls back to date -u (BSD + GNU).
|
|
260
|
-
TZ=UTC printf -v ts '%(%Y-%m-%dT%H:%M:%S)T.000Z' -1 2>/dev/null || ts=""
|
|
261
|
-
case "$ts" in
|
|
262
|
-
""|*%*) ts="$(TZ=UTC date -u '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null || true)" ;;
|
|
263
|
-
esac
|
|
264
|
-
[ -n "$ts" ] || ts="1970-01-01T00:00:00.000Z"
|
|
265
|
-
if [ ! -f "$path" ]; then
|
|
266
|
-
printf '{"contractVersion":1,"sessionId":"%s","contextStatus":"unresolved","toolWrites":%s,"updatedAt":"%s"}\n' \
|
|
267
|
-
"$sid" "$next" "$ts" >"$tmp" || return 1
|
|
268
|
-
REPLY="$next"
|
|
269
|
-
return 0
|
|
270
|
-
fi
|
|
271
|
-
if [ "$have_jq" -eq 1 ]; then
|
|
272
|
-
if printf '%s' "$body" | jq --argjson n "$next" --arg ts "$ts" \
|
|
273
|
-
'.toolWrites = $n | .updatedAt = $ts' >"$tmp" 2>/dev/null \
|
|
274
|
-
&& [ -s "$tmp" ]; then
|
|
275
|
-
written=1
|
|
276
|
-
elif jq -n empty >/dev/null 2>&1; then
|
|
277
|
-
# jq works but could not parse the file (malformed/partial JSON).
|
|
278
|
-
# Never replace the CLI's file with a stub: leave it untouched and skip.
|
|
279
|
-
rm -f -- "$tmp" 2>/dev/null
|
|
280
|
-
return 1
|
|
281
|
-
fi
|
|
282
|
-
# else: jq on PATH is unusable; take the pure-bash path below.
|
|
283
|
-
fi
|
|
284
|
-
if [ "$written" -eq 0 ]; then
|
|
285
|
-
# No-jq fallback: pure-bash in-place edit of the two values (no extra
|
|
286
|
-
# spawns, Bash 3.2 safe). Works for compact and pretty JSON in any key
|
|
287
|
-
# order; every other byte of the CLI's file is left as is.
|
|
288
|
-
local pre digits add
|
|
289
|
-
case "$body" in
|
|
290
|
-
*"\"toolWrites\""*)
|
|
291
|
-
pre="${body%%\"toolWrites\"*}"
|
|
292
|
-
rest="${body#*\"toolWrites\"}"
|
|
293
|
-
while [ "${rest#"${rest%%[![:space:]:]*}"}" != "$rest" ]; do rest="${rest#?}"; done
|
|
294
|
-
digits="${rest%%[!0-9]*}"
|
|
295
|
-
rest="${rest#"$digits"}"
|
|
296
|
-
body="$pre\"toolWrites\": $next$rest"
|
|
297
|
-
;;
|
|
298
|
-
esac
|
|
299
|
-
case "$body" in
|
|
300
|
-
*"\"updatedAt\""*)
|
|
301
|
-
pre="${body%%\"updatedAt\"*}"
|
|
302
|
-
rest="${body#*\"updatedAt\"}"
|
|
303
|
-
while [ "${rest#"${rest%%[![:space:]:]*}"}" != "$rest" ]; do rest="${rest#?}"; done
|
|
304
|
-
case "$rest" in
|
|
305
|
-
\"*) rest="${rest#\"}"; rest="${rest#*\"}" ;;
|
|
306
|
-
esac
|
|
307
|
-
body="$pre\"updatedAt\": \"$ts\"$rest"
|
|
308
|
-
;;
|
|
309
|
-
esac
|
|
310
|
-
add=""
|
|
311
|
-
case "$body" in *"\"toolWrites\""*) ;; *) add="\"toolWrites\": $next" ;; esac
|
|
312
|
-
case "$body" in *"\"updatedAt\""*) ;; *) add="${add:+$add, }\"updatedAt\": \"$ts\"" ;; esac
|
|
313
|
-
if [ -n "$add" ]; then
|
|
314
|
-
# Insert right after the opening brace so the closing brace stays put.
|
|
315
|
-
case "$body" in
|
|
316
|
-
*\{*)
|
|
317
|
-
pre="${body%%\{*}"
|
|
318
|
-
rest="${body#*\{}"
|
|
319
|
-
local peek="$rest"
|
|
320
|
-
while [ "${peek#"${peek%%[![:space:]]*}"}" != "$peek" ]; do peek="${peek#?}"; done
|
|
321
|
-
case "$peek" in
|
|
322
|
-
\}*) body="$pre{$add$peek" ;;
|
|
323
|
-
*) body="$pre{$add, $rest" ;;
|
|
324
|
-
esac
|
|
325
|
-
;;
|
|
326
|
-
*) body="{$add}" ;;
|
|
327
|
-
esac
|
|
328
|
-
fi
|
|
329
|
-
if printf '%s\n' "$body" >"$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
|
330
|
-
written=1
|
|
331
|
-
fi
|
|
332
|
-
fi
|
|
333
|
-
if [ "$written" -eq 0 ]; then
|
|
334
|
-
rm -f -- "$tmp" 2>/dev/null
|
|
335
|
-
return 1
|
|
336
|
-
fi
|
|
337
|
-
REPLY="$next"
|
|
338
|
-
return 0
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
# _work_mesh_live_bump_locked <sid> <path> (caller holds the lock)
|
|
342
|
-
_work_mesh_live_bump_locked() {
|
|
343
|
-
local sid="$1" path="$2" tmp seen again
|
|
344
|
-
tmp="$path.tmp.$$"
|
|
345
|
-
_work_mesh_live_bump_render "$sid" "$path" "$tmp" || return 1
|
|
346
|
-
seen="$_WM_SEEN"
|
|
347
|
-
if [ -n "${WORK_MESH_BUMP_TEST_SLEEP_AFTER_READ:-}" ]; then
|
|
348
|
-
sleep "$WORK_MESH_BUMP_TEST_SLEEP_AFTER_READ" 2>/dev/null || true
|
|
349
|
-
fi
|
|
350
|
-
# Second line of defense: re-read right before mv; if a concurrent writer
|
|
351
|
-
# (hq-cli reconcile/ack) changed the file since our read, rebuild from it.
|
|
352
|
-
again=""
|
|
353
|
-
[ -f "$path" ] && again="$(<"$path")"
|
|
354
|
-
if [ "$again" != "$seen" ]; then
|
|
355
|
-
rm -f -- "$tmp" 2>/dev/null
|
|
356
|
-
_work_mesh_live_bump_render "$sid" "$path" "$tmp" || return 1
|
|
357
|
-
fi
|
|
358
|
-
mv -f -- "$tmp" "$path" 2>/dev/null || { rm -f -- "$tmp"; return 1; }
|
|
359
|
-
chmod 600 -- "$path" 2>/dev/null || true
|
|
360
|
-
return 0
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
# work_mesh_live_is_file_write_tool <tool_name> <bash_command?>
|
|
364
|
-
work_mesh_live_is_file_write_tool() {
|
|
365
|
-
local tool="$1" cmd="${2:-}"
|
|
366
|
-
case "$tool" in
|
|
367
|
-
Edit|Write|MultiEdit|NotebookEdit|apply_patch|StrReplace|search_replace|write) return 0 ;;
|
|
368
|
-
Bash|Shell|run_terminal_command)
|
|
369
|
-
# Redirect / common file-mutating bash forms (no nested quotes in case arms).
|
|
370
|
-
case "$cmd" in
|
|
371
|
-
*\>*|*tee\ *|*mv\ *|*cp\ *|*truncate\ *) return 0 ;;
|
|
372
|
-
*sed\ -i*|*dd\ if=*) return 0 ;;
|
|
373
|
-
*) return 1 ;;
|
|
374
|
-
esac
|
|
375
|
-
;;
|
|
376
|
-
*) return 1 ;;
|
|
377
|
-
esac
|
|
378
|
-
}
|