@indigoai-us/hq-cli 5.239.0 → 5.240.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.
@@ -0,0 +1,329 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { parseUserYaml } from "../../utils/user-yaml-error.js";
5
+ import { WorkerResolutionError } from "./errors.js";
6
+ const REGISTRY_RELATIVE_PATH = path.join("core", "workers", "registry.yaml");
7
+ function fail(code, message, details = {}) {
8
+ throw new WorkerResolutionError(code, message, details);
9
+ }
10
+ function isPlainObject(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ function withinRoot(root, target) {
14
+ const relative = path.relative(root, target);
15
+ return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
16
+ }
17
+ function resolveWorkerDirectory(hqRoot, workerPath, id) {
18
+ const relative = workerPath.replaceAll("\\", "/").replace(/\/+$/, "");
19
+ if (relative.length === 0 ||
20
+ path.posix.isAbsolute(relative) ||
21
+ relative.split("/").some((part) => part === ".." || part === ".")) {
22
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} has an unsafe path`, {
23
+ worker: id,
24
+ path: workerPath,
25
+ });
26
+ }
27
+ const root = path.resolve(hqRoot);
28
+ const abs = path.resolve(root, ...relative.split("/"));
29
+ if (!withinRoot(root, abs)) {
30
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} escapes HQ_ROOT`, {
31
+ worker: id,
32
+ path: workerPath,
33
+ });
34
+ }
35
+ let realRoot;
36
+ let realWorkerDir;
37
+ try {
38
+ realRoot = fs.realpathSync(root);
39
+ realWorkerDir = fs.realpathSync(abs);
40
+ }
41
+ catch (err) {
42
+ fail("registry_worker_unreadable", `Cannot resolve registry worker ${JSON.stringify(id)} at ${abs}: ${err instanceof Error ? err.message : String(err)}`, {
43
+ worker: id,
44
+ path: abs,
45
+ });
46
+ }
47
+ if (!withinRoot(realRoot, realWorkerDir)) {
48
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} resolves outside HQ_ROOT`, {
49
+ worker: id,
50
+ path: workerPath,
51
+ });
52
+ }
53
+ return { abs: realWorkerDir, relative };
54
+ }
55
+ function readWorkerYaml(hqRoot, workerPath, id) {
56
+ const directory = resolveWorkerDirectory(hqRoot, workerPath, id);
57
+ const filePath = path.join(directory.abs, "worker.yaml");
58
+ let stat;
59
+ let text;
60
+ try {
61
+ stat = fs.lstatSync(filePath);
62
+ if (stat.isSymbolicLink() || !stat.isFile()) {
63
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml must be a regular file`, {
64
+ worker: id,
65
+ path: filePath,
66
+ });
67
+ }
68
+ text = fs.readFileSync(filePath, "utf8");
69
+ }
70
+ catch (err) {
71
+ if (err instanceof WorkerResolutionError)
72
+ throw err;
73
+ fail("registry_worker_unreadable", `Cannot read registry worker ${JSON.stringify(id)} at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, {
74
+ worker: id,
75
+ path: filePath,
76
+ });
77
+ }
78
+ let parsed;
79
+ try {
80
+ parsed = parseUserYaml(text, filePath);
81
+ }
82
+ catch (err) {
83
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} has invalid worker.yaml at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, {
84
+ worker: id,
85
+ path: filePath,
86
+ });
87
+ }
88
+ if (!isPlainObject(parsed)) {
89
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml must contain an object`, {
90
+ worker: id,
91
+ path: filePath,
92
+ });
93
+ }
94
+ const realFile = fs.realpathSync(filePath);
95
+ const realRoot = fs.realpathSync(path.resolve(hqRoot));
96
+ if (!withinRoot(realRoot, realFile)) {
97
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml resolves outside HQ_ROOT`, {
98
+ worker: id,
99
+ path: filePath,
100
+ });
101
+ }
102
+ return {
103
+ filePath,
104
+ text,
105
+ yaml: parsed,
106
+ sha256: `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`,
107
+ };
108
+ }
109
+ function textValue(value, field, id) {
110
+ if (value === undefined || value === null)
111
+ return "";
112
+ if (typeof value === "string")
113
+ return value.trim();
114
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
115
+ return value.map((item) => item.trim()).filter(Boolean).join("\n");
116
+ }
117
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must be a string or string array`, {
118
+ worker: id,
119
+ field,
120
+ });
121
+ }
122
+ function stringList(value, field, id) {
123
+ if (value === undefined || value === null)
124
+ return [];
125
+ const values = typeof value === "string" ? [value] : value;
126
+ if (!Array.isArray(values) || values.some((item) => typeof item !== "string" || item.trim().length === 0)) {
127
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must be an array of non-empty strings`, {
128
+ worker: id,
129
+ field,
130
+ });
131
+ }
132
+ const cleaned = values.map((item) => item.trim());
133
+ if (new Set(cleaned).size !== cleaned.length) {
134
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must not contain duplicates`, {
135
+ worker: id,
136
+ field,
137
+ });
138
+ }
139
+ return cleaned;
140
+ }
141
+ function verificationCommands(value, id) {
142
+ if (value === undefined || value === null)
143
+ return [];
144
+ if (!Array.isArray(value)) {
145
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute must be an array`, {
146
+ worker: id,
147
+ field: "verification.post_execute",
148
+ });
149
+ }
150
+ const commands = value.map((entry, index) => {
151
+ if (typeof entry === "string" && entry.trim() !== "")
152
+ return entry.trim();
153
+ if (isPlainObject(entry) && typeof entry.command === "string" && entry.command.trim() !== "") {
154
+ return entry.command.trim();
155
+ }
156
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute[${index}] must contain a non-empty command`, {
157
+ worker: id,
158
+ field: `verification.post_execute[${index}]`,
159
+ });
160
+ });
161
+ if (new Set(commands).size !== commands.length) {
162
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute must not contain duplicate commands`, {
163
+ worker: id,
164
+ field: "verification.post_execute",
165
+ });
166
+ }
167
+ return commands;
168
+ }
169
+ function personaFromYaml(hqRoot, id, workerPath, yaml, sha256, registryDescription, pinnedApproval) {
170
+ if (yaml.worker?.id !== undefined && yaml.worker.id !== id) {
171
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml declares a different id`, {
172
+ worker: id,
173
+ declared_id: yaml.worker.id,
174
+ });
175
+ }
176
+ const workerDir = resolveWorkerDirectory(hqRoot, workerPath, id).abs;
177
+ const description = textValue(yaml.worker?.description ?? yaml.description ?? registryDescription, "description", id) || id;
178
+ const instructions = textValue(yaml.instructions, "instructions", id);
179
+ const skillFiles = [];
180
+ if (yaml.skills !== undefined) {
181
+ if (!Array.isArray(yaml.skills)) {
182
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} skills must be an array`, {
183
+ worker: id,
184
+ field: "skills",
185
+ });
186
+ }
187
+ for (const [index, skill] of yaml.skills.entries()) {
188
+ if (!isPlainObject(skill) || typeof skill.file !== "string" || skill.file.trim().length === 0) {
189
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} skills[${index}].file must be a non-empty string`, {
190
+ worker: id,
191
+ field: `skills[${index}].file`,
192
+ });
193
+ }
194
+ skillFiles.push(path.resolve(workerDir, skill.file.trim()));
195
+ }
196
+ }
197
+ let verification = {};
198
+ if (yaml.verification !== undefined) {
199
+ if (!isPlainObject(yaml.verification)) {
200
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification must be an object`, {
201
+ worker: id,
202
+ field: "verification",
203
+ });
204
+ }
205
+ verification = yaml.verification;
206
+ }
207
+ if (verification.approval_required !== undefined && typeof verification.approval_required !== "boolean") {
208
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.approval_required must be a boolean`, {
209
+ worker: id,
210
+ field: "verification.approval_required",
211
+ });
212
+ }
213
+ const humanCheckpoints = pinnedApproval?.human_checkpoints ??
214
+ stringList(verification.human_checkpoints, "verification.human_checkpoints", id);
215
+ return {
216
+ id,
217
+ path: workerPath,
218
+ workerYamlSha256: sha256,
219
+ description,
220
+ instructions,
221
+ skillFiles,
222
+ postExecute: verificationCommands(verification.post_execute, id),
223
+ approvalRequired: pinnedApproval?.approval_required ?? verification.approval_required === true,
224
+ humanCheckpoints: [...humanCheckpoints],
225
+ };
226
+ }
227
+ function registryEntries(hqRoot) {
228
+ const registryPath = path.join(hqRoot, REGISTRY_RELATIVE_PATH);
229
+ let text;
230
+ try {
231
+ text = fs.readFileSync(registryPath, "utf8");
232
+ }
233
+ catch (err) {
234
+ fail("registry_worker_unreadable", `Cannot read worker registry at ${registryPath}: ${err instanceof Error ? err.message : String(err)}`, {
235
+ path: registryPath,
236
+ });
237
+ }
238
+ let parsed;
239
+ try {
240
+ parsed = parseUserYaml(text, registryPath);
241
+ }
242
+ catch (err) {
243
+ fail("registry_worker_unreadable", `Cannot parse worker registry at ${registryPath}: ${err instanceof Error ? err.message : String(err)}`, {
244
+ path: registryPath,
245
+ });
246
+ }
247
+ if (!isPlainObject(parsed) || !Array.isArray(parsed.workers)) {
248
+ fail("registry_worker_unreadable", `Worker registry at ${registryPath} must contain a workers array`, {
249
+ path: registryPath,
250
+ });
251
+ }
252
+ return parsed.workers.filter(isPlainObject);
253
+ }
254
+ /** Resolve an active registry worker for a lane without company fallback. */
255
+ export function resolveRegistryPersona(hqRoot, id, laneCompany) {
256
+ const workerId = id.trim();
257
+ if (!workerId) {
258
+ fail("registry_worker_invalid", "registry_worker must be a non-empty worker id", { worker: id });
259
+ }
260
+ const candidates = registryEntries(hqRoot).filter((entry) => entry.id === workerId);
261
+ if (candidates.length === 0) {
262
+ fail("registry_worker_not_found", `Registry worker ${JSON.stringify(workerId)} was not found`, {
263
+ worker: workerId,
264
+ path: path.join(hqRoot, REGISTRY_RELATIVE_PATH),
265
+ });
266
+ }
267
+ if (candidates.length > 1) {
268
+ fail("registry_worker_ambiguous", `Registry worker ${JSON.stringify(workerId)} appears more than once`, {
269
+ worker: workerId,
270
+ paths: candidates.map((entry) => entry.path),
271
+ });
272
+ }
273
+ const entry = candidates[0];
274
+ if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
275
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} has no path`, {
276
+ worker: workerId,
277
+ });
278
+ }
279
+ if (entry.company !== undefined && typeof entry.company !== "string") {
280
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} company must be a string`, {
281
+ worker: workerId,
282
+ });
283
+ }
284
+ if (typeof entry.company === "string" && entry.company.trim().length === 0) {
285
+ fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} company must be non-empty when supplied`, {
286
+ worker: workerId,
287
+ });
288
+ }
289
+ const workerCompany = typeof entry.company === "string" && entry.company.trim().length > 0
290
+ ? entry.company.trim()
291
+ : undefined;
292
+ if (workerCompany !== undefined && workerCompany !== laneCompany) {
293
+ fail("registry_worker_company_mismatch", `Registry worker ${JSON.stringify(workerId)} belongs to company ${workerCompany} and is not available to ${laneCompany ?? "(none)"}`, {
294
+ worker: workerId,
295
+ worker_company: workerCompany,
296
+ lane_company: laneCompany ?? null,
297
+ });
298
+ }
299
+ const status = typeof entry.status === "string" && entry.status.trim() !== ""
300
+ ? entry.status.trim()
301
+ : "active";
302
+ if (status !== "active") {
303
+ fail("registry_worker_inactive", `Registry worker ${JSON.stringify(workerId)} has status ${JSON.stringify(status)} (need active)`, {
304
+ worker: workerId,
305
+ status,
306
+ });
307
+ }
308
+ const worker = readWorkerYaml(hqRoot, entry.path.trim(), workerId);
309
+ const persona = personaFromYaml(hqRoot, workerId, entry.path.trim(), worker.yaml, worker.sha256, entry.description);
310
+ return persona;
311
+ }
312
+ /** Re-read the recorded path on resume; registry changes cannot redirect a lane. */
313
+ export function loadRegistryPersonaFromRecord(hqRoot, record) {
314
+ const worker = readWorkerYaml(hqRoot, record.path, record.id);
315
+ return personaFromYaml(hqRoot, record.id, record.path, worker.yaml, worker.sha256, undefined, {
316
+ approval_required: record.approval_required,
317
+ human_checkpoints: record.human_checkpoints,
318
+ });
319
+ }
320
+ export function registryWorkerRecord(persona) {
321
+ return {
322
+ id: persona.id,
323
+ path: persona.path,
324
+ worker_yaml_sha256: persona.workerYamlSha256,
325
+ approval_required: persona.approvalRequired,
326
+ human_checkpoints: [...persona.humanCheckpoints],
327
+ };
328
+ }
329
+ //# sourceMappingURL=registry-persona.js.map
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import * as fs from "node:fs";
26
26
  import { type WorkerProfile } from "./profile.js";
27
+ import type { RegistryWorkerRecord } from "./types.js";
27
28
  export declare const WORKER_REGISTRY_REL: string;
28
29
  export interface RegistryWorkerEntry {
29
30
  id: string;
@@ -115,5 +116,7 @@ export declare function tenancyFromRegistryPath(entry: RegistryWorkerEntry): Wor
115
116
  */
116
117
  export declare function resolveContextBasePaths(hqRoot: string, workerDir: string, bases: string[] | undefined, deps?: RegistryDeps): string[];
117
118
  export declare function resolveSkillFiles(workerDir: string, skills: WorkerYamlSkill[] | undefined, deps?: RegistryDeps): string[];
118
- export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps): ResolvedLaneWorker;
119
+ export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps, options?: {
120
+ registryWorkerRecord?: RegistryWorkerRecord;
121
+ }): ResolvedLaneWorker;
119
122
  //# sourceMappingURL=registry.d.ts.map
@@ -28,7 +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
+ import { loadWorkerProfileWithRegistry, workerProfileMarker, } from "./profile.js";
32
32
  export const WORKER_REGISTRY_REL = path.join("core", "workers", "registry.yaml");
33
33
  function defaultRegistryDeps() {
34
34
  return {
@@ -371,10 +371,15 @@ function readRegistryOrEmpty(hqRoot, deps) {
371
371
  return [];
372
372
  return readWorkerRegistryFile(hqRoot, deps);
373
373
  }
374
- export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps()) {
374
+ export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps(), options = {}) {
375
375
  const personalRel = path.posix.join("personal", "workers", workerId);
376
376
  const profile = workerProfileMarker(hqRoot, workerId) === "present"
377
- ? loadWorkerProfile(hqRoot, workerId)
377
+ ? loadWorkerProfileWithRegistry(hqRoot, workerId, {
378
+ laneCompany,
379
+ ...(options.registryWorkerRecord !== undefined
380
+ ? { registryWorkerRecord: options.registryWorkerRecord }
381
+ : {}),
382
+ })
378
383
  : undefined;
379
384
  const personalClaim = profile ? "profile" : personalDirClaim(hqRoot, workerId, deps);
380
385
  if (personalClaim === "missing_yaml") {
@@ -239,7 +239,7 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
239
239
  export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
240
240
  envVar: string;
241
241
  home: string;
242
- }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string): NodeJS.ProcessEnv;
242
+ }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"]): NodeJS.ProcessEnv;
243
243
  export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
244
244
  /** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
245
245
  export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
@@ -23,7 +23,8 @@ import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
23
23
  import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
24
24
  import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
25
25
  import { AccountResolutionError, LanesError } from "./errors.js";
26
- import { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
26
+ import { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
27
+ import { registryWorkerRecord } from "./registry-persona.js";
27
28
  import { resolveMachineIdentity, } from "./machine-identity.js";
28
29
  import { appendCappedHistory, listLaneIds, readLane, retryUpdateLane } from "./store.js";
29
30
  import { copySessionFiles, planSessionCopy } from "./move-account.js";
@@ -1236,7 +1237,7 @@ export function buildProviderCommand(provider, input) {
1236
1237
  args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, appendCodexWorkerPolicyDirFlag(parsed, appendProviderProfileFlags(parsed, args, input.profile), input.workerPolicyDir), input.model), input.effort), input.fastMode),
1237
1238
  };
1238
1239
  }
1239
- export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir) {
1240
+ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker) {
1240
1241
  const accepted = Object.values(PROVIDER_HOME_ENV);
1241
1242
  if (!accepted.includes(resolved.envVar)) {
1242
1243
  throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
@@ -1245,6 +1246,8 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
1245
1246
  delete env.HQ_PROJECT_ID;
1246
1247
  delete env.HQ_STORY_ID;
1247
1248
  delete env.HQ_POLICY_WORKER_DIR;
1249
+ delete env.HQ_LANE_APPROVAL_REQUIRED;
1250
+ delete env.HQ_LANE_HUMAN_CHECKPOINTS;
1248
1251
  env[resolved.envVar] = resolved.home;
1249
1252
  delete env[CLAUDE_AUTO_COMPACT_ENV];
1250
1253
  if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
@@ -1252,6 +1255,12 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
1252
1255
  }
1253
1256
  env.HQ_LANE_ID = laneId;
1254
1257
  env.HQ_ROOT = hqRoot;
1258
+ if (registryWorker && (registryWorker.approval_required || registryWorker.human_checkpoints.length > 0)) {
1259
+ env.HQ_LANE_APPROVAL_REQUIRED = "1";
1260
+ env.HQ_LANE_HUMAN_CHECKPOINTS = registryWorker.human_checkpoints.length > 0
1261
+ ? registryWorker.human_checkpoints.join(",")
1262
+ : "external_publish";
1263
+ }
1255
1264
  if (workerPolicyDir !== undefined &&
1256
1265
  (resolved.envVar === PROVIDER_HOME_ENV.claude || resolved.envVar === PROVIDER_HOME_ENV.codex)) {
1257
1266
  env.HQ_POLICY_WORKER_DIR = workerPolicyDir;
@@ -1659,7 +1668,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1659
1668
  ensureDir(logsDir);
1660
1669
  ensureDir(path.join(runDir, "envelopes"));
1661
1670
  const profile = existing.worker_profile
1662
- ? loadWorkerProfile(hqRoot, existing.worker_profile)
1671
+ ? loadWorkerProfileWithRegistry(hqRoot, existing.worker_profile, {
1672
+ laneCompany: existing.company?.slug ?? existing.mesh_company?.slug,
1673
+ ...(existing.registry_worker !== undefined
1674
+ ? { registryWorkerRecord: existing.registry_worker }
1675
+ : {}),
1676
+ })
1663
1677
  : undefined;
1664
1678
  if (existing.host.kind === "outpost" &&
1665
1679
  profile !== undefined &&
@@ -1827,7 +1841,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1827
1841
  const autoCompactTokens = lane.provider === "claude"
1828
1842
  ? providerAutoCompactTokens(config.providers.claude)
1829
1843
  : undefined;
1830
- const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
1844
+ const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker);
1831
1845
  const laneTurnId = randomUUID();
1832
1846
  childEnv.HQ_LANE_TURN_ID = laneTurnId;
1833
1847
  const model = resolveLaneModel({
@@ -2023,10 +2037,13 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2023
2037
  ...(profile
2024
2038
  ? {
2025
2039
  worker_profile_hash: profile.contentHash,
2040
+ registry_worker: profile.registryWorker !== undefined
2041
+ ? registryWorkerRecord(profile.registryWorker)
2042
+ : undefined,
2026
2043
  worker_profile_ignored: [...ignoredProfileFields],
2027
2044
  worker_policy_delivery: policyDelivery,
2028
2045
  }
2029
- : { worker_policy_delivery: undefined }),
2046
+ : { registry_worker: undefined, worker_policy_delivery: undefined }),
2030
2047
  };
2031
2048
  if (resume !== undefined) {
2032
2049
  next.provider_session_id = resume.session_id;
@@ -2240,7 +2257,11 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2240
2257
  ensureDir(logsDir);
2241
2258
  const config = loadLanesConfig(hqRoot);
2242
2259
  const profile = lane.worker_profile
2243
- ? loadWorkerProfile(hqRoot, lane.worker_profile, {
2260
+ ? loadWorkerProfileWithRegistry(hqRoot, lane.worker_profile, {
2261
+ laneCompany: lane.company?.slug ?? lane.mesh_company?.slug,
2262
+ ...(lane.registry_worker !== undefined
2263
+ ? { registryWorkerRecord: lane.registry_worker }
2264
+ : {}),
2244
2265
  onUnknownMetaFields: (fields) => {
2245
2266
  process.stderr.write(`hq lanes watcher: envelope repair ignored unknown worker profile meta field(s) for ${lane.worker_profile}: ${fields.join(", ")}\n`);
2246
2267
  input.onProfileWarning?.(fields);
@@ -2289,7 +2310,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2289
2310
  const autoCompactTokens = lane.provider === "claude"
2290
2311
  ? providerAutoCompactTokens(config.providers.claude)
2291
2312
  : undefined;
2292
- const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
2313
+ const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker);
2293
2314
  const accountCfg = config.providers[lane.provider].accounts[resolved.id];
2294
2315
  const model = resolveLaneModel({
2295
2316
  cliModel: pinnedModel,
@@ -543,6 +543,91 @@ export function parseLaneRecord(raw, filePath) {
543
543
  raw.worker_policy_delivery !== "prompt") {
544
544
  throw new LaneSchemaError(`Unknown worker policy delivery mode ${JSON.stringify(raw.worker_policy_delivery)}`, { path: filePath, field: "worker_policy_delivery", value: raw.worker_policy_delivery });
545
545
  }
546
+ if (raw.registry_worker !== undefined) {
547
+ const worker = raw.registry_worker;
548
+ if (!isPlainObject(worker) || Object.keys(worker).some((key) => !["id", "path", "worker_yaml_sha256", "approval_required", "human_checkpoints"].includes(key))) {
549
+ throw new LaneSchemaError("registry_worker must be a pinned registry worker record", {
550
+ path: filePath,
551
+ field: "registry_worker",
552
+ });
553
+ }
554
+ for (const key of ["id", "path"]) {
555
+ if (typeof worker[key] !== "string" || worker[key].trim().length === 0) {
556
+ throw new LaneSchemaError(`registry_worker.${key} must be a non-empty string`, {
557
+ path: filePath,
558
+ field: `registry_worker.${key}`,
559
+ });
560
+ }
561
+ }
562
+ const registryPath = worker.path.replaceAll("\\", "/");
563
+ if (path.posix.isAbsolute(registryPath) || registryPath.split("/").some((part) => part === ".." || part === ".")) {
564
+ throw new LaneSchemaError("registry_worker.path must stay within HQ_ROOT", {
565
+ path: filePath,
566
+ field: "registry_worker.path",
567
+ });
568
+ }
569
+ if (typeof worker.worker_yaml_sha256 !== "string" || !/^sha256:[a-f0-9]{64}$/.test(worker.worker_yaml_sha256)) {
570
+ throw new LaneSchemaError("registry_worker.worker_yaml_sha256 must be a SHA-256 hash", {
571
+ path: filePath,
572
+ field: "registry_worker.worker_yaml_sha256",
573
+ });
574
+ }
575
+ if (typeof worker.approval_required !== "boolean") {
576
+ throw new LaneSchemaError("registry_worker.approval_required must be a boolean", {
577
+ path: filePath,
578
+ field: "registry_worker.approval_required",
579
+ });
580
+ }
581
+ if (!Array.isArray(worker.human_checkpoints) || worker.human_checkpoints.some((checkpoint) => typeof checkpoint !== "string" || checkpoint.trim().length === 0) ||
582
+ new Set(worker.human_checkpoints).size !== worker.human_checkpoints.length) {
583
+ throw new LaneSchemaError("registry_worker.human_checkpoints must be unique non-empty strings", {
584
+ path: filePath,
585
+ field: "registry_worker.human_checkpoints",
586
+ });
587
+ }
588
+ }
589
+ if (raw.registry_worker_approval_history !== undefined) {
590
+ if (!Array.isArray(raw.registry_worker_approval_history)) {
591
+ throw new LaneSchemaError("registry_worker_approval_history must be an array when present", {
592
+ path: filePath,
593
+ field: "registry_worker_approval_history",
594
+ });
595
+ }
596
+ for (const [index, entry] of raw.registry_worker_approval_history.entries()) {
597
+ const field = `registry_worker_approval_history[${index}]`;
598
+ if (!isPlainObject(entry)) {
599
+ throw new LaneSchemaError(`${field} must be an approval audit event`, { path: filePath, field });
600
+ }
601
+ const expected = entry.event === "approved"
602
+ ? ["event", "approval_id", "checkpoint", "registry_worker_id", "worker_yaml_sha256", "reason", "actor", "at"]
603
+ : entry.event === "consumed"
604
+ ? ["event", "approval_id", "envelope_ref", "at"]
605
+ : [];
606
+ if (expected.length === 0 || Object.keys(entry).some((key) => !expected.includes(key))) {
607
+ throw new LaneSchemaError(`${field} has an unknown event or field`, { path: filePath, field });
608
+ }
609
+ for (const key of ["approval_id", ...(entry.event === "approved" ? ["checkpoint", "reason", "actor"] : ["envelope_ref"])]) {
610
+ if (typeof entry[key] !== "string" || entry[key].trim().length === 0) {
611
+ throw new LaneSchemaError(`${field}.${key} must be a non-empty string`, { path: filePath, field });
612
+ }
613
+ }
614
+ if (entry.event === "approved" && entry.worker_yaml_sha256 !== undefined &&
615
+ (typeof entry.worker_yaml_sha256 !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.worker_yaml_sha256))) {
616
+ throw new LaneSchemaError(`${field}.worker_yaml_sha256 must be a SHA-256 hash`, {
617
+ path: filePath,
618
+ field: `${field}.worker_yaml_sha256`,
619
+ });
620
+ }
621
+ if (entry.event === "approved" && entry.registry_worker_id !== undefined &&
622
+ (typeof entry.registry_worker_id !== "string" || entry.registry_worker_id.trim().length === 0)) {
623
+ throw new LaneSchemaError(`${field}.registry_worker_id must be a non-empty string`, {
624
+ path: filePath,
625
+ field: `${field}.registry_worker_id`,
626
+ });
627
+ }
628
+ requireTimestamp(entry.at, `${field}.at`, filePath);
629
+ }
630
+ }
546
631
  parseLaneStories(raw, filePath);
547
632
  if (raw.last_envelope !== undefined) {
548
633
  if (!isPlainObject(raw.last_envelope) || typeof raw.last_envelope.ref !== "string" ||
@@ -920,6 +1005,11 @@ export function mergeLaneRecords(live, sidecars) {
920
1005
  worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
921
1006
  }
922
1007
  : {}),
1008
+ ...(all.some((record) => record.registry_worker_approval_history !== undefined)
1009
+ ? {
1010
+ registry_worker_approval_history: unionHistory(all.map((record) => record.registry_worker_approval_history ?? []), entryIdentity, base.registry_worker_approval_history ?? []),
1011
+ }
1012
+ : {}),
923
1013
  ...(all.some((record) => record.failed_resume_history !== undefined)
924
1014
  ? {
925
1015
  failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
@@ -295,6 +295,31 @@ export interface WorkerProfileRepinHistoryEntry {
295
295
  at: string;
296
296
  reason: string;
297
297
  }
298
+ /** The registry worker identity and approval contract pinned at lane creation. */
299
+ export interface RegistryWorkerRecord {
300
+ id: string;
301
+ path: string;
302
+ worker_yaml_sha256: string;
303
+ approval_required: boolean;
304
+ human_checkpoints: string[];
305
+ }
306
+ /** Append-only approval and consumption events for registry worker checkpoints. */
307
+ export type RegistryWorkerApprovalHistoryEntry = {
308
+ event: "approved";
309
+ approval_id: string;
310
+ checkpoint: string;
311
+ /** Pins this approval to the registry worker version that received it. */
312
+ registry_worker_id?: string;
313
+ worker_yaml_sha256?: string;
314
+ reason: string;
315
+ actor: string;
316
+ at: string;
317
+ } | {
318
+ event: "consumed";
319
+ approval_id: string;
320
+ envelope_ref: string;
321
+ at: string;
322
+ };
298
323
  export interface AttributionHistoryEntry {
299
324
  project_id: string;
300
325
  story_id: string;
@@ -390,6 +415,10 @@ export interface LaneRecord {
390
415
  hire_grant?: number;
391
416
  /** Content hash of the profile last applied to this lane. */
392
417
  worker_profile_hash?: string;
418
+ /** Registry persona selected by the profile, pinned at lane creation. */
419
+ registry_worker?: RegistryWorkerRecord;
420
+ /** Append-only approvals and one-envelope consumption records. */
421
+ registry_worker_approval_history?: RegistryWorkerApprovalHistoryEntry[];
393
422
  /** Bounded audit history of authorized Outpost profile hash re-pins. */
394
423
  worker_profile_repin_history?: WorkerProfileRepinHistoryEntry[];
395
424
  /** Profile fields intentionally unsupported by the selected provider. */
@@ -223,7 +223,7 @@ export const LANE_FIELD_WRITERS = {
223
223
  state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
224
224
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
225
225
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
226
- priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
226
+ priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
227
  keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
228
228
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
229
229
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
@@ -164,6 +164,8 @@ export declare function applyTerminalToLane(hqRoot: string, laneId: string, clas
164
164
  export declare function applyTerminalToLaneWithResult(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): {
165
165
  record: LaneRecord;
166
166
  changed: boolean;
167
+ envelope?: LaneEnvelope;
168
+ classification: TerminalClassification;
167
169
  };
168
170
  export declare function isPidAlive(pid: number): boolean;
169
171
  export interface LatestEnvelope {