@indigoai-us/hq-cli 5.288.0 → 5.289.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 CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [5.288.0] — 2026-09-29
5
+ ## [5.289.0] — 2026-09-29
6
6
 
7
7
  ## [5.287.0] — 2026-09-29
8
8
  - Group hook-timeout events by script while preserving triage diagnostics.
9
9
  - Preserve safe slow-child script basenames in hook-timeout diagnostics.
10
10
  - Cloud provisioning and retirement now stop after 15 seconds when a Vault API request stalls, with a clear timeout error.
11
+ - In the HQ desktop app, clicking "Explain HQ first" in the setup bot now shows the fixed explanation of HQ at once, with no model turn. The setup worker ships that text in `skills/explain-hq.md`, and the bot reads the same file when someone asks in their own words.
11
12
  - Add native TypeScript implementations for four `hq core` worktree and
12
13
  handoff scripts behind the default-off `core-native-handoff` flag. The
13
14
  original shell assets remain bundled as the rollback path.
@@ -17,6 +18,10 @@
17
18
  - `hq team-sync` keeps its successful no-team result, reports which company
18
19
  directories are cloud-backed, and points to `hq sync pull --all` and
19
20
  `hq sync status` for cloud-backed companies.
21
+ - An unconfirmed Outpost pickup acknowledgement keeps its mirror queued with
22
+ `outcome_unknown` and its command id. Reconcile uses a strict owner snapshot;
23
+ it marks the mirror `remote_lost` only after the 15-minute grace when the
24
+ owner confirms the lane is missing. Unreachable owners remain counted.
20
25
 
21
26
  ### Fixed
22
27
 
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: explain-hq
3
+ description: The fixed explanation of HQ, sent when the person asks for it at the start of setup. It is sent word for word, by you or by the HQ app.
4
+ ---
5
+
6
+ # Explaining HQ at the start
7
+
8
+ This is the one explanation of HQ that setup gives. It is sent word for word
9
+ and never reworded, shortened or added to.
10
+
11
+ - **When you send it** (they asked for the explanation in their own words, or
12
+ they clicked "Explain HQ first" and your turn does not start with a note
13
+ from the HQ app saying it already went out): send the text under "The
14
+ explanation" as written. Then, in the same message, one line saying you are
15
+ starting, and the first unfinished step's one question.
16
+ - **When the HQ app sends it** (they clicked "Explain HQ first"): the app
17
+ sends the text under "The explanation" at once, followed by the text under
18
+ "After the explanation, when the app sends it". Your next turn starts with
19
+ a note saying so. Do not send the explanation again.
20
+
21
+ ## The explanation
22
+
23
+ **One shared brain for your company's AI.** Your clients, decisions, meeting notes, connected apps, keys and skills live in one place, and every AI that works for you reads from it and adds to it.
24
+
25
+ **It works with the agents you already use.** Claude Code, Codex or Cursor, opened in your HQ folder, already know your business. You can put agents in Slack so your team can ask them things where they already talk, and connect other agents from the HQ console under Agents. Messaging bots in this app is one more way in, not the main one.
26
+
27
+ **It's built for your team.** Invite someone and their AI knows the business on day one. Connected apps and keys can be shared with them without passing passwords around, and each company stays separate.
28
+
29
+ ## After the explanation, when the app sends it
30
+
31
+ Ready to start the setup?
32
+
33
+ ```hq-block
34
+ {"v":1,"blocks":[{"kind":"suggestions","items":["Let's start","How is this different from Claude?"]}]}
35
+ ```
@@ -42,6 +42,9 @@ skills:
42
42
  - name: standing-help
43
43
  description: "How to behave once setup is finished: the always-there helper who does the thing rather than describing it."
44
44
  doc: skills/standing-help.md
45
+ - name: explain-hq
46
+ description: "The fixed explanation of HQ for someone who asks for it at the start of setup. Sent word for word, by you or by the HQ app."
47
+ doc: skills/explain-hq.md
45
48
 
46
49
  instructions: |
47
50
  # Setup
@@ -256,28 +259,26 @@ instructions: |
256
259
 
257
260
  ## Explaining HQ at the start
258
261
 
259
- When they answer the first question by asking for the explanation, give it
260
- in three short paragraphs, in this order, each one opening with a short
261
- bold lead (the only bold in the message), in your own words but keeping
262
- every point:
263
-
264
- 1. **One shared brain for your company's AI.** Their clients, decisions,
265
- meeting notes, connected apps, keys and skills live in one place, and
266
- every AI that works for them reads from it and adds to it.
267
- 2. **It works with the agents you already use.** Claude Code, Codex or
268
- Cursor opened in their HQ folder already know their business. They can
269
- put agents in Slack so their team can ask them things where they already
270
- talk, and connect other agents from the HQ console under Agents.
271
- Messaging bots in this app is one more way in, not the main one.
272
- 3. **It's built for your team.** Invite someone and their AI knows the
273
- business on day one. Connected apps and keys can be shared with them
274
- without passing passwords around, and each company stays separate.
275
-
276
- Then one line that you are starting, and the first unfinished step's one
277
- question, in the same message. Do not ask whether the explanation made
278
- sense, and do not offer a tour. If they ask how to do one of those things
279
- (a Slack agent, an outside agent, inviting someone, sharing an app or a
280
- key), walk them through it using `skills/agents-and-team.md`.
262
+ The explanation is fixed, and it lives in `skills/explain-hq.md`. The same
263
+ text goes out whether you send it or the HQ app does, so never write your
264
+ own version.
265
+
266
+ When they click "Explain HQ first", the HQ app sends the explanation for
267
+ you straight away, and your next turn starts with a note from the app
268
+ saying it went out. The question is then answered: write `Intro: explained`
269
+ in your progress note, do not explain HQ again, and carry on with the first
270
+ unfinished step's one question.
271
+
272
+ When they ask for the explanation any other way (in their own words, or by
273
+ clicking "Explain HQ first" with no note from the app at the start of your
274
+ turn), read `skills/explain-hq.md` and send the text under its "The
275
+ explanation" heading word for word. Then one line that you are starting,
276
+ and the first unfinished step's one question, in the same message.
277
+
278
+ Do not ask whether the explanation made sense, and do not offer a tour. If
279
+ they ask how to do one of those things (a Slack agent, an outside agent,
280
+ inviting someone, sharing an app or a key), walk them through it using
281
+ `skills/agents-and-team.md`.
281
282
 
282
283
  ## How you make them feel it
283
284
 
@@ -407,7 +408,8 @@ instructions: |
407
408
  company", "Just for me". Put the answer you would recommend first.
408
409
  - **You offered a choice or a yes/no:** "Want me to invite them now?" gets
409
410
  "Yes, invite them", "Not yet".
410
- - **The explain-or-jump question:** "Explain HQ first", "Jump straight in".
411
+ - **The explain-or-jump question:** "Explain HQ first", "Jump straight in",
412
+ in exactly those words. The HQ app answers "Explain HQ first" by itself.
411
413
  - **You finished something and did not ask anything:** the one or two next
412
414
  steps most people would take ("Connect my calls", "How do I add my team?").
413
415
 
@@ -79,6 +79,7 @@ export { MAX_LANE_DEPTH };
79
79
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
80
80
  export const LANE_REMOTE_MIRROR_SYNC_FLAG = "lanes.remote-done-mirror";
81
81
  const REMOTE_MIRROR_REFRESH_BATCH_SIZE = 100;
82
+ const REMOTE_LANE_NOT_FOUND_GRACE_MS = 15 * 60_000;
82
83
  /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
83
84
  export { emitLaneDecisionEvents };
84
85
  export { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG };
@@ -1129,6 +1130,7 @@ function normalizedLaneListQuery(query) {
1129
1130
  ...(hierarchy === undefined ? {} : { hierarchy }),
1130
1131
  ...(query.snapshot ? { snapshot: true } : {}),
1131
1132
  ...(query.strictSnapshot ? { strictSnapshot: true } : {}),
1133
+ ...(query.includeForeignHostRows ? { includeForeignHostRows: true } : {}),
1132
1134
  };
1133
1135
  }
1134
1136
  async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions = {}) {
@@ -1191,7 +1193,7 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1191
1193
  // Outpost's own store, so preserve those local identities for accounting.
1192
1194
  const hasLocalHostIdentity = rowHost.kind === "local" &&
1193
1195
  typeof rowHost.id === "string" && rowHost.id.trim() !== "";
1194
- if (!belongsToOutpost && !hasLocalHostIdentity)
1196
+ if (!belongsToOutpost && !hasLocalHostIdentity && !query.includeForeignHostRows)
1195
1197
  continue;
1196
1198
  try {
1197
1199
  lanes.push(parseLaneRecord(normalizeLaneRecordView(raw)));
@@ -1216,6 +1218,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
1216
1218
  const result = {
1217
1219
  updated: new Map(),
1218
1220
  unverifiedRemote: new Map(),
1221
+ missingOnOwner: new Map(),
1219
1222
  };
1220
1223
  const host = reconcileHostFromCtx(ctx);
1221
1224
  if (ctx.env.HQ_LANES_REMOTE_READ === "1" || parentLaneIds?.size === 0)
@@ -1285,6 +1288,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
1285
1288
  laneIds: batch.map((lane) => lane.lane_id),
1286
1289
  snapshot: true,
1287
1290
  strictSnapshot: true,
1291
+ includeForeignHostRows: true,
1288
1292
  });
1289
1293
  }
1290
1294
  catch (error) {
@@ -1306,6 +1310,8 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
1306
1310
  !ownerIsHostedByOutpost ||
1307
1311
  (owner.company?.uid ?? null) !== (mirror.company?.uid ?? null) ||
1308
1312
  (owner.company?.slug ?? null) !== (mirror.company?.slug ?? null)) {
1313
+ if (matches.length === 0)
1314
+ result.missingOnOwner.set(mirror.lane_id, mirror);
1309
1315
  const detail = matches.length === 0
1310
1316
  ? "owner did not return the requested lane"
1311
1317
  : matches.length > 1
@@ -1329,6 +1335,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
1329
1335
  }
1330
1336
  const needsUpdate = before.state !== owner.state ||
1331
1337
  before.queued_reason !== (owner.state === "queued" ? owner.queued_reason : undefined) ||
1338
+ JSON.stringify(before.queued_details) !== JSON.stringify(owner.state === "queued" ? owner.queued_details : undefined) ||
1332
1339
  before.ended_at !== endedAt ||
1333
1340
  before.envelope_summary !== envelopeSummary ||
1334
1341
  (isTerminalLaneState(owner.state) && (JSON.stringify(before.last_envelope) !== JSON.stringify(owner.last_envelope) ||
@@ -1348,6 +1355,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
1348
1355
  ...current,
1349
1356
  state: owner.state,
1350
1357
  queued_reason: owner.state === "queued" ? owner.queued_reason : undefined,
1358
+ queued_details: owner.state === "queued" ? owner.queued_details : undefined,
1351
1359
  ended_at: endedAt,
1352
1360
  envelope_summary: envelopeSummary,
1353
1361
  ...(isTerminalLaneState(owner.state) ? {
@@ -2445,6 +2453,47 @@ function markDispatchFailure(hqRoot, laneId, details) {
2445
2453
  };
2446
2454
  });
2447
2455
  }
2456
+ function retainQueuedOutpostAfterUnknownDelivery(hqRoot, laneId, outpostId, commandId) {
2457
+ return casUpdateLane(hqRoot, laneId, (current) => {
2458
+ if (current.state !== "queued" ||
2459
+ current.host.kind !== "outpost" ||
2460
+ current.host.id !== outpostId)
2461
+ return current;
2462
+ return {
2463
+ ...current,
2464
+ queued_details: {
2465
+ reason: "outpost_dispatch_unknown",
2466
+ outcome_unknown: true,
2467
+ command_id: commandId,
2468
+ },
2469
+ };
2470
+ });
2471
+ }
2472
+ function markRemoteLaneLost(hqRoot, laneId, outpostId, at, writer, operatorReason, expectedVersion) {
2473
+ return retryUpdateLane(hqRoot, laneId, writer, (current) => {
2474
+ if (current.host.kind !== "outpost" ||
2475
+ current.host.id !== outpostId ||
2476
+ (expectedVersion !== undefined && current.version !== expectedVersion) ||
2477
+ isTerminalLaneState(current.state))
2478
+ return current;
2479
+ return {
2480
+ ...current,
2481
+ state: "failed",
2482
+ queued_reason: undefined,
2483
+ queued_details: undefined,
2484
+ terminal_reason: "remote_lost",
2485
+ terminal_details: {
2486
+ outpost_id: outpostId,
2487
+ owner_error: "lane_not_found",
2488
+ ...(operatorReason === undefined ? {} : { operator_reason: operatorReason }),
2489
+ },
2490
+ stop_history: [
2491
+ ...(current.stop_history ?? []),
2492
+ { at, reason: "remote_lost" },
2493
+ ],
2494
+ };
2495
+ });
2496
+ }
2448
2497
  /**
2449
2498
  * The CAS updater a pickup failure applies. A lane that is already terminal
2450
2499
  * for any reason (a cancel that landed after the caller's last read, an
@@ -3528,6 +3577,22 @@ async function dispatchCreatedOutpostLane(ctx, input) {
3528
3577
  exit_code: dispatched.exitCode,
3529
3578
  });
3530
3579
  }
3580
+ const commandId = dispatched.commandId?.trim();
3581
+ if (dispatched.done !== true || !commandId) {
3582
+ const details = {
3583
+ outpost_id: outpostId,
3584
+ lane_id: lane.lane_id,
3585
+ command_id: commandId || null,
3586
+ status: dispatched.status,
3587
+ done: dispatched.done ?? false,
3588
+ exit_code: dispatched.exitCode,
3589
+ outcome_unknown: true,
3590
+ };
3591
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm pickup for lane ${lane.lane_id}; the queued lane will be reconciled against its owner`, {
3592
+ ...details,
3593
+ state: retainQueuedOutpostAfterUnknownDelivery(hqRoot, lane.lane_id, outpostId, commandId || null).state,
3594
+ });
3595
+ }
3531
3596
  const stdoutTrimmed = dispatched.stdout.trim();
3532
3597
  if (remote === null && stdoutTrimmed !== "" && dispatched.status !== "Success") {
3533
3598
  if (preserveQueuedOnFailure) {
@@ -3606,6 +3671,9 @@ async function dispatchCreatedOutpostLane(ctx, input) {
3606
3671
  catch (err) {
3607
3672
  if (preserveQueuedOnFailure)
3608
3673
  throw err;
3674
+ if (isLanesError(err) && err.error === "outpost_delivery_unconfirmed" && err.details.outcome_unknown === true) {
3675
+ throw err;
3676
+ }
3609
3677
  if (isLanesError(err) &&
3610
3678
  err.error === DISPATCH_TERMINAL_REASON &&
3611
3679
  readLane(hqRoot, lane.lane_id).state === "failed") {
@@ -4551,7 +4619,7 @@ async function runList(ctx, opts, fastRouteFlags) {
4551
4619
  }
4552
4620
  const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
4553
4621
  const remoteRefresh = remoteRead
4554
- ? { updated: new Map(), unverifiedRemote: new Map() }
4622
+ ? { updated: new Map(), unverifiedRemote: new Map(), missingOnOwner: new Map() }
4555
4623
  : await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(selectedLanes
4556
4624
  .filter((lane) => !isTerminalLaneState(lane.state))
4557
4625
  .map((lane) => lane.lane_id)));
@@ -7035,11 +7103,21 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId, wa
7035
7103
  commandId,
7036
7104
  };
7037
7105
  }
7106
+ function isDefiniteOutpostLaneNotFound(dispatched, laneId, ownerError) {
7107
+ return dispatched.done === true &&
7108
+ Boolean(dispatched.commandId?.trim()) &&
7109
+ dispatched.status === "Failed" &&
7110
+ dispatched.exitCode !== null &&
7111
+ dispatched.exitCode !== 0 &&
7112
+ ownerError?.error === "lane_not_found" &&
7113
+ ownerError.details.lane_id === laneId;
7114
+ }
7038
7115
  async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
7039
7116
  if (lane.host.kind !== "outpost") {
7040
7117
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
7041
7118
  }
7042
7119
  const outpostId = lane.host.id;
7120
+ const hqRoot = ctx.hqRoot();
7043
7121
  const waitMs = outpostSingleLaneWaitMs(ctx.env);
7044
7122
  let dispatched;
7045
7123
  try {
@@ -7063,7 +7141,36 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
7063
7141
  required_option: "--reason",
7064
7142
  });
7065
7143
  }
7144
+ const commandId = dispatched.commandId?.trim();
7066
7145
  const ownerError = ownerErrorFromOutput(dispatched.stdout);
7146
+ if (isDefiniteOutpostLaneNotFound(dispatched, lane.lane_id, ownerError)) {
7147
+ const at = ctx.now().toISOString();
7148
+ const updated = markRemoteLaneLost(ctx.hqRoot(), lane.lane_id, outpostId, at, "command", reason);
7149
+ if (updated.terminal_reason !== "remote_lost") {
7150
+ throw new LanesError("already_terminal", `Lane ${lane.lane_id} became terminal before its missing Outpost record could be marked remote_lost`, { lane_id: lane.lane_id, state: updated.state });
7151
+ }
7152
+ const cancelled = cancelPendingQuestionsForLane(hqRoot, lane.lane_id, at, "remote_lost");
7153
+ await emitLaneTerminalStateSoft(ctx, updated);
7154
+ if (!quiet)
7155
+ printJson({
7156
+ ok: true,
7157
+ action: "stop",
7158
+ reason: "remote_lost",
7159
+ lane: updated,
7160
+ terminal: true,
7161
+ resumable: false,
7162
+ cancelled_questions: cancelled.map((question) => question.question_id),
7163
+ outpost_id: outpostId,
7164
+ outpost_command_id: commandId,
7165
+ outpost_result: {
7166
+ state: "failed",
7167
+ reason: "remote_lost",
7168
+ status: dispatched.status,
7169
+ exit_code: dispatched.exitCode,
7170
+ },
7171
+ });
7172
+ return;
7173
+ }
7067
7174
  if (ownerError) {
7068
7175
  throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the stop command: ${ownerError.message}`, {
7069
7176
  lane_id: lane.lane_id,
@@ -7075,7 +7182,6 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
7075
7182
  owner_error: ownerError,
7076
7183
  });
7077
7184
  }
7078
- const commandId = dispatched.commandId?.trim();
7079
7185
  if (dispatched.done !== true || !commandId) {
7080
7186
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
7081
7187
  lane_id: lane.lane_id,
@@ -7559,8 +7665,42 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7559
7665
  const fromEnv = ctx.env.HQ_ROOT?.trim();
7560
7666
  const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
7561
7667
  const mirrorRefresh = opts.dryRun
7562
- ? { updated: new Map(), unverifiedRemote: new Map() }
7668
+ ? {
7669
+ updated: new Map(),
7670
+ unverifiedRemote: new Map(),
7671
+ missingOnOwner: new Map(),
7672
+ }
7563
7673
  : await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot));
7674
+ const remoteLost = [];
7675
+ const remoteLostErrors = [];
7676
+ if (!opts.dryRun && mirrorRefresh.missingOnOwner.size > 0) {
7677
+ const at = ctx.now().toISOString();
7678
+ const nowMs = Date.parse(at);
7679
+ for (const [laneId, mirror] of mirrorRefresh.missingOnOwner) {
7680
+ const createdAtMs = Date.parse(mirror.created_at);
7681
+ if (!Number.isFinite(createdAtMs) ||
7682
+ nowMs - createdAtMs < REMOTE_LANE_NOT_FOUND_GRACE_MS)
7683
+ continue;
7684
+ try {
7685
+ const updated = markRemoteLaneLost(hqRoot, laneId, mirror.host.id, at, "reconcile", undefined, mirror.version);
7686
+ if (updated.terminal_reason !== "remote_lost") {
7687
+ if (updated.version !== mirror.version) {
7688
+ mirrorRefresh.unverifiedRemote.set(laneId, "mirror changed after the owner snapshot");
7689
+ }
7690
+ continue;
7691
+ }
7692
+ remoteLost.push({ id: laneId, reason: "remote_lost" });
7693
+ cancelPendingQuestionsForLane(hqRoot, laneId, at, "remote_lost");
7694
+ await emitLaneTerminalStateSoft(ctx, updated);
7695
+ }
7696
+ catch (err) {
7697
+ remoteLostErrors.push({
7698
+ laneId,
7699
+ message: err instanceof Error ? err.message : String(err),
7700
+ });
7701
+ }
7702
+ }
7703
+ }
7564
7704
  const summary = await runReconcilePass(hqRoot, {
7565
7705
  now: ctx.now,
7566
7706
  host: reconcileHostFromCtx(ctx),
@@ -7676,10 +7816,22 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7676
7816
  }
7677
7817
  },
7678
7818
  });
7819
+ for (const failure of remoteLostErrors) {
7820
+ summary.errors.push({
7821
+ code: "remote_lane_terminalize_failed",
7822
+ message: `Could not mark remote lane ${failure.laneId} lost: ${failure.message}`,
7823
+ });
7824
+ summary.ok = false;
7825
+ }
7826
+ const terminalizedRemoteIds = new Set(remoteLost.map(({ id }) => id));
7679
7827
  printJson({
7680
7828
  ...summary,
7829
+ ...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
7681
7830
  ...(mirrorRefresh.unverifiedRemote.size > 0
7682
- ? { unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()] }
7831
+ ? {
7832
+ unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()]
7833
+ .filter((id) => !terminalizedRemoteIds.has(id)),
7834
+ }
7683
7835
  : {}),
7684
7836
  });
7685
7837
  if (!summary.ok) {
@@ -52,7 +52,7 @@ import * as path from "node:path";
52
52
  import { fullJitterDelayMs } from "../mesh/live/backoff.js";
53
53
  import { acquirePidLock, defaultPidLockDeps, releasePidLock } from "../mesh/live/daemon/pid-lock.js";
54
54
  import { BotApiError } from "./api.js";
55
- import { effectiveBotCompanies, effectiveBotKind, patchBotConfig, readBotConfig, SETUP_BOT_WORKER_ID } from "./config.js";
55
+ import { effectiveBotCompanies, effectiveBotKind, patchBotConfig, readBotConfig, resolveBotMemoryDir, SETUP_BOT_WORKER_ID } from "./config.js";
56
56
  import { ensureBotSessionMeta } from "./company-bind.js";
57
57
  import { readBotCredsIdentity } from "./creds.js";
58
58
  import { isProcessed, markProcessed, readInboxState } from "./inbox-state.js";
@@ -61,6 +61,7 @@ import { containsNoReplySentinel, ProgressPoster, } from "./progress.js";
61
61
  import { splitBody } from "./split.js";
62
62
  import { createBotLogger } from "./log.js";
63
63
  import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
64
+ import { loadSetupExplainer, patchSetupIntroState, readSetupIntroState, readSetupProgressIntro, replyOffersExplain, SETUP_EXPLAINED_NOTE, setupExplainDue, } from "./setup-explain.js";
64
65
  import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled } from "./runtime/claude.js";
65
66
  import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
66
67
  import { botAttachmentsDir, dmSessionScope } from "./paths.js";
@@ -296,6 +297,26 @@ export async function runBot(deps) {
296
297
  const turnCwd = config.hqRoot;
297
298
  // HQ's setup bot is a guided conversation: no "working on it" filler between steps.
298
299
  const isSetupBot = config.workerId === SETUP_WORKER_ID;
300
+ /**
301
+ * Setup bot, owner DM: remember whether the bot's latest reply asked the
302
+ * explain-or-jump question, so a click on "Explain HQ first" can be answered
303
+ * at once (setup-explain.ts). Never lets a bookkeeping error touch the turn.
304
+ */
305
+ const noteSetupReply = (reply) => {
306
+ if (!isSetupBot)
307
+ return;
308
+ try {
309
+ const offered = replyOffersExplain(reply);
310
+ const current = readSetupIntroState(dir);
311
+ if (offered && !current.offeredAt)
312
+ patchSetupIntroState(dir, { offeredAt: now().toISOString() });
313
+ else if (!offered && current.offeredAt)
314
+ patchSetupIntroState(dir, { offeredAt: undefined });
315
+ }
316
+ catch (err) {
317
+ log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
318
+ }
319
+ };
299
320
  // Scheduled runs are reported in the owner's own zone: "every weekday at 9am"
300
321
  // was set up here, so "due 13:00 UTC" would read as the wrong time.
301
322
  const timeZone = machineTimeZone();
@@ -1105,6 +1126,7 @@ export async function runBot(deps) {
1105
1126
  return;
1106
1127
  }
1107
1128
  clearInflight(dir, KICKOFF_MESSAGE_ID);
1129
+ noteSetupReply(outcome.reply);
1108
1130
  if (readBotConfig(dir)?.kickoffPendingSignIn)
1109
1131
  patchBotConfig(dir, { kickoffPendingSignIn: false });
1110
1132
  const st = readStatus();
@@ -1323,6 +1345,54 @@ export async function runBot(deps) {
1323
1345
  await finish("failed", 0, `${FAILURE_LIMIT} model failures in 10 minutes; last: ${outcome.excerpt}`);
1324
1346
  }
1325
1347
  };
1348
+ /**
1349
+ * The instant answer to "Explain HQ first" (setup-explain.ts). Returns true
1350
+ * when it answered the message (sent, marked processed, acked); false sends
1351
+ * the message down the normal model path, so nothing here can leave a
1352
+ * message unanswered.
1353
+ */
1354
+ const sendSetupExplanation = async (item, text, attachments) => {
1355
+ const id = item.messageId;
1356
+ let reply;
1357
+ try {
1358
+ const due = setupExplainDue({
1359
+ text,
1360
+ attachments,
1361
+ state: readSetupIntroState(dir),
1362
+ progress: readSetupProgressIntro(resolveBotMemoryDir(config)),
1363
+ });
1364
+ if (!due)
1365
+ return false;
1366
+ const explainer = loadSetupExplainer(config.hqRoot, config.workerDir);
1367
+ if (!explainer) {
1368
+ log("warn", `no usable explain-hq.md in ${config.workerDir}; answering ${id} with a model turn`);
1369
+ return false;
1370
+ }
1371
+ reply = explainer.reply;
1372
+ }
1373
+ catch (err) {
1374
+ log("warn", `setup explanation check failed (${err instanceof Error ? err.message : String(err)}); answering ${id} with a model turn`);
1375
+ return false;
1376
+ }
1377
+ try {
1378
+ await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, reply, item.rootEventId, "human");
1379
+ }
1380
+ catch (err) {
1381
+ log("warn", `setup explanation for ${id} could not be sent (${err instanceof Error ? err.message : String(err)}); answering with a model turn`);
1382
+ return false;
1383
+ }
1384
+ markProcessed(dir, id, now().toISOString());
1385
+ try {
1386
+ patchSetupIntroState(dir, { explainedAt: now().toISOString(), notePending: true, offeredAt: undefined });
1387
+ }
1388
+ catch (err) {
1389
+ log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
1390
+ }
1391
+ await api.ackInbox(config.agentUid, id);
1392
+ setStatus({ repliesSent: (readStatus()?.repliesSent ?? 0) + 1, lastActivityAt: now().toISOString() });
1393
+ log("info", `answered ${id} with the setup explanation (no model turn)`);
1394
+ return true;
1395
+ };
1326
1396
  const handleItem = async (item) => {
1327
1397
  const id = item.messageId;
1328
1398
  const inboxState = readInboxState(dir);
@@ -1381,6 +1451,11 @@ export async function runBot(deps) {
1381
1451
  await api.ackInbox(config.agentUid, id);
1382
1452
  return;
1383
1453
  }
1454
+ // Setup bot: "Explain HQ first" clicked while that question is pending is
1455
+ // answered with the fixed explanation, with no model turn.
1456
+ const ownerDm = target.kind === "dm" && target.peerUid === config.ownerUid;
1457
+ if (isSetupBot && ownerDm && (await sendSetupExplanation(item, messageText, attachmentRefs.length)))
1458
+ return;
1384
1459
  const attempt = (attempts.get(id) ?? 0) + 1;
1385
1460
  attempts.set(id, attempt);
1386
1461
  const reactionId = item.eventId ?? id;
@@ -1441,7 +1516,10 @@ export async function runBot(deps) {
1441
1516
  text = text ? `${text}\n\n${block}` : block;
1442
1517
  }
1443
1518
  const sessionScope = target.kind === "room" ? target.sessionScope : dmSessionScope(target.peerUid, config.ownerUid);
1444
- const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? text :
1519
+ // The turn after the app sent the explanation: tell the model it went out.
1520
+ const setupNote = isSetupBot && ownerDm && readSetupIntroState(dir).notePending === true;
1521
+ const ownerText = setupNote ? `${SETUP_EXPLAINED_NOTE}\n\n${text}` : text;
1522
+ const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? ownerText :
1445
1523
  `[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
1446
1524
  const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
1447
1525
  const { send, sendFinal, saved } = trackingSend(markerTarget, item.rootEventId);
@@ -1463,6 +1541,18 @@ export async function runBot(deps) {
1463
1541
  ? { afterProgressPost: () => api.postAgentStatus(target.channelId, ROOM_THINKING_STATUS, target.rootEventId) }
1464
1542
  : {}),
1465
1543
  });
1544
+ // The model saw the note once its turn ran. A turn that never started, or
1545
+ // one held for a coding-tool sign-in, runs again and keeps the note for it.
1546
+ if (setupNote && (outcome.ok || (outcome.started && !isRuntimeSignInFailure(outcome.excerpt)))) {
1547
+ try {
1548
+ patchSetupIntroState(dir, { notePending: false });
1549
+ }
1550
+ catch (err) {
1551
+ log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
1552
+ }
1553
+ }
1554
+ if (outcome.ok && ownerDm)
1555
+ noteSetupReply(outcome.reply);
1466
1556
  if (outcome.ok) {
1467
1557
  // A no_reply sentinel or intentionally empty reply: ack silently, post nothing.
1468
1558
  if (outcome.silent) {
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The setup bot's instant "Explain HQ first" answer.
3
+ *
4
+ * The setup bot's first question is whether to explain HQ or jump straight in,
5
+ * with "Explain HQ first" as a suggested reply. The explanation is fixed text,
6
+ * so a click on that button does not need a model turn: the runner sends the
7
+ * text from the setup worker's `skills/explain-hq.md` at once, and the bot's
8
+ * next turn starts with a note saying the explanation already went out.
9
+ *
10
+ * The text has one home, the worker file. The model reads the same file when
11
+ * someone asks for the explanation in their own words, so the two paths never
12
+ * drift. Anything unexpected (file missing or malformed, the question not
13
+ * pending, any other message) falls back to the normal model turn.
14
+ */
15
+ /** The suggested reply that triggers the instant answer. Exact match only. */
16
+ export declare const EXPLAIN_HQ_SUGGESTION = "Explain HQ first";
17
+ /** Where the explanation lives, relative to the setup worker folder. */
18
+ export declare const EXPLAIN_HQ_FILE: string;
19
+ /** The setup bot's progress note in its memory folder (setup worker.yaml). */
20
+ export declare const SETUP_PROGRESS_FILE = "setup-progress.md";
21
+ /** Runner-owned state, in the bot's own folder (never the model's memory). */
22
+ export declare const SETUP_INTRO_STATE_FILE = "setup-intro.json";
23
+ /**
24
+ * Put in front of the next message from the person, so the model knows the
25
+ * explanation went out without it. It never reaches the person.
26
+ */
27
+ export declare const SETUP_EXPLAINED_NOTE: string;
28
+ export interface SetupExplainer {
29
+ /** The explanation, word for word (shared with the model path). */
30
+ explanation: string;
31
+ /** What the app adds after it: a line offering to continue, and suggestions. */
32
+ closing: string;
33
+ /** The whole message the app sends. */
34
+ reply: string;
35
+ }
36
+ /** Parse the worker file. Null when it is not the shape the app can send. */
37
+ export declare function parseSetupExplainer(body: string): SetupExplainer | null;
38
+ /** Read the explanation from the setup worker folder the bot runs. */
39
+ export declare function loadSetupExplainer(hqRoot: string, workerDir: string): SetupExplainer | null;
40
+ export type SetupProgressIntro = "asked" | "answered" | "finished" | null;
41
+ /** What the model's progress note says about the opening question. */
42
+ export declare function readSetupProgressIntro(memoryDir: string): SetupProgressIntro;
43
+ export interface SetupIntroState {
44
+ v: 1;
45
+ /** The bot's latest owner-DM reply asked the explain-or-jump question. */
46
+ offeredAt?: string;
47
+ /** The app sent the explanation (never sent twice). */
48
+ explainedAt?: string;
49
+ /** The model has not yet been told that the explanation went out. */
50
+ notePending?: boolean;
51
+ }
52
+ export declare function readSetupIntroState(dir: string): SetupIntroState;
53
+ export declare function patchSetupIntroState(dir: string, patch: Partial<Omit<SetupIntroState, "v">>): SetupIntroState;
54
+ /** A bot reply that asks the explain-or-jump question (the words or the button). */
55
+ export declare function replyOffersExplain(reply: string): boolean;
56
+ /**
57
+ * The instant answer is due only when ALL of these hold: the message is exactly
58
+ * the button text with nothing attached, the question is pending (the bot's
59
+ * last reply offered it, or its note says it asked), and the explanation has
60
+ * not gone out yet (neither by the app nor, per the note, by the model).
61
+ */
62
+ export declare function setupExplainDue(input: {
63
+ text: string;
64
+ attachments: number;
65
+ state: SetupIntroState;
66
+ progress: SetupProgressIntro;
67
+ }): boolean;
68
+ //# sourceMappingURL=setup-explain.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The setup bot's instant "Explain HQ first" answer.
3
+ *
4
+ * The setup bot's first question is whether to explain HQ or jump straight in,
5
+ * with "Explain HQ first" as a suggested reply. The explanation is fixed text,
6
+ * so a click on that button does not need a model turn: the runner sends the
7
+ * text from the setup worker's `skills/explain-hq.md` at once, and the bot's
8
+ * next turn starts with a note saying the explanation already went out.
9
+ *
10
+ * The text has one home, the worker file. The model reads the same file when
11
+ * someone asks for the explanation in their own words, so the two paths never
12
+ * drift. Anything unexpected (file missing or malformed, the question not
13
+ * pending, any other message) falls back to the normal model turn.
14
+ */
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ /** The suggested reply that triggers the instant answer. Exact match only. */
18
+ export const EXPLAIN_HQ_SUGGESTION = "Explain HQ first";
19
+ /** Where the explanation lives, relative to the setup worker folder. */
20
+ export const EXPLAIN_HQ_FILE = path.join("skills", "explain-hq.md");
21
+ /** The setup bot's progress note in its memory folder (setup worker.yaml). */
22
+ export const SETUP_PROGRESS_FILE = "setup-progress.md";
23
+ /** Runner-owned state, in the bot's own folder (never the model's memory). */
24
+ export const SETUP_INTRO_STATE_FILE = "setup-intro.json";
25
+ const EXPLANATION_HEADING = /^##[ \t]+The explanation[ \t]*$/m;
26
+ const CLOSING_HEADING = /^##[ \t]+After the explanation, when the app sends it[ \t]*$/m;
27
+ /**
28
+ * Put in front of the next message from the person, so the model knows the
29
+ * explanation went out without it. It never reaches the person.
30
+ */
31
+ export const SETUP_EXPLAINED_NOTE = `[Note from the HQ app: the person clicked "${EXPLAIN_HQ_SUGGESTION}", and the app already sent them the explanation ` +
32
+ `from skills/explain-hq.md word for word, ending by asking whether to start the setup. The explain-or-jump question is answered. ` +
33
+ "Write `Intro: explained` in your progress note, do not explain HQ again, and carry on with the first unfinished setup step. " +
34
+ "Their reply to that message follows.]";
35
+ /** The text between a heading and the next `## ` heading (or the end). */
36
+ function sectionAfter(body, heading) {
37
+ const match = heading.exec(body);
38
+ if (!match)
39
+ return null;
40
+ const rest = body.slice(match.index + match[0].length);
41
+ const next = /^##[ \t]/m.exec(rest);
42
+ return (next ? rest.slice(0, next.index) : rest).trim();
43
+ }
44
+ /** True when the closing carries exactly one well-formed suggestions block. */
45
+ function hasSuggestionsBlock(closing) {
46
+ const fence = /^```hq-block\n(.+)\n```$/m.exec(closing);
47
+ if (!fence)
48
+ return false;
49
+ try {
50
+ const parsed = JSON.parse(fence[1]);
51
+ const block = parsed.blocks?.[0];
52
+ return (parsed.v === 1 &&
53
+ block?.kind === "suggestions" &&
54
+ Array.isArray(block.items) &&
55
+ block.items.length >= 2 &&
56
+ block.items.every((item) => typeof item === "string" && item.trim().length > 0));
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ /** Parse the worker file. Null when it is not the shape the app can send. */
63
+ export function parseSetupExplainer(body) {
64
+ const text = body.replace(/\r\n/g, "\n");
65
+ const explanation = sectionAfter(text, EXPLANATION_HEADING);
66
+ const closing = sectionAfter(text, CLOSING_HEADING);
67
+ if (!explanation || !closing)
68
+ return null;
69
+ // The closing must end with the block (nothing after it) so the app shows the buttons.
70
+ if (!closing.endsWith("```") || !hasSuggestionsBlock(closing))
71
+ return null;
72
+ return { explanation, closing, reply: `${explanation}\n\n${closing}` };
73
+ }
74
+ /** Read the explanation from the setup worker folder the bot runs. */
75
+ export function loadSetupExplainer(hqRoot, workerDir) {
76
+ try {
77
+ return parseSetupExplainer(fs.readFileSync(path.resolve(hqRoot, workerDir, EXPLAIN_HQ_FILE), "utf8"));
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /** What the model's progress note says about the opening question. */
84
+ export function readSetupProgressIntro(memoryDir) {
85
+ let note;
86
+ try {
87
+ note = fs.readFileSync(path.join(memoryDir, SETUP_PROGRESS_FILE), "utf8");
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ if (/^Status:[ \t]*finished/im.test(note))
93
+ return "finished";
94
+ if (/^Intro:[ \t]*(explained|skipped)\b/im.test(note))
95
+ return "answered";
96
+ if (/^Intro:[ \t]*asked\b/im.test(note))
97
+ return "asked";
98
+ return null;
99
+ }
100
+ export function readSetupIntroState(dir) {
101
+ try {
102
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, SETUP_INTRO_STATE_FILE), "utf8"));
103
+ return parsed && parsed.v === 1 ? parsed : { v: 1 };
104
+ }
105
+ catch {
106
+ return { v: 1 };
107
+ }
108
+ }
109
+ export function patchSetupIntroState(dir, patch) {
110
+ const next = { ...readSetupIntroState(dir), ...patch, v: 1 };
111
+ for (const key of Object.keys(next)) {
112
+ if (next[key] === undefined)
113
+ delete next[key];
114
+ }
115
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
116
+ const dest = path.join(dir, SETUP_INTRO_STATE_FILE);
117
+ const tmp = `${dest}.tmp.${process.pid}`;
118
+ fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
119
+ fs.renameSync(tmp, dest);
120
+ return next;
121
+ }
122
+ /** A bot reply that asks the explain-or-jump question (the words or the button). */
123
+ export function replyOffersExplain(reply) {
124
+ return reply.includes(`"${EXPLAIN_HQ_SUGGESTION}"`) || /explain HQ to you/i.test(reply);
125
+ }
126
+ /**
127
+ * The instant answer is due only when ALL of these hold: the message is exactly
128
+ * the button text with nothing attached, the question is pending (the bot's
129
+ * last reply offered it, or its note says it asked), and the explanation has
130
+ * not gone out yet (neither by the app nor, per the note, by the model).
131
+ */
132
+ export function setupExplainDue(input) {
133
+ if (input.text !== EXPLAIN_HQ_SUGGESTION || input.attachments > 0)
134
+ return false;
135
+ if (input.state.explainedAt)
136
+ return false;
137
+ if (input.progress === "answered" || input.progress === "finished")
138
+ return false;
139
+ return Boolean(input.state.offeredAt) || input.progress === "asked";
140
+ }
141
+ //# sourceMappingURL=setup-explain.js.map
@@ -532,26 +532,44 @@ export function parseLaneRecord(raw, filePath) {
532
532
  const details = raw.queued_details;
533
533
  if (details === null || typeof details !== "object" || Array.isArray(details) ||
534
534
  typeof details.reason !== "string") {
535
- throw new LaneSchemaError("queued_details must describe a worker_profile_changed queue reason", {
535
+ throw new LaneSchemaError("queued_details must describe a supported queue condition", {
536
536
  path: filePath,
537
537
  field: "queued_details",
538
538
  });
539
539
  }
540
540
  const detailsRecord = details;
541
- if (detailsRecord.reason !== "worker_profile_changed" ||
542
- (detailsRecord.expected_hash !== null && typeof detailsRecord.expected_hash !== "string") ||
543
- typeof detailsRecord.actual_hash !== "string" || detailsRecord.actual_hash === "" ||
544
- typeof detailsRecord.recovery_command !== "string" || detailsRecord.recovery_command.trim() === "") {
545
- throw new LaneSchemaError("queued_details must describe a worker_profile_changed queue reason", {
546
- path: filePath,
547
- field: "queued_details",
548
- });
541
+ if (detailsRecord.reason === "worker_profile_changed") {
542
+ if ((detailsRecord.expected_hash !== null && typeof detailsRecord.expected_hash !== "string") ||
543
+ typeof detailsRecord.actual_hash !== "string" || detailsRecord.actual_hash === "" ||
544
+ typeof detailsRecord.recovery_command !== "string" || detailsRecord.recovery_command.trim() === "") {
545
+ throw new LaneSchemaError("queued_details must describe a worker_profile_changed queue reason", {
546
+ path: filePath,
547
+ field: "queued_details",
548
+ });
549
+ }
550
+ if (raw.queued_reason !== "worker_profile_changed") {
551
+ throw new LaneSchemaError("queued_details requires queued_reason worker_profile_changed", {
552
+ path: filePath,
553
+ field: "queued_details",
554
+ queued_reason: raw.queued_reason,
555
+ });
556
+ }
557
+ }
558
+ else if (detailsRecord.reason === "outpost_dispatch_unknown") {
559
+ if (raw.state !== "queued" ||
560
+ detailsRecord.outcome_unknown !== true ||
561
+ (detailsRecord.command_id !== null &&
562
+ (typeof detailsRecord.command_id !== "string" || detailsRecord.command_id.trim() === ""))) {
563
+ throw new LaneSchemaError("queued_details must describe an unknown Outpost dispatch on a queued lane", {
564
+ path: filePath,
565
+ field: "queued_details",
566
+ });
567
+ }
549
568
  }
550
- if (raw.queued_reason !== "worker_profile_changed") {
551
- throw new LaneSchemaError("queued_details requires queued_reason worker_profile_changed", {
569
+ else {
570
+ throw new LaneSchemaError("queued_details must describe a supported queue condition", {
552
571
  path: filePath,
553
572
  field: "queued_details",
554
- queued_reason: raw.queued_reason,
555
573
  });
556
574
  }
557
575
  }
@@ -175,12 +175,16 @@ export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
175
175
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
176
176
  export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed"];
177
177
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
178
- export interface QueuedDetails {
178
+ export type QueuedDetails = {
179
179
  reason: "worker_profile_changed";
180
180
  expected_hash: string | null;
181
181
  actual_hash: string;
182
182
  recovery_command: string;
183
- }
183
+ } | {
184
+ reason: "outpost_dispatch_unknown";
185
+ outcome_unknown: true;
186
+ command_id: string | null;
187
+ };
184
188
  export declare function isQueuedReason(value: unknown): value is QueuedReason;
185
189
  export declare const HOST_KINDS: readonly ["local", "outpost"];
186
190
  export type HostKind = (typeof HOST_KINDS)[number];
@@ -231,7 +231,7 @@ export const LANE_FIELD_WRITERS = {
231
231
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
232
232
  last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
233
233
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
234
- admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
234
+ admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command", "reconcile"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
235
235
  quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
236
236
  provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
237
237
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.288.0",
3
+ "version": "5.289.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {