@indigoai-us/hq-cli 5.260.0 → 5.261.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,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.261.0] — 2026-09-28
6
+
7
+ ### Fixed
8
+
9
+ - Standing keep-alive leads now resume stories that remain open. Completed
10
+ stories resume only on standing leads with queued work, and reconcile reports
11
+ why other completed lanes stay idle.
12
+
5
13
  ## [5.260.0] — 2026-09-28
6
14
 
7
15
  ### Added
@@ -50,7 +50,7 @@ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
50
50
  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, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
51
51
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
52
52
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
53
- import { keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
53
+ import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
54
54
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
55
55
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
56
56
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -4524,6 +4524,9 @@ async function runShow(ctx, laneId) {
4524
4524
  : storedLane;
4525
4525
  const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
4526
4526
  const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
4527
+ const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
4528
+ ? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete }, storedLane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, storedLane))
4529
+ : null;
4527
4530
  const managerStatusGate = await resolveManagerStatusGateForLane(hqRoot, storedLane, { env: ctx.env });
4528
4531
  const managerStatusTarget = managerStatusGate.target;
4529
4532
  const blockerFreshness = await resolveBlockerFreshnessForLane(hqRoot, storedLane, {
@@ -4560,6 +4563,7 @@ async function runShow(ctx, laneId) {
4560
4563
  enabled: keepAliveState.enabled,
4561
4564
  source: keepAliveState.source,
4562
4565
  standing: storedLane.keep_alive_standing === true,
4566
+ done_state_verdict: doneStateVerdict,
4563
4567
  opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
4564
4568
  blocked_reason: storedLane.keep_alive_blocked_reason ??
4565
4569
  (storedLane.keep_alive === true ? null : "not_opted_in"),
@@ -27,11 +27,9 @@ export interface KeepAliveDoneEnvelope {
27
27
  decision?: string;
28
28
  story_complete?: boolean;
29
29
  }
30
- /**
31
- * Shared done-state policy: ordinary opted-in lanes resume open stories;
32
- * standing leads wait for queued work, even when each turn marks the story
33
- * complete. Watch and reconcile both use this decision.
34
- */
30
+ export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complete";
31
+ /** Shared done-state policy used by watch, reconcile, and the show command. */
32
+ export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): DoneKeepAliveVerdict | null;
35
33
  export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
36
34
  /** Work signals already persisted for a lane that ended its turn. */
37
35
  export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
@@ -89,17 +89,18 @@ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText)
89
89
  }
90
90
  export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
91
91
  export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
92
- /**
93
- * Shared done-state policy: ordinary opted-in lanes resume open stories;
94
- * standing leads wait for queued work, even when each turn marks the story
95
- * complete. Watch and reconcile both use this decision.
96
- */
97
- export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
92
+ /** Shared done-state policy used by watch, reconcile, and the show command. */
93
+ export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork) {
98
94
  if (envelope.decision !== "done")
99
- return false;
100
- return lane.keep_alive_standing === true
101
- ? hasQueuedWork
102
- : envelope.story_complete !== true;
95
+ return null;
96
+ if (envelope.story_complete !== true)
97
+ return "eligible";
98
+ if (lane.keep_alive_standing !== true)
99
+ return "story_complete";
100
+ return hasQueuedWork ? "eligible" : "standing_idle";
101
+ }
102
+ export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
103
+ return doneKeepAliveVerdict(lane, envelope, hasQueuedWork) === "eligible";
103
104
  }
104
105
  /** Work signals already persisted for a lane that ended its turn. */
105
106
  export function hasPendingKeepAliveWork(hqRoot, lane, questionRecords) {
@@ -22,7 +22,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
22
22
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
23
23
  import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
24
24
  import { readLanesRegistryFlag } from "./registry-flag.js";
25
- import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
25
+ import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
26
26
  import { providerSupportsSessionResume } from "./delivery.js";
27
27
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
28
28
  import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
@@ -848,7 +848,7 @@ export async function runPassBody(hqRoot, deps) {
848
848
  if (!envelope || envelope.decision !== "done")
849
849
  continue;
850
850
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
851
- if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork))
851
+ if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
852
852
  continue;
853
853
  }
854
854
  const key = keepAliveCompanyKey(lane.company);
@@ -1137,10 +1137,8 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1137
1137
  if (terminalEnvelope && latest) {
1138
1138
  const envelope = terminalEnvelope;
1139
1139
  next.story_status = envelope.story_status;
1140
- if (envelope.story_complete !== undefined) {
1141
- next.story_complete = envelope.story_complete;
1142
- next.story_complete_reason = envelope.story_complete_reason;
1143
- }
1140
+ next.story_complete = envelope.story_complete;
1141
+ next.story_complete_reason = envelope.story_complete_reason;
1144
1142
  const storyStatus = envelope.decision === "blocked"
1145
1143
  ? "blocked"
1146
1144
  : envelope.decision === "done"
@@ -1550,6 +1548,13 @@ function recordKeepAliveSkip(hqRoot, lane, reason, summary, deps, detail) {
1550
1548
  });
1551
1549
  }
1552
1550
  }
1551
+ function recordDoneKeepAlivePolicySkip(summary, lane, verdict) {
1552
+ summary.skipped.push({
1553
+ kind: "lane",
1554
+ id: lane.lane_id,
1555
+ why: `keep_alive_${verdict}`,
1556
+ });
1557
+ }
1553
1558
  function readLatestInterruptedClassificationAt(hqRoot, lane) {
1554
1559
  const logPath = watcherOutLogPath(hqRoot, lane.lane_id);
1555
1560
  let body;
@@ -1624,7 +1629,10 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1624
1629
  if (!envelope || envelope.decision !== "done")
1625
1630
  continue;
1626
1631
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
1627
- if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork)) {
1632
+ const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork);
1633
+ if (verdict !== "eligible") {
1634
+ if (verdict !== null)
1635
+ recordDoneKeepAlivePolicySkip(summary, lane, verdict);
1628
1636
  if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
1629
1637
  casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
1630
1638
  current.state === "done" &&
@@ -1713,8 +1721,15 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1713
1721
  if (!latest || latest.envelope.decision !== "done")
1714
1722
  continue;
1715
1723
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
1716
- if (!canResumeDoneKeepAliveLane(lane, latest.envelope, hasQueuedWork))
1724
+ const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork);
1725
+ if (verdict !== "eligible") {
1726
+ if (verdict !== null)
1727
+ recordDoneKeepAlivePolicySkip(summary, lane, verdict);
1728
+ if (latest.envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
1729
+ casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === "done", (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
1730
+ }
1717
1731
  continue;
1732
+ }
1718
1733
  try {
1719
1734
  eligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, lane, latest.ref);
1720
1735
  }
@@ -1788,7 +1803,10 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
1788
1803
  if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
1789
1804
  continue;
1790
1805
  const hasQueuedWork = current.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, current, questions);
1791
- if (!canResumeDoneKeepAliveLane(current, currentEnvelope.envelope, hasQueuedWork)) {
1806
+ const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork);
1807
+ if (verdict !== "eligible") {
1808
+ if (verdict !== null)
1809
+ recordDoneKeepAlivePolicySkip(summary, current, verdict);
1792
1810
  if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
1793
1811
  casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1794
1812
  latestLane.state === "done" &&
@@ -434,7 +434,7 @@ export interface LaneRecord {
434
434
  worker_profile?: string;
435
435
  /** Whether this lead lane should resume completed turns while its story is open. */
436
436
  keep_alive?: boolean;
437
- /** Whether a keep-alive lead waits for queued work after every done turn. */
437
+ /** Whether completed keep-alive stories wait for queued work after a done turn. */
438
438
  keep_alive_standing?: boolean;
439
439
  /** Time this lane most recently explicitly opted into keep-alive. */
440
440
  keep_alive_opted_in_at?: string;
@@ -405,10 +405,8 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
405
405
  ...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
406
406
  };
407
407
  next.story_status = envelope.story_status;
408
- if (envelope.story_complete !== undefined) {
409
- next.story_complete = envelope.story_complete;
410
- next.story_complete_reason = envelope.story_complete_reason;
411
- }
408
+ next.story_complete = envelope.story_complete;
409
+ next.story_complete_reason = envelope.story_complete_reason;
412
410
  const currentProject = current.project_id;
413
411
  const currentStory = current.story_id;
414
412
  const storyStatus = envelope.decision === "blocked"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.260.0",
3
+ "version": "5.261.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {