@indigoai-us/hq-cli 5.124.0 → 5.126.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 +149 -0
- package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
- package/assets/bot-workers/setup/skills/your-business.md +270 -0
- package/assets/bot-workers/setup/worker.yaml +189 -58
- package/dist/command-catalog.generated.d.ts +414 -5
- package/dist/command-catalog.generated.js +531 -5
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +75 -0
- package/dist/commands/lanes.js +2032 -0
- package/dist/commands/login.js +49 -6
- package/dist/commands/mesh.js +133 -28
- package/dist/commands/onboard-create-identity.d.ts +26 -0
- package/dist/commands/onboard-create-identity.js +23 -0
- package/dist/commands/onboard.js +13 -4
- package/dist/lib/bot/progress.d.ts +6 -0
- package/dist/lib/bot/progress.js +16 -0
- package/dist/lib/bot/runtime/grok.d.ts +30 -7
- package/dist/lib/bot/runtime/grok.js +86 -8
- package/dist/lib/bot/runtime/index.js +15 -8
- package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
- package/dist/lib/bot/runtime/messages-stream.js +49 -3
- package/dist/lib/doctor/checks/work-context.js +4 -2
- package/dist/lib/lanes/accounts.d.ts +43 -0
- package/dist/lib/lanes/accounts.js +128 -0
- package/dist/lib/lanes/config.d.ts +56 -0
- package/dist/lib/lanes/config.js +351 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/index.d.ts +19 -0
- package/dist/lib/lanes/index.js +12 -0
- package/dist/lib/lanes/liveness.d.ts +89 -0
- package/dist/lib/lanes/liveness.js +273 -0
- package/dist/lib/lanes/registry.d.ts +103 -0
- package/dist/lib/lanes/registry.js +247 -0
- package/dist/lib/lanes/store.d.ts +64 -0
- package/dist/lib/lanes/store.js +627 -0
- package/dist/lib/lanes/throttle.d.ts +89 -0
- package/dist/lib/lanes/throttle.js +297 -0
- package/dist/lib/lanes/types.d.ts +193 -0
- package/dist/lib/lanes/types.js +217 -0
- package/dist/lib/mesh/live/backfill-held.d.ts +2 -0
- package/dist/lib/mesh/live/backfill-held.js +8 -4
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +6 -0
- package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/config.js +75 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +21 -1
- package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
- package/dist/lib/mesh/live/daemon/index.js +3 -3
- package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
- package/dist/lib/mesh/live/daemon/install.js +444 -2
- package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/paths.js +2 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/run.js +129 -10
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/flush.d.ts +3 -0
- package/dist/lib/mesh/live/flush.js +11 -0
- package/dist/lib/scan-packages/index.js +17 -12
- package/dist/lib/work-context/company.d.ts +8 -0
- package/dist/lib/work-context/company.js +62 -18
- package/dist/lib/work-context/config.d.ts +55 -0
- package/dist/lib/work-context/config.js +333 -67
- package/dist/lib/work-context/contract.d.ts +2 -0
- package/dist/lib/work-context/default-company.d.ts +43 -0
- package/dist/lib/work-context/default-company.js +83 -0
- package/dist/lib/work-context/outbox.d.ts +5 -1
- package/dist/lib/work-context/outbox.js +5 -0
- package/dist/lib/work-context/reconcile.d.ts +4 -0
- package/dist/lib/work-context/reconcile.js +86 -9
- package/dist/lib/work-context/state.d.ts +2 -0
- package/dist/lib/work-context/state.js +14 -0
- package/dist/lib/work-context/windows-acl.d.ts +123 -0
- package/dist/lib/work-context/windows-acl.js +425 -0
- package/dist/utils/pack-contributions.js +5 -4
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane record, question routing record, envelope, and config types (spec §3,
|
|
3
|
+
* §4, §5.5, §6.2). Finite sets are string-literal unions plus `const` arrays
|
|
4
|
+
* so the command layer can validate flags explicitly.
|
|
5
|
+
*/
|
|
6
|
+
import { LanesError, LaneSchemaError } from "./errors.js";
|
|
7
|
+
export const LANE_SCHEMA_VERSION = "hq-lane.v1";
|
|
8
|
+
export const QUESTION_SCHEMA_VERSION = "hq-lane-question.v1";
|
|
9
|
+
export const ENVELOPE_SCHEMA_VERSION = "hq-lane-envelope.v1";
|
|
10
|
+
export const CONFIG_SCHEMA_VERSION = "hq-lanes-config.v1";
|
|
11
|
+
export const LANE_STATES = [
|
|
12
|
+
"queued",
|
|
13
|
+
"running",
|
|
14
|
+
"awaiting_input",
|
|
15
|
+
"blocked",
|
|
16
|
+
"done",
|
|
17
|
+
"failed",
|
|
18
|
+
"interrupted",
|
|
19
|
+
"cancelled",
|
|
20
|
+
"killed",
|
|
21
|
+
];
|
|
22
|
+
/** Exact terminal set from spec §6.7. `awaiting_input` and `blocked` are not terminal. */
|
|
23
|
+
export const TERMINAL_LANE_STATES = [
|
|
24
|
+
"done",
|
|
25
|
+
"failed",
|
|
26
|
+
"interrupted",
|
|
27
|
+
"cancelled",
|
|
28
|
+
"killed",
|
|
29
|
+
];
|
|
30
|
+
export const LANE_PROVIDERS = ["claude", "codex", "grok"];
|
|
31
|
+
export const QUEUED_REASONS = ["cap", "queue_position", "throttle"];
|
|
32
|
+
export const HOST_KINDS = ["local", "outpost"];
|
|
33
|
+
export const SENIOR_KINDS = ["session", "lane", "user"];
|
|
34
|
+
export const ENVELOPE_DECISIONS = ["done", "ask", "blocked"];
|
|
35
|
+
export const STORY_STATUSES = [
|
|
36
|
+
"queued",
|
|
37
|
+
"in_progress",
|
|
38
|
+
"review",
|
|
39
|
+
"done",
|
|
40
|
+
];
|
|
41
|
+
export const QUESTION_STATUSES = [
|
|
42
|
+
"pending",
|
|
43
|
+
"answered",
|
|
44
|
+
"cancelled",
|
|
45
|
+
"expired",
|
|
46
|
+
];
|
|
47
|
+
export const RAISED_VIA = ["cli", "envelope"];
|
|
48
|
+
export const ROUTING_REASONS = [
|
|
49
|
+
"asked",
|
|
50
|
+
"escalated",
|
|
51
|
+
"audience_ended",
|
|
52
|
+
"reparented",
|
|
53
|
+
"unanswered_timeout",
|
|
54
|
+
"cancelled",
|
|
55
|
+
];
|
|
56
|
+
export const QUESTION_BODY_STORES = ["company", "personal"];
|
|
57
|
+
export const LIVENESS_STATES = ["live", "idle", "ended", "lost"];
|
|
58
|
+
export const HISTORY_CAP = 50;
|
|
59
|
+
const TERMINAL_SET = new Set(TERMINAL_LANE_STATES);
|
|
60
|
+
export function isLaneState(value) {
|
|
61
|
+
return typeof value === "string" && LANE_STATES.includes(value);
|
|
62
|
+
}
|
|
63
|
+
export function isTerminalLaneState(value) {
|
|
64
|
+
return typeof value === "string" && TERMINAL_SET.has(value);
|
|
65
|
+
}
|
|
66
|
+
export function isLaneProvider(value) {
|
|
67
|
+
return (typeof value === "string" &&
|
|
68
|
+
LANE_PROVIDERS.includes(value));
|
|
69
|
+
}
|
|
70
|
+
export function isSeniorKind(value) {
|
|
71
|
+
return (typeof value === "string" &&
|
|
72
|
+
SENIOR_KINDS.includes(value));
|
|
73
|
+
}
|
|
74
|
+
export function isHostKind(value) {
|
|
75
|
+
return (typeof value === "string" &&
|
|
76
|
+
HOST_KINDS.includes(value));
|
|
77
|
+
}
|
|
78
|
+
export function isEnvelopeDecision(value) {
|
|
79
|
+
return (typeof value === "string" &&
|
|
80
|
+
ENVELOPE_DECISIONS.includes(value));
|
|
81
|
+
}
|
|
82
|
+
export function isQuestionStatus(value) {
|
|
83
|
+
return (typeof value === "string" &&
|
|
84
|
+
QUESTION_STATUSES.includes(value));
|
|
85
|
+
}
|
|
86
|
+
export function isLivenessState(value) {
|
|
87
|
+
return (typeof value === "string" &&
|
|
88
|
+
LIVENESS_STATES.includes(value));
|
|
89
|
+
}
|
|
90
|
+
export function parseLaneProvider(value) {
|
|
91
|
+
if (isLaneProvider(value))
|
|
92
|
+
return value;
|
|
93
|
+
throw new LanesError("unknown_provider", `Unknown provider ${JSON.stringify(value)}. Accepted: ${LANE_PROVIDERS.join(", ")}`, { accepted: [...LANE_PROVIDERS], value });
|
|
94
|
+
}
|
|
95
|
+
export function parseLaneState(value) {
|
|
96
|
+
if (isLaneState(value))
|
|
97
|
+
return value;
|
|
98
|
+
throw new LanesError("unknown_state", `Unknown lane state ${JSON.stringify(value)}. Accepted: ${LANE_STATES.join(", ")}`, { accepted: [...LANE_STATES], value });
|
|
99
|
+
}
|
|
100
|
+
export function parseQuestionStatus(value) {
|
|
101
|
+
if (isQuestionStatus(value))
|
|
102
|
+
return value;
|
|
103
|
+
throw new LanesError("unknown_status", `Unknown question status ${JSON.stringify(value)}. Accepted: ${QUESTION_STATUSES.join(", ")}`, { accepted: [...QUESTION_STATUSES], value });
|
|
104
|
+
}
|
|
105
|
+
export function parseLivenessState(value) {
|
|
106
|
+
if (isLivenessState(value))
|
|
107
|
+
return value;
|
|
108
|
+
throw new LanesError("unknown_state", `Unknown session liveness state ${JSON.stringify(value)}. Accepted: ${LIVENESS_STATES.join(", ")}`, { accepted: [...LIVENESS_STATES], value });
|
|
109
|
+
}
|
|
110
|
+
export function parseSeniorKind(value) {
|
|
111
|
+
if (isSeniorKind(value))
|
|
112
|
+
return value;
|
|
113
|
+
throw new LanesError("unknown_senior_kind", `Unknown senior kind ${JSON.stringify(value)}. Accepted: ${SENIOR_KINDS.join(", ")}`, { accepted: [...SENIOR_KINDS], value });
|
|
114
|
+
}
|
|
115
|
+
function isPlainObject(value) {
|
|
116
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
function requireString(value, field) {
|
|
119
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
120
|
+
throw new LaneSchemaError(`Expected non-empty string for ${field}`, {
|
|
121
|
+
field,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
export function parseMajorSchema(schemaVersion, expectedPrefix, expectedMajor) {
|
|
127
|
+
const match = new RegExp(`^${expectedPrefix}\\.v(\\d+)$`).exec(schemaVersion);
|
|
128
|
+
if (!match) {
|
|
129
|
+
throw new LaneSchemaError(`Unknown schema_version ${JSON.stringify(schemaVersion)} (expected ${expectedPrefix}.v${expectedMajor})`, { schema_version: schemaVersion, expected: `${expectedPrefix}.v${expectedMajor}` });
|
|
130
|
+
}
|
|
131
|
+
const major = Number(match[1]);
|
|
132
|
+
if (major !== expectedMajor) {
|
|
133
|
+
throw new LaneSchemaError(`Unsupported ${expectedPrefix} major version ${major} (found ${schemaVersion})`, { schema_version: schemaVersion, major });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function parseLaneEnvelope(raw) {
|
|
137
|
+
if (!isPlainObject(raw)) {
|
|
138
|
+
throw new LaneSchemaError("Lane envelope must be a JSON object");
|
|
139
|
+
}
|
|
140
|
+
const extra = Object.keys(raw).filter((key) => ![
|
|
141
|
+
"schema_version",
|
|
142
|
+
"decision",
|
|
143
|
+
"summary",
|
|
144
|
+
"questions",
|
|
145
|
+
"story_status",
|
|
146
|
+
"artifacts",
|
|
147
|
+
"blocked_reason",
|
|
148
|
+
].includes(key));
|
|
149
|
+
if (extra.length > 0) {
|
|
150
|
+
throw new LaneSchemaError(`Lane envelope rejects unknown properties: ${extra.join(", ")}`, { extra });
|
|
151
|
+
}
|
|
152
|
+
const schema_version = requireString(raw.schema_version, "schema_version");
|
|
153
|
+
parseMajorSchema(schema_version, "hq-lane-envelope", 1);
|
|
154
|
+
if (!isEnvelopeDecision(raw.decision)) {
|
|
155
|
+
throw new LaneSchemaError(`Unknown envelope decision ${JSON.stringify(raw.decision)}. Accepted: ${ENVELOPE_DECISIONS.join(", ")}`, { accepted: [...ENVELOPE_DECISIONS], value: raw.decision });
|
|
156
|
+
}
|
|
157
|
+
const summary = requireString(raw.summary, "summary");
|
|
158
|
+
if (typeof raw.story_status !== "string" ||
|
|
159
|
+
!STORY_STATUSES.includes(raw.story_status)) {
|
|
160
|
+
throw new LaneSchemaError(`Unknown story_status ${JSON.stringify(raw.story_status)}. Accepted: ${STORY_STATUSES.join(", ")}`, { accepted: [...STORY_STATUSES], value: raw.story_status });
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(raw.artifacts)) {
|
|
163
|
+
throw new LaneSchemaError("Envelope artifacts must be an array");
|
|
164
|
+
}
|
|
165
|
+
const questions = raw.questions;
|
|
166
|
+
if (raw.decision === "ask") {
|
|
167
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
168
|
+
throw new LaneSchemaError('questions must be non-empty when decision is "ask"');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (questions !== undefined) {
|
|
172
|
+
if (!Array.isArray(questions) || questions.length !== 0) {
|
|
173
|
+
throw new LaneSchemaError('questions must be omitted or empty unless decision is "ask"');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (raw.decision === "blocked") {
|
|
177
|
+
if (typeof raw.blocked_reason !== "string" || raw.blocked_reason.length === 0) {
|
|
178
|
+
throw new LaneSchemaError('blocked_reason is required when decision is "blocked"');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (raw.blocked_reason !== undefined) {
|
|
182
|
+
throw new LaneSchemaError('blocked_reason is present exactly when decision is "blocked"');
|
|
183
|
+
}
|
|
184
|
+
const envelope = {
|
|
185
|
+
schema_version: ENVELOPE_SCHEMA_VERSION,
|
|
186
|
+
decision: raw.decision,
|
|
187
|
+
summary,
|
|
188
|
+
story_status: raw.story_status,
|
|
189
|
+
artifacts: raw.artifacts,
|
|
190
|
+
};
|
|
191
|
+
if (Array.isArray(questions) && questions.length > 0) {
|
|
192
|
+
envelope.questions = questions.map((item, index) => {
|
|
193
|
+
if (!isPlainObject(item)) {
|
|
194
|
+
throw new LaneSchemaError(`questions[${index}] must be an object`);
|
|
195
|
+
}
|
|
196
|
+
const options = item.options;
|
|
197
|
+
if (!Array.isArray(options) ||
|
|
198
|
+
options.some((opt) => typeof opt !== "string")) {
|
|
199
|
+
throw new LaneSchemaError(`questions[${index}].options must be string[]`);
|
|
200
|
+
}
|
|
201
|
+
const parsed = {
|
|
202
|
+
question: requireString(item.question, `questions[${index}].question`),
|
|
203
|
+
context: requireString(item.context, `questions[${index}].context`),
|
|
204
|
+
options,
|
|
205
|
+
};
|
|
206
|
+
if (item.recommended !== undefined) {
|
|
207
|
+
parsed.recommended = requireString(item.recommended, `questions[${index}].recommended`);
|
|
208
|
+
}
|
|
209
|
+
return parsed;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (typeof raw.blocked_reason === "string") {
|
|
213
|
+
envelope.blocked_reason = raw.blocked_reason;
|
|
214
|
+
}
|
|
215
|
+
return envelope;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -111,6 +111,8 @@ export interface BackfillHeldDeps {
|
|
|
111
111
|
* meta) in BOTH the dry-run predictor and the write path, so the two agree.
|
|
112
112
|
*/
|
|
113
113
|
remoteOwnerHint?: string;
|
|
114
|
+
/** Historical device-default repair is opt-in; false keeps no-evidence sessions held. */
|
|
115
|
+
useDeviceDefault?: boolean;
|
|
114
116
|
/** Env passed to the dry-run resolver (tests). Default process.env. */
|
|
115
117
|
env?: NodeJS.ProcessEnv;
|
|
116
118
|
/**
|
|
@@ -231,7 +231,7 @@ export function observationFromHeldRef(ref, contractVersion, clientOperationId)
|
|
|
231
231
|
* the write run can still under-count; the gross all-or-nothing mismatch is
|
|
232
232
|
* gone because both paths key off this same resolver + evidence.
|
|
233
233
|
*/
|
|
234
|
-
function defaultPredictCompany(ref, workContextRoot, remoteOwnerHint, env) {
|
|
234
|
+
function defaultPredictCompany(ref, workContextRoot, remoteOwnerHint, env, useDeviceDefault) {
|
|
235
235
|
// This predictor is called only with a ref already rebuilt from trusted local
|
|
236
236
|
// evidence. Do not let raw held-event cwd/hqRoot values re-enter resolution.
|
|
237
237
|
const remoteOwnerSlug = remoteOwnerHint?.trim();
|
|
@@ -241,6 +241,7 @@ function defaultPredictCompany(ref, workContextRoot, remoteOwnerHint, env) {
|
|
|
241
241
|
env,
|
|
242
242
|
trusted: ref.companySlug ? { companySlug: ref.companySlug } : undefined,
|
|
243
243
|
remoteOwnerSlug,
|
|
244
|
+
allowDeviceDefault: useDeviceDefault,
|
|
244
245
|
});
|
|
245
246
|
return resolution.status === "resolved";
|
|
246
247
|
}
|
|
@@ -385,7 +386,7 @@ export async function backfillHeldSessions(deps) {
|
|
|
385
386
|
const env = deps.env ?? process.env;
|
|
386
387
|
const newOperationId = deps.newOperationId ?? (() => globalThis.crypto.randomUUID());
|
|
387
388
|
const predictCompany = deps.predictCompany ??
|
|
388
|
-
((ref) => defaultPredictCompany(ref, deps.workContextRoot, deps.remoteOwnerHint, env));
|
|
389
|
+
((ref) => defaultPredictCompany(ref, deps.workContextRoot, deps.remoteOwnerHint, env, deps.useDeviceDefault === true));
|
|
389
390
|
const sessions = readHeldSessions(deps.workMeshRoot);
|
|
390
391
|
// An acked register is authoritative local evidence. Older clients could
|
|
391
392
|
// lose its state projection on a later hook write; recover that projection
|
|
@@ -434,7 +435,10 @@ export async function backfillHeldSessions(deps) {
|
|
|
434
435
|
continue;
|
|
435
436
|
}
|
|
436
437
|
}
|
|
437
|
-
|
|
438
|
+
// Historical repair may use the explicitly opted-in device default only
|
|
439
|
+
// when there is no durable local company evidence. Never manufacture a
|
|
440
|
+
// slug from the held event itself.
|
|
441
|
+
if (!evidence.candidate?.slug && deps.useDeviceDefault !== true) {
|
|
438
442
|
result.unresolved += 1;
|
|
439
443
|
deps.log?.(`would stay unresolved ${sessionId} (${hasOnlyStaleEventEvidence(ref, state) ? "STALE_EVIDENCE" : "no local company evidence"})`);
|
|
440
444
|
continue;
|
|
@@ -442,7 +446,7 @@ export async function backfillHeldSessions(deps) {
|
|
|
442
446
|
const evidenceRef = {
|
|
443
447
|
sessionId,
|
|
444
448
|
harness,
|
|
445
|
-
companySlug: evidence.candidate
|
|
449
|
+
companySlug: evidence.candidate?.slug,
|
|
446
450
|
// Held-event fields are transport input, not trusted context. Preserve
|
|
447
451
|
// project/task only when the durable session state already owns them.
|
|
448
452
|
project: state?.projectId,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* authoritative in ProjectView because `hq mesh story` writes there.
|
|
7
7
|
* Metadata-only snapshot — never prompts or tokens.
|
|
8
8
|
*/
|
|
9
|
+
import type { SessionStateFile } from "../../../work-context/state.js";
|
|
9
10
|
export declare const BOARD_REFRESH_INTERVAL_MS = 60000;
|
|
10
11
|
export interface BoardStorySnapshot {
|
|
11
12
|
id: string;
|
|
@@ -26,6 +27,8 @@ export interface BoardRefreshDeps {
|
|
|
26
27
|
/** Per-session last refresh timestamps (ms). */
|
|
27
28
|
lastRefresh?: Map<string, number>;
|
|
28
29
|
intervalMs?: number;
|
|
30
|
+
/** Authorizes a device-default-derived session before its tenant read. */
|
|
31
|
+
authorizeDeviceDefaultTenant?: (sessionId: string, state: SessionStateFile) => Promise<boolean>;
|
|
29
32
|
}
|
|
30
33
|
export declare function formatBoardMarkdown(input: {
|
|
31
34
|
sessionId: string;
|
|
@@ -92,6 +92,12 @@ export async function refreshBoundSessionBoards(deps) {
|
|
|
92
92
|
skipped += 1;
|
|
93
93
|
continue;
|
|
94
94
|
}
|
|
95
|
+
if (state.companyConfidence === "device_default" &&
|
|
96
|
+
deps.authorizeDeviceDefaultTenant &&
|
|
97
|
+
!(await deps.authorizeDeviceDefaultTenant(sessionId, state))) {
|
|
98
|
+
skipped += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
95
101
|
try {
|
|
96
102
|
const board = await deps.readBoard(state.companyUid, state.projectId);
|
|
97
103
|
if (!board) {
|
|
@@ -3,6 +3,33 @@ export interface DaemonConfig {
|
|
|
3
3
|
}
|
|
4
4
|
export declare function daemonConfigPath(daemonDirectory: string): string;
|
|
5
5
|
export declare function verifyHqRoot(candidate: string | undefined): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* A directory the daemon may be installed for: it must carry
|
|
8
|
+
* companies/manifest.yaml (what verifyHqRoot demands at runtime) and one of
|
|
9
|
+
* the tree markers utils/hq-roots.ts accepts (core/, .claude/, personal/).
|
|
10
|
+
* A bare .claude/ directory — every repo with Claude settings has one — is
|
|
11
|
+
* not an HQ root and must never be persisted or passed as --hq-root.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isInstallableHqRoot(dir: string): boolean;
|
|
14
|
+
/** Nearest ancestor of startDir (inclusive) that is an installable HQ root, realpath'd; null if none. */
|
|
15
|
+
export declare function findAncestorHqRoot(startDir: string): string | null;
|
|
16
|
+
export interface InstallHqRootResolution {
|
|
17
|
+
root: string;
|
|
18
|
+
source: "flag" | "env" | "ancestor";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the HQ root `hq mesh daemon install` records and passes to the
|
|
22
|
+
* service. Precedence: --hq-root, then HQ_ROOT, then the nearest ancestor of
|
|
23
|
+
* cwd that is an installable HQ root. An explicit value that is not an HQ
|
|
24
|
+
* root is an error rather than a silent fallback, and with nothing
|
|
25
|
+
* qualifying the install is refused: an unverified root would be persisted
|
|
26
|
+
* and trusted by the daemon for company attribution.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveInstallHqRoot(opts?: {
|
|
29
|
+
explicit?: string;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
}): InstallHqRootResolution;
|
|
6
33
|
export declare function readDaemonConfig(daemonDirectory: string): DaemonConfig;
|
|
7
34
|
/** Record a verified local HQ checkout without trusting a caller-supplied path at runtime. */
|
|
8
35
|
export declare function registerTrustedHqRoot(daemonDirectory: string, candidate: string | undefined): DaemonConfig;
|
|
@@ -16,6 +16,81 @@ export function verifyHqRoot(candidate) {
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A directory the daemon may be installed for: it must carry
|
|
21
|
+
* companies/manifest.yaml (what verifyHqRoot demands at runtime) and one of
|
|
22
|
+
* the tree markers utils/hq-roots.ts accepts (core/, .claude/, personal/).
|
|
23
|
+
* A bare .claude/ directory — every repo with Claude settings has one — is
|
|
24
|
+
* not an HQ root and must never be persisted or passed as --hq-root.
|
|
25
|
+
*/
|
|
26
|
+
export function isInstallableHqRoot(dir) {
|
|
27
|
+
if (verifyHqRoot(dir) === null)
|
|
28
|
+
return false;
|
|
29
|
+
return ["core", ".claude", "personal"].some((marker) => {
|
|
30
|
+
try {
|
|
31
|
+
return fs.statSync(path.join(dir, marker)).isDirectory();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Nearest ancestor of startDir (inclusive) that is an installable HQ root, realpath'd; null if none. */
|
|
39
|
+
export function findAncestorHqRoot(startDir) {
|
|
40
|
+
let dir;
|
|
41
|
+
try {
|
|
42
|
+
dir = fs.realpathSync(startDir);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
dir = path.resolve(startDir);
|
|
46
|
+
}
|
|
47
|
+
for (;;) {
|
|
48
|
+
if (isInstallableHqRoot(dir))
|
|
49
|
+
return fs.realpathSync(dir);
|
|
50
|
+
const parent = path.dirname(dir);
|
|
51
|
+
if (parent === dir)
|
|
52
|
+
return null;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the HQ root `hq mesh daemon install` records and passes to the
|
|
58
|
+
* service. Precedence: --hq-root, then HQ_ROOT, then the nearest ancestor of
|
|
59
|
+
* cwd that is an installable HQ root. An explicit value that is not an HQ
|
|
60
|
+
* root is an error rather than a silent fallback, and with nothing
|
|
61
|
+
* qualifying the install is refused: an unverified root would be persisted
|
|
62
|
+
* and trusted by the daemon for company attribution.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveInstallHqRoot(opts = {}) {
|
|
65
|
+
const env = opts.env ?? process.env;
|
|
66
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
67
|
+
const explicit = opts.explicit?.trim();
|
|
68
|
+
if (explicit) {
|
|
69
|
+
if (!isInstallableHqRoot(explicit)) {
|
|
70
|
+
throw expectedError(`--hq-root ${explicit} is not an HQ root (needs companies/manifest.yaml plus core/, .claude/, or personal/).`);
|
|
71
|
+
}
|
|
72
|
+
return { root: fs.realpathSync(explicit), source: "flag" };
|
|
73
|
+
}
|
|
74
|
+
const fromEnv = env.HQ_ROOT?.trim();
|
|
75
|
+
if (fromEnv) {
|
|
76
|
+
if (!isInstallableHqRoot(fromEnv)) {
|
|
77
|
+
throw expectedError(`HQ_ROOT=${fromEnv} is not an HQ root (needs companies/manifest.yaml plus core/, .claude/, or personal/). ` +
|
|
78
|
+
`Fix HQ_ROOT or pass --hq-root <path>.`);
|
|
79
|
+
}
|
|
80
|
+
return { root: fs.realpathSync(fromEnv), source: "env" };
|
|
81
|
+
}
|
|
82
|
+
const ancestor = findAncestorHqRoot(cwd);
|
|
83
|
+
if (!ancestor) {
|
|
84
|
+
throw expectedError(`No HQ root found above ${cwd} (a directory with companies/manifest.yaml plus core/, .claude/, or personal/). ` +
|
|
85
|
+
`Run from inside your HQ tree or pass --hq-root <path>.`);
|
|
86
|
+
}
|
|
87
|
+
return { root: ancestor, source: "ancestor" };
|
|
88
|
+
}
|
|
89
|
+
function expectedError(message) {
|
|
90
|
+
const err = new Error(message);
|
|
91
|
+
err.expected = true;
|
|
92
|
+
return err;
|
|
93
|
+
}
|
|
19
94
|
export function readDaemonConfig(daemonDirectory) {
|
|
20
95
|
try {
|
|
21
96
|
const raw = JSON.parse(fs.readFileSync(daemonConfigPath(daemonDirectory), "utf8"));
|
|
@@ -19,6 +19,7 @@ export interface DaemonDoctorReport {
|
|
|
19
19
|
heldCount: number;
|
|
20
20
|
/** Distinct held sessions with neither bound state nor local company evidence. */
|
|
21
21
|
heldWithNoLocalEvidence: number;
|
|
22
|
+
attributedByDeviceDefault: number;
|
|
22
23
|
deadLetterCount: number;
|
|
23
24
|
/** Events in any local queue still marked no-company / NEEDS_COMPANY / NONE. */
|
|
24
25
|
unattributedEvents: UnattributedEventCounts;
|
|
@@ -47,6 +48,8 @@ export interface DaemonDoctorReport {
|
|
|
47
48
|
rejected?: number;
|
|
48
49
|
error?: string;
|
|
49
50
|
};
|
|
51
|
+
/** Work held or quarantined after default-derived membership was denied. */
|
|
52
|
+
deviceDefaultAuthorizationDenials: number;
|
|
50
53
|
}
|
|
51
54
|
export interface DaemonDoctorDeps {
|
|
52
55
|
home?: string;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
7
8
|
import { describeCognitoTokenSource, } from "../../../../utils/cognito-session.js";
|
|
8
9
|
import { listOutboxOperations, outboxStats, } from "../../../work-context/outbox.js";
|
|
9
10
|
import { readSessionState } from "../../../work-context/state.js";
|
|
@@ -52,6 +53,21 @@ function oldestJsonlAgeMs(filePath, nowMs) {
|
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
function countDeviceDefaultAttribution(root) {
|
|
57
|
+
try {
|
|
58
|
+
return fs.readdirSync(path.join(root, "sessions")).filter((entry) => {
|
|
59
|
+
if (!entry.endsWith(".json"))
|
|
60
|
+
return false;
|
|
61
|
+
const state = readSessionState(entry.slice(0, -5), root);
|
|
62
|
+
return state?.companyConfidence === "device_default" &&
|
|
63
|
+
Boolean(state.companyUid) &&
|
|
64
|
+
state.contextStatus !== "needs_company";
|
|
65
|
+
}).length;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
55
71
|
export function collectDaemonDoctor(deps = {}) {
|
|
56
72
|
const home = deps.home ?? os.homedir();
|
|
57
73
|
const env = deps.env ?? process.env;
|
|
@@ -88,6 +104,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
88
104
|
!hasCurrentAcknowledgedRegistration(ref, state, outboxOperations, deps.hqRoot ?? env.HQ_ROOT?.trim(), trustedRoots) &&
|
|
89
105
|
!refWithLocalCompanyEvidence(ref, deps.hqRoot ?? env.HQ_ROOT?.trim(), trustedRoots, state));
|
|
90
106
|
}).length;
|
|
107
|
+
const attributedByDeviceDefault = countDeviceDefaultAttribution(ctxRoot);
|
|
91
108
|
const deadLetterCount = countJsonlLines(deadPath);
|
|
92
109
|
const unattributedEvents = countUnattributedEvents(meshRoot);
|
|
93
110
|
const outbox = outboxStats(ctxRoot);
|
|
@@ -95,6 +112,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
95
112
|
const companiesOnline = state?.companiesOnline ?? [];
|
|
96
113
|
const emitMode = state?.emitMode ?? "legacy";
|
|
97
114
|
const subscribed = mqttState === "connected";
|
|
115
|
+
const deviceDefaultAuthorizationDenials = state?.deviceDefaultAuthorizationDenials ?? 0;
|
|
98
116
|
const oldestSpoolAgeMs = oldestJsonlAgeMs(spoolPath, now());
|
|
99
117
|
const unhealthyReasons = [];
|
|
100
118
|
const online = mqttState === "connected" && companiesOnline.length > 0;
|
|
@@ -140,6 +158,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
140
158
|
spoolDepth,
|
|
141
159
|
heldCount,
|
|
142
160
|
heldWithNoLocalEvidence,
|
|
161
|
+
attributedByDeviceDefault,
|
|
143
162
|
deadLetterCount,
|
|
144
163
|
unattributedEvents,
|
|
145
164
|
outboxDepth: outbox.depth,
|
|
@@ -164,6 +183,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
164
183
|
error: emitState.lastError,
|
|
165
184
|
}
|
|
166
185
|
: undefined,
|
|
186
|
+
deviceDefaultAuthorizationDenials,
|
|
167
187
|
};
|
|
168
188
|
}
|
|
169
189
|
export function formatDaemonDoctor(report) {
|
|
@@ -179,7 +199,7 @@ export function formatDaemonDoctor(report) {
|
|
|
179
199
|
// receive-only (direct) mode.
|
|
180
200
|
lines.push(`spool depth: ${report.spoolDepth}`, `held: ${report.heldCount}`, `held with no local evidence: ${report.heldWithNoLocalEvidence}`, `outbox depth: ${report.outboxDepth}`);
|
|
181
201
|
}
|
|
182
|
-
lines.push(`dead-letter: ${report.deadLetterCount}`, `unattributed events: ${report.unattributedEvents.total} ` +
|
|
202
|
+
lines.push(`attributed by device default: ${report.attributedByDeviceDefault}`, `device-default authorization denials: ${report.deviceDefaultAuthorizationDenials}`, `dead-letter: ${report.deadLetterCount}`, `unattributed events: ${report.unattributedEvents.total} ` +
|
|
183
203
|
`(spool=${report.unattributedEvents.spool} held=${report.unattributedEvents.held} dead-letter=${report.unattributedEvents.deadLetter})`, `token source: ${report.tokenSource}`, `actor kind: ${report.actorKind}`);
|
|
184
204
|
if (report.emitMode === "legacy") {
|
|
185
205
|
lines.push(`last flush: ${report.lastFlushAt ?? "(never)"}${report.lastFlushResult
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DAEMON_DIRNAME, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_NAME, DAEMON_PID_NAME, DAEMON_STATE_NAME, LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, daemonDir, daemonLogPath, daemonPidPath, daemonStatePath, sessionBoardPath, } from "./paths.js";
|
|
1
|
+
export { DAEMON_DIRNAME, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_NAME, DAEMON_PID_NAME, DAEMON_STATE_NAME, LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, WINDOWS_TASK_NAME, daemonDir, daemonLogPath, daemonPidPath, daemonStatePath, sessionBoardPath, } from "./paths.js";
|
|
2
2
|
export { acquirePidLock, defaultPidLockDeps, parsePidLock, pidLockStatus, readPidLock, releasePidLock, resolveDaemonDir, } from "./pid-lock.js";
|
|
3
3
|
export type { PidLockDeps, PidLockRecord, PidLockResult } from "./pid-lock.js";
|
|
4
4
|
export { CREDENTIAL_RENEWAL_FRACTION, DEFAULT_REFUSED_RETRY_MS, MAX_RETRY_DELAY_MS, MAX_TIMER_DELAY_MS, MQTT_KEEPALIVE_SECONDS, REALTIME_CREDENTIALS_PATH, REFUSED_RETRY_ENV, CredentialRenewalManager, CredentialVendError, classifyCredentialVendFailure, clampRetryDelayMs, createContract3Fetcher, defaultRefusedRetryMs, extractVendErrorCode, isCredentialVendRefused, normalizeContract3Bundle, parseRetryAfterMs, realTimerHost, refusedRetryDelayMs, renewalDelayMs, scheduleBoundedTimeout, } from "./credentials.js";
|
|
@@ -12,9 +12,10 @@ 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, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, uninstallDaemonService, } from "./install.js";
|
|
15
|
+
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, parseSchtasksLastResult, parseWindowsTaskHqRoot, readRecordedWindowsHqRoot, schtasksLastResultFailed, uninstallDaemonService, windowsLaunchMode, windowsLogonNotice, } from "./install.js";
|
|
16
16
|
export type { DaemonInstallPlatform, InstallPaths, InstallResult, } from "./install.js";
|
|
17
|
-
export { daemonConfigPath, readDaemonConfig, registerTrustedHqRoot, verifyHqRoot } from "./config.js";
|
|
17
|
+
export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, resolveInstallHqRoot, verifyHqRoot, } from "./config.js";
|
|
18
|
+
export type { InstallHqRootResolution } from "./config.js";
|
|
18
19
|
export type { DaemonConfig } from "./config.js";
|
|
19
20
|
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatDaemonDoctor, } from "./doctor.js";
|
|
20
21
|
export type { DaemonDoctorDeps, DaemonDoctorReport } from "./doctor.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DAEMON_DIRNAME, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_NAME, DAEMON_PID_NAME, DAEMON_STATE_NAME, LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, daemonDir, daemonLogPath, daemonPidPath, daemonStatePath, sessionBoardPath, } from "./paths.js";
|
|
1
|
+
export { DAEMON_DIRNAME, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_NAME, DAEMON_PID_NAME, DAEMON_STATE_NAME, LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, WINDOWS_TASK_NAME, daemonDir, daemonLogPath, daemonPidPath, daemonStatePath, sessionBoardPath, } from "./paths.js";
|
|
2
2
|
export { acquirePidLock, defaultPidLockDeps, parsePidLock, pidLockStatus, readPidLock, releasePidLock, resolveDaemonDir, } from "./pid-lock.js";
|
|
3
3
|
export { CREDENTIAL_RENEWAL_FRACTION, DEFAULT_REFUSED_RETRY_MS, MAX_RETRY_DELAY_MS, MAX_TIMER_DELAY_MS, MQTT_KEEPALIVE_SECONDS, REALTIME_CREDENTIALS_PATH, REFUSED_RETRY_ENV, CredentialRenewalManager, CredentialVendError, classifyCredentialVendFailure, clampRetryDelayMs, createContract3Fetcher, defaultRefusedRetryMs, extractVendErrorCode, isCredentialVendRefused, normalizeContract3Bundle, parseRetryAfterMs, realTimerHost, refusedRetryDelayMs, renewalDelayMs, scheduleBoundedTimeout, } from "./credentials.js";
|
|
4
4
|
export { amzDateOf, hex, presignIotWssUrl, rfc3986Encode } from "./presign.js";
|
|
@@ -6,8 +6,8 @@ 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, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, uninstallDaemonService, } from "./install.js";
|
|
10
|
-
export { daemonConfigPath, readDaemonConfig, registerTrustedHqRoot, verifyHqRoot } from "./config.js";
|
|
9
|
+
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, parseSchtasksLastResult, parseWindowsTaskHqRoot, readRecordedWindowsHqRoot, schtasksLastResultFailed, uninstallDaemonService, windowsLaunchMode, windowsLogonNotice, } from "./install.js";
|
|
10
|
+
export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, resolveInstallHqRoot, verifyHqRoot, } from "./config.js";
|
|
11
11
|
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatDaemonDoctor, } from "./doctor.js";
|
|
12
12
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
13
13
|
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|