@indigoai-us/hq-cli 5.239.0 → 5.241.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 (34) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/command-catalog.generated.d.ts +41 -0
  3. package/dist/command-catalog.generated.js +54 -0
  4. package/dist/commands/agent-enroll.d.ts +8 -0
  5. package/dist/commands/agent-enroll.js +19 -2
  6. package/dist/commands/lanes.js +194 -17
  7. package/dist/lib/agent-kit/creds.d.ts +30 -2
  8. package/dist/lib/agent-kit/creds.js +75 -6
  9. package/dist/lib/lanes/approval-gate.d.ts +8 -0
  10. package/dist/lib/lanes/approval-gate.js +90 -0
  11. package/dist/lib/lanes/claude-balance.d.ts +2 -0
  12. package/dist/lib/lanes/claude-balance.js +2 -1
  13. package/dist/lib/lanes/errors.d.ts +1 -1
  14. package/dist/lib/lanes/index.d.ts +3 -1
  15. package/dist/lib/lanes/index.js +2 -1
  16. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  17. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  18. package/dist/lib/lanes/outpost-registry-worker-approval.d.ts +18 -0
  19. package/dist/lib/lanes/outpost-registry-worker-approval.js +109 -0
  20. package/dist/lib/lanes/profile.d.ts +10 -0
  21. package/dist/lib/lanes/profile.js +88 -0
  22. package/dist/lib/lanes/reconcile.js +38 -11
  23. package/dist/lib/lanes/registry-persona.d.ts +20 -0
  24. package/dist/lib/lanes/registry-persona.js +329 -0
  25. package/dist/lib/lanes/registry.d.ts +4 -1
  26. package/dist/lib/lanes/registry.js +8 -3
  27. package/dist/lib/lanes/spawn.d.ts +1 -1
  28. package/dist/lib/lanes/spawn.js +28 -7
  29. package/dist/lib/lanes/store.js +90 -0
  30. package/dist/lib/lanes/types.d.ts +29 -0
  31. package/dist/lib/lanes/types.js +1 -1
  32. package/dist/lib/lanes/watcher.d.ts +2 -0
  33. package/dist/lib/lanes/watcher.js +46 -15
  34. package/package.json +1 -1
@@ -11,6 +11,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
11
11
  import { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
12
12
  import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
13
13
  import { effectiveAccountHome } from "./account-home.js";
14
+ import { enforceRegistryApprovalGate } from "./approval-gate.js";
14
15
  import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
15
16
  import { MAX_LANE_DEPTH } from "./constants.js";
16
17
  import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
@@ -1059,13 +1060,22 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1059
1060
  process.stderr.write(`hq lanes reconcile: terminal ask refused for ${lane.lane_id}: ${questionRefusalCode}\n`);
1060
1061
  }
1061
1062
  }
1063
+ let terminalEnvelope = latest?.envelope;
1064
+ let terminalClassification = repairedClassification;
1062
1065
  const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) => {
1066
+ const approvalGate = latest?.envelope
1067
+ ? enforceRegistryApprovalGate(current, latest.envelope)
1068
+ : undefined;
1069
+ terminalEnvelope = approvalGate?.envelope ?? latest?.envelope;
1070
+ terminalClassification = approvalGate?.violation
1071
+ ? { state: "blocked", reason: "approval_gate_violated" }
1072
+ : repairedClassification;
1063
1073
  const next = {
1064
1074
  ...current,
1065
- state: repairedClassification.state,
1075
+ state: terminalClassification.state,
1066
1076
  queued_reason: undefined,
1067
- terminal_reason: repairedClassification.reason ?? "host_lost",
1068
- interrupted_at: repairedClassification.state === "interrupted" ? nowIso : undefined,
1077
+ terminal_reason: terminalClassification.reason ?? "host_lost",
1078
+ interrupted_at: terminalClassification.state === "interrupted" ? nowIso : undefined,
1069
1079
  ...(providerRejection
1070
1080
  ? { terminal_details: providerRejectionDetails(providerRejection, current) }
1071
1081
  : {}),
@@ -1073,8 +1083,16 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1073
1083
  ...(quota?.marker ? { model_quota_marker: quota.marker } : {}),
1074
1084
  last_host_report_at: nowIso,
1075
1085
  };
1076
- if (latest?.envelope) {
1077
- const envelope = latest.envelope;
1086
+ if (approvalGate && !approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
1087
+ next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
1088
+ event: "consumed",
1089
+ approval_id: approvalId,
1090
+ envelope_ref: latest.ref,
1091
+ at: nowIso,
1092
+ }), current.registry_worker_approval_history ?? []);
1093
+ }
1094
+ if (terminalEnvelope && latest) {
1095
+ const envelope = terminalEnvelope;
1078
1096
  next.story_status = envelope.story_status;
1079
1097
  if (envelope.story_complete !== undefined) {
1080
1098
  next.story_complete = envelope.story_complete;
@@ -1095,6 +1113,15 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1095
1113
  : { blocked_reason: undefined }),
1096
1114
  }
1097
1115
  : story);
1116
+ if (approvalGate?.violation) {
1117
+ next.last_envelope = {
1118
+ ref: latest.ref,
1119
+ decision: envelope.decision,
1120
+ at: nowIso,
1121
+ artifacts: envelope.artifacts,
1122
+ blocked_reason: envelope.blocked_reason,
1123
+ };
1124
+ }
1098
1125
  }
1099
1126
  return next;
1100
1127
  }, deps.dryRun);
@@ -1102,20 +1129,20 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1102
1129
  summary.dead_host.push({
1103
1130
  id: lane.lane_id,
1104
1131
  from: "running",
1105
- to: repairedClassification.state,
1106
- reason: repairedClassification.reason ?? "host_lost",
1132
+ to: terminalClassification.state,
1133
+ reason: terminalClassification.reason ?? "host_lost",
1107
1134
  });
1108
- if (latest?.envelope && questionRefusalCode === null) {
1135
+ if (terminalEnvelope && latest && questionRefusalCode === null) {
1109
1136
  const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
1110
1137
  if (mesh) {
1111
1138
  const meshEmitTimeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
1112
1139
  let timer;
1113
1140
  try {
1114
- const questionIds = latest.envelope.decision === "ask"
1115
- ? envelopeQuestionIds(lane.lane_id, latest.ref, latest.envelope)
1141
+ const questionIds = terminalEnvelope.decision === "ask"
1142
+ ? envelopeQuestionIds(lane.lane_id, latest.ref, terminalEnvelope)
1116
1143
  : undefined;
1117
1144
  await Promise.race([
1118
- emitLaneDecisionEvents(mesh, result.record, latest.envelope, { questionIds }),
1145
+ emitLaneDecisionEvents(mesh, result.record, terminalEnvelope, { questionIds }),
1119
1146
  new Promise((_resolve, reject) => {
1120
1147
  timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${meshEmitTimeoutMs}ms`)), meshEmitTimeoutMs);
1121
1148
  }),
@@ -0,0 +1,20 @@
1
+ import type { RegistryWorkerRecord } from "./types.js";
2
+ export interface RegistryPersona {
3
+ id: string;
4
+ /** Registry-relative worker directory. Kept for stable resume resolution. */
5
+ path: string;
6
+ workerYamlSha256: string;
7
+ description: string;
8
+ instructions: string;
9
+ /** Skill files are path references only. Their contents are never inlined. */
10
+ skillFiles: string[];
11
+ postExecute: string[];
12
+ approvalRequired: boolean;
13
+ humanCheckpoints: string[];
14
+ }
15
+ /** Resolve an active registry worker for a lane without company fallback. */
16
+ export declare function resolveRegistryPersona(hqRoot: string, id: string, laneCompany: string | undefined): RegistryPersona;
17
+ /** Re-read the recorded path on resume; registry changes cannot redirect a lane. */
18
+ export declare function loadRegistryPersonaFromRecord(hqRoot: string, record: RegistryWorkerRecord): RegistryPersona;
19
+ export declare function registryWorkerRecord(persona: RegistryPersona): RegistryWorkerRecord;
20
+ //# sourceMappingURL=registry-persona.d.ts.map
@@ -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,