@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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.240.0] — 2026-09-27
6
+
7
+ ### Added
8
+
9
+ - `hq lanes` worker profiles can pin a registry persona from `worker.yaml`.
10
+ The composed prompt includes the worker's instructions, skill paths, done
11
+ criteria, and stop checkpoints. Registry approval requirements are passed to
12
+ workers, enforced on external-action envelopes, and recorded through the new
13
+ senior-only `hq lanes approve` command.
14
+
5
15
  ## [5.239.0] — 2026-09-27
6
16
 
7
17
  ### Added
@@ -7507,6 +7507,25 @@ export declare const COMMAND_CATALOG: readonly [{
7507
7507
  readonly description: "JSON on stdout (always)";
7508
7508
  }];
7509
7509
  readonly subcommands: readonly [];
7510
+ }, {
7511
+ readonly name: "approve-target";
7512
+ readonly description: "";
7513
+ readonly aliases: readonly [];
7514
+ readonly hidden: true;
7515
+ readonly usage: "[options] <lane-id>";
7516
+ readonly arguments: readonly [{
7517
+ readonly name: "lane-id";
7518
+ readonly required: true;
7519
+ readonly variadic: false;
7520
+ }];
7521
+ readonly options: readonly [{
7522
+ readonly flags: "--approval-context <base64url>";
7523
+ readonly description: "Primary-authorized registry worker approval";
7524
+ }, {
7525
+ readonly flags: "--json";
7526
+ readonly description: "JSON on stdout (always)";
7527
+ }];
7528
+ readonly subcommands: readonly [];
7510
7529
  }, {
7511
7530
  readonly name: "reparent-target";
7512
7531
  readonly description: "";
@@ -8193,6 +8212,28 @@ export declare const COMMAND_CATALOG: readonly [{
8193
8212
  readonly description: "JSON on stdout (always)";
8194
8213
  }];
8195
8214
  readonly subcommands: readonly [];
8215
+ }, {
8216
+ readonly name: "approve";
8217
+ readonly description: "Record senior approval for a registry worker checkpoint";
8218
+ readonly aliases: readonly [];
8219
+ readonly hidden: false;
8220
+ readonly usage: "[options] <lane-id>";
8221
+ readonly arguments: readonly [{
8222
+ readonly name: "lane-id";
8223
+ readonly required: true;
8224
+ readonly variadic: false;
8225
+ }];
8226
+ readonly options: readonly [{
8227
+ readonly flags: "--checkpoint <name>";
8228
+ readonly description: "Exact registry worker checkpoint name";
8229
+ }, {
8230
+ readonly flags: "--reason <text>";
8231
+ readonly description: "Why the checkpoint is approved";
8232
+ }, {
8233
+ readonly flags: "--json";
8234
+ readonly description: "JSON on stdout (always)";
8235
+ }];
8236
+ readonly subcommands: readonly [];
8196
8237
  }, {
8197
8238
  readonly name: "ask";
8198
8239
  readonly description: "Queue a mid-run question without blocking the lane";
@@ -9705,6 +9705,31 @@ export const COMMAND_CATALOG = [
9705
9705
  ],
9706
9706
  "subcommands": []
9707
9707
  },
9708
+ {
9709
+ "name": "approve-target",
9710
+ "description": "",
9711
+ "aliases": [],
9712
+ "hidden": true,
9713
+ "usage": "[options] <lane-id>",
9714
+ "arguments": [
9715
+ {
9716
+ "name": "lane-id",
9717
+ "required": true,
9718
+ "variadic": false
9719
+ }
9720
+ ],
9721
+ "options": [
9722
+ {
9723
+ "flags": "--approval-context <base64url>",
9724
+ "description": "Primary-authorized registry worker approval"
9725
+ },
9726
+ {
9727
+ "flags": "--json",
9728
+ "description": "JSON on stdout (always)"
9729
+ }
9730
+ ],
9731
+ "subcommands": []
9732
+ },
9708
9733
  {
9709
9734
  "name": "reparent-target",
9710
9735
  "description": "",
@@ -10601,6 +10626,35 @@ export const COMMAND_CATALOG = [
10601
10626
  ],
10602
10627
  "subcommands": []
10603
10628
  },
10629
+ {
10630
+ "name": "approve",
10631
+ "description": "Record senior approval for a registry worker checkpoint",
10632
+ "aliases": [],
10633
+ "hidden": false,
10634
+ "usage": "[options] <lane-id>",
10635
+ "arguments": [
10636
+ {
10637
+ "name": "lane-id",
10638
+ "required": true,
10639
+ "variadic": false
10640
+ }
10641
+ ],
10642
+ "options": [
10643
+ {
10644
+ "flags": "--checkpoint <name>",
10645
+ "description": "Exact registry worker checkpoint name"
10646
+ },
10647
+ {
10648
+ "flags": "--reason <text>",
10649
+ "description": "Why the checkpoint is approved"
10650
+ },
10651
+ {
10652
+ "flags": "--json",
10653
+ "description": "JSON on stdout (always)"
10654
+ }
10655
+ ],
10656
+ "subcommands": []
10657
+ },
10604
10658
  {
10605
10659
  "name": "ask",
10606
10660
  "description": "Queue a mid-run question without blocking the lane",
@@ -24,6 +24,7 @@ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
24
24
  import { defaultTrackerActor, LANES_TRACKER_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
25
25
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
26
26
  import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
27
+ import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
27
28
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
28
29
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
29
30
  import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
@@ -40,7 +41,7 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
40
41
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
41
42
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
42
43
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
43
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
44
+ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
44
45
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
45
46
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
46
47
  import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
@@ -3059,6 +3060,9 @@ async function runCreate(ctx, opts) {
3059
3060
  worker_profile: profile.id,
3060
3061
  worker_profile_hash: profile.contentHash,
3061
3062
  worker_profile_ignored: ignoredWorkerProfileFields(provider),
3063
+ ...(profile.registryWorker !== undefined
3064
+ ? { registry_worker: registryWorkerRecord(profile.registryWorker) }
3065
+ : {}),
3062
3066
  }
3063
3067
  : {}),
3064
3068
  keep_alive: keepAlive,
@@ -3470,7 +3474,12 @@ function verifyOutpostProfileContent(hqRoot, lane) {
3470
3474
  if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
3471
3475
  return;
3472
3476
  const expectedHash = lane.worker_profile_hash;
3473
- const profile = loadWorkerProfile(hqRoot, lane.worker_profile);
3477
+ const profile = loadWorkerProfileWithRegistry(hqRoot, lane.worker_profile, {
3478
+ laneCompany: lane.company?.slug ?? lane.mesh_company?.slug,
3479
+ ...(lane.registry_worker !== undefined
3480
+ ? { registryWorkerRecord: lane.registry_worker }
3481
+ : {}),
3482
+ });
3474
3483
  if (expectedHash !== profile.contentHash) {
3475
3484
  throw new LanesError("worker_profile_hash_mismatch", `Worker profile ${JSON.stringify(lane.worker_profile)} on Outpost ${lane.host.id} does not match the hash recorded by the creator`, {
3476
3485
  lane_id: lane.lane_id,
@@ -3692,7 +3701,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3692
3701
  ? "grant-target"
3693
3702
  : action === "reparent"
3694
3703
  ? "reparent-target"
3695
- : action,
3704
+ : action === "approve"
3705
+ ? "approve-target"
3706
+ : action,
3696
3707
  laneId: lane.lane_id,
3697
3708
  hqBinary: action === "grant" || action === "reparent"
3698
3709
  ? outpostCliOverride(ctx, outpostId)
@@ -3712,6 +3723,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3712
3723
  grantContext: contexts.grantContext,
3713
3724
  profileRepinContext: contexts.profileRepinContext,
3714
3725
  failedResumeContext: contexts.failedResumeContext,
3726
+ approvalContext: options.approvalContext,
3715
3727
  compact: options.compact,
3716
3728
  }),
3717
3729
  });
@@ -3820,17 +3832,20 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3820
3832
  ? remote.accepted === true
3821
3833
  : action === "grant"
3822
3834
  ? remote.hire_grant === Number(options.hire)
3823
- : action === "reparent"
3824
- ? remote.lane !== null && typeof remote.lane === "object" &&
3825
- Array.isArray(remote.changed_descendant_ids)
3826
- : (((remote.delivery_status === "worker_spawned" ||
3827
- remote.delivery_status === "queued_on_target_account") &&
3828
- remote.lane !== undefined) ||
3829
- (remote.delivery_status === "queued_on_admission" &&
3830
- remote.lane !== null &&
3831
- typeof remote.lane === "object" &&
3832
- !Array.isArray(remote.lane) &&
3833
- remote.lane.state === "queued")));
3835
+ : action === "approve"
3836
+ ? remote.checkpoint === options.checkpoint &&
3837
+ remote.lane !== null && typeof remote.lane === "object" && !Array.isArray(remote.lane)
3838
+ : action === "reparent"
3839
+ ? remote.lane !== null && typeof remote.lane === "object" &&
3840
+ Array.isArray(remote.changed_descendant_ids)
3841
+ : (((remote.delivery_status === "worker_spawned" ||
3842
+ remote.delivery_status === "queued_on_target_account") &&
3843
+ remote.lane !== undefined) ||
3844
+ (remote.delivery_status === "queued_on_admission" &&
3845
+ remote.lane !== null &&
3846
+ typeof remote.lane === "object" &&
3847
+ !Array.isArray(remote.lane) &&
3848
+ remote.lane.state === "queued")));
3834
3849
  if (!resultConfirmed) {
3835
3850
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3836
3851
  lane_id: lane.lane_id,
@@ -3866,7 +3881,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3866
3881
  });
3867
3882
  return;
3868
3883
  }
3869
- if (action === "reparent" || action === "resume") {
3884
+ if (action === "approve" || action === "reparent" || action === "resume") {
3870
3885
  const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3871
3886
  const descendants = [];
3872
3887
  if (action === "reparent") {
@@ -5189,6 +5204,141 @@ async function runGrant(ctx, laneId, opts) {
5189
5204
  const updated = await applyHireGrant(ctx, laneId, hireGrant, "senior");
5190
5205
  printGrantResult(hqRoot, updated);
5191
5206
  }
5207
+ async function runApprove(ctx, laneId, opts) {
5208
+ const checkpoint = opts.checkpoint?.trim();
5209
+ const reason = opts.reason?.trim();
5210
+ if (!checkpoint) {
5211
+ throw new LanesError("registry_worker_checkpoint_required", "--checkpoint requires a non-empty checkpoint name", { lane_id: laneId });
5212
+ }
5213
+ if (!reason) {
5214
+ throw new LanesError("registry_worker_approval_reason_required", "--reason requires a non-empty explanation", { lane_id: laneId });
5215
+ }
5216
+ assertNoSecretText("approval reason", reason);
5217
+ const hqRoot = ctx.hqRoot();
5218
+ const initial = readLane(hqRoot, laneId);
5219
+ const remoteOutpost = initial.host.kind === "outpost" && !laneIsHostedHere(ctx, initial);
5220
+ if (!initial.registry_worker ||
5221
+ (!initial.registry_worker.approval_required && initial.registry_worker.human_checkpoints.length === 0)) {
5222
+ throw new LanesError("registry_worker_approval_unavailable", `Lane ${laneId} has no registry worker approval gate`, { lane_id: laneId });
5223
+ }
5224
+ const allowed = initial.registry_worker.human_checkpoints.length > 0
5225
+ ? initial.registry_worker.human_checkpoints
5226
+ : ["external_publish"];
5227
+ if (!allowed.includes(checkpoint)) {
5228
+ throw new LanesError("registry_worker_checkpoint_unknown", `Checkpoint ${JSON.stringify(checkpoint)} is not declared by the lane's registry worker`, { lane_id: laneId, checkpoint, accepted: allowed });
5229
+ }
5230
+ if (!callerMatchesSenior(ctx, initial.senior)) {
5231
+ throw new LanesError("registry_worker_approval_unauthorized", `Only senior ${seniorKey(initial.senior)} may approve lane ${laneId}'s registry worker checkpoint`, { lane_id: laneId, senior: initial.senior });
5232
+ }
5233
+ if (remoteOutpost) {
5234
+ const approvalContext = createOutpostRegistryWorkerApprovalContext({
5235
+ lane_id: initial.lane_id,
5236
+ expected_senior: initial.senior,
5237
+ actor: resumeProfileRepinActor(ctx),
5238
+ checkpoint,
5239
+ reason,
5240
+ approval_id: randomBytes(16).toString("hex"),
5241
+ registry_worker_id: initial.registry_worker.id,
5242
+ worker_yaml_sha256: initial.registry_worker.worker_yaml_sha256,
5243
+ at: ctx.now().toISOString(),
5244
+ });
5245
+ await dispatchOutpostLaneCommand(ctx, initial, "approve", {
5246
+ approvalContext,
5247
+ checkpoint,
5248
+ });
5249
+ return;
5250
+ }
5251
+ assertLaneRunsHereForOperatorMutation(ctx, initial, "approve");
5252
+ const approvalId = randomBytes(16).toString("hex");
5253
+ const at = ctx.now().toISOString();
5254
+ const actor = resumeProfileRepinActor(ctx);
5255
+ const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
5256
+ assertLaneRunsHereForOperatorMutation(ctx, current, "approve");
5257
+ if (!callerMatchesSenior(ctx, current.senior)) {
5258
+ throw new LanesError("registry_worker_approval_unauthorized", `Only senior ${seniorKey(current.senior)} may approve lane ${laneId}'s registry worker checkpoint`, { lane_id: laneId, senior: current.senior });
5259
+ }
5260
+ if (JSON.stringify(current.registry_worker) !== JSON.stringify(initial.registry_worker) ||
5261
+ !current.registry_worker ||
5262
+ !current.registry_worker.human_checkpoints.concat(current.registry_worker.approval_required && current.registry_worker.human_checkpoints.length === 0
5263
+ ? ["external_publish"]
5264
+ : []).includes(checkpoint)) {
5265
+ throw new LanesError("registry_worker_approval_stale", `Lane ${laneId}'s registry worker approval contract changed before the checkpoint was recorded`, { lane_id: laneId });
5266
+ }
5267
+ return {
5268
+ ...current,
5269
+ registry_worker_approval_history: appendCappedHistory(current.registry_worker_approval_history ?? [], {
5270
+ event: "approved",
5271
+ approval_id: approvalId,
5272
+ checkpoint,
5273
+ registry_worker_id: current.registry_worker.id,
5274
+ worker_yaml_sha256: current.registry_worker.worker_yaml_sha256,
5275
+ reason,
5276
+ actor,
5277
+ at,
5278
+ }),
5279
+ };
5280
+ });
5281
+ printJson({ ok: true, action: "approve", lane_id: laneId, checkpoint, lane: updated });
5282
+ }
5283
+ async function runApproveOwnerTarget(ctx, laneId, opts) {
5284
+ if (!ctx.isSsmOwnerDispatch()) {
5285
+ throw new LanesError("registry_worker_approval_dispatch_unauthorized", "The Outpost approval target is available only to an SSM Run Command dispatch", { lane_id: laneId });
5286
+ }
5287
+ if (opts.approvalContext === undefined) {
5288
+ throw new LanesError("registry_worker_approval_context_invalid", "--approval-context is required on the Outpost owner target", { lane_id: laneId });
5289
+ }
5290
+ const authorization = parseOutpostRegistryWorkerApprovalContext(opts.approvalContext, laneId);
5291
+ assertNoSecretText("approval reason", authorization.reason);
5292
+ const hqRoot = ctx.hqRoot();
5293
+ const validateOwnerState = (current) => {
5294
+ if (current.host.kind !== "outpost" || !laneIsHostedHere(ctx, current)) {
5295
+ throw new LanesError("lane_remote", `Outpost approval target cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: current.host });
5296
+ }
5297
+ if (!sameHireGrantSenior(current.senior, authorization.expected_senior) ||
5298
+ current.registry_worker?.id !== authorization.registry_worker_id ||
5299
+ current.registry_worker.worker_yaml_sha256 !== authorization.worker_yaml_sha256) {
5300
+ throw new LanesError("registry_worker_approval_stale", `Lane ${laneId}'s senior or registry worker changed after approval was authorized`, { lane_id: laneId });
5301
+ }
5302
+ if (!current.registry_worker ||
5303
+ (!current.registry_worker.approval_required && current.registry_worker.human_checkpoints.length === 0)) {
5304
+ throw new LanesError("registry_worker_approval_unavailable", `Lane ${laneId} has no registry worker approval gate`, { lane_id: laneId });
5305
+ }
5306
+ const allowed = current.registry_worker.human_checkpoints.length > 0
5307
+ ? current.registry_worker.human_checkpoints
5308
+ : ["external_publish"];
5309
+ if (!allowed.includes(authorization.checkpoint)) {
5310
+ throw new LanesError("registry_worker_approval_stale", `Lane ${laneId}'s registry worker no longer declares checkpoint ${JSON.stringify(authorization.checkpoint)}`, { lane_id: laneId, checkpoint: authorization.checkpoint });
5311
+ }
5312
+ };
5313
+ validateOwnerState(readLane(hqRoot, laneId));
5314
+ const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
5315
+ validateOwnerState(current);
5316
+ const history = current.registry_worker_approval_history ?? [];
5317
+ if (history.some((entry) => entry.event === "approved" && entry.approval_id === authorization.approval_id)) {
5318
+ return current;
5319
+ }
5320
+ return {
5321
+ ...current,
5322
+ registry_worker_approval_history: appendCappedHistory(history, {
5323
+ event: "approved",
5324
+ approval_id: authorization.approval_id,
5325
+ checkpoint: authorization.checkpoint,
5326
+ registry_worker_id: authorization.registry_worker_id,
5327
+ worker_yaml_sha256: authorization.worker_yaml_sha256,
5328
+ reason: authorization.reason,
5329
+ actor: authorization.actor,
5330
+ at: authorization.at,
5331
+ }),
5332
+ };
5333
+ });
5334
+ printJson({
5335
+ ok: true,
5336
+ action: "approve",
5337
+ lane_id: laneId,
5338
+ checkpoint: authorization.checkpoint,
5339
+ lane: updated,
5340
+ });
5341
+ }
5192
5342
  async function runGrantOwnerTarget(ctx, laneId, opts) {
5193
5343
  if (!ctx.isSsmOwnerDispatch()) {
5194
5344
  throw new LanesError("hire_grant_dispatch_unauthorized", "The Outpost grant target is available only to an SSM Run Command dispatch", { lane_id: laneId });
@@ -5350,7 +5500,9 @@ async function runAdmit(ctx, laneId, opts) {
5350
5500
  }, ctx.sleep);
5351
5501
  }
5352
5502
  function resolveResumeProfile(ctx, lane, workerId) {
5353
- const worker = resolveLaneWorker(ctx.hqRoot(), workerId.trim(), lane.company?.slug ?? lane.mesh_company?.slug);
5503
+ const worker = resolveLaneWorker(ctx.hqRoot(), workerId.trim(), lane.company?.slug ?? lane.mesh_company?.slug, undefined, lane.worker_profile === workerId.trim() && lane.registry_worker !== undefined
5504
+ ? { registryWorkerRecord: lane.registry_worker }
5505
+ : {});
5354
5506
  if (worker.profile === undefined) {
5355
5507
  throw new LanesError("worker_profile_required", `Worker ${JSON.stringify(workerId)} resolves to a registry worker, not a reusable provider profile; resume --worker requires personal/workers/<id>/meta.yaml`, { worker: workerId, path: worker.path });
5356
5508
  }
@@ -5393,6 +5545,9 @@ function applyResumeProfileAndEffort(current, next, input) {
5393
5545
  next.worker_pack = input.profile.pack;
5394
5546
  next.worker_profile = profile.id;
5395
5547
  next.worker_profile_hash = profile.contentHash;
5548
+ next.registry_worker = profile.registryWorker !== undefined
5549
+ ? registryWorkerRecord(profile.registryWorker)
5550
+ : undefined;
5396
5551
  if (input.profileRepin !== undefined &&
5397
5552
  current.host.kind === "outpost" &&
5398
5553
  current.worker_profile_hash !== profile.contentHash) {
@@ -9282,6 +9437,13 @@ export function registerLanesCommand(parent, deps = {}) {
9282
9437
  .action(wrap(async (opts) => {
9283
9438
  await runCreate(ctx, opts);
9284
9439
  }));
9440
+ lanes
9441
+ .command("approve-target", { hidden: true })
9442
+ .argument("<lane-id>", "Lane id")
9443
+ .requiredOption("--approval-context <base64url>", "Primary-authorized registry worker approval")
9444
+ .action(wrap(async (laneId, opts) => {
9445
+ await runApproveOwnerTarget(ctx, laneId, opts);
9446
+ }));
9285
9447
  lanes
9286
9448
  .command("reparent-target", { hidden: true })
9287
9449
  .argument("<lane-id>", "Lane id")
@@ -9581,6 +9743,15 @@ export function registerLanesCommand(parent, deps = {}) {
9581
9743
  .action(wrap(async (laneId, options) => {
9582
9744
  await runStop(ctx, laneId, options.reason);
9583
9745
  }));
9746
+ lanes
9747
+ .command("approve")
9748
+ .description("Record senior approval for a registry worker checkpoint")
9749
+ .argument("<lane-id>", "Lane id")
9750
+ .requiredOption("--checkpoint <name>", "Exact registry worker checkpoint name")
9751
+ .requiredOption("--reason <text>", "Why the checkpoint is approved")
9752
+ .action(wrap(async (laneId, opts) => {
9753
+ await runApprove(ctx, laneId, opts);
9754
+ }));
9584
9755
  lanes
9585
9756
  .command("ask")
9586
9757
  .description("Queue a mid-run question without blocking the lane")
@@ -0,0 +1,8 @@
1
+ import type { LaneEnvelope, LaneRecord } from "./types.js";
2
+ export interface RegistryApprovalGateResult {
3
+ envelope: LaneEnvelope;
4
+ approvedIdsToConsume: string[];
5
+ violation: boolean;
6
+ }
7
+ export declare function enforceRegistryApprovalGate(lane: LaneRecord, envelope: LaneEnvelope): RegistryApprovalGateResult;
8
+ //# sourceMappingURL=approval-gate.d.ts.map
@@ -0,0 +1,90 @@
1
+ function record(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value)
3
+ ? value
4
+ : undefined;
5
+ }
6
+ function text(value) {
7
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
8
+ }
9
+ function externalActionCheckpoint(artifactValue) {
10
+ const artifact = record(artifactValue);
11
+ if (!artifact)
12
+ return undefined;
13
+ const kind = (text(artifact.kind) ?? text(artifact.type) ?? "").toLowerCase().replace(/[\s-]+/g, "_");
14
+ const status = (text(artifact.status) ?? text(artifact.state) ?? "").toLowerCase();
15
+ const hasDate = (...keys) => keys.some((key) => text(artifact[key]) !== undefined);
16
+ const url = text(artifact.url) ?? "";
17
+ const mergedPr = (["pr", "pull_request", "merged_pr"].includes(kind) ||
18
+ /\/(?:pull|pulls|merge_requests)\/\d+(?:\/|$)/i.test(url)) &&
19
+ (status === "merged" || artifact.merged === true || hasDate("merged_at")) ||
20
+ artifact.merged === true || status === "merged";
21
+ const pushedTag = (["tag", "git_tag"].includes(kind) ||
22
+ text(artifact.tag) !== undefined || text(artifact.tag_name) !== undefined) &&
23
+ (status === "pushed" || artifact.pushed === true || hasDate("pushed_at"));
24
+ const publishedPackage = (["package", "npm_package", "published_package"].includes(kind) ||
25
+ text(artifact.package) !== undefined || text(artifact.package_name) !== undefined) &&
26
+ (status === "published" || artifact.published === true || hasDate("published_at"));
27
+ const deployment = ["deploy", "deployment", "deployed"].includes(kind) &&
28
+ (status === "" || ["deployed", "success", "succeeded", "successful", "complete", "completed", "live"].includes(status) ||
29
+ artifact.deployed === true || hasDate("deployed_at"));
30
+ if (!mergedPr && !pushedTag && !publishedPackage && !deployment)
31
+ return undefined;
32
+ return text(artifact.checkpoint) ?? null;
33
+ }
34
+ function approvedEvents(history, workerId, workerYamlSha256) {
35
+ const consumed = new Set(history.flatMap((entry) => entry.event === "consumed" ? [entry.approval_id] : []));
36
+ return history.filter((entry) => entry.event === "approved" &&
37
+ entry.registry_worker_id === workerId &&
38
+ entry.worker_yaml_sha256 === workerYamlSha256 &&
39
+ !consumed.has(entry.approval_id));
40
+ }
41
+ export function enforceRegistryApprovalGate(lane, envelope) {
42
+ const registryWorker = lane.registry_worker;
43
+ const gateEnabled = registryWorker !== undefined &&
44
+ (registryWorker.approval_required || registryWorker.human_checkpoints.length > 0);
45
+ if (!gateEnabled || envelope.decision !== "done") {
46
+ return { envelope, approvedIdsToConsume: [], violation: false };
47
+ }
48
+ const actionCheckpoints = envelope.artifacts
49
+ .map(externalActionCheckpoint)
50
+ .filter((checkpoint) => checkpoint !== undefined);
51
+ if (actionCheckpoints.length === 0) {
52
+ return { envelope, approvedIdsToConsume: [], violation: false };
53
+ }
54
+ const missingCheckpoint = actionCheckpoints.includes(null);
55
+ const requiredCheckpoints = [...new Set(actionCheckpoints.filter((checkpoint) => checkpoint !== null))];
56
+ const configuredCheckpoints = registryWorker.human_checkpoints.length > 0
57
+ ? new Set(registryWorker.human_checkpoints)
58
+ : new Set(["external_publish"]);
59
+ const approvals = approvedEvents(lane.registry_worker_approval_history ?? [], registryWorker.id, registryWorker.worker_yaml_sha256);
60
+ const used = [];
61
+ const missing = missingCheckpoint ? ["(checkpoint missing)"] : [];
62
+ for (const checkpoint of requiredCheckpoints) {
63
+ if (!configuredCheckpoints.has(checkpoint)) {
64
+ missing.push(checkpoint);
65
+ continue;
66
+ }
67
+ const approval = approvals.find((entry) => entry.checkpoint === checkpoint && !used.includes(entry.approval_id));
68
+ if (!approval)
69
+ missing.push(checkpoint);
70
+ else
71
+ used.push(approval.approval_id);
72
+ }
73
+ if (missing.length === 0) {
74
+ return { envelope, approvedIdsToConsume: used, violation: false };
75
+ }
76
+ return {
77
+ envelope: {
78
+ ...envelope,
79
+ decision: "blocked",
80
+ summary: `External action requires senior approval at checkpoint: ${missing.join(", ")}.`,
81
+ story_status: "review",
82
+ blocked_reason: "approval_gate_violated",
83
+ story_complete: false,
84
+ story_complete_reason: "The reported external action has no matching recorded approval.",
85
+ },
86
+ approvedIdsToConsume: [],
87
+ violation: true,
88
+ };
89
+ }
90
+ //# sourceMappingURL=approval-gate.js.map
@@ -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