@indigoai-us/hq-cli 5.141.0 → 5.143.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 (36) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +77 -2
  3. package/dist/command-catalog.generated.d.ts +7 -1
  4. package/dist/command-catalog.generated.js +9 -1
  5. package/dist/commands/agents.d.ts +1 -0
  6. package/dist/commands/agents.js +4 -8
  7. package/dist/commands/dm.js +1 -5
  8. package/dist/commands/lanes.js +125 -26
  9. package/dist/commands/mesh.js +25 -2
  10. package/dist/commands/report.d.ts +3 -1
  11. package/dist/commands/report.js +69 -15
  12. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  13. package/dist/lib/bot/api.d.ts +4 -0
  14. package/dist/lib/bot/api.js +13 -1
  15. package/dist/lib/lanes/effort.d.ts +3 -2
  16. package/dist/lib/lanes/effort.js +5 -1
  17. package/dist/lib/lanes/errors.d.ts +1 -1
  18. package/dist/lib/lanes/index.d.ts +2 -0
  19. package/dist/lib/lanes/index.js +1 -0
  20. package/dist/lib/lanes/mesh-events.d.ts +4 -5
  21. package/dist/lib/lanes/mesh-events.js +14 -4
  22. package/dist/lib/lanes/profile.d.ts +48 -0
  23. package/dist/lib/lanes/profile.js +405 -0
  24. package/dist/lib/lanes/reconcile.js +25 -3
  25. package/dist/lib/lanes/registry.d.ts +3 -0
  26. package/dist/lib/lanes/registry.js +45 -1
  27. package/dist/lib/lanes/spawn.d.ts +14 -2
  28. package/dist/lib/lanes/spawn.js +238 -14
  29. package/dist/lib/lanes/store.js +10 -0
  30. package/dist/lib/lanes/types.d.ts +11 -1
  31. package/dist/lib/lanes/types.js +3 -3
  32. package/dist/lib/notify-recipient.d.ts +7 -0
  33. package/dist/lib/notify-recipient.js +11 -0
  34. package/dist/lib/report-publish.d.ts +2 -2
  35. package/dist/lib/report-publish.js +9 -8
  36. package/package.json +1 -1
@@ -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);
@@ -11,7 +11,7 @@ import { PROVIDER_HOME_ENV } from "./accounts.js";
11
11
  import { type RunCompactionFn } from "./compaction.js";
12
12
  import { type LaunchTrigger } from "./delivery.js";
13
13
  import { type MachineIdentity } from "./machine-identity.js";
14
- import { type LaneProvider, type LaneRecord } from "./types.js";
14
+ import { type LaneCompanyRef, type LaneProvider, type LaneRecord } from "./types.js";
15
15
  export declare const LANES_RUNS_REL: string;
16
16
  /** Written by reconcile before a resume spawn; the worker prompt names this path. */
17
17
  export declare const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
@@ -27,12 +27,18 @@ export declare const CODEX_FAST_MODE_FLAGS: readonly ["-c", "service_tier=\"fast
27
27
  export declare function parseCliModel(raw: string | undefined): string | undefined;
28
28
  export declare function resolveLaneModel(input: {
29
29
  cliModel?: string;
30
+ profileModel?: string;
30
31
  accountModel?: string;
31
32
  providerModel?: string;
32
33
  }): string | undefined;
33
34
  export declare function appendModelFlag(provider: LaneProvider, args: string[], model: string | undefined): string[];
34
35
  /** Add Codex fast-mode flags before the prompt/positional separator. */
35
36
  export declare function appendFastModeFlags(provider: LaneProvider, args: string[], enabled: boolean | undefined): string[];
37
+ export interface ProviderProfileLaunch {
38
+ systemPrompt: string;
39
+ settingsPath?: string;
40
+ ignoredFields?: readonly string[];
41
+ }
36
42
  export type DetachedStdio = ["ignore", number, number];
37
43
  export interface DetachedSpawnOptions {
38
44
  detached: true;
@@ -197,6 +203,8 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
197
203
  effort?: string;
198
204
  /** Enable Codex's fast service tier for this resolved model. */
199
205
  fastMode?: boolean;
206
+ /** Per-spawn worker profile instructions and materialized settings. */
207
+ profile?: ProviderProfileLaunch;
200
208
  /** Continue a recorded session. Only providers in PROVIDER_SESSION_RESUME accept this. */
201
209
  resume?: ProviderResumeInput;
202
210
  /**
@@ -209,7 +217,7 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
209
217
  export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
210
218
  envVar: string;
211
219
  home: string;
212
- }, laneId: string, hqRoot: string, autoCompactTokens?: number): NodeJS.ProcessEnv;
220
+ }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef): NodeJS.ProcessEnv;
213
221
  export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonRaw: string, details: Record<string, unknown>): LaneRecord;
214
222
  /**
215
223
  * Spec §5.2 executing-side check. Compares this machine's identity to
@@ -227,6 +235,10 @@ export interface SpawnManifestFile {
227
235
  /** Byte length of logs/worker.out before this run started; its result is after this. */
228
236
  worker_out_offset?: number;
229
237
  fast_mode?: boolean;
238
+ worker_profile?: string;
239
+ worker_profile_hash?: string;
240
+ worker_profile_ignored?: string[];
241
+ worker_profile_settings?: string;
230
242
  }
231
243
  /** Read the per-run manifest. Missing file or field reads as absent. */
232
244
  export declare function readSpawnManifest(runDir: string): SpawnManifestFile;