@indigoai-us/hq-cli 5.141.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.
@@ -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
@@ -11,6 +11,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
11
11
  import { accountMaxRunning, loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
12
12
  import { MAX_LANE_DEPTH } from "./constants.js";
13
13
  import { LanesError, isLaneVersionConflict } from "./errors.js";
14
+ import { loadWorkerProfile } from "./profile.js";
14
15
  import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, } from "./liveness.js";
15
16
  import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, spawnEnvelopeRepair, spawnLaneWorker, } from "./spawn.js";
16
17
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
@@ -18,7 +19,7 @@ import { applyThrottleDecision, defaultThrottleDeps, emptyThrottleState, evaluat
18
19
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
19
20
  import { answeredQuestionEntries, raiseEnvelopeQuestions } from "./questions.js";
20
21
  import { SILENCE_MS, classifyLaneExit, isPidAlive, readLatestEnvelope, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
21
- import { createLaneMesh, emitLaneDecisionEvents } from "./mesh-events.js";
22
+ import { createLaneMesh, emitLaneDecisionEvents, laneMeshCompany } from "./mesh-events.js";
22
23
  import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
23
24
  import { providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
24
25
  /** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
@@ -317,13 +318,14 @@ async function emitAgedQuestionAtUser(summary, deps, lane, question) {
317
318
  if (deps.dryRun)
318
319
  return;
319
320
  const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
320
- if (!mesh || !lane.company || !lane.thread_id)
321
+ const company = laneMeshCompany(lane);
322
+ if (!mesh || !company || !lane.thread_id)
321
323
  return;
322
324
  const timeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
323
325
  let timer;
324
326
  try {
325
327
  await Promise.race([
326
- mesh.appendThreadEvent(lane.company, lane.thread_id, "lane_question_at_user", { lane_id: lane.lane_id, question_id: question.question_id }),
328
+ mesh.appendThreadEvent(company, lane.thread_id, "lane_question_at_user", { lane_id: lane.lane_id, question_id: question.question_id }),
327
329
  new Promise((_resolve, reject) => {
328
330
  timer = setTimeout(() => reject(new Error(`question-at-user mesh emit timed out after ${timeoutMs}ms`)), timeoutMs);
329
331
  }),
@@ -1011,6 +1013,26 @@ async function admitQueued(hqRoot, lanes, summary, deps, now) {
1011
1013
  continue;
1012
1014
  }
1013
1015
  }
1016
+ let profileMaxConcurrent;
1017
+ if (lane.worker_profile !== undefined) {
1018
+ try {
1019
+ profileMaxConcurrent = loadWorkerProfile(hqRoot, lane.worker_profile).meta.max_concurrent;
1020
+ }
1021
+ catch (err) {
1022
+ markDirty(summary, {
1023
+ kind: "lane",
1024
+ id: lane.lane_id,
1025
+ why: `profile admission failed: ${err instanceof Error ? err.message : String(err)}`,
1026
+ });
1027
+ continue;
1028
+ }
1029
+ }
1030
+ if (profileMaxConcurrent !== undefined) {
1031
+ const profileRunning = running.filter((row) => row.worker_profile === lane.worker_profile).length +
1032
+ spawned.filter((row) => row.worker_profile === lane.worker_profile).length;
1033
+ if (profileRunning >= profileMaxConcurrent)
1034
+ continue;
1035
+ }
1014
1036
  try {
1015
1037
  if (deps.dryRun) {
1016
1038
  summary.admitted.push({
@@ -23,6 +23,7 @@
23
23
  * `worker_tenancy_unresolved` when that cannot be determined.
24
24
  */
25
25
  import * as fs from "node:fs";
26
+ import { type WorkerProfile } from "./profile.js";
26
27
  export declare const WORKER_REGISTRY_REL: string;
27
28
  export interface RegistryWorkerEntry {
28
29
  id: string;
@@ -87,6 +88,8 @@ export interface ResolvedLaneWorker {
87
88
  humanCheckpoints: string[];
88
89
  /** Set when the worker's realpath lives in a package. */
89
90
  pack?: string;
91
+ /** Set when this worker was resolved from personal/workers/<id>/meta.yaml. */
92
+ profile?: WorkerProfile;
90
93
  }
91
94
  export interface RegistryDeps {
92
95
  existsSync: (p: string) => boolean;
@@ -28,6 +28,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
28
28
  import { readContributes } from "../scan-packages/index.js";
29
29
  import { contributionLinks, linkStatus, } from "../../utils/pack-contributions.js";
30
30
  import { WorkerResolutionError } from "./errors.js";
31
+ import { loadWorkerProfile, workerProfileMarker, } from "./profile.js";
31
32
  export const WORKER_REGISTRY_REL = path.join("core", "workers", "registry.yaml");
32
33
  function defaultRegistryDeps() {
33
34
  return {
@@ -247,6 +248,9 @@ function personalDirClaim(hqRoot, workerId, deps) {
247
248
  const dir = personalWorkerDir(hqRoot, workerId);
248
249
  let st;
249
250
  try {
251
+ const linkStat = fs.lstatSync(dir);
252
+ if (linkStat.isSymbolicLink())
253
+ return "none";
250
254
  st = deps.statSync(dir);
251
255
  }
252
256
  catch (err) {
@@ -369,7 +373,10 @@ function readRegistryOrEmpty(hqRoot, deps) {
369
373
  }
370
374
  export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps()) {
371
375
  const personalRel = path.posix.join("personal", "workers", workerId);
372
- const personalClaim = personalDirClaim(hqRoot, workerId, deps);
376
+ const profile = workerProfileMarker(hqRoot, workerId) === "present"
377
+ ? loadWorkerProfile(hqRoot, workerId)
378
+ : undefined;
379
+ const personalClaim = profile ? "profile" : personalDirClaim(hqRoot, workerId, deps);
373
380
  if (personalClaim === "missing_yaml") {
374
381
  throw new WorkerResolutionError("worker_not_found", `Worker ${JSON.stringify(workerId)} is at ${personalRel}/ but worker.yaml is missing`, { worker: workerId, path: `${personalRel}/`, missing: "worker.yaml" });
375
382
  }
@@ -390,6 +397,43 @@ export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultR
390
397
  realpath: realpathOf(hqRoot, rel),
391
398
  });
392
399
  }
400
+ if (profile !== undefined && candidates.length > 0) {
401
+ throw new WorkerResolutionError("worker_profile_ambiguous", "Worker " +
402
+ JSON.stringify(workerId) +
403
+ " has both a personal profile and a registry worker; name one explicitly instead of falling through", {
404
+ worker: workerId,
405
+ profile_path: personalRel + "/",
406
+ registry_paths: candidates.map((item) => item.path),
407
+ });
408
+ }
409
+ if (profile !== undefined) {
410
+ const description = "Reusable worker profile " + profile.id;
411
+ return {
412
+ id: profile.id,
413
+ path: personalRel + "/",
414
+ type: "WorkerProfile",
415
+ status: "active",
416
+ description,
417
+ tenancy: { kind: "global" },
418
+ workerYaml: {
419
+ worker: {
420
+ id: profile.id,
421
+ name: profile.id,
422
+ description,
423
+ type: "WorkerProfile",
424
+ status: "active",
425
+ },
426
+ instructions: "Apply reusable worker profile " + profile.id + " at every provider spawn.",
427
+ },
428
+ workerDir: profile.dir,
429
+ resolvedContextBase: [],
430
+ skillFiles: [],
431
+ defaultTimeout: undefined,
432
+ approvalRequired: false,
433
+ humanCheckpoints: [],
434
+ profile,
435
+ };
436
+ }
393
437
  if (personalClaim === "yaml") {
394
438
  const rel = normalizeRel(personalRel);
395
439
  const real = realpathOf(hqRoot, rel);