@indigoai-us/hq-cli 5.345.57 → 5.345.59

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 (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/command-catalog.generated.d.ts +12 -0
  3. package/dist/command-catalog.generated.js +15 -0
  4. package/dist/commands/access.d.ts +14 -0
  5. package/dist/commands/access.js +69 -1
  6. package/dist/commands/agent-kit.js +20 -10
  7. package/dist/commands/billing.d.ts +13 -0
  8. package/dist/commands/billing.js +31 -0
  9. package/dist/commands/db-dump.js +2 -2
  10. package/dist/commands/db-remote-deps.js +6 -0
  11. package/dist/commands/lanes-loop.js +3 -1
  12. package/dist/commands/lanes-questions.js +16 -0
  13. package/dist/commands/lanes-session-start.js +19 -3
  14. package/dist/commands/lanes-shared.d.ts +1 -0
  15. package/dist/commands/lanes.d.ts +3 -1
  16. package/dist/commands/lanes.js +52 -2
  17. package/dist/lib/agent-kit/inbox-poll-backoff-flag.d.ts +19 -0
  18. package/dist/lib/agent-kit/inbox-poll-backoff-flag.js +70 -0
  19. package/dist/lib/agent-kit/run/inbox.d.ts +7 -3
  20. package/dist/lib/agent-kit/run/inbox.js +45 -6
  21. package/dist/lib/agent-kit/run/stop-signals.d.ts +14 -0
  22. package/dist/lib/agent-kit/run/stop-signals.js +26 -0
  23. package/dist/lib/agent-kit/services.d.ts +2 -2
  24. package/dist/lib/agent-kit/services.js +5 -3
  25. package/dist/lib/anywhere-runtime-flag-key.d.ts +1 -0
  26. package/dist/lib/anywhere-runtime-flag-key.js +1 -0
  27. package/dist/lib/anywhere-runtime-revocation-marker.d.ts +4 -0
  28. package/dist/lib/anywhere-runtime-revocation-marker.js +26 -0
  29. package/dist/lib/daemon/run.d.ts +2 -0
  30. package/dist/lib/daemon/run.js +17 -4
  31. package/dist/lib/daemon/server.js +7 -0
  32. package/dist/lib/db/control-plane.d.ts +12 -1
  33. package/dist/lib/db/usage-format.d.ts +2 -1
  34. package/dist/lib/db/usage-format.js +19 -0
  35. package/dist/lib/install/provider-wrappers.js +13 -0
  36. package/dist/lib/lanes/loop.d.ts +23 -1
  37. package/dist/lib/lanes/loop.js +134 -19
  38. package/dist/lib/lanes/questions.d.ts +5 -0
  39. package/dist/lib/lanes/reconcile.js +3 -0
  40. package/dist/lib/lanes/story-ledger.js +30 -20
  41. package/dist/lib/lanes/types.js +1 -1
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.59] — 2026-10-09
6
+ - Keep conduct-mode SessionStart injection out of unattended sessions unless `HQ_AUTO_CONDUCT` explicitly forces it on.
7
+ - Back off the external agent-kit inbox poller from 15 seconds to 30 seconds after repeated empty polls, reset to 15 seconds when a message is mirrored, and stop promptly on SIGTERM or SIGINT with a 10-second deadline for stalled shutdowns. The default-on `agents.inbox-poll-backoff-agent-kit` hq-flags kill switch preserves the regular cadence when disabled; installed services retain a configured `HQ_FLAGS_API_URL`.
8
+
9
+ - Fix loop-lane recovery: retries receive the fixed per-attempt phase budget; withdrawn and stalled IDs can be reused; `interrupt --story/--phase` targets matching work; and stall answers resume through the controller.
10
+ - `hq db usage --app` says when a deleted app's database was removed, how long its data is kept, and that it is not billed, instead of showing usage and an estimate.
11
+ - `hq db sql --app` and `hq db dump --app` say a deleted app's database was removed, or that the app has no database, instead of "not available yet".
12
+ - `hq billing status` lists database apps: how many are active, the $10 monthly price, overage per app this month, whether the company is exempt, and which subscription carries the charge.
13
+ - Installed Claude and Codex provider wrappers now bypass HQ when the daemon's refreshed runtime flag or person setting is revoked, using a per-user marker under the HQ registry directory.
14
+ - Lane updates preserve completed stories and lead-set states while applying lane progress in order.
15
+ - `hq lanes resume --story` can clear a prior unacknowledged story-switch note.
16
+
17
+ ## [5.345.58] — 2026-10-09
18
+ - hq access now accepts `--as <member>` to view another member's access, read-only.
19
+
5
20
  ## [5.345.57] — 2026-10-09
6
21
  - Add `hq lanes create --loop` and `hq lanes enqueue` for persistent worker lanes that process FIFO phase envelopes, return generic results or section 8 handoffs, reuse sessions per story, retry one stall, and park a second stall for the senior. Loop behavior uses the default-on `lanes.conduct-loop` hq-flags kill switch. See `docs/lanes.md` for command output and exit codes.
7
22
 
@@ -2871,6 +2871,10 @@ export declare const COMMAND_CATALOG: readonly [{
2871
2871
  readonly flags: "--company <slug>";
2872
2872
  readonly description: "Company slug";
2873
2873
  readonly hidden: false;
2874
+ }, {
2875
+ readonly flags: "--as <person-uid>";
2876
+ readonly description: "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)";
2877
+ readonly hidden: false;
2874
2878
  }, {
2875
2879
  readonly flags: "--json";
2876
2880
  readonly description: "Print a single JSON object";
@@ -12931,6 +12935,14 @@ export declare const COMMAND_CATALOG: readonly [{
12931
12935
  readonly flags: "--reason <text>";
12932
12936
  readonly description: "Record the reason in the lane's stop history";
12933
12937
  readonly hidden: false;
12938
+ }, {
12939
+ readonly flags: "--story <id>";
12940
+ readonly description: "Withdraw only this story's queued phases (loop lanes only)";
12941
+ readonly hidden: false;
12942
+ }, {
12943
+ readonly flags: "--phase <name>";
12944
+ readonly description: "Limit --story interruption to one phase";
12945
+ readonly hidden: false;
12934
12946
  }, {
12935
12947
  readonly flags: "--json";
12936
12948
  readonly description: "JSON on stdout (always)";
@@ -3462,6 +3462,11 @@ export const COMMAND_CATALOG = [
3462
3462
  "description": "Company slug",
3463
3463
  "hidden": false
3464
3464
  },
3465
+ {
3466
+ "flags": "--as <person-uid>",
3467
+ "description": "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)",
3468
+ "hidden": false
3469
+ },
3465
3470
  {
3466
3471
  "flags": "--json",
3467
3472
  "description": "Print a single JSON object",
@@ -15736,6 +15741,16 @@ export const COMMAND_CATALOG = [
15736
15741
  "description": "Record the reason in the lane's stop history",
15737
15742
  "hidden": false
15738
15743
  },
15744
+ {
15745
+ "flags": "--story <id>",
15746
+ "description": "Withdraw only this story's queued phases (loop lanes only)",
15747
+ "hidden": false
15748
+ },
15749
+ {
15750
+ "flags": "--phase <name>",
15751
+ "description": "Limit --story interruption to one phase",
15752
+ "hidden": false
15753
+ },
15739
15754
  {
15740
15755
  "flags": "--json",
15741
15756
  "description": "JSON on stdout (always)",
@@ -135,6 +135,15 @@ export interface RunAccessResult {
135
135
  alreadyAskedAt?: string;
136
136
  requestNote?: string;
137
137
  }
138
+ export interface AccessAsResult {
139
+ personUid: string;
140
+ company: string;
141
+ path: string;
142
+ permission: "read" | "write" | "admin" | null;
143
+ canRead: boolean;
144
+ roleBypass: boolean;
145
+ exitCode: 0 | 3;
146
+ }
138
147
  export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
139
148
  export declare function formatAccessOutcome(result: RunAccessResult): string;
140
149
  export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
@@ -149,6 +158,11 @@ export declare function resolveAccessCompany(target: string, opts?: {
149
158
  company?: string;
150
159
  hqRoot?: string;
151
160
  }): string | undefined;
161
+ export declare function runAccessForPerson(rawPath: string, personUid: string, opts?: {
162
+ company?: string;
163
+ hqRoot?: string;
164
+ }): Promise<AccessAsResult>;
165
+ export declare function formatAccessForPerson(result: AccessAsResult): string;
152
166
  export declare function runAccessForPath(path: string, opts?: {
153
167
  company?: string;
154
168
  hqRoot?: string;
@@ -20,7 +20,7 @@ import * as readline from "node:readline";
20
20
  import { ListObjectsV2Command, } from "@aws-sdk/client-s3";
21
21
  import { DEFAULT_HQ_ROOT } from "../utils/cognito-session.js";
22
22
  import { getCompanyUid, vaultApiFetch, resolveCallerUid } from "../utils/vault-api.js";
23
- import { parseCompanySlugFromPath, resolveBrowseSession, makeCompanyPresignFactory, runGet, } from "./files-browse.js";
23
+ import { parseCompanySlugFromPath, classifyAclSource, resolveBrowseSession, makeCompanyPresignFactory, runGet, toBucketRelative, } from "./files-browse.js";
24
24
  import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
25
25
  import { listActiveMembers } from "./members.js";
26
26
  import { buildDmBody } from "./dm.js";
@@ -87,6 +87,10 @@ function normalizeTarget(raw, slug) {
87
87
  t = t.slice(anchor.length);
88
88
  return t;
89
89
  }
90
+ function fileGrantCoversPath(grantPath, targetPath) {
91
+ return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
92
+ }
93
+ const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
90
94
  function httpStatusOf(err) {
91
95
  if (!err || typeof err !== "object")
92
96
  return undefined;
@@ -637,6 +641,7 @@ export function registerAccessCommand(program) {
637
641
  .command("access <path-or-query>")
638
642
  .description("Tell whether a vault file exists and whether you can read it. Never a bare not-found. Exit codes: 0 local/not-synced, 2 never-existed, 3 no-access, 4 ambiguous.")
639
643
  .option("--company <slug>", "Company slug")
644
+ .option("--as <person-uid>", "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)")
640
645
  .option("--json", "Print a single JSON object")
641
646
  .option("--no-fix", "Diagnose only: do not fetch, pin, or repair sync")
642
647
  .option("--yes", "Answer the one send-confirmation yes (hosts must pass this explicitly)")
@@ -650,6 +655,12 @@ Exit codes:
650
655
  `)
651
656
  .action(async (target, opts) => {
652
657
  try {
658
+ if (opts.as) {
659
+ const result = await runAccessForPerson(target, opts.as, opts);
660
+ console.log(opts.json ? JSON.stringify(result) : formatAccessForPerson(result));
661
+ process.exitCode = result.exitCode;
662
+ return;
663
+ }
653
664
  const result = await runAccessForPath(target, opts);
654
665
  process.exitCode = result.exitCode;
655
666
  }
@@ -669,6 +680,63 @@ export function resolveAccessCompany(target, opts) {
669
680
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
670
681
  return opts?.company ?? slugFromTarget(target) ?? readActiveCompany(hqRoot);
671
682
  }
683
+ export async function runAccessForPerson(rawPath, personUid, opts) {
684
+ const uid = personUid.trim();
685
+ if (!/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(uid)) {
686
+ throw new Error("Pass the person's exact prs_ or agt_ UID to --as.");
687
+ }
688
+ const pathSlug = slugFromTarget(rawPath);
689
+ if (opts?.company && pathSlug && opts.company !== pathSlug) {
690
+ throw new Error("The --company value does not match the company in the path.");
691
+ }
692
+ const company = resolveAccessCompany(rawPath, opts);
693
+ if (!company)
694
+ throw new Error("Pass --company <slug> or an anchored company path.");
695
+ const relativePath = normalizeTarget(rawPath, company);
696
+ if (!relativePath || relativePath.split(/[\\/]/).includes("..")) {
697
+ throw new Error("Pass a company-relative path without parent-directory segments.");
698
+ }
699
+ const { token } = await resolveBrowseSession();
700
+ const companyUid = await getCompanyUid(token, company);
701
+ const response = await vaultApiFetch({
702
+ token,
703
+ path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(uid)}/access`,
704
+ method: "GET",
705
+ });
706
+ if (!response.ok) {
707
+ if (response.status === 403) {
708
+ throw new Error("Only the person themself or a company owner/admin can check this access.");
709
+ }
710
+ if (response.status === 404) {
711
+ throw new Error("That person is not an active member of this company.");
712
+ }
713
+ throw new Error(`Member access check failed (${response.status}).`);
714
+ }
715
+ const projection = (await response.json());
716
+ if (projection.personUid !== uid || !projection.files) {
717
+ throw new Error("The member access response did not match the requested person.");
718
+ }
719
+ const roleBypass = projection.files.roleBypass === true;
720
+ const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
721
+ const matchingPermissions = grants
722
+ .filter((grant) => typeof grant.path === "string" && typeof grant.permission === "string" &&
723
+ grant.permission in FILE_PERMISSION_RANK &&
724
+ fileGrantCoversPath(toBucketRelative(grant.path, company), relativePath))
725
+ .map((grant) => grant.permission);
726
+ const permission = roleBypass ? "admin" : matchingPermissions.reduce((best, candidate) => best === null || FILE_PERMISSION_RANK[candidate] > FILE_PERMISSION_RANK[best] ? candidate : best, null);
727
+ return {
728
+ personUid: uid,
729
+ company,
730
+ path: `companies/${company}/${relativePath}`,
731
+ permission,
732
+ canRead: permission !== null,
733
+ roleBypass,
734
+ exitCode: permission === null ? 3 : 0,
735
+ };
736
+ }
737
+ export function formatAccessForPerson(result) {
738
+ return `${result.personUid} has ${result.permission ?? "no access"} access to ${result.path}.`;
739
+ }
672
740
  export async function runAccessForPath(path, opts) {
673
741
  const target = path;
674
742
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
@@ -30,6 +30,7 @@ import { isKitService, KIT_SERVICES, enabledKitServices, kitServiceSpec, kitServ
30
30
  import { writeKitSkills } from "../lib/agent-kit/skills.js";
31
31
  import { runHeartbeatLoop } from "../lib/agent-kit/run/heartbeat.js";
32
32
  import { runInboxLoop } from "../lib/agent-kit/run/inbox.js";
33
+ import { createKitStopController } from "../lib/agent-kit/run/stop-signals.js";
33
34
  import { superviseKitServices } from "../lib/agent-kit/run/supervisor.js";
34
35
  import { fallbackStatus, rebootCrontabLine, ensureRebootCrontab, shouldUseFallback, startFallback, stopFallback, } from "../lib/agent-kit/fallback.js";
35
36
  import { serviceLogPath } from "../lib/agent-kit/paths.js";
@@ -182,17 +183,26 @@ export async function runKitService(service, rt) {
182
183
  credsSource: rt.credsSource ?? "default",
183
184
  });
184
185
  return;
185
- case "inbox":
186
- await runInboxLoop({
187
- paths: rt.paths,
188
- agentUid: rt.creds.entityUid,
189
- apiBaseUrl: rt.creds.apiBaseUrl,
190
- pollMs: rt.config.inboxPollMs,
191
- ack: rt.config.inboxAck,
192
- getToken: rt.getToken,
193
- log: rt.log,
194
- });
186
+ case "inbox": {
187
+ const stop = createKitStopController();
188
+ try {
189
+ await runInboxLoop({
190
+ paths: rt.paths,
191
+ agentUid: rt.creds.entityUid,
192
+ apiBaseUrl: rt.creds.apiBaseUrl,
193
+ pollMs: rt.config.inboxPollMs,
194
+ ack: rt.config.inboxAck,
195
+ getToken: rt.getToken,
196
+ log: rt.log,
197
+ companySlug: rt.creds.companySlug,
198
+ signal: stop.signal,
199
+ });
200
+ }
201
+ finally {
202
+ stop.dispose();
203
+ }
195
204
  return;
205
+ }
196
206
  }
197
207
  }
198
208
  /** `kit run all`: supervise the kit services as child processes with restart backoff. */
@@ -16,5 +16,18 @@
16
16
  * answer such a request
17
17
  */
18
18
  import { Command } from "commander";
19
+ interface DatabaseBilling {
20
+ period: string;
21
+ activeApps: number;
22
+ pricePerAppCents: number;
23
+ exempt: boolean;
24
+ chargedOn: "team" | "dedicated" | "existing" | null;
25
+ apps: Array<{
26
+ appId: string;
27
+ overageCents: number;
28
+ }>;
29
+ }
30
+ export declare function formatDatabaseBilling(db: DatabaseBilling | null | undefined): string[];
19
31
  export declare function registerBillingCommand(program: Command): void;
32
+ export {};
20
33
  //# sourceMappingURL=billing.d.ts.map
@@ -21,6 +21,35 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
21
21
  import { mintPaymentLink } from "../utils/billing-gate.js";
22
22
  import { upgradeToTeam } from "../utils/team-upgrade.js";
23
23
  import { registerBillingSponsorCommands } from "./billing-sponsor.js";
24
+ const CHARGED_ON = {
25
+ team: "the Team subscription",
26
+ dedicated: "the separate HQ database apps subscription",
27
+ existing: "the company's existing subscription",
28
+ };
29
+ function dollars(cents) {
30
+ return `$${(cents / 100).toFixed(2)}`;
31
+ }
32
+ export function formatDatabaseBilling(db) {
33
+ if (db === undefined)
34
+ return [];
35
+ if (db === null)
36
+ return [`${chalk.bold("Database apps")}: could not be read right now`];
37
+ const lines = [
38
+ `${chalk.bold("Database apps")}: ${db.activeApps} active, ${dollars(db.pricePerAppCents)} a month each`,
39
+ ];
40
+ if (db.exempt) {
41
+ lines.push(" Not billed: this company is exempt from database app charges.");
42
+ }
43
+ else if (db.activeApps > 0) {
44
+ lines.push(db.chargedOn
45
+ ? ` Charged on ${CHARGED_ON[db.chargedOn]}.`
46
+ : " Not on a subscription yet. The charge is added on the next billing sync.");
47
+ }
48
+ for (const app of db.apps) {
49
+ lines.push(` ${app.appId}: ${dollars(app.overageCents)} overage so far in ${db.period}`);
50
+ }
51
+ return lines;
52
+ }
24
53
  function fail(err) {
25
54
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
26
55
  process.exit(1);
@@ -69,6 +98,8 @@ export function registerBillingCommand(program) {
69
98
  console.log(`${chalk.bold("Card")}: ${chalk.yellow("no card on file")} — ` +
70
99
  `run \`hq billing checkout\` to add one before provisioning paid resources.`);
71
100
  }
101
+ for (const line of formatDatabaseBilling(summary.databases))
102
+ console.log(line);
72
103
  }
73
104
  catch (err) {
74
105
  fail(err);
@@ -8,8 +8,8 @@ import { dumpRemoteSchema, openRemoteAppConnection } from "../lib/db/remote-engi
8
8
  import { isPlanGateError } from "../utils/plan-gate-error.js";
9
9
  import { defaultRemoteDeps, describeRemoteError, remoteEngineDeps, resolveAppId, } from "./db-remote-deps.js";
10
10
  export function describeSessionGap(error) {
11
- const status = error.status;
12
- if (status === 404 || status === 405) {
11
+ const { status, code } = error;
12
+ if ((status === 404 || status === 405) && !code) {
13
13
  return "Remote database access for apps is not available yet. The HQ API has no database session route.";
14
14
  }
15
15
  return null;
@@ -71,6 +71,12 @@ export function describeRemoteError(error) {
71
71
  if (status === 404 && code === "DB_USAGE_NOT_PROVISIONED") {
72
72
  return "This company has no remote database yet. Run hq db provision first.";
73
73
  }
74
+ if (status === 404 && code === "DB_APP_REMOVED") {
75
+ return "This app's database was removed, so it can't be opened. Run hq db usage --app <app> to see how long its data is kept.";
76
+ }
77
+ if (status === 404 && code === "DB_APP_NOT_FOUND") {
78
+ return "This app has no database in this company. Check the app name, or deploy it with database enabled.";
79
+ }
74
80
  if (status === 404 && code === "DB_USAGE_APP_NOT_FOUND") {
75
81
  return "No database found for that app in this company. Check the app name, or deploy it with database enabled.";
76
82
  }
@@ -176,12 +176,13 @@ export async function runLaneLoopController(ctx, laneId, deps) {
176
176
  signal.removeEventListener("abort", abortPhase);
177
177
  }
178
178
  },
179
- raiseQuestion: ({ envelope, envelopeId, attempt, error }) => {
179
+ raiseQuestion: ({ envelope, envelopeId, attempt, error, questionId }) => {
180
180
  const current = readLane(hqRoot, laneId);
181
181
  const data = envelope;
182
182
  raiseLaneQuestion({
183
183
  hqRoot,
184
184
  lane: current,
185
+ questionId,
185
186
  raisedVia: "cli",
186
187
  at: ctx.now().toISOString(),
187
188
  body: {
@@ -189,6 +190,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
189
190
  context: `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
190
191
  options: ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
191
192
  recommended: "Retry with a longer deadline",
193
+ loop_stall: { lane_id: laneId, envelope_id: envelopeId, attempt },
192
194
  },
193
195
  });
194
196
  },
@@ -1,9 +1,11 @@
1
+ import * as path from "node:path";
1
2
  import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
2
3
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
3
4
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
4
5
  import { appendCappedHistory, createQuestion, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
5
6
  import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
6
7
  import { LanesError } from "../lib/lanes/errors.js";
8
+ import { applyLoopStallAnswer } from "../lib/lanes/loop.js";
7
9
  export function resolveAnswerChoice(choice, options, freeform) {
8
10
  const trimmed = choice.trim();
9
11
  if (/^[0-9]+$/.test(trimmed)) {
@@ -854,6 +856,20 @@ export function createLaneQuestionsHandlers(deps) {
854
856
  },
855
857
  };
856
858
  });
859
+ if (body.loop_stall?.lane_id === askingLane.lane_id && askingLane.loop_mode === true) {
860
+ const resolved = applyLoopStallAnswer({
861
+ runDir: path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id),
862
+ questionId,
863
+ choice,
864
+ ...(opts.notes ? { notes: opts.notes } : {}),
865
+ at,
866
+ });
867
+ if (!resolved)
868
+ throw new LanesError("loop_stall_result_missing", `Loop stall result for question ${questionId} is missing`, { question_id: questionId, lane_id: askingLane.lane_id });
869
+ const controllerPid = await deps.restartLoopController(ctx, askingLane.lane_id);
870
+ printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action, controller_pid: controllerPid });
871
+ return;
872
+ }
857
873
  const latestAskingLane = readLane(hqRoot, askingLane.lane_id);
858
874
  if (latestAskingLane.state === "awaiting_input") {
859
875
  let wake;
@@ -16,6 +16,18 @@ function enabledFromSettings(root) {
16
16
  const value = /^\s+default_enabled:\s*["']?(true|false|yes|no|on|off|1|0)["']?\s*(?:#.*)?$/mi.exec(block)?.[1]?.toLowerCase();
17
17
  return value === "true" || value === "yes" || value === "on" || value === "1";
18
18
  }
19
+ function isUnattended(env) {
20
+ const flags = `${env.HQ_UNATTENDED ?? ""}${env.HQ_SESSION_UNATTENDED ?? ""}${env.CLAUDE_HEADLESS ?? ""}`;
21
+ if (["1", "true", "TRUE", "yes", "YES", "on", "ON"].some((marker) => flags.includes(marker)))
22
+ return true;
23
+ return [
24
+ env.HQ_BOT_AGENT_UID,
25
+ env.HQ_MACHINE_CREDS_FILE,
26
+ env.HQ_AGENT_CLAUDE_TASKFILE,
27
+ env.HQ_AGENT_COMPANY_DIR,
28
+ env.HQ_AGENT_STATUS_FILE,
29
+ ].some((marker) => Boolean(marker));
30
+ }
19
31
  function sessionWasDisabled(root, env, payloadSessionId) {
20
32
  const sessionId = payloadSessionId || env.HQ_SESSION_ID;
21
33
  if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId))
@@ -169,11 +181,15 @@ export async function runLaneSessionStart(options = {}) {
169
181
  (options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn)))(root, env);
170
182
  if (sessionWasDisabled(root, env, payloadSessionId))
171
183
  return;
184
+ const autoConduct = env.HQ_AUTO_CONDUCT ?? "";
185
+ const forceOn = /^(1|true|on|yes)$/i.test(autoConduct);
186
+ if (/^(0|false|off|no)$/i.test(autoConduct))
187
+ return;
188
+ if (!forceOn && isUnattended(env))
189
+ return;
172
190
  let enabled = enabledFromSettings(root);
173
- if (/^(1|true|on|yes)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
191
+ if (forceOn)
174
192
  enabled = true;
175
- else if (/^(0|false|off|no)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
176
- enabled = false;
177
193
  if (!enabled)
178
194
  return;
179
195
  const core = path.join(root, ".claude", "skills", "conduct", "conductor-core.md");
@@ -474,6 +474,7 @@ export interface LaneQuestionsHandlerDeps {
474
474
  printJson(value: unknown): void;
475
475
  readSessionMeta(hqRoot: string, sessionId: string, ctx: Ctx): Record<string, unknown> | null;
476
476
  reconcileHostFromCtx(ctx: Ctx): ReconcileHost;
477
+ restartLoopController(ctx: Ctx, laneId: string): Promise<number>;
477
478
  resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
478
479
  resolveAudience(hqRoot: string, question: QuestionRecord, ctx: Ctx): SeniorRef;
479
480
  sameQuestionAnswer(left: QuestionAnswer | undefined, right: QuestionAnswer): boolean;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Command } from "commander";
9
9
  import type { MonitorLaneMessagePayload, MonitorLaneMessageResult } from "./lanes-message.js";
10
- import { type LanesMesh, type OutpostRouteIdentity, type SpawnLoginResult } from "./lanes-shared.js";
10
+ import { type Ctx, type LanesMesh, type OutpostRouteIdentity, type SpawnLoginResult } from "./lanes-shared.js";
11
11
  export type { LanesMesh, SpawnLoginResult } from "./lanes-shared.js";
12
12
  export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
13
13
  export { resolveAnswerChoice } from "./lanes-questions.js";
@@ -36,6 +36,8 @@ export interface LanesCommandDeps {
36
36
  onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
37
37
  /** Test seam for failures after a message is durably queued. */
38
38
  onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move" | "quota_hold") => void;
39
+ /** Test seam for routing loop stall answers back through their controller. */
40
+ restartLoopController?: (ctx: Ctx, laneId: string) => Promise<number>;
39
41
  hqRoot?: () => string;
40
42
  /** Start HQ-root walk here instead of process.cwd(). Tests only. */
41
43
  cwd?: () => string;
@@ -43,7 +43,7 @@ import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFoc
43
43
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
44
44
  import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, } from "../lib/lanes/flag-keys.js";
45
45
  import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
46
- import { enqueueLoopEnvelope, interruptLoopQueue, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
46
+ import { enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
47
47
  import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
48
48
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
49
49
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
@@ -580,6 +580,13 @@ export function parseQuestionBody(raw) {
580
580
  return null;
581
581
  if (obj.notes !== undefined && typeof obj.notes !== "string")
582
582
  return null;
583
+ if (obj.loop_stall !== undefined && (typeof obj.loop_stall !== "object" || obj.loop_stall === null || Array.isArray(obj.loop_stall) ||
584
+ typeof obj.loop_stall.lane_id !== "string" ||
585
+ typeof obj.loop_stall.envelope_id !== "string" ||
586
+ typeof obj.loop_stall.attempt !== "number" ||
587
+ !Number.isSafeInteger(obj.loop_stall.attempt) ||
588
+ obj.loop_stall.attempt < 1))
589
+ return null;
583
590
  const options = obj.options.filter((item) => typeof item === "string");
584
591
  const body = {
585
592
  question: obj.question,
@@ -590,6 +597,8 @@ export function parseQuestionBody(raw) {
590
597
  body.recommended = obj.recommended;
591
598
  if (typeof obj.notes === "string")
592
599
  body.notes = obj.notes;
600
+ if (obj.loop_stall !== undefined)
601
+ body.loop_stall = obj.loop_stall;
593
602
  return body;
594
603
  }
595
604
  function readQuestionBodyFile(hqRoot, question) {
@@ -8614,6 +8623,29 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8614
8623
  throw new LanesError("interrupt_remote_unsupported", `Lane ${laneId} runs on Outpost ${lane.host.id}; run hq lanes interrupt on that Outpost`, { lane_id: laneId, outpost_id: lane.host.id });
8615
8624
  }
8616
8625
  assertLaneRunsHereForOperatorMutation(ctx, lane, "interrupt");
8626
+ if (options.story !== undefined && lane.loop_mode !== true) {
8627
+ throw new LanesError("usage_error", "--story is only supported for loop lanes", { lane_id: laneId });
8628
+ }
8629
+ if (options.phase !== undefined && options.story === undefined) {
8630
+ throw new LanesError("usage_error", "--phase requires --story", { lane_id: laneId });
8631
+ }
8632
+ if (lane.loop_mode === true && options.story !== undefined) {
8633
+ const scoped = interruptLoopQueueForStory(runDir(hqRoot, lane.lane_id), options.story, options.phase);
8634
+ if (scoped.already_picked_up.length === 0 || lane.state !== "running") {
8635
+ if (!options.quiet)
8636
+ printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...scoped });
8637
+ return;
8638
+ }
8639
+ options = { ...options, preserveLoopQueue: true };
8640
+ // The matching active phase uses the same provider interruption path as a deadline abort.
8641
+ if (!reason)
8642
+ reason = "story-scoped loop phase interrupt";
8643
+ const remaining = scoped;
8644
+ await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true });
8645
+ if (!options.quiet)
8646
+ printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...remaining });
8647
+ return;
8648
+ }
8617
8649
  const queueInterrupt = lane.loop_mode === true && options.preserveLoopQueue !== true
8618
8650
  ? interruptLoopQueue(runDir(hqRoot, lane.lane_id))
8619
8651
  : undefined;
@@ -9390,6 +9422,22 @@ export function registerLanesCommand(parent, deps = {}) {
9390
9422
  printJson,
9391
9423
  readSessionMeta,
9392
9424
  reconcileHostFromCtx,
9425
+ restartLoopController: async (handlerCtx, laneId) => {
9426
+ if (deps.restartLoopController)
9427
+ return deps.restartLoopController(handlerCtx, laneId);
9428
+ const current = readLane(handlerCtx.hqRoot(), laneId);
9429
+ if (current.state === "running") {
9430
+ await runInterrupt(handlerCtx, laneId, "loop stall question answered; restarting controller", { preserveLoopQueue: true, quiet: true });
9431
+ }
9432
+ const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
9433
+ if (loopControllerIsAlive(runDirPath)) {
9434
+ const pid = loopStatus(runDirPath, laneId).pid;
9435
+ if (pid === undefined)
9436
+ throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} is alive without a recorded pid`, { lane_id: laneId });
9437
+ return pid;
9438
+ }
9439
+ return startLaneLoopController(handlerCtx, laneId);
9440
+ },
9393
9441
  resolveActiveQuestionCompany,
9394
9442
  resolveAudience,
9395
9443
  sameQuestionAnswer,
@@ -10034,8 +10082,10 @@ export function registerLanesCommand(parent, deps = {}) {
10034
10082
  .description("Stop a running lane's turn and keep it resumable with hq lanes resume")
10035
10083
  .argument("<lane-id>", "Lane id")
10036
10084
  .option("--reason <text>", "Record the reason in the lane's stop history")
10085
+ .option("--story <id>", "Withdraw only this story's queued phases (loop lanes only)")
10086
+ .option("--phase <name>", "Limit --story interruption to one phase")
10037
10087
  .action(wrap(async (laneId, options) => {
10038
- await runInterrupt(ctx, laneId, options.reason);
10088
+ await runInterrupt(ctx, laneId, options.reason, options);
10039
10089
  }));
10040
10090
  lanes
10041
10091
  .command("approve")
@@ -0,0 +1,19 @@
1
+ import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-client";
2
+ export declare const AGENT_KIT_INBOX_BACKOFF_FLAG = "agents.inbox-poll-backoff-agent-kit";
3
+ export declare const AGENT_KIT_INBOX_BACKOFF_DEFAULT = true;
4
+ export declare const AGENT_KIT_INBOX_BACKOFF_CACHE_MS: number;
5
+ export declare const AGENT_KIT_INBOX_BACKOFF_TIMEOUT_MS = 2000;
6
+ type InboxBackoffFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
7
+ export interface AgentKitInboxBackoffFlagDependencies {
8
+ companySlug?: string;
9
+ getToken: FlagClientOptions["getToken"];
10
+ createClient?: (options: FlagClientOptions) => InboxBackoffFlagClient;
11
+ endpoint?: string;
12
+ fetch?: typeof fetch;
13
+ now?: () => number;
14
+ reportFailure?: (error: unknown) => void;
15
+ }
16
+ /** Read the default-on kill switch; registry outages preserve idle backoff. */
17
+ export declare function createAgentKitInboxBackoffFlagReader(dependencies: AgentKitInboxBackoffFlagDependencies): () => Promise<boolean>;
18
+ export {};
19
+ //# sourceMappingURL=inbox-poll-backoff-flag.d.ts.map