@indigoai-us/hq-cli 5.126.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.
- package/CHANGELOG.md +211 -0
- package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
- package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
- package/dist/command-catalog.generated.d.ts +245 -11
- package/dist/command-catalog.generated.js +323 -11
- package/dist/command-registration-plan.d.ts +1 -0
- package/dist/command-registration-plan.js +11 -2
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/lanes.d.ts +55 -16
- package/dist/commands/lanes.js +1187 -250
- package/dist/commands/mesh.js +254 -19
- package/dist/lib/lanes/accounts.d.ts +67 -7
- package/dist/lib/lanes/accounts.js +233 -43
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +15 -0
- package/dist/lib/lanes/config.js +65 -8
- package/dist/lib/lanes/delivery.d.ts +151 -0
- package/dist/lib/lanes/delivery.js +354 -0
- package/dist/lib/lanes/dropbox.d.ts +96 -0
- package/dist/lib/lanes/dropbox.js +408 -0
- package/dist/lib/lanes/effort.d.ts +50 -0
- package/dist/lib/lanes/effort.js +109 -0
- package/dist/lib/lanes/errors.d.ts +2 -2
- package/dist/lib/lanes/hooks-install.d.ts +81 -0
- package/dist/lib/lanes/hooks-install.js +326 -0
- package/dist/lib/lanes/hq-root.d.ts +19 -0
- package/dist/lib/lanes/hq-root.js +28 -0
- package/dist/lib/lanes/index.d.ts +35 -5
- package/dist/lib/lanes/index.js +19 -4
- package/dist/lib/lanes/liveness.d.ts +7 -1
- package/dist/lib/lanes/liveness.js +64 -5
- package/dist/lib/lanes/machine-identity.d.ts +34 -0
- package/dist/lib/lanes/machine-identity.js +92 -0
- package/dist/lib/lanes/mesh-events.d.ts +109 -0
- package/dist/lib/lanes/mesh-events.js +275 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
- package/dist/lib/lanes/outpost-dispatch.js +90 -0
- package/dist/lib/lanes/questions.d.ts +75 -0
- package/dist/lib/lanes/questions.js +243 -0
- package/dist/lib/lanes/quota.d.ts +95 -0
- package/dist/lib/lanes/quota.js +320 -0
- package/dist/lib/lanes/reconcile-install.d.ts +69 -0
- package/dist/lib/lanes/reconcile-install.js +221 -0
- package/dist/lib/lanes/reconcile.d.ts +102 -0
- package/dist/lib/lanes/reconcile.js +985 -0
- package/dist/lib/lanes/registry.d.ts +18 -5
- package/dist/lib/lanes/registry.js +293 -19
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +7 -3
- package/dist/lib/lanes/store.js +150 -32
- package/dist/lib/lanes/types.d.ts +346 -4
- package/dist/lib/lanes/types.js +138 -5
- package/dist/lib/lanes/watcher-main.d.ts +9 -0
- package/dist/lib/lanes/watcher-main.js +22 -0
- package/dist/lib/lanes/watcher.d.ts +216 -0
- package/dist/lib/lanes/watcher.js +859 -0
- package/dist/lib/mesh/api.d.ts +71 -7
- package/dist/lib/mesh/api.js +167 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
- package/dist/lib/mesh/live/backfill-held.js +66 -21
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +111 -54
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
- package/dist/lib/scan-packages/index.d.ts +2 -0
- package/dist/lib/scan-packages/index.js +1 -1
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/package.json +3 -1
|
@@ -7,6 +7,158 @@ export declare const LANE_SCHEMA_VERSION: "hq-lane.v1";
|
|
|
7
7
|
export declare const QUESTION_SCHEMA_VERSION: "hq-lane-question.v1";
|
|
8
8
|
export declare const ENVELOPE_SCHEMA_VERSION: "hq-lane-envelope.v1";
|
|
9
9
|
export declare const CONFIG_SCHEMA_VERSION: "hq-lanes-config.v1";
|
|
10
|
+
/**
|
|
11
|
+
* The envelope contract as a JSON schema. `parseLaneEnvelope` is the runtime
|
|
12
|
+
* check; this is the text handed to a worker on the repair turn (spec §4) so
|
|
13
|
+
* the two never drift apart in what they name.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LANE_ENVELOPE_JSON_SCHEMA: {
|
|
16
|
+
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
17
|
+
readonly title: "hq-lane-envelope.v1";
|
|
18
|
+
readonly type: "object";
|
|
19
|
+
readonly additionalProperties: false;
|
|
20
|
+
readonly required: readonly ["schema_version", "decision", "summary", "story_status", "artifacts"];
|
|
21
|
+
readonly properties: {
|
|
22
|
+
readonly schema_version: {
|
|
23
|
+
readonly const: "hq-lane-envelope.v1";
|
|
24
|
+
};
|
|
25
|
+
readonly decision: {
|
|
26
|
+
readonly enum: readonly ["done", "ask", "blocked"];
|
|
27
|
+
};
|
|
28
|
+
readonly summary: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
readonly minLength: 1;
|
|
31
|
+
};
|
|
32
|
+
readonly questions: {
|
|
33
|
+
readonly type: "array";
|
|
34
|
+
readonly items: {
|
|
35
|
+
readonly type: "object";
|
|
36
|
+
readonly additionalProperties: false;
|
|
37
|
+
readonly required: readonly ["question", "context", "options"];
|
|
38
|
+
readonly properties: {
|
|
39
|
+
readonly question: {
|
|
40
|
+
readonly type: "string";
|
|
41
|
+
};
|
|
42
|
+
readonly context: {
|
|
43
|
+
readonly type: "string";
|
|
44
|
+
};
|
|
45
|
+
readonly options: {
|
|
46
|
+
readonly type: "array";
|
|
47
|
+
readonly items: {
|
|
48
|
+
readonly type: "string";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
readonly recommended: {
|
|
52
|
+
readonly type: "string";
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
readonly story_status: {
|
|
58
|
+
readonly enum: readonly ["queued", "in_progress", "review", "done"];
|
|
59
|
+
};
|
|
60
|
+
readonly artifacts: {
|
|
61
|
+
readonly type: "array";
|
|
62
|
+
};
|
|
63
|
+
readonly blocked_reason: {
|
|
64
|
+
readonly type: "string";
|
|
65
|
+
};
|
|
66
|
+
readonly story_complete: {
|
|
67
|
+
readonly type: "boolean";
|
|
68
|
+
};
|
|
69
|
+
readonly story_complete_reason: {
|
|
70
|
+
readonly type: "string";
|
|
71
|
+
readonly minLength: 1;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
readonly allOf: readonly [{
|
|
75
|
+
readonly if: {
|
|
76
|
+
readonly properties: {
|
|
77
|
+
readonly decision: {
|
|
78
|
+
readonly const: "ask";
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
readonly then: {
|
|
83
|
+
readonly required: readonly ["questions"];
|
|
84
|
+
readonly properties: {
|
|
85
|
+
readonly questions: {
|
|
86
|
+
readonly minItems: 1;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
readonly else: {
|
|
91
|
+
readonly not: {
|
|
92
|
+
readonly required: readonly ["questions"];
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
}, {
|
|
96
|
+
readonly if: {
|
|
97
|
+
readonly properties: {
|
|
98
|
+
readonly decision: {
|
|
99
|
+
readonly const: "blocked";
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
readonly then: {
|
|
104
|
+
readonly required: readonly ["blocked_reason"];
|
|
105
|
+
readonly properties: {
|
|
106
|
+
readonly blocked_reason: {
|
|
107
|
+
readonly minLength: 1;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
readonly else: {
|
|
112
|
+
readonly not: {
|
|
113
|
+
readonly required: readonly ["blocked_reason"];
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
}, {
|
|
117
|
+
readonly if: {
|
|
118
|
+
readonly required: readonly ["story_complete"];
|
|
119
|
+
readonly properties: {
|
|
120
|
+
readonly story_complete: {
|
|
121
|
+
readonly const: true;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
readonly then: {
|
|
126
|
+
readonly required: readonly ["story_complete_reason"];
|
|
127
|
+
readonly properties: {
|
|
128
|
+
readonly story_status: {
|
|
129
|
+
readonly const: "done";
|
|
130
|
+
};
|
|
131
|
+
readonly story_complete_reason: {
|
|
132
|
+
readonly minLength: 1;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
readonly else: {
|
|
137
|
+
readonly if: {
|
|
138
|
+
readonly required: readonly ["story_complete"];
|
|
139
|
+
};
|
|
140
|
+
readonly then: {
|
|
141
|
+
readonly required: readonly ["story_complete_reason"];
|
|
142
|
+
readonly properties: {
|
|
143
|
+
readonly story_status: {
|
|
144
|
+
readonly not: {
|
|
145
|
+
readonly const: "done";
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
readonly story_complete_reason: {
|
|
149
|
+
readonly minLength: 1;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
readonly else: {
|
|
154
|
+
readonly not: {
|
|
155
|
+
readonly required: readonly ["story_complete_reason"];
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
}];
|
|
160
|
+
readonly description: string;
|
|
161
|
+
};
|
|
10
162
|
export declare const LANE_STATES: readonly ["queued", "running", "awaiting_input", "blocked", "done", "failed", "interrupted", "cancelled", "killed"];
|
|
11
163
|
export type LaneState = (typeof LANE_STATES)[number];
|
|
12
164
|
/** Exact terminal set from spec §6.7. `awaiting_input` and `blocked` are not terminal. */
|
|
@@ -14,7 +166,7 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
|
|
|
14
166
|
export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
|
|
15
167
|
export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
|
|
16
168
|
export type LaneProvider = (typeof LANE_PROVIDERS)[number];
|
|
17
|
-
export declare const QUEUED_REASONS: readonly ["cap", "queue_position", "throttle"];
|
|
169
|
+
export declare const QUEUED_REASONS: readonly ["cap", "queue_position", "throttle", "provider_binary_missing"];
|
|
18
170
|
export type QueuedReason = (typeof QUEUED_REASONS)[number];
|
|
19
171
|
export declare const HOST_KINDS: readonly ["local", "outpost"];
|
|
20
172
|
export type HostKind = (typeof HOST_KINDS)[number];
|
|
@@ -24,6 +176,12 @@ export declare const ENVELOPE_DECISIONS: readonly ["done", "ask", "blocked"];
|
|
|
24
176
|
export type EnvelopeDecision = (typeof ENVELOPE_DECISIONS)[number];
|
|
25
177
|
export declare const STORY_STATUSES: readonly ["queued", "in_progress", "review", "done"];
|
|
26
178
|
export type StoryStatus = (typeof STORY_STATUSES)[number];
|
|
179
|
+
/**
|
|
180
|
+
* `expired` is accepted on read for records that already carry it. Spec §9
|
|
181
|
+
* ageing advances hop with `unanswered_timeout` and never expires a question,
|
|
182
|
+
* so reconcile (the only writer that may age without an operator) does not
|
|
183
|
+
* write this value. Kept in the enum so `--status expired` and old files parse.
|
|
184
|
+
*/
|
|
27
185
|
export declare const QUESTION_STATUSES: readonly ["pending", "answered", "cancelled", "expired"];
|
|
28
186
|
export type QuestionStatus = (typeof QUESTION_STATUSES)[number];
|
|
29
187
|
export declare const RAISED_VIA: readonly ["cli", "envelope"];
|
|
@@ -82,7 +240,8 @@ export interface LastEnvelopeRef {
|
|
|
82
240
|
export interface LaneRecord {
|
|
83
241
|
schema_version: typeof LANE_SCHEMA_VERSION | string;
|
|
84
242
|
lane_id: string;
|
|
85
|
-
|
|
243
|
+
/** `null` is a core/personal lane (spec §6.2 / §10). Not an empty slug. */
|
|
244
|
+
company: LaneCompanyRef | null;
|
|
86
245
|
project_id: string;
|
|
87
246
|
story_id: string;
|
|
88
247
|
thread_id?: string;
|
|
@@ -93,14 +252,159 @@ export interface LaneRecord {
|
|
|
93
252
|
queued_reason?: QueuedReason;
|
|
94
253
|
priority: number;
|
|
95
254
|
worker?: string;
|
|
255
|
+
/** Registry-relative directory of the resolved worker, when --worker was set. */
|
|
256
|
+
worker_path?: string;
|
|
257
|
+
/** Package name when the worker's realpath is under core/packages. */
|
|
258
|
+
worker_pack?: string;
|
|
96
259
|
provider: LaneProvider;
|
|
97
260
|
account?: string;
|
|
98
261
|
host: LaneHost;
|
|
262
|
+
/**
|
|
263
|
+
* Creating machine's identity when `host.kind` is `outpost` (spec §5.2).
|
|
264
|
+
* Distinct from `host`, which names where the lane is meant to run.
|
|
265
|
+
*/
|
|
266
|
+
origin_host?: string;
|
|
267
|
+
/** How `origin_host` was resolved. Required to compare like-with-like. */
|
|
268
|
+
origin_host_source?: "imds" | "hostname";
|
|
99
269
|
version: number;
|
|
100
270
|
created_at: string;
|
|
101
271
|
updated_at: string;
|
|
102
272
|
attribution_history: AttributionHistoryEntry[];
|
|
103
273
|
last_envelope?: LastEnvelopeRef;
|
|
274
|
+
/** Effective model id recorded at create/spawn. Omitted when the provider chooses. */
|
|
275
|
+
model?: string;
|
|
276
|
+
/** Effective effort level passed to the provider at the last spawn (effort.ts). */
|
|
277
|
+
effort?: string;
|
|
278
|
+
/** Where `effort` came from. `cli` pins it across later spawns. */
|
|
279
|
+
effort_source?: "cli" | "account" | "provider" | "default_rule" | "none";
|
|
280
|
+
/** Time of the last operator message accepted for this lane. Drives compaction. */
|
|
281
|
+
last_message_at?: string;
|
|
282
|
+
/** Last automatic compaction of a resumed session (compaction.ts). */
|
|
283
|
+
last_compaction?: LaneCompactionRecord;
|
|
284
|
+
/**
|
|
285
|
+
* Copied from the last ingested envelope. Omitted when no envelope has
|
|
286
|
+
* been ingested — absence is not false.
|
|
287
|
+
*/
|
|
288
|
+
story_complete?: boolean;
|
|
289
|
+
story_complete_reason?: string;
|
|
290
|
+
/** Copied from the last ingested envelope when present. */
|
|
291
|
+
story_status?: StoryStatus;
|
|
292
|
+
/** Set by the reconcile pass when `--timeout` has passed (spec §5.7). */
|
|
293
|
+
overdue?: boolean;
|
|
294
|
+
overdue_at?: string;
|
|
295
|
+
/** Executing-host heartbeat; stale values mean the host stopped reporting. */
|
|
296
|
+
last_host_report_at?: string;
|
|
297
|
+
/**
|
|
298
|
+
* Set by the watcher when a running worker has made no tool call and
|
|
299
|
+
* produced no output for the silence window; cleared on the next progress
|
|
300
|
+
* signal or terminal write. Not a verdict: a long single tool call looks
|
|
301
|
+
* the same as a hang. See watcher.ts.
|
|
302
|
+
*/
|
|
303
|
+
stalled?: boolean;
|
|
304
|
+
stalled_at?: string;
|
|
305
|
+
stalled_reason?: string;
|
|
306
|
+
/** Set when the reconcile pass force-admits under the starvation bound. */
|
|
307
|
+
admitted_reason?: string;
|
|
308
|
+
terminal_reason?: string;
|
|
309
|
+
/**
|
|
310
|
+
* Structured detail behind a named `terminal_reason` (dispatch, pickup and
|
|
311
|
+
* spawn failures), or the missing-binary search path behind
|
|
312
|
+
* `provider_binary_missing`. Written by the command and spawn writers.
|
|
313
|
+
*/
|
|
314
|
+
terminal_details?: Record<string, unknown>;
|
|
315
|
+
/** SSM command id of the `hq outposts exec` pickup that started this lane. */
|
|
316
|
+
outpost_command_id?: string;
|
|
317
|
+
/**
|
|
318
|
+
* Provider reset time copied from a 429 result. Absent when the text
|
|
319
|
+
* named none. Distinct from the operator's account enabled switch.
|
|
320
|
+
*/
|
|
321
|
+
quota_reset_at?: string;
|
|
322
|
+
/** Set when a 429 refused one model for credits, e.g. "needs credits for claude-fable-5-1". */
|
|
323
|
+
model_quota_marker?: string;
|
|
324
|
+
/**
|
|
325
|
+
* Provider session id parsed from the worker's own stdout when a resumable
|
|
326
|
+
* provider exits: Claude's JSON `result.session_id`, Codex's
|
|
327
|
+
* `thread.started.thread_id`. Absent when the provider has none or the
|
|
328
|
+
* output carried none; `provider_session_id_absent` names why. Never a
|
|
329
|
+
* guessed or path-derived value.
|
|
330
|
+
*/
|
|
331
|
+
provider_session_id?: string;
|
|
332
|
+
provider_session_id_absent?: string;
|
|
333
|
+
/** Set at spawn when this run continued a recorded session instead of starting fresh. */
|
|
334
|
+
last_resume?: LaneResumeRecord;
|
|
335
|
+
/** Cache counters from the last exited run's result. Nothing else from the payload. */
|
|
336
|
+
last_run_usage?: LaneRunUsage;
|
|
337
|
+
/**
|
|
338
|
+
* Spec §4: one repair turn when the final output fails parseLaneEnvelope.
|
|
339
|
+
* Written by the watcher before the repair spawn, updated on the repair
|
|
340
|
+
* run's exit. Absent when every envelope this lane wrote parsed first time.
|
|
341
|
+
*/
|
|
342
|
+
envelope_repair?: LaneEnvelopeRepair;
|
|
343
|
+
/** Last successful lane_progress mesh delivery, preserved across watcher restarts. */
|
|
344
|
+
last_progress_event_at?: string;
|
|
345
|
+
}
|
|
346
|
+
/** The v1 fields that must exist on every durable lane record (spec §3). */
|
|
347
|
+
export declare const REQUIRED_LANE_RECORD_FIELDS: readonly ["schema_version", "lane_id", "company", "project_id", "story_id", "depth", "senior", "senior_history", "state", "priority", "provider", "host", "version", "created_at", "updated_at", "attribution_history"];
|
|
348
|
+
export declare const LANE_WRITERS: readonly ["creator", "command", "watcher", "reconcile", "spawn"];
|
|
349
|
+
export type LaneWriter = (typeof LANE_WRITERS)[number];
|
|
350
|
+
/** Field ownership at the durable-record mutation boundary (spec §10.1). */
|
|
351
|
+
export declare const LANE_FIELD_WRITERS: Readonly<Record<keyof LaneRecord, readonly LaneWriter[]>>;
|
|
352
|
+
export interface LaneEnvelopeRepair {
|
|
353
|
+
/** False when the parse failed but no repair turn could run (see `skipped_reason`). */
|
|
354
|
+
attempted: boolean;
|
|
355
|
+
/**
|
|
356
|
+
* How the repair turn ran: `resume` continues the recorded session (claude,
|
|
357
|
+
* codex); `fresh_turn` is a new single-turn invocation carrying the raw
|
|
358
|
+
* output (grok). Absent when no repair ran.
|
|
359
|
+
*/
|
|
360
|
+
mode?: "resume" | "fresh_turn";
|
|
361
|
+
succeeded: boolean;
|
|
362
|
+
first_error: string;
|
|
363
|
+
/** Parser error from the repair run's output, when that also failed. */
|
|
364
|
+
second_error?: string;
|
|
365
|
+
/** Why no repair turn ran: the provider cannot resume, or no session id was recorded. */
|
|
366
|
+
skipped_reason?: string;
|
|
367
|
+
/** Envelope slot the repair was asked to rewrite. */
|
|
368
|
+
envelope_ref?: string;
|
|
369
|
+
at: string;
|
|
370
|
+
}
|
|
371
|
+
export interface LaneResumeRecord {
|
|
372
|
+
session_id: string;
|
|
373
|
+
at: string;
|
|
374
|
+
trigger: "answered_questions" | "message" | "resume";
|
|
375
|
+
message_id?: string;
|
|
376
|
+
/**
|
|
377
|
+
* `hq lanes resume` only: the state the lane was in when the operator
|
|
378
|
+
* resumed it (`interrupted`, `done` or `blocked`) and the text they sent.
|
|
379
|
+
*/
|
|
380
|
+
resumed_from?: "interrupted" | "done" | "blocked";
|
|
381
|
+
text?: string;
|
|
382
|
+
}
|
|
383
|
+
export interface LaneRunUsage {
|
|
384
|
+
cache_read_input_tokens: number | null;
|
|
385
|
+
cache_creation_input_tokens: number | null;
|
|
386
|
+
}
|
|
387
|
+
export interface LaneCompactionRecord {
|
|
388
|
+
at: string;
|
|
389
|
+
/** `claude_print_compact`: `claude -p --resume <sid> "/compact"` before the resume. */
|
|
390
|
+
mechanism: "claude_print_compact";
|
|
391
|
+
session_id: string;
|
|
392
|
+
/** `compacted` only when the session transcript gained a compact boundary. */
|
|
393
|
+
outcome: "compacted" | "no_effect" | "failed";
|
|
394
|
+
reason?: string;
|
|
395
|
+
duration_ms: number;
|
|
396
|
+
/** Milliseconds between the reference time and the resume that compacted. */
|
|
397
|
+
elapsed_ms: number;
|
|
398
|
+
/** `last_message_at`, or `created_at` when no message was ever sent. */
|
|
399
|
+
reference_at: string;
|
|
400
|
+
/** Context size the provider recorded at the compact boundary. */
|
|
401
|
+
pre_tokens: number | null;
|
|
402
|
+
/** Usage of the last run before compaction. */
|
|
403
|
+
before: LaneRunUsage | null;
|
|
404
|
+
/** Usage of the first run after compaction, once that run has exited. */
|
|
405
|
+
after: LaneRunUsage | null;
|
|
406
|
+
/** `last_resume.at` of the run that followed this compaction. */
|
|
407
|
+
resume_at?: string;
|
|
104
408
|
}
|
|
105
409
|
export interface QuestionBodyRef {
|
|
106
410
|
store: QuestionBodyStore;
|
|
@@ -115,16 +419,27 @@ export interface RoutingHistoryEntry {
|
|
|
115
419
|
}
|
|
116
420
|
export interface QuestionAnswer {
|
|
117
421
|
choice: string;
|
|
422
|
+
/** Whether the answer selected an offered option or supplied new text. */
|
|
423
|
+
answer_kind?: "option" | "freeform";
|
|
118
424
|
notes_ref?: string;
|
|
119
425
|
answered_by: string;
|
|
120
426
|
answered_at: string;
|
|
121
427
|
answered_level: number;
|
|
122
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* A question whose options amount to "continue" or "stop" the brief's own
|
|
431
|
+
* work. The brief already authorises finishing everything it lists, so such a
|
|
432
|
+
* question is flagged for the operator; nothing answers it automatically.
|
|
433
|
+
*/
|
|
434
|
+
export type QuestionKind = "continue";
|
|
123
435
|
export interface QuestionRecord {
|
|
124
436
|
schema_version: typeof QUESTION_SCHEMA_VERSION | string;
|
|
125
437
|
question_id: string;
|
|
126
438
|
lane_id: string;
|
|
127
|
-
|
|
439
|
+
/** Set by the raiser when the options are only continue/stop. */
|
|
440
|
+
question_kind?: QuestionKind;
|
|
441
|
+
/** `null` when the originating lane has no company. */
|
|
442
|
+
company: LaneCompanyRef | null;
|
|
128
443
|
project_id: string;
|
|
129
444
|
story_id: string;
|
|
130
445
|
thread_id?: string;
|
|
@@ -140,6 +455,8 @@ export interface QuestionRecord {
|
|
|
140
455
|
body: QuestionBodyRef;
|
|
141
456
|
routing_history: RoutingHistoryEntry[];
|
|
142
457
|
answer?: QuestionAnswer;
|
|
458
|
+
/** Merge-on-read preservation of answers retained by conflict sidecars. */
|
|
459
|
+
answer_history?: QuestionAnswer[];
|
|
143
460
|
}
|
|
144
461
|
export interface EnvelopeQuestion {
|
|
145
462
|
question: string;
|
|
@@ -155,17 +472,42 @@ export interface LaneEnvelope {
|
|
|
155
472
|
story_status: StoryStatus;
|
|
156
473
|
artifacts: unknown[];
|
|
157
474
|
blocked_reason?: string;
|
|
475
|
+
story_complete?: boolean;
|
|
476
|
+
story_complete_reason?: string;
|
|
158
477
|
}
|
|
159
478
|
export interface AccountConfig {
|
|
160
479
|
home: string;
|
|
161
480
|
enabled: boolean;
|
|
162
481
|
disabled_reason?: string;
|
|
163
482
|
max_running?: number;
|
|
483
|
+
/** Default model for this account. Beats the provider default; beaten by --model. */
|
|
484
|
+
model?: string;
|
|
485
|
+
/** Default effort for this account. Beats the provider default; beaten by --effort. */
|
|
486
|
+
effort?: string;
|
|
164
487
|
}
|
|
165
488
|
export interface ProviderConfig {
|
|
166
489
|
max_running: number;
|
|
167
490
|
max_queued: number;
|
|
168
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Accepted and ignored for selection. Kept so existing config files parse.
|
|
493
|
+
* Account pick is least-loaded among enabled, logged-in accounts.
|
|
494
|
+
*/
|
|
495
|
+
default_account?: string;
|
|
496
|
+
/** Default model for this provider when the account does not set one. */
|
|
497
|
+
model?: string;
|
|
498
|
+
/** Default effort for this provider when the account does not set one. */
|
|
499
|
+
effort?: string;
|
|
500
|
+
/**
|
|
501
|
+
* Resume-time compaction threshold: a resumed session whose last message is
|
|
502
|
+
* at least this old is compacted first. Absent means the one-hour default.
|
|
503
|
+
*/
|
|
504
|
+
compact_after_ms?: number;
|
|
505
|
+
/**
|
|
506
|
+
* Watcher throttle for `lane_progress` thread events: at most one per this
|
|
507
|
+
* interval, and only on a tick that saw new tool activity or output. Absent
|
|
508
|
+
* means the five-minute default.
|
|
509
|
+
*/
|
|
510
|
+
progress_event_min_interval_ms?: number;
|
|
169
511
|
accounts: Record<string, AccountConfig>;
|
|
170
512
|
}
|
|
171
513
|
export interface ThrottleConfig {
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -8,6 +8,73 @@ export const LANE_SCHEMA_VERSION = "hq-lane.v1";
|
|
|
8
8
|
export const QUESTION_SCHEMA_VERSION = "hq-lane-question.v1";
|
|
9
9
|
export const ENVELOPE_SCHEMA_VERSION = "hq-lane-envelope.v1";
|
|
10
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
|
+
};
|
|
11
78
|
export const LANE_STATES = [
|
|
12
79
|
"queued",
|
|
13
80
|
"running",
|
|
@@ -28,7 +95,7 @@ export const TERMINAL_LANE_STATES = [
|
|
|
28
95
|
"killed",
|
|
29
96
|
];
|
|
30
97
|
export const LANE_PROVIDERS = ["claude", "codex", "grok"];
|
|
31
|
-
export const QUEUED_REASONS = ["cap", "queue_position", "throttle"];
|
|
98
|
+
export const QUEUED_REASONS = ["cap", "queue_position", "throttle", "provider_binary_missing"];
|
|
32
99
|
export const HOST_KINDS = ["local", "outpost"];
|
|
33
100
|
export const SENIOR_KINDS = ["session", "lane", "user"];
|
|
34
101
|
export const ENVELOPE_DECISIONS = ["done", "ask", "blocked"];
|
|
@@ -38,6 +105,12 @@ export const STORY_STATUSES = [
|
|
|
38
105
|
"review",
|
|
39
106
|
"done",
|
|
40
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
|
+
*/
|
|
41
114
|
export const QUESTION_STATUSES = [
|
|
42
115
|
"pending",
|
|
43
116
|
"answered",
|
|
@@ -112,6 +185,29 @@ export function parseSeniorKind(value) {
|
|
|
112
185
|
return value;
|
|
113
186
|
throw new LanesError("unknown_senior_kind", `Unknown senior kind ${JSON.stringify(value)}. Accepted: ${SENIOR_KINDS.join(", ")}`, { accepted: [...SENIOR_KINDS], value });
|
|
114
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
|
+
};
|
|
115
211
|
function isPlainObject(value) {
|
|
116
212
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
117
213
|
}
|
|
@@ -145,6 +241,8 @@ export function parseLaneEnvelope(raw) {
|
|
|
145
241
|
"story_status",
|
|
146
242
|
"artifacts",
|
|
147
243
|
"blocked_reason",
|
|
244
|
+
"story_complete",
|
|
245
|
+
"story_complete_reason",
|
|
148
246
|
].includes(key));
|
|
149
247
|
if (extra.length > 0) {
|
|
150
248
|
throw new LaneSchemaError(`Lane envelope rejects unknown properties: ${extra.join(", ")}`, { extra });
|
|
@@ -169,9 +267,7 @@ export function parseLaneEnvelope(raw) {
|
|
|
169
267
|
}
|
|
170
268
|
}
|
|
171
269
|
else if (questions !== undefined) {
|
|
172
|
-
|
|
173
|
-
throw new LaneSchemaError('questions must be omitted or empty unless decision is "ask"');
|
|
174
|
-
}
|
|
270
|
+
throw new LaneSchemaError('questions is present exactly when decision is "ask"');
|
|
175
271
|
}
|
|
176
272
|
if (raw.decision === "blocked") {
|
|
177
273
|
if (typeof raw.blocked_reason !== "string" || raw.blocked_reason.length === 0) {
|
|
@@ -181,13 +277,50 @@ export function parseLaneEnvelope(raw) {
|
|
|
181
277
|
else if (raw.blocked_reason !== undefined) {
|
|
182
278
|
throw new LaneSchemaError('blocked_reason is present exactly when decision is "blocked"');
|
|
183
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
|
+
}
|
|
184
313
|
const envelope = {
|
|
185
314
|
schema_version: ENVELOPE_SCHEMA_VERSION,
|
|
186
315
|
decision: raw.decision,
|
|
187
316
|
summary,
|
|
188
|
-
story_status:
|
|
317
|
+
story_status: storyStatus,
|
|
189
318
|
artifacts: raw.artifacts,
|
|
190
319
|
};
|
|
320
|
+
if (storyComplete !== undefined) {
|
|
321
|
+
envelope.story_complete = storyComplete;
|
|
322
|
+
envelope.story_complete_reason = storyCompleteReason;
|
|
323
|
+
}
|
|
191
324
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
192
325
|
envelope.questions = questions.map((item, index) => {
|
|
193
326
|
if (!isPlainObject(item)) {
|
|
@@ -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
|