@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.
@@ -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.238.0",
3
+ "version": "5.240.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {