@indigoai-us/hq-cli 5.260.0 → 5.262.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,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.262.0] — 2026-09-28
6
+
7
+ ### Added
8
+
9
+ - The `checkpoint.ontology.concept-project-link` flag (off by default) lets
10
+ checkpoint ontology link a knowledge concept whose alias is ambiguous to the
11
+ one project with the same canonical name, such as HQ. It writes nothing.
12
+
13
+ ### Fixed
14
+
15
+ - `hq doctor --live-runtimes` uses a Windows `.cmd` shim only when the sibling file exists.
16
+
17
+ ## [5.261.0] — 2026-09-28
18
+
19
+ ### Fixed
20
+
21
+ - Standing keep-alive leads now resume stories that remain open. Completed
22
+ stories resume only on standing leads with queued work, and reconcile reports
23
+ why other completed lanes stay idle.
24
+
5
25
  ## [5.260.0] — 2026-09-28
6
26
 
7
27
  ### 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"),
@@ -370,6 +370,21 @@ export function applyOntologyRun(options) {
370
370
  }
371
371
  continue;
372
372
  }
373
+ if (resolved.tier === "ambiguous_alias") {
374
+ // checkpoint.ontology.concept-project-link: a knowledge concept whose name is the
375
+ // canonical name of exactly one project links to that project. Nothing is written.
376
+ // Like the tier 1 and 2 links above, it runs before the stopword guard,
377
+ // which only stops new entities.
378
+ const projects = plan.concept_project_link === true && input.kind === "knowledge" && candidate.type === "concept"
379
+ ? [...(index.canonical.get(entityKey("project", normalizeEntityName(canonical, "project"))) ?? [])]
380
+ : [];
381
+ if (projects.length === 1) {
382
+ const linked = index.byRel.get(projects[0]);
383
+ report.matched += 1;
384
+ report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: projects[0] });
385
+ continue;
386
+ }
387
+ }
373
388
  // A generic word never becomes a new entity, whatever else it collides with.
374
389
  if (isRejectedName(normalizeEntityName(canonical, candidate.type))) {
375
390
  note(report.rejected, "stopword");
@@ -78,6 +78,17 @@ export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-pe
78
78
  * to 0.803 and cost recall 0.023 on a replay of six runs.
79
79
  */
80
80
  export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
81
+ /**
82
+ * A knowledge concept candidate whose name is an alias of more than one
83
+ * entity, and which apply would defer as `ambiguous_alias`, links instead to
84
+ * the one project whose canonical name it is (`HQ` typed as a concept beside
85
+ * `project/HQ` and several concepts aliased `HQ`). The plan carries
86
+ * `concept_project_link: true`. CO-51: over six runs of the shipping config
87
+ * the model typed HQ as a concept in some runs and not others, and the
88
+ * deferral lost 61 gold links; a replay of this rule raised recall from 0.639
89
+ * to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
90
+ */
91
+ export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
81
92
  export declare const PLAN_FILE = "ontology-plan.json";
82
93
  export declare const CANDIDATES_FILE = "ontology-candidates.json";
83
94
  export declare const REPORT_FILE = "ontology-report.json";
@@ -150,6 +161,8 @@ export interface OntologyPlan {
150
161
  first_name_people?: true;
151
162
  /** Set by checkpoint.ontology.skip-ambiguous-concept-links: apply rejects a knowledge link to a concept whose name another type also holds. */
152
163
  skip_ambiguous_concept_links?: true;
164
+ /** Set by checkpoint.ontology.concept-project-link: apply links a knowledge concept with an ambiguous alias to the one project of that canonical name. */
165
+ concept_project_link?: true;
153
166
  inputs: PlanInput[];
154
167
  /** The session's summary, decisions and learnings, for the `session` input. */
155
168
  session_text: string | null;
@@ -82,6 +82,17 @@ export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
82
82
  * to 0.803 and cost recall 0.023 on a replay of six runs.
83
83
  */
84
84
  export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
85
+ /**
86
+ * A knowledge concept candidate whose name is an alias of more than one
87
+ * entity, and which apply would defer as `ambiguous_alias`, links instead to
88
+ * the one project whose canonical name it is (`HQ` typed as a concept beside
89
+ * `project/HQ` and several concepts aliased `HQ`). The plan carries
90
+ * `concept_project_link: true`. CO-51: over six runs of the shipping config
91
+ * the model typed HQ as a concept in some runs and not others, and the
92
+ * deferral lost 61 gold links; a replay of this rule raised recall from 0.639
93
+ * to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
94
+ */
95
+ export const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
85
96
  export const PLAN_FILE = "ontology-plan.json";
86
97
  export const CANDIDATES_FILE = "ontology-candidates.json";
87
98
  export const REPORT_FILE = "ontology-report.json";
@@ -265,6 +276,7 @@ export async function planOntologyRun(options) {
265
276
  const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
266
277
  const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
267
278
  const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
279
+ const conceptProjectLink = await options.readFlag(CONCEPT_PROJECT_LINK_FLAG, company);
268
280
  const touched = sessionTouched(hqRoot, company, payload.files);
269
281
  // A first run takes its boundary from the entity tree, so the ledger is
270
282
  // refreshed before merge-back moves any `last_updated` forward.
@@ -380,6 +392,7 @@ export async function planOntologyRun(options) {
380
392
  ...(noLinkAliases ? { link_aliases: false } : {}),
381
393
  ...(firstNamePeople ? { first_name_people: true } : {}),
382
394
  ...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
395
+ ...(conceptProjectLink ? { concept_project_link: true } : {}),
383
396
  inputs,
384
397
  session_text: sessionText,
385
398
  pending_after_run: remaining,
@@ -76,6 +76,8 @@ export interface ProbeOutcome {
76
76
  export interface RuntimeHealthDeps {
77
77
  /** Resolve an executable against PATH; null when not found. */
78
78
  resolveBinary?: (binary: string) => string | null;
79
+ /** Check whether a Windows command shim exists next to its resolved binary. */
80
+ fileExists?: (file: string) => boolean;
79
81
  /** Execute one probe. The default spawns from the OS temp dir. */
80
82
  execProbe?: (file: string, args: string[], timeoutMs: number) => Promise<ProbeOutcome>;
81
83
  /** Per-probe timeout. Default {@link DEFAULT_PROBE_TIMEOUT_MS}. */
@@ -103,7 +103,7 @@ async function runRuntimeChecks(context, deps) {
103
103
  const resolved = {
104
104
  resolveBinary: deps.resolveBinary ?? resolveOnPath,
105
105
  execProbe: deps.execProbe ??
106
- ((file, args, timeoutMs) => defaultExecProbe(file, args, timeoutMs, deps.platform ?? process.platform)),
106
+ ((file, args, timeoutMs) => defaultExecProbe(file, args, timeoutMs, deps.platform ?? process.platform, deps.fileExists ?? fs.existsSync)),
107
107
  timeoutMs: deps.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
108
108
  live: context.liveRuntimes === true,
109
109
  };
@@ -270,10 +270,13 @@ export function resolveOnPath(binary) {
270
270
  * CLI that reads piped stdin in non-TTY mode — blocks forever on a pipe that
271
271
  * never reaches EOF, which turns every probe into a timeout.
272
272
  */
273
- function defaultExecProbe(file, args, timeoutMs, platform) {
273
+ function defaultExecProbe(file, args, timeoutMs, platform, fileExists) {
274
274
  return new Promise((resolve) => {
275
- const command = platform === "win32" && !/\.(?:cmd|bat|exe|com)$/i.test(file)
276
- ? `${file}.cmd`
275
+ const cmdSibling = `${file}.cmd`;
276
+ const command = platform === "win32" &&
277
+ !/\.(?:cmd|bat|exe|com)$/i.test(file) &&
278
+ fileExists(cmdSibling)
279
+ ? cmdSibling
277
280
  : file;
278
281
  const plan = planCommandSpawn(command, args, platform);
279
282
  const child = spawn(plan.cmd, plan.args, {
@@ -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.262.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {