@indigoai-us/hq-cli 5.215.0 → 5.217.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,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.217.0] — 2026-09-26
6
+
7
+ ### Fixed
8
+
9
+ - Keep-alive resumes a lane interrupted by `exited_with_partial_output` when
10
+ the interruption followed its explicit opt-in. Quota and host-loss
11
+ interruptions, login failures, and stopped lanes remain ineligible.
12
+
13
+ ## [5.216.0] — 2026-09-26
14
+
15
+ ### Fixed
16
+
17
+ - Lane watchers keep valid questions from a malformed `ask` envelope when
18
+ repair is skipped, cannot start, or still produces invalid output. The lane
19
+ stores the validation errors, and recovered question context includes them.
20
+ Repair spawns ignore unknown profile metadata and log the field names.
21
+ `hq lanes create` and `hq lanes resume --worker` keep strict validation.
22
+
23
+ - `hq lanes` stops sending status updates for stories missing from the project
24
+ board. The watcher reports the refusal once per lane run and keeps appending
25
+ lane events.
26
+
5
27
  ## [5.215.0] — 2026-09-26
6
28
 
7
29
  ### Fixed
@@ -9,6 +31,7 @@
9
31
  - hq-cli 5.211.0 could resume a retired lead from its worker profile's
10
32
  `keep_alive` value. Reconcile now requires an explicit lane opt-in written
11
33
  after the latest envelope, and skips retired or superseded leads.
34
+
12
35
  ## [5.214.0] — 2026-09-26
13
36
 
14
37
  ### Added
@@ -36,7 +36,7 @@ import { CompanySelectionError } from "../utils/company-selection-error.js";
36
36
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
37
37
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
38
38
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
39
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
39
+ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
40
40
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
41
41
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
42
42
  import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
@@ -897,41 +897,6 @@ async function withResumeHireAdmission(ctx, lane, fn) {
897
897
  return fn();
898
898
  });
899
899
  }
900
- function hireGrantCapacityError(parent, children) {
901
- const grant = parent.hire_grant ?? 0;
902
- const grantor = (parent.senior.kind === "lane" || parent.senior.kind === "session") &&
903
- parent.senior.id
904
- ? { kind: parent.senior.kind, id: parent.senior.id }
905
- : null;
906
- const grantorReason = grantor
907
- ? undefined
908
- : parent.senior.kind === "user"
909
- ? "senior_is_user"
910
- : "parent_senior_missing_id";
911
- const capacity = children.length === grant
912
- ? `is at its hire grant (${children.length} of ${grant} alive children)`
913
- : `has exhausted its hire grant (${children.length} alive children; grant ${grant})`;
914
- const grantCommand = `hq lanes grant ${parent.lane_id} --hire <n>`;
915
- const authorityGuidance = grantor
916
- ? `Its senior ${grantor.kind} ${grantor.id} can raise it with: ${grantCommand}`
917
- : parent.senior.kind === "user"
918
- ? `Its senior is the user and can raise it with: ${grantCommand}`
919
- : `No senior is recorded, so an operator must resolve ownership before running: ${grantCommand}`;
920
- const message = `Lane ${parent.lane_id} ${capacity}. ${authorityGuidance}`;
921
- return new LanesError("hire_grant_exhausted", message, {
922
- senior_lane_id: parent.lane_id,
923
- parent_grant: grant,
924
- alive_children: children.map((child) => child.lane_id),
925
- alive_child_count: children.length,
926
- grantor,
927
- ...(grantorReason ? { grantor_reason: grantorReason } : {}),
928
- message,
929
- ask_guidance: {
930
- envelope: "decision=ask; include the desired total grant and the reason",
931
- senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
932
- },
933
- });
934
- }
935
900
  const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
936
901
  const MAX_PLACEMENT_OUTPOSTS = 16;
937
902
  function outpostIdentityUnknownError(message, expectedId, details = {}) {
@@ -2072,7 +2037,7 @@ async function emitLaneTerminalStateSoft(ctx, lane) {
2072
2037
  process.stderr.write(`hq lanes: terminal mesh update failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
2073
2038
  }
2074
2039
  }
2075
- async function emitLaneStartedForStories(ctx, company, lane, threadId) {
2040
+ async function emitLaneStartedForStories(ctx, company, lane, threadId, knownAbsentStories = new Set()) {
2076
2041
  let ready = lane;
2077
2042
  if (ctx.mesh.ensureLaneThread) {
2078
2043
  try {
@@ -2108,6 +2073,8 @@ async function emitLaneStartedForStories(ctx, company, lane, threadId) {
2108
2073
  failures.push(err);
2109
2074
  continue;
2110
2075
  }
2076
+ if (knownAbsentStories.has(storyBoardStoryKey(story.project_id, story.story_id)))
2077
+ continue;
2111
2078
  try {
2112
2079
  await ctx.mesh.patchStoryStatus(company, story.project_id, story.story_id, "in_progress");
2113
2080
  }
@@ -2135,20 +2102,30 @@ function storyNotOnBoardWarning(err) {
2135
2102
  ? err.message
2136
2103
  : undefined;
2137
2104
  }
2105
+ function storyBoardStoryKey(projectId, storyId) {
2106
+ return JSON.stringify([projectId, storyId]);
2107
+ }
2138
2108
  function combinedStoryWarning(lane, additional = []) {
2139
2109
  const warnings = [storyWarning(lane), ...additional]
2140
2110
  .filter((warning) => warning !== undefined && warning.trim().length > 0);
2141
2111
  return [...new Set(warnings)].join(" ") || null;
2142
2112
  }
2143
- async function storyBoardWarning(ctx, company, projectId, storyIds) {
2113
+ async function storyBoardCheck(ctx, company, projectId, storyIds) {
2144
2114
  try {
2145
2115
  await assertStoriesOnBoard(ctx, company, projectId, storyIds);
2146
- return undefined;
2116
+ return { absentStoryIds: new Set() };
2147
2117
  }
2148
2118
  catch (err) {
2149
2119
  const warning = storyNotOnBoardWarning(err);
2150
- if (warning !== undefined)
2151
- return warning;
2120
+ if (warning !== undefined) {
2121
+ const storyId = isLanesError(err) && typeof err.details.story_id === "string"
2122
+ ? err.details.story_id
2123
+ : undefined;
2124
+ return {
2125
+ warning,
2126
+ absentStoryIds: new Set(storyId ? [storyId] : []),
2127
+ };
2128
+ }
2152
2129
  throw err;
2153
2130
  }
2154
2131
  }
@@ -2451,10 +2428,14 @@ async function runCreate(ctx, opts) {
2451
2428
  const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
2452
2429
  assertSameTenant(company, senior, seniorCompany);
2453
2430
  const createStoryWarnings = [];
2431
+ const knownAbsentStories = new Set();
2454
2432
  if (meshCompany && storyIds.length > 0) {
2455
- const warning = await storyBoardWarning(ctx, meshCompany, project, storyIds);
2456
- if (warning !== undefined)
2457
- createStoryWarnings.push(warning);
2433
+ const boardCheck = await storyBoardCheck(ctx, meshCompany, project, storyIds);
2434
+ if (boardCheck.warning !== undefined)
2435
+ createStoryWarnings.push(boardCheck.warning);
2436
+ for (const storyId of boardCheck.absentStoryIds) {
2437
+ knownAbsentStories.add(storyBoardStoryKey(project, storyId));
2438
+ }
2458
2439
  }
2459
2440
  let resolvedWorker;
2460
2441
  if (opts.worker) {
@@ -3067,7 +3048,7 @@ async function runCreate(ctx, opts) {
3067
3048
  locateTranscript: ctx.locateTranscript,
3068
3049
  });
3069
3050
  if (meshCompany) {
3070
- createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id));
3051
+ createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id, knownAbsentStories));
3071
3052
  }
3072
3053
  }
3073
3054
  else if (outpostId) {
@@ -4510,6 +4491,7 @@ async function runShow(ctx, laneId) {
4510
4491
  provider_session_id: lane.provider_session_id ?? null,
4511
4492
  provider_session_id_absent: lane.provider_session_id_absent ?? null,
4512
4493
  envelope_repair_mode: lane.envelope_repair?.mode ?? null,
4494
+ envelope_validation_errors: lane.envelope_validation_errors ?? [],
4513
4495
  last_resume: lane.last_resume ?? null,
4514
4496
  account: lane.account ?? null,
4515
4497
  account_history: lane.account_history ?? [],
@@ -6327,7 +6309,7 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
6327
6309
  }
6328
6310
  return updated;
6329
6311
  }
6330
- async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}) {
6312
+ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
6331
6313
  const company = lane.mesh_company ?? lane.company;
6332
6314
  if (!company)
6333
6315
  return;
@@ -6341,7 +6323,9 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
6341
6323
  ...payload,
6342
6324
  lane_status: lane.state,
6343
6325
  });
6344
- await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
6326
+ if (!skipStoryStatusPatch) {
6327
+ await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
6328
+ }
6345
6329
  }
6346
6330
  function sameCommittedStory(current, expected, projectId, storyId) {
6347
6331
  const currentStory = laneStory(current, projectId, storyId);
@@ -6350,9 +6334,9 @@ function sameCommittedStory(current, expected, projectId, storyId) {
6350
6334
  current.story_id === expected.story_id &&
6351
6335
  JSON.stringify(currentStory) === JSON.stringify(expectedStory));
6352
6336
  }
6353
- async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}) {
6337
+ async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
6354
6338
  try {
6355
- await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload);
6339
+ await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
6356
6340
  return undefined;
6357
6341
  }
6358
6342
  catch (err) {
@@ -6408,10 +6392,12 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
6408
6392
  }
6409
6393
  const company = lane.mesh_company ?? lane.company;
6410
6394
  const warnings = [];
6395
+ let storyKnownAbsent = false;
6411
6396
  if (company) {
6412
- const warning = await storyBoardWarning(ctx, company, projectId, [normalizedStoryId]);
6413
- if (warning !== undefined)
6414
- warnings.push(warning);
6397
+ const boardCheck = await storyBoardCheck(ctx, company, projectId, [normalizedStoryId]);
6398
+ if (boardCheck.warning !== undefined)
6399
+ warnings.push(boardCheck.warning);
6400
+ storyKnownAbsent = boardCheck.absentStoryIds.has(normalizedStoryId);
6415
6401
  }
6416
6402
  const at = ctx.now().toISOString();
6417
6403
  const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
@@ -6427,7 +6413,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
6427
6413
  no_story_reason: undefined,
6428
6414
  };
6429
6415
  });
6430
- const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
6416
+ const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress", {}, storyKnownAbsent);
6431
6417
  if (transitionWarning !== undefined)
6432
6418
  warnings.push(transitionWarning);
6433
6419
  const finalLane = readLane(hqRoot, laneId);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
5
5
  export { MAX_LANE_DEPTH } from "./constants.js";
6
- export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
6
+ export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
7
7
  export type { HireGrantBackfillRow } from "./hire-grants.js";
8
8
  export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
9
9
  export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
5
5
  export { MAX_LANE_DEPTH } from "./constants.js";
6
- export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
6
+ export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
7
7
  export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
8
8
  export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
9
9
  export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
@@ -5,6 +5,7 @@ export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
5
5
  export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
6
6
  export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
7
7
  export declare 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.";
8
+ export declare 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.";
8
9
  export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
9
10
  export declare function retiredLaneRegistryPath(hqRoot: string): string;
10
11
  /** Missing registry means no retired lanes; every present invalid file fails closed. */
@@ -7,6 +7,7 @@ export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
7
7
  export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
8
8
  export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
9
9
  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.";
10
+ 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.";
10
11
  export function retiredLaneRegistryPath(hqRoot) {
11
12
  return path.join(hqRoot, "workspace", "lane-briefs", "juniors.json");
12
13
  }
@@ -92,6 +92,7 @@ export interface CreateLaneMeshOptions {
92
92
  }) => Promise<void>;
93
93
  }
94
94
  export declare function storyNotOnBoardError(company: LaneCompanyRef, projectId: string, storyId: string): LanesError;
95
+ export declare function laneStoryStatusSuppressionKey(company: LaneCompanyRef, projectId: string, storyId: string): string;
95
96
  export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
96
97
  /** Resolve the explicit mesh attribution before falling back to the lane tenant. */
97
98
  export declare function laneMeshCompany(lane: Pick<LaneRecord, "company" | "mesh_company">): LaneCompanyRef | undefined;
@@ -106,6 +107,8 @@ export declare function laneMeshEventIdentity(lane: LaneRecord): Record<string,
106
107
  export declare function emitLaneDecisionEvents(mesh: LaneMesh, lane: LaneRecord, envelope: Pick<LaneEnvelope, "decision" | "summary" | "blocked_reason">, opts?: {
107
108
  questionIds?: string[];
108
109
  signal?: AbortSignal;
110
+ /** Per-watcher-run cache for story_not_on_board refusals. */
111
+ storyStatusSuppressions?: Set<string>;
109
112
  }): Promise<void>;
110
113
  /** Emit an abnormal terminal state even when the worker produced no envelope. */
111
114
  export declare function emitLaneTerminalStateEvent(mesh: LaneMesh, lane: LaneRecord, state: Exclude<TerminalLaneState, "done">, summary?: string, signal?: AbortSignal): Promise<void>;
@@ -229,6 +229,17 @@ function isUnknownProjectStory(error) {
229
229
  typeof candidate.message === "string" &&
230
230
  candidate.message.includes("Unknown story"));
231
231
  }
232
+ function isUncodedStoryPatchNotFound(error) {
233
+ if (!error || typeof error !== "object")
234
+ return false;
235
+ const candidate = error;
236
+ // The story-absent route response is exactly `404 Not found` without a code.
237
+ // Keep coded 404s, especially PROJECT_VIEW_NOT_FOUND, on their own paths.
238
+ return (candidate.status === 404 &&
239
+ candidate.code === undefined &&
240
+ typeof candidate.message === "string" &&
241
+ /^404 not found$/i.test(candidate.message));
242
+ }
232
243
  function shellQuote(value) {
233
244
  return "'" + value.replace(/'/g, "'\\''") + "'";
234
245
  }
@@ -245,6 +256,9 @@ export function storyNotOnBoardError(company, projectId, storyId) {
245
256
  command,
246
257
  });
247
258
  }
259
+ export function laneStoryStatusSuppressionKey(company, projectId, storyId) {
260
+ return JSON.stringify([company.uid, projectId, storyId]);
261
+ }
248
262
  function isAlreadyRegistered(error) {
249
263
  if (!error || typeof error !== "object")
250
264
  return false;
@@ -365,7 +379,7 @@ export function createLaneMesh(opts = {}) {
365
379
  await patchStoryStatus(tok, company.uid, projectId, storyId, status);
366
380
  }
367
381
  catch (error) {
368
- if (isUnknownProjectStory(error)) {
382
+ if (isUnknownProjectStory(error) || isUncodedStoryPatchNotFound(error)) {
369
383
  throw storyNotOnBoardError(company, projectId, storyId);
370
384
  }
371
385
  throw error;
@@ -437,6 +451,21 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
437
451
  const threadId = await ensureLaneEventThread(mesh, company, lane, lane.state, opts.signal);
438
452
  if (!company || !threadId)
439
453
  return;
454
+ const patchStoryStatus = async (status) => {
455
+ const suppressions = opts.storyStatusSuppressions;
456
+ const key = laneStoryStatusSuppressionKey(company, lane.project_id, lane.story_id);
457
+ if (suppressions?.has(key))
458
+ return;
459
+ try {
460
+ await mesh.patchStoryStatus(company, lane.project_id, lane.story_id, status);
461
+ }
462
+ catch (err) {
463
+ if (suppressions && err instanceof LanesError && err.error === "story_not_on_board") {
464
+ suppressions.add(key);
465
+ }
466
+ throw err;
467
+ }
468
+ };
440
469
  switch (envelope.decision) {
441
470
  case "done": {
442
471
  const outcomes = await Promise.allSettled([
@@ -444,7 +473,7 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
444
473
  ...laneMeshEventIdentity(lane),
445
474
  summary: envelope.summary,
446
475
  }, opts.signal),
447
- mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "review"),
476
+ patchStoryStatus("review"),
448
477
  ]);
449
478
  const failure = outcomes.find((outcome) => outcome.status === "rejected");
450
479
  if (failure?.status === "rejected")
@@ -458,7 +487,7 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
458
487
  ...laneMeshEventIdentity(lane),
459
488
  blocked_reason: envelope.blocked_reason ?? "",
460
489
  }),
461
- mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "in_progress"),
490
+ patchStoryStatus("in_progress"),
462
491
  ]);
463
492
  const failure = outcomes.find((outcome) => outcome.status === "rejected");
464
493
  if (failure?.status === "rejected")
@@ -33,10 +33,14 @@ export interface WorkerProfileRuntime {
33
33
  settingsPath?: string;
34
34
  hooksDir?: string;
35
35
  }
36
+ export interface LoadWorkerProfileOptions {
37
+ /** Unknown keys may be ignored only by callers with an explicit recovery path. */
38
+ onUnknownMetaFields?: (fields: readonly string[]) => void;
39
+ }
36
40
  export declare function personalWorkerProfileDir(hqRoot: string, id: string): string;
37
41
  /** A meta.yaml file is the explicit marker for a personal worker profile. */
38
42
  export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
39
- export declare function loadWorkerProfile(hqRoot: string, id: string): WorkerProfile;
43
+ export declare function loadWorkerProfile(hqRoot: string, id: string, options?: LoadWorkerProfileOptions): WorkerProfile;
40
44
  export interface EffectiveLaneKeepAlive {
41
45
  enabled: boolean;
42
46
  source: "lane" | "none";
@@ -117,15 +117,18 @@ function collectFiles(id, dir, relativeDir) {
117
117
  visit(absoluteDir, relativeDir);
118
118
  return files.sort((a, b) => a.localeCompare(b));
119
119
  }
120
- function parseMeta(id, dir, raw) {
120
+ function parseMeta(id, dir, raw, options = {}) {
121
121
  if (!isPlainObject(raw)) {
122
122
  throw profileError(id, dir, "meta.yaml must contain an object");
123
123
  }
124
124
  const unknown = Object.keys(raw).filter((key) => !PROFILE_META_KEYS.has(key));
125
125
  if (unknown.length > 0) {
126
- throw profileError(id, dir, `meta.yaml has unknown field(s): ${unknown.join(", ")}`, {
127
- fields: unknown,
128
- });
126
+ if (options.onUnknownMetaFields === undefined) {
127
+ throw profileError(id, dir, `meta.yaml has unknown field(s): ${unknown.join(", ")}`, {
128
+ fields: unknown,
129
+ });
130
+ }
131
+ options.onUnknownMetaFields(unknown);
129
132
  }
130
133
  const provider = raw.provider;
131
134
  if (typeof provider !== "string" || !isLaneProvider(provider.trim())) {
@@ -305,7 +308,7 @@ export function workerProfileMarker(hqRoot, id) {
305
308
  return "present";
306
309
  return "present";
307
310
  }
308
- export function loadWorkerProfile(hqRoot, id) {
311
+ export function loadWorkerProfile(hqRoot, id, options = {}) {
309
312
  const dir = personalWorkerProfileDir(hqRoot, id);
310
313
  if (!validProfileId(id)) {
311
314
  throw profileError(id, dir, "profile id must be a simple directory name", {
@@ -338,7 +341,7 @@ export function loadWorkerProfile(hqRoot, id) {
338
341
  field: PROFILE_META,
339
342
  });
340
343
  }
341
- const meta = parseMeta(id, dir, metaRaw);
344
+ const meta = parseMeta(id, dir, metaRaw, options);
342
345
  const systemPromptPath = path.join(dir, PROFILE_SYSTEM_PROMPT);
343
346
  const systemPrompt = readRequiredText(id, dir, systemPromptPath, PROFILE_SYSTEM_PROMPT);
344
347
  if (systemPrompt.trim().length === 0) {
@@ -71,5 +71,5 @@ export declare function envelopeQuestionId(laneId: string, envelopeRef: string,
71
71
  * Spec §6.1: each envelope questions[] entry becomes one queue record, via
72
72
  * raiseLaneQuestion (the same function the CLI ask path uses).
73
73
  */
74
- export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string): QuestionRecord[];
74
+ export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string, questionIndexes?: readonly number[]): QuestionRecord[];
75
75
  //# sourceMappingURL=questions.d.ts.map
@@ -222,7 +222,7 @@ function envelopeQuestionBody(item) {
222
222
  * Spec §6.1: each envelope questions[] entry becomes one queue record, via
223
223
  * raiseLaneQuestion (the same function the CLI ask path uses).
224
224
  */
225
- export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at) {
225
+ export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at, questionIndexes) {
226
226
  const items = envelope.questions ?? [];
227
227
  const raised = [];
228
228
  for (let i = 0; i < items.length; i += 1) {
@@ -235,7 +235,7 @@ export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at)
235
235
  body: envelopeQuestionBody(item),
236
236
  raisedVia: "envelope",
237
237
  at,
238
- questionId: envelopeQuestionId(lane.lane_id, envelopeRef, i),
238
+ questionId: envelopeQuestionId(lane.lane_id, envelopeRef, questionIndexes?.[i] ?? i),
239
239
  }));
240
240
  }
241
241
  return raised;
@@ -78,6 +78,8 @@ export interface ReconcileDeps {
78
78
  mesh?: LaneMesh | null;
79
79
  /** Bound on one terminal mesh emit during dead-host recovery. Default 10 s. */
80
80
  meshEmitTimeoutMs?: number;
81
+ /** Fresh keep-alive eligibility read; injected to exercise opt-in changes before queueing. */
82
+ readKeepAliveRecheckLane?: (hqRoot: string, laneId: string) => LaneRecord;
81
83
  /**
82
84
  * Awaited after lane/question records and liveness are loaded, before any
83
85
  * CAS write. Test seam so two in-process passes can share a snapshot.