@indigoai-us/hq-cli 5.140.0 → 5.142.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.
@@ -69,7 +69,7 @@ import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOut
69
69
  import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
70
70
  import { isRuntimeSignInFailure, SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
71
71
  import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
72
- import { captureHeartbeatIdentityMismatch, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
72
+ import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
73
73
  import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
74
74
  import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
75
75
  export const INBOX_POLL_INTERVAL_MS = 2_000;
@@ -459,7 +459,7 @@ export async function runBot(deps) {
459
459
  if (err instanceof BotApiError &&
460
460
  isHeartbeatIdentityMismatch({ status: err.status, code: err.code, message: err.detail })) {
461
461
  const tokenLabel = err.tokenAgentUid ?? "unknown";
462
- const reason = `heartbeat stopped: identity mismatch configured agentUid=${config.agentUid} token agentUid=${tokenLabel}`;
462
+ const reason = identityMismatchMessage("heartbeat", config.agentUid, tokenLabel);
463
463
  setStatus({
464
464
  lastHeartbeatOk: false,
465
465
  lastHeartbeatError: reason.slice(0, 400),
@@ -1,5 +1,6 @@
1
1
  import type { CaptureContext } from "@sentry/node";
2
2
  export declare const HEARTBEAT_IDENTITY_MISMATCH_CODE = "AGENT_IDENTITY_MISMATCH";
3
+ export declare function identityMismatchMessage(surface: "heartbeat" | "bot jobs", configuredAgentUid: string, tokenAgentUid?: string): string;
3
4
  export type HeartbeatRunner = "agent-kit" | "bot";
4
5
  export type HeartbeatAgentUidSource = "config_file" | "env" | "flag";
5
6
  export type HeartbeatCredsSource = "explicit" | "default";
@@ -2,6 +2,11 @@ import { CLI_VERSION } from "../cli-version.js";
2
2
  import { Sentry } from "../sentry.js";
3
3
  import { peekIdToken } from "../utils/id-token.js";
4
4
  export const HEARTBEAT_IDENTITY_MISMATCH_CODE = "AGENT_IDENTITY_MISMATCH";
5
+ export function identityMismatchMessage(surface, configuredAgentUid, tokenAgentUid) {
6
+ return (`${surface} stopped: identity mismatch configured agentUid=${configuredAgentUid} ` +
7
+ `token agentUid=${tokenAgentUid ?? "unknown"}; fix the bot config to use the token agentUid, ` +
8
+ `or re-mint credentials for the configured agentUid.`);
9
+ }
5
10
  const AGENT_UID_RE = /^agt_[A-Za-z0-9_-]{1,100}$/;
6
11
  function normalize(value) {
7
12
  return (value ?? "").replace(/[^A-Za-z0-9]/g, "").toLowerCase();
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import type { LaneProvider } from "./types.js";
21
21
  export declare const PROVIDER_EFFORT_LEVELS: Record<LaneProvider, readonly string[]>;
22
- export declare const EFFORT_SOURCES: readonly ["cli", "account", "provider", "default_rule", "none"];
22
+ export declare const EFFORT_SOURCES: readonly ["cli", "profile", "account", "provider", "default_rule", "none"];
23
23
  export type EffortSource = (typeof EFFORT_SOURCES)[number];
24
24
  export interface ResolvedEffort {
25
25
  effort?: string;
@@ -35,9 +35,10 @@ export declare function parseConfigEffort(provider: LaneProvider, raw: unknown,
35
35
  * Keyed on the model already chosen for the lane; it does not pick a model.
36
36
  */
37
37
  export declare function defaultEffortForModel(model: string | undefined): string | undefined;
38
- /** Precedence: `--effort`, then the account, then the provider, then the default rule. */
38
+ /** Precedence: `--effort`, profile, account, provider, then the default rule. */
39
39
  export declare function resolveLaneEffort(input: {
40
40
  cliEffort?: string;
41
+ profileEffort?: string;
41
42
  accountEffort?: string;
42
43
  providerEffort?: string;
43
44
  model?: string;
@@ -25,6 +25,7 @@ export const PROVIDER_EFFORT_LEVELS = {
25
25
  };
26
26
  export const EFFORT_SOURCES = [
27
27
  "cli",
28
+ "profile",
28
29
  "account",
29
30
  "provider",
30
31
  "default_rule",
@@ -65,10 +66,13 @@ export function defaultEffortForModel(model) {
65
66
  }
66
67
  return undefined;
67
68
  }
68
- /** Precedence: `--effort`, then the account, then the provider, then the default rule. */
69
+ /** Precedence: `--effort`, profile, account, provider, then the default rule. */
69
70
  export function resolveLaneEffort(input) {
70
71
  if (input.cliEffort !== undefined)
71
72
  return { effort: input.cliEffort, source: "cli" };
73
+ if (input.profileEffort !== undefined) {
74
+ return { effort: input.profileEffort, source: "profile" };
75
+ }
72
76
  if (input.accountEffort !== undefined) {
73
77
  return { effort: input.accountEffort, source: "account" };
74
78
  }
@@ -34,6 +34,6 @@ export declare class AccountResolutionError extends LanesError {
34
34
  constructor(error: "unknown_account" | "account_disabled" | "no_enabled_account" | "account_model_unavailable" | "account_quota_exhausted", message: string, details?: Record<string, unknown>);
35
35
  }
36
36
  export declare class WorkerResolutionError extends LanesError {
37
- constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "package_workers_unmounted" | "worker_registry_unreadable", message: string, details?: Record<string, unknown>);
37
+ constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "worker_profile_invalid" | "worker_profile_ambiguous" | "package_workers_unmounted" | "worker_registry_unreadable", message: string, details?: Record<string, unknown>);
38
38
  }
39
39
  //# sourceMappingURL=errors.d.ts.map
@@ -45,6 +45,8 @@ export type { KnownModels, KnownModelsDeps, ProbeCommand, ProbeResult } from "./
45
45
  export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
46
46
  export type { SessionCopyPlan, SessionCopyResult } from "./move-account.js";
47
47
  export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
48
+ export { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, personalWorkerProfileDir, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
49
+ export type { WorkerProfile, WorkerProfileMeta, WorkerProfileRuntime, } from "./profile.js";
48
50
  export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
49
51
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
50
52
  export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
@@ -25,6 +25,7 @@ export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeRe
25
25
  export { CODEX_MODELS_CACHE_NAME, MODEL_PROBE_TIMEOUT_MS, claudeKnownModels, codexKnownModels, defaultProbe, grokKnownModels, knownModels, modelIsKnown, parseClaudeHelpModels, parseGrokModelsOutput, validateLaneModel, } from "./models.js";
26
26
  export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
27
27
  export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
28
+ export { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, personalWorkerProfileDir, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
28
29
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
29
30
  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";
30
31
  export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
@@ -80,11 +80,10 @@ export interface CreateLaneMeshOptions {
80
80
  token?: () => Promise<string>;
81
81
  }
82
82
  export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
83
- /** Company + thread present. A core/personal lane (company null) emits nothing. */
84
- export declare function laneCanEmit(lane: Pick<LaneRecord, "company" | "thread_id">): lane is Pick<LaneRecord, "company" | "thread_id"> & {
85
- company: LaneCompanyRef;
86
- thread_id: string;
87
- };
83
+ /** Resolve the explicit mesh attribution before falling back to the lane tenant. */
84
+ export declare function laneMeshCompany(lane: Pick<LaneRecord, "company" | "mesh_company">): LaneCompanyRef | undefined;
85
+ /** Company attribution + thread present. Unscoped personal lanes emit nothing. */
86
+ export declare function laneCanEmit(lane: Pick<LaneRecord, "company" | "mesh_company" | "thread_id">): boolean;
88
87
  /**
89
88
  * Spec §8 terminal-envelope events. `done` → lane_done and story → review;
90
89
  * `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
@@ -186,9 +186,13 @@ export function createLaneMesh(opts = {}) {
186
186
  },
187
187
  };
188
188
  }
189
- /** Company + thread present. A core/personal lane (company null) emits nothing. */
189
+ /** Resolve the explicit mesh attribution before falling back to the lane tenant. */
190
+ export function laneMeshCompany(lane) {
191
+ return lane.mesh_company ?? lane.company ?? undefined;
192
+ }
193
+ /** Company attribution + thread present. Unscoped personal lanes emit nothing. */
190
194
  export function laneCanEmit(lane) {
191
- return lane.company !== null && lane.company !== undefined && !!lane.thread_id;
195
+ return laneMeshCompany(lane) !== undefined && !!lane.thread_id;
192
196
  }
193
197
  function baseIdentity(lane) {
194
198
  const lastMove = lane.account_history?.[lane.account_history.length - 1];
@@ -209,8 +213,10 @@ function baseIdentity(lane) {
209
213
  export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
210
214
  if (!laneCanEmit(lane))
211
215
  return;
212
- const company = lane.company;
216
+ const company = laneMeshCompany(lane);
213
217
  const threadId = lane.thread_id;
218
+ if (!company || !threadId)
219
+ return;
214
220
  switch (envelope.decision) {
215
221
  case "done": {
216
222
  const outcomes = await Promise.allSettled([
@@ -260,7 +266,11 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
260
266
  export async function emitLaneProgressEvent(mesh, lane, detail, signal) {
261
267
  if (!laneCanEmit(lane))
262
268
  return;
263
- await mesh.appendThreadEvent(lane.company, lane.thread_id, "lane_progress", {
269
+ const company = laneMeshCompany(lane);
270
+ const threadId = lane.thread_id;
271
+ if (!company || !threadId)
272
+ return;
273
+ await mesh.appendThreadEvent(company, threadId, "lane_progress", {
264
274
  ...baseIdentity(lane),
265
275
  detail,
266
276
  }, signal);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Reusable lane worker profiles stored under personal/workers/<id>.
3
+ *
4
+ * A profile is deliberately independent from the worker registry. The
5
+ * registry describes a worker persona that can be resolved from the company
6
+ * and package trees; this module describes the provider launch contract that
7
+ * must be re-read for every spawn.
8
+ */
9
+ import { type LaneProvider } from "./types.js";
10
+ export interface WorkerProfileMeta {
11
+ version?: string;
12
+ provider: LaneProvider;
13
+ model: string;
14
+ account: string;
15
+ effort: string;
16
+ fast_mode: boolean;
17
+ max_concurrent?: number;
18
+ source?: string;
19
+ accounts?: string[];
20
+ }
21
+ export interface WorkerProfile {
22
+ id: string;
23
+ dir: string;
24
+ meta: WorkerProfileMeta;
25
+ systemPrompt: string;
26
+ settings?: Record<string, unknown>;
27
+ policyFiles: string[];
28
+ hookFiles: string[];
29
+ contentHash: string;
30
+ }
31
+ export interface WorkerProfileRuntime {
32
+ settingsPath?: string;
33
+ hooksDir?: string;
34
+ }
35
+ export declare function personalWorkerProfileDir(hqRoot: string, id: string): string;
36
+ /** A meta.yaml file is the explicit marker for a personal worker profile. */
37
+ export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
38
+ export declare function loadWorkerProfile(hqRoot: string, id: string): WorkerProfile;
39
+ export declare function workerProfilePrompt(profile: WorkerProfile): string;
40
+ export declare function ignoredWorkerProfileFields(provider: LaneProvider): string[];
41
+ /**
42
+ * Materialize provider settings and hook files in the lane run directory.
43
+ * The path is intentionally run-local so every fresh, resumed, moved, and
44
+ * repair spawn gets the profile currently on disk without mutating an account
45
+ * home or a shared provider configuration.
46
+ */
47
+ export declare function materializeWorkerProfile(profile: WorkerProfile, runDir: string): WorkerProfileRuntime;
48
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Reusable lane worker profiles stored under personal/workers/<id>.
3
+ *
4
+ * A profile is deliberately independent from the worker registry. The
5
+ * registry describes a worker persona that can be resolved from the company
6
+ * and package trees; this module describes the provider launch contract that
7
+ * must be re-read for every spawn.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { parseUserYaml } from "../../utils/user-yaml-error.js";
13
+ import { WorkerResolutionError } from "./errors.js";
14
+ import { PROVIDER_EFFORT_LEVELS } from "./effort.js";
15
+ import { LANE_PROVIDERS, isLaneProvider, } from "./types.js";
16
+ const PROFILE_META = "meta.yaml";
17
+ const PROFILE_SYSTEM_PROMPT = "system-prompt.md";
18
+ const PROFILE_SETTINGS = "settings.json";
19
+ const PROFILE_POLICIES = "policies";
20
+ const PROFILE_HOOKS = "hooks";
21
+ const PROFILE_META_KEYS = new Set([
22
+ "version",
23
+ "provider",
24
+ "model",
25
+ "account",
26
+ "effort",
27
+ "fast_mode",
28
+ "max_concurrent",
29
+ "source",
30
+ "accounts",
31
+ ]);
32
+ function isPlainObject(value) {
33
+ return value !== null && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+ function profileError(id, dir, message, details = {}) {
36
+ return new WorkerResolutionError("worker_profile_invalid", `Worker profile ${JSON.stringify(id)} at ${dir}: ${message}`, { worker: id, path: dir, ...details });
37
+ }
38
+ function validProfileId(id) {
39
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id);
40
+ }
41
+ function lstatOrAbsent(filePath) {
42
+ try {
43
+ return fs.lstatSync(filePath);
44
+ }
45
+ catch (err) {
46
+ const code = err.code;
47
+ if (code === "ENOENT")
48
+ return null;
49
+ throw err;
50
+ }
51
+ }
52
+ function requireRegularFile(id, dir, filePath, field) {
53
+ let stat;
54
+ try {
55
+ stat = fs.lstatSync(filePath);
56
+ }
57
+ catch (err) {
58
+ throw profileError(id, dir, `cannot read ${field}: ${err instanceof Error ? err.message : String(err)}`, { field, file: filePath });
59
+ }
60
+ if (stat.isSymbolicLink() || !stat.isFile()) {
61
+ throw profileError(id, dir, `${field} must be a regular file`, {
62
+ field,
63
+ file: filePath,
64
+ });
65
+ }
66
+ return stat;
67
+ }
68
+ function readRequiredText(id, dir, filePath, field) {
69
+ requireRegularFile(id, dir, filePath, field);
70
+ try {
71
+ return fs.readFileSync(filePath, "utf8");
72
+ }
73
+ catch (err) {
74
+ throw profileError(id, dir, `cannot read ${field}: ${err instanceof Error ? err.message : String(err)}`, { field, file: filePath });
75
+ }
76
+ }
77
+ function collectFiles(id, dir, relativeDir) {
78
+ const absoluteDir = path.join(dir, relativeDir);
79
+ const stat = lstatOrAbsent(absoluteDir);
80
+ if (stat === null)
81
+ return [];
82
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
83
+ throw profileError(id, dir, `${relativeDir} must be a directory`, {
84
+ field: relativeDir,
85
+ });
86
+ }
87
+ const files = [];
88
+ const visit = (current, currentRelative) => {
89
+ let entries;
90
+ try {
91
+ entries = fs.readdirSync(current, { withFileTypes: true });
92
+ }
93
+ catch (err) {
94
+ throw profileError(id, dir, `cannot read ${currentRelative}: ${err instanceof Error ? err.message : String(err)}`, { field: currentRelative });
95
+ }
96
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
97
+ const entryRelative = path.posix.join(currentRelative, entry.name);
98
+ const entryPath = path.join(dir, entryRelative);
99
+ if (entry.isSymbolicLink()) {
100
+ throw profileError(id, dir, `${entryRelative} must not be a symlink`, {
101
+ field: entryRelative,
102
+ });
103
+ }
104
+ if (entry.isDirectory()) {
105
+ visit(entryPath, entryRelative);
106
+ continue;
107
+ }
108
+ if (!entry.isFile()) {
109
+ throw profileError(id, dir, `${entryRelative} must be a regular file`, {
110
+ field: entryRelative,
111
+ });
112
+ }
113
+ files.push(entryRelative);
114
+ }
115
+ };
116
+ visit(absoluteDir, relativeDir);
117
+ return files.sort((a, b) => a.localeCompare(b));
118
+ }
119
+ function parseMeta(id, dir, raw) {
120
+ if (!isPlainObject(raw)) {
121
+ throw profileError(id, dir, "meta.yaml must contain an object");
122
+ }
123
+ const unknown = Object.keys(raw).filter((key) => !PROFILE_META_KEYS.has(key));
124
+ if (unknown.length > 0) {
125
+ throw profileError(id, dir, `meta.yaml has unknown field(s): ${unknown.join(", ")}`, {
126
+ fields: unknown,
127
+ });
128
+ }
129
+ const provider = raw.provider;
130
+ if (typeof provider !== "string" || !isLaneProvider(provider.trim())) {
131
+ throw profileError(id, dir, "meta.yaml provider must be claude, codex, or grok", {
132
+ field: "provider",
133
+ value: provider,
134
+ accepted: [...LANE_PROVIDERS],
135
+ });
136
+ }
137
+ const providerValue = provider.trim();
138
+ const requiredString = (field) => {
139
+ const value = raw[field];
140
+ if (typeof value !== "string" || value.trim().length === 0) {
141
+ throw profileError(id, dir, `meta.yaml ${field} must be a non-empty string`, {
142
+ field,
143
+ value,
144
+ });
145
+ }
146
+ return value.trim();
147
+ };
148
+ const model = requiredString("model");
149
+ const account = requiredString("account");
150
+ const effort = requiredString("effort");
151
+ if (!PROVIDER_EFFORT_LEVELS[providerValue].includes(effort)) {
152
+ throw profileError(id, dir, `meta.yaml effort ${JSON.stringify(effort)} is invalid for ${providerValue}`, {
153
+ field: "effort",
154
+ provider: providerValue,
155
+ value: effort,
156
+ accepted: [...PROVIDER_EFFORT_LEVELS[providerValue]],
157
+ });
158
+ }
159
+ if (typeof raw.fast_mode !== "boolean") {
160
+ throw profileError(id, dir, "meta.yaml fast_mode must be a boolean", {
161
+ field: "fast_mode",
162
+ value: raw.fast_mode,
163
+ });
164
+ }
165
+ let version;
166
+ if (raw.version !== undefined) {
167
+ if (typeof raw.version !== "string" || raw.version.trim().length === 0) {
168
+ throw profileError(id, dir, "meta.yaml version must be a non-empty string", {
169
+ field: "version",
170
+ value: raw.version,
171
+ });
172
+ }
173
+ version = raw.version.trim();
174
+ }
175
+ let maxConcurrent;
176
+ if (raw.max_concurrent !== undefined) {
177
+ if (typeof raw.max_concurrent !== "number" || !Number.isInteger(raw.max_concurrent) || raw.max_concurrent < 1) {
178
+ throw profileError(id, dir, "meta.yaml max_concurrent must be a positive integer", {
179
+ field: "max_concurrent",
180
+ value: raw.max_concurrent,
181
+ });
182
+ }
183
+ maxConcurrent = raw.max_concurrent;
184
+ }
185
+ let source;
186
+ if (raw.source !== undefined) {
187
+ if (typeof raw.source !== "string" || raw.source.trim().length === 0) {
188
+ throw profileError(id, dir, "meta.yaml source must be a non-empty string when present", {
189
+ field: "source",
190
+ value: raw.source,
191
+ });
192
+ }
193
+ source = raw.source.trim();
194
+ }
195
+ let accounts;
196
+ if (raw.accounts !== undefined) {
197
+ if (!Array.isArray(raw.accounts) || raw.accounts.length === 0 || raw.accounts.some((item) => typeof item !== "string" || item.trim().length === 0)) {
198
+ throw profileError(id, dir, "meta.yaml accounts must be a non-empty string array when present", {
199
+ field: "accounts",
200
+ value: raw.accounts,
201
+ });
202
+ }
203
+ accounts = raw.accounts.map((item) => item.trim());
204
+ }
205
+ return {
206
+ ...(version !== undefined ? { version } : {}),
207
+ provider: providerValue,
208
+ model,
209
+ account,
210
+ effort,
211
+ fast_mode: raw.fast_mode,
212
+ ...(maxConcurrent !== undefined ? { max_concurrent: maxConcurrent } : {}),
213
+ ...(source !== undefined ? { source } : {}),
214
+ ...(accounts !== undefined ? { accounts } : {}),
215
+ };
216
+ }
217
+ function readSettings(id, dir) {
218
+ const filePath = path.join(dir, PROFILE_SETTINGS);
219
+ const stat = lstatOrAbsent(filePath);
220
+ if (stat === null)
221
+ return {};
222
+ requireRegularFile(id, dir, filePath, PROFILE_SETTINGS);
223
+ let parsed;
224
+ try {
225
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
226
+ }
227
+ catch (err) {
228
+ throw profileError(id, dir, `settings.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, {
229
+ field: PROFILE_SETTINGS,
230
+ });
231
+ }
232
+ if (!isPlainObject(parsed)) {
233
+ throw profileError(id, dir, "settings.json must contain a JSON object", {
234
+ field: PROFILE_SETTINGS,
235
+ });
236
+ }
237
+ return { settings: parsed, relative: PROFILE_SETTINGS };
238
+ }
239
+ function hashProfileFiles(dir, relativeFiles) {
240
+ const hash = createHash("sha256");
241
+ for (const relative of relativeFiles.sort((a, b) => a.localeCompare(b))) {
242
+ const file = path.join(dir, relative);
243
+ hash.update(relative);
244
+ hash.update("\0");
245
+ hash.update(fs.readFileSync(file));
246
+ hash.update("\0");
247
+ }
248
+ return `sha256:${hash.digest("hex")}`;
249
+ }
250
+ export function personalWorkerProfileDir(hqRoot, id) {
251
+ return path.join(hqRoot, "personal", "workers", id);
252
+ }
253
+ /** A meta.yaml file is the explicit marker for a personal worker profile. */
254
+ export function workerProfileMarker(hqRoot, id) {
255
+ if (!validProfileId(id))
256
+ return "absent";
257
+ const dirStat = lstatOrAbsent(personalWorkerProfileDir(hqRoot, id));
258
+ if (dirStat === null || dirStat.isSymbolicLink() || !dirStat.isDirectory()) {
259
+ return "absent";
260
+ }
261
+ const stat = lstatOrAbsent(path.join(personalWorkerProfileDir(hqRoot, id), PROFILE_META));
262
+ if (stat === null)
263
+ return "absent";
264
+ if (stat.isSymbolicLink() || !stat.isFile())
265
+ return "present";
266
+ return "present";
267
+ }
268
+ export function loadWorkerProfile(hqRoot, id) {
269
+ const dir = personalWorkerProfileDir(hqRoot, id);
270
+ if (!validProfileId(id)) {
271
+ throw profileError(id, dir, "profile id must be a simple directory name", {
272
+ field: "worker",
273
+ });
274
+ }
275
+ const dirStat = lstatOrAbsent(dir);
276
+ if (dirStat === null) {
277
+ throw profileError(id, dir, "profile directory does not exist", {
278
+ field: "profile",
279
+ });
280
+ }
281
+ if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) {
282
+ throw profileError(id, dir, "profile directory must be a regular directory", {
283
+ field: "profile",
284
+ });
285
+ }
286
+ const metaPath = path.join(dir, PROFILE_META);
287
+ const metaStat = lstatOrAbsent(metaPath);
288
+ if (metaStat === null) {
289
+ throw new WorkerResolutionError("worker_profile_invalid", `Worker profile ${JSON.stringify(id)} is missing ${metaPath}`, { worker: id, path: dir, missing: PROFILE_META });
290
+ }
291
+ const metaText = readRequiredText(id, dir, metaPath, PROFILE_META);
292
+ let metaRaw;
293
+ try {
294
+ metaRaw = parseUserYaml(metaText, metaPath);
295
+ }
296
+ catch (err) {
297
+ throw profileError(id, dir, `meta.yaml is not valid YAML: ${err instanceof Error ? err.message : String(err)}`, {
298
+ field: PROFILE_META,
299
+ });
300
+ }
301
+ const meta = parseMeta(id, dir, metaRaw);
302
+ const systemPromptPath = path.join(dir, PROFILE_SYSTEM_PROMPT);
303
+ const systemPrompt = readRequiredText(id, dir, systemPromptPath, PROFILE_SYSTEM_PROMPT);
304
+ if (systemPrompt.trim().length === 0) {
305
+ throw profileError(id, dir, "system-prompt.md must not be empty", {
306
+ field: PROFILE_SYSTEM_PROMPT,
307
+ });
308
+ }
309
+ const settingsResult = readSettings(id, dir);
310
+ const policyFiles = collectFiles(id, dir, PROFILE_POLICIES);
311
+ const hookFiles = collectFiles(id, dir, PROFILE_HOOKS);
312
+ const hashFiles = [
313
+ PROFILE_META,
314
+ PROFILE_SYSTEM_PROMPT,
315
+ ...(settingsResult.relative ? [settingsResult.relative] : []),
316
+ ...policyFiles,
317
+ ...hookFiles,
318
+ ];
319
+ return {
320
+ id,
321
+ dir,
322
+ meta,
323
+ systemPrompt,
324
+ ...(settingsResult.settings !== undefined ? { settings: settingsResult.settings } : {}),
325
+ policyFiles,
326
+ hookFiles,
327
+ contentHash: hashProfileFiles(dir, hashFiles),
328
+ };
329
+ }
330
+ export function workerProfilePrompt(profile) {
331
+ const parts = [profile.systemPrompt.trimEnd()];
332
+ for (const relative of profile.policyFiles) {
333
+ const content = readRequiredText(profile.id, profile.dir, path.join(profile.dir, relative), relative);
334
+ parts.push(`\n## Profile policy: ${relative}\n${content.trimEnd()}`);
335
+ }
336
+ return parts.join("\n");
337
+ }
338
+ export function ignoredWorkerProfileFields(provider) {
339
+ return provider === "claude" ? [] : ["settings", "hooks"];
340
+ }
341
+ function shellQuote(value) {
342
+ return `'${value.replace(/'/g, "'\\''")}'`;
343
+ }
344
+ /**
345
+ * Hooks may be flat (`hooks/check.sh`) or grouped by Claude lifecycle event
346
+ * (`hooks/PreToolUse/check.sh`). Flat hooks run at SessionStart.
347
+ */
348
+ function profileHookEvent(relative) {
349
+ const parts = relative.split("/");
350
+ return parts.length >= 3 && parts[1] ? parts[1] : "SessionStart";
351
+ }
352
+ /**
353
+ * Materialize provider settings and hook files in the lane run directory.
354
+ * The path is intentionally run-local so every fresh, resumed, moved, and
355
+ * repair spawn gets the profile currently on disk without mutating an account
356
+ * home or a shared provider configuration.
357
+ */
358
+ export function materializeWorkerProfile(profile, runDir) {
359
+ const runtimeDir = path.join(runDir, "worker-profile");
360
+ fs.rmSync(runtimeDir, { recursive: true, force: true });
361
+ const hooksDir = profile.hookFiles.length > 0 ? path.join(runtimeDir, PROFILE_HOOKS) : undefined;
362
+ if (hooksDir !== undefined)
363
+ fs.mkdirSync(hooksDir, { recursive: true });
364
+ for (const relative of profile.hookFiles) {
365
+ const destination = path.join(runtimeDir, relative);
366
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
367
+ const source = path.join(profile.dir, relative);
368
+ fs.copyFileSync(source, destination);
369
+ fs.chmodSync(destination, fs.statSync(source).mode & 0o777);
370
+ }
371
+ const settingsPath = profile.settings !== undefined || hooksDir !== undefined
372
+ ? path.join(runtimeDir, "settings.json")
373
+ : undefined;
374
+ if (settingsPath !== undefined) {
375
+ fs.mkdirSync(runtimeDir, { recursive: true });
376
+ const settings = profile.settings === undefined
377
+ ? {}
378
+ : JSON.parse(JSON.stringify(profile.settings));
379
+ if (hooksDir !== undefined) {
380
+ const registeredHooks = isPlainObject(settings.hooks)
381
+ ? { ...settings.hooks }
382
+ : {};
383
+ for (const relative of profile.hookFiles) {
384
+ const event = profileHookEvent(relative);
385
+ const eventHooks = Array.isArray(registeredHooks[event])
386
+ ? [...registeredHooks[event]]
387
+ : [];
388
+ eventHooks.push({
389
+ hooks: [{
390
+ type: "command",
391
+ command: `bash ${shellQuote(path.join(runtimeDir, relative))}`,
392
+ }],
393
+ });
394
+ registeredHooks[event] = eventHooks;
395
+ }
396
+ settings.hooks = registeredHooks;
397
+ }
398
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
399
+ }
400
+ return {
401
+ ...(settingsPath !== undefined ? { settingsPath } : {}),
402
+ ...(hooksDir !== undefined ? { hooksDir } : {}),
403
+ };
404
+ }
405
+ //# sourceMappingURL=profile.js.map