@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,275 @@
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 { appendThreadEvent, clamp, ensureProjectThread, patchStoryStatus, requireToken, resolveMeshCompany, } from "../mesh/api.js";
46
+ import { LanesError } from "./errors.js";
47
+ export const LANE_MESH_EVENTS = [
48
+ "lane_started",
49
+ "lane_progress",
50
+ "lane_question",
51
+ "lane_question_at_user",
52
+ "lane_ask",
53
+ "lane_blocked",
54
+ "lane_done",
55
+ ];
56
+ /** Roster kinds this module emits. Subset of the server's `validKinds`. */
57
+ export const WIRE_EVENT_KINDS = [
58
+ "run_started",
59
+ "run_progress",
60
+ "run_complete",
61
+ "question",
62
+ "note",
63
+ ];
64
+ /** Server caps: activity title/summary 2000, question text 500, note text 500. */
65
+ export const WIRE_ACTIVITY_TEXT_MAX = 2000;
66
+ export const WIRE_QUESTION_TEXT_MAX = 500;
67
+ export const WIRE_NOTE_TEXT_MAX = 500;
68
+ export function parseLaneMeshEvent(value) {
69
+ if (typeof value === "string" &&
70
+ LANE_MESH_EVENTS.includes(value)) {
71
+ return value;
72
+ }
73
+ throw new LanesError("unknown_lane_mesh_event", `Unknown lane mesh event ${JSON.stringify(value)}. Accepted: ${LANE_MESH_EVENTS.join(", ")}`, { accepted: [...LANE_MESH_EVENTS], value });
74
+ }
75
+ function str(value) {
76
+ if (value === undefined || value === null)
77
+ return "";
78
+ return typeof value === "string" ? value : JSON.stringify(value);
79
+ }
80
+ function identity(payload) {
81
+ const lane = str(payload.lane_id) || "unknown-lane";
82
+ const project = str(payload.project_id);
83
+ const story = str(payload.story_id);
84
+ return project || story ? `${lane} ${project}/${story}` : lane;
85
+ }
86
+ function nonEmpty(text, fallback) {
87
+ return text.trim().length > 0 ? text : fallback;
88
+ }
89
+ /**
90
+ * Lane event → roster kind + validator-shaped payload. Pure. Every text
91
+ * field is pre-clamped to the server cap so a long summary is truncated
92
+ * here rather than rejected there.
93
+ */
94
+ export function toWireThreadEvent(event, payload) {
95
+ const kind = parseLaneMeshEvent(event);
96
+ const id = identity(payload);
97
+ switch (kind) {
98
+ case "lane_started":
99
+ return {
100
+ eventKind: "run_started",
101
+ payload: {
102
+ title: clamp(`lane_started ${str(payload.lane_id) || "unknown-lane"}`, WIRE_ACTIVITY_TEXT_MAX),
103
+ summary: clamp(`Lane ${id} started`, WIRE_ACTIVITY_TEXT_MAX),
104
+ },
105
+ };
106
+ case "lane_progress": {
107
+ const detail = str(payload.detail);
108
+ return {
109
+ eventKind: "run_progress",
110
+ payload: {
111
+ summary: clamp(nonEmpty(`lane_progress ${id}${detail ? `: ${detail}` : ""}`, "lane_progress"), WIRE_ACTIVITY_TEXT_MAX),
112
+ },
113
+ };
114
+ }
115
+ case "lane_done": {
116
+ const summary = str(payload.summary);
117
+ return {
118
+ eventKind: "run_complete",
119
+ payload: {
120
+ title: clamp(`lane_done ${str(payload.lane_id) || "unknown-lane"}`, WIRE_ACTIVITY_TEXT_MAX),
121
+ summary: clamp(nonEmpty(`${id}: ${summary}`.replace(/:\s*$/, ""), `lane_done ${id}`), WIRE_ACTIVITY_TEXT_MAX),
122
+ },
123
+ };
124
+ }
125
+ case "lane_blocked": {
126
+ const reason = str(payload.blocked_reason);
127
+ // The roster's `blocked` event kind would flip the shared project
128
+ // thread, which spec §8 keeps lane events away from. Use the accepted
129
+ // audit note kind to preserve a filterable blocked marker.
130
+ return {
131
+ eventKind: "note",
132
+ payload: {
133
+ text: clamp(`lane_blocked ${id}${reason ? `: ${reason}` : ""}`, WIRE_NOTE_TEXT_MAX),
134
+ noteKind: "audit",
135
+ },
136
+ };
137
+ }
138
+ case "lane_question":
139
+ case "lane_question_at_user": {
140
+ const questionId = str(payload.question_id);
141
+ const hop = payload.hop === undefined ? "" : ` hop ${str(payload.hop)}`;
142
+ const out = {
143
+ text: clamp(`${kind} ${id}${hop}`, WIRE_QUESTION_TEXT_MAX),
144
+ };
145
+ if (questionId)
146
+ out.questionId = questionId;
147
+ return { eventKind: "question", payload: out };
148
+ }
149
+ case "lane_ask": {
150
+ const ids = Array.isArray(payload.question_ids)
151
+ ? payload.question_ids.map((item) => str(item)).filter(Boolean)
152
+ : [];
153
+ const out = {
154
+ text: clamp(`lane_ask ${id}: ${ids.length} question${ids.length === 1 ? "" : "s"}${ids.length ? ` ${ids.join(" ")}` : ""}`, WIRE_QUESTION_TEXT_MAX),
155
+ };
156
+ if (ids[0])
157
+ out.questionId = ids[0];
158
+ return { eventKind: "question", payload: out };
159
+ }
160
+ default: {
161
+ const _exhaustive = kind;
162
+ return _exhaustive;
163
+ }
164
+ }
165
+ }
166
+ export function createLaneMesh(opts = {}) {
167
+ const interactive = opts.interactive ?? true;
168
+ const token = opts.token ?? (() => requireToken({ interactive }));
169
+ return {
170
+ async ensureProjectThread(company, projectId) {
171
+ const tok = await token();
172
+ const resolved = await resolveMeshCompany(tok, company.slug);
173
+ const ensured = await ensureProjectThread(tok, resolved, projectId, {
174
+ summary: `HQ lane ${projectId}`,
175
+ });
176
+ return ensured.threadId;
177
+ },
178
+ async appendThreadEvent(company, threadId, event, payload, signal) {
179
+ const wire = toWireThreadEvent(event, payload);
180
+ const tok = await token();
181
+ await appendThreadEvent(tok, company.uid, threadId, wire.eventKind, wire.payload, signal);
182
+ },
183
+ async patchStoryStatus(company, projectId, storyId, status) {
184
+ const tok = await token();
185
+ await patchStoryStatus(tok, company.uid, projectId, storyId, status);
186
+ },
187
+ };
188
+ }
189
+ /** Company + thread present. A core/personal lane (company null) emits nothing. */
190
+ export function laneCanEmit(lane) {
191
+ return lane.company !== null && lane.company !== undefined && !!lane.thread_id;
192
+ }
193
+ function baseIdentity(lane) {
194
+ return {
195
+ lane_id: lane.lane_id,
196
+ project_id: lane.project_id,
197
+ story_id: lane.story_id,
198
+ };
199
+ }
200
+ /**
201
+ * Spec §8 terminal-envelope events. `done` → lane_done and story → review;
202
+ * `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
203
+ * never set to `done` here: promotion belongs to the senior.
204
+ */
205
+ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
206
+ if (!laneCanEmit(lane))
207
+ return;
208
+ const company = lane.company;
209
+ const threadId = lane.thread_id;
210
+ switch (envelope.decision) {
211
+ case "done": {
212
+ const outcomes = await Promise.allSettled([
213
+ mesh.appendThreadEvent(company, threadId, "lane_done", {
214
+ ...baseIdentity(lane),
215
+ summary: envelope.summary,
216
+ }, opts.signal),
217
+ mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "review"),
218
+ ]);
219
+ const failure = outcomes.find((outcome) => outcome.status === "rejected");
220
+ if (failure?.status === "rejected")
221
+ throw failure.reason;
222
+ return;
223
+ }
224
+ case "blocked":
225
+ await mesh.appendThreadEvent(company, threadId, "lane_blocked", {
226
+ ...baseIdentity(lane),
227
+ blocked_reason: envelope.blocked_reason ?? "",
228
+ });
229
+ return;
230
+ case "ask":
231
+ // A mesh question event has one `questionId`; sending one event per id
232
+ // avoids silently losing ids when the display text reaches its wire cap.
233
+ {
234
+ const failures = [];
235
+ for (const questionId of opts.questionIds ?? []) {
236
+ try {
237
+ await mesh.appendThreadEvent(company, threadId, "lane_ask", {
238
+ ...baseIdentity(lane),
239
+ question_ids: [questionId],
240
+ }, opts.signal);
241
+ }
242
+ catch (err) {
243
+ failures.push(err);
244
+ }
245
+ }
246
+ if (failures.length > 0)
247
+ throw failures[0];
248
+ }
249
+ return;
250
+ default: {
251
+ const _exhaustive = envelope.decision;
252
+ return _exhaustive;
253
+ }
254
+ }
255
+ }
256
+ export async function emitLaneProgressEvent(mesh, lane, detail, signal) {
257
+ if (!laneCanEmit(lane))
258
+ return;
259
+ await mesh.appendThreadEvent(lane.company, lane.thread_id, "lane_progress", {
260
+ ...baseIdentity(lane),
261
+ detail,
262
+ }, signal);
263
+ }
264
+ /**
265
+ * Throttle for lane_progress (spec §8 progress row). True when no progress
266
+ * event has been sent yet, or the last one is at least `minIntervalMs` old.
267
+ * The caller only asks on a tick that saw new activity; a silent tick never
268
+ * reaches this function.
269
+ */
270
+ export function progressEventDue(input) {
271
+ if (input.lastEmitMs === null)
272
+ return true;
273
+ return input.nowMs - input.lastEmitMs >= input.minIntervalMs;
274
+ }
275
+ //# sourceMappingURL=mesh-events.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Push an Outpost-targeted lane through `hq outposts exec` (spec §5.2).
3
+ *
4
+ * The transport is the existing OutpostsClient submit/wait pair — the same
5
+ * path as `hq outposts exec --async`. Create stages a payload, then runs
6
+ * `hq lanes pickup --payload-url` on the box. A submit or wait failure is
7
+ * `outpost_dispatch_failed`, never a queued lane nobody will pick up.
8
+ */
9
+ import { OutpostsClient } from "@indigoai-us/hq-cloud/outposts";
10
+ import type { LaneRecord } from "./types.js";
11
+ export declare const PICKUP_EXEC_TIMEOUT_SECONDS = 120;
12
+ export declare const PICKUP_WAIT_MS: number;
13
+ export interface OutpostPickupPayload {
14
+ lane: LaneRecord;
15
+ brief: string;
16
+ timeout?: {
17
+ timeout_secs: number;
18
+ deadline_at: string;
19
+ };
20
+ }
21
+ export interface OutpostDispatchResult {
22
+ commandId: string;
23
+ status: string;
24
+ done?: boolean;
25
+ exitCode: number | null;
26
+ stdout: string;
27
+ stderr: string;
28
+ }
29
+ export interface OutpostExecPort {
30
+ stageExecInput: (outpostId?: string) => Promise<{
31
+ putUrl: string;
32
+ getUrl: string;
33
+ }>;
34
+ submitExec: (command: string, outpostId?: string, timeoutSeconds?: number) => Promise<{
35
+ commandId: string;
36
+ }>;
37
+ waitForExecResult: (commandId: string, outpostId?: string, options?: {
38
+ signal?: AbortSignal;
39
+ }) => Promise<{
40
+ status: string;
41
+ done: boolean;
42
+ exitCode?: number | null;
43
+ stdout?: string;
44
+ stderr?: string;
45
+ }>;
46
+ }
47
+ export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPayload;
48
+ /** Same wrapping `hq outposts exec` applies: run from the box's HQ folder. */
49
+ export declare function buildOutpostPickupCommand(payloadUrl: string): string;
50
+ export declare function stageOutpostPayload(port: Pick<OutpostExecPort, "stageExecInput">, outpostId: string, bytes: Uint8Array, put?: typeof fetch): Promise<{
51
+ getUrl: string;
52
+ }>;
53
+ export declare function dispatchOutpostPickup(port: OutpostExecPort, input: {
54
+ outpostId: string;
55
+ command: string;
56
+ timeoutSeconds?: number;
57
+ waitMs?: number;
58
+ }): Promise<OutpostDispatchResult>;
59
+ export declare function dispatchErrorMessage(err: unknown): string;
60
+ /** Factory matching `hq outposts exec`'s client wiring. */
61
+ export declare function createOutpostsClient(opts: {
62
+ transport: ConstructorParameters<typeof OutpostsClient>[0]["transport"];
63
+ token: ConstructorParameters<typeof OutpostsClient>[0]["token"];
64
+ }): OutpostsClient;
65
+ //# sourceMappingURL=outpost-dispatch.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Push an Outpost-targeted lane through `hq outposts exec` (spec §5.2).
3
+ *
4
+ * The transport is the existing OutpostsClient submit/wait pair — the same
5
+ * path as `hq outposts exec --async`. Create stages a payload, then runs
6
+ * `hq lanes pickup --payload-url` on the box. A submit or wait failure is
7
+ * `outpost_dispatch_failed`, never a queued lane nobody will pick up.
8
+ */
9
+ import { OutpostsClient, joinCommandParts, withRemoteHqDir } from "@indigoai-us/hq-cloud/outposts";
10
+ import { LanesError } from "./errors.js";
11
+ export const PICKUP_EXEC_TIMEOUT_SECONDS = 120;
12
+ export const PICKUP_WAIT_MS = PICKUP_EXEC_TIMEOUT_SECONDS * 1000;
13
+ export function parseOutpostPickupPayload(raw) {
14
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
15
+ throw new LanesError("outpost_payload_invalid", "Outpost pickup payload must be a JSON object", {});
16
+ }
17
+ const body = raw;
18
+ if (body.lane === null || typeof body.lane !== "object" || Array.isArray(body.lane)) {
19
+ throw new LanesError("outpost_payload_invalid", "Outpost pickup payload.lane must be an object", {});
20
+ }
21
+ if (typeof body.brief !== "string") {
22
+ throw new LanesError("outpost_payload_invalid", "Outpost pickup payload.brief must be a string", {});
23
+ }
24
+ let timeout;
25
+ if (body.timeout !== undefined && body.timeout !== null) {
26
+ if (typeof body.timeout !== "object" || Array.isArray(body.timeout)) {
27
+ throw new LanesError("outpost_payload_invalid", "Outpost pickup payload.timeout must be an object", {});
28
+ }
29
+ const t = body.timeout;
30
+ if (typeof t.timeout_secs !== "number" || typeof t.deadline_at !== "string") {
31
+ throw new LanesError("outpost_payload_invalid", "Outpost pickup payload.timeout needs timeout_secs and deadline_at", {});
32
+ }
33
+ timeout = { timeout_secs: t.timeout_secs, deadline_at: t.deadline_at };
34
+ }
35
+ return {
36
+ lane: body.lane,
37
+ brief: body.brief,
38
+ timeout,
39
+ };
40
+ }
41
+ /** Same wrapping `hq outposts exec` applies: run from the box's HQ folder. */
42
+ export function buildOutpostPickupCommand(payloadUrl) {
43
+ return withRemoteHqDir(joinCommandParts(["hq", "lanes", "pickup", "--payload-url", payloadUrl]));
44
+ }
45
+ export async function stageOutpostPayload(port, outpostId, bytes, put = fetch) {
46
+ const staged = await port.stageExecInput(outpostId);
47
+ const upload = await put(staged.putUrl, {
48
+ method: "PUT",
49
+ headers: { "Content-Length": String(bytes.byteLength) },
50
+ body: bytes,
51
+ });
52
+ if (!upload.ok) {
53
+ throw new LanesError("outpost_dispatch_failed", `Could not upload lane payload: HTTP ${upload.status}`, { status: upload.status, outpost_id: outpostId });
54
+ }
55
+ return { getUrl: staged.getUrl };
56
+ }
57
+ export async function dispatchOutpostPickup(port, input) {
58
+ const submitted = await port.submitExec(input.command, input.outpostId, input.timeoutSeconds ?? PICKUP_EXEC_TIMEOUT_SECONDS);
59
+ const ac = new AbortController();
60
+ const waitMs = input.waitMs ?? PICKUP_WAIT_MS;
61
+ const timer = setTimeout(() => ac.abort(), waitMs);
62
+ try {
63
+ const result = await port.waitForExecResult(submitted.commandId, input.outpostId, { signal: ac.signal });
64
+ return {
65
+ commandId: submitted.commandId,
66
+ status: result.status,
67
+ done: result.done,
68
+ exitCode: result.exitCode ?? null,
69
+ stdout: result.stdout ?? "",
70
+ stderr: result.stderr ?? "",
71
+ };
72
+ }
73
+ catch (err) {
74
+ if (ac.signal.aborted) {
75
+ throw new LanesError("outpost_dispatch_failed", `Timed out waiting for Outpost pickup (${waitMs}ms)`, { outpost_id: input.outpostId, command_id: submitted.commandId });
76
+ }
77
+ throw err;
78
+ }
79
+ finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+ export function dispatchErrorMessage(err) {
84
+ return err instanceof Error ? err.message : String(err);
85
+ }
86
+ /** Factory matching `hq outposts exec`'s client wiring. */
87
+ export function createOutpostsClient(opts) {
88
+ return new OutpostsClient(opts);
89
+ }
90
+ //# sourceMappingURL=outpost-dispatch.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Raise a lane question (spec §6.1). One function for the CLI `hq lanes ask`
3
+ * path and for envelope `decision: "ask"` ingestion. Two writers here would
4
+ * drift on hop, origin, raised_via, body store, and routing_history.
5
+ */
6
+ import { type LaneEnvelope, type LaneRecord, type QuestionBodyStore, type QuestionKind, type QuestionRecord, type RaisedVia } from "./types.js";
7
+ export interface QuestionBody {
8
+ question: string;
9
+ context: string;
10
+ options: string[];
11
+ recommended?: string;
12
+ notes?: string;
13
+ }
14
+ export declare function parseQuestionBodyStore(value: unknown): QuestionBodyStore;
15
+ export declare function bodyStoreForLane(lane: Pick<LaneRecord, "company">): QuestionBodyStore;
16
+ export declare function questionBodyRef(questionId: string): string;
17
+ export declare function sha256Text(text: string): string;
18
+ /** Tenant-store path for a question body (spec §6.2 / §10). Never the personal vault for company prose. */
19
+ export declare function questionBodyAbsPath(hqRoot: string, question: Pick<QuestionRecord, "company" | "body" | "question_id">): string;
20
+ export declare function writeQuestionBody(hqRoot: string, question: Pick<QuestionRecord, "company" | "body" | "question_id">, body: QuestionBody): {
21
+ store: QuestionBodyStore;
22
+ ref: string;
23
+ digest: string;
24
+ };
25
+ export interface RaiseLaneQuestionInput {
26
+ hqRoot: string;
27
+ lane: LaneRecord;
28
+ body: QuestionBody;
29
+ raisedVia: RaisedVia;
30
+ at: string;
31
+ questionId?: string;
32
+ }
33
+ /**
34
+ * Persist one question the way `hq lanes ask` does: createQuestion, write the
35
+ * tenant body, then CAS routing_history with reason `asked`.
36
+ */
37
+ export declare function raiseLaneQuestion(input: RaiseLaneQuestionInput): QuestionRecord;
38
+ /**
39
+ * Options that, trimmed, case-folded and stripped of trailing punctuation,
40
+ * are exactly {continue, stop} in either order. The brief authorises the work
41
+ * it lists, so this question asks the operator nothing the brief did not
42
+ * already settle; it is recorded as `question_kind: "continue"`.
43
+ */
44
+ export declare function classifyQuestionKind(options: readonly string[]): QuestionKind | undefined;
45
+ /** The store a lane's question bodies live in, derived from the lane and never the reverse. */
46
+ export declare function questionBodyPathForLane(hqRoot: string, lane: Pick<LaneRecord, "company">, questionId: string): string;
47
+ /**
48
+ * Free-text notes the operator attached with `questions answer --notes`.
49
+ * Read from the lane's own store (spec §6.5): the company store for a company
50
+ * lane, the personal store otherwise. Returns undefined when the answer has
51
+ * no notes_ref or the body carries no notes; a body the host cannot read is
52
+ * reported and skipped rather than failing the resume.
53
+ */
54
+ export declare function readAnswerNotes(hqRoot: string, lane: Pick<LaneRecord, "company">, question: Pick<QuestionRecord, "question_id" | "answer">): string | undefined;
55
+ export interface AnsweredQuestionEntry {
56
+ question_id: string;
57
+ choice: string | undefined;
58
+ answered_at: string | undefined;
59
+ answered_by: string | undefined;
60
+ notes?: string;
61
+ }
62
+ /**
63
+ * The `answered-questions.json` a resumed worker is handed (spec §6.5). Ids,
64
+ * choices and timestamps come from the routing record; the operator's notes
65
+ * come from the lane's own body store and are written only into the run
66
+ * directory on the executing host.
67
+ */
68
+ export declare function answeredQuestionEntries(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company">, questions: QuestionRecord[]): AnsweredQuestionEntry[];
69
+ export declare function envelopeQuestionId(laneId: string, envelopeRef: string, index: number): string;
70
+ /**
71
+ * Spec §6.1: each envelope questions[] entry becomes one queue record, via
72
+ * raiseLaneQuestion (the same function the CLI ask path uses).
73
+ */
74
+ export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string): QuestionRecord[];
75
+ //# sourceMappingURL=questions.d.ts.map