@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,350 @@
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
+ /**
12
+ * The envelope contract as a JSON schema. `parseLaneEnvelope` is the runtime
13
+ * check; this is the text handed to a worker on the repair turn (spec §4) so
14
+ * the two never drift apart in what they name.
15
+ */
16
+ export const LANE_ENVELOPE_JSON_SCHEMA = {
17
+ $schema: "https://json-schema.org/draft/2020-12/schema",
18
+ title: "hq-lane-envelope.v1",
19
+ type: "object",
20
+ additionalProperties: false,
21
+ required: ["schema_version", "decision", "summary", "story_status", "artifacts"],
22
+ properties: {
23
+ schema_version: { const: "hq-lane-envelope.v1" },
24
+ decision: { enum: ["done", "ask", "blocked"] },
25
+ summary: { type: "string", minLength: 1 },
26
+ questions: {
27
+ type: "array",
28
+ items: {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ required: ["question", "context", "options"],
32
+ properties: {
33
+ question: { type: "string" },
34
+ context: { type: "string" },
35
+ options: { type: "array", items: { type: "string" } },
36
+ recommended: { type: "string" },
37
+ },
38
+ },
39
+ },
40
+ story_status: { enum: ["queued", "in_progress", "review", "done"] },
41
+ artifacts: { type: "array" },
42
+ blocked_reason: { type: "string" },
43
+ story_complete: { type: "boolean" },
44
+ story_complete_reason: { type: "string", minLength: 1 },
45
+ },
46
+ allOf: [
47
+ {
48
+ if: { properties: { decision: { const: "ask" } } },
49
+ then: { required: ["questions"], properties: { questions: { minItems: 1 } } },
50
+ else: { not: { required: ["questions"] } },
51
+ },
52
+ {
53
+ if: { properties: { decision: { const: "blocked" } } },
54
+ then: { required: ["blocked_reason"], properties: { blocked_reason: { minLength: 1 } } },
55
+ else: { not: { required: ["blocked_reason"] } },
56
+ },
57
+ {
58
+ if: { required: ["story_complete"], properties: { story_complete: { const: true } } },
59
+ then: {
60
+ required: ["story_complete_reason"],
61
+ properties: { story_status: { const: "done" }, story_complete_reason: { minLength: 1 } },
62
+ },
63
+ else: {
64
+ if: { required: ["story_complete"] },
65
+ then: {
66
+ required: ["story_complete_reason"],
67
+ properties: { story_status: { not: { const: "done" } }, story_complete_reason: { minLength: 1 } },
68
+ },
69
+ else: { not: { required: ["story_complete_reason"] } },
70
+ },
71
+ },
72
+ ],
73
+ description: "questions is present and non-empty exactly when decision is ask. " +
74
+ "blocked_reason is present exactly when decision is blocked. " +
75
+ "story_complete true requires story_status done; story_complete false requires story_status other than done; " +
76
+ "story_complete_reason is required whenever story_complete is present.",
77
+ };
78
+ export const LANE_STATES = [
79
+ "queued",
80
+ "running",
81
+ "awaiting_input",
82
+ "blocked",
83
+ "done",
84
+ "failed",
85
+ "interrupted",
86
+ "cancelled",
87
+ "killed",
88
+ ];
89
+ /** Exact terminal set from spec §6.7. `awaiting_input` and `blocked` are not terminal. */
90
+ export const TERMINAL_LANE_STATES = [
91
+ "done",
92
+ "failed",
93
+ "interrupted",
94
+ "cancelled",
95
+ "killed",
96
+ ];
97
+ export const LANE_PROVIDERS = ["claude", "codex", "grok"];
98
+ export const QUEUED_REASONS = ["cap", "queue_position", "throttle", "provider_binary_missing"];
99
+ export const HOST_KINDS = ["local", "outpost"];
100
+ export const SENIOR_KINDS = ["session", "lane", "user"];
101
+ export const ENVELOPE_DECISIONS = ["done", "ask", "blocked"];
102
+ export const STORY_STATUSES = [
103
+ "queued",
104
+ "in_progress",
105
+ "review",
106
+ "done",
107
+ ];
108
+ /**
109
+ * `expired` is accepted on read for records that already carry it. Spec §9
110
+ * ageing advances hop with `unanswered_timeout` and never expires a question,
111
+ * so reconcile (the only writer that may age without an operator) does not
112
+ * write this value. Kept in the enum so `--status expired` and old files parse.
113
+ */
114
+ export const QUESTION_STATUSES = [
115
+ "pending",
116
+ "answered",
117
+ "cancelled",
118
+ "expired",
119
+ ];
120
+ export const RAISED_VIA = ["cli", "envelope"];
121
+ export const ROUTING_REASONS = [
122
+ "asked",
123
+ "escalated",
124
+ "audience_ended",
125
+ "reparented",
126
+ "unanswered_timeout",
127
+ "cancelled",
128
+ ];
129
+ export const QUESTION_BODY_STORES = ["company", "personal"];
130
+ export const LIVENESS_STATES = ["live", "idle", "ended", "lost"];
131
+ export const HISTORY_CAP = 50;
132
+ const TERMINAL_SET = new Set(TERMINAL_LANE_STATES);
133
+ export function isLaneState(value) {
134
+ return typeof value === "string" && LANE_STATES.includes(value);
135
+ }
136
+ export function isTerminalLaneState(value) {
137
+ return typeof value === "string" && TERMINAL_SET.has(value);
138
+ }
139
+ export function isLaneProvider(value) {
140
+ return (typeof value === "string" &&
141
+ LANE_PROVIDERS.includes(value));
142
+ }
143
+ export function isSeniorKind(value) {
144
+ return (typeof value === "string" &&
145
+ SENIOR_KINDS.includes(value));
146
+ }
147
+ export function isHostKind(value) {
148
+ return (typeof value === "string" &&
149
+ HOST_KINDS.includes(value));
150
+ }
151
+ export function isEnvelopeDecision(value) {
152
+ return (typeof value === "string" &&
153
+ ENVELOPE_DECISIONS.includes(value));
154
+ }
155
+ export function isQuestionStatus(value) {
156
+ return (typeof value === "string" &&
157
+ QUESTION_STATUSES.includes(value));
158
+ }
159
+ export function isLivenessState(value) {
160
+ return (typeof value === "string" &&
161
+ LIVENESS_STATES.includes(value));
162
+ }
163
+ export function parseLaneProvider(value) {
164
+ if (isLaneProvider(value))
165
+ return value;
166
+ throw new LanesError("unknown_provider", `Unknown provider ${JSON.stringify(value)}. Accepted: ${LANE_PROVIDERS.join(", ")}`, { accepted: [...LANE_PROVIDERS], value });
167
+ }
168
+ export function parseLaneState(value) {
169
+ if (isLaneState(value))
170
+ return value;
171
+ throw new LanesError("unknown_state", `Unknown lane state ${JSON.stringify(value)}. Accepted: ${LANE_STATES.join(", ")}`, { accepted: [...LANE_STATES], value });
172
+ }
173
+ export function parseQuestionStatus(value) {
174
+ if (isQuestionStatus(value))
175
+ return value;
176
+ throw new LanesError("unknown_status", `Unknown question status ${JSON.stringify(value)}. Accepted: ${QUESTION_STATUSES.join(", ")}`, { accepted: [...QUESTION_STATUSES], value });
177
+ }
178
+ export function parseLivenessState(value) {
179
+ if (isLivenessState(value))
180
+ return value;
181
+ throw new LanesError("unknown_state", `Unknown session liveness state ${JSON.stringify(value)}. Accepted: ${LIVENESS_STATES.join(", ")}`, { accepted: [...LIVENESS_STATES], value });
182
+ }
183
+ export function parseSeniorKind(value) {
184
+ if (isSeniorKind(value))
185
+ return value;
186
+ throw new LanesError("unknown_senior_kind", `Unknown senior kind ${JSON.stringify(value)}. Accepted: ${SENIOR_KINDS.join(", ")}`, { accepted: [...SENIOR_KINDS], value });
187
+ }
188
+ /** The v1 fields that must exist on every durable lane record (spec §3). */
189
+ export const REQUIRED_LANE_RECORD_FIELDS = [
190
+ "schema_version", "lane_id", "company", "project_id", "story_id", "depth",
191
+ "senior", "senior_history", "state", "priority", "provider", "host",
192
+ "version", "created_at", "updated_at", "attribution_history",
193
+ ];
194
+ export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spawn"];
195
+ /** Field ownership at the durable-record mutation boundary (spec §10.1). */
196
+ export const LANE_FIELD_WRITERS = {
197
+ schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
198
+ project_id: ["command"], story_id: ["command"], thread_id: ["creator", "command"],
199
+ depth: ["command"], senior: ["command"], senior_history: ["command"],
200
+ state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "reconcile"],
201
+ priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"],
202
+ provider: ["creator"], account: ["creator", "spawn"], host: ["creator"], origin_host: ["creator"], origin_host_source: ["creator"],
203
+ version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
204
+ last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"],
205
+ last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
206
+ overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
207
+ admitted_reason: ["reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], terminal_details: ["command", "spawn"],
208
+ quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
209
+ provider_session_id: ["watcher"], provider_session_id_absent: ["watcher"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], last_progress_event_at: ["watcher"],
210
+ };
211
+ function isPlainObject(value) {
212
+ return value !== null && typeof value === "object" && !Array.isArray(value);
213
+ }
214
+ function requireString(value, field) {
215
+ if (typeof value !== "string" || value.length === 0) {
216
+ throw new LaneSchemaError(`Expected non-empty string for ${field}`, {
217
+ field,
218
+ });
219
+ }
220
+ return value;
221
+ }
222
+ export function parseMajorSchema(schemaVersion, expectedPrefix, expectedMajor) {
223
+ const match = new RegExp(`^${expectedPrefix}\\.v(\\d+)$`).exec(schemaVersion);
224
+ if (!match) {
225
+ throw new LaneSchemaError(`Unknown schema_version ${JSON.stringify(schemaVersion)} (expected ${expectedPrefix}.v${expectedMajor})`, { schema_version: schemaVersion, expected: `${expectedPrefix}.v${expectedMajor}` });
226
+ }
227
+ const major = Number(match[1]);
228
+ if (major !== expectedMajor) {
229
+ throw new LaneSchemaError(`Unsupported ${expectedPrefix} major version ${major} (found ${schemaVersion})`, { schema_version: schemaVersion, major });
230
+ }
231
+ }
232
+ export function parseLaneEnvelope(raw) {
233
+ if (!isPlainObject(raw)) {
234
+ throw new LaneSchemaError("Lane envelope must be a JSON object");
235
+ }
236
+ const extra = Object.keys(raw).filter((key) => ![
237
+ "schema_version",
238
+ "decision",
239
+ "summary",
240
+ "questions",
241
+ "story_status",
242
+ "artifacts",
243
+ "blocked_reason",
244
+ "story_complete",
245
+ "story_complete_reason",
246
+ ].includes(key));
247
+ if (extra.length > 0) {
248
+ throw new LaneSchemaError(`Lane envelope rejects unknown properties: ${extra.join(", ")}`, { extra });
249
+ }
250
+ const schema_version = requireString(raw.schema_version, "schema_version");
251
+ parseMajorSchema(schema_version, "hq-lane-envelope", 1);
252
+ if (!isEnvelopeDecision(raw.decision)) {
253
+ throw new LaneSchemaError(`Unknown envelope decision ${JSON.stringify(raw.decision)}. Accepted: ${ENVELOPE_DECISIONS.join(", ")}`, { accepted: [...ENVELOPE_DECISIONS], value: raw.decision });
254
+ }
255
+ const summary = requireString(raw.summary, "summary");
256
+ if (typeof raw.story_status !== "string" ||
257
+ !STORY_STATUSES.includes(raw.story_status)) {
258
+ throw new LaneSchemaError(`Unknown story_status ${JSON.stringify(raw.story_status)}. Accepted: ${STORY_STATUSES.join(", ")}`, { accepted: [...STORY_STATUSES], value: raw.story_status });
259
+ }
260
+ if (!Array.isArray(raw.artifacts)) {
261
+ throw new LaneSchemaError("Envelope artifacts must be an array");
262
+ }
263
+ const questions = raw.questions;
264
+ if (raw.decision === "ask") {
265
+ if (!Array.isArray(questions) || questions.length === 0) {
266
+ throw new LaneSchemaError('questions must be non-empty when decision is "ask"');
267
+ }
268
+ }
269
+ else if (questions !== undefined) {
270
+ throw new LaneSchemaError('questions is present exactly when decision is "ask"');
271
+ }
272
+ if (raw.decision === "blocked") {
273
+ if (typeof raw.blocked_reason !== "string" || raw.blocked_reason.length === 0) {
274
+ throw new LaneSchemaError('blocked_reason is required when decision is "blocked"');
275
+ }
276
+ }
277
+ else if (raw.blocked_reason !== undefined) {
278
+ throw new LaneSchemaError('blocked_reason is present exactly when decision is "blocked"');
279
+ }
280
+ const storyStatus = raw.story_status;
281
+ let storyComplete;
282
+ let storyCompleteReason;
283
+ if (raw.story_complete !== undefined) {
284
+ if (typeof raw.story_complete !== "boolean") {
285
+ throw new LaneSchemaError("story_complete must be a boolean", {
286
+ field: "story_complete",
287
+ });
288
+ }
289
+ if (typeof raw.story_complete_reason !== "string" ||
290
+ raw.story_complete_reason.trim().length === 0) {
291
+ throw new LaneSchemaError("story_complete_reason is required when story_complete is present", { field: "story_complete_reason" });
292
+ }
293
+ if (raw.story_complete === true && storyStatus !== "done") {
294
+ throw new LaneSchemaError("story_complete true requires story_status done", {
295
+ error: "story_complete_status_mismatch",
296
+ story_complete: true,
297
+ story_status: storyStatus,
298
+ });
299
+ }
300
+ if (raw.story_complete === false && storyStatus === "done") {
301
+ throw new LaneSchemaError("story_complete false forbids story_status done", {
302
+ error: "story_complete_status_mismatch",
303
+ story_complete: false,
304
+ story_status: storyStatus,
305
+ });
306
+ }
307
+ storyComplete = raw.story_complete;
308
+ storyCompleteReason = raw.story_complete_reason.trim();
309
+ }
310
+ else if (raw.story_complete_reason !== undefined) {
311
+ throw new LaneSchemaError("story_complete_reason is present exactly when story_complete is present", { field: "story_complete_reason" });
312
+ }
313
+ const envelope = {
314
+ schema_version: ENVELOPE_SCHEMA_VERSION,
315
+ decision: raw.decision,
316
+ summary,
317
+ story_status: storyStatus,
318
+ artifacts: raw.artifacts,
319
+ };
320
+ if (storyComplete !== undefined) {
321
+ envelope.story_complete = storyComplete;
322
+ envelope.story_complete_reason = storyCompleteReason;
323
+ }
324
+ if (Array.isArray(questions) && questions.length > 0) {
325
+ envelope.questions = questions.map((item, index) => {
326
+ if (!isPlainObject(item)) {
327
+ throw new LaneSchemaError(`questions[${index}] must be an object`);
328
+ }
329
+ const options = item.options;
330
+ if (!Array.isArray(options) ||
331
+ options.some((opt) => typeof opt !== "string")) {
332
+ throw new LaneSchemaError(`questions[${index}].options must be string[]`);
333
+ }
334
+ const parsed = {
335
+ question: requireString(item.question, `questions[${index}].question`),
336
+ context: requireString(item.context, `questions[${index}].context`),
337
+ options,
338
+ };
339
+ if (item.recommended !== undefined) {
340
+ parsed.recommended = requireString(item.recommended, `questions[${index}].recommended`);
341
+ }
342
+ return parsed;
343
+ });
344
+ }
345
+ if (typeof raw.blocked_reason === "string") {
346
+ envelope.blocked_reason = raw.blocked_reason;
347
+ }
348
+ return envelope;
349
+ }
350
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Detached entry for the per-lane watcher. Spawned with
3
+ * `detached: true` from `spawnLaneWorker`; not a session job.
4
+ *
5
+ * Usage: node watcher-main.js <lane-id>
6
+ * Requires HQ_ROOT in the environment.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=watcher-main.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Detached entry for the per-lane watcher. Spawned with
3
+ * `detached: true` from `spawnLaneWorker`; not a session job.
4
+ *
5
+ * Usage: node watcher-main.js <lane-id>
6
+ * Requires HQ_ROOT in the environment.
7
+ */
8
+ import { runWatcherLoop } from "./watcher.js";
9
+ function main() {
10
+ const laneId = process.argv[2]?.trim();
11
+ const hqRoot = process.env.HQ_ROOT?.trim();
12
+ if (!laneId || !hqRoot) {
13
+ process.stderr.write("hq lanes watcher: requires lane id argv and HQ_ROOT in the environment\n");
14
+ process.exit(1);
15
+ }
16
+ void runWatcherLoop(hqRoot, laneId).catch((err) => {
17
+ process.stderr.write(`hq lanes watcher: ${err instanceof Error ? err.message : String(err)}${err instanceof Error && err.stack ? `\n${err.stack}` : ""}\n`);
18
+ process.exit(1);
19
+ });
20
+ }
21
+ main();
22
+ //# sourceMappingURL=watcher-main.js.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Per-lane watcher (spec §5.7).
3
+ *
4
+ * Armed at spawn, detached from the caller. Emits on progress, terminal, and
5
+ * silence (20 minutes with the process still alive). Terminal classification
6
+ * follows spec §5: an envelope decides done/ask/blocked; a process that exits
7
+ * with partial output and no envelope is `interrupted`; silence is never
8
+ * success.
9
+ *
10
+ * Progress has two sources. Output files (worker.out, worker.err,
11
+ * last-message.txt) cover codex and grok, which stream as they work. They do
12
+ * not cover Claude: `claude -p --output-format json` writes nothing until it
13
+ * exits, so a healthy Claude lane used to hit the silence deadline at exactly
14
+ * 20 minutes every time, and a hung one looked identical. The second source
15
+ * is logs/tool-activity.json, which the drain hook rewrites on every
16
+ * PreToolUse call (dropbox.ts). A worker that is working calls tools; a
17
+ * worker that has not called a tool or written output for the whole window
18
+ * is `stalled`, and the watcher says so on the lane record.
19
+ *
20
+ * What this cannot tell apart: a worker hung inside one tool call from a
21
+ * worker running one legitimately long tool call (a 25-minute build); a
22
+ * worker looping on tool calls without making progress from a working one;
23
+ * a model stuck mid-generation with no tool call from one that is writing a
24
+ * long answer. It also depends on the drain hook being registered in the
25
+ * provider home (hooks-install.ts): with no hook there is no touch, and the
26
+ * lane reads as stalled every window, which is the pre-existing behaviour.
27
+ * Distinguishing a hung tool from a slow one needs the provider to report
28
+ * per-call state, which none of the three CLIs expose today. `stalled` is
29
+ * therefore a flag for the senior to look at, never a kill.
30
+ *
31
+ * Queue admission of the next lane when a slot frees is owned by the
32
+ * periodic reconcile pass and is out of scope here.
33
+ */
34
+ import { type LaneEnvelope, type LaneRecord, type LaneState } from "./types.js";
35
+ import { type QuotaSignal } from "./quota.js";
36
+ import { type LaneMesh } from "./mesh-events.js";
37
+ import { type EnvelopeRepairSpawnInput } from "./spawn.js";
38
+ /** Spec §5.7: no signal for 20 minutes while the process is still alive. */
39
+ export declare const SILENCE_MS: number;
40
+ export declare const WATCHER_POLL_MS = 5000;
41
+ /** Synced last_host_report_at cadence. Reconcile dead-host TTL is 20 minutes. */
42
+ export declare const WATCHER_HEARTBEAT_MS: number;
43
+ /**
44
+ * Cap on logs/watcher.out. A decision line is typically under 300 bytes, so
45
+ * 256 KiB holds hundreds of transitions. A stuck lane cannot fill the disk:
46
+ * once the file would exceed this, the oldest complete lines are dropped.
47
+ */
48
+ export declare const WATCHER_OUT_MAX_BYTES: number;
49
+ export declare const WATCHER_DECISION_EVENTS: readonly ["liveness_transition", "silence_deadline", "envelope_observed", "envelope_invalid", "envelope_repair", "classification", "terminal_write", "mesh_event", "mesh_event_failed"];
50
+ /** Terminal reason when the envelope failed to parse after the repair turn (spec §4). */
51
+ export declare const ENVELOPE_INVALID_REASON: "envelope_invalid";
52
+ export type WatcherDecisionEvent = (typeof WATCHER_DECISION_EVENTS)[number];
53
+ export declare const WATCHER_SIGNAL_KINDS: readonly ["progress", "terminal", "silence"];
54
+ export type WatcherSignalKind = (typeof WATCHER_SIGNAL_KINDS)[number];
55
+ export interface TerminalClassification {
56
+ state: Extract<LaneState, "done" | "awaiting_input" | "blocked" | "interrupted" | "failed">;
57
+ reason?: string;
58
+ }
59
+ export interface WatcherSignal {
60
+ kind: WatcherSignalKind;
61
+ at: string;
62
+ classification?: TerminalClassification;
63
+ }
64
+ export interface WatcherTickInput {
65
+ nowMs: number;
66
+ processAlive: boolean;
67
+ progressed: boolean;
68
+ lastSignalMs: number;
69
+ envelope: LaneEnvelope | null;
70
+ hasPartialOutput: boolean;
71
+ silenceMs?: number;
72
+ quota?: QuotaSignal | null;
73
+ invalidEnvelope?: {
74
+ errors: string[];
75
+ } | null;
76
+ }
77
+ export interface WatcherTickResult {
78
+ emit?: WatcherSignal;
79
+ nextLastSignalMs: number;
80
+ done: boolean;
81
+ }
82
+ export declare function parseWatcherSignalKind(value: unknown): WatcherSignalKind;
83
+ export declare function classifyLaneExit(input: {
84
+ envelope: LaneEnvelope | null;
85
+ hasPartialOutput: boolean;
86
+ /**
87
+ * Set when the worker's own result is a 429 or a usage-limit error.
88
+ * Distinct from a crash: the reason is account_quota_exhausted or
89
+ * account_model_unavailable, not exited_with_partial_output.
90
+ */
91
+ quota?: QuotaSignal | null;
92
+ /**
93
+ * Set when the run's envelope file exists but does not parse and no
94
+ * repair turn remains. Failed, regardless of partial output: the worker
95
+ * did finish, it just did not honour the contract.
96
+ */
97
+ invalidEnvelope?: {
98
+ errors: string[];
99
+ } | null;
100
+ }): TerminalClassification;
101
+ export declare function tickWatcher(input: WatcherTickInput): WatcherTickResult;
102
+ export declare function watcherEventsPath(hqRoot: string, laneId: string): string;
103
+ export declare function watcherOutLogPath(hqRoot: string, laneId: string): string;
104
+ export declare function appendCappedLog(filePath: string, line: string, maxBytes?: number): void;
105
+ export declare function writeWatcherDecision(filePath: string, record: Record<string, unknown>, maxBytes?: number): void;
106
+ export declare function watcherLastSignalPath(hqRoot: string, laneId: string): string;
107
+ export declare function appendWatcherSignal(hqRoot: string, laneId: string, signal: WatcherSignal): void;
108
+ /**
109
+ * Slot-free notification. The periodic reconcile pass admits the next queued
110
+ * lane; this watcher must not do it. Intentionally empty.
111
+ */
112
+ export declare function notifySlotFreed(_input: {
113
+ hqRoot: string;
114
+ provider: LaneRecord["provider"];
115
+ account?: string;
116
+ }): void;
117
+ export interface ApplyTerminalOpts {
118
+ envelope?: LaneEnvelope | null;
119
+ envelopeRef?: string;
120
+ at?: string;
121
+ quota?: QuotaSignal | null;
122
+ }
123
+ export declare function stalledReason(silenceMs: number): string;
124
+ /**
125
+ * The consumer of the silence signal. A silence emit on a running lane sets
126
+ * `stalled` on the record so `hq lanes list` and `show` surface it; the next
127
+ * progress emit clears it. Nothing here changes `state`.
128
+ */
129
+ export declare function markLaneStalled(hqRoot: string, laneId: string, at: string, reason: string): LaneRecord;
130
+ export declare function clearLaneStalled(hqRoot: string, laneId: string): LaneRecord;
131
+ export declare function applyTerminalToLane(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): LaneRecord;
132
+ /** Terminal write outcome for emitters: only the CAS winner may publish mesh effects. */
133
+ export declare function applyTerminalToLaneWithResult(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): {
134
+ record: LaneRecord;
135
+ changed: boolean;
136
+ };
137
+ export declare function isPidAlive(pid: number): boolean;
138
+ export interface LatestEnvelope {
139
+ envelope: LaneEnvelope;
140
+ ref: string;
141
+ }
142
+ /** The newest envelope file of this run, parsed or not. */
143
+ export interface LatestEnvelopeFile {
144
+ ref: string;
145
+ /** Absolute path of the file. */
146
+ path: string;
147
+ text: string;
148
+ envelope: LaneEnvelope | null;
149
+ /** Parser message when `envelope` is null. */
150
+ error: string | null;
151
+ }
152
+ export declare function readLatestEnvelope(hqRoot: string, laneId: string, minName?: string): LaneEnvelope | null;
153
+ /** Valid-only view over `readLatestEnvelopeFile`; an unparseable file reads as absent. */
154
+ export declare function readLatestEnvelopeRecord(hqRoot: string, laneId: string, minName?: string): LatestEnvelope | null;
155
+ /**
156
+ * `minName` is this run's envelope slot (basename from spawn.json). Envelopes
157
+ * that sort before it belong to an earlier run of the same lane and must not
158
+ * classify this exit: a resumed run that dies before writing its own envelope
159
+ * would otherwise be marked done again from the previous one.
160
+ */
161
+ export declare function readLatestEnvelopeFile(hqRoot: string, laneId: string, minName?: string): LatestEnvelopeFile | null;
162
+ export interface WatcherLoopDeps {
163
+ sleep?: (ms: number) => Promise<void>;
164
+ now?: () => number;
165
+ isAlive?: (pid: number) => boolean;
166
+ readStarttime?: (pid: number) => number | null;
167
+ pollMs?: number;
168
+ silenceMs?: number;
169
+ watcherOutPath?: string;
170
+ watcherOutMaxBytes?: number;
171
+ /**
172
+ * Mesh seam (spec §8). Default: `createLaneMesh({ interactive: false })`.
173
+ * `null` disables emission entirely (tests that are not about mesh).
174
+ */
175
+ mesh?: LaneMesh | null;
176
+ /** Override for `providers.<provider>.progress_event_min_interval_ms`. */
177
+ progressEventMinIntervalMs?: number;
178
+ /** Bound a hung mesh request so watcher liveness polling continues. */
179
+ meshRequestTimeoutMs?: number;
180
+ /** Spec §4 repair spawn. Default: `spawnEnvelopeRepair`. */
181
+ spawnRepair?: (hqRoot: string, lane: LaneRecord, input: EnvelopeRepairSpawnInput) => number;
182
+ }
183
+ /**
184
+ * Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
185
+ * the event name and the error message, and never propagates: the lane's
186
+ * state is decided by the envelope and the process, not by the network.
187
+ */
188
+ export declare function emitMeshSoft(event: string, logDecision: (record: Record<string, unknown>) => void, at: string, fn: (signal: AbortSignal) => Promise<void>, timeoutMs?: number): Promise<boolean>;
189
+ /** Question ids an ask envelope raised, as `raiseEnvelopeQuestions` mints them. */
190
+ export declare function envelopeQuestionIds(laneId: string, envelopeRef: string, envelope: LaneEnvelope): string[];
191
+ export declare function runWatcherLoop(hqRoot: string, laneId: string, deps?: WatcherLoopDeps): Promise<void>;
192
+ export type EnvelopeRepairDecision = {
193
+ action: "spawned";
194
+ } | {
195
+ action: "give_up";
196
+ errors: string[];
197
+ };
198
+ /**
199
+ * Spec §4 repair decision for an exited worker whose envelope file does not
200
+ * parse. First failure on a resumable provider with a recorded session id:
201
+ * record `envelope_repair` on the lane and spawn the repair turn. A provider
202
+ * with no session resume gets the same single attempt as a fresh turn. Anything
203
+ * else (already attempted, no session id on a resumable provider, spawn
204
+ * failure): give up with every error collected, and the caller classifies
205
+ * the lane `failed` with reason `envelope_invalid`.
206
+ */
207
+ export declare function decideEnvelopeRepair(hqRoot: string, laneId: string, latest: LatestEnvelopeFile, nowMs: number, spawnRepair: NonNullable<WatcherLoopDeps["spawnRepair"]>, logDecision: (record: Record<string, unknown>) => void): EnvelopeRepairDecision;
208
+ /** Exported so tests can prove the watcher reads the live record, not spawn env. */
209
+ export declare function laneIdentityFromRecord(hqRoot: string, laneId: string): {
210
+ lane_id: string;
211
+ company: LaneRecord["company"];
212
+ project_id: string;
213
+ story_id: string;
214
+ senior: LaneRecord["senior"];
215
+ };
216
+ //# sourceMappingURL=watcher.d.ts.map