@indigoai-us/hq-cli 5.287.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +48 -5
  2. package/assets/bot-workers/setup/skills/explain-hq.md +35 -0
  3. package/assets/bot-workers/setup/worker.yaml +25 -23
  4. package/dist/command-catalog.generated.d.ts +3 -0
  5. package/dist/command-catalog.generated.js +4 -0
  6. package/dist/commands/agent-enroll.js +10 -2
  7. package/dist/commands/agents.d.ts +1 -0
  8. package/dist/commands/agents.js +12 -4
  9. package/dist/commands/core-ontology.d.ts +4 -1
  10. package/dist/commands/core-ontology.js +26 -0
  11. package/dist/commands/lanes-resume.js +41 -2
  12. package/dist/commands/lanes-shared.d.ts +2 -0
  13. package/dist/commands/lanes.js +204 -6
  14. package/dist/lanes-drain-hook.d.ts +12 -0
  15. package/dist/lanes-drain-hook.js +59 -55
  16. package/dist/lib/bot/run.js +92 -2
  17. package/dist/lib/bot/setup-explain.d.ts +68 -0
  18. package/dist/lib/bot/setup-explain.js +141 -0
  19. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +10 -1
  20. package/dist/lib/checkpoint-ontology/merge-copies.js +17 -2
  21. package/dist/lib/checkpoint-ontology/plan.d.ts +0 -88
  22. package/dist/lib/checkpoint-ontology/plan.js +80 -107
  23. package/dist/lib/lanes/dropbox.d.ts +1 -2
  24. package/dist/lib/lanes/dropbox.js +10 -111
  25. package/dist/lib/lanes/message-replay.d.ts +8 -0
  26. package/dist/lib/lanes/message-replay.js +119 -0
  27. package/dist/lib/lanes/store.js +30 -12
  28. package/dist/lib/lanes/types.d.ts +6 -2
  29. package/dist/lib/lanes/types.js +1 -1
  30. package/dist/lib/plan-limit-nag.js +11 -0
  31. package/dist/main.js +17 -4
  32. package/dist/utils/cli-telemetry.d.ts +21 -0
  33. package/dist/utils/cli-telemetry.js +101 -1
  34. package/dist/utils/cognito-refresh-sentry.d.ts +3 -0
  35. package/dist/utils/cognito-refresh-sentry.js +49 -0
  36. package/dist/utils/cognito-session.js +55 -0
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.289.0] — 2026-09-29
6
+
5
7
  ## [5.287.0] — 2026-09-29
6
8
  - Group hook-timeout events by script while preserving triage diagnostics.
7
9
  - Preserve safe slow-child script basenames in hook-timeout diagnostics.
8
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.
9
12
  - Add native TypeScript implementations for four `hq core` worktree and
10
13
  handoff scripts behind the default-off `core-native-handoff` flag. The
11
14
  original shell assets remain bundled as the rollback path.
@@ -15,6 +18,44 @@
15
18
  - `hq team-sync` keeps its successful no-team result, reports which company
16
19
  directories are cloud-backed, and points to `hq sync pull --all` and
17
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.
25
+
26
+ ### Fixed
27
+
28
+ - Confirm primary-dispatched Outpost profile recovery when it is queued for
29
+ reconcile, and sync the cleared recovery state to the primary lane mirror.
30
+ - Cache-first `hq lanes list` now allows one detached Outpost summary refresh
31
+ per HQ root at a time, avoiding duplicate work from overlapping list calls.
32
+
33
+ ### Fixed
34
+
35
+ - The lanes hook reads only transcript bytes appended since its last check.
36
+ It still replays compaction messages and resets the offset when a transcript
37
+ is replaced or truncated.
38
+
39
+ ### Added
40
+
41
+ - `hq core ontology merge-copies --only <file>` handles only the copy -> original pairs listed in a JSON file and reports every other found pair as skipped (`not_listed`).
42
+
43
+ ### Changed
44
+
45
+ - The checkpoint ontology step always applies its ten quality rules (company
46
+ brief, annotation stripping on grok, grok-first backend, no link aliases,
47
+ first-name people, ambiguous concept links, dominant sibling link, hub
48
+ mention floor, concept-project link, named evidence). Their
49
+ `checkpoint.ontology.*` flags, on for everyone since 2026-09-29, are no
50
+ longer read.
51
+
52
+ ### Added
53
+
54
+ - Record company-scoped exposure for printed CLI plan-limit notices when the default-off `billing.limit-at-action-prompt` flag is enabled; count an upgrade engagement only after the user confirms opening the link.
55
+
56
+ ### Fixed
57
+
58
+ - `hq secrets` and other noninteractive person-token callers reuse a fresh same-principal session written by another process while a stale refresh is rejected.
18
59
 
19
60
  ## [5.286.0] — 2026-09-29
20
61
 
@@ -27,11 +68,6 @@
27
68
  it, and output the old daemon had not read reaches the new one. Other
28
69
  platforms keep the anonymous pipe.
29
70
 
30
- ### Fixed
31
-
32
- - Cache-first `hq lanes list` now allows one detached Outpost summary refresh
33
- per HQ root at a time, avoiding duplicate work from overlapping list calls.
34
-
35
71
  ## [5.285.0] — 2026-09-29
36
72
 
37
73
  - Route eligible Linux CI jobs onto ARM runners while retaining existing test and platform gates.
@@ -42,6 +78,13 @@
42
78
 
43
79
  ### Fixed
44
80
 
81
+ - Recover a queued Outpost lane held for `worker_profile_changed` with its
82
+ printed `hq lanes resume <id> --worker <profile>` command. The lane owner's
83
+ profile copy is authoritative: the command records an authorized re-pin in
84
+ history, leaves the lane queued for normal reconcile admission, and syncs the
85
+ cleared recovery state back to the primary mirror. Other queued lanes still
86
+ require `hq lanes admit`.
87
+
45
88
  - A remote `hq lanes list` read no longer dispatches owner hire summaries,
46
89
  capacity probes, or mirror snapshots to other Outposts. Two Outposts that
47
90
  each held a lane placed on the other ran `hq lanes list --json` on each other
@@ -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
 
@@ -5205,6 +5205,9 @@ export declare const COMMAND_CATALOG: readonly [{
5205
5205
  }, {
5206
5206
  readonly flags: "--limit <n>";
5207
5207
  readonly description: "handle at most this many copies";
5208
+ }, {
5209
+ readonly flags: "--only <file>";
5210
+ readonly description: "handle only the copy -> original pairs listed in this JSON file";
5208
5211
  }, {
5209
5212
  readonly flags: "--json";
5210
5213
  readonly description: "print JSON";
@@ -6730,6 +6730,10 @@ export const COMMAND_CATALOG = [
6730
6730
  "flags": "--limit <n>",
6731
6731
  "description": "handle at most this many copies"
6732
6732
  },
6733
+ {
6734
+ "flags": "--only <file>",
6735
+ "description": "handle only the copy -> original pairs listed in this JSON file"
6736
+ },
6733
6737
  {
6734
6738
  "flags": "--json",
6735
6739
  "description": "print JSON"
@@ -20,6 +20,7 @@ import * as os from "node:os";
20
20
  import { CLI_VERSION } from "../cli-version.js";
21
21
  import { DEFAULT_VAULT_API_URL, personTokenCacheFile, } from "../utils/cognito-session.js";
22
22
  import { HQ_CLIENT_NAME } from "../utils/vault-api.js";
23
+ import { queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, } from "../utils/cli-telemetry.js";
23
24
  import { networkTransportErrorCode } from "../utils/network-transport-error.js";
24
25
  import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
25
26
  import { generateHostKeyPair, machineCredsFileExists, readExternalMachineCreds, writeHostKeyPair, writeMachineCreds, } from "../lib/agent-kit/creds.js";
@@ -373,9 +374,14 @@ export function registerAgentEnrollCommand(agent) {
373
374
  else if (err instanceof EnrollError && err.upgradeUrl) {
374
375
  const interactive = canOfferTeamUpgrade();
375
376
  console.error(chalk.red(formatEnrollFailureMessage(err, { urlOnly: !interactive })));
377
+ const exposure = err.code === "billing_required"
378
+ ? queueCliLimitNoticeExposure("agents", "refusal")
379
+ : null;
376
380
  if (interactive) {
377
381
  try {
378
- await offerUpgradeLink(err.upgradeUrl, { interactive: true });
382
+ const opened = await offerUpgradeLink(err.upgradeUrl, { interactive: true });
383
+ if (opened)
384
+ queueCliLimitNoticeEngagement(exposure);
379
385
  }
380
386
  catch (openErr) {
381
387
  const message = openErr instanceof Error ? openErr.message : String(openErr);
@@ -386,7 +392,9 @@ export function registerAgentEnrollCommand(agent) {
386
392
  else {
387
393
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
388
394
  }
389
- process.exit(1);
395
+ // Let the CLI's bounded finalization flush the best-effort prompt
396
+ // telemetry before the process exits with the same failure status.
397
+ process.exitCode = 1;
390
398
  }
391
399
  });
392
400
  }
@@ -62,6 +62,7 @@ export declare function agentCreatePlanLimitFromPayload(status: number, body: un
62
62
  /** Print a server-priced plan block without minting or rewriting its URL. */
63
63
  export declare function surfaceAgentCreatePlanLimit(planLimit: AgentCreatePlanLimit, opts?: {
64
64
  json?: boolean;
65
+ companyUid?: string;
65
66
  }): Promise<void>;
66
67
  /**
67
68
  * Resolve a closed-set option value, or exit(1) with a message naming the
@@ -34,6 +34,7 @@ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
34
34
  import { maxLength } from "../utils/table-width.js";
35
35
  import { DEFAULT_VAULT_API_URL } from "../utils/cognito-session.js";
36
36
  import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
37
+ import { queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, } from "../utils/cli-telemetry.js";
37
38
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
38
39
  import { consoleOrigin } from "./integrations-connect.js";
39
40
  import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, waitForTerminalSession, } from "@indigoai-us/hq-cloud/outposts/node";
@@ -128,13 +129,14 @@ function formatServerPrice(amountMinor, currency) {
128
129
  async function offerAgentCheckout(url) {
129
130
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
130
131
  try {
131
- await offerUpgradeLink(url, {
132
+ return await offerUpgradeLink(url, {
132
133
  interactive: true,
133
134
  ask: (question) => new Promise((resolve) => rl.question(question, resolve)),
134
135
  });
135
136
  }
136
137
  catch {
137
138
  console.error("Could not open the upgrade page automatically. Use the URL above.");
139
+ return false;
138
140
  }
139
141
  finally {
140
142
  rl.close();
@@ -149,11 +151,14 @@ export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
149
151
  if (!planLimit.checkoutUrl) {
150
152
  console.error(reason);
151
153
  console.error("Ask a company owner to upgrade before creating an agent.");
154
+ queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
152
155
  return;
153
156
  }
154
157
  console.error(formatUpgradeRefusal(reason, planLimit.checkoutUrl, { urlOnly: !interactive }));
155
- if (interactive)
156
- await offerAgentCheckout(planLimit.checkoutUrl);
158
+ const exposure = queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
159
+ if (interactive && await offerAgentCheckout(planLimit.checkoutUrl)) {
160
+ queueCliLimitNoticeEngagement(exposure);
161
+ }
157
162
  }
158
163
  /**
159
164
  * Resolve a closed-set option value, or exit(1) with a message naming the
@@ -1548,7 +1553,10 @@ export function registerAgentsCommand(program) {
1548
1553
  if (!err.agentCreatePlanLimit) {
1549
1554
  throw new Error("HQ Pro returned an unrecognized agent payment-required response.");
1550
1555
  }
1551
- await surfaceAgentCreatePlanLimit(err.agentCreatePlanLimit, opts);
1556
+ await surfaceAgentCreatePlanLimit(err.agentCreatePlanLimit, {
1557
+ ...opts,
1558
+ companyUid,
1559
+ });
1552
1560
  // Keep the payment-required status through normal CLI teardown.
1553
1561
  // Unlike process.exit(), this is testable and cannot be caught by
1554
1562
  // this action's outer error boundary as a generic failure.
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { Command } from "commander";
11
11
  import { type FlagReader } from "../lib/checkpoint-ontology/plan.js";
12
- import { type DeleteVaultKey } from "../lib/checkpoint-ontology/merge-copies.js";
12
+ import { type DeleteVaultKey, type ListedPair } from "../lib/checkpoint-ontology/merge-copies.js";
13
13
  export declare const ONTOLOGY_PAYLOAD_FILE = "payload.json";
14
14
  /** The hq-flags read, scoped to the payload company. */
15
15
  export declare const registryFlagReader: FlagReader;
@@ -32,8 +32,11 @@ export interface MergeCopiesCommandOptions {
32
32
  delete?: boolean;
33
33
  variants?: boolean;
34
34
  limit?: number;
35
+ only?: string;
35
36
  json?: boolean;
36
37
  }
38
+ /** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
39
+ export declare function readListedPairs(file: string): ListedPair[];
37
40
  /**
38
41
  * `hq core ontology merge-copies`: merge `<slug> <n>.md` copies into their
39
42
  * original (`--apply`), then delete merged copies from the vault
@@ -153,8 +153,31 @@ function mergeCopiesSummary(report) {
153
153
  lines.push(` held ${plan.copy}: ${plan.held}`);
154
154
  for (const error of report.errors)
155
155
  lines.push(` error ${error.path}: ${error.error}`);
156
+ for (const entry of report.only_unmatched ?? [])
157
+ lines.push(` not found ${entry.copy} -> ${entry.original}`);
156
158
  return lines.join("\n");
157
159
  }
160
+ /** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
161
+ export function readListedPairs(file) {
162
+ let parsed;
163
+ try {
164
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
165
+ }
166
+ catch (error) {
167
+ throw new OntologyUsageError(`ontology: --only file ${file} is not readable JSON: ${error.message}`);
168
+ }
169
+ if (!Array.isArray(parsed) ||
170
+ parsed.length === 0 ||
171
+ !parsed.every((entry) => {
172
+ if (!entry || typeof entry !== "object")
173
+ return false;
174
+ const record = entry;
175
+ return typeof record.copy === "string" && typeof record.original === "string";
176
+ })) {
177
+ throw new OntologyUsageError("ontology: --only must be a non-empty JSON array of { copy, original } paths");
178
+ }
179
+ return parsed.map((entry) => ({ copy: entry.copy, original: entry.original }));
180
+ }
158
181
  /**
159
182
  * `hq core ontology merge-copies`: merge `<slug> <n>.md` copies into their
160
183
  * original (`--apply`), then delete merged copies from the vault
@@ -171,6 +194,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
171
194
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1)) {
172
195
  throw new OntologyUsageError("ontology: --limit must be a positive whole number");
173
196
  }
197
+ const only = options.only !== undefined ? readListedPairs(options.only) : undefined;
174
198
  const resolution = resolveOntologyCompany(hqRoot, slug);
175
199
  if (!resolution.ok)
176
200
  throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
@@ -195,6 +219,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
195
219
  now: deps.now?.() ?? new Date(),
196
220
  ...(options.variants ? { pairKind: "variants" } : {}),
197
221
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
222
+ ...(only ? { only } : {}),
198
223
  ...(deleteVaultKey ? { deleteVaultKey } : {}),
199
224
  });
200
225
  return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
@@ -248,6 +273,7 @@ export function registerCoreOntologyCommand(core) {
248
273
  .option("--delete", "delete fully merged copies from the vault by exact key (after sync pushed the merge)")
249
274
  .option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
250
275
  .option("--limit <n>", "handle at most this many copies", (value) => Number(value))
276
+ .option("--only <file>", "handle only the copy -> original pairs listed in this JSON file")
251
277
  .option("--json", "print JSON")
252
278
  .option("--hq-root <path>", "HQ installation to operate on")
253
279
  .action(async (options) => {
@@ -153,10 +153,27 @@ export function createLaneResumeHandlers(deps) {
153
153
  if (opts.account !== undefined && !providerSessionPortable(lane.provider)) {
154
154
  throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
155
155
  }
156
- if (lane.state === "queued") {
156
+ if (lane.state === "queued" &&
157
+ (lane.queued_reason !== "worker_profile_changed" || opts.worker === undefined)) {
157
158
  throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
158
159
  }
159
- const refusal = resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
160
+ const queuedProfileRecovery = lane.state === "queued" &&
161
+ lane.queued_reason === "worker_profile_changed" &&
162
+ opts.worker !== undefined;
163
+ if (queuedProfileRecovery && (opts.text !== undefined ||
164
+ opts.project !== undefined ||
165
+ opts.story !== undefined ||
166
+ opts.account !== undefined ||
167
+ opts.model !== undefined ||
168
+ opts.effort !== undefined ||
169
+ opts.keepAlive !== undefined ||
170
+ opts.meshCompany !== undefined ||
171
+ opts.compact === false)) {
172
+ throw new LanesError("worker_profile_recovery_options_invalid", "Queued worker-profile recovery accepts only --worker; rerun hq lanes resume after reconcile admits the lane", { lane_id: laneId });
173
+ }
174
+ const refusal = queuedProfileRecovery
175
+ ? null
176
+ : resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
160
177
  if (refusal !== null) {
161
178
  throw new LanesError(refusal.error, refusal.message, {
162
179
  lane_id: laneId,
@@ -206,6 +223,28 @@ export function createLaneResumeHandlers(deps) {
206
223
  if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
207
224
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
208
225
  }
226
+ if (lane.state === "queued" && lane.queued_reason === "worker_profile_changed") {
227
+ if (resumeProfile === undefined) {
228
+ throw new LanesError("worker_profile_required", `Lane ${laneId} needs --worker ${lane.worker_profile ?? "<profile>"} to recover its queued profile`, { lane_id: laneId, worker: lane.worker_profile ?? null });
229
+ }
230
+ const recovered = recordResumeSetup(ctx, lane, {
231
+ profile: resumeProfile,
232
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
233
+ clearWorkerProfileChangedQueue: true,
234
+ });
235
+ printJson({
236
+ ok: true,
237
+ action: "resume",
238
+ lane_id: laneId,
239
+ delivery: "profile_repin",
240
+ delivery_status: "queued_for_reconcile",
241
+ profile_repin: profileRepin !== undefined
242
+ ? recovered.worker_profile_repin_history?.at(-1) ?? null
243
+ : null,
244
+ lane: recovered,
245
+ });
246
+ return;
247
+ }
209
248
  const automaticAccountSelection = opts.account === undefined
210
249
  ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, { repairUnavailableAccount: true })
211
250
  : undefined;
@@ -113,6 +113,8 @@ export interface RecordResumeSetupInput {
113
113
  meshCompany?: LaneCompanyRef;
114
114
  threadId?: string;
115
115
  profileRepin?: ResumeProfileRepinAuthorization;
116
+ /** Clear only the recoverable worker_profile_changed queue after its profile is revalidated. */
117
+ clearWorkerProfileChangedQueue?: boolean;
116
118
  }
117
119
  export declare function accountRequestKeepsCurrentLane(lane: LaneRecord, rawAccount: string): boolean;
118
120
  export declare function assertCurrentResumeAccountAvailable(ctx: Ctx, lane: LaneRecord): void;