@indigoai-us/hq-cli 5.131.0 → 5.133.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 (66) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/command-catalog.generated.d.ts +12 -0
  3. package/dist/command-catalog.generated.js +16 -0
  4. package/dist/commands/lanes.d.ts +15 -4
  5. package/dist/commands/lanes.js +478 -48
  6. package/dist/commands/mesh.js +24 -2
  7. package/dist/commands/update.js +1 -1
  8. package/dist/lib/bot/paths.d.ts +7 -5
  9. package/dist/lib/bot/paths.js +8 -1
  10. package/dist/lib/bot/prompt.d.ts +0 -2
  11. package/dist/lib/bot/prompt.js +5 -11
  12. package/dist/lib/bot/room-policy.d.ts +1 -24
  13. package/dist/lib/bot/room-policy.js +2 -35
  14. package/dist/lib/bot/run.d.ts +7 -8
  15. package/dist/lib/bot/run.js +21 -33
  16. package/dist/lib/bot/session.d.ts +1 -1
  17. package/dist/lib/bot/session.js +1 -1
  18. package/dist/lib/lanes/accounts.d.ts +6 -0
  19. package/dist/lib/lanes/accounts.js +2 -1
  20. package/dist/lib/lanes/config.d.ts +4 -0
  21. package/dist/lib/lanes/config.js +9 -0
  22. package/dist/lib/lanes/constants.d.ts +3 -0
  23. package/dist/lib/lanes/constants.js +3 -0
  24. package/dist/lib/lanes/index.d.ts +11 -3
  25. package/dist/lib/lanes/index.js +6 -1
  26. package/dist/lib/lanes/mesh-events.js +4 -0
  27. package/dist/lib/lanes/models.d.ts +78 -0
  28. package/dist/lib/lanes/models.js +205 -0
  29. package/dist/lib/lanes/move-account.d.ts +56 -0
  30. package/dist/lib/lanes/move-account.js +193 -0
  31. package/dist/lib/lanes/provider-rejection.d.ts +86 -0
  32. package/dist/lib/lanes/provider-rejection.js +258 -0
  33. package/dist/lib/lanes/quota.d.ts +23 -3
  34. package/dist/lib/lanes/quota.js +88 -14
  35. package/dist/lib/lanes/reconcile-install.d.ts +24 -3
  36. package/dist/lib/lanes/reconcile-install.js +48 -12
  37. package/dist/lib/lanes/reconcile.d.ts +5 -2
  38. package/dist/lib/lanes/reconcile.js +135 -13
  39. package/dist/lib/lanes/spawn.js +26 -5
  40. package/dist/lib/lanes/store.js +10 -0
  41. package/dist/lib/lanes/types.d.ts +51 -2
  42. package/dist/lib/lanes/types.js +5 -3
  43. package/dist/lib/lanes/watcher.d.ts +27 -0
  44. package/dist/lib/lanes/watcher.js +68 -5
  45. package/dist/lib/mesh/live/backfill-held.d.ts +4 -3
  46. package/dist/lib/mesh/live/backfill-held.js +37 -14
  47. package/dist/lib/mesh/live/daemon/doctor.d.ts +6 -0
  48. package/dist/lib/mesh/live/daemon/doctor.js +16 -2
  49. package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
  50. package/dist/lib/mesh/live/daemon/install.js +100 -7
  51. package/dist/lib/mesh/live/daemon/legacy-listen.d.ts +43 -0
  52. package/dist/lib/mesh/live/daemon/legacy-listen.js +89 -0
  53. package/dist/lib/mesh/live/daemon/live-cache.d.ts +26 -0
  54. package/dist/lib/mesh/live/daemon/live-cache.js +116 -0
  55. package/dist/lib/mesh/live/daemon/presence.d.ts +7 -3
  56. package/dist/lib/mesh/live/daemon/presence.js +10 -9
  57. package/dist/lib/mesh/live/daemon/run.d.ts +18 -0
  58. package/dist/lib/mesh/live/daemon/run.js +65 -6
  59. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +31 -0
  60. package/dist/lib/mesh/live/daemon/work-subscribe.js +124 -0
  61. package/dist/lib/mesh/live/emit-evidence.d.ts +19 -0
  62. package/dist/lib/mesh/live/emit-evidence.js +20 -2
  63. package/dist/lib/work-context/company.d.ts +16 -0
  64. package/dist/lib/work-context/company.js +46 -14
  65. package/dist/utils/self-update.js +4 -1
  66. package/package.json +2 -2
@@ -2,8 +2,9 @@
2
2
  * Library layer for `hq lanes`. The command module should import from here.
3
3
  */
4
4
  export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
5
+ export { MAX_LANE_DEPTH } from "./constants.js";
5
6
  export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
6
- export { DEFAULT_ACCOUNT_HOMES, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, writeLanesConfig, } from "./config.js";
7
+ export { DEFAULT_ACCOUNT_HOMES, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
7
8
  export { resolveLanesHqRoot } from "./hq-root.js";
8
9
  export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
9
10
  export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, updateLane, updateQuestion, } from "./store.js";
@@ -17,6 +18,10 @@ export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PRO
17
18
  export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
18
19
  export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
19
20
  export { RECONCILE_TIMER_INTERVAL, defaultSystemdUserDir, installReconcileTimer, providerPathEnv, renderReconcileService, renderReconcileTimer, uninstallReconcileTimer, } from "./reconcile-install.js";
21
+ export { unitPathEnv } from "./reconcile-install.js";
22
+ export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeResultRejection, parseCodexTurnFailed, parseGrokErrorOutput, parseProviderRejection, readProviderRejection, retryableFromStatus, } from "./provider-rejection.js";
23
+ export { CODEX_MODELS_CACHE_NAME, MODEL_PROBE_TIMEOUT_MS, claudeKnownModels, codexKnownModels, defaultProbe, grokKnownModels, knownModels, modelIsKnown, parseClaudeHelpModels, parseGrokModelsOutput, validateLaneModel, } from "./models.js";
24
+ export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
20
25
  export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
21
26
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
22
27
  export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
@@ -191,10 +191,14 @@ export function laneCanEmit(lane) {
191
191
  return lane.company !== null && lane.company !== undefined && !!lane.thread_id;
192
192
  }
193
193
  function baseIdentity(lane) {
194
+ const lastMove = lane.account_history?.[lane.account_history.length - 1];
194
195
  return {
195
196
  lane_id: lane.lane_id,
196
197
  project_id: lane.project_id,
197
198
  story_id: lane.story_id,
199
+ ...(lane.account !== undefined ? { account: lane.account } : {}),
200
+ // A moved lane names the move on every event its run emits (US-023 §8).
201
+ ...(lastMove !== undefined ? { account_move: { from: lastMove.from, to: lastMove.to, at: lastMove.at } } : {}),
198
202
  };
199
203
  }
200
204
  /**
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Per-provider known model ids, for validating `hq lanes create --model`.
3
+ *
4
+ * No provider adapter in this package carries a model list, so each id set
5
+ * is read from where the provider itself keeps it, the way the accounts
6
+ * probes read provider state: from the account home or the provider CLI,
7
+ * never from a list written into this package.
8
+ *
9
+ * - codex: `<CODEX_HOME>/models_cache.json`, the model catalog the codex CLI
10
+ * fetches and caches per home. Every `models[].slug` is accepted.
11
+ * - grok: `grok models` (run with `GROK_HOME` set to the account home),
12
+ * which prints `Available models:` followed by one id per line. It lists
13
+ * models even when the home is not logged in.
14
+ * - claude: `claude --help`, whose `--model` text names the accepted aliases
15
+ * (`'fable', 'opus', or 'sonnet'`) and says a full name is accepted too.
16
+ * An alias from that text or any `claude-` full name passes.
17
+ *
18
+ * A source that cannot be read (no cache yet, CLI missing, probe failed)
19
+ * returns null and the caller skips validation: the provider's own
20
+ * rejection at run time is still classified `provider_rejected`.
21
+ */
22
+ import type { LaneProvider } from "./types.js";
23
+ export declare const CODEX_MODELS_CACHE_NAME = "models_cache.json";
24
+ export declare const MODEL_PROBE_TIMEOUT_MS = 10000;
25
+ export interface ProbeResult {
26
+ status: number | null;
27
+ stdout: string;
28
+ stderr: string;
29
+ }
30
+ export type ProbeCommand = (command: string, args: string[], env: NodeJS.ProcessEnv) => ProbeResult;
31
+ export interface KnownModelsDeps {
32
+ readFileSync?: (p: string, encoding: "utf8") => string;
33
+ probe?: ProbeCommand;
34
+ env?: NodeJS.ProcessEnv;
35
+ }
36
+ export interface KnownModels {
37
+ provider: LaneProvider;
38
+ /** Exact ids the provider accepts. */
39
+ ids: string[];
40
+ /** Prefixes accepted in addition to `ids` (claude full names). */
41
+ prefixes: string[];
42
+ /** Where the list came from, for the refusal and the report. */
43
+ source: string;
44
+ }
45
+ export declare function defaultProbe(command: string, args: string[], env: NodeJS.ProcessEnv): ProbeResult;
46
+ /** Read `models[].slug` from the codex catalog cache in `home`. Null when absent or unreadable. */
47
+ export declare function codexKnownModels(home: string, readFileSync?: NonNullable<KnownModelsDeps["readFileSync"]>): KnownModels | null;
48
+ /**
49
+ * Parse `grok models` output:
50
+ *
51
+ * Available models:
52
+ * * grok-4.7 (default)
53
+ * - grok-4.7-build-fast
54
+ */
55
+ export declare function parseGrokModelsOutput(text: string): string[];
56
+ export declare function grokKnownModels(home: string, probe?: ProbeCommand, env?: NodeJS.ProcessEnv): KnownModels | null;
57
+ /**
58
+ * Parse the `--model` option text of `claude --help`:
59
+ *
60
+ * --model <model> Model for the current session. Provide an alias for
61
+ * the latest model (e.g. 'fable', 'opus', or 'sonnet')
62
+ * or a model's full name (e.g. 'claude-fable-5').
63
+ *
64
+ * Returns the quoted aliases and the prefix of the quoted full-name example.
65
+ */
66
+ export declare function parseClaudeHelpModels(text: string): {
67
+ aliases: string[];
68
+ prefixes: string[];
69
+ };
70
+ export declare function claudeKnownModels(probe?: ProbeCommand, env?: NodeJS.ProcessEnv): KnownModels | null;
71
+ export declare function knownModels(provider: LaneProvider, home: string, deps?: KnownModelsDeps): KnownModels | null;
72
+ export declare function modelIsKnown(known: KnownModels, model: string): boolean;
73
+ /**
74
+ * Refuse a `--model` the provider does not know. Returns the source used, or
75
+ * null when no source was readable and the value passed unvalidated.
76
+ */
77
+ export declare function validateLaneModel(provider: LaneProvider, model: string, home: string, deps?: KnownModelsDeps): string | null;
78
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Per-provider known model ids, for validating `hq lanes create --model`.
3
+ *
4
+ * No provider adapter in this package carries a model list, so each id set
5
+ * is read from where the provider itself keeps it, the way the accounts
6
+ * probes read provider state: from the account home or the provider CLI,
7
+ * never from a list written into this package.
8
+ *
9
+ * - codex: `<CODEX_HOME>/models_cache.json`, the model catalog the codex CLI
10
+ * fetches and caches per home. Every `models[].slug` is accepted.
11
+ * - grok: `grok models` (run with `GROK_HOME` set to the account home),
12
+ * which prints `Available models:` followed by one id per line. It lists
13
+ * models even when the home is not logged in.
14
+ * - claude: `claude --help`, whose `--model` text names the accepted aliases
15
+ * (`'fable', 'opus', or 'sonnet'`) and says a full name is accepted too.
16
+ * An alias from that text or any `claude-` full name passes.
17
+ *
18
+ * A source that cannot be read (no cache yet, CLI missing, probe failed)
19
+ * returns null and the caller skips validation: the provider's own
20
+ * rejection at run time is still classified `provider_rejected`.
21
+ */
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+ import { spawnSync } from "node:child_process";
25
+ import { LanesError } from "./errors.js";
26
+ import { PROVIDER_HOME_ENV } from "./accounts.js";
27
+ export const CODEX_MODELS_CACHE_NAME = "models_cache.json";
28
+ export const MODEL_PROBE_TIMEOUT_MS = 10_000;
29
+ export function defaultProbe(command, args, env) {
30
+ const result = spawnSync(command, args, {
31
+ env,
32
+ encoding: "utf8",
33
+ timeout: MODEL_PROBE_TIMEOUT_MS,
34
+ stdio: ["ignore", "pipe", "pipe"],
35
+ });
36
+ if (result.error) {
37
+ return { status: null, stdout: "", stderr: result.error.message };
38
+ }
39
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
40
+ }
41
+ function isPlainObject(value) {
42
+ return value !== null && typeof value === "object" && !Array.isArray(value);
43
+ }
44
+ /** Read `models[].slug` from the codex catalog cache in `home`. Null when absent or unreadable. */
45
+ export function codexKnownModels(home, readFileSync = (p, enc) => fs.readFileSync(p, enc)) {
46
+ const source = path.join(home, CODEX_MODELS_CACHE_NAME);
47
+ let text;
48
+ try {
49
+ text = readFileSync(source, "utf8");
50
+ }
51
+ catch (err) {
52
+ const code = err.code;
53
+ if (code === "ENOENT")
54
+ return null;
55
+ // EACCES, EISDIR, EIO: the list cannot be read, which is the same
56
+ // contract as a missing or corrupt cache. Reported, never an internal failure.
57
+ process.stderr.write(`hq lanes: cannot read ${source} (${err instanceof Error ? err.message : String(err)}); --model is not validated for codex\n`);
58
+ return null;
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(text);
63
+ }
64
+ catch (err) {
65
+ process.stderr.write(`hq lanes: ${source} is not JSON (${err instanceof Error ? err.message : String(err)}); --model is not validated for codex\n`);
66
+ return null;
67
+ }
68
+ if (!isPlainObject(parsed) || !Array.isArray(parsed.models))
69
+ return null;
70
+ const ids = [];
71
+ for (const entry of parsed.models) {
72
+ if (isPlainObject(entry) && typeof entry.slug === "string" && entry.slug.trim().length > 0) {
73
+ ids.push(entry.slug.trim());
74
+ }
75
+ }
76
+ if (ids.length === 0)
77
+ return null;
78
+ return { provider: "codex", ids, prefixes: [], source };
79
+ }
80
+ /**
81
+ * Parse `grok models` output:
82
+ *
83
+ * Available models:
84
+ * * grok-4.7 (default)
85
+ * - grok-4.7-build-fast
86
+ */
87
+ export function parseGrokModelsOutput(text) {
88
+ const ids = [];
89
+ let inList = false;
90
+ for (const raw of text.split("\n")) {
91
+ const line = raw.trim();
92
+ if (/^available models:/i.test(line)) {
93
+ inList = true;
94
+ continue;
95
+ }
96
+ if (!inList)
97
+ continue;
98
+ const match = /^[*-]\s+(\S+)/.exec(line);
99
+ if (match) {
100
+ ids.push(match[1]);
101
+ }
102
+ else if (line.length === 0 && ids.length > 0) {
103
+ break;
104
+ }
105
+ }
106
+ return ids;
107
+ }
108
+ export function grokKnownModels(home, probe = defaultProbe, env = process.env) {
109
+ const source = "grok models";
110
+ const result = probe("grok", ["models"], { ...env, [PROVIDER_HOME_ENV.grok]: home });
111
+ // Only a probe that exited 0 is a catalog. A wrapper or auth check can
112
+ // print a partial list and then fail; an id missing from that output must
113
+ // not refuse the model, so a failed probe skips validation.
114
+ if (result.status !== 0) {
115
+ process.stderr.write(`hq lanes: \`${source}\` failed (exit ${result.status ?? "spawn error"}${result.stderr.trim() ? `: ${result.stderr.trim().split("\n")[0]}` : ""}); --model is not validated for grok\n`);
116
+ return null;
117
+ }
118
+ const ids = parseGrokModelsOutput(result.stdout);
119
+ if (ids.length === 0) {
120
+ process.stderr.write(`hq lanes: \`${source}\` listed no models (exit ${result.status ?? "spawn error"}${result.stderr.trim() ? `: ${result.stderr.trim().split("\n")[0]}` : ""}); --model is not validated for grok\n`);
121
+ return null;
122
+ }
123
+ return { provider: "grok", ids, prefixes: [], source };
124
+ }
125
+ /**
126
+ * Parse the `--model` option text of `claude --help`:
127
+ *
128
+ * --model <model> Model for the current session. Provide an alias for
129
+ * the latest model (e.g. 'fable', 'opus', or 'sonnet')
130
+ * or a model's full name (e.g. 'claude-fable-5').
131
+ *
132
+ * Returns the quoted aliases and the prefix of the quoted full-name example.
133
+ */
134
+ export function parseClaudeHelpModels(text) {
135
+ const start = text.indexOf("--model");
136
+ if (start === -1)
137
+ return { aliases: [], prefixes: [] };
138
+ const rest = text.slice(start);
139
+ const end = rest.search(/\n\s*-{1,2}[A-Za-z]/);
140
+ const block = end === -1 ? rest : rest.slice(0, end);
141
+ const aliases = [];
142
+ const prefixes = new Set();
143
+ // Only quoted tokens shaped like an id: the possessive in "model's" also
144
+ // opens a quote, and must not be read as one.
145
+ for (const match of block.matchAll(/'([A-Za-z0-9][A-Za-z0-9._[\]-]*)'/g)) {
146
+ const value = match[1];
147
+ const dash = value.indexOf("-");
148
+ if (dash > 0) {
149
+ prefixes.add(value.slice(0, dash + 1));
150
+ }
151
+ else {
152
+ aliases.push(value);
153
+ }
154
+ }
155
+ return { aliases, prefixes: [...prefixes] };
156
+ }
157
+ export function claudeKnownModels(probe = defaultProbe, env = process.env) {
158
+ const source = "claude --help";
159
+ const result = probe("claude", ["--help"], env);
160
+ if (result.status !== 0) {
161
+ process.stderr.write(`hq lanes: \`${source}\` failed (exit ${result.status ?? "spawn error"}${result.stderr.trim() ? `: ${result.stderr.trim().split("\n")[0]}` : ""}); --model is not validated for claude\n`);
162
+ return null;
163
+ }
164
+ const parsed = parseClaudeHelpModels(result.stdout);
165
+ if (parsed.aliases.length === 0 && parsed.prefixes.length === 0) {
166
+ process.stderr.write(`hq lanes: \`${source}\` named no --model aliases (exit ${result.status ?? "spawn error"}); --model is not validated for claude\n`);
167
+ return null;
168
+ }
169
+ return { provider: "claude", ids: parsed.aliases, prefixes: parsed.prefixes, source };
170
+ }
171
+ export function knownModels(provider, home, deps = {}) {
172
+ switch (provider) {
173
+ case "codex":
174
+ return codexKnownModels(home, deps.readFileSync);
175
+ case "grok":
176
+ return grokKnownModels(home, deps.probe, deps.env);
177
+ case "claude":
178
+ return claudeKnownModels(deps.probe, deps.env);
179
+ default: {
180
+ const _exhaustive = provider;
181
+ return _exhaustive;
182
+ }
183
+ }
184
+ }
185
+ export function modelIsKnown(known, model) {
186
+ if (known.ids.includes(model))
187
+ return true;
188
+ return known.prefixes.some((prefix) => model.startsWith(prefix) && model.length > prefix.length);
189
+ }
190
+ /**
191
+ * Refuse a `--model` the provider does not know. Returns the source used, or
192
+ * null when no source was readable and the value passed unvalidated.
193
+ */
194
+ export function validateLaneModel(provider, model, home, deps = {}) {
195
+ const known = knownModels(provider, home, deps);
196
+ if (known === null)
197
+ return null;
198
+ if (modelIsKnown(known, model))
199
+ return known.source;
200
+ const accepted = [...known.ids, ...known.prefixes.map((prefix) => `${prefix}*`)];
201
+ const message = `Provider ${provider} does not know model ${JSON.stringify(model)} (source: ${known.source}). Accepted: ${accepted.join(", ")}`;
202
+ // `message` is in details too: LanesError.toJSON prints details only.
203
+ throw new LanesError("model_unsupported", message, { provider, model, accepted, source: known.source, message });
204
+ }
205
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * US-023: move a lane's provider session to another account of the same
3
+ * provider. A provider CLI only resumes sessions stored under its own home
4
+ * (`CLAUDE_CONFIG_DIR/projects/<encoded cwd>/<id>.jsonl`,
5
+ * `CODEX_HOME/sessions/YYYY/MM/DD/rollout-*-<id>.jsonl`), so the session
6
+ * files are copied into the target home first. The source copy is never
7
+ * moved or deleted, so the lane can be moved back. Only session transcripts
8
+ * are copied: a credential file name is refused by name, whatever path it
9
+ * arrives on.
10
+ */
11
+ import type { LaneProvider } from "./types.js";
12
+ export declare const SESSION_MOVE_REASON: "operator_move";
13
+ export interface SessionCopyPlan {
14
+ provider: LaneProvider;
15
+ /** Source files, absolute. */
16
+ sources: string[];
17
+ /** Target files, absolute, in the same order as `sources`. */
18
+ targets: string[];
19
+ /** Source directory copied as a tree (claude subagent transcripts), or null. */
20
+ source_dir: string | null;
21
+ target_dir: string | null;
22
+ }
23
+ export interface SessionCopyResult extends SessionCopyPlan {
24
+ /** Target paths that were written by this call (already-present ones are not listed). */
25
+ copied: string[];
26
+ }
27
+ /** `claude` keys a project directory by its cwd with every non-alphanumeric character as `-`. */
28
+ export declare function encodeClaudeProjectDir(cwd: string): string;
29
+ /**
30
+ * Locate the claude transcript `<sessionId>.jsonl` and its optional sibling
31
+ * `<sessionId>/` directory under `<home>/projects/<encoded cwd>/`.
32
+ */
33
+ export declare function planClaudeSessionCopy(sourceHome: string, targetHome: string, cwd: string, sessionId: string): SessionCopyPlan;
34
+ /** Walk `<home>/sessions/YYYY/MM/DD/` for `rollout-*-<sessionId>.jsonl`. */
35
+ export declare function findCodexRollout(home: string, sessionId: string): string | null;
36
+ export declare function planCodexSessionCopy(sourceHome: string, targetHome: string, sessionId: string): SessionCopyPlan;
37
+ export declare function planSessionCopy(provider: LaneProvider, input: {
38
+ sourceHome: string;
39
+ targetHome: string;
40
+ cwd: string;
41
+ sessionId: string;
42
+ }): SessionCopyPlan;
43
+ /** Every file a plan would write, transcript(s) first, then the subagent tree. */
44
+ export declare function planFiles(plan: SessionCopyPlan): Array<{
45
+ from: string;
46
+ to: string;
47
+ }>;
48
+ /**
49
+ * Execute a plan: copy the transcript(s) and the subagent tree into the
50
+ * target home. Every path is checked against the credential file names
51
+ * before the first byte is copied, so a refusal writes nothing. A file
52
+ * already present in the target with the same content is left alone; one
53
+ * that differs is replaced by the source copy. The source is never touched.
54
+ */
55
+ export declare function copySessionFiles(plan: SessionCopyPlan): SessionCopyResult;
56
+ //# sourceMappingURL=move-account.d.ts.map
@@ -0,0 +1,193 @@
1
+ /**
2
+ * US-023: move a lane's provider session to another account of the same
3
+ * provider. A provider CLI only resumes sessions stored under its own home
4
+ * (`CLAUDE_CONFIG_DIR/projects/<encoded cwd>/<id>.jsonl`,
5
+ * `CODEX_HOME/sessions/YYYY/MM/DD/rollout-*-<id>.jsonl`), so the session
6
+ * files are copied into the target home first. The source copy is never
7
+ * moved or deleted, so the lane can be moved back. Only session transcripts
8
+ * are copied: a credential file name is refused by name, whatever path it
9
+ * arrives on.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { PROVIDER_CREDENTIAL_FILES } from "./accounts.js";
14
+ import { LanesError } from "./errors.js";
15
+ export const SESSION_MOVE_REASON = "operator_move";
16
+ /** `claude` keys a project directory by its cwd with every non-alphanumeric character as `-`. */
17
+ export function encodeClaudeProjectDir(cwd) {
18
+ return cwd.replace(/[^A-Za-z0-9]/g, "-");
19
+ }
20
+ function credentialName(provider, file) {
21
+ const base = path.basename(file);
22
+ return Object.values(PROVIDER_CREDENTIAL_FILES).some((names) => names.includes(base)) ||
23
+ PROVIDER_CREDENTIAL_FILES[provider].includes(base);
24
+ }
25
+ function isDirectory(p) {
26
+ try {
27
+ return fs.statSync(p).isDirectory();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function isFile(p) {
34
+ try {
35
+ return fs.statSync(p).isFile();
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ function sessionMissing(provider, sessionId, searched) {
42
+ return new LanesError("provider_session_missing", `No ${provider} session ${sessionId} under the source account home. Searched: ${searched.join(", ")}`, { provider, session_id: sessionId, searched });
43
+ }
44
+ /**
45
+ * Locate the claude transcript `<sessionId>.jsonl` and its optional sibling
46
+ * `<sessionId>/` directory under `<home>/projects/<encoded cwd>/`.
47
+ */
48
+ export function planClaudeSessionCopy(sourceHome, targetHome, cwd, sessionId) {
49
+ const rel = path.join("projects", encodeClaudeProjectDir(cwd));
50
+ const transcript = path.join(sourceHome, rel, `${sessionId}.jsonl`);
51
+ if (!isFile(transcript))
52
+ throw sessionMissing("claude", sessionId, [transcript]);
53
+ const subagents = path.join(sourceHome, rel, sessionId);
54
+ const hasSubagents = isDirectory(subagents);
55
+ return {
56
+ provider: "claude",
57
+ sources: [transcript],
58
+ targets: [path.join(targetHome, rel, `${sessionId}.jsonl`)],
59
+ source_dir: hasSubagents ? subagents : null,
60
+ target_dir: hasSubagents ? path.join(targetHome, rel, sessionId) : null,
61
+ };
62
+ }
63
+ /** Walk `<home>/sessions/YYYY/MM/DD/` for `rollout-*-<sessionId>.jsonl`. */
64
+ export function findCodexRollout(home, sessionId) {
65
+ const root = path.join(home, "sessions");
66
+ if (!isDirectory(root))
67
+ return null;
68
+ const suffix = `-${sessionId}.jsonl`;
69
+ const stack = [{ dir: root, depth: 0 }];
70
+ while (stack.length > 0) {
71
+ const { dir, depth } = stack.pop();
72
+ let entries;
73
+ try {
74
+ entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ for (const entry of entries) {
80
+ const full = path.join(dir, entry.name);
81
+ if (entry.isDirectory()) {
82
+ if (depth < 3)
83
+ stack.push({ dir: full, depth: depth + 1 });
84
+ }
85
+ else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix)) {
86
+ return full;
87
+ }
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ export function planCodexSessionCopy(sourceHome, targetHome, sessionId) {
93
+ const rollout = findCodexRollout(sourceHome, sessionId);
94
+ if (rollout === null) {
95
+ throw sessionMissing("codex", sessionId, [path.join(sourceHome, "sessions", "**", `rollout-*-${sessionId}.jsonl`)]);
96
+ }
97
+ const rel = path.relative(sourceHome, rollout);
98
+ return {
99
+ provider: "codex",
100
+ sources: [rollout],
101
+ targets: [path.join(targetHome, rel)],
102
+ source_dir: null,
103
+ target_dir: null,
104
+ };
105
+ }
106
+ export function planSessionCopy(provider, input) {
107
+ switch (provider) {
108
+ case "claude":
109
+ return planClaudeSessionCopy(input.sourceHome, input.targetHome, input.cwd, input.sessionId);
110
+ case "codex":
111
+ return planCodexSessionCopy(input.sourceHome, input.targetHome, input.sessionId);
112
+ case "grok":
113
+ throw new LanesError("provider_session_not_portable", "Provider grok has no session resume, so a lane cannot move between grok accounts", { provider });
114
+ default: {
115
+ const _exhaustive = provider;
116
+ return _exhaustive;
117
+ }
118
+ }
119
+ }
120
+ function refuseCredential(provider, file) {
121
+ if (credentialName(provider, file)) {
122
+ throw new LanesError("credential_copy_refused", `Refusing to copy ${path.basename(file)}: only session transcripts move between accounts`, { provider, file: path.basename(file) });
123
+ }
124
+ }
125
+ function sameContent(a, b) {
126
+ try {
127
+ const sa = fs.statSync(a);
128
+ const sb = fs.statSync(b);
129
+ if (sa.size !== sb.size)
130
+ return false;
131
+ return fs.readFileSync(a).equals(fs.readFileSync(b));
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ /**
138
+ * Copy one file via a temp name and rename, so a partial copy is never a
139
+ * visible transcript. A target that already exists is refreshed when its
140
+ * content differs: the lane last ran on the source account, so the source
141
+ * copy is the newer one (a lane moved A→B and back to A must not resume A's
142
+ * pre-move transcript). Returns false when the target already matched.
143
+ */
144
+ function copyFileAtomic(source, target) {
145
+ if (fs.existsSync(target) && sameContent(source, target))
146
+ return false;
147
+ fs.mkdirSync(path.dirname(target), { recursive: true });
148
+ const tmp = `${target}.partial.${process.pid}`;
149
+ fs.copyFileSync(source, tmp);
150
+ fs.renameSync(tmp, target);
151
+ return true;
152
+ }
153
+ function listTree(sourceDir, targetDir, out) {
154
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
155
+ const from = path.join(sourceDir, entry.name);
156
+ const to = path.join(targetDir, entry.name);
157
+ if (entry.isDirectory()) {
158
+ listTree(from, to, out);
159
+ }
160
+ else if (entry.isFile()) {
161
+ out.push({ from, to });
162
+ }
163
+ }
164
+ }
165
+ /** Every file a plan would write, transcript(s) first, then the subagent tree. */
166
+ export function planFiles(plan) {
167
+ const files = plan.sources.map((from, i) => ({ from, to: plan.targets[i] }));
168
+ if (plan.source_dir !== null && plan.target_dir !== null) {
169
+ listTree(plan.source_dir, plan.target_dir, files);
170
+ }
171
+ return files;
172
+ }
173
+ /**
174
+ * Execute a plan: copy the transcript(s) and the subagent tree into the
175
+ * target home. Every path is checked against the credential file names
176
+ * before the first byte is copied, so a refusal writes nothing. A file
177
+ * already present in the target with the same content is left alone; one
178
+ * that differs is replaced by the source copy. The source is never touched.
179
+ */
180
+ export function copySessionFiles(plan) {
181
+ const files = planFiles(plan);
182
+ for (const file of files) {
183
+ refuseCredential(plan.provider, file.from);
184
+ refuseCredential(plan.provider, file.to);
185
+ }
186
+ const copied = [];
187
+ for (const file of files) {
188
+ if (copyFileAtomic(file.from, file.to))
189
+ copied.push(file.to);
190
+ }
191
+ return { ...plan, copied };
192
+ }
193
+ //# sourceMappingURL=move-account.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Provider-level turn failures read off the worker's own stdout.
3
+ *
4
+ * A worker whose turn the provider refused (a 400 for a model the account
5
+ * cannot use, a 402 for an exhausted balance, a 5xx) exits with partial
6
+ * output and no envelope. Without this module the watcher classifies that
7
+ * `interrupted` with `exited_with_partial_output`, and the operator has to
8
+ * read the raw log to learn why. Here the failure is named: the lane is
9
+ * `failed` with `terminal_reason: provider_rejected` and the provider's own
10
+ * message on `terminal_details`.
11
+ *
12
+ * A 429 is parsed here like any other status so that a codex or grok 429,
13
+ * which the claude-shaped quota reader cannot see, still reaches the quota
14
+ * path: the watcher routes a `status === QUOTA_STATUS` failure to quota.ts
15
+ * (reset time, account hold) and everything else to `provider_rejected`.
16
+ *
17
+ * Only the fields named below are read out of the payload. The prompt, the
18
+ * response body and everything else stay on disk. Nothing here logs.
19
+ */
20
+ import type { LaneProvider } from "./types.js";
21
+ export declare const PROVIDER_REJECTED_REASON: "provider_rejected";
22
+ /** Bytes of the provider message kept on the record. The log keeps the rest. */
23
+ export declare const PROVIDER_REJECTION_MESSAGE_CAP = 2000;
24
+ /** Status the watcher routes to the quota path instead of `provider_rejected`. */
25
+ export declare const QUOTA_STATUS = 429;
26
+ export interface ProviderRejection {
27
+ reason: typeof PROVIDER_REJECTED_REASON;
28
+ provider: LaneProvider;
29
+ /** The provider's error message, verbatim, capped at PROVIDER_REJECTION_MESSAGE_CAP. */
30
+ message: string;
31
+ /** HTTP status the provider reported; null when the payload named none. */
32
+ status: number | null;
33
+ /** Provider error class, e.g. `invalid_request_error`; null when absent. */
34
+ error_type: string | null;
35
+ /** Derived from the status: a transient class (408, 425, 5xx) is retryable; a 4xx is not. */
36
+ retryable: boolean;
37
+ /** Which event on the stream carried the failure. */
38
+ event: "codex.turn.failed" | "claude.result.is_error" | "grok.error";
39
+ }
40
+ export declare function capMessage(message: string): string;
41
+ /**
42
+ * Whether a later run could succeed without the operator changing anything.
43
+ * A 400 model rejection cannot; a request timeout, a too-early, or a server
44
+ * error can. An unknown status is not retryable: guessing "try again" on a
45
+ * failure we cannot classify would loop the lane.
46
+ */
47
+ export declare function retryableFromStatus(status: number | null): boolean;
48
+ /**
49
+ * Codex `exec --json`: `{"type":"turn.failed","error":{"message":"<text>"}}`.
50
+ * Measured 2026-09-22 (lane 01M34HCCP1ZDN3H68ESCRZE7RZ): the message is
51
+ * itself a JSON string, `{"type":"error","status":400,"error":{"type":
52
+ * "invalid_request_error","message":"The 'terra' model is not supported when
53
+ * using Codex with a ChatGPT account."}}`. The inner message and status are
54
+ * read when it parses; otherwise the outer text is the message.
55
+ */
56
+ export declare function parseCodexTurnFailed(text: string): ProviderRejection | null;
57
+ /**
58
+ * Claude `-p --output-format json`: the last `type: "result"` object with
59
+ * `is_error: true` and a numeric `api_error_status`. A result that is an
60
+ * error with no status is not a provider rejection: it is either the quota
61
+ * text shape quota.ts reads, or the worker's own failure, and keeps the
62
+ * existing classification.
63
+ */
64
+ export declare function parseClaudeResultRejection(text: string): ProviderRejection | null;
65
+ /**
66
+ * Grok `--output-format json` prints one object. Measured 2026-09-22 (lane
67
+ * 01M33X586J18J1B6GK0E6M4Y07): `{"type":"error","message":"Internal error:
68
+ * {\n \"message\": \"API error (status 402 Payment Required): Grok Build
69
+ * usage balance exhausted\",\n \"http_status\": 402, ...}", ...}`. The
70
+ * inner object carries `message` and `http_status`; when it does not parse,
71
+ * the status is read from `(status NNN` in the text.
72
+ */
73
+ export declare function parseGrokErrorOutput(text: string): ProviderRejection | null;
74
+ /** The `terminal_details` record both the watcher and the reconcile pass write for a rejection. */
75
+ export declare function providerRejectionDetails(rejection: ProviderRejection, lane: {
76
+ model?: string;
77
+ account?: string;
78
+ }): Record<string, unknown>;
79
+ export declare function parseProviderRejection(provider: LaneProvider, text: string): ProviderRejection | null;
80
+ /**
81
+ * Read this run's bytes of `logs/worker.out` (past `workerOutOffset`, the
82
+ * same cut delivery.ts uses) and return the provider rejection they carry,
83
+ * or null. A missing file is null; any other read error propagates.
84
+ */
85
+ export declare function readProviderRejection(runDir: string, provider: LaneProvider, workerOutOffset?: number): ProviderRejection | null;
86
+ //# sourceMappingURL=provider-rejection.d.ts.map