@indigoai-us/hq-cli 5.311.4 → 5.312.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 (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/command-catalog.generated.d.ts +95 -0
  3. package/dist/command-catalog.generated.js +120 -0
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/daemon.js +22 -0
  7. package/dist/commands/dm.js +22 -6
  8. package/dist/commands/files-browse.js +16 -4
  9. package/dist/commands/lanes-message.js +17 -2
  10. package/dist/commands/lanes-shared.d.ts +1 -0
  11. package/dist/commands/lanes.js +96 -20
  12. package/dist/commands/vault-retention-flag.d.ts +3 -0
  13. package/dist/commands/vault-retention-flag.js +9 -0
  14. package/dist/commands/vault-retention.d.ts +11 -0
  15. package/dist/commands/vault-retention.js +69 -0
  16. package/dist/lib/core-utils/sentry-report.js +21 -4
  17. package/dist/lib/daemon/catalog.d.ts +2 -0
  18. package/dist/lib/daemon/catalog.js +13 -1
  19. package/dist/lib/daemon/config.d.ts +2 -0
  20. package/dist/lib/daemon/config.js +4 -0
  21. package/dist/lib/daemon/run.d.ts +2 -0
  22. package/dist/lib/daemon/run.js +5 -0
  23. package/dist/lib/lanes/autopilot.d.ts +38 -1
  24. package/dist/lib/lanes/autopilot.js +353 -5
  25. package/dist/lib/lanes/flag-keys.d.ts +9 -1
  26. package/dist/lib/lanes/flag-keys.js +9 -1
  27. package/dist/lib/lanes/keep-alive.d.ts +2 -0
  28. package/dist/lib/lanes/keep-alive.js +2 -0
  29. package/dist/lib/lanes/reconcile.d.ts +5 -0
  30. package/dist/lib/lanes/reconcile.js +57 -4
  31. package/dist/lib/lanes/watcher-main.js +20 -1
  32. package/dist/lib/lanes/watcher.d.ts +9 -0
  33. package/dist/lib/lanes/watcher.js +47 -1
  34. package/package.json +1 -1
@@ -33,6 +33,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
33
33
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
34
34
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
35
35
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
36
+ import { LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
36
37
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
37
38
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
38
39
  import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
@@ -1197,14 +1198,20 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1197
1198
  throw new LanesError("outpost_lane_list_unavailable", `Could not read lanes from Outpost ${outpostId}: ${message}`, { outpost_id: outpostId });
1198
1199
  }
1199
1200
  if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
1200
- throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, { outpost_id: outpostId, status: result.status, done: result.done ?? false, exit_code: result.exitCode });
1201
+ throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, {
1202
+ outpost_id: outpostId,
1203
+ command_id: result.commandId,
1204
+ status: result.status,
1205
+ done: result.done ?? false,
1206
+ exit_code: result.exitCode,
1207
+ });
1201
1208
  }
1202
1209
  let body;
1203
1210
  try {
1204
1211
  body = JSON.parse(result.stdout);
1205
1212
  }
1206
1213
  catch (err) {
1207
- throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
1214
+ throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId, command_id: result.commandId });
1208
1215
  }
1209
1216
  if (body === null || typeof body !== "object" || Array.isArray(body) ||
1210
1217
  body.ok !== true ||
@@ -1212,6 +1219,7 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1212
1219
  !Array.isArray(body.lanes)) {
1213
1220
  throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned a malformed lane list`, {
1214
1221
  outpost_id: outpostId,
1222
+ command_id: result.commandId,
1215
1223
  });
1216
1224
  }
1217
1225
  const listed = body.lanes;
@@ -1499,7 +1507,12 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1499
1507
  }
1500
1508
  const separator = laneId.lastIndexOf("_");
1501
1509
  const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
1502
- if (!hostSuffix || sanitizeHostComponent(hostSuffix) !== hostSuffix) {
1510
+ const ownerCompany = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
1511
+ ? await options.allowHostlessOwnerLookup()
1512
+ : undefined;
1513
+ const hostlessUlidLookup = ownerCompany !== undefined;
1514
+ if ((!hostSuffix && !hostlessUlidLookup) ||
1515
+ (hostSuffix && sanitizeHostComponent(hostSuffix) !== hostSuffix)) {
1503
1516
  throw commandLaneNotFound(laneId, options.outpostHint === true);
1504
1517
  }
1505
1518
  let rawOutpostRoutes;
@@ -1542,7 +1555,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1542
1555
  if (requestedOutpostId !== undefined && !outpostIds.includes(requestedOutpostId)) {
1543
1556
  throw new LanesError("outpost_not_registered", `Outpost ${requestedOutpostId} is not registered`, { lane_id: laneId, outpost_id: requestedOutpostId });
1544
1557
  }
1545
- const hostRoutes = outpostRoutes.filter((route) => outpostRouteMatchesHost(route, hostSuffix));
1558
+ const hostRoutes = hostlessUlidLookup
1559
+ ? []
1560
+ : outpostRoutes.filter((route) => outpostRouteMatchesHost(route, hostSuffix));
1546
1561
  const identity = ctx.readOutpostIdentity();
1547
1562
  const localHostRoutes = outpostRoutes.filter((route) => outpostRouteMatchesHost(route, ctx.hostname()));
1548
1563
  if (localHostRoutes.length > 1 || (identity.kind === "id" && localHostRoutes.some((route) => route.outpostId !== identity.id))) {
@@ -1553,20 +1568,22 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1553
1568
  const hostOutpostIdSet = new Set(hostOutpostIds);
1554
1569
  const matches = [];
1555
1570
  const failedOutpostIds = [];
1571
+ const failedOutpostCommands = [];
1556
1572
  const probedOutpostIds = [];
1557
1573
  const lookupOutpostIds = requestedOutpostId !== undefined
1558
1574
  ? [requestedOutpostId]
1559
- : [
1560
- ...hostOutpostIds.filter((id) => id !== localOutpostId),
1561
- ...outpostIds.filter((id) => id !== localOutpostId && !hostOutpostIdSet.has(id)),
1562
- ];
1575
+ : hostlessUlidLookup
1576
+ ? outpostIds.filter((id) => id !== localOutpostId)
1577
+ : [
1578
+ ...hostOutpostIds.filter((id) => id !== localOutpostId),
1579
+ ...outpostIds.filter((id) => id !== localOutpostId && !hostOutpostIdSet.has(id)),
1580
+ ];
1563
1581
  for (const outpostId of lookupOutpostIds) {
1564
1582
  let ownerLanes;
1565
1583
  try {
1566
- ownerLanes = await readOutpostLaneRecords(ctx, outpostId, {
1567
- hierarchy: [laneId],
1568
- snapshot: true,
1569
- });
1584
+ ownerLanes = await readOutpostLaneRecords(ctx, outpostId, hostlessUlidLookup
1585
+ ? { snapshot: true }
1586
+ : { hierarchy: [laneId], snapshot: true });
1570
1587
  probedOutpostIds.push(outpostId);
1571
1588
  }
1572
1589
  catch (err) {
@@ -1575,16 +1592,38 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1575
1592
  if (isLanesError(err) && err.error === "outpost_wait_invalid")
1576
1593
  throw err;
1577
1594
  failedOutpostIds.push(outpostId);
1595
+ failedOutpostCommands.push({
1596
+ outpost_id: outpostId,
1597
+ ...(isLanesError(err) && typeof err.details.command_id === "string"
1598
+ ? { command_id: err.details.command_id }
1599
+ : {}),
1600
+ });
1578
1601
  continue;
1579
1602
  }
1580
1603
  for (const lane of ownerLanes) {
1581
- if (lane.lane_id === laneId &&
1582
- lane.host.kind === "local" &&
1583
- sanitizeHostComponent(lane.host.id) === hostSuffix) {
1604
+ const recordedSuffix = lane.lane_id.slice(lane.lane_id.lastIndexOf("_") + 1);
1605
+ const laneCompany = hostlessUlidLookup
1606
+ ? resolveLaneCompany(ctx.hqRoot(), lane).company
1607
+ : undefined;
1608
+ const companyMatches = !hostlessUlidLookup || (ownerCompany !== undefined &&
1609
+ laneCompany?.uid === ownerCompany.uid &&
1610
+ laneCompany.slug === ownerCompany.slug);
1611
+ if (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
1612
+ sanitizeHostComponent(lane.host.id) === hostSuffix) ||
1613
+ (hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
1614
+ sanitizeHostComponent(lane.host.id) === recordedSuffix &&
1615
+ companyMatches))) {
1584
1616
  matches.push({ outpostId, lane });
1585
1617
  }
1586
1618
  }
1587
1619
  }
1620
+ if (hostlessUlidLookup && failedOutpostIds.length > 0) {
1621
+ throw new LanesError("lane_owner_lookup_unavailable", `Could not verify whether lane ${laneId} is registered on every Outpost`, {
1622
+ lane_id: laneId,
1623
+ failed_outpost_ids: failedOutpostIds,
1624
+ remote_commands: failedOutpostCommands,
1625
+ });
1626
+ }
1588
1627
  if (matches.length === 0) {
1589
1628
  throw commandLaneNotFound(laneId, options.outpostHint === true, {
1590
1629
  ...(failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : {}),
@@ -1604,9 +1643,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1604
1643
  }
1605
1644
  function laneUlidPart(laneId) {
1606
1645
  const separator = laneId.indexOf("_");
1607
- if (separator !== 26)
1646
+ if (separator !== -1 && separator !== 26)
1608
1647
  return undefined;
1609
- const ulid = laneId.slice(0, separator);
1648
+ const ulid = separator === -1 ? laneId : laneId.slice(0, separator);
1610
1649
  return /^[0-9A-HJKMNP-TV-Z]{26}$/i.test(ulid) ? ulid.toUpperCase() : undefined;
1611
1650
  }
1612
1651
  function commandLaneNotFound(laneId, outpostHint, details = {}) {
@@ -7660,7 +7699,7 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
7660
7699
  async function runReconcile(ctx, opts, recoverQueuedMessages) {
7661
7700
  const fromEnv = ctx.env.HQ_ROOT?.trim();
7662
7701
  const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
7663
- const mirrorRefresh = opts.dryRun
7702
+ const mirrorRefresh = opts.dryRun || opts.terminalKeepAliveCandidate
7664
7703
  ? {
7665
7704
  updated: new Map(),
7666
7705
  unverifiedRemote: new Map(),
@@ -7724,6 +7763,9 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7724
7763
  recoverQueuedMessages,
7725
7764
  mesh: ctx.mesh,
7726
7765
  env: ctx.env,
7766
+ ...(opts.terminalKeepAliveCandidate !== undefined
7767
+ ? { terminalKeepAliveCandidate: opts.terminalKeepAliveCandidate }
7768
+ : {}),
7727
7769
  prepareKeepAliveClaudeResume: async (lane) => {
7728
7770
  const recoveredLane = laneWithRecoveredProviderSession(hqRoot, lane);
7729
7771
  const pendingModel = recoveredLane.pending_settings?.model;
@@ -8041,7 +8083,16 @@ export function registerLanesCommand(parent, deps = {}) {
8041
8083
  persistClaudeAccountSelection,
8042
8084
  printJson,
8043
8085
  recordResumeSetup,
8044
- resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
8086
+ resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId, {
8087
+ allowHostlessOwnerLookup: async () => {
8088
+ const company = await commandCtx.resolveCompany();
8089
+ const enabled = await readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
8090
+ company,
8091
+ env: commandCtx.env,
8092
+ });
8093
+ return enabled ? company : undefined;
8094
+ },
8095
+ }),
8045
8096
  resolveActiveQuestionCompany,
8046
8097
  hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
8047
8098
  cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
@@ -8406,7 +8457,8 @@ export function registerLanesCommand(parent, deps = {}) {
8406
8457
  .command("message")
8407
8458
  .description("Send a follow-up message to a lane")
8408
8459
  .argument("<lane-id>", "Lane id")
8409
- .requiredOption("--text <msg>", "Message text")
8460
+ .option("--text <msg>", "Message text")
8461
+ .option("--text-file <path>", "Read message text from a file")
8410
8462
  .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
8411
8463
  .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
8412
8464
  .option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
@@ -8745,6 +8797,30 @@ export function registerLanesCommand(parent, deps = {}) {
8745
8797
  await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
8746
8798
  });
8747
8799
  }));
8800
+ lanes
8801
+ .command("_keep-alive-terminal-trigger <laneId> <version> <dueAt>", { hidden: true })
8802
+ .action(wrap(async (laneId, versionText, dueAt) => {
8803
+ const version = Number.parseInt(versionText, 10);
8804
+ if (!Number.isSafeInteger(version) || version < 1 || String(version) !== versionText) {
8805
+ throw new LanesError("invalid_lane_version", "Terminal keep-alive trigger requires a positive lane version", {
8806
+ lane_id: laneId,
8807
+ version: versionText,
8808
+ });
8809
+ }
8810
+ const dueAtMs = Date.parse(dueAt);
8811
+ if (!Number.isFinite(dueAtMs)) {
8812
+ throw new LanesError("invalid_terminal_trigger_time", "Terminal keep-alive trigger requires a valid due time", {
8813
+ lane_id: laneId,
8814
+ due_at: dueAt,
8815
+ });
8816
+ }
8817
+ const delayMs = dueAtMs - Date.now();
8818
+ if (delayMs > 0)
8819
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
8820
+ await runReconcile(ctx, { terminalKeepAliveCandidate: { laneId, version } }, async (lane) => {
8821
+ await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
8822
+ });
8823
+ }));
8748
8824
  reconcile
8749
8825
  .command("install")
8750
8826
  .description("Write systemd user unit and timer files (idempotent)")
@@ -0,0 +1,3 @@
1
+ export declare const VAULT_RETENTION_SETTINGS_FLAG = "cli.vault-retention-settings";
2
+ export declare function vaultRetentionSettingsEnabled(companyUid?: string, idToken?: string): boolean | Promise<boolean>;
3
+ //# sourceMappingURL=vault-retention-flag.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { resolveCompanyFlagGate, resolveProcessFlagGate } from "../lib/flag-registry.js";
2
+ export const VAULT_RETENTION_SETTINGS_FLAG = "cli.vault-retention-settings";
3
+ export function vaultRetentionSettingsEnabled(companyUid, idToken) {
4
+ if (companyUid && idToken) {
5
+ return resolveCompanyFlagGate(companyUid, idToken, VAULT_RETENTION_SETTINGS_FLAG, { fallback: false }, () => false);
6
+ }
7
+ return resolveProcessFlagGate(VAULT_RETENTION_SETTINGS_FLAG, { fallback: false }, () => false);
8
+ }
9
+ //# sourceMappingURL=vault-retention-flag.js.map
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ import { vaultApiFetch } from "../utils/vault-api.js";
3
+ type Dependencies = {
4
+ enabled: (companyUid?: string, token?: string) => boolean | Promise<boolean>;
5
+ token: () => Promise<string>;
6
+ companyUid: (token: string, slug: string | undefined) => Promise<string>;
7
+ request: typeof vaultApiFetch;
8
+ };
9
+ export declare function registerVaultRetentionCommand(program: Command, deps?: Dependencies): void;
10
+ export {};
11
+ //# sourceMappingURL=vault-retention.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
2
+ import { markExpectedUserError } from "../utils/expected-cli-error.js";
3
+ import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
4
+ import { vaultRetentionSettingsEnabled } from "./vault-retention-flag.js";
5
+ const defaultDependencies = {
6
+ enabled: vaultRetentionSettingsEnabled,
7
+ token: ensureCognitoToken,
8
+ companyUid: getCompanyUid,
9
+ request: vaultApiFetch,
10
+ };
11
+ export function registerVaultRetentionCommand(program, deps = defaultDependencies) {
12
+ const vault = program.command("vault").description("Manage HQ vault settings");
13
+ const retention = vault
14
+ .command("retention")
15
+ .description("Get or set the default retention for future vault object versions")
16
+ .option("--company <slug>", "Company vault slug (omit for your personal vault)");
17
+ const companyOf = (command) => command.opts().company ??
18
+ retention.opts().company;
19
+ retention
20
+ .command("get")
21
+ .description("Show the vault's default retention in days")
22
+ .action(async function () {
23
+ await execute(this, "GET");
24
+ });
25
+ retention
26
+ .command("set")
27
+ .description("Set the vault's default retention in days (1-30)")
28
+ .argument("<days>")
29
+ .action(async function (rawDays) {
30
+ const days = Number(rawDays);
31
+ if (!Number.isInteger(days) || days < 1 || days > 30) {
32
+ throw new Error("days must be an integer from 1 through 30");
33
+ }
34
+ await execute(this, "PUT", days);
35
+ });
36
+ async function execute(command, method, days) {
37
+ const slug = companyOf(command);
38
+ let token;
39
+ let companyUid;
40
+ if (slug) {
41
+ token = await deps.token();
42
+ companyUid = await deps.companyUid(token, slug);
43
+ if (!(await deps.enabled(companyUid, token))) {
44
+ throw markExpectedUserError(new Error("Vault retention settings are not enabled for this company."));
45
+ }
46
+ }
47
+ else {
48
+ if (!(await deps.enabled())) {
49
+ throw markExpectedUserError(new Error("Vault retention settings are not enabled for this company."));
50
+ }
51
+ token = await deps.token();
52
+ }
53
+ const query = companyUid ? { companyUid } : undefined;
54
+ const response = await deps.request({
55
+ token,
56
+ path: "/vault/retention",
57
+ method,
58
+ ...(query ? { query } : {}),
59
+ ...(days === undefined ? {} : { body: { days } }),
60
+ });
61
+ if (!response.ok) {
62
+ const body = (await response.json().catch(() => ({})));
63
+ throw new Error(body.error ?? body.message ?? response.statusText);
64
+ }
65
+ const body = (await response.json());
66
+ process.stdout.write(`${body.days} days\n`);
67
+ }
68
+ }
69
+ //# sourceMappingURL=vault-retention.js.map
@@ -20,6 +20,7 @@ import { mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync,
20
20
  import { tmpdir } from "node:os";
21
21
  import { dirname, join } from "node:path";
22
22
  import * as Sentry from "@sentry/node";
23
+ import { gte as semverGte, valid as validSemver } from "semver";
23
24
  import { CLI_VERSION } from "../../cli-version.js";
24
25
  import { beforeSend } from "../../sentry-before-send.js";
25
26
  import { resolveLiveRoot } from "../../utils/hq-roots.js";
@@ -47,6 +48,10 @@ const HOOK_TIMEOUT_EXCEEDED_METRIC = "hq.hook_timeout_exceeded";
47
48
  const MAX_HOOK_SEQUENCE_ENTRIES = 20;
48
49
  const MAX_HOOK_TIMEOUT_DEBUG_PHASES = 24;
49
50
  const MAX_HOOK_TIMEOUT_DEBUG_MS = 86_400_000;
51
+ // This is the hqVersion embedded in the first hq-core tag whose master-hook
52
+ // replaces BASH_ENV with /dev/null before reading it (tag v15.0.157-beta.2).
53
+ // Earlier cores only report whether BASH_ENV is non-empty, which is ambiguous.
54
+ const BASH_ENV_NEUTRALIZED_CORE_VERSION = "15.0.157-beta.2";
50
55
  const HOOK_TIMEOUT_RATE_LIMIT_WINDOW_MS = 10 * 60 * 1_000;
51
56
  const RATE_LIMIT_LOCK_WAIT_MS = 500;
52
57
  const RATE_LIMIT_LOCK_POLL_MS = 5;
@@ -578,9 +583,10 @@ function hookTimeoutDebugContextTags(context) {
578
583
  function safeSlowChild(metadata) {
579
584
  const value = metadata.slow_child;
580
585
  const slowChildMs = metadata.slow_child_ms;
586
+ const bashEnv = bashEnvTag(metadata);
581
587
  const hasMeasuredChild = typeof slowChildMs === "number" && Number.isFinite(slowChildMs) && slowChildMs >= 0;
582
588
  if (metadata.hook_event === "SessionStart" &&
583
- (metadata.bash_env_set === true || metadata.bash_env_set === "set") &&
589
+ (bashEnv === "profile" || bashEnv === "set") &&
584
590
  !hasMeasuredChild &&
585
591
  (typeof value !== "string" || value.length === 0 || value === "other")) {
586
592
  return "entry-bash-startup";
@@ -605,8 +611,15 @@ function isMachineWideHookSlowness(metadata) {
605
611
  }
606
612
  function bashEnvTag(metadata) {
607
613
  const value = metadata.bash_env_set;
608
- if (value === true || value === "set")
609
- return "set";
614
+ if (value === "profile" || value === "devnull")
615
+ return value;
616
+ if (value === true || value === "set") {
617
+ const coreVersion = metadata.hq_version;
618
+ const parsedCoreVersion = typeof coreVersion === "string" ? validSemver(coreVersion) : null;
619
+ if (!parsedCoreVersion)
620
+ return "unknown";
621
+ return semverGte(parsedCoreVersion, BASH_ENV_NEUTRALIZED_CORE_VERSION) ? "devnull" : "set";
622
+ }
610
623
  if (value === false || value === "unset")
611
624
  return "unset";
612
625
  return "unknown";
@@ -882,9 +895,13 @@ function timeoutRateLimitLease(event, kind, options, dependencies) {
882
895
  }
883
896
  }
884
897
  function metricAttributes(metadata) {
885
- return Object.fromEntries(Object.entries(metadata)
898
+ const attributes = Object.fromEntries(Object.entries(metadata)
886
899
  .filter(([, value]) => isScalarMetadataValue(value))
887
900
  .map(([key, value]) => [key, typeof value === "string" ? safeTagValue(value) : value]));
901
+ if (Object.hasOwn(metadata, "bash_env_set")) {
902
+ attributes.bash_env_set = bashEnvTag(metadata);
903
+ }
904
+ return attributes;
888
905
  }
889
906
  function addHookSequenceBreadcrumbs(event, dependencies) {
890
907
  const sequence = event.metadata.hook_sequence;
@@ -79,6 +79,8 @@ export interface PlanContext {
79
79
  sessionHostRunnerPath: string | null;
80
80
  /** Environment retained from legacy outpost sync systemd drop-ins. */
81
81
  legacySyncEnv: Record<string, string>;
82
+ /** Keep the sync runner's event-driven instant-sync watcher enabled. */
83
+ instantSyncEnabled?: boolean;
82
84
  /** Keep the local sync runner stopped until the operator resumes it. */
83
85
  syncPaused: boolean;
84
86
  /** Persisted/effective owner selected for this machine. */
@@ -128,7 +128,19 @@ function planService(service, ctx) {
128
128
  command: {
129
129
  type: "node",
130
130
  script: ctx.syncRunnerPath,
131
- args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push", "--owner", SYNC_RUNNER_OWNER],
131
+ args: [
132
+ "--companies",
133
+ "--direction",
134
+ "both",
135
+ "--on-conflict",
136
+ "keep",
137
+ "--hq-root",
138
+ ctx.hqRoot ?? "",
139
+ "--watch",
140
+ ...(ctx.instantSyncEnabled === false ? [] : ["--event-push"]),
141
+ "--owner",
142
+ SYNC_RUNNER_OWNER,
143
+ ],
132
144
  },
133
145
  env: { ...ctx.legacySyncEnv },
134
146
  heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
@@ -14,6 +14,8 @@ export interface DaemonConfig {
14
14
  hqCloudVersion?: string;
15
15
  /** Persisted machine-local sync pause. */
16
16
  syncPaused?: boolean;
17
+ /** Desktop Instant Sync preference, applied when the sync unit is planned. */
18
+ instantSyncEnabled?: boolean;
17
19
  /** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
18
20
  syncOwner?: SyncOwner;
19
21
  /** Services turned on here beyond the machine type's defaults. */
@@ -45,6 +45,8 @@ export function readDaemonConfig(file) {
45
45
  enable: stringList(obj.enable),
46
46
  disable: stringList(obj.disable),
47
47
  };
48
+ if (typeof obj.instantSyncEnabled === "boolean")
49
+ config.instantSyncEnabled = obj.instantSyncEnabled;
48
50
  if (isMachineType(obj.machineType))
49
51
  config.machineType = obj.machineType;
50
52
  if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
@@ -67,6 +69,8 @@ export function writeDaemonConfig(file, config) {
67
69
  if (config.hqCloudVersion)
68
70
  ordered.hqCloudVersion = config.hqCloudVersion;
69
71
  ordered.syncPaused = config.syncPaused === true;
72
+ if (typeof config.instantSyncEnabled === "boolean")
73
+ ordered.instantSyncEnabled = config.instantSyncEnabled;
70
74
  if (config.syncOwner)
71
75
  ordered.syncOwner = config.syncOwner;
72
76
  ordered.enable = config.enable;
@@ -24,6 +24,8 @@ export interface RunDaemonOptions {
24
24
  hqBinary: string;
25
25
  }
26
26
  export declare function resolveSyncRunnerPath(): string | null;
27
+ /** The hosted desktop app uses this env value when planning daemon services. */
28
+ export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
27
29
  export declare function listLocalBots(root?: string): string[];
28
30
  export declare function windowsTaskExists(taskName: string): boolean;
29
31
  export declare function probeLegacyUnits(platform: NodeJS.Platform, home: string): LegacyUnit[];
@@ -53,6 +53,10 @@ export function resolveSyncRunnerPath() {
53
53
  return null;
54
54
  }
55
55
  }
56
+ /** The hosted desktop app uses this env value when planning daemon services. */
57
+ export function daemonInstantSyncEnabled(env, configured) {
58
+ return configured ?? env.HQ_DAEMON_INSTANT_SYNC !== "0";
59
+ }
56
60
  export function listLocalBots(root = botsRoot()) {
57
61
  if (!fs.existsSync(root))
58
62
  return [];
@@ -447,6 +451,7 @@ export async function runDaemon(opts) {
447
451
  heartbeatRunnerPath: runners.heartbeat,
448
452
  sessionHostRunnerPath: runners.sessionHost,
449
453
  legacySyncEnv: readLegacySyncEnv(platform),
454
+ instantSyncEnabled: daemonInstantSyncEnabled(env, config.instantSyncEnabled),
450
455
  signedIn: isSignedIn,
451
456
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
452
457
  syncPaused: config.syncPaused === true,
@@ -2,9 +2,13 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
2
2
  import type { LaneRecord } from "./types.js";
3
3
  export declare const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
4
4
  export declare const LEAD_AUTOPILOT_DEFAULT_ENABLED = false;
5
+ export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
6
+ export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DEFAULT_ENABLED = false;
7
+ export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS: number;
5
8
  export declare const LEAD_AUTOPILOT_REVIEW_DELAY_MS: number;
6
9
  export declare const LEAD_AUTOPILOT_FILL_DELAY_MS: number;
7
10
  export declare const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
11
+ export declare const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
8
12
  export interface RegisteredLead {
9
13
  team: string;
10
14
  lane: string;
@@ -12,7 +16,7 @@ export interface RegisteredLead {
12
16
  status: string;
13
17
  prd: string;
14
18
  }
15
- export type AutopilotCondition = "review_finished_worker" | "fill_grant" | "plan_free_slots" | "flat_kpi" | "blocker_record_needs_update" | "project_ready_to_close" | "stale_worker_ledger" | "story_without_live_lane";
19
+ export type AutopilotCondition = "review_finished_worker" | "fill_grant" | "plan_free_slots" | "flat_kpi" | "stale_kpi" | "blocker_record_needs_update" | "project_ready_to_close" | "stale_worker_ledger" | "story_without_live_lane" | "missed_deadline";
16
20
  export interface AutopilotWake {
17
21
  condition: AutopilotCondition;
18
22
  lead_lane: string;
@@ -20,6 +24,23 @@ export interface AutopilotWake {
20
24
  worker_lane?: string;
21
25
  prompt: string;
22
26
  fingerprint: string;
27
+ kpi_id?: string;
28
+ }
29
+ export interface AutopilotKpiStatus {
30
+ lead_lane: string;
31
+ team: string;
32
+ kpi: string;
33
+ age_minutes: number | null;
34
+ stale: boolean;
35
+ stale_after_minutes: number;
36
+ status?: string;
37
+ error?: "invalid_kpi";
38
+ }
39
+ interface AutopilotDeadlineError {
40
+ lead_lane: string;
41
+ item_ref: string;
42
+ field: string;
43
+ reason: string;
23
44
  }
24
45
  export interface AutopilotPassResult {
25
46
  ok: true;
@@ -38,11 +59,23 @@ export interface AutopilotPassResult {
38
59
  story_id: string;
39
60
  at: string;
40
61
  fingerprint: string;
62
+ owner?: string;
63
+ due?: string;
64
+ miss_count?: number;
65
+ }>;
66
+ missed_deadlines?: Array<{
67
+ lead_lane: string;
68
+ item: string;
69
+ owner: string;
70
+ due: string;
71
+ miss_count: number;
41
72
  }>;
73
+ deadline_errors?: AutopilotDeadlineError[];
42
74
  skipped: Array<{
43
75
  lead_lane: string;
44
76
  reason: string;
45
77
  }>;
78
+ kpi_status?: AutopilotKpiStatus[];
46
79
  /** Set on an Outpost that has not received a lead registry from the primary yet. */
47
80
  registry?: "missing";
48
81
  }
@@ -54,6 +87,7 @@ interface ProjectStory {
54
87
  laneIds?: string[];
55
88
  updatedAt?: string;
56
89
  externalBlocker?: string;
90
+ deadlineFields?: Record<string, unknown>;
57
91
  }
58
92
  export declare function planLeadAutopilotWakes(input: {
59
93
  lead: LaneRecord;
@@ -62,6 +96,7 @@ export declare function planLeadAutopilotWakes(input: {
62
96
  now: Date;
63
97
  managerStatus?: Record<string, unknown>;
64
98
  keepAliveResumed?: boolean;
99
+ kpiStalenessEnabled?: boolean;
65
100
  }): AutopilotWake[];
66
101
  export declare function runLeadAutopilotPass(input: {
67
102
  hqRoot: string;
@@ -71,7 +106,9 @@ export declare function runLeadAutopilotPass(input: {
71
106
  now: Date;
72
107
  dryRun?: boolean;
73
108
  readFlag?: typeof readLanesRegistryFlag;
109
+ readDeadlineFlag?: typeof readLanesRegistryFlag;
74
110
  keepAliveResumedLeads?: ReadonlySet<string>;
111
+ missedDeadlineEscalationDelayMs?: number;
75
112
  startWorker?: (input: {
76
113
  lead: LaneRecord;
77
114
  story: ProjectStory;