@indigoai-us/hq-cli 5.345.131 → 5.345.132

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,10 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [5.345.131] — 2026-10-14
5
+ ## [5.345.132] — 2026-10-14
6
6
  - `hq lanes --account auto` and `hq capacity route` no longer refuse every signed-in account when the capacity collector is off or its readings are stale; those accounts are picked with unknown usage, below accounts with fresh headroom.
7
7
  - `hq lanes create` now resumes an earlier worker session only when the project, model, and effort match, and otherwise starts a fresh thread.
8
+ - A loop lane starting its next phase now stays running in `hq lanes status` and `hq lanes list` while the phase resumes, instead of showing done for several seconds.
8
9
  - A loop lane waiting for its next phase now shows as running in `hq lanes status` and `hq lanes list`, and becomes done only after it is stopped.
10
+ - Recognize Git Bash on Git for Windows 2.56+, which reports `MSYSTEM=UCRT64` instead of `MINGW64`, so delegate jq calls keep their byte-exact output and journal titles with newlines get a valid file name on Windows.
9
11
 
10
12
  ## [5.345.130] — 2026-10-14
11
13
  - Add native TypeScript `hq core workflow run` orchestration with the bundled runner as its flag-off fallback.
@@ -36,8 +36,9 @@ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
36
36
  command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
37
37
 
38
38
  # jq.exe on MSYS may use CRT text mode unless --binary is supported.
39
+ # Git for Windows 2.56+ reports MSYSTEM=UCRT64; match every MSYS2 environment.
39
40
  case "${OSTYPE:-}:${MSYSTEM:-}" in
40
- msys*:*|*:MINGW*)
41
+ msys*:*|*:MINGW*|*:UCRT*|*:CLANG*|*:MSYS)
41
42
  # Probe --binary once per script run, in the top-level shell. Most jq
42
43
  # calls run inside $(...) subshells, so a cache filled lazily inside jq()
43
44
  # would be lost and the probe would repeat on every call.
@@ -43,8 +43,9 @@ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
43
43
  command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
44
44
 
45
45
  # jq.exe on MSYS may use CRT text mode unless --binary is supported.
46
+ # Git for Windows 2.56+ reports MSYSTEM=UCRT64; match every MSYS2 environment.
46
47
  case "${OSTYPE:-}:${MSYSTEM:-}" in
47
- msys*:*|*:MINGW*)
48
+ msys*:*|*:MINGW*|*:UCRT*|*:CLANG*|*:MSYS)
48
49
  _hq_jq_binary_supported=""
49
50
  jq() {
50
51
  local stdout_file stderr_file status binary_supported
@@ -37,7 +37,7 @@ warn() { echo "session-journal: $*" >&2; }
37
37
 
38
38
  slugify() {
39
39
  # Win32 rejects newline file-name bytes; otherwise keep the POSIX slug pipeline.
40
- local slug_input="$1"; if [[ "${OSTYPE:-}" == msys* ]] || [[ "${MSYSTEM:-}" == MINGW* ]]; then slug_input="${slug_input//$'\n'/-}"; fi; printf '%s' "$slug_input" \
40
+ local slug_input="$1"; if [[ "${OSTYPE:-}" == msys* ]] || [[ "${MSYSTEM:-}" =~ ^(MINGW|UCRT|CLANG|MSYS) ]]; then slug_input="${slug_input//$'\n'/-}"; fi; printf '%s' "$slug_input" \
41
41
  | tr '[:upper:]' '[:lower:]' \
42
42
  | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \
43
43
  | cut -c1-40
@@ -5,15 +5,11 @@ import type { LaneRecord } from "../lib/lanes/types.js";
5
5
  * alive, not finished. The watcher still writes `done` when the phase's worker
6
6
  * exits (the controller reads that as the phase-complete signal); this moves
7
7
  * the lane back to `running`, the same state an admitted loop lane holds
8
- * before its first phase. Only a stop makes a loop lane `done`.
8
+ * before its first phase. Only a stop makes a loop lane `done`. The next
9
+ * phase resumes the lane from `running` (`loopIdle`), so it is not written
10
+ * `done` again while the loop is active.
9
11
  */
10
12
  export declare function settleLoopLaneIdle(hqRoot: string, laneId: string): LaneRecord;
11
- /**
12
- * The next phase resumes the provider session, which `hq lanes resume` only
13
- * accepts from a resumable state. An idle loop lane sits in `running` with no
14
- * worker, so it is handed to resume as the `done` lane the phase left.
15
- */
16
- export declare function prepareIdleLoopLaneForResume(hqRoot: string, laneId: string): LaneRecord;
17
13
  export interface LoopControllerDeps {
18
14
  resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
19
15
  interruptLane(laneId: string, reason: string, options: {
@@ -20,28 +20,19 @@ function loopRunDir(hqRoot, laneId) {
20
20
  * alive, not finished. The watcher still writes `done` when the phase's worker
21
21
  * exits (the controller reads that as the phase-complete signal); this moves
22
22
  * the lane back to `running`, the same state an admitted loop lane holds
23
- * before its first phase. Only a stop makes a loop lane `done`.
23
+ * before its first phase. Only a stop makes a loop lane `done`. The next
24
+ * phase resumes the lane from `running` (`loopIdle`), so it is not written
25
+ * `done` again while the loop is active.
24
26
  */
25
27
  export function settleLoopLaneIdle(hqRoot, laneId) {
26
- return casLoopLaneState(hqRoot, laneId, "done", "running");
27
- }
28
- /**
29
- * The next phase resumes the provider session, which `hq lanes resume` only
30
- * accepts from a resumable state. An idle loop lane sits in `running` with no
31
- * worker, so it is handed to resume as the `done` lane the phase left.
32
- */
33
- export function prepareIdleLoopLaneForResume(hqRoot, laneId) {
34
- return casLoopLaneState(hqRoot, laneId, "running", "done");
35
- }
36
- function casLoopLaneState(hqRoot, laneId, from, to) {
37
28
  for (let attempt = 0;; attempt += 1) {
38
29
  const current = readLane(hqRoot, laneId);
39
- if (current.loop_mode !== true || current.state !== from)
30
+ if (current.loop_mode !== true || current.state !== "done")
40
31
  return current;
41
32
  try {
42
33
  return updateLane(hqRoot, laneId, current.version, "command", (record) => ({
43
34
  ...record,
44
- state: to,
35
+ state: "running",
45
36
  terminal_reason: undefined,
46
37
  terminal_details: undefined,
47
38
  }));
@@ -188,8 +179,6 @@ export async function runLaneLoopController(ctx, laneId, deps) {
188
179
  }));
189
180
  }
190
181
  else {
191
- if (current.state === "running")
192
- current = prepareIdleLoopLaneForResume(hqRoot, laneId);
193
182
  await deps.resumeLane(laneId, {
194
183
  text: `Read ${briefPath} and follow it as the complete instruction for this phase.`,
195
184
  project: current.project_id,
@@ -197,6 +186,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
197
186
  freshThread: fresh,
198
187
  ...(sessionId ? { loopSessionId: sessionId } : {}),
199
188
  ...(repairError !== undefined ? { loopHandoffRepair: true } : {}),
189
+ ...(current.state === "running" ? { loopIdle: true } : {}),
200
190
  workingDirectory: worktree,
201
191
  });
202
192
  }
@@ -1,6 +1,6 @@
1
1
  import { BALANCE_MOVABLE_STATES } from "../lib/lanes/claude-balance.js";
2
2
  import { LanesError } from "../lib/lanes/errors.js";
3
- import { appendCappedHistory, isFailedQuotaKeepAliveLane, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
3
+ import { appendCappedHistory, isFailedQuotaKeepAliveLane, isLoopIdleResumeLane, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
4
4
  import { parseCliEffort } from "../lib/lanes/effort.js";
5
5
  import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
6
6
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
@@ -164,6 +164,7 @@ export function createLaneResumeHandlers(deps) {
164
164
  }
165
165
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
166
166
  const loopHandoffRepair = isLoopHandoffRepairResume(lane, opts);
167
+ const loopIdle = isLoopIdleResumeLane(lane, opts.loopIdle);
167
168
  if (opts.loopHandoffRepair === true && !loopHandoffRepair) {
168
169
  throw new LanesError("loop_handoff_repair_session_mismatch", "Loop handoff repair requires the awaiting-input loop lane's current provider session", { lane_id: laneId, state: lane.state });
169
170
  }
@@ -250,7 +251,11 @@ export function createLaneResumeHandlers(deps) {
250
251
  }
251
252
  const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
252
253
  ? null
253
- : resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane), allowFresh: opts.freshThread === true });
254
+ : resumeCommandRefusal(lane, {
255
+ allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
256
+ allowFresh: opts.freshThread === true,
257
+ allowLoopIdle: loopIdle,
258
+ });
254
259
  if (refusal !== null) {
255
260
  throw new LanesError(refusal.error, refusal.message, {
256
261
  lane_id: laneId,
@@ -260,7 +265,10 @@ export function createLaneResumeHandlers(deps) {
260
265
  });
261
266
  }
262
267
  assertLaneRunsHereForAdmission(ctx, lane);
263
- const resumedFrom = lane.state;
268
+ // An idle loop lane resumes from the phase that finished; it stays `running`.
269
+ const resumedFrom = loopIdle
270
+ ? "done"
271
+ : lane.state;
264
272
  const text = opts.text?.trim() ? opts.text : undefined;
265
273
  const at = ctx.now().toISOString();
266
274
  const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
@@ -442,6 +450,7 @@ export function createLaneResumeHandlers(deps) {
442
450
  ...(profileRepin !== undefined ? { profileRepin } : {}),
443
451
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
444
452
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
453
+ ...(loopIdle ? { loopIdle: true } : {}),
445
454
  pending: {
446
455
  trigger: "resume",
447
456
  resumed_from: resumedFrom,
@@ -478,6 +487,7 @@ export function createLaneResumeHandlers(deps) {
478
487
  ...(profileRepin !== undefined ? { profileRepin } : {}),
479
488
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
480
489
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
490
+ ...(loopIdle ? { loopIdle: true } : {}),
481
491
  pending: {
482
492
  trigger: "resume",
483
493
  resumed_from: resumedFrom,
@@ -509,6 +519,7 @@ export function createLaneResumeHandlers(deps) {
509
519
  ...(profileRepin !== undefined ? { profileRepin } : {}),
510
520
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
511
521
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
522
+ ...(loopIdle ? { loopIdle: true } : {}),
512
523
  pending: {
513
524
  trigger: "resume",
514
525
  resumed_from: resumedFrom,
@@ -643,6 +654,7 @@ export function createLaneResumeHandlers(deps) {
643
654
  kind: "resume",
644
655
  from: resumedFrom,
645
656
  ...(opts.freshThread === true ? { fresh: true } : {}),
657
+ ...(loopIdle ? { loopIdle: true } : {}),
646
658
  ...(resumedFrom === "awaiting_input" && (pendingQuestions.length === 0 || loopHandoffRepair) ? { allowAwaitingInput: true } : {}),
647
659
  ...(opts.fromFailed === true ? { fromFailed: true } : {}),
648
660
  ...(failedResumeExpectedSenior !== undefined
@@ -83,6 +83,11 @@ export interface LaneResumeOptions {
83
83
  loopSessionId?: string;
84
84
  /** Allows only a section 8 handoff repair to continue a loop lane with a pending question. */
85
85
  loopHandoffRepair?: boolean;
86
+ /**
87
+ * Internal loop runner: start the next phase of a loop lane that is idle in
88
+ * `running` between phases. The lane is resumed from `running` directly.
89
+ */
90
+ loopIdle?: boolean;
86
91
  meshCompany?: string;
87
92
  profileRepinContext?: string;
88
93
  grantContext?: string;
@@ -121,6 +126,8 @@ export interface PrepareAccountMoveInput {
121
126
  refreshProfileSettings?: boolean;
122
127
  /** Provenance recorded for the account this move selects. */
123
128
  accountSource?: "cli" | "balanced" | "profile";
129
+ /** The loop controller's next-phase resume of a loop lane idle in `running`. */
130
+ loopIdle?: boolean;
124
131
  }
125
132
  export interface RecordResumeSetupInput {
126
133
  profile?: ResolvedResumeProfile;
@@ -77,7 +77,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
77
77
  import { listLaneReviews } from "../lib/lanes/reviews.js";
78
78
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
79
79
  import { remoteControlSelectionConfig, remoteControlSessionHome } from "../lib/lanes/remote-control-lane.js";
80
- import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
80
+ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, isLoopIdleResumeLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
81
81
  import { admitQueuedNow } from "../lib/lanes/reconcile.js";
82
82
  import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
83
83
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
@@ -7418,7 +7418,9 @@ async function prepareAccountMove(ctx, lane, input) {
7418
7418
  const now = ctx.now();
7419
7419
  const laneId = lane.lane_id;
7420
7420
  assertLaneRunsHereForAdmission(ctx, lane);
7421
- if (lane.state === "running") {
7421
+ // An idle loop lane is `running` with no worker between phases, so its
7422
+ // session can move before the next phase starts.
7423
+ if (lane.state === "running" && !isLoopIdleResumeLane(lane, input.loopIdle)) {
7422
7424
  throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
7423
7425
  lane_id: laneId,
7424
7426
  state: lane.state,
@@ -68,7 +68,12 @@ export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "n
68
68
  export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
69
69
  /** Additional presentation-only result used by `hq lanes show`. */
70
70
  export type LaneShowDeliveryMechanism = LaneDeliveryMechanism | "questions";
71
- export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details">;
71
+ export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details"> & Partial<Pick<LaneRecord, "loop_mode">>;
72
+ /**
73
+ * A loop lane between phases is `running` with no worker. Only the loop
74
+ * controller's next-phase resume may continue it from that state.
75
+ */
76
+ export declare function isLoopIdleResumeLane(lane: DeliveryLane, loopIdle: boolean | undefined): boolean;
72
77
  export declare function isFailedQuotaKeepAliveLane(lane: DeliveryLane): boolean;
73
78
  /**
74
79
  * `dropbox`: a process is (or is about to be) alive to drain the inbox.
@@ -147,6 +152,12 @@ export type LaunchTrigger = {
147
152
  /** Loop phases may deliberately start a fresh session at a story boundary. */
148
153
  fresh?: true;
149
154
  allowAwaitingInput?: true;
155
+ /**
156
+ * The loop controller starting the next phase of a loop lane that sits
157
+ * in `running` with no worker between phases. The lane stays `running`
158
+ * through the resume; it is never written `done` to get here.
159
+ */
160
+ loopIdle?: true;
150
161
  fromFailed?: true;
151
162
  /** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
152
163
  expectedSenior?: SeniorRef;
@@ -167,6 +178,7 @@ export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
167
178
  allowFailed?: boolean;
168
179
  allowAwaitingInput?: boolean;
169
180
  allowFresh?: boolean;
181
+ allowLoopIdle?: boolean;
170
182
  }): {
171
183
  error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
172
184
  message: string;
@@ -87,6 +87,13 @@ export function isResumeCommandState(state) {
87
87
  return RESUME_COMMAND_STATES.includes(state);
88
88
  }
89
89
  export const LANE_DELIVERY_MECHANISMS = ["dropbox", "resume", "none"];
90
+ /**
91
+ * A loop lane between phases is `running` with no worker. Only the loop
92
+ * controller's next-phase resume may continue it from that state.
93
+ */
94
+ export function isLoopIdleResumeLane(lane, loopIdle) {
95
+ return loopIdle === true && lane.loop_mode === true && lane.state === "running";
96
+ }
90
97
  /** Only the terminal result of a failed automatic envelope repair is recoverable. */
91
98
  function isFailedEnvelopeRepairLane(lane) {
92
99
  return (lane.state === "failed" &&
@@ -356,6 +363,7 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
356
363
  */
357
364
  export function resumeCommandRefusal(lane, options = {}) {
358
365
  if (!isResumeCommandState(lane.state) &&
366
+ !isLoopIdleResumeLane(lane, options.allowLoopIdle) &&
359
367
  !(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
360
368
  !isFailedEnvelopeRepairLane(lane) &&
361
369
  !isFailedQuotaKeepAliveLane(lane) &&
@@ -414,6 +422,7 @@ export function planLaneLaunch(lane, trigger) {
414
422
  allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
415
423
  allowAwaitingInput: trigger.allowAwaitingInput === true,
416
424
  allowFresh: trigger.fresh === true,
425
+ allowLoopIdle: trigger.loopIdle === true,
417
426
  });
418
427
  if (trigger.fresh === true && refusal === null)
419
428
  return { mode: "fresh" };
@@ -446,6 +455,8 @@ export function claimableStatesFor(trigger, lane) {
446
455
  const states = trigger.fromFailed === true || envelopeRepairFailed || (lane !== undefined && isFailedQuotaKeepAliveLane(lane))
447
456
  ? [...RESUME_COMMAND_STATES, "failed"]
448
457
  : [...RESUME_COMMAND_STATES];
458
+ if (lane !== undefined && isLoopIdleResumeLane(lane, trigger.loopIdle))
459
+ states.push("running");
449
460
  return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
450
461
  }
451
462
  default: {
@@ -63,7 +63,7 @@ export type { RegistryPersona } from "./registry-persona.js";
63
63
  export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
64
64
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
65
65
  export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
66
- 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, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
66
+ 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, isLoopIdleResumeLane, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
67
67
  export type { DeliveryLane, LaneDeliveryMechanism, LaneShowDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
68
68
  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";
69
69
  export type { DrainDeps, DrainHookResult, DropboxMessage, ToolActivityRecord, } from "./dropbox.js";
@@ -34,7 +34,7 @@ export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorke
34
34
  export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
35
35
  export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
36
36
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
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, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
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, isLoopIdleResumeLane, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
38
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";
39
39
  export { DRAIN_HOOK_BASENAME, DRAIN_HOOK_REGISTRATION_KINDS, DRAIN_HOOK_TIMEOUT_SECONDS, PROVIDER_REGISTRATION_KIND, drainHookCommand, drainHookRegistrationPath, drainHookShPath, installDrainHook, lanesHookAssetsDir, mergeClaudeSettingsHook, mergeCodexConfigHook, probeDrainHookRegistration, renderDrainHookJson, } from "./hooks-install.js";
40
40
  export { EFFORT_SOURCES, PROVIDER_EFFORT_LEVELS, appendEffortFlag, defaultEffortForModel, parseCliEffort, parseConfigEffort, resolveLaneEffort, } from "./effort.js";
@@ -23,7 +23,7 @@ import { installDrainHook } from "./hooks-install.js";
23
23
  import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
24
24
  import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
25
25
  import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
26
- import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
26
+ import { claimableStatesFor, isLoopIdleResumeLane, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
27
27
  import { AccountResolutionError, LanesError } from "./errors.js";
28
28
  import { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
29
29
  import { registryWorkerRecord } from "./registry-persona.js";
@@ -2143,7 +2143,17 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2143
2143
  ? { provider_home_at_spawn: previousSpawn.provider_home_at_spawn }
2144
2144
  : {}),
2145
2145
  };
2146
- if (existing.state === "running") {
2146
+ const loopIdleResume = launch.trigger?.kind === "resume" &&
2147
+ isLoopIdleResumeLane(existing, launch.trigger.loopIdle);
2148
+ if (loopIdleResume) {
2149
+ // An idle loop lane has no worker between phases. A live worker pid means
2150
+ // a phase is still running, so this resume would start a second worker.
2151
+ const priorWorker = readPidRecord(path.join(runDir, "worker.pid"));
2152
+ if (priorWorker !== null && isPidRecordAlive(priorWorker)) {
2153
+ throw new LanesError("lane_not_resumable", `Lane ${lane.lane_id} is already running`, { lane_id: lane.lane_id, state: existing.state, worker_pid: priorWorker.pid });
2154
+ }
2155
+ }
2156
+ else if (existing.state === "running") {
2147
2157
  if (launch.trigger?.kind === "resume") {
2148
2158
  throw new LanesError("lane_not_resumable", `Lane ${lane.lane_id} is already running`, { lane_id: lane.lane_id, state: existing.state });
2149
2159
  }
@@ -2643,7 +2653,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2643
2653
  let priorStartRecorded = false;
2644
2654
  const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
2645
2655
  claimed = false;
2646
- if (current.state === "running" && current.account === resolved.id) {
2656
+ if (current.state === "running" && current.account === resolved.id && !loopIdleResume) {
2647
2657
  if (trigger.kind === "resume") {
2648
2658
  throw new LanesError("lane_not_resumable", `Lane ${current.lane_id} is already running`, { lane_id: current.lane_id, state: current.state });
2649
2659
  }
@@ -9,5 +9,6 @@ export type JqInvoke = (args: string[], input: Buffer | string | undefined, cwd:
9
9
  stdout?: "capture" | "ignore";
10
10
  stderr?: "pipe" | "ignore";
11
11
  }) => JqResult;
12
+ export declare function isMsys2Environment(msystem: string | undefined): boolean;
12
13
  export declare function createShellCompatibleJqInvoker(invoke: JqInvoke, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, resolveShellType: (cwd: string) => string | undefined): JqInvoke;
13
14
  //# sourceMappingURL=jq-windows-compat.d.ts.map
@@ -21,6 +21,10 @@ function normalizeResult(result) {
21
21
  stderr: stderrBytes.toString("utf8"),
22
22
  };
23
23
  }
24
+ // MSYS2 environment names. Git for Windows 2.56+ reports UCRT64 instead of MINGW64.
25
+ export function isMsys2Environment(msystem) {
26
+ return /^(?:MINGW|UCRT|CLANG|MSYS)/i.test(msystem ?? "");
27
+ }
24
28
  export function createShellCompatibleJqInvoker(invoke, platform, env, resolveShellType) {
25
29
  let binaryOptionSupported;
26
30
  let shellTypeResolved = false;
@@ -32,7 +36,7 @@ export function createShellCompatibleJqInvoker(invoke, platform, env, resolveShe
32
36
  shellType = resolveShellType(cwd);
33
37
  shellTypeResolved = true;
34
38
  }
35
- const msysMode = /^msys/i.test(shellType ?? "") || /^MINGW/i.test(env.MSYSTEM ?? "");
39
+ const msysMode = /^msys/i.test(shellType ?? "") || isMsys2Environment(env.MSYSTEM);
36
40
  if (!msysMode)
37
41
  return invoke(args, input, cwd, options);
38
42
  if (binaryOptionSupported === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.131",
3
+ "version": "5.345.132",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {