@indigoai-us/hq-cli 5.125.0 → 5.127.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 (108) hide show
  1. package/CHANGELOG.md +329 -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/assets/lanes-hooks/hq-lanes-drain.json +15 -0
  6. package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
  7. package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
  8. package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
  9. package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
  10. package/dist/command-catalog.generated.d.ts +643 -6
  11. package/dist/command-catalog.generated.js +836 -6
  12. package/dist/command-registration-plan.d.ts +7 -0
  13. package/dist/command-registration-plan.js +11 -1
  14. package/dist/commands/agents.d.ts +12 -0
  15. package/dist/commands/agents.js +46 -2
  16. package/dist/commands/bot.d.ts +10 -0
  17. package/dist/commands/bot.js +6 -0
  18. package/dist/commands/lanes.d.ts +114 -0
  19. package/dist/commands/lanes.js +2969 -0
  20. package/dist/commands/mesh.js +287 -24
  21. package/dist/commands/onboard-create-identity.d.ts +26 -0
  22. package/dist/commands/onboard-create-identity.js +23 -0
  23. package/dist/commands/onboard.js +13 -4
  24. package/dist/lib/bot/progress.d.ts +6 -0
  25. package/dist/lib/bot/progress.js +16 -0
  26. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  27. package/dist/lib/bot/runtime/grok.js +86 -8
  28. package/dist/lib/bot/runtime/index.js +15 -8
  29. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  30. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  31. package/dist/lib/lanes/accounts.d.ts +103 -0
  32. package/dist/lib/lanes/accounts.js +318 -0
  33. package/dist/lib/lanes/compaction.d.ts +96 -0
  34. package/dist/lib/lanes/compaction.js +197 -0
  35. package/dist/lib/lanes/config.d.ts +71 -0
  36. package/dist/lib/lanes/config.js +408 -0
  37. package/dist/lib/lanes/delivery.d.ts +151 -0
  38. package/dist/lib/lanes/delivery.js +354 -0
  39. package/dist/lib/lanes/dropbox.d.ts +96 -0
  40. package/dist/lib/lanes/dropbox.js +408 -0
  41. package/dist/lib/lanes/effort.d.ts +50 -0
  42. package/dist/lib/lanes/effort.js +109 -0
  43. package/dist/lib/lanes/errors.d.ts +39 -0
  44. package/dist/lib/lanes/errors.js +72 -0
  45. package/dist/lib/lanes/hooks-install.d.ts +81 -0
  46. package/dist/lib/lanes/hooks-install.js +326 -0
  47. package/dist/lib/lanes/hq-root.d.ts +19 -0
  48. package/dist/lib/lanes/hq-root.js +28 -0
  49. package/dist/lib/lanes/index.d.ts +49 -0
  50. package/dist/lib/lanes/index.js +27 -0
  51. package/dist/lib/lanes/liveness.d.ts +95 -0
  52. package/dist/lib/lanes/liveness.js +332 -0
  53. package/dist/lib/lanes/machine-identity.d.ts +34 -0
  54. package/dist/lib/lanes/machine-identity.js +92 -0
  55. package/dist/lib/lanes/mesh-events.d.ts +109 -0
  56. package/dist/lib/lanes/mesh-events.js +275 -0
  57. package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
  58. package/dist/lib/lanes/outpost-dispatch.js +90 -0
  59. package/dist/lib/lanes/questions.d.ts +75 -0
  60. package/dist/lib/lanes/questions.js +243 -0
  61. package/dist/lib/lanes/quota.d.ts +95 -0
  62. package/dist/lib/lanes/quota.js +320 -0
  63. package/dist/lib/lanes/reconcile-install.d.ts +69 -0
  64. package/dist/lib/lanes/reconcile-install.js +221 -0
  65. package/dist/lib/lanes/reconcile.d.ts +102 -0
  66. package/dist/lib/lanes/reconcile.js +985 -0
  67. package/dist/lib/lanes/registry.d.ts +116 -0
  68. package/dist/lib/lanes/registry.js +521 -0
  69. package/dist/lib/lanes/spawn.d.ts +274 -0
  70. package/dist/lib/lanes/spawn.js +1363 -0
  71. package/dist/lib/lanes/store.d.ts +68 -0
  72. package/dist/lib/lanes/store.js +745 -0
  73. package/dist/lib/lanes/throttle.d.ts +89 -0
  74. package/dist/lib/lanes/throttle.js +297 -0
  75. package/dist/lib/lanes/types.d.ts +535 -0
  76. package/dist/lib/lanes/types.js +350 -0
  77. package/dist/lib/lanes/watcher-main.d.ts +9 -0
  78. package/dist/lib/lanes/watcher-main.js +22 -0
  79. package/dist/lib/lanes/watcher.d.ts +216 -0
  80. package/dist/lib/lanes/watcher.js +859 -0
  81. package/dist/lib/mesh/api.d.ts +71 -7
  82. package/dist/lib/mesh/api.js +167 -28
  83. package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
  84. package/dist/lib/mesh/live/backfill-held.js +66 -21
  85. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  86. package/dist/lib/mesh/live/daemon/config.js +75 -0
  87. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  88. package/dist/lib/mesh/live/daemon/index.js +3 -3
  89. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  90. package/dist/lib/mesh/live/daemon/install.js +444 -2
  91. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  92. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  93. package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
  94. package/dist/lib/mesh/live/daemon/run.js +111 -54
  95. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  96. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
  97. package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
  98. package/dist/lib/scan-packages/index.d.ts +2 -0
  99. package/dist/lib/scan-packages/index.js +18 -13
  100. package/dist/lib/work-context/config.d.ts +20 -0
  101. package/dist/lib/work-context/config.js +26 -5
  102. package/dist/lib/work-context/reconcile.js +28 -7
  103. package/dist/lib/work-context/state.d.ts +12 -0
  104. package/dist/lib/work-context/state.js +131 -3
  105. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  106. package/dist/lib/work-context/windows-acl.js +425 -0
  107. package/dist/utils/pack-contributions.js +5 -4
  108. package/package.json +3 -1
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Session liveness from the work-mesh spool (spec §9).
3
+ *
4
+ * Source: ~/.hq/work-mesh/spool.jsonl, events session_start / turn_start /
5
+ * turn_end / session_end keyed by session id. Nothing latches, including
6
+ * `ended` — a later event for a reused id supersedes session_end.
7
+ *
8
+ * An index is a single compact file (workspace/lanes/sessions/index.json).
9
+ * This module never stats workspace/sessions/.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { workMeshSpoolPath, workMeshRoot } from "../mesh/live/paths.js";
15
+ import { daemonDir, daemonStatePath } from "../mesh/live/daemon/paths.js";
16
+ import { LanesError } from "./errors.js";
17
+ import { DEFAULT_LIVE_WITHIN_MS, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, loadLanesConfig, } from "./config.js";
18
+ import { lanesStoreRoot } from "./store.js";
19
+ export const LIVE_WITHIN_MS = DEFAULT_LIVE_WITHIN_MS;
20
+ export const TURN_START_LOST_MS = DEFAULT_TURN_START_LOST_MS;
21
+ export const SILENT_LOST_MS = DEFAULT_SILENT_LOST_MS;
22
+ export const LIVENESS_EVENT_KINDS = [
23
+ "session_start",
24
+ "turn_start",
25
+ "turn_end",
26
+ "session_end",
27
+ ];
28
+ export function defaultLivenessDeps() {
29
+ return {
30
+ now: () => Date.now(),
31
+ homedir: () => os.homedir(),
32
+ env: process.env,
33
+ readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
34
+ writeFileSync: fs.writeFileSync.bind(fs),
35
+ renameSync: fs.renameSync.bind(fs),
36
+ unlinkSync: fs.unlinkSync.bind(fs),
37
+ existsSync: (file) => fs.existsSync(file),
38
+ mkdirSync: fs.mkdirSync.bind(fs),
39
+ statSync: (file) => fs.statSync(file),
40
+ };
41
+ }
42
+ export function defaultSpoolPath(deps = defaultLivenessDeps()) {
43
+ return workMeshSpoolPath(workMeshRoot(deps.homedir(), deps.env));
44
+ }
45
+ export function livenessIndexPath(hqRoot) {
46
+ return path.join(lanesStoreRoot(hqRoot), "sessions", "index.json");
47
+ }
48
+ function isPlainObject(value) {
49
+ return value !== null && typeof value === "object" && !Array.isArray(value);
50
+ }
51
+ /**
52
+ * Classify one session from its last spool event. `ended` is not latched:
53
+ * a later event for the same id is classified from that later event.
54
+ *
55
+ * Mid-turn (`turn_start` younger than 2h) stays `live` even after the 10
56
+ * minute recency window — spec §9's prose treats that as working, not idle.
57
+ * `session_end` as the last event is `ended` even when recent (fast path).
58
+ */
59
+ export function classifySessionLiveness(entry, nowMs, presenceConnected = false, windows = {
60
+ live_within_ms: LIVE_WITHIN_MS,
61
+ turn_start_lost_ms: TURN_START_LOST_MS,
62
+ silent_lost_ms: SILENT_LOST_MS,
63
+ }) {
64
+ const at = Date.parse(entry.lastAt);
65
+ const age = Number.isFinite(at) ? nowMs - at : Number.POSITIVE_INFINITY;
66
+ const kind = entry.lastKind;
67
+ if (kind === "session_end")
68
+ return "ended";
69
+ // Box-wide MQTT connected is not per-session presence. A crashed
70
+ // mid-turn session must still become `lost` so §6.8 can advance.
71
+ void presenceConnected;
72
+ if (kind === "turn_start" && age < windows.turn_start_lost_ms)
73
+ return "live";
74
+ if (age <= windows.live_within_ms)
75
+ return "live";
76
+ if (kind === "turn_start" && age >= windows.turn_start_lost_ms)
77
+ return "lost";
78
+ if (age >= windows.silent_lost_ms)
79
+ return "lost";
80
+ if (kind === "turn_end")
81
+ return "idle";
82
+ return "idle";
83
+ }
84
+ export function ingestSpoolLine(line, sessions, lineNo) {
85
+ const trimmed = line.trim();
86
+ if (!trimmed)
87
+ return;
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(trimmed);
91
+ }
92
+ catch (err) {
93
+ process.stderr.write(`lanes liveness: skipping malformed spool line ${lineNo}: ${err instanceof Error ? err.message : String(err)}\n`);
94
+ return;
95
+ }
96
+ if (!isPlainObject(parsed))
97
+ return;
98
+ const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
99
+ const kind = typeof parsed.kind === "string" ? parsed.kind : "";
100
+ const at = typeof parsed.at === "string" ? parsed.at : "";
101
+ if (!sessionId || !kind || !at)
102
+ return;
103
+ if (!Number.isFinite(Date.parse(at))) {
104
+ process.stderr.write(`lanes liveness: skipping spool line ${lineNo} with unparseable at\n`);
105
+ return;
106
+ }
107
+ const existing = sessions[sessionId];
108
+ const seq = typeof parsed.seq === "number" ? parsed.seq : undefined;
109
+ if (existing) {
110
+ const existingAt = Date.parse(existing.lastAt);
111
+ const nextAt = Date.parse(at);
112
+ if (nextAt < existingAt)
113
+ return;
114
+ if (nextAt === existingAt &&
115
+ seq !== undefined &&
116
+ existing.lastSeq !== undefined &&
117
+ seq < existing.lastSeq) {
118
+ return;
119
+ }
120
+ }
121
+ const entry = { sessionId, lastKind: kind, lastAt: at };
122
+ if (seq !== undefined)
123
+ entry.lastSeq = seq;
124
+ sessions[sessionId] = entry;
125
+ }
126
+ export function buildLivenessIndexFromSpool(spoolContents, meta) {
127
+ const sessions = {};
128
+ const lines = spoolContents.split("\n");
129
+ for (let i = 0; i < lines.length; i += 1) {
130
+ ingestSpoolLine(lines[i] ?? "", sessions, i + 1);
131
+ }
132
+ return {
133
+ schema_version: "hq-lane-liveness-index.v1",
134
+ built_at: meta.built_at,
135
+ spool_path: meta.spool_path,
136
+ spool_bytes: meta.spool_bytes,
137
+ spool_mtime_ms: meta.spool_mtime_ms,
138
+ sessions,
139
+ };
140
+ }
141
+ function atomicWriteJson(target, value, deps) {
142
+ const dir = path.dirname(target);
143
+ deps.mkdirSync(dir, { recursive: true });
144
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
145
+ try {
146
+ deps.writeFileSync(tmp, `${JSON.stringify(value)}\n`, { encoding: "utf8" });
147
+ deps.renameSync(tmp, target);
148
+ }
149
+ catch (err) {
150
+ try {
151
+ deps.unlinkSync(tmp);
152
+ }
153
+ catch (unlinkErr) {
154
+ const code = unlinkErr.code;
155
+ if (code !== "ENOENT") {
156
+ process.stderr.write(`lanes liveness: unlink tmp failed for ${tmp}: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}\n`);
157
+ }
158
+ }
159
+ throw err;
160
+ }
161
+ }
162
+ export function rebuildLivenessIndex(hqRoot, spoolPath, deps = defaultLivenessDeps()) {
163
+ let contents;
164
+ let st;
165
+ try {
166
+ st = deps.statSync(spoolPath);
167
+ contents = deps.readFileSync(spoolPath, "utf8");
168
+ }
169
+ catch (err) {
170
+ const code = err.code;
171
+ if (code === "ENOENT") {
172
+ const empty = {
173
+ schema_version: "hq-lane-liveness-index.v1",
174
+ built_at: new Date(deps.now()).toISOString(),
175
+ spool_path: spoolPath,
176
+ spool_bytes: 0,
177
+ spool_mtime_ms: 0,
178
+ sessions: {},
179
+ };
180
+ atomicWriteJson(livenessIndexPath(hqRoot), empty, deps);
181
+ return empty;
182
+ }
183
+ throw err;
184
+ }
185
+ const index = buildLivenessIndexFromSpool(contents, {
186
+ spool_path: spoolPath,
187
+ spool_bytes: st.size,
188
+ spool_mtime_ms: Math.floor(st.mtimeMs),
189
+ built_at: new Date(deps.now()).toISOString(),
190
+ });
191
+ atomicWriteJson(livenessIndexPath(hqRoot), index, deps);
192
+ return index;
193
+ }
194
+ export function readLivenessIndex(hqRoot, deps = defaultLivenessDeps()) {
195
+ const filePath = livenessIndexPath(hqRoot);
196
+ if (!deps.existsSync(filePath))
197
+ return null;
198
+ let raw;
199
+ try {
200
+ raw = deps.readFileSync(filePath, "utf8");
201
+ }
202
+ catch (err) {
203
+ process.stderr.write(`lanes liveness: failed to read index ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
204
+ throw err;
205
+ }
206
+ try {
207
+ const parsed = JSON.parse(raw);
208
+ if (!isPlainObject(parsed) || parsed.schema_version !== "hq-lane-liveness-index.v1") {
209
+ throw new Error(`unexpected liveness index schema in ${filePath}`);
210
+ }
211
+ if (!isPlainObject(parsed.sessions)) {
212
+ throw new Error(`liveness index sessions map missing in ${filePath}`);
213
+ }
214
+ return parsed;
215
+ }
216
+ catch (err) {
217
+ process.stderr.write(`lanes liveness: invalid index ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
218
+ throw err;
219
+ }
220
+ }
221
+ /**
222
+ * Return the compact index, rebuilding from the spool when missing or stale.
223
+ * Never walks workspace/sessions/.
224
+ */
225
+ export function loadLivenessIndex(hqRoot, spoolPath, deps = defaultLivenessDeps(), persist = true) {
226
+ const spool = spoolPath ?? defaultSpoolPath(deps);
227
+ const existing = (() => {
228
+ try {
229
+ return readLivenessIndex(hqRoot, deps);
230
+ }
231
+ catch (err) {
232
+ process.stderr.write(`lanes liveness: rebuilding after invalid index: ${err instanceof Error ? err.message : String(err)}\n`);
233
+ return null;
234
+ }
235
+ })();
236
+ if (existing) {
237
+ try {
238
+ const st = deps.statSync(spool);
239
+ if (existing.spool_path === spool &&
240
+ existing.spool_bytes === st.size &&
241
+ existing.spool_mtime_ms === Math.floor(st.mtimeMs)) {
242
+ return existing;
243
+ }
244
+ }
245
+ catch (err) {
246
+ const code = err.code;
247
+ if (code !== "ENOENT")
248
+ throw err;
249
+ if (existing.spool_bytes === 0)
250
+ return existing;
251
+ }
252
+ }
253
+ if (!persist) {
254
+ let contents = "";
255
+ let stSize = 0;
256
+ let stMtime = 0;
257
+ try {
258
+ const st = deps.statSync(spool);
259
+ stSize = st.size;
260
+ stMtime = Math.floor(st.mtimeMs);
261
+ contents = deps.readFileSync(spool, "utf8");
262
+ }
263
+ catch (err) {
264
+ const code = err.code;
265
+ if (code !== "ENOENT")
266
+ throw err;
267
+ }
268
+ return buildLivenessIndexFromSpool(contents, {
269
+ spool_path: spool,
270
+ spool_bytes: stSize,
271
+ spool_mtime_ms: stMtime,
272
+ built_at: new Date(deps.now()).toISOString(),
273
+ });
274
+ }
275
+ return rebuildLivenessIndex(hqRoot, spool, deps);
276
+ }
277
+ /**
278
+ * Spec §9: a session is also `live` when the mesh daemon reports presence
279
+ * connected, not only from the spool. Missing state file → not connected.
280
+ * Unreadable or corrupt state is an error (unknown must not resolve to healthy).
281
+ */
282
+ export function readDaemonPresenceConnected(deps = defaultLivenessDeps()) {
283
+ const statePath = daemonStatePath(daemonDir(undefined, deps.homedir(), deps.env));
284
+ if (!deps.existsSync(statePath))
285
+ return false;
286
+ let raw;
287
+ try {
288
+ raw = deps.readFileSync(statePath, "utf8");
289
+ }
290
+ catch (err) {
291
+ process.stderr.write(`lanes liveness: failed to read daemon state ${statePath}: ${err instanceof Error ? err.message : String(err)}\n`);
292
+ throw err;
293
+ }
294
+ let parsed;
295
+ try {
296
+ parsed = JSON.parse(raw);
297
+ }
298
+ catch (err) {
299
+ throw new LanesError("daemon_presence_unreadable", `Mesh daemon state ${statePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, { path: statePath });
300
+ }
301
+ if (!isPlainObject(parsed)) {
302
+ throw new LanesError("daemon_presence_unreadable", `Mesh daemon state ${statePath} is not an object`, { path: statePath });
303
+ }
304
+ if (parsed.v !== 1) {
305
+ throw new LanesError("daemon_presence_unreadable", `Mesh daemon state ${statePath} has unexpected v ${JSON.stringify(parsed.v)}`, { path: statePath, v: parsed.v });
306
+ }
307
+ return parsed.mqttState === "connected";
308
+ }
309
+ export function sessionLiveness(hqRoot, sessionId, opts = {}) {
310
+ const deps = opts.deps ?? defaultLivenessDeps();
311
+ const index = loadLivenessIndex(hqRoot, opts.spoolPath, deps);
312
+ const entry = index.sessions[sessionId];
313
+ if (!entry)
314
+ return null;
315
+ const windows = opts.windows ?? loadLanesConfig(hqRoot).liveness;
316
+ const presenceConnected = opts.presenceConnected ?? readDaemonPresenceConnected(deps);
317
+ return classifySessionLiveness(entry, opts.nowMs ?? deps.now(), presenceConnected, windows);
318
+ }
319
+ export function listSessionLiveness(hqRoot, opts = {}) {
320
+ const deps = opts.deps ?? defaultLivenessDeps();
321
+ const index = loadLivenessIndex(hqRoot, opts.spoolPath, deps);
322
+ const nowMs = opts.nowMs ?? deps.now();
323
+ const windows = opts.windows ?? loadLanesConfig(hqRoot).liveness;
324
+ const presenceConnected = opts.presenceConnected ?? readDaemonPresenceConnected(deps);
325
+ return Object.values(index.sessions).map((entry) => ({
326
+ sessionId: entry.sessionId,
327
+ state: classifySessionLiveness(entry, nowMs, presenceConnected, windows),
328
+ lastKind: entry.lastKind,
329
+ lastAt: entry.lastAt,
330
+ }));
331
+ }
332
+ //# sourceMappingURL=liveness.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared machine identity for Outpost self-target refusal (spec §5.2).
3
+ *
4
+ * One function, one fallback order, used by both the creating side
5
+ * (`origin_host` stamp) and the executing side (setup-time compare).
6
+ * EC2 instance id from IMDS, then hostname. Unresolvable is a status,
7
+ * never a guessed id.
8
+ *
9
+ * Only Outpost lane create and Outpost lane setup may call this. A local
10
+ * lane must not touch IMDS / the network.
11
+ */
12
+ export declare const IMDS_HOST = "169.254.169.254";
13
+ export declare const IMDS_TOKEN_URL = "http://169.254.169.254/latest/api/token";
14
+ export declare const IMDS_INSTANCE_ID_URL = "http://169.254.169.254/latest/meta-data/instance-id";
15
+ export declare const IMDS_TIMEOUT_MS = 250;
16
+ export type MachineIdentity = {
17
+ status: "ok";
18
+ id: string;
19
+ source: "imds" | "hostname";
20
+ imds?: string | null;
21
+ hostname?: string | null;
22
+ } | {
23
+ status: "unresolvable";
24
+ reason: string;
25
+ };
26
+ export interface MachineIdentityDeps {
27
+ /** Returns the IMDS instance id, or null when IMDS is unavailable. */
28
+ readImdsInstanceId?: () => string | null;
29
+ hostname?: () => string;
30
+ }
31
+ /** IMDSv2 token then instance-id. Any failure is null, not a thrown guess. */
32
+ export declare function defaultReadImdsInstanceId(): string | null;
33
+ export declare function resolveMachineIdentity(deps?: MachineIdentityDeps): MachineIdentity;
34
+ //# sourceMappingURL=machine-identity.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared machine identity for Outpost self-target refusal (spec §5.2).
3
+ *
4
+ * One function, one fallback order, used by both the creating side
5
+ * (`origin_host` stamp) and the executing side (setup-time compare).
6
+ * EC2 instance id from IMDS, then hostname. Unresolvable is a status,
7
+ * never a guessed id.
8
+ *
9
+ * Only Outpost lane create and Outpost lane setup may call this. A local
10
+ * lane must not touch IMDS / the network.
11
+ */
12
+ import { spawnSync } from "node:child_process";
13
+ import * as os from "node:os";
14
+ export const IMDS_HOST = "169.254.169.254";
15
+ export const IMDS_TOKEN_URL = `http://${IMDS_HOST}/latest/api/token`;
16
+ export const IMDS_INSTANCE_ID_URL = `http://${IMDS_HOST}/latest/meta-data/instance-id`;
17
+ export const IMDS_TIMEOUT_MS = 250;
18
+ function curlImds(method, url, headers) {
19
+ const args = ["-sS", "-f", "-m", "1", "-X", method];
20
+ for (const [key, value] of Object.entries(headers)) {
21
+ args.push("-H", `${key}: ${value}`);
22
+ }
23
+ args.push(url);
24
+ try {
25
+ const result = spawnSync("curl", args, {
26
+ encoding: "utf8",
27
+ timeout: IMDS_TIMEOUT_MS,
28
+ });
29
+ if (result.error || result.status !== 0)
30
+ return null;
31
+ const body = (result.stdout ?? "").trim();
32
+ return body.length > 0 ? body : null;
33
+ }
34
+ catch (err) {
35
+ process.stderr.write(`hq lanes: IMDS request failed for ${url}: ${err instanceof Error ? err.message : String(err)}\n`);
36
+ return null;
37
+ }
38
+ }
39
+ /** IMDSv2 token then instance-id. Any failure is null, not a thrown guess. */
40
+ export function defaultReadImdsInstanceId() {
41
+ const token = curlImds("PUT", IMDS_TOKEN_URL, {
42
+ "X-aws-ec2-metadata-token-ttl-seconds": "60",
43
+ });
44
+ if (!token)
45
+ return null;
46
+ return curlImds("GET", IMDS_INSTANCE_ID_URL, {
47
+ "X-aws-ec2-metadata-token": token,
48
+ });
49
+ }
50
+ export function resolveMachineIdentity(deps = {}) {
51
+ const readImds = deps.readImdsInstanceId ?? defaultReadImdsInstanceId;
52
+ let imdsId = null;
53
+ try {
54
+ const raw = readImds();
55
+ imdsId = typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
56
+ }
57
+ catch (err) {
58
+ process.stderr.write(`hq lanes: IMDS identity lookup failed: ${err instanceof Error ? err.message : String(err)}\n`);
59
+ }
60
+ let hostname = null;
61
+ try {
62
+ const hostnameFn = deps.hostname ?? (() => os.hostname());
63
+ const host = hostnameFn();
64
+ hostname = typeof host === "string" && host.trim() !== "" ? host.trim() : null;
65
+ }
66
+ catch (err) {
67
+ process.stderr.write(`hq lanes: hostname identity lookup failed: ${err instanceof Error ? err.message : String(err)}\n`);
68
+ if (!imdsId) {
69
+ return {
70
+ status: "unresolvable",
71
+ reason: err instanceof Error ? err.message : String(err),
72
+ };
73
+ }
74
+ }
75
+ if (imdsId) {
76
+ return { status: "ok", id: imdsId, source: "imds", imds: imdsId, hostname };
77
+ }
78
+ if (hostname) {
79
+ return {
80
+ status: "ok",
81
+ id: hostname,
82
+ source: "hostname",
83
+ imds: null,
84
+ hostname,
85
+ };
86
+ }
87
+ return {
88
+ status: "unresolvable",
89
+ reason: "IMDS unavailable and hostname empty",
90
+ };
91
+ }
92
+ //# sourceMappingURL=machine-identity.js.map
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Lane → work mesh events (spec §8).
3
+ *
4
+ * The lane side speaks the spec's vocabulary: lane_started, lane_progress,
5
+ * lane_question, lane_question_at_user, lane_ask, lane_blocked, lane_done.
6
+ * The work-mesh append endpoint accepts a closed roster of event kinds
7
+ * (hq-pro `src/vault-service/handlers/work-mesh.ts`, `validKinds`) and
8
+ * strips every payload field its per-kind validator does not name
9
+ * (hq-pro `src/vault-service/work-mesh/events.ts`). A `lane_*` kind on the
10
+ * wire is a 400. `toWireThreadEvent` is therefore the single place that
11
+ * turns a lane event into a roster kind whose payload carries the lane
12
+ * identity in the text fields the server keeps.
13
+ *
14
+ * Mapping, and why each kind:
15
+ *
16
+ * | lane event | wire kind | server effect |
17
+ * |------------------------|----------------|----------------------------------|
18
+ * | lane_started | run_started | status-neutral activity |
19
+ * | lane_progress | run_progress | status-neutral activity |
20
+ * | lane_done | run_complete | status-neutral; story → review |
21
+ * | lane_blocked | note (system) | status-neutral; the project |
22
+ * | | | thread is shared by every lane, |
23
+ * | | | so one lane must not flip it to |
24
+ * | | | `blocked` (spec §8 keeps thread |
25
+ * | | | status out of lane events). The |
26
+ * | | | payload carries `blocked: true` |
27
+ * | | | and `blocked_reason` for filters |
28
+ * | lane_question | question | status-neutral, carries the id |
29
+ * | lane_question_at_user | question | same |
30
+ * | lane_ask | question | same; ids of the ask envelope |
31
+ *
32
+ * Token source for detached emitters (the per-lane watcher): the same
33
+ * `requireToken()` every `hq mesh session … --enqueue` hook call uses,
34
+ * which is `ensureCognitoToken()` over the person cache at
35
+ * `~/.hq/cognito-tokens.json`, or a machine mint when
36
+ * `HQ_MACHINE_CREDS_FILE` / the default agent creds file is readable
37
+ * (`src/utils/cognito-session.ts`). The watcher inherits the creator's
38
+ * `HOME` (spawn.ts `armLaneWatcher` copies the caller env and only adds
39
+ * `HQ_ROOT` / `HQ_LANE_ID`; the provider account home rides its own
40
+ * variable, never `HOME`), so it reads the same cache the hooks read. It
41
+ * passes `interactive: false`: a cache that cannot refresh throws instead
42
+ * of opening a browser nobody is watching, and the watcher records
43
+ * `mesh_event_failed` and moves on. No credential file is read here.
44
+ */
45
+ import type { LaneCompanyRef, LaneEnvelope, LaneRecord } from "./types.js";
46
+ export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done"];
47
+ export type LaneMeshEvent = (typeof LANE_MESH_EVENTS)[number];
48
+ /** Roster kinds this module emits. Subset of the server's `validKinds`. */
49
+ export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "question", "note"];
50
+ export type WireEventKind = (typeof WIRE_EVENT_KINDS)[number];
51
+ /** Server caps: activity title/summary 2000, question text 500, note text 500. */
52
+ export declare const WIRE_ACTIVITY_TEXT_MAX = 2000;
53
+ export declare const WIRE_QUESTION_TEXT_MAX = 500;
54
+ export declare const WIRE_NOTE_TEXT_MAX = 500;
55
+ export interface WireThreadEvent {
56
+ eventKind: WireEventKind;
57
+ payload: Record<string, unknown>;
58
+ }
59
+ export declare function parseLaneMeshEvent(value: unknown): LaneMeshEvent;
60
+ /**
61
+ * Lane event → roster kind + validator-shaped payload. Pure. Every text
62
+ * field is pre-clamped to the server cap so a long summary is truncated
63
+ * here rather than rejected there.
64
+ */
65
+ export declare function toWireThreadEvent(event: LaneMeshEvent, payload: Record<string, unknown>): WireThreadEvent;
66
+ /**
67
+ * The seam every lane emitter goes through. `appendThreadEvent` takes the
68
+ * spec's lane event name; the default implementation maps it to the wire
69
+ * kind. Tests substitute a recording implementation and see lane names.
70
+ */
71
+ export interface LaneMesh {
72
+ ensureProjectThread: (company: LaneCompanyRef, projectId: string) => Promise<string>;
73
+ appendThreadEvent: (company: LaneCompanyRef, threadId: string, event: LaneMeshEvent, payload: Record<string, unknown>, signal?: AbortSignal) => Promise<void>;
74
+ patchStoryStatus: (company: LaneCompanyRef, projectId: string, storyId: string, status: string) => Promise<void>;
75
+ }
76
+ export interface CreateLaneMeshOptions {
77
+ /** False for detached callers (the watcher). See the token note above. */
78
+ interactive?: boolean;
79
+ /** Test seam. Defaults to the shared `requireToken`. */
80
+ token?: () => Promise<string>;
81
+ }
82
+ export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
83
+ /** Company + thread present. A core/personal lane (company null) emits nothing. */
84
+ export declare function laneCanEmit(lane: Pick<LaneRecord, "company" | "thread_id">): lane is Pick<LaneRecord, "company" | "thread_id"> & {
85
+ company: LaneCompanyRef;
86
+ thread_id: string;
87
+ };
88
+ /**
89
+ * Spec §8 terminal-envelope events. `done` → lane_done and story → review;
90
+ * `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
91
+ * never set to `done` here: promotion belongs to the senior.
92
+ */
93
+ export declare function emitLaneDecisionEvents(mesh: LaneMesh, lane: LaneRecord, envelope: Pick<LaneEnvelope, "decision" | "summary" | "blocked_reason">, opts?: {
94
+ questionIds?: string[];
95
+ signal?: AbortSignal;
96
+ }): Promise<void>;
97
+ export declare function emitLaneProgressEvent(mesh: LaneMesh, lane: LaneRecord, detail: string, signal?: AbortSignal): Promise<void>;
98
+ /**
99
+ * Throttle for lane_progress (spec §8 progress row). True when no progress
100
+ * event has been sent yet, or the last one is at least `minIntervalMs` old.
101
+ * The caller only asks on a tick that saw new activity; a silent tick never
102
+ * reaches this function.
103
+ */
104
+ export declare function progressEventDue(input: {
105
+ lastEmitMs: number | null;
106
+ nowMs: number;
107
+ minIntervalMs: number;
108
+ }): boolean;
109
+ //# sourceMappingURL=mesh-events.d.ts.map