@indigoai-us/hq-cli 5.238.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.
@@ -34,6 +34,6 @@ export declare class AccountResolutionError extends LanesError {
34
34
  constructor(error: "unknown_account" | "account_disabled" | "no_enabled_account" | "account_model_unavailable" | "account_quota_exhausted", message: string, details?: Record<string, unknown>);
35
35
  }
36
36
  export declare class WorkerResolutionError extends LanesError {
37
- constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "worker_profile_invalid" | "worker_policy_invalid" | "worker_profile_ambiguous" | "package_workers_unmounted" | "worker_registry_unreadable", message: string, details?: Record<string, unknown>);
37
+ constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "worker_profile_invalid" | "worker_policy_invalid" | "worker_profile_ambiguous" | "package_workers_unmounted" | "worker_registry_unreadable" | "registry_worker_not_found" | "registry_worker_company_mismatch" | "registry_worker_inactive" | "registry_worker_invalid" | "registry_worker_unreadable" | "registry_worker_ambiguous", message: string, details?: Record<string, unknown>);
38
38
  }
39
39
  //# sourceMappingURL=errors.d.ts.map
@@ -56,8 +56,10 @@ export type { KnownModels, KnownModelsDeps, ProbeCommand, ProbeResult } from "./
56
56
  export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
57
57
  export type { SessionCopyPlan, SessionCopyResult } from "./move-account.js";
58
58
  export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
59
- export { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, personalWorkerProfileDir, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
59
+ export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
60
+ export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
60
61
  export type { WorkerProfile, WorkerProfileMeta, WorkerProfileRuntime, } from "./profile.js";
62
+ export type { RegistryPersona } from "./registry-persona.js";
61
63
  export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
62
64
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
63
65
  export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
@@ -31,7 +31,8 @@ export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeRe
31
31
  export { CODEX_MODELS_CACHE_NAME, MODEL_PROBE_TIMEOUT_MS, claudeKnownModels, codexKnownModels, defaultProbe, grokKnownModels, knownModels, modelIsKnown, parseClaudeHelpModels, parseGrokModelsOutput, validateLaneModel, } from "./models.js";
32
32
  export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
33
33
  export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
34
- export { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, personalWorkerProfileDir, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
34
+ export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
35
+ export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
35
36
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
36
37
  export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
37
38
  export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target" | "reparent-target";
30
+ action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target" | "reparent-target" | "approve-target";
31
31
  laneId: string;
32
32
  hqBinary?: string;
33
33
  text?: string;
@@ -43,6 +43,7 @@ export interface OutpostLaneCommand {
43
43
  hire?: string;
44
44
  grantContext?: string;
45
45
  profileRepinContext?: string;
46
+ approvalContext?: string;
46
47
  senior?: string;
47
48
  reason?: string;
48
49
  compact?: boolean;
@@ -151,6 +151,7 @@ export function buildOutpostLaneCommand(input) {
151
151
  ["--hire", input.hire],
152
152
  ["--grant-context", input.grantContext],
153
153
  ["--profile-repin-context", input.profileRepinContext],
154
+ ["--approval-context", input.approvalContext],
154
155
  ["--failed-resume-context", input.failedResumeContext],
155
156
  ["--senior", input.senior],
156
157
  ["--reason", input.reason],
@@ -0,0 +1,18 @@
1
+ import { type SeniorRef } from "./types.js";
2
+ declare const APPROVAL_CONTEXT_SCHEMA: "hq-lane-registry-worker-approval-context.v1";
3
+ export interface OutpostRegistryWorkerApprovalContext {
4
+ schema_version: typeof APPROVAL_CONTEXT_SCHEMA;
5
+ lane_id: string;
6
+ expected_senior: SeniorRef;
7
+ actor: string;
8
+ checkpoint: string;
9
+ reason: string;
10
+ approval_id: string;
11
+ registry_worker_id: string;
12
+ worker_yaml_sha256: string;
13
+ at: string;
14
+ }
15
+ export declare function createOutpostRegistryWorkerApprovalContext(input: Omit<OutpostRegistryWorkerApprovalContext, "schema_version">): string;
16
+ export declare function parseOutpostRegistryWorkerApprovalContext(encoded: string, expectedLaneId: string): OutpostRegistryWorkerApprovalContext;
17
+ export {};
18
+ //# sourceMappingURL=outpost-registry-worker-approval.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { LanesError } from "./errors.js";
2
+ import { isSeniorKind } from "./types.js";
3
+ const APPROVAL_CONTEXT_SCHEMA = "hq-lane-registry-worker-approval-context.v1";
4
+ const MAX_CONTEXT_BYTES = 16 * 1024;
5
+ function contextError(message) {
6
+ return new LanesError("registry_worker_approval_context_invalid", message, {});
7
+ }
8
+ function copySenior(senior) {
9
+ return senior.id === undefined
10
+ ? { kind: senior.kind }
11
+ : { kind: senior.kind, id: senior.id };
12
+ }
13
+ function parseSenior(value) {
14
+ if (value === null || typeof value !== "object" || Array.isArray(value))
15
+ return null;
16
+ const record = value;
17
+ if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
18
+ return null;
19
+ if (!isSeniorKind(record.kind))
20
+ return null;
21
+ if (record.id !== undefined &&
22
+ (typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
23
+ return null;
24
+ if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
25
+ return null;
26
+ if (record.kind === "user" && record.id !== undefined)
27
+ return null;
28
+ return copySenior({ kind: record.kind, ...(record.id === undefined ? {} : { id: record.id }) });
29
+ }
30
+ function nonEmpty(value) {
31
+ return typeof value === "string" && value.trim().length > 0;
32
+ }
33
+ export function createOutpostRegistryWorkerApprovalContext(input) {
34
+ if (!nonEmpty(input.lane_id) ||
35
+ !nonEmpty(input.actor) ||
36
+ !nonEmpty(input.checkpoint) ||
37
+ !nonEmpty(input.reason) ||
38
+ !nonEmpty(input.approval_id) ||
39
+ !nonEmpty(input.registry_worker_id) ||
40
+ !/^sha256:[a-f0-9]{64}$/.test(input.worker_yaml_sha256) ||
41
+ !Number.isFinite(Date.parse(input.at))) {
42
+ throw contextError("Outpost registry worker approval fields are incomplete");
43
+ }
44
+ const context = {
45
+ schema_version: APPROVAL_CONTEXT_SCHEMA,
46
+ ...input,
47
+ expected_senior: copySenior(input.expected_senior),
48
+ };
49
+ const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
50
+ if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
51
+ throw contextError(`Registry worker approval context for lane ${input.lane_id} is too large`);
52
+ }
53
+ return encoded;
54
+ }
55
+ export function parseOutpostRegistryWorkerApprovalContext(encoded, expectedLaneId) {
56
+ if (encoded.length === 0 ||
57
+ encoded.length > MAX_CONTEXT_BYTES ||
58
+ !/^[A-Za-z0-9_-]+$/.test(encoded) ||
59
+ Buffer.from(encoded, "base64url").toString("base64url") !== encoded) {
60
+ throw contextError("Registry worker approval context must be bounded canonical base64url text");
61
+ }
62
+ let raw;
63
+ try {
64
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(encoded, "base64url"));
65
+ raw = JSON.parse(decoded);
66
+ }
67
+ catch {
68
+ throw contextError("Registry worker approval context is not valid UTF-8 JSON");
69
+ }
70
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
71
+ throw contextError("Registry worker approval context must be a JSON object");
72
+ }
73
+ const record = raw;
74
+ const allowed = [
75
+ "schema_version", "lane_id", "expected_senior", "actor", "checkpoint", "reason",
76
+ "approval_id", "registry_worker_id", "worker_yaml_sha256", "at",
77
+ ];
78
+ if (Object.keys(record).some((key) => !allowed.includes(key))) {
79
+ throw contextError("Registry worker approval context contains unknown fields");
80
+ }
81
+ const expectedSenior = parseSenior(record.expected_senior);
82
+ if (record.schema_version !== APPROVAL_CONTEXT_SCHEMA ||
83
+ record.lane_id !== expectedLaneId ||
84
+ expectedSenior === null ||
85
+ !nonEmpty(record.actor) ||
86
+ !nonEmpty(record.checkpoint) ||
87
+ !nonEmpty(record.reason) ||
88
+ !nonEmpty(record.approval_id) ||
89
+ !nonEmpty(record.registry_worker_id) ||
90
+ typeof record.worker_yaml_sha256 !== "string" ||
91
+ !/^sha256:[a-f0-9]{64}$/.test(record.worker_yaml_sha256) ||
92
+ typeof record.at !== "string" ||
93
+ !Number.isFinite(Date.parse(record.at))) {
94
+ throw contextError(`Registry worker approval context is invalid for lane ${expectedLaneId}`);
95
+ }
96
+ return {
97
+ schema_version: APPROVAL_CONTEXT_SCHEMA,
98
+ lane_id: expectedLaneId,
99
+ expected_senior: expectedSenior,
100
+ actor: record.actor,
101
+ checkpoint: record.checkpoint,
102
+ reason: record.reason,
103
+ approval_id: record.approval_id,
104
+ registry_worker_id: record.registry_worker_id,
105
+ worker_yaml_sha256: record.worker_yaml_sha256,
106
+ at: record.at,
107
+ };
108
+ }
109
+ //# sourceMappingURL=outpost-registry-worker-approval.js.map
@@ -7,6 +7,9 @@
7
7
  * must be re-read for every spawn.
8
8
  */
9
9
  import { type LaneRecord, type LaneProvider } from "./types.js";
10
+ import type { RegistryWorkerRecord } from "./types.js";
11
+ import { type RegistryPersona } from "./registry-persona.js";
12
+ export declare const REGISTRY_PERSONA_PROMPT_MAX_CHARS = 12000;
10
13
  export interface WorkerProfileMeta {
11
14
  version?: string;
12
15
  provider: LaneProvider;
@@ -18,6 +21,7 @@ export interface WorkerProfileMeta {
18
21
  source?: string;
19
22
  accounts?: string[];
20
23
  keep_alive?: boolean;
24
+ registry_worker?: string;
21
25
  }
22
26
  export interface WorkerProfile {
23
27
  id: string;
@@ -28,6 +32,7 @@ export interface WorkerProfile {
28
32
  policyFiles: string[];
29
33
  hookFiles: string[];
30
34
  contentHash: string;
35
+ registryWorker?: RegistryPersona;
31
36
  }
32
37
  export interface WorkerProfileRuntime {
33
38
  settingsPath?: string;
@@ -41,6 +46,11 @@ export declare function personalWorkerProfileDir(hqRoot: string, id: string): st
41
46
  /** A meta.yaml file is the explicit marker for a personal worker profile. */
42
47
  export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
43
48
  export declare function loadWorkerProfile(hqRoot: string, id: string, options?: LoadWorkerProfileOptions): WorkerProfile;
49
+ export declare function loadWorkerProfileWithRegistry(hqRoot: string, id: string, options?: {
50
+ laneCompany?: string;
51
+ registryWorkerRecord?: RegistryWorkerRecord;
52
+ onUnknownMetaFields?: (fields: readonly string[]) => void;
53
+ }): WorkerProfile;
44
54
  export interface EffectiveLaneKeepAlive {
45
55
  enabled: boolean;
46
56
  source: "lane" | "none";
@@ -13,11 +13,13 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
13
13
  import { WorkerResolutionError } from "./errors.js";
14
14
  import { PROVIDER_EFFORT_LEVELS } from "./effort.js";
15
15
  import { LANE_PROVIDERS, isLaneProvider, } from "./types.js";
16
+ import { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
16
17
  const PROFILE_META = "meta.yaml";
17
18
  const PROFILE_SYSTEM_PROMPT = "system-prompt.md";
18
19
  const PROFILE_SETTINGS = "settings.json";
19
20
  const PROFILE_POLICIES = "policies";
20
21
  const PROFILE_HOOKS = "hooks";
22
+ export const REGISTRY_PERSONA_PROMPT_MAX_CHARS = 12_000;
21
23
  const PROFILE_META_KEYS = new Set([
22
24
  "version",
23
25
  "provider",
@@ -29,6 +31,7 @@ const PROFILE_META_KEYS = new Set([
29
31
  "source",
30
32
  "accounts",
31
33
  "keep_alive",
34
+ "registry_worker",
32
35
  ]);
33
36
  function isPlainObject(value) {
34
37
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -214,6 +217,7 @@ function parseMeta(id, dir, raw, options = {}) {
214
217
  }
215
218
  accounts = raw.accounts.map((item) => item.trim());
216
219
  }
220
+ const registryWorker = optionalString("registry_worker");
217
221
  return {
218
222
  ...(version !== undefined ? { version } : {}),
219
223
  provider: providerValue,
@@ -225,6 +229,7 @@ function parseMeta(id, dir, raw, options = {}) {
225
229
  ...(maxConcurrent !== undefined ? { max_concurrent: maxConcurrent } : {}),
226
230
  ...(source !== undefined ? { source } : {}),
227
231
  ...(accounts !== undefined ? { accounts } : {}),
232
+ ...(registryWorker !== undefined ? { registry_worker: registryWorker } : {}),
228
233
  };
229
234
  }
230
235
  function validatePolicyFrontmatter(id, profileDir, relative) {
@@ -373,6 +378,39 @@ export function loadWorkerProfile(hqRoot, id, options = {}) {
373
378
  contentHash: hashProfileFiles(dir, hashFiles),
374
379
  };
375
380
  }
381
+ export function loadWorkerProfileWithRegistry(hqRoot, id, options = {}) {
382
+ const profile = loadWorkerProfile(hqRoot, id, options);
383
+ const requestedId = profile.meta.registry_worker;
384
+ if (requestedId === undefined) {
385
+ if (options.registryWorkerRecord !== undefined) {
386
+ throw new WorkerResolutionError("registry_worker_invalid", `Worker profile ${JSON.stringify(id)} no longer names its recorded registry worker`, { worker: id, registry_worker: options.registryWorkerRecord.id });
387
+ }
388
+ return profile;
389
+ }
390
+ const persona = options.registryWorkerRecord !== undefined
391
+ ? (() => {
392
+ if (options.registryWorkerRecord?.id !== requestedId) {
393
+ throw new WorkerResolutionError("registry_worker_invalid", `Worker profile ${JSON.stringify(id)} names registry worker ${JSON.stringify(requestedId)}, but the lane records ${JSON.stringify(options.registryWorkerRecord?.id)}`, {
394
+ worker: id,
395
+ profile_registry_worker: requestedId,
396
+ lane_registry_worker: options.registryWorkerRecord?.id,
397
+ });
398
+ }
399
+ return loadRegistryPersonaFromRecord(hqRoot, options.registryWorkerRecord);
400
+ })()
401
+ : resolveRegistryPersona(hqRoot, requestedId, options.laneCompany);
402
+ const record = registryWorkerRecord(persona);
403
+ const hash = createHash("sha256")
404
+ .update(profile.contentHash)
405
+ .update("\0registry_worker\0")
406
+ .update(JSON.stringify({ id: record.id, path: record.path, worker_yaml_sha256: record.worker_yaml_sha256 }))
407
+ .digest("hex");
408
+ return {
409
+ ...profile,
410
+ registryWorker: persona,
411
+ contentHash: `sha256:${hash}`,
412
+ };
413
+ }
376
414
  /** Resolve keep-alive from the lane record; profile metadata only seeds new spawns. */
377
415
  export function resolveLaneKeepAlive(_hqRoot, lane) {
378
416
  return lane.keep_alive === undefined
@@ -381,6 +419,9 @@ export function resolveLaneKeepAlive(_hqRoot, lane) {
381
419
  }
382
420
  export function workerProfilePrompt(profile, delivery = "prompt") {
383
421
  const parts = [profile.systemPrompt.trimEnd()];
422
+ if (profile.registryWorker !== undefined) {
423
+ parts.push(formatRegistryWorkerPrompt(profile.registryWorker));
424
+ }
384
425
  if (delivery === "hook")
385
426
  return parts.join("\n");
386
427
  for (const relative of profile.policyFiles) {
@@ -389,6 +430,53 @@ export function workerProfilePrompt(profile, delivery = "prompt") {
389
430
  }
390
431
  return parts.join("\n");
391
432
  }
433
+ function formatRegistryWorkerPrompt(persona) {
434
+ const lines = [
435
+ "## Registry worker",
436
+ `Description: ${persona.description}`,
437
+ "",
438
+ "Instructions:",
439
+ persona.instructions || "No additional instructions were supplied.",
440
+ ];
441
+ if (persona.skillFiles.length > 0) {
442
+ lines.push("", "Skills (path references only; do not inline these files):");
443
+ for (const file of persona.skillFiles)
444
+ lines.push(`- ${file}`);
445
+ }
446
+ if (persona.postExecute.length > 0) {
447
+ lines.push("", "Done criteria:");
448
+ for (const command of persona.postExecute)
449
+ lines.push(`- ${command}`);
450
+ }
451
+ const safetyLines = [];
452
+ if (persona.humanCheckpoints.length > 0) {
453
+ safetyLines.push("Human checkpoints:");
454
+ for (const checkpoint of persona.humanCheckpoints) {
455
+ safetyLines.push(`- STOP before proceeding with ${checkpoint}. Report that this checkpoint is reached to the senior and wait for approval. Do not perform the action before it is approved.`);
456
+ }
457
+ }
458
+ else if (persona.approvalRequired) {
459
+ safetyLines.push("Human checkpoint:", "- STOP before any external publish action. Report checkpoint external_publish to the senior and wait for approval. Do not perform the action before it is approved.");
460
+ }
461
+ if (persona.approvalRequired || persona.humanCheckpoints.length > 0) {
462
+ safetyLines.push("For an approved external action, include the exact approved checkpoint in the artifact's checkpoint field. Each approval applies to the next matching envelope only.");
463
+ }
464
+ if (safetyLines.length > 0)
465
+ lines.push("");
466
+ const complete = [...lines, ...safetyLines].join("\n");
467
+ if (complete.length <= REGISTRY_PERSONA_PROMPT_MAX_CHARS)
468
+ return complete;
469
+ const suffix = [
470
+ "",
471
+ `[Registry worker section truncated at ${REGISTRY_PERSONA_PROMPT_MAX_CHARS} characters; descriptive material, skill paths, or done criteria may be omitted.]`,
472
+ ...(safetyLines.length > 0 ? ["", ...safetyLines] : []),
473
+ ].join("\n");
474
+ const prefixBudget = REGISTRY_PERSONA_PROMPT_MAX_CHARS - suffix.length;
475
+ if (prefixBudget < 0) {
476
+ throw new WorkerResolutionError("registry_worker_invalid", `Registry worker ${JSON.stringify(persona.id)} checkpoint instructions exceed the bounded prompt limit`, { worker: persona.id, max_chars: REGISTRY_PERSONA_PROMPT_MAX_CHARS });
477
+ }
478
+ return `${lines.join("\n").slice(0, prefixBudget).trimEnd()}${suffix}`;
479
+ }
392
480
  export function ignoredWorkerProfileFields(provider) {
393
481
  return provider === "claude" ? [] : ["settings", "hooks"];
394
482
  }
@@ -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