@indigoai-us/hq-cli 5.158.0 → 5.160.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.
@@ -421,6 +421,11 @@ export function parseLaneRecord(raw, filePath) {
421
421
  if (!isLaneProvider(raw.provider)) {
422
422
  throw new LaneSchemaError(`Unknown provider ${JSON.stringify(raw.provider)}`, { path: filePath, value: raw.provider });
423
423
  }
424
+ if (raw.worker_policy_delivery !== undefined &&
425
+ raw.worker_policy_delivery !== "hook" &&
426
+ raw.worker_policy_delivery !== "prompt") {
427
+ 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 });
428
+ }
424
429
  parseLaneStories(raw, filePath);
425
430
  parseOptionalCompanyField(raw, filePath);
426
431
  parseOptionalMeshCompanyField(raw, filePath);
@@ -296,6 +296,8 @@ export interface LaneRecord {
296
296
  worker_profile_hash?: string;
297
297
  /** Profile fields intentionally unsupported by the selected provider. */
298
298
  worker_profile_ignored?: string[];
299
+ /** How policy files were delivered on the most recent profile spawn. */
300
+ worker_policy_delivery?: "hook" | "prompt";
299
301
  provider: LaneProvider;
300
302
  account?: string;
301
303
  /** Eligible accounts for a queue entry created without an explicit account. */
@@ -400,9 +402,9 @@ export interface LaneRecord {
400
402
  /** Cache counters from the last exited run's result. Nothing else from the payload. */
401
403
  last_run_usage?: LaneRunUsage;
402
404
  /**
403
- * Spec §4: one repair turn when the final output fails parseLaneEnvelope.
404
- * Written by the watcher before the repair spawn, updated on the repair
405
- * run's exit. Absent when every envelope this lane wrote parsed first time.
405
+ * Spec §4: one repair turn per expected envelope slot when the final output
406
+ * is invalid or missing. Written before repair spawn and updated on repair
407
+ * exit. Absent when each slot's first envelope parsed successfully.
406
408
  */
407
409
  envelope_repair?: LaneEnvelopeRepair;
408
410
  /** Last successful lane_progress mesh delivery, preserved across watcher restarts. */
@@ -214,12 +214,12 @@ export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spaw
214
214
  /** Field ownership at the durable-record mutation boundary (spec §10.1). */
215
215
  export const LANE_FIELD_WRITERS = {
216
216
  schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
217
- project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator"],
217
+ project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
218
218
  depth: ["command"], senior: ["command"], senior_history: ["command"],
219
219
  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"],
220
220
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
221
221
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
222
- priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"], worker_profile: ["creator"], worker_profile_hash: ["creator", "spawn"], worker_profile_ignored: ["creator", "spawn"],
222
+ 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_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
223
223
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
224
224
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
225
225
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
@@ -174,8 +174,10 @@ export interface LatestEnvelopeFile {
174
174
  path: string;
175
175
  text: string;
176
176
  envelope: LaneEnvelope | null;
177
- /** Parser message when `envelope` is null. */
177
+ /** Parser message, or the missing-slot reason, when `envelope` is null. */
178
178
  error: string | null;
179
+ /** True when the manifest's expected envelope slot was never created. */
180
+ missing?: boolean;
179
181
  }
180
182
  export declare function readLatestEnvelope(hqRoot: string, laneId: string, minName?: string): LaneEnvelope | null;
181
183
  /** Valid-only view over `readLatestEnvelopeFile`; an unparseable file reads as absent. */
@@ -187,6 +189,8 @@ export declare function readLatestEnvelopeRecord(hqRoot: string, laneId: string,
187
189
  * would otherwise be marked done again from the previous one.
188
190
  */
189
191
  export declare function readLatestEnvelopeFile(hqRoot: string, laneId: string, minName?: string): LatestEnvelopeFile | null;
192
+ /** A descriptor for this run's manifest slot when no envelope file was written. */
193
+ export declare function missingExpectedEnvelopeFile(hqRoot: string, laneId: string, envelopePath: string | undefined): LatestEnvelopeFile | null;
190
194
  export interface WatcherLoopDeps {
191
195
  sleep?: (ms: number) => Promise<void>;
192
196
  now?: () => number;
@@ -230,13 +234,15 @@ export type EnvelopeRepairDecision = {
230
234
  errors: string[];
231
235
  };
232
236
  /**
233
- * Spec §4 repair decision for an exited worker whose envelope file does not
234
- * parse. First failure on a resumable provider with a recorded session id:
235
- * record `envelope_repair` on the lane and spawn the repair turn. A provider
236
- * with no session resume gets the same single attempt as a fresh turn. Anything
237
- * else (already attempted, no session id on a resumable provider, spawn
238
- * failure): give up with every error collected, and the caller classifies
239
- * the lane `failed` with reason `envelope_invalid`.
237
+ * Spec §4 repair decision for an exited worker whose expected envelope is
238
+ * invalid or missing. First failure on a resumable provider with a recorded
239
+ * session id: record `envelope_repair` on the lane and spawn the repair turn.
240
+ * A provider with no session resume gets the same single attempt as a fresh
241
+ * turn for invalid files. Anything else (already attempted, no session id on a
242
+ * resumable provider, non-running lane, spawn failure): give up with every
243
+ * error collected. The caller preserves the normal interrupted classification
244
+ * for a missing file and uses `envelope_invalid` only for an existing invalid
245
+ * file.
240
246
  */
241
247
  export declare function decideEnvelopeRepair(hqRoot: string, laneId: string, latest: LatestEnvelopeFile, nowMs: number, spawnRepair: NonNullable<WatcherLoopDeps["spawnRepair"]>, logDecision: (record: Record<string, unknown>) => void): EnvelopeRepairDecision;
242
248
  /** Exported so tests can prove the watcher reads the live record, not spawn env. */
@@ -499,6 +499,25 @@ export function readLatestEnvelopeFile(hqRoot, laneId, minName) {
499
499
  };
500
500
  }
501
501
  }
502
+ /** A descriptor for this run's manifest slot when no envelope file was written. */
503
+ export function missingExpectedEnvelopeFile(hqRoot, laneId, envelopePath) {
504
+ if (!envelopePath || !path.isAbsolute(envelopePath))
505
+ return null;
506
+ const runDir = laneRunDir(hqRoot, laneId);
507
+ const expectedPath = path.resolve(envelopePath);
508
+ const expectedDir = path.resolve(runDir, "envelopes");
509
+ const filename = path.basename(expectedPath);
510
+ if (path.dirname(expectedPath) !== expectedDir || !filename.endsWith(".json"))
511
+ return null;
512
+ return {
513
+ ref: path.posix.join("envelopes", filename),
514
+ path: expectedPath,
515
+ text: "",
516
+ envelope: null,
517
+ error: `Missing expected envelope file: ${expectedPath}`,
518
+ missing: true,
519
+ };
520
+ }
502
521
  /**
503
522
  * Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
504
523
  * the event name and the error message, and never propagates: the lane's
@@ -677,6 +696,9 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
677
696
  }
678
697
  }
679
698
  const latestFile = readLatestEnvelopeFile(hqRoot, laneId, minEnvelopeName);
699
+ const missingExpected = !processAlive && latestFile === null
700
+ ? missingExpectedEnvelopeFile(hqRoot, laneId, manifest.envelope_path)
701
+ : null;
680
702
  const latest = latestFile && latestFile.envelope
681
703
  ? { envelope: latestFile.envelope, ref: latestFile.ref }
682
704
  : null;
@@ -719,30 +741,37 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
719
741
  process.stderr.write(`hq lanes watcher: quota read failed for ${laneId}: ${err instanceof Error ? err.message : String(err)}\n`);
720
742
  }
721
743
  }
722
- // Spec §4: the process exited and its envelope file does not parse.
723
- // Exactly one repair turn, then failed with both errors. A provider
744
+ // Spec §4: the process exited and its envelope file does not parse, or
745
+ // this run never wrote its expected slot. Exactly one repair turn. A provider
724
746
  // rejection skips the repair: the provider refused the turn, so a
725
747
  // second turn on the same model and account would be refused the same
726
748
  // way. A quota signal skips it too: a repair turn on an exhausted account
727
749
  // fails the same way, and would end the lane envelope_invalid without
728
750
  // ever recording the hold. The quota result applies immediately instead.
729
751
  let invalidEnvelope = null;
752
+ const repairCandidate = latestFile ?? missingExpected;
730
753
  if (!processAlive &&
731
754
  !envelope &&
732
755
  !providerRejection &&
733
756
  !quota &&
734
- latestFile &&
735
- latestFile.envelope === null) {
736
- const repair = decideEnvelopeRepair(hqRoot, laneId, latestFile, now(), spawnRepair, logDecision);
737
- if (repair.action === "spawned") {
738
- lastSignalMs = now();
739
- lastAlive = null;
740
- if (progressed)
741
- lastSeenMtime = newestMtime;
742
- await sleep(pollMs);
743
- continue;
757
+ repairCandidate &&
758
+ repairCandidate.envelope === null) {
759
+ const lane = repairCandidate.missing ? readLaneSoft(hqRoot, laneId) : null;
760
+ const missingRepairAllowed = !repairCandidate.missing ||
761
+ (lane !== null && envelopeRepairMode(lane.provider) === "resume");
762
+ if (missingRepairAllowed) {
763
+ const repair = decideEnvelopeRepair(hqRoot, laneId, repairCandidate, now(), spawnRepair, logDecision);
764
+ if (repair.action === "spawned") {
765
+ lastSignalMs = now();
766
+ lastAlive = null;
767
+ if (progressed)
768
+ lastSeenMtime = newestMtime;
769
+ await sleep(pollMs);
770
+ continue;
771
+ }
772
+ if (!repairCandidate.missing)
773
+ invalidEnvelope = { errors: repair.errors };
744
774
  }
745
- invalidEnvelope = { errors: repair.errors };
746
775
  }
747
776
  if (processAlive === false && envelope && latestFile) {
748
777
  markRepairSucceeded(hqRoot, laneId, latestFile.ref);
@@ -919,13 +948,15 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
919
948
  }
920
949
  }
921
950
  /**
922
- * Spec §4 repair decision for an exited worker whose envelope file does not
923
- * parse. First failure on a resumable provider with a recorded session id:
924
- * record `envelope_repair` on the lane and spawn the repair turn. A provider
925
- * with no session resume gets the same single attempt as a fresh turn. Anything
926
- * else (already attempted, no session id on a resumable provider, spawn
927
- * failure): give up with every error collected, and the caller classifies
928
- * the lane `failed` with reason `envelope_invalid`.
951
+ * Spec §4 repair decision for an exited worker whose expected envelope is
952
+ * invalid or missing. First failure on a resumable provider with a recorded
953
+ * session id: record `envelope_repair` on the lane and spawn the repair turn.
954
+ * A provider with no session resume gets the same single attempt as a fresh
955
+ * turn for invalid files. Anything else (already attempted, no session id on a
956
+ * resumable provider, non-running lane, spawn failure): give up with every
957
+ * error collected. The caller preserves the normal interrupted classification
958
+ * for a missing file and uses `envelope_invalid` only for an existing invalid
959
+ * file.
929
960
  */
930
961
  export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair, logDecision) {
931
962
  const at = new Date(nowMs).toISOString();
@@ -933,8 +964,18 @@ export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair,
933
964
  const lane = readLaneSoft(hqRoot, laneId);
934
965
  if (!lane)
935
966
  return { action: "give_up", errors: [error] };
967
+ if (lane.state !== "running") {
968
+ logDecision({ event: "envelope_repair", at, outcome: "skipped", skipped_reason: "lane_not_running", error });
969
+ return { action: "give_up", errors: [error] };
970
+ }
971
+ const runDir = laneRunDir(hqRoot, laneId);
972
+ const manifest = readSpawnManifest(runDir);
936
973
  const prior = lane.envelope_repair;
937
- if (prior && prior.attempted) {
974
+ const repairRef = missingExpectedEnvelopeFile(hqRoot, laneId, manifest.envelope_path)?.ref ??
975
+ prior?.envelope_ref ??
976
+ latest.ref;
977
+ const priorAppliesToSlot = prior?.envelope_ref === undefined || prior.envelope_ref === repairRef;
978
+ if (prior && prior.attempted && priorAppliesToSlot) {
938
979
  // The repair turn ran and its output does not parse either.
939
980
  if (prior.attempted) {
940
981
  const errors = [prior.first_error, error];
@@ -952,21 +993,21 @@ export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair,
952
993
  }
953
994
  return { action: "give_up", errors: [prior.first_error] };
954
995
  }
955
- const runDir = laneRunDir(hqRoot, laneId);
956
996
  // `resume` (claude, codex) continues the recorded session; `fresh_turn`
957
997
  // (grok) runs the repair prompt as a new single turn on the same account.
958
998
  const mode = envelopeRepairMode(lane.provider);
959
999
  let skipped;
960
1000
  let sessionId = null;
961
1001
  if (mode === "resume") {
962
- const result = readWorkerRunResult(runDir, lane.provider, readSpawnManifest(runDir).worker_out_offset ?? 0);
963
- sessionId = result?.session_id ?? null;
1002
+ const result = readWorkerRunResult(runDir, lane.provider, manifest.worker_out_offset ?? 0);
1003
+ sessionId = result?.session_id ??
1004
+ [manifest.resume?.session_id, lane.provider_session_id, lane.last_resume?.session_id].find((candidate) => typeof candidate === "string" && candidate.trim().length > 0) ?? null;
964
1005
  if (sessionId === null)
965
1006
  skipped = result?.session_id_absent ?? "no_result_json";
966
1007
  }
967
1008
  const base = {
968
1009
  first_error: error,
969
- envelope_ref: latest.ref,
1010
+ envelope_ref: repairRef,
970
1011
  at,
971
1012
  };
972
1013
  if (skipped !== undefined) {
@@ -981,6 +1022,7 @@ export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair,
981
1022
  envelopePath: latest.path,
982
1023
  error,
983
1024
  rawText: latest.text,
1025
+ missing: latest.missing,
984
1026
  });
985
1027
  logDecision({ event: "envelope_repair", at, outcome: "spawned", mode, pid, session_id: sessionId, error });
986
1028
  return { action: "spawned" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.158.0",
3
+ "version": "5.160.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {