@indigoai-us/hq-cli 5.345.155 → 5.345.159

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 (169) hide show
  1. package/CHANGELOG.md +289 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +536 -7
  6. package/dist/command-catalog.generated.js +676 -24
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-inbox.js +2 -2
  12. package/dist/commands/agent-kit.js +8 -6
  13. package/dist/commands/bot.js +7 -4
  14. package/dist/commands/channel-suggest.d.ts +56 -0
  15. package/dist/commands/channel-suggest.js +170 -0
  16. package/dist/commands/channels.js +7 -1
  17. package/dist/commands/cloud.d.ts +45 -0
  18. package/dist/commands/cloud.js +150 -10
  19. package/dist/commands/company.js +4 -3
  20. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  21. package/dist/commands/core-checkpoint-native.js +67 -2
  22. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  23. package/dist/commands/core-pipeline-driver-native.js +131 -5
  24. package/dist/commands/core.js +13 -1
  25. package/dist/commands/crm.js +83 -6
  26. package/dist/commands/deploy-access.d.ts +33 -0
  27. package/dist/commands/deploy-access.js +322 -0
  28. package/dist/commands/dm-attach.d.ts +106 -0
  29. package/dist/commands/dm-attach.js +184 -0
  30. package/dist/commands/dm.d.ts +37 -0
  31. package/dist/commands/dm.js +172 -10
  32. package/dist/commands/feedback-show.d.ts +40 -0
  33. package/dist/commands/feedback-show.js +261 -0
  34. package/dist/commands/feedback.d.ts +21 -0
  35. package/dist/commands/feedback.js +152 -2
  36. package/dist/commands/files-browse.d.ts +16 -0
  37. package/dist/commands/files-browse.js +52 -1
  38. package/dist/commands/files.d.ts +24 -0
  39. package/dist/commands/files.js +52 -1
  40. package/dist/commands/integrations-connect.js +7 -3
  41. package/dist/commands/integrations-core.d.ts +13 -1
  42. package/dist/commands/integrations-core.js +49 -4
  43. package/dist/commands/integrations.js +4 -2
  44. package/dist/commands/lanes-accounts.js +16 -1
  45. package/dist/commands/lanes-loop.js +14 -4
  46. package/dist/commands/lanes-message.js +6 -4
  47. package/dist/commands/lanes-resume.js +36 -13
  48. package/dist/commands/lanes-shared.d.ts +5 -0
  49. package/dist/commands/lanes-shared.js +13 -3
  50. package/dist/commands/lanes-watch.js +1 -0
  51. package/dist/commands/lanes.d.ts +20 -1
  52. package/dist/commands/lanes.js +201 -51
  53. package/dist/commands/mesh.js +149 -1
  54. package/dist/commands/monitor.d.ts +1 -0
  55. package/dist/commands/monitor.js +4 -1
  56. package/dist/commands/repo.d.ts +41 -0
  57. package/dist/commands/repo.js +331 -0
  58. package/dist/commands/scan.d.ts +3 -1
  59. package/dist/commands/scan.js +7 -3
  60. package/dist/commands/secrets.d.ts +37 -0
  61. package/dist/commands/secrets.js +185 -16
  62. package/dist/commands/signals.js +17 -5
  63. package/dist/commands/staff/listings.d.ts +41 -0
  64. package/dist/commands/staff/listings.js +160 -0
  65. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  66. package/dist/commands/staff/reset-test-account.js +4 -3
  67. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  68. package/dist/hq-cloud-sync-runner/package.json +1 -1
  69. package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
  70. package/dist/lib/agent-kit/inbox-state.js +5 -1
  71. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  72. package/dist/lib/agent-kit/run/inbox.js +2 -1
  73. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  74. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  75. package/dist/lib/agent-kit/wake.d.ts +4 -0
  76. package/dist/lib/agent-kit/wake.js +49 -3
  77. package/dist/lib/bot/inflight.d.ts +7 -1
  78. package/dist/lib/bot/inflight.js +10 -2
  79. package/dist/lib/bot/prompt.js +3 -0
  80. package/dist/lib/bot/run.js +72 -20
  81. package/dist/lib/bot/session.d.ts +14 -0
  82. package/dist/lib/bot/session.js +70 -0
  83. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  84. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  85. package/dist/lib/capacity/lane-login.d.ts +6 -0
  86. package/dist/lib/capacity/lane-login.js +5 -0
  87. package/dist/lib/core-utils/worktree.js +2 -0
  88. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  89. package/dist/lib/daemon/exit-cause.js +88 -0
  90. package/dist/lib/daemon/handoff.d.ts +2 -0
  91. package/dist/lib/daemon/handoff.js +6 -1
  92. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  93. package/dist/lib/daemon/paths.d.ts +2 -0
  94. package/dist/lib/daemon/paths.js +1 -0
  95. package/dist/lib/daemon/run.js +13 -0
  96. package/dist/lib/daemon/supervisor.d.ts +54 -1
  97. package/dist/lib/daemon/supervisor.js +297 -21
  98. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  99. package/dist/lib/daemon/unit-holds.js +86 -0
  100. package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
  101. package/dist/lib/doctor/checks/sync-health.js +66 -1
  102. package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
  103. package/dist/lib/doctor/company-resolution-state.js +89 -0
  104. package/dist/lib/doctor/fix/apply.d.ts +36 -6
  105. package/dist/lib/doctor/fix/apply.js +128 -12
  106. package/dist/lib/doctor/types.d.ts +1 -1
  107. package/dist/lib/doctor/types.js +1 -0
  108. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  109. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  110. package/dist/lib/hook-engine/resolve.js +6 -2
  111. package/dist/lib/index-render/company-knowledge.d.ts +1 -1
  112. package/dist/lib/index-render/company-knowledge.js +4 -5
  113. package/dist/lib/index-render/index.js +44 -3
  114. package/dist/lib/index-render/projects.d.ts +1 -1
  115. package/dist/lib/index-render/projects.js +5 -5
  116. package/dist/lib/lanes/account-company.d.ts +28 -0
  117. package/dist/lib/lanes/account-company.js +53 -0
  118. package/dist/lib/lanes/accounts.d.ts +22 -3
  119. package/dist/lib/lanes/accounts.js +38 -2
  120. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  121. package/dist/lib/lanes/claude-balance.js +6 -1
  122. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  123. package/dist/lib/lanes/claude-credential.js +163 -0
  124. package/dist/lib/lanes/compaction.d.ts +2 -0
  125. package/dist/lib/lanes/compaction.js +9 -0
  126. package/dist/lib/lanes/config.js +6 -0
  127. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  128. package/dist/lib/lanes/hooks-install.js +2 -1
  129. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  130. package/dist/lib/lanes/host-capacity.js +1 -0
  131. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  132. package/dist/lib/lanes/keep-alive.js +3 -1
  133. package/dist/lib/lanes/links.js +10 -4
  134. package/dist/lib/lanes/loop.js +16 -1
  135. package/dist/lib/lanes/node-binary.d.ts +17 -0
  136. package/dist/lib/lanes/node-binary.js +30 -0
  137. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  138. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  139. package/dist/lib/lanes/reconcile.js +4 -0
  140. package/dist/lib/lanes/remote-control-lane.js +2 -0
  141. package/dist/lib/lanes/spawn.d.ts +3 -0
  142. package/dist/lib/lanes/spawn.js +119 -9
  143. package/dist/lib/lanes/store.d.ts +6 -0
  144. package/dist/lib/lanes/store.js +107 -18
  145. package/dist/lib/lanes/types.d.ts +2 -0
  146. package/dist/lib/lanes/watch.js +28 -5
  147. package/dist/lib/lanes/watcher-main.js +4 -3
  148. package/dist/lib/lanes/watcher.d.ts +4 -0
  149. package/dist/lib/lanes/watcher.js +110 -21
  150. package/dist/lib/mesh/api.d.ts +32 -0
  151. package/dist/lib/mesh/api.js +75 -0
  152. package/dist/lib/secret-scan/detect.js +9 -0
  153. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  154. package/dist/lib/secret-scan/repo-config.js +237 -0
  155. package/dist/lib/secret-scan/rules.d.ts +7 -1
  156. package/dist/lib/secret-scan/rules.js +3 -1
  157. package/dist/lib/secret-scan/staged.d.ts +5 -0
  158. package/dist/lib/secret-scan/staged.js +19 -2
  159. package/dist/lib/secret-scan/types.d.ts +2 -0
  160. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  161. package/dist/lib/sync/server-owned-local-only.js +75 -0
  162. package/dist/sentry-before-send.js +1 -0
  163. package/dist/utils/access-denied-hint.js +6 -0
  164. package/dist/utils/client-health.d.ts +34 -0
  165. package/dist/utils/client-health.js +96 -3
  166. package/dist/utils/package-use-lease.js +27 -9
  167. package/dist/utils/version-gate.d.ts +7 -0
  168. package/dist/utils/version-gate.js +66 -0
  169. package/package.json +8 -8
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Expiry check for the Claude OAuth credential a lane worker will use.
3
+ *
4
+ * `hq lanes accounts list` used to report `logged_in: true` when a
5
+ * credential merely existed (a `.credentials.json` file, or `claude auth
6
+ * status` saying a keychain item is present). An expired access token still
7
+ * passed, and the lane exited seconds after start with an OAuth expiry
8
+ * error. This module reads the same credential the worker reads and reports
9
+ * whether its access token is still valid.
10
+ *
11
+ * The worker's credential is keyed by CLAUDE_CONFIG_DIR:
12
+ * - macOS: the login user's Keychain item `Claude Code-credentials` for the
13
+ * default home (the worker runs with the variable unset), or the
14
+ * hash-suffixed item for any other home; `<home>/.credentials.json` when no
15
+ * Keychain item exists.
16
+ * - elsewhere: `<home>/.credentials.json`.
17
+ *
18
+ * An access token past its expiry is only reported expired when there is no
19
+ * refresh token, or the capacity collector has recorded the refresh token as
20
+ * revoked. Otherwise Claude Code renews the access token at startup, so the
21
+ * account is still usable.
22
+ *
23
+ * Only `claudeAiOauth.expiresAt` and the presence of the tokens are read.
24
+ * Token values are never returned, logged, or copied; the refresh token is
25
+ * only passed to the revoked check, which compares a fingerprint.
26
+ */
27
+ import { spawnSync } from "node:child_process";
28
+ import * as fs from "node:fs";
29
+ import * as os from "node:os";
30
+ import * as path from "node:path";
31
+ import { CLAUDE_KEYCHAIN_SERVICE, claudeKeychainServiceName, isRefreshTokenRevoked, } from "../capacity/collectors/claude-oauth.js";
32
+ import { resolveLanesHqRoot } from "./hq-root.js";
33
+ /** Revoked check against the capacity collector's marker file. Never throws. */
34
+ export function defaultIsRefreshRevoked(refreshToken) {
35
+ try {
36
+ return isRefreshTokenRevoked(resolveLanesHqRoot(), refreshToken);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ function safeUsername() {
43
+ try {
44
+ return os.userInfo().username || null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ export const CLAUDE_CREDENTIAL_SECURITY_TIMEOUT_MS = 5_000;
51
+ /** `security find-generic-password` exit status when the item does not exist. */
52
+ const SECURITY_ITEM_NOT_FOUND = 44;
53
+ /** Keychain service the worker reads for this home. */
54
+ export function laneClaudeKeychainService(target) {
55
+ return target.defaultHome ? CLAUDE_KEYCHAIN_SERVICE : claudeKeychainServiceName(target.home);
56
+ }
57
+ /** Classify a credential document. Returns only state and expiry. */
58
+ export function claudeTokenStateFromDocument(text, nowMs, isRefreshRevoked = () => false) {
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(text);
62
+ }
63
+ catch {
64
+ return { state: "unknown", expires_at: null };
65
+ }
66
+ if (!isRecord(parsed))
67
+ return { state: "unknown", expires_at: null };
68
+ const oauth = parsed.claudeAiOauth;
69
+ if (!isRecord(oauth))
70
+ return { state: "missing", expires_at: null };
71
+ const token = typeof oauth.accessToken === "string" ? oauth.accessToken : oauth.access_token;
72
+ if (typeof token !== "string" || token.trim() === "")
73
+ return { state: "missing", expires_at: null };
74
+ const raw = oauth.expiresAt;
75
+ if (typeof raw !== "number" || !Number.isFinite(raw))
76
+ return { state: "valid", expires_at: null };
77
+ const ms = raw < 1e12 ? raw * 1000 : raw;
78
+ const date = new Date(ms);
79
+ const expires_at = Number.isNaN(date.getTime()) ? null : date.toISOString();
80
+ if (ms > nowMs)
81
+ return { state: "valid", expires_at };
82
+ // Past expiry. Claude Code renews the access token from the refresh token
83
+ // at startup, so this is only a dead sign-in when there is nothing to renew
84
+ // with or the refresh token is known to be revoked.
85
+ const refresh = typeof oauth.refreshToken === "string" ? oauth.refreshToken : oauth.refresh_token;
86
+ if (typeof refresh !== "string" || refresh.trim() === "")
87
+ return { state: "expired", expires_at };
88
+ let revoked = false;
89
+ try {
90
+ revoked = isRefreshRevoked(refresh);
91
+ }
92
+ catch {
93
+ revoked = false;
94
+ }
95
+ return { state: revoked ? "expired" : "valid", expires_at };
96
+ }
97
+ function isRecord(value) {
98
+ return value !== null && typeof value === "object" && !Array.isArray(value);
99
+ }
100
+ function defaultSecurity(args) {
101
+ const result = spawnSync("security", [...args], {
102
+ encoding: "utf8",
103
+ timeout: CLAUDE_CREDENTIAL_SECURITY_TIMEOUT_MS,
104
+ stdio: ["ignore", "pipe", "ignore"],
105
+ maxBuffer: 8 * 1024 * 1024,
106
+ });
107
+ const timedOut = result.error?.code === "ETIMEDOUT";
108
+ return {
109
+ status: result.error ? null : result.status,
110
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
111
+ timedOut,
112
+ };
113
+ }
114
+ export function createClaudeCredentialReader(deps = {}) {
115
+ const platform = deps.platform ?? process.platform;
116
+ const now = deps.now ?? (() => Date.now());
117
+ const security = deps.security ?? defaultSecurity;
118
+ const readFile = deps.readFile ?? ((file) => fs.readFileSync(file, "utf8"));
119
+ const isRefreshRevoked = deps.isRefreshRevoked ?? defaultIsRefreshRevoked;
120
+ const fromFile = (home) => {
121
+ let text;
122
+ try {
123
+ text = readFile(path.join(home, ".credentials.json"));
124
+ }
125
+ catch (err) {
126
+ const code = err?.code;
127
+ return { state: code === "ENOENT" ? "missing" : "unknown", expires_at: null, source: null };
128
+ }
129
+ return { ...claudeTokenStateFromDocument(text, now(), isRefreshRevoked), source: "file" };
130
+ };
131
+ return (target) => {
132
+ if (platform !== "darwin")
133
+ return fromFile(target.home);
134
+ const service = laneClaudeKeychainService(target);
135
+ let read;
136
+ try {
137
+ // Claude Code writes its item under the login user's account name. A
138
+ // second item with the same service and another account (MCP OAuth
139
+ // only) can exist, so ask for the user's item first, as the capacity
140
+ // collector does, and fall back to any item for the service.
141
+ const username = deps.username ?? safeUsername();
142
+ read = username
143
+ ? security(["find-generic-password", "-s", service, "-a", username, "-w"])
144
+ : { status: SECURITY_ITEM_NOT_FOUND, stdout: "", timedOut: false };
145
+ if (!read.timedOut && read.status === SECURITY_ITEM_NOT_FOUND) {
146
+ read = security(["find-generic-password", "-s", service, "-w"]);
147
+ }
148
+ }
149
+ catch {
150
+ return { state: "unknown", expires_at: null, source: null };
151
+ }
152
+ if (read.timedOut)
153
+ return { state: "unknown", expires_at: null, source: null };
154
+ if (read.status === SECURITY_ITEM_NOT_FOUND)
155
+ return fromFile(target.home);
156
+ if (read.status !== 0)
157
+ return { state: "unknown", expires_at: null, source: null };
158
+ return { ...claudeTokenStateFromDocument(read.stdout.replace(/\n$/, ""), now(), isRefreshRevoked), source: "keychain" };
159
+ };
160
+ }
161
+ /** Production reader: real Keychain on macOS, the credentials file elsewhere. */
162
+ export const readLaneClaudeCredential = createClaudeCredentialReader();
163
+ //# sourceMappingURL=claude-credential.js.map
@@ -79,6 +79,8 @@ export type RunCompactionFn = (input: {
79
79
  }) => CompactionCommandResult;
80
80
  /** Synchronous `/compact`. stdout is discarded; stderr goes to the run dir. */
81
81
  export declare const defaultRunCompaction: RunCompactionFn;
82
+ /** Stderr line printed before a resume-time `/compact` starts. */
83
+ export declare function compactionStartNotice(sessionId: string): string;
82
84
  export interface CompactLaneSessionInput {
83
85
  command: string;
84
86
  /** Full argv of `claude -p --resume <sid> "/compact" …`, built by buildProviderCommand. */
@@ -388,6 +388,12 @@ export const defaultRunCompaction = (input) => {
388
388
  fs.closeSync(errFd);
389
389
  }
390
390
  };
391
+ /** Stderr line printed before a resume-time `/compact` starts. */
392
+ export function compactionStartNotice(sessionId) {
393
+ const minutes = Math.round(COMPACTION_TIMEOUT_MS / 60_000);
394
+ return `hq lanes: compacting session ${sessionId} before resume; this can take up to ${minutes} minutes. ` +
395
+ `Re-run with --no-compact to skip it.\n`;
396
+ }
391
397
  /**
392
398
  * Run `/compact` on the recorded session and report what happened. A failed
393
399
  * or ineffective compaction is recorded, not thrown: the resume that needed
@@ -398,6 +404,9 @@ export function compactLaneSession(input) {
398
404
  const locate = input.locateTranscript ?? findSessionTranscript;
399
405
  const started = input.now();
400
406
  const beforeProbe = probeCompactBoundaries(locate(input.accountHome, input.sessionId));
407
+ // `/compact` runs synchronously and can take minutes. Say so before it
408
+ // starts, so a resume that is waiting on it does not look hung.
409
+ process.stderr.write(compactionStartNotice(input.sessionId));
401
410
  const result = run({
402
411
  command: input.command,
403
412
  args: input.args,
@@ -186,6 +186,12 @@ function parseAccount(provider, id, raw, deps) {
186
186
  home: canonicalizeHome(raw.home, deps),
187
187
  enabled: raw.enabled,
188
188
  };
189
+ if (raw.company !== undefined) {
190
+ if (typeof raw.company !== "string" || raw.company.trim() === "" || raw.company.trim() !== raw.company) {
191
+ throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id}.company must be a non-empty company slug or UID`, { provider, account: id });
192
+ }
193
+ account.company = raw.company;
194
+ }
189
195
  if (raw.hosts !== undefined) {
190
196
  const field = `providers.${provider}.accounts.${id}.hosts`;
191
197
  if (!isPlainObject(raw.hosts)) {
@@ -1,6 +1,7 @@
1
1
  import { spawnSync as nodeSpawnSync } from "node:child_process";
2
2
  import * as path from "node:path";
3
3
  import { LanesError } from "./errors.js";
4
+ import { liveNodeBinary } from "./node-binary.js";
4
5
  const LANE_RUNNER_ENTRY_NAMES = new Set(["watcher-main", "worker-process", "remote-control-lane"]);
5
6
  /**
6
7
  * True for the detached lane runner scripts. They are not the hq CLI: running
@@ -42,7 +43,7 @@ export function spawnEnvelopeRepairViaCurrentCli(hqRoot, lane, input, deps = {})
42
43
  isLaneRunnerEntry(cliEntry)) {
43
44
  throw new LanesError("repair_cli_entry_invalid", "Lane watcher has no valid absolute hq CLI entrypoint for envelope repair", { lane_id: lane.lane_id });
44
45
  }
45
- const result = run(process.execPath, [cliEntry, "lanes", "_spawn-envelope-repair"], {
46
+ const result = run(liveNodeBinary(), [cliEntry, "lanes", "_spawn-envelope-repair"], {
46
47
  encoding: "utf8",
47
48
  timeout: 120_000,
48
49
  input: JSON.stringify(request),
@@ -25,6 +25,7 @@ import * as path from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { parse as parseToml, TomlError } from "smol-toml";
27
27
  import { LanesError } from "./errors.js";
28
+ import { liveNodeBinary } from "./node-binary.js";
28
29
  import { parseLaneProvider } from "./types.js";
29
30
  export const DRAIN_HOOK_BASENAME = "hq-lanes-drain";
30
31
  export const DRAIN_HOOK_TIMEOUT_SECONDS = 30;
@@ -49,7 +50,7 @@ export const PROVIDER_REGISTRATION_KIND = {
49
50
  };
50
51
  export function drainHookCommand(_shPath, provider) {
51
52
  const providerEnv = provider === "grok" ? "HQ_LANE_PROVIDER=grok " : "";
52
- return `${providerEnv}${shellQuote(process.execPath)} ${shellQuote(drainHookEntryPath())}`;
53
+ return `${providerEnv}${shellQuote(liveNodeBinary())} ${shellQuote(drainHookEntryPath())}`;
53
54
  }
54
55
  function legacyDrainHookCommand(shPath) {
55
56
  return `bash ${shPath}`;
@@ -1,4 +1,5 @@
1
1
  import { type AuthState } from "./accounts.js";
2
+ import type { ClaudeCredentialReader } from "./claude-credential.js";
2
3
  import { type QuotaState } from "./quota.js";
3
4
  import type { LaneHost, LaneProvider, LaneRecord, LanesConfig } from "./types.js";
4
5
  export interface HostCapacityAccount {
@@ -63,6 +64,7 @@ export declare function buildHostCapacitySnapshot(input: {
63
64
  hostname?: string;
64
65
  now: Date;
65
66
  quotaState?: QuotaState;
67
+ readClaudeCredential?: ClaudeCredentialReader;
66
68
  }): HostCapacitySnapshot;
67
69
  /** Reject malformed remote snapshots before they can influence host selection. */
68
70
  export declare function parseHostCapacitySnapshot(raw: unknown): HostCapacitySnapshot;
@@ -31,6 +31,7 @@ export function buildHostCapacitySnapshot(input) {
31
31
  ...(input.probe !== undefined ? { probe: input.probe } : {}),
32
32
  ...(input.env !== undefined ? { env: input.env } : {}),
33
33
  ...(input.hostname !== undefined ? { hostname: input.hostname } : {}),
34
+ ...(input.readClaudeCredential !== undefined ? { readClaudeCredential: input.readClaudeCredential } : {}),
34
35
  });
35
36
  const accountLanes = providerLanes.filter((lane) => lane.account === id);
36
37
  const liveLanes = accountLanes.filter((lane) => lane.state === "running" || lane.state === "queued" || lane.state === "awaiting_input");
@@ -30,7 +30,7 @@ export declare function queueLaneTerminalResume(current: LaneRecord, at: string,
30
30
  trigger?: "resume" | "watcher";
31
31
  }): LaneTerminalResumeResult;
32
32
  /** Persist a manager status miss and queue its one bounded follow-up turn. */
33
- export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
33
+ export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string, skip?: (current: LaneRecord) => boolean): LaneRecord;
34
34
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
35
35
  export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
36
36
  export declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
@@ -75,8 +75,10 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
75
75
  };
76
76
  }
77
77
  /** Persist a manager status miss and queue its one bounded follow-up turn. */
78
- export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText) {
78
+ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText, skip = () => false) {
79
79
  return retryUpdateLane(hqRoot, laneId, "watcher", (current) => {
80
+ if (skip(current))
81
+ return current;
80
82
  const gate = current.status_gate;
81
83
  if (gate === undefined || gate.turn_id !== entry.turn_id)
82
84
  return current;
@@ -117,6 +117,9 @@ function resolvePointer(root, sid, role, options) {
117
117
  if (role === "child" && !safeId(child))
118
118
  throw new LaneLinkError("child_name_invalid", `Invalid child name: ${child ?? ""}`);
119
119
  const link = options.link ?? pointer.link;
120
+ if (role === "conductor" && link !== pointer.link) {
121
+ throw new LaneLinkError("link_role_invalid", "A conductor can only consume or wait on its own link");
122
+ }
120
123
  if (role === "child") {
121
124
  if (options.link !== undefined && !safeId(options.link))
122
125
  throw new LaneLinkError("link_invalid", "Invalid --link");
@@ -412,15 +415,18 @@ export async function runLaneLink(subcommand, options) {
412
415
  }
413
416
  if (subcommand === "inbox" || subcommand === "read") {
414
417
  const role = subcommand === "inbox" ? "child" : "conductor";
415
- const resolved = role === "conductor" && options.link !== undefined
416
- ? { link: options.link }
417
- : resolvePointer(root, requireSid(), role, options);
418
+ // Preserve explicit-link validation errors before resolving ownership.
419
+ if (role === "conductor" && options.link !== undefined)
420
+ linkPath(root, options.link);
421
+ const resolved = resolvePointer(root, requireSid(), role, options);
418
422
  linkPath(root, resolved.link);
419
423
  const text = drainLinkQueue(queueDir(root, resolved.link, role === "child" ? resolved.child : undefined));
420
424
  return { output: { messages: text } };
421
425
  }
422
426
  if (subcommand === "wait") {
423
- const resolved = options.link ? { link: options.link } : resolvePointer(root, requireSid(), "conductor", options);
427
+ if (options.link !== undefined)
428
+ linkPath(root, options.link);
429
+ const resolved = resolvePointer(root, requireSid(), "conductor", options);
424
430
  return await waitForReport(linkPath(root, resolved.link), options.timeout ?? 1800, options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))));
425
431
  }
426
432
  if (subcommand === "list") {
@@ -1002,6 +1002,21 @@ function appendJournal(runDir, value) {
1002
1002
  fs.mkdirSync(path.dirname(file), { recursive: true });
1003
1003
  fs.appendFileSync(file, `${JSON.stringify(value)}\n`, { mode: 0o600 });
1004
1004
  }
1005
+ /**
1006
+ * Where a synthetic failed or blocked handoff goes. The phase worker may have
1007
+ * written its own handoff to result_path already (for example one that failed
1008
+ * validation). That file is the only copy of the worker's evidence, so it is
1009
+ * never overwritten: the synthetic handoff is written beside it instead
1010
+ * (`<name>.lane-failure.json`). With no worker file, result_path itself gets
1011
+ * the synthetic handoff so the pipeline driver still sees the phase end.
1012
+ */
1013
+ function syntheticHandoffPath(resultPath) {
1014
+ if (!fs.existsSync(resultPath))
1015
+ return resultPath;
1016
+ const ext = path.extname(resultPath);
1017
+ const base = ext ? resultPath.slice(0, -ext.length) : resultPath;
1018
+ return `${base}.lane-failure${ext || ".json"}`;
1019
+ }
1005
1020
  function writeResultAt(runDir, activeFile, record, result, includeHandoffValidationError = true) {
1006
1021
  const localPath = path.join(resultsDir(runDir), resultName(record.envelope_id));
1007
1022
  atomicJson(localPath, result);
@@ -1025,7 +1040,7 @@ function writeResultAt(runDir, activeFile, record, result, includeHandoffValidat
1025
1040
  back_pressure: { tests: "skip", lint: "skip", typecheck: "skip", build: "skip" },
1026
1041
  context_for_next: [result.error ?? result.status, ...(result.question_id ? [`Decision question: ${result.question_id}`] : [])].join("\n"),
1027
1042
  };
1028
- atomicJson(path.resolve(externalPath), failure);
1043
+ atomicJson(syntheticHandoffPath(path.resolve(externalPath)), failure);
1029
1044
  }
1030
1045
  }
1031
1046
  else
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The Node binary to use for a child process started by a long-lived lane
3
+ * process (loop worker, watcher, drain hook registration).
4
+ *
5
+ * `process.execPath` is fixed when the process starts. A Node upgrade can
6
+ * remove that file while the process keeps running (Homebrew deletes the old
7
+ * Cellar directory), and every later child then fails to start. When the
8
+ * launch-time binary is gone:
9
+ *
10
+ * 1. For a Homebrew Cellar path (`<prefix>/Cellar/<formula>/<version>/bin/node`),
11
+ * use the stable `<prefix>/bin/node` symlink when it exists. That points at
12
+ * the upgraded install of the same prefix and does not depend on PATH or on
13
+ * a version manager shim reading the child's cwd.
14
+ * 2. Otherwise fall back to `node` resolved through the child env PATH.
15
+ */
16
+ export declare function liveNodeBinary(execPath?: string, exists?: (candidate: string) => boolean): string;
17
+ //# sourceMappingURL=node-binary.d.ts.map
@@ -0,0 +1,30 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ /**
4
+ * The Node binary to use for a child process started by a long-lived lane
5
+ * process (loop worker, watcher, drain hook registration).
6
+ *
7
+ * `process.execPath` is fixed when the process starts. A Node upgrade can
8
+ * remove that file while the process keeps running (Homebrew deletes the old
9
+ * Cellar directory), and every later child then fails to start. When the
10
+ * launch-time binary is gone:
11
+ *
12
+ * 1. For a Homebrew Cellar path (`<prefix>/Cellar/<formula>/<version>/bin/node`),
13
+ * use the stable `<prefix>/bin/node` symlink when it exists. That points at
14
+ * the upgraded install of the same prefix and does not depend on PATH or on
15
+ * a version manager shim reading the child's cwd.
16
+ * 2. Otherwise fall back to `node` resolved through the child env PATH.
17
+ */
18
+ export function liveNodeBinary(execPath = process.execPath, exists = fs.existsSync) {
19
+ if (exists(execPath))
20
+ return execPath;
21
+ const parts = execPath.split(path.sep);
22
+ const cellar = parts.lastIndexOf("Cellar");
23
+ if (cellar > 0) {
24
+ const prefixLink = path.join(parts.slice(0, cellar).join(path.sep) || path.sep, "bin", "node");
25
+ if (exists(prefixLink))
26
+ return prefixLink;
27
+ }
28
+ return "node";
29
+ }
30
+ //# sourceMappingURL=node-binary.js.map
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target" | "blocked-escalation-target";
30
+ action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target" | "set-target" | "blocked-escalation-target";
31
31
  laneId: string;
32
32
  hqBinary?: string;
33
33
  text?: string;
@@ -46,6 +46,7 @@ export interface OutpostLaneCommand {
46
46
  profileRepinContext?: string;
47
47
  approvalContext?: string;
48
48
  senior?: string;
49
+ actor?: string;
49
50
  reason?: string;
50
51
  company?: string;
51
52
  compact?: boolean;
@@ -225,6 +225,7 @@ export function buildOutpostLaneCommand(input) {
225
225
  ["--approval-context", input.approvalContext],
226
226
  ["--failed-resume-context", input.failedResumeContext],
227
227
  ["--senior", input.senior],
228
+ ["--actor", input.actor],
228
229
  ["--reason", input.reason],
229
230
  ["--company", input.company],
230
231
  ["--message-id", input.messageId],
@@ -5,6 +5,7 @@
5
5
  * compare-and-set. The per-lane watcher owns progress/terminal/silence of a
6
6
  * live process — this pass owns the transitions no single lane can own.
7
7
  */
8
+ import { accountMatchesCompany } from "./account-company.js";
8
9
  import * as fs from "node:fs";
9
10
  import * as path from "node:path";
10
11
  import { parseUserYaml } from "../../utils/user-yaml-error.js";
@@ -724,6 +725,8 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
724
725
  for (const account of lane.admission_accounts) {
725
726
  if (account === lane.account)
726
727
  continue;
728
+ if (!accountMatchesCompany(config.providers[lane.provider].accounts[account], resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts))
729
+ continue;
727
730
  if (config.providers[lane.provider].accounts[account]?.enabled !== true)
728
731
  continue;
729
732
  const status = accountStatus(lane.provider, account, config, {
@@ -754,6 +757,7 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
754
757
  function writeAdmissionAccount(hqRoot, lane, account, dryRun) {
755
758
  return casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "queued" &&
756
759
  current.account === lane.account &&
760
+ accountMatchesCompany(loadLanesConfig(hqRoot).providers[current.provider].accounts[account], resolveLaneCompany(hqRoot, current).company, loadLanesConfig(hqRoot).providers[current.provider].accounts) &&
757
761
  current.admission_accounts?.includes(account) === true, (current) => ({ ...current, account }), dryRun).record;
758
762
  }
759
763
  function casQuestionIf(hqRoot, questionId, shouldWrite, updater, dryRun) {
@@ -15,6 +15,7 @@ import { isOwnedPoolForwarder, poolHostsFile, readRemoteControlPoolsConfig } fro
15
15
  import { accountStatus, isDefaultProviderHome } from "./accounts.js";
16
16
  import { effectiveAccountHome } from "./account-home.js";
17
17
  import { LanesError } from "./errors.js";
18
+ import { readLaneClaudeCredential } from "./claude-credential.js";
18
19
  import { defaultProbe } from "./models.js";
19
20
  import { encodeClaudeProjectDir } from "./move-account.js";
20
21
  /** Keep every credential and network route tied to the selected pool. */
@@ -117,6 +118,7 @@ export function prepareRemoteControlLane(input) {
117
118
  }
118
119
  const status = accountStatus("claude", "a", input.config, {
119
120
  hostname: input.hostname, existsSync: fs.existsSync, probe: defaultProbe, env: input.env,
121
+ readClaudeCredential: readLaneClaudeCredential,
120
122
  });
121
123
  if (!status.logged_in) {
122
124
  throw new LanesError("remote_control_login_missing", "Claude account a must be logged in for Remote Control", {});
@@ -8,6 +8,7 @@
8
8
  * mutable fields are re-read from the lane record (spec §5.4).
9
9
  */
10
10
  import { PROVIDER_HOME_ENV } from "./accounts.js";
11
+ import { type ClaudeCredentialReader } from "./claude-credential.js";
11
12
  import { type RunCompactionFn } from "./compaction.js";
12
13
  import { type ResolvedEffort } from "./effort.js";
13
14
  import { type KnownModelsDeps } from "./models.js";
@@ -66,6 +67,8 @@ export type ProveDetachedFn = (pid: number) => DetachProof;
66
67
  export type KillProcessFn = (pid: number, signal?: NodeJS.Signals) => void;
67
68
  export interface SpawnLaneDeps {
68
69
  env?: NodeJS.ProcessEnv;
70
+ /** Claude credential expiry check; see spawnCredentialReader for the default. */
71
+ readClaudeCredential?: ClaudeCredentialReader;
69
72
  /** Builds the lane's fingerprint from the worker env. Defaults to laneStartFingerprint. */
70
73
  buildFingerprint?: (hqRoot: string, workerEnv: NodeJS.ProcessEnv) => string | null;
71
74
  /** Test seam for the default-off quota account failover registry gate. */