@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
@@ -543,6 +543,91 @@ export function parseLaneRecord(raw, filePath) {
543
543
  raw.worker_policy_delivery !== "prompt") {
544
544
  throw new LaneSchemaError(`Unknown worker policy delivery mode ${JSON.stringify(raw.worker_policy_delivery)}`, { path: filePath, field: "worker_policy_delivery", value: raw.worker_policy_delivery });
545
545
  }
546
+ if (raw.registry_worker !== undefined) {
547
+ const worker = raw.registry_worker;
548
+ if (!isPlainObject(worker) || Object.keys(worker).some((key) => !["id", "path", "worker_yaml_sha256", "approval_required", "human_checkpoints"].includes(key))) {
549
+ throw new LaneSchemaError("registry_worker must be a pinned registry worker record", {
550
+ path: filePath,
551
+ field: "registry_worker",
552
+ });
553
+ }
554
+ for (const key of ["id", "path"]) {
555
+ if (typeof worker[key] !== "string" || worker[key].trim().length === 0) {
556
+ throw new LaneSchemaError(`registry_worker.${key} must be a non-empty string`, {
557
+ path: filePath,
558
+ field: `registry_worker.${key}`,
559
+ });
560
+ }
561
+ }
562
+ const registryPath = worker.path.replaceAll("\\", "/");
563
+ if (path.posix.isAbsolute(registryPath) || registryPath.split("/").some((part) => part === ".." || part === ".")) {
564
+ throw new LaneSchemaError("registry_worker.path must stay within HQ_ROOT", {
565
+ path: filePath,
566
+ field: "registry_worker.path",
567
+ });
568
+ }
569
+ if (typeof worker.worker_yaml_sha256 !== "string" || !/^sha256:[a-f0-9]{64}$/.test(worker.worker_yaml_sha256)) {
570
+ throw new LaneSchemaError("registry_worker.worker_yaml_sha256 must be a SHA-256 hash", {
571
+ path: filePath,
572
+ field: "registry_worker.worker_yaml_sha256",
573
+ });
574
+ }
575
+ if (typeof worker.approval_required !== "boolean") {
576
+ throw new LaneSchemaError("registry_worker.approval_required must be a boolean", {
577
+ path: filePath,
578
+ field: "registry_worker.approval_required",
579
+ });
580
+ }
581
+ if (!Array.isArray(worker.human_checkpoints) || worker.human_checkpoints.some((checkpoint) => typeof checkpoint !== "string" || checkpoint.trim().length === 0) ||
582
+ new Set(worker.human_checkpoints).size !== worker.human_checkpoints.length) {
583
+ throw new LaneSchemaError("registry_worker.human_checkpoints must be unique non-empty strings", {
584
+ path: filePath,
585
+ field: "registry_worker.human_checkpoints",
586
+ });
587
+ }
588
+ }
589
+ if (raw.registry_worker_approval_history !== undefined) {
590
+ if (!Array.isArray(raw.registry_worker_approval_history)) {
591
+ throw new LaneSchemaError("registry_worker_approval_history must be an array when present", {
592
+ path: filePath,
593
+ field: "registry_worker_approval_history",
594
+ });
595
+ }
596
+ for (const [index, entry] of raw.registry_worker_approval_history.entries()) {
597
+ const field = `registry_worker_approval_history[${index}]`;
598
+ if (!isPlainObject(entry)) {
599
+ throw new LaneSchemaError(`${field} must be an approval audit event`, { path: filePath, field });
600
+ }
601
+ const expected = entry.event === "approved"
602
+ ? ["event", "approval_id", "checkpoint", "registry_worker_id", "worker_yaml_sha256", "reason", "actor", "at"]
603
+ : entry.event === "consumed"
604
+ ? ["event", "approval_id", "envelope_ref", "at"]
605
+ : [];
606
+ if (expected.length === 0 || Object.keys(entry).some((key) => !expected.includes(key))) {
607
+ throw new LaneSchemaError(`${field} has an unknown event or field`, { path: filePath, field });
608
+ }
609
+ for (const key of ["approval_id", ...(entry.event === "approved" ? ["checkpoint", "reason", "actor"] : ["envelope_ref"])]) {
610
+ if (typeof entry[key] !== "string" || entry[key].trim().length === 0) {
611
+ throw new LaneSchemaError(`${field}.${key} must be a non-empty string`, { path: filePath, field });
612
+ }
613
+ }
614
+ if (entry.event === "approved" && entry.worker_yaml_sha256 !== undefined &&
615
+ (typeof entry.worker_yaml_sha256 !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.worker_yaml_sha256))) {
616
+ throw new LaneSchemaError(`${field}.worker_yaml_sha256 must be a SHA-256 hash`, {
617
+ path: filePath,
618
+ field: `${field}.worker_yaml_sha256`,
619
+ });
620
+ }
621
+ if (entry.event === "approved" && entry.registry_worker_id !== undefined &&
622
+ (typeof entry.registry_worker_id !== "string" || entry.registry_worker_id.trim().length === 0)) {
623
+ throw new LaneSchemaError(`${field}.registry_worker_id must be a non-empty string`, {
624
+ path: filePath,
625
+ field: `${field}.registry_worker_id`,
626
+ });
627
+ }
628
+ requireTimestamp(entry.at, `${field}.at`, filePath);
629
+ }
630
+ }
546
631
  parseLaneStories(raw, filePath);
547
632
  if (raw.last_envelope !== undefined) {
548
633
  if (!isPlainObject(raw.last_envelope) || typeof raw.last_envelope.ref !== "string" ||
@@ -920,6 +1005,11 @@ export function mergeLaneRecords(live, sidecars) {
920
1005
  worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
921
1006
  }
922
1007
  : {}),
1008
+ ...(all.some((record) => record.registry_worker_approval_history !== undefined)
1009
+ ? {
1010
+ registry_worker_approval_history: unionHistory(all.map((record) => record.registry_worker_approval_history ?? []), entryIdentity, base.registry_worker_approval_history ?? []),
1011
+ }
1012
+ : {}),
923
1013
  ...(all.some((record) => record.failed_resume_history !== undefined)
924
1014
  ? {
925
1015
  failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
@@ -295,6 +295,31 @@ export interface WorkerProfileRepinHistoryEntry {
295
295
  at: string;
296
296
  reason: string;
297
297
  }
298
+ /** The registry worker identity and approval contract pinned at lane creation. */
299
+ export interface RegistryWorkerRecord {
300
+ id: string;
301
+ path: string;
302
+ worker_yaml_sha256: string;
303
+ approval_required: boolean;
304
+ human_checkpoints: string[];
305
+ }
306
+ /** Append-only approval and consumption events for registry worker checkpoints. */
307
+ export type RegistryWorkerApprovalHistoryEntry = {
308
+ event: "approved";
309
+ approval_id: string;
310
+ checkpoint: string;
311
+ /** Pins this approval to the registry worker version that received it. */
312
+ registry_worker_id?: string;
313
+ worker_yaml_sha256?: string;
314
+ reason: string;
315
+ actor: string;
316
+ at: string;
317
+ } | {
318
+ event: "consumed";
319
+ approval_id: string;
320
+ envelope_ref: string;
321
+ at: string;
322
+ };
298
323
  export interface AttributionHistoryEntry {
299
324
  project_id: string;
300
325
  story_id: string;
@@ -390,6 +415,10 @@ export interface LaneRecord {
390
415
  hire_grant?: number;
391
416
  /** Content hash of the profile last applied to this lane. */
392
417
  worker_profile_hash?: string;
418
+ /** Registry persona selected by the profile, pinned at lane creation. */
419
+ registry_worker?: RegistryWorkerRecord;
420
+ /** Append-only approvals and one-envelope consumption records. */
421
+ registry_worker_approval_history?: RegistryWorkerApprovalHistoryEntry[];
393
422
  /** Bounded audit history of authorized Outpost profile hash re-pins. */
394
423
  worker_profile_repin_history?: WorkerProfileRepinHistoryEntry[];
395
424
  /** Profile fields intentionally unsupported by the selected provider. */
@@ -223,7 +223,7 @@ export const LANE_FIELD_WRITERS = {
223
223
  state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
224
224
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
225
225
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
226
- priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
226
+ priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
227
  keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
228
228
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
229
229
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
@@ -164,6 +164,8 @@ export declare function applyTerminalToLane(hqRoot: string, laneId: string, clas
164
164
  export declare function applyTerminalToLaneWithResult(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): {
165
165
  record: LaneRecord;
166
166
  changed: boolean;
167
+ envelope?: LaneEnvelope;
168
+ classification: TerminalClassification;
167
169
  };
168
170
  export declare function isPidAlive(pid: number): boolean;
169
171
  export interface LatestEnvelope {
@@ -37,7 +37,8 @@ import { LanesError } from "./errors.js";
37
37
  import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
38
38
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, } from "./types.js";
39
39
  import { laneStories, storyStatusForEnvelope } from "./stories.js";
40
- import { readLane, retryUpdateLane, withLaneHireLocks } from "./store.js";
40
+ import { appendCappedHistory, readLane, retryUpdateLane, withLaneHireLocks } from "./store.js";
41
+ import { enforceRegistryApprovalGate } from "./approval-gate.js";
41
42
  import { readWorkerRunResult } from "./delivery.js";
42
43
  import { applyDefaultQuotaHold, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
43
44
  import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, isProviderLoginFailure, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
@@ -311,6 +312,7 @@ export function applyTerminalToLane(hqRoot, laneId, classification, opts = {}) {
311
312
  /** Terminal write outcome for emitters: only the CAS winner may publish mesh effects. */
312
313
  export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}) {
313
314
  const at = opts.at ?? new Date().toISOString();
315
+ const envelopeRef = opts.envelopeRef ?? "envelopes/unknown.json";
314
316
  const existing = readLane(hqRoot, laneId);
315
317
  let questionRefusalCode = null;
316
318
  if (!isTerminalLaneState(existing.state) &&
@@ -334,7 +336,16 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
334
336
  reason: `question_record_refused:${questionRefusalCode}`,
335
337
  };
336
338
  let changed = false;
339
+ let effectiveEnvelope = opts.envelope ?? undefined;
340
+ let effectiveClassification = terminalClassification;
337
341
  const updated = retryUpdateLane(hqRoot, laneId, "watcher", (current) => {
342
+ const approvalGate = opts.envelope !== undefined && opts.envelope !== null
343
+ ? enforceRegistryApprovalGate(current, opts.envelope)
344
+ : undefined;
345
+ effectiveEnvelope = approvalGate?.envelope ?? opts.envelope ?? undefined;
346
+ effectiveClassification = approvalGate?.violation
347
+ ? { state: "blocked", reason: "approval_gate_violated" }
348
+ : terminalClassification;
338
349
  const terminal = current.state === "done" ||
339
350
  current.state === "failed" ||
340
351
  current.state === "interrupted" ||
@@ -343,24 +354,24 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
343
354
  if (terminal)
344
355
  return current;
345
356
  if (current.state === "awaiting_input" || current.state === "blocked") {
346
- if (current.state === classification.state)
357
+ if (current.state === effectiveClassification.state)
347
358
  return current;
348
359
  }
349
360
  const next = {
350
361
  ...current,
351
362
  ...runResultFields(hqRoot, current),
352
- state: terminalClassification.state,
363
+ state: effectiveClassification.state,
353
364
  queued_reason: undefined,
354
365
  stalled: undefined,
355
366
  stalled_at: undefined,
356
367
  stalled_reason: undefined,
357
- interrupted_at: classification.state === "interrupted" ? at : undefined,
368
+ interrupted_at: effectiveClassification.state === "interrupted" ? at : undefined,
358
369
  envelope_validation_errors: opts.envelopeValidationErrors,
359
370
  };
360
371
  changed = true;
361
- if (terminalClassification.reason) {
372
+ if (effectiveClassification.reason) {
362
373
  next.terminal_reason =
363
- terminalClassification.reason;
374
+ effectiveClassification.reason;
364
375
  }
365
376
  if (opts.quota?.quota_reset_at) {
366
377
  next.quota_reset_at = opts.quota.quota_reset_at;
@@ -371,10 +382,18 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
371
382
  if (opts.providerRejection) {
372
383
  next.terminal_details = providerRejectionDetails(opts.providerRejection, current);
373
384
  }
374
- const envelope = opts.envelope;
375
- if (envelope && opts.envelopeRef) {
385
+ const envelope = effectiveEnvelope;
386
+ if (approvalGate && !approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
387
+ next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
388
+ event: "consumed",
389
+ approval_id: approvalId,
390
+ envelope_ref: envelopeRef,
391
+ at,
392
+ }), current.registry_worker_approval_history ?? []);
393
+ }
394
+ if (envelope) {
376
395
  next.last_envelope = {
377
- ref: opts.envelopeRef,
396
+ ref: envelopeRef,
378
397
  decision: envelope.decision,
379
398
  at,
380
399
  artifacts: envelope.artifacts,
@@ -411,7 +430,12 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
411
430
  provider: updated.provider,
412
431
  account: updated.account,
413
432
  });
414
- return { record: updated, changed };
433
+ return {
434
+ record: updated,
435
+ changed,
436
+ ...(effectiveEnvelope !== undefined ? { envelope: effectiveEnvelope } : {}),
437
+ classification: effectiveClassification,
438
+ };
415
439
  }
416
440
  export function isPidAlive(pid) {
417
441
  try {
@@ -1145,6 +1169,8 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1145
1169
  providerRejection,
1146
1170
  });
1147
1171
  const updated = terminalWrite.record;
1172
+ const effectiveEnvelope = terminalWrite.envelope ?? envelope;
1173
+ const effectiveClassification = terminalWrite.classification;
1148
1174
  logDecision({
1149
1175
  event: "terminal_write",
1150
1176
  at: tick.emit.at,
@@ -1155,11 +1181,16 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1155
1181
  // lane_blocked; ask → lane_ask with the queue ids. After the state
1156
1182
  // write, so a mesh failure can never leave the lane unclassified.
1157
1183
  if (mesh && terminalWrite.changed) {
1158
- if (envelope && latest && updated.state === classification.state) {
1159
- const questionIds = envelope.decision === "ask"
1160
- ? envelopeQuestionIds(laneId, latest.ref, envelope, questionIndexes)
1184
+ const envelopeState = effectiveEnvelope?.decision === "ask"
1185
+ ? "awaiting_input"
1186
+ : effectiveEnvelope?.decision === "blocked"
1187
+ ? "blocked"
1188
+ : "done";
1189
+ if (effectiveEnvelope && latest && updated.state === envelopeState) {
1190
+ const questionIds = effectiveEnvelope.decision === "ask"
1191
+ ? envelopeQuestionIds(laneId, latest.ref, effectiveEnvelope, questionIndexes)
1161
1192
  : undefined;
1162
- await emitMeshSoft(`lane_${envelope.decision}`, logDecision, tick.emit.at, (signal) => emitLaneDecisionEvents(mesh, updated, envelope, {
1193
+ await emitMeshSoft(`lane_${effectiveEnvelope.decision}`, logDecision, tick.emit.at, (signal) => emitLaneDecisionEvents(mesh, updated, effectiveEnvelope, {
1163
1194
  questionIds,
1164
1195
  signal,
1165
1196
  storyStatusSuppressions,
@@ -1167,7 +1198,7 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1167
1198
  }
1168
1199
  else if (updated.state === "failed" || updated.state === "interrupted" ||
1169
1200
  updated.state === "cancelled" || updated.state === "killed") {
1170
- await emitMeshSoft(`lane_${updated.state}`, logDecision, tick.emit.at, (signal) => emitLaneTerminalStateEvent(mesh, updated, updated.state, updated.terminal_reason ?? classification.reason, signal), meshRequestTimeoutMs);
1201
+ await emitMeshSoft(`lane_${updated.state}`, logDecision, tick.emit.at, (signal) => emitLaneTerminalStateEvent(mesh, updated, updated.state, updated.terminal_reason ?? effectiveClassification.reason, signal), meshRequestTimeoutMs);
1171
1202
  }
1172
1203
  }
1173
1204
  if (terminalWrite.changed && deps.recoverQueuedMessages) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.239.0",
3
+ "version": "5.241.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {