@indigoai-us/hq-cli 5.125.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
  3. package/assets/bot-workers/setup/skills/your-business.md +270 -0
  4. package/assets/bot-workers/setup/worker.yaml +189 -58
  5. package/dist/command-catalog.generated.d.ts +407 -4
  6. package/dist/command-catalog.generated.js +522 -4
  7. package/dist/command-registration-plan.d.ts +6 -0
  8. package/dist/command-registration-plan.js +1 -0
  9. package/dist/commands/bot.d.ts +10 -0
  10. package/dist/commands/bot.js +6 -0
  11. package/dist/commands/lanes.d.ts +75 -0
  12. package/dist/commands/lanes.js +2032 -0
  13. package/dist/commands/mesh.js +37 -9
  14. package/dist/commands/onboard-create-identity.d.ts +26 -0
  15. package/dist/commands/onboard-create-identity.js +23 -0
  16. package/dist/commands/onboard.js +13 -4
  17. package/dist/lib/bot/progress.d.ts +6 -0
  18. package/dist/lib/bot/progress.js +16 -0
  19. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  20. package/dist/lib/bot/runtime/grok.js +86 -8
  21. package/dist/lib/bot/runtime/index.js +15 -8
  22. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  23. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  24. package/dist/lib/lanes/accounts.d.ts +43 -0
  25. package/dist/lib/lanes/accounts.js +128 -0
  26. package/dist/lib/lanes/config.d.ts +56 -0
  27. package/dist/lib/lanes/config.js +351 -0
  28. package/dist/lib/lanes/errors.d.ts +39 -0
  29. package/dist/lib/lanes/errors.js +72 -0
  30. package/dist/lib/lanes/index.d.ts +19 -0
  31. package/dist/lib/lanes/index.js +12 -0
  32. package/dist/lib/lanes/liveness.d.ts +89 -0
  33. package/dist/lib/lanes/liveness.js +273 -0
  34. package/dist/lib/lanes/registry.d.ts +103 -0
  35. package/dist/lib/lanes/registry.js +247 -0
  36. package/dist/lib/lanes/store.d.ts +64 -0
  37. package/dist/lib/lanes/store.js +627 -0
  38. package/dist/lib/lanes/throttle.d.ts +89 -0
  39. package/dist/lib/lanes/throttle.js +297 -0
  40. package/dist/lib/lanes/types.d.ts +193 -0
  41. package/dist/lib/lanes/types.js +217 -0
  42. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  43. package/dist/lib/mesh/live/daemon/config.js +75 -0
  44. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  45. package/dist/lib/mesh/live/daemon/index.js +3 -3
  46. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  47. package/dist/lib/mesh/live/daemon/install.js +444 -2
  48. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  49. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  50. package/dist/lib/scan-packages/index.js +17 -12
  51. package/dist/lib/work-context/config.d.ts +20 -0
  52. package/dist/lib/work-context/config.js +26 -5
  53. package/dist/lib/work-context/reconcile.js +28 -7
  54. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  55. package/dist/lib/work-context/windows-acl.js +425 -0
  56. package/dist/utils/pack-contributions.js +5 -4
  57. 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
@@ -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"));
@@ -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";
@@ -4,13 +4,36 @@
4
4
  * - macOS: LaunchAgent ai.getindigo.hq-mesh-daemon
5
5
  * - Linux: systemd user unit hq-mesh-daemon.service
6
6
  * (documented fallback: root-owned unit running as ec2-user)
7
+ * - Windows: per-user Scheduled Task ai.getindigo.hq-mesh-daemon registered
8
+ * from a rendered task XML via schtasks.exe (no native dependencies)
7
9
  *
8
10
  * The unit embeds the hq binary resolved at install time. Logs go to
9
11
  * ~/.hq/work-mesh/daemon/log (rotation at 5MB handled by the daemon).
10
12
  * Tests must not actually install services — use a temp HOME/prefix.
11
13
  */
12
14
  import * as fs from "node:fs";
13
- export type DaemonInstallPlatform = "darwin" | "linux" | "other";
15
+ export type DaemonInstallPlatform = "darwin" | "linux" | "win32" | "other";
16
+ /** Result of one schtasks.exe invocation (tests inject; never the real binary). */
17
+ export interface SchtasksResult {
18
+ status: number | null;
19
+ stdout: string;
20
+ stderr: string;
21
+ }
22
+ export type SchtasksRunner = (args: string[]) => SchtasksResult;
23
+ /**
24
+ * `schtasks /End` terminates the task's root process only. The daemon is
25
+ * launched through `conhost.exe --headless`, so ending the task kills conhost
26
+ * and leaves node running as an orphan that still holds the pid lock. Stop
27
+ * the recorded daemon pid explicitly. Returns the pid that was signalled, or
28
+ * null when no live daemon pid was recorded.
29
+ */
30
+ export declare function stopWindowsDaemonProcess(dir: string, deps?: {
31
+ readFileSync?: typeof fs.readFileSync;
32
+ existsSync?: typeof fs.existsSync;
33
+ kill?: (pid: number) => void;
34
+ selfPid?: number;
35
+ }): number | null;
36
+ export declare const defaultSchtasks: SchtasksRunner;
14
37
  export interface InstallPaths {
15
38
  home: string;
16
39
  hqBinary: string;
@@ -21,10 +44,22 @@ export interface InstallPaths {
21
44
  plistPath: string;
22
45
  systemdUserDir: string;
23
46
  systemdUnitPath: string;
47
+ /** Windows: rendered Task Scheduler XML kept under the daemon dir. */
48
+ windowsTaskXmlPath: string;
49
+ /** Windows: `DOMAIN\user` the task is registered for (logon trigger + principal). */
50
+ windowsUser: string;
51
+ /**
52
+ * Windows: `conhost.exe` used as `conhost --headless node hq ...` so the
53
+ * logon-triggered daemon gets no visible console window. Undefined when the
54
+ * host has no conhost (the task then runs node directly).
55
+ */
56
+ windowsHeadlessLauncher?: string;
24
57
  /** When true, render the root/ec2-user system unit instead of user unit. */
25
58
  systemUser?: string;
26
59
  /** Verified HQ root to persist and pass to the installed service. */
27
60
  hqRoot?: string;
61
+ /** How hqRoot was chosen (shown in the Windows install confirmation). */
62
+ hqRootSource?: "flag" | "env" | "ancestor";
28
63
  }
29
64
  export interface InstallResult {
30
65
  platform: DaemonInstallPlatform;
@@ -34,6 +69,22 @@ export interface InstallResult {
34
69
  dest?: string;
35
70
  running?: boolean;
36
71
  message: string;
72
+ /**
73
+ * Windows status: the task's "Last Result" code from `schtasks /Query /V`
74
+ * (normalized to unsigned 32-bit), or null when unavailable.
75
+ */
76
+ lastResult?: number | null;
77
+ /** Windows status: true when lastResult indicates the last start failed. */
78
+ lastRunFailed?: boolean;
79
+ /** One plain line of operator context (Windows: logged-on-only visibility). */
80
+ notice?: string;
81
+ /**
82
+ * Windows status: HQ root read back from the registered task XML (else the
83
+ * persisted daemon config), never re-resolved from cwd. Null when unknown.
84
+ */
85
+ recordedHqRoot?: string | null;
86
+ /** Windows status: whether recordedHqRoot still passes isInstallableHqRoot. */
87
+ rootValid?: boolean;
37
88
  }
38
89
  /** Best-effort restart result used after an in-place CLI upgrade. */
39
90
  export interface RestartDaemonResult {
@@ -54,6 +105,8 @@ export declare function restartDaemonServiceIfInstalled(opts?: {
54
105
  run?: (command: string, args: string[]) => {
55
106
  status: number | null;
56
107
  };
108
+ /** Windows: terminate the recorded daemon pid after `schtasks /End` (tests inject). */
109
+ kill?: (pid: number) => void;
57
110
  }): RestartDaemonResult;
58
111
  export declare function detectPlatform(platform?: NodeJS.Platform): DaemonInstallPlatform;
59
112
  /**
@@ -76,7 +129,34 @@ export declare function buildInstallPaths(opts: {
76
129
  argv?: string[];
77
130
  execPath?: string;
78
131
  hqRoot?: string;
132
+ hqRootSource?: "flag" | "env" | "ancestor";
133
+ /** Windows account (`DOMAIN\user`) the task runs as; defaults to the current user. */
134
+ windowsUser?: string;
135
+ /** Windows headless launcher override (tests); defaults to %SystemRoot%\System32\conhost.exe when present. */
136
+ windowsHeadlessLauncher?: string | null;
79
137
  }): InstallPaths;
138
+ /** `DOMAIN\user` for the current account, as Task Scheduler expects. */
139
+ export declare function resolveWindowsUser(env?: NodeJS.ProcessEnv): string;
140
+ /**
141
+ * `conhost.exe --headless` is undocumented. It shipped with the ConPTY
142
+ * conhost in Windows 10 1809 (build 17763); older conhost builds reject the
143
+ * flag and the task would fail on every logon. Gate on the kernel build
144
+ * reported by os.release() ("10.0.<build>") plus a presence check.
145
+ */
146
+ export declare const WINDOWS_CONHOST_HEADLESS_MIN_BUILD = 17763;
147
+ /** Build number from an os.release() string such as "10.0.26200"; null if unparsable. */
148
+ export declare function parseWindowsBuild(release: string): number | null;
149
+ /**
150
+ * `conhost.exe --headless` (Windows 10 1809+) runs a console app with no
151
+ * window. Returns the launcher path only when conhost.exe exists under
152
+ * SystemRoot and the host build is new enough to honour `--headless`.
153
+ */
154
+ export declare function resolveWindowsHeadlessLauncher(env?: NodeJS.ProcessEnv, existsSync?: typeof fs.existsSync, release?: string): string | undefined;
155
+ export type WindowsLaunchMode = "conhost-headless" | "direct";
156
+ /** Which launch wrapper the rendered task uses (recorded in the install message). */
157
+ export declare function windowsLaunchMode(paths: Pick<InstallPaths, "windowsHeadlessLauncher">): WindowsLaunchMode;
158
+ /** The task uses the interactive token, so it exists only inside the user's logon session. */
159
+ export declare function windowsLogonNotice(paths: Pick<InstallPaths, "windowsUser">): string;
80
160
  export declare function renderLaunchdPlist(paths: InstallPaths): string;
81
161
  export declare function renderSystemdUserUnit(paths: InstallPaths): string;
82
162
  /**
@@ -84,6 +164,55 @@ export declare function renderSystemdUserUnit(paths: InstallPaths): string;
84
164
  * Documented fallback — written only when --system-user is passed.
85
165
  */
86
166
  export declare function renderSystemdSystemUnit(paths: InstallPaths): string;
167
+ /**
168
+ * Windows Task Scheduler definition (schtasks /Create /XML). Runs at logon of
169
+ * the registering user, hidden, no execution time limit, one instance, and
170
+ * restarts every minute on failure. HOME/USERPROFILE come from the user's own
171
+ * session. Task Scheduler XML has no environment block, so the HQ root the
172
+ * install was run for is passed explicitly as `mesh daemon run --hq-root
173
+ * <path>` (the launchd/systemd equivalent of Environment=HQ_ROOT) and is also
174
+ * persisted in the daemon config as a trusted root.
175
+ *
176
+ * LogonType is InteractiveToken: S4U ("run whether user is logged on or not")
177
+ * needs elevation to register, and the interactive token is what an
178
+ * unprivileged `hq mesh daemon install` can create. As a consequence the task
179
+ * runs only while the registering user is logged on. The console window that
180
+ * an interactive-token task would otherwise open is avoided by launching
181
+ * through `conhost.exe --headless` when the host supports it (see
182
+ * resolveWindowsHeadlessLauncher); otherwise node is run directly.
183
+ */
184
+ export declare function renderWindowsTaskXml(paths: InstallPaths): string;
185
+ /** schtasks stores task XML as UTF-16 LE with BOM; match it so /XML parses. */
186
+ export declare function encodeWindowsTaskXml(rendered: string): Buffer;
187
+ /** Status column of `schtasks /Query /FO CSV /NH`: "TaskName","Next Run Time","Status". */
188
+ export declare function parseSchtasksCsvStatus(stdout: string): string | null;
189
+ /**
190
+ * "Last Result" of `schtasks /Query /TN <name> /V /FO CSV`, normalized to an
191
+ * unsigned 32-bit code (schtasks prints HRESULT/NTSTATUS values as signed
192
+ * decimals, e.g. -2147024894 for 0x80070002). Prefers the English header
193
+ * when present; otherwise uses the fixed column position. Null when the
194
+ * output carries no task row or the cell is not numeric.
195
+ */
196
+ export declare function parseSchtasksLastResult(stdout: string): number | null;
197
+ /**
198
+ * Task Scheduler informational results mean "no failed start": 0 is success,
199
+ * and every SCHED_S_* code (ready, running, not yet run, terminated, queued,
200
+ * ...) is a success-severity HRESULT (bit 31 clear) in FACILITY_ITF (0x4)
201
+ * with code 0x1300-0x13FF, i.e. 0x00041300-0x000413FF. Everything else
202
+ * (0x8007xxxx, 0xC000xxxx, process exit codes such as 1) is a failed start.
203
+ */
204
+ export declare function schtasksLastResultFailed(code: number | null): boolean;
205
+ /** `--hq-root <path>` from a rendered task's <Arguments> (XML-escaped, optionally quoted). */
206
+ export declare function parseWindowsTaskHqRoot(xml: string): string | null;
207
+ /**
208
+ * The HQ root the installed Windows task actually carries: the task XML on
209
+ * disk first, else the first persisted trusted root. Reads state only; never
210
+ * resolves from cwd, so status reports what install recorded.
211
+ */
212
+ export declare function readRecordedWindowsHqRoot(paths: Pick<InstallPaths, "windowsTaskXmlPath" | "daemonDir">, deps?: {
213
+ existsSync?: typeof fs.existsSync;
214
+ readFileSync?: typeof fs.readFileSync;
215
+ }): string | null;
87
216
  export declare function installDaemonService(opts: {
88
217
  platform?: DaemonInstallPlatform;
89
218
  paths?: InstallPaths;
@@ -97,6 +226,10 @@ export declare function installDaemonService(opts: {
97
226
  reloadSystemd?: () => {
98
227
  status: number | null;
99
228
  };
229
+ /** Windows: schtasks.exe runner (tests inject; never the real binary). */
230
+ schtasks?: SchtasksRunner;
231
+ /** Windows: terminate the recorded daemon pid when replacing a task (tests inject). */
232
+ kill?: (pid: number) => void;
100
233
  }): InstallResult;
101
234
  export declare function uninstallDaemonService(opts: {
102
235
  platform?: DaemonInstallPlatform;
@@ -104,6 +237,10 @@ export declare function uninstallDaemonService(opts: {
104
237
  unlinkSync?: typeof fs.unlinkSync;
105
238
  existsSync?: typeof fs.existsSync;
106
239
  deactivate?: (platform: DaemonInstallPlatform, dest: string) => void;
240
+ /** Windows: schtasks.exe runner (tests inject; never the real binary). */
241
+ schtasks?: SchtasksRunner;
242
+ /** Windows: terminate the recorded daemon pid after `schtasks /End` (tests inject). */
243
+ kill?: (pid: number) => void;
107
244
  }): InstallResult;
108
245
  export declare function daemonServiceStatus(opts: {
109
246
  platform?: DaemonInstallPlatform;
@@ -111,6 +248,11 @@ export declare function daemonServiceStatus(opts: {
111
248
  existsSync?: typeof fs.existsSync;
112
249
  /** Optional live probe (launchctl/systemctl); tests inject. */
113
250
  isLoaded?: (platform: DaemonInstallPlatform, dest: string) => boolean;
251
+ /** Windows: schtasks.exe runner (tests inject; never the real binary). */
252
+ schtasks?: SchtasksRunner;
253
+ /** Windows: liveness probe for the daemon pid lock (tests inject). */
254
+ isProcessAlive?: (pid: number) => boolean;
255
+ readFileSync?: typeof fs.readFileSync;
114
256
  }): InstallResult;
115
257
  /** Read baked template files from assets/ (optional; renderers are primary). */
116
258
  export declare function readAssetTemplate(name: "launchd.plist.template" | "systemd.user.service.template" | "systemd.system.service.template"): string | null;