@indigoai-us/hq-cli 5.122.2 → 5.123.1
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 +59 -0
- package/README.md +46 -0
- package/dist/command-catalog.generated.d.ts +6 -3
- package/dist/command-catalog.generated.js +7 -3
- package/dist/commands/dm.d.ts +54 -1
- package/dist/commands/dm.js +204 -8
- package/dist/commands/mesh.js +7 -2
- package/dist/commands/update.js +6 -0
- package/dist/commands/whoami.d.ts +2 -2
- package/dist/commands/whoami.js +18 -19
- package/dist/lib/billing/plan-lock.d.ts +64 -69
- package/dist/lib/billing/plan-lock.js +230 -161
- package/dist/lib/mesh/api.js +96 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +43 -7
- package/dist/lib/mesh/live/backfill-held.js +323 -42
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +4 -1
- package/dist/lib/mesh/live/daemon/board-refresh.js +71 -30
- package/dist/lib/mesh/live/daemon/config.d.ts +9 -0
- package/dist/lib/mesh/live/daemon/config.js +45 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +6 -1
- package/dist/lib/mesh/live/daemon/credentials.js +20 -4
- package/dist/lib/mesh/live/daemon/doctor.d.ts +6 -0
- package/dist/lib/mesh/live/daemon/doctor.js +29 -2
- package/dist/lib/mesh/live/daemon/index.d.ts +4 -2
- package/dist/lib/mesh/live/daemon/index.js +3 -2
- package/dist/lib/mesh/live/daemon/install.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/install.js +54 -4
- package/dist/lib/mesh/live/daemon/run.d.ts +6 -0
- package/dist/lib/mesh/live/daemon/run.js +50 -3
- package/dist/lib/mesh/live/daemon/state.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/version-drift.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/version-drift.js +47 -0
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +4 -0
- package/dist/lib/work-context/reconcile.js +67 -6
- package/dist/lib/work-context/state.d.ts +5 -3
- package/dist/lib/work-context/state.js +28 -4
- package/dist/utils/cli-telemetry.d.ts +21 -0
- package/dist/utils/cli-telemetry.js +77 -1
- package/dist/utils/self-update.d.ts +3 -0
- package/dist/utils/self-update.js +11 -0
- package/package.json +3 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Refresh ~/.hq/work-context/sessions/<sid>/board.md for bound sessions
|
|
3
|
-
* from
|
|
3
|
+
* from the ProjectView read route at most every 60s.
|
|
4
|
+
*
|
|
5
|
+
* Board JSON remains the fallback for older projects, but story status is
|
|
6
|
+
* authoritative in ProjectView because `hq mesh story` writes there.
|
|
4
7
|
* Metadata-only snapshot — never prompts or tokens.
|
|
5
8
|
*/
|
|
6
9
|
import * as fs from "node:fs";
|
|
@@ -115,9 +118,72 @@ export async function refreshBoundSessionBoards(deps) {
|
|
|
115
118
|
}
|
|
116
119
|
return { refreshed, skipped, errors };
|
|
117
120
|
}
|
|
121
|
+
function storiesFromProject(project) {
|
|
122
|
+
const storiesRaw = Array.isArray(project.stories) ? project.stories : [];
|
|
123
|
+
const stories = [];
|
|
124
|
+
for (const s of storiesRaw) {
|
|
125
|
+
if (!s || typeof s !== "object" || Array.isArray(s))
|
|
126
|
+
continue;
|
|
127
|
+
const row = s;
|
|
128
|
+
const id = typeof row.id === "string"
|
|
129
|
+
? row.id
|
|
130
|
+
: typeof row.storyId === "string"
|
|
131
|
+
? row.storyId
|
|
132
|
+
: typeof row.taskId === "string"
|
|
133
|
+
? row.taskId
|
|
134
|
+
: "";
|
|
135
|
+
if (!id)
|
|
136
|
+
continue;
|
|
137
|
+
stories.push({
|
|
138
|
+
id,
|
|
139
|
+
title: typeof row.title === "string"
|
|
140
|
+
? row.title.slice(0, 120)
|
|
141
|
+
: typeof row.name === "string"
|
|
142
|
+
? row.name.slice(0, 120)
|
|
143
|
+
: undefined,
|
|
144
|
+
status: typeof row.status === "string" ? row.status : undefined,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return stories;
|
|
148
|
+
}
|
|
149
|
+
function projectViewSnapshot(raw, companyUid, projectId) {
|
|
150
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
151
|
+
return null;
|
|
152
|
+
const project = raw;
|
|
153
|
+
if (project.companyUid !== companyUid || project.projectId !== projectId)
|
|
154
|
+
return null;
|
|
155
|
+
return {
|
|
156
|
+
projectId,
|
|
157
|
+
projectName: typeof project.name === "string"
|
|
158
|
+
? project.name
|
|
159
|
+
: typeof project.title === "string"
|
|
160
|
+
? project.title
|
|
161
|
+
: undefined,
|
|
162
|
+
stories: storiesFromProject(project),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function isMissingProject(error) {
|
|
166
|
+
// hq-pro's GET ProjectView handler emits PROJECT_VIEW_NOT_FOUND; retain the
|
|
167
|
+
// legacy PROJECT_NOT_FOUND code for compatibility with older producers.
|
|
168
|
+
return (error instanceof Error &&
|
|
169
|
+
/^404\s+(?:PROJECT_VIEW_NOT_FOUND|PROJECT_NOT_FOUND)\b/.test(error.message));
|
|
170
|
+
}
|
|
118
171
|
/** Build a BoardReader from meshJson-compatible fetch. */
|
|
119
172
|
export function createVaultBoardReader(opts) {
|
|
120
173
|
return async (companyUid, projectId) => {
|
|
174
|
+
// ProjectView is the live story-status source. Only use board.json when
|
|
175
|
+
// this project has not been migrated into ProjectView yet.
|
|
176
|
+
try {
|
|
177
|
+
const view = await opts.getJson(`/v1/work-mesh/projects/${encodeURIComponent(projectId)}?companyUid=${encodeURIComponent(companyUid)}`);
|
|
178
|
+
const snapshot = projectViewSnapshot(view, companyUid, projectId);
|
|
179
|
+
if (snapshot)
|
|
180
|
+
return snapshot;
|
|
181
|
+
throw new Error("ProjectView response did not match the requested company and project");
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!isMissingProject(error))
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
121
187
|
const raw = await opts.getJson(`/companies/${encodeURIComponent(companyUid)}/board`);
|
|
122
188
|
if (!raw || typeof raw !== "object")
|
|
123
189
|
return null;
|
|
@@ -140,35 +206,10 @@ export function createVaultBoardReader(opts) {
|
|
|
140
206
|
const id = typeof p.id === "string" ? p.id : typeof p.projectId === "string" ? p.projectId : "";
|
|
141
207
|
if (id !== projectId)
|
|
142
208
|
continue;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
: Array.isArray(p.tasks
|
|
146
|
-
|
|
147
|
-
: [];
|
|
148
|
-
const stories = [];
|
|
149
|
-
for (const s of storiesRaw) {
|
|
150
|
-
if (!s || typeof s !== "object" || Array.isArray(s))
|
|
151
|
-
continue;
|
|
152
|
-
const row = s;
|
|
153
|
-
const sid = typeof row.id === "string"
|
|
154
|
-
? row.id
|
|
155
|
-
: typeof row.storyId === "string"
|
|
156
|
-
? row.storyId
|
|
157
|
-
: typeof row.taskId === "string"
|
|
158
|
-
? row.taskId
|
|
159
|
-
: "";
|
|
160
|
-
if (!sid)
|
|
161
|
-
continue;
|
|
162
|
-
stories.push({
|
|
163
|
-
id: sid,
|
|
164
|
-
title: typeof row.title === "string"
|
|
165
|
-
? row.title.slice(0, 120)
|
|
166
|
-
: typeof row.name === "string"
|
|
167
|
-
? row.name.slice(0, 120)
|
|
168
|
-
: undefined,
|
|
169
|
-
status: typeof row.status === "string" ? row.status : undefined,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
209
|
+
const stories = storiesFromProject({
|
|
210
|
+
...p,
|
|
211
|
+
stories: Array.isArray(p.stories) ? p.stories : p.tasks,
|
|
212
|
+
});
|
|
172
213
|
return {
|
|
173
214
|
projectId,
|
|
174
215
|
projectName: typeof p.name === "string"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface DaemonConfig {
|
|
2
|
+
trustedHqRoots: string[];
|
|
3
|
+
}
|
|
4
|
+
export declare function daemonConfigPath(daemonDirectory: string): string;
|
|
5
|
+
export declare function verifyHqRoot(candidate: string | undefined): string | null;
|
|
6
|
+
export declare function readDaemonConfig(daemonDirectory: string): DaemonConfig;
|
|
7
|
+
/** Record a verified local HQ checkout without trusting a caller-supplied path at runtime. */
|
|
8
|
+
export declare function registerTrustedHqRoot(daemonDirectory: string, candidate: string | undefined): DaemonConfig;
|
|
9
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export function daemonConfigPath(daemonDirectory) {
|
|
4
|
+
return path.join(daemonDirectory, "config.json");
|
|
5
|
+
}
|
|
6
|
+
export function verifyHqRoot(candidate) {
|
|
7
|
+
if (!candidate?.trim())
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const root = fs.realpathSync(candidate);
|
|
11
|
+
return fs.statSync(path.join(root, "companies", "manifest.yaml")).isFile()
|
|
12
|
+
? root
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function readDaemonConfig(daemonDirectory) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(daemonConfigPath(daemonDirectory), "utf8"));
|
|
22
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
23
|
+
return { trustedHqRoots: [] };
|
|
24
|
+
const roots = raw.trustedHqRoots;
|
|
25
|
+
if (!Array.isArray(roots))
|
|
26
|
+
return { trustedHqRoots: [] };
|
|
27
|
+
return { trustedHqRoots: roots.filter((root) => typeof root === "string")
|
|
28
|
+
.map(verifyHqRoot).filter((root) => Boolean(root)) };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { trustedHqRoots: [] };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Record a verified local HQ checkout without trusting a caller-supplied path at runtime. */
|
|
35
|
+
export function registerTrustedHqRoot(daemonDirectory, candidate) {
|
|
36
|
+
const root = verifyHqRoot(candidate);
|
|
37
|
+
const current = readDaemonConfig(daemonDirectory);
|
|
38
|
+
if (!root || current.trustedHqRoots.includes(root))
|
|
39
|
+
return current;
|
|
40
|
+
const next = { trustedHqRoots: [...current.trustedHqRoots, root] };
|
|
41
|
+
fs.mkdirSync(daemonDirectory, { recursive: true, mode: 0o700 });
|
|
42
|
+
fs.writeFileSync(daemonConfigPath(daemonDirectory), `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -49,6 +49,11 @@ export interface PresenceCompany {
|
|
|
49
49
|
companyUid: string;
|
|
50
50
|
presenceTopic: string;
|
|
51
51
|
}
|
|
52
|
+
export interface DroppedPresenceCompany {
|
|
53
|
+
companyUid: string;
|
|
54
|
+
/** Server-supplied cause when available; otherwise a deliberately neutral label. */
|
|
55
|
+
reason: string;
|
|
56
|
+
}
|
|
52
57
|
export interface Contract3Bundle {
|
|
53
58
|
contractVersion: 3;
|
|
54
59
|
credentials: IotCredentials;
|
|
@@ -58,7 +63,7 @@ export interface Contract3Bundle {
|
|
|
58
63
|
actorUid: string;
|
|
59
64
|
actorType: "human" | "agent";
|
|
60
65
|
companies: PresenceCompany[];
|
|
61
|
-
droppedCompanies:
|
|
66
|
+
droppedCompanies: DroppedPresenceCompany[];
|
|
62
67
|
expiresAt: string;
|
|
63
68
|
}
|
|
64
69
|
export type CredentialsFetcher = () => Promise<Contract3Bundle>;
|
|
@@ -229,9 +229,25 @@ export function normalizeContract3Bundle(raw) {
|
|
|
229
229
|
if (!expiresAt) {
|
|
230
230
|
throw new Error("contract-3 vend missing expiresAt");
|
|
231
231
|
}
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
const droppedCompanies = [];
|
|
233
|
+
for (const item of Array.isArray(r.droppedCompanies) ? r.droppedCompanies : []) {
|
|
234
|
+
if (typeof item === "string" && item.trim()) {
|
|
235
|
+
droppedCompanies.push({ companyUid: item, reason: "unknown" });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
239
|
+
continue;
|
|
240
|
+
const row = item;
|
|
241
|
+
const companyUid = typeof row.companyUid === "string" ? row.companyUid.trim() : "";
|
|
242
|
+
if (!companyUid)
|
|
243
|
+
continue;
|
|
244
|
+
droppedCompanies.push({
|
|
245
|
+
companyUid,
|
|
246
|
+
reason: typeof row.reason === "string" && row.reason.trim()
|
|
247
|
+
? row.reason.trim()
|
|
248
|
+
: "not included",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
235
251
|
return {
|
|
236
252
|
contractVersion: 3,
|
|
237
253
|
credentials: {
|
|
@@ -246,7 +262,7 @@ export function normalizeContract3Bundle(raw) {
|
|
|
246
262
|
actorUid,
|
|
247
263
|
actorType,
|
|
248
264
|
companies,
|
|
249
|
-
droppedCompanies
|
|
265
|
+
droppedCompanies,
|
|
250
266
|
expiresAt,
|
|
251
267
|
};
|
|
252
268
|
}
|
|
@@ -17,6 +17,8 @@ export interface DaemonDoctorReport {
|
|
|
17
17
|
subscribed: boolean;
|
|
18
18
|
spoolDepth: number;
|
|
19
19
|
heldCount: number;
|
|
20
|
+
/** Distinct held sessions with neither bound state nor local company evidence. */
|
|
21
|
+
heldWithNoLocalEvidence: number;
|
|
20
22
|
deadLetterCount: number;
|
|
21
23
|
/** Events in any local queue still marked no-company / NEEDS_COMPANY / NONE. */
|
|
22
24
|
unattributedEvents: UnattributedEventCounts;
|
|
@@ -32,6 +34,8 @@ export interface DaemonDoctorReport {
|
|
|
32
34
|
actorKind: CognitoActorKind;
|
|
33
35
|
/** Credential vend refused (disabled / unsupported / 403). */
|
|
34
36
|
presenceRefusal?: DaemonStateFile["presenceRefusal"];
|
|
37
|
+
/** Companies the current presence credential did not include. */
|
|
38
|
+
presenceDroppedCompanies: NonNullable<DaemonStateFile["presenceDroppedCompanies"]>;
|
|
35
39
|
/** Pending events awaiting a retry drain (POST /v1/mesh/events). */
|
|
36
40
|
emitRetryDepth: number;
|
|
37
41
|
/** Last successful direct-emit POST time. */
|
|
@@ -48,6 +52,8 @@ export interface DaemonDoctorDeps {
|
|
|
48
52
|
home?: string;
|
|
49
53
|
meshRoot?: string;
|
|
50
54
|
workContextRoot?: string;
|
|
55
|
+
/** Independently configured HQ root for held-event evidence checks. */
|
|
56
|
+
hqRoot?: string;
|
|
51
57
|
env?: NodeJS.ProcessEnv;
|
|
52
58
|
isProcessAlive?: (pid: number) => boolean;
|
|
53
59
|
now?: () => number;
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import { describeCognitoTokenSource, } from "../../../../utils/cognito-session.js";
|
|
8
|
-
import { outboxStats, } from "../../../work-context/outbox.js";
|
|
8
|
+
import { listOutboxOperations, outboxStats, } from "../../../work-context/outbox.js";
|
|
9
|
+
import { readSessionState } from "../../../work-context/state.js";
|
|
9
10
|
import { workContextRoot } from "../../../work-context/paths.js";
|
|
10
11
|
import { countJsonlLines, countUnattributedEvents, } from "../spool.js";
|
|
11
12
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../paths.js";
|
|
12
13
|
import { daemonDir, daemonPidPath } from "./paths.js";
|
|
13
14
|
import { readEmitState } from "../emit.js";
|
|
14
15
|
import { emitRetryDepth } from "../emit-retry.js";
|
|
16
|
+
import { hasCurrentAcknowledgedRegistration, readHeldSessions, refWithLocalCompanyEvidence, } from "../backfill-held.js";
|
|
17
|
+
import { readDaemonConfig } from "./config.js";
|
|
15
18
|
import { pidLockStatus, readPidLock } from "./pid-lock.js";
|
|
16
19
|
import { readDaemonState } from "./state.js";
|
|
17
20
|
export const UNHEALTHY_SPOOL_AGE_MS = 60_000;
|
|
@@ -77,6 +80,14 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
77
80
|
const deadPath = workMeshDeadLetterPath(meshRoot);
|
|
78
81
|
const spoolDepth = countJsonlLines(spoolPath);
|
|
79
82
|
const heldCount = countJsonlLines(heldPath);
|
|
83
|
+
const trustedRoots = readDaemonConfig(dir).trustedHqRoots;
|
|
84
|
+
const outboxOperations = listOutboxOperations(ctxRoot);
|
|
85
|
+
const heldWithNoLocalEvidence = readHeldSessions(meshRoot).filter((ref) => {
|
|
86
|
+
const state = readSessionState(ref.sessionId, ctxRoot);
|
|
87
|
+
return (!state?.companyUid &&
|
|
88
|
+
!hasCurrentAcknowledgedRegistration(ref, state, outboxOperations, deps.hqRoot ?? env.HQ_ROOT?.trim(), trustedRoots) &&
|
|
89
|
+
!refWithLocalCompanyEvidence(ref, deps.hqRoot ?? env.HQ_ROOT?.trim(), trustedRoots, state));
|
|
90
|
+
}).length;
|
|
80
91
|
const deadLetterCount = countJsonlLines(deadPath);
|
|
81
92
|
const unattributedEvents = countUnattributedEvents(meshRoot);
|
|
82
93
|
const outbox = outboxStats(ctxRoot);
|
|
@@ -100,11 +111,18 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
100
111
|
if (emitMode === "legacy" && heldCount > 0) {
|
|
101
112
|
unhealthyReasons.push(`held events awaiting company attribution=${heldCount}`);
|
|
102
113
|
}
|
|
114
|
+
if (heldWithNoLocalEvidence > 0) {
|
|
115
|
+
unhealthyReasons.push(`held with no local evidence=${heldWithNoLocalEvidence}`);
|
|
116
|
+
}
|
|
103
117
|
if (unattributedEvents.total > 0) {
|
|
104
118
|
unhealthyReasons.push(`unattributed events=${unattributedEvents.total} ` +
|
|
105
119
|
`(spool=${unattributedEvents.spool} held=${unattributedEvents.held} dead-letter=${unattributedEvents.deadLetter})`);
|
|
106
120
|
}
|
|
107
121
|
const presenceRefusal = state?.presenceRefusal ?? null;
|
|
122
|
+
const presenceDroppedCompanies = state?.presenceDroppedCompanies ?? [];
|
|
123
|
+
if (presenceDroppedCompanies.length > 0) {
|
|
124
|
+
unhealthyReasons.push(`presence unavailable for ${presenceDroppedCompanies.length} companies`);
|
|
125
|
+
}
|
|
108
126
|
if (presenceRefusal) {
|
|
109
127
|
unhealthyReasons.push(`presence credential refused (${presenceRefusal.code}); next retry ${presenceRefusal.nextRetryAt}`);
|
|
110
128
|
}
|
|
@@ -121,6 +139,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
121
139
|
companiesOnline,
|
|
122
140
|
spoolDepth,
|
|
123
141
|
heldCount,
|
|
142
|
+
heldWithNoLocalEvidence,
|
|
124
143
|
deadLetterCount,
|
|
125
144
|
unattributedEvents,
|
|
126
145
|
outboxDepth: outbox.depth,
|
|
@@ -132,6 +151,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
132
151
|
tokenSource: auth.tokenSource,
|
|
133
152
|
actorKind: auth.actorKind,
|
|
134
153
|
presenceRefusal,
|
|
154
|
+
presenceDroppedCompanies,
|
|
135
155
|
emitMode,
|
|
136
156
|
subscribed,
|
|
137
157
|
emitRetryDepth: emitRetry,
|
|
@@ -157,7 +177,7 @@ export function formatDaemonDoctor(report) {
|
|
|
157
177
|
if (report.emitMode === "legacy") {
|
|
158
178
|
// Legacy emit path only: the spool/held/outbox queues do not exist in
|
|
159
179
|
// receive-only (direct) mode.
|
|
160
|
-
lines.push(`spool depth: ${report.spoolDepth}`, `held: ${report.heldCount}`, `outbox depth: ${report.outboxDepth}`);
|
|
180
|
+
lines.push(`spool depth: ${report.spoolDepth}`, `held: ${report.heldCount}`, `held with no local evidence: ${report.heldWithNoLocalEvidence}`, `outbox depth: ${report.outboxDepth}`);
|
|
161
181
|
}
|
|
162
182
|
lines.push(`dead-letter: ${report.deadLetterCount}`, `unattributed events: ${report.unattributedEvents.total} ` +
|
|
163
183
|
`(spool=${report.unattributedEvents.spool} held=${report.unattributedEvents.held} dead-letter=${report.unattributedEvents.deadLetter})`, `token source: ${report.tokenSource}`, `actor kind: ${report.actorKind}`);
|
|
@@ -174,6 +194,13 @@ export function formatDaemonDoctor(report) {
|
|
|
174
194
|
if (report.presenceRefusal) {
|
|
175
195
|
lines.push(`presence refusal: ${report.presenceRefusal.code} (HTTP ${report.presenceRefusal.status}); next retry at ${report.presenceRefusal.nextRetryAt}`);
|
|
176
196
|
}
|
|
197
|
+
if (report.presenceDroppedCompanies.length > 0) {
|
|
198
|
+
lines.push(`presence not published for ${report.presenceDroppedCompanies.length} companies: ` +
|
|
199
|
+
report.presenceDroppedCompanies
|
|
200
|
+
.map((company) => `${company.companyUid} (${company.reason})`)
|
|
201
|
+
.join(", ") +
|
|
202
|
+
`; presence credential holds ${report.companiesOnline.length} companies; these were not included`);
|
|
203
|
+
}
|
|
177
204
|
if (report.unhealthy) {
|
|
178
205
|
lines.push(`health: UNHEALTHY`);
|
|
179
206
|
for (const reason of report.unhealthyReasons) {
|
|
@@ -12,12 +12,14 @@ export type { DaemonStateFile, PresenceRefusalState } from "./state.js";
|
|
|
12
12
|
export { appendDaemonLog, daemonLogLine, ensureDaemonLog, resolveDaemonAssetDir, rotateDaemonLogIfNeeded, } from "./log.js";
|
|
13
13
|
export { BOARD_REFRESH_INTERVAL_MS, createVaultBoardReader, formatBoardMarkdown, refreshBoundSessionBoards, writeBoardMarkdown, } from "./board-refresh.js";
|
|
14
14
|
export type { BoardReader, BoardRefreshDeps, BoardStorySnapshot } from "./board-refresh.js";
|
|
15
|
-
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, uninstallDaemonService, } from "./install.js";
|
|
15
|
+
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, uninstallDaemonService, } from "./install.js";
|
|
16
16
|
export type { DaemonInstallPlatform, InstallPaths, InstallResult, } from "./install.js";
|
|
17
|
+
export { daemonConfigPath, readDaemonConfig, registerTrustedHqRoot, verifyHqRoot } from "./config.js";
|
|
18
|
+
export type { DaemonConfig } from "./config.js";
|
|
17
19
|
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatDaemonDoctor, } from "./doctor.js";
|
|
18
20
|
export type { DaemonDoctorDeps, DaemonDoctorReport } from "./doctor.js";
|
|
19
21
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
20
|
-
export { FLUSH_INTERVAL_MS, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
22
|
+
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
21
23
|
export type { DaemonHandle, DaemonRunDeps } from "./run.js";
|
|
22
24
|
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|
|
23
25
|
export type { DiscoveredTranscript, DiscoverScanMeta, DiscoverTranscriptsResult, TranscriptDiscoverCache, TranscriptFs, TranscriptKind, TranscriptWatchDeps, TranscriptWatchTickResult, } from "./transcript-watch.js";
|
|
@@ -6,9 +6,10 @@ export { CLOSE_LOG_ALWAYS_COUNT, CLOSE_LOG_INTERVAL_MS, CONNECT_STABLE_GRACE_MS,
|
|
|
6
6
|
export { defaultDaemonState, patchDaemonState, readDaemonState, writeDaemonState, } from "./state.js";
|
|
7
7
|
export { appendDaemonLog, daemonLogLine, ensureDaemonLog, resolveDaemonAssetDir, rotateDaemonLogIfNeeded, } from "./log.js";
|
|
8
8
|
export { BOARD_REFRESH_INTERVAL_MS, createVaultBoardReader, formatBoardMarkdown, refreshBoundSessionBoards, writeBoardMarkdown, } from "./board-refresh.js";
|
|
9
|
-
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, uninstallDaemonService, } from "./install.js";
|
|
9
|
+
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, uninstallDaemonService, } from "./install.js";
|
|
10
|
+
export { daemonConfigPath, readDaemonConfig, registerTrustedHqRoot, verifyHqRoot } from "./config.js";
|
|
10
11
|
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatDaemonDoctor, } from "./doctor.js";
|
|
11
12
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
12
|
-
export { FLUSH_INTERVAL_MS, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
13
|
+
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
13
14
|
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|
|
14
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -23,6 +23,8 @@ export interface InstallPaths {
|
|
|
23
23
|
systemdUnitPath: string;
|
|
24
24
|
/** When true, render the root/ec2-user system unit instead of user unit. */
|
|
25
25
|
systemUser?: string;
|
|
26
|
+
/** Verified HQ root to persist and pass to the installed service. */
|
|
27
|
+
hqRoot?: string;
|
|
26
28
|
}
|
|
27
29
|
export interface InstallResult {
|
|
28
30
|
platform: DaemonInstallPlatform;
|
|
@@ -33,6 +35,26 @@ export interface InstallResult {
|
|
|
33
35
|
running?: boolean;
|
|
34
36
|
message: string;
|
|
35
37
|
}
|
|
38
|
+
/** Best-effort restart result used after an in-place CLI upgrade. */
|
|
39
|
+
export interface RestartDaemonResult {
|
|
40
|
+
attempted: boolean;
|
|
41
|
+
restarted: boolean;
|
|
42
|
+
message?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Restart the installed user daemon after a CLI upgrade. This is deliberately
|
|
46
|
+
* best-effort: an update must remain successful if a user has no service,
|
|
47
|
+
* launchctl/systemd is unavailable, or the user manager is not running.
|
|
48
|
+
*/
|
|
49
|
+
export declare function restartDaemonServiceIfInstalled(opts?: {
|
|
50
|
+
platform?: DaemonInstallPlatform;
|
|
51
|
+
home?: string;
|
|
52
|
+
uid?: number;
|
|
53
|
+
existsSync?: typeof fs.existsSync;
|
|
54
|
+
run?: (command: string, args: string[]) => {
|
|
55
|
+
status: number | null;
|
|
56
|
+
};
|
|
57
|
+
}): RestartDaemonResult;
|
|
36
58
|
export declare function detectPlatform(platform?: NodeJS.Platform): DaemonInstallPlatform;
|
|
37
59
|
/**
|
|
38
60
|
* HQ updates moved the CLI from toolchain/node/bin/hq to
|
|
@@ -53,6 +75,7 @@ export declare function buildInstallPaths(opts: {
|
|
|
53
75
|
env?: NodeJS.ProcessEnv;
|
|
54
76
|
argv?: string[];
|
|
55
77
|
execPath?: string;
|
|
78
|
+
hqRoot?: string;
|
|
56
79
|
}): InstallPaths;
|
|
57
80
|
export declare function renderLaunchdPlist(paths: InstallPaths): string;
|
|
58
81
|
export declare function renderSystemdUserUnit(paths: InstallPaths): string;
|
|
@@ -70,6 +93,10 @@ export declare function installDaemonService(opts: {
|
|
|
70
93
|
mkdirSync?: typeof fs.mkdirSync;
|
|
71
94
|
/** Optional: actually load the service (disabled in tests). */
|
|
72
95
|
activate?: (platform: DaemonInstallPlatform, dest: string) => void;
|
|
96
|
+
/** Reload a rewritten user unit so a legacy Restart=on-failure unit migrates immediately. */
|
|
97
|
+
reloadSystemd?: () => {
|
|
98
|
+
status: number | null;
|
|
99
|
+
};
|
|
73
100
|
}): InstallResult;
|
|
74
101
|
export declare function uninstallDaemonService(opts: {
|
|
75
102
|
platform?: DaemonInstallPlatform;
|
|
@@ -12,8 +12,54 @@
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
15
16
|
import { resolveDaemonAssetDir } from "./log.js";
|
|
16
17
|
import { LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, daemonDir, daemonLogPath, } from "./paths.js";
|
|
18
|
+
import { registerTrustedHqRoot } from "./config.js";
|
|
19
|
+
/**
|
|
20
|
+
* Restart the installed user daemon after a CLI upgrade. This is deliberately
|
|
21
|
+
* best-effort: an update must remain successful if a user has no service,
|
|
22
|
+
* launchctl/systemd is unavailable, or the user manager is not running.
|
|
23
|
+
*/
|
|
24
|
+
export function restartDaemonServiceIfInstalled(opts = {}) {
|
|
25
|
+
const platform = opts.platform ?? detectPlatform();
|
|
26
|
+
const home = opts.home ?? os.homedir();
|
|
27
|
+
const existsSync = opts.existsSync ?? fs.existsSync;
|
|
28
|
+
const run = opts.run ?? ((command, args) => spawnSync(command, args, { stdio: "ignore" }));
|
|
29
|
+
try {
|
|
30
|
+
if (platform === "darwin") {
|
|
31
|
+
const plist = path.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
32
|
+
if (!existsSync(plist))
|
|
33
|
+
return { attempted: false, restarted: false };
|
|
34
|
+
const uid = opts.uid ?? process.getuid?.();
|
|
35
|
+
if (typeof uid !== "number")
|
|
36
|
+
return { attempted: false, restarted: false };
|
|
37
|
+
const result = run("launchctl", ["kickstart", "-k", `gui/${uid}/${LAUNCHD_LABEL}`]);
|
|
38
|
+
return { attempted: true, restarted: result.status === 0 };
|
|
39
|
+
}
|
|
40
|
+
if (platform === "linux") {
|
|
41
|
+
const unit = path.join(home, ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
|
|
42
|
+
if (existsSync(unit)) {
|
|
43
|
+
const result = run("systemctl", ["--user", "restart", SYSTEMD_UNIT_NAME]);
|
|
44
|
+
return { attempted: true, restarted: result.status === 0 };
|
|
45
|
+
}
|
|
46
|
+
const systemUnit = path.join("/etc", "systemd", "system", SYSTEMD_UNIT_NAME);
|
|
47
|
+
if (existsSync(systemUnit)) {
|
|
48
|
+
return {
|
|
49
|
+
attempted: true,
|
|
50
|
+
restarted: false,
|
|
51
|
+
message: "system unit detected; restart requires the box's service manager",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { attempted: false, restarted: false };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// The caller has already completed the update; service restart failure is
|
|
59
|
+
// intentionally non-fatal and will be retried by normal service recovery.
|
|
60
|
+
}
|
|
61
|
+
return { attempted: false, restarted: false };
|
|
62
|
+
}
|
|
17
63
|
export function detectPlatform(platform = process.platform) {
|
|
18
64
|
if (platform === "darwin")
|
|
19
65
|
return "darwin";
|
|
@@ -83,6 +129,7 @@ export function buildInstallPaths(opts) {
|
|
|
83
129
|
systemdUserDir: path.join(home, ".config", "systemd", "user"),
|
|
84
130
|
systemdUnitPath: path.join(home, ".config", "systemd", "user", SYSTEMD_UNIT_NAME),
|
|
85
131
|
systemUser: opts.systemUser,
|
|
132
|
+
hqRoot: opts.hqRoot,
|
|
86
133
|
};
|
|
87
134
|
}
|
|
88
135
|
export function renderLaunchdPlist(paths) {
|
|
@@ -116,6 +163,7 @@ export function renderLaunchdPlist(paths) {
|
|
|
116
163
|
<string>${escapeXml(paths.home)}</string>
|
|
117
164
|
<key>PATH</key>
|
|
118
165
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
166
|
+
${paths.hqRoot ? `<key>HQ_ROOT</key>\n <string>${escapeXml(paths.hqRoot)}</string>` : ""}
|
|
119
167
|
</dict>
|
|
120
168
|
</dict>
|
|
121
169
|
</plist>
|
|
@@ -131,11 +179,11 @@ Wants=network-online.target
|
|
|
131
179
|
Type=simple
|
|
132
180
|
ExecStart=${shellQuote(paths.nodeBinary)} ${shellQuote(paths.hqBinary)} mesh daemon run
|
|
133
181
|
WorkingDirectory=${paths.daemonDir}
|
|
134
|
-
Restart=
|
|
182
|
+
Restart=always
|
|
135
183
|
RestartSec=5
|
|
136
184
|
Environment=HOME=${paths.home}
|
|
137
185
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
138
|
-
StandardOutput=append:${paths.logPath}
|
|
186
|
+
${paths.hqRoot ? `Environment=HQ_ROOT=${shellQuote(paths.hqRoot)}\n` : ""}StandardOutput=append:${paths.logPath}
|
|
139
187
|
StandardError=append:${paths.logPath}
|
|
140
188
|
|
|
141
189
|
[Install]
|
|
@@ -163,13 +211,13 @@ User=${user}
|
|
|
163
211
|
Group=${user}
|
|
164
212
|
ExecStart=${shellQuote(paths.nodeBinary)} ${shellQuote(paths.hqBinary)} mesh daemon run
|
|
165
213
|
WorkingDirectory=/home/${user}/.hq/work-mesh/daemon
|
|
166
|
-
Restart=
|
|
214
|
+
Restart=always
|
|
167
215
|
RestartSec=5
|
|
168
216
|
Environment=HOME=/home/${user}
|
|
169
217
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
170
218
|
Environment=HQ_MACHINE_CREDS_FILE=/home/${user}/.hq-agent/machine-creds.json
|
|
171
219
|
Environment=HQ_AGENT_IDENTITY_FILE=/var/lib/hq-agent/identity.json
|
|
172
|
-
StandardOutput=append:/home/${user}/.hq/work-mesh/daemon/log
|
|
220
|
+
${paths.hqRoot ? `Environment=HQ_ROOT=${shellQuote(paths.hqRoot)}\n` : ""}StandardOutput=append:/home/${user}/.hq/work-mesh/daemon/log
|
|
173
221
|
StandardError=append:/home/${user}/.hq/work-mesh/daemon/log
|
|
174
222
|
|
|
175
223
|
[Install]
|
|
@@ -195,6 +243,7 @@ export function installDaemonService(opts) {
|
|
|
195
243
|
const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
|
|
196
244
|
const mkdirSync = opts.mkdirSync ?? fs.mkdirSync;
|
|
197
245
|
mkdirSync(paths.daemonDir, { recursive: true, mode: 0o700 });
|
|
246
|
+
registerTrustedHqRoot(paths.daemonDir, paths.hqRoot);
|
|
198
247
|
if (platform === "darwin") {
|
|
199
248
|
const rendered = renderLaunchdPlist(paths);
|
|
200
249
|
mkdirSync(paths.launchAgentsDir, { recursive: true });
|
|
@@ -232,6 +281,7 @@ export function installDaemonService(opts) {
|
|
|
232
281
|
const rendered = renderSystemdUserUnit(paths);
|
|
233
282
|
mkdirSync(paths.systemdUserDir, { recursive: true });
|
|
234
283
|
writeFileSync(paths.systemdUnitPath, rendered, { mode: 0o600 });
|
|
284
|
+
(opts.reloadSystemd ?? (() => spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" })))();
|
|
235
285
|
opts.activate?.(platform, paths.systemdUnitPath);
|
|
236
286
|
return {
|
|
237
287
|
platform,
|
|
@@ -19,6 +19,8 @@ export declare const SPOOL_DEBOUNCE_MS = 2000;
|
|
|
19
19
|
export declare const FLUSH_INTERVAL_MS = 10000;
|
|
20
20
|
/** Keep startup recovery bounded: the next daemon start continues the backlog. */
|
|
21
21
|
export declare const HELD_BACKFILL_STARTUP_BATCH_SIZE = 100;
|
|
22
|
+
/** Non-zero exit reserved for CLI version drift so legacy Restart=on-failure units restart. */
|
|
23
|
+
export declare const DAEMON_VERSION_DRIFT_EXIT_CODE = 75;
|
|
22
24
|
/**
|
|
23
25
|
* Only producer appends to spool.jsonl should wake the watcher. Every other
|
|
24
26
|
* file in the work-mesh root (held.jsonl, spool.<ts>.claimed, held.<ts>.claimed,
|
|
@@ -80,6 +82,10 @@ export interface DaemonRunDeps {
|
|
|
80
82
|
log?: (dir: string, line: string) => void;
|
|
81
83
|
/** Stop after this many ms (tests). */
|
|
82
84
|
runForMs?: number;
|
|
85
|
+
/** CLI version loaded by this daemon. Defaults to this package's version. */
|
|
86
|
+
daemonVersion?: string;
|
|
87
|
+
/** Read the currently installed CLI version behind the launch entrypoint. */
|
|
88
|
+
readInstalledCliVersion?: () => string | null;
|
|
83
89
|
random?: () => number;
|
|
84
90
|
now?: () => Date;
|
|
85
91
|
}
|