@indigoai-us/hq-cli 5.295.0 → 5.296.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.
@@ -36,7 +36,7 @@ import { registerCoreHandoffCommand } from "./core-handoff.js";
36
36
  import { registerCoreOntologyCommand } from "./core-ontology.js";
37
37
  import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
38
38
  import { registerCorePolicyCommand } from "./core-policy.js";
39
- import { registerCoreJobsNativeCommand } from "./core-jobs-native.js";
39
+ import { registerCoreJobsNativeCommand } from "./core-job-native.js";
40
40
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
41
41
  import { resolveLiveRoot } from "../utils/hq-roots.js";
42
42
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
@@ -530,6 +530,7 @@ export function createLaneMessageHandlers(deps) {
530
530
  pending_resume: {
531
531
  session_id: sessionId,
532
532
  trigger: "message",
533
+ caller: ctx.env.HQ_LANE_ID?.trim() || "user",
533
534
  text: deliveryText,
534
535
  ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
535
536
  message_id: messageId,
@@ -359,6 +359,7 @@ export function createLaneResumeHandlers(deps) {
359
359
  pending_resume: {
360
360
  session_id: sessionId,
361
361
  trigger: "resume",
362
+ caller: ctx.env.HQ_LANE_ID?.trim() || "user",
362
363
  resumed_from: resumedFrom,
363
364
  ...(opts.fromFailed === true ? { from_failed: true } : {}),
364
365
  ...(failedResumeExpectedSenior !== undefined
@@ -8,6 +8,7 @@ export declare function createLaneStoryHandlers(deps: LaneStoryHandlerDeps): {
8
8
  runStoryList: (ctx: Ctx, laneId: string) => Promise<void>;
9
9
  runStoryAdd: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
10
10
  runStoryStart: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
11
+ runStoryReview: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
11
12
  runStoryBlock: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
12
13
  runStoryDone: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
13
14
  };
@@ -18,6 +18,13 @@ export function createLaneStoryHandlers(deps) {
18
18
  }
19
19
  return { projectId, storyId: normalizedStoryId, story };
20
20
  }
21
+ function reviewableStoryTarget(lane, storyId, project) {
22
+ const target = storyCommandTarget(lane, storyId, project);
23
+ if (target.story.status === "done") {
24
+ throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be moved to review`, { lane_id: lane.lane_id, project_id: target.projectId, story_id: target.storyId });
25
+ }
26
+ return target;
27
+ }
21
28
  function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
22
29
  const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
23
30
  ? {
@@ -229,6 +236,38 @@ export function createLaneStoryHandlers(deps) {
229
236
  const finalLane = readLane(hqRoot, laneId);
230
237
  printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
231
238
  }
239
+ async function runStoryReview(ctx, laneId, storyId, opts) {
240
+ const hqRoot = ctx.hqRoot();
241
+ let lane = readLane(hqRoot, laneId);
242
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
243
+ const target = reviewableStoryTarget(lane, storyId, opts.project);
244
+ const at = ctx.now().toISOString();
245
+ const warnings = [];
246
+ const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
247
+ lane = thread.lane;
248
+ if (thread.warning !== undefined)
249
+ warnings.push(thread.warning);
250
+ const before = lane;
251
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
252
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
253
+ reviewableStoryTarget(current, target.storyId, target.projectId);
254
+ return updateCarriedStory(current, target.projectId, target.storyId, "review", at);
255
+ });
256
+ const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_progress", "review", { detail: "Story moved to review" });
257
+ if (transitionWarning !== undefined)
258
+ warnings.push(transitionWarning);
259
+ const finalLane = readLane(hqRoot, laneId);
260
+ printJson({
261
+ ok: true,
262
+ action: "story.review",
263
+ lane_id: laneId,
264
+ project_id: target.projectId,
265
+ story_id: target.storyId,
266
+ status: "review",
267
+ story_warning: combinedStoryWarning(finalLane, warnings),
268
+ lane: finalLane,
269
+ });
270
+ }
232
271
  async function runStoryBlock(ctx, laneId, storyId, opts) {
233
272
  const hqRoot = ctx.hqRoot();
234
273
  let lane = readLane(hqRoot, laneId);
@@ -303,6 +342,6 @@ export function createLaneStoryHandlers(deps) {
303
342
  const finalLane = readLane(hqRoot, laneId);
304
343
  printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
305
344
  }
306
- return { runStoryList, runStoryAdd, runStoryStart, runStoryBlock, runStoryDone };
345
+ return { runStoryList, runStoryAdd, runStoryStart, runStoryReview, runStoryBlock, runStoryDone };
307
346
  }
308
347
  //# sourceMappingURL=lanes-stories.js.map
@@ -5062,10 +5062,12 @@ async function runShow(ctx, laneId) {
5062
5062
  })
5063
5063
  .slice(0, 20)
5064
5064
  .map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
5065
+ const laneForShow = { ...lane };
5066
+ delete laneForShow.resume_history;
5065
5067
  printJson({
5066
5068
  ok: true,
5067
5069
  action: "show",
5068
- lane,
5070
+ lane: laneForShow,
5069
5071
  ...(laneIsSenior ? { reviews: recentReviews, reviews_skipped: reviewListing.skipped } : {}),
5070
5072
  ...hire,
5071
5073
  overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, ctx.now()),
@@ -5079,6 +5081,8 @@ async function runShow(ctx, laneId) {
5079
5081
  envelope_repair_mode: lane.envelope_repair?.mode ?? null,
5080
5082
  envelope_validation_errors: lane.envelope_validation_errors ?? [],
5081
5083
  last_resume: lane.last_resume ?? null,
5084
+ // Public JSON key for the bounded start audit; `last_resume` remains unchanged.
5085
+ resume_history: lane.resume_history ?? [],
5082
5086
  keep_alive_history: lane.keep_alive_history ?? [],
5083
5087
  account: lane.account ?? null,
5084
5088
  account_history: lane.account_history ?? [],
@@ -6191,7 +6195,12 @@ async function prepareAccountMove(ctx, lane, input) {
6191
6195
  }),
6192
6196
  pending_resume: input.deferTurnStart
6193
6197
  ? undefined
6194
- : { session_id: sessionId, at, ...input.pending },
6198
+ : {
6199
+ session_id: sessionId,
6200
+ at,
6201
+ caller: ctx.env.HQ_LANE_ID?.trim() || "user",
6202
+ ...input.pending,
6203
+ },
6195
6204
  };
6196
6205
  applyResumeProfileAndEffort(current, next, {
6197
6206
  ...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
@@ -8171,6 +8180,15 @@ export function registerLanesCommand(parent, deps = {}) {
8171
8180
  .action(wrap(async (laneId, storyId, opts) => {
8172
8181
  await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
8173
8182
  }));
8183
+ stories
8184
+ .command("review")
8185
+ .description("Move a carried story to review")
8186
+ .argument("<lane-id>", "Lane id")
8187
+ .argument("<story-id>", "Story id")
8188
+ .option("--project <slug>", "Project id; defaults to the lane's current project")
8189
+ .action(wrap(async (laneId, storyId, opts) => {
8190
+ await storyHandlers.runStoryReview(ctx, laneId, storyId, opts);
8191
+ }));
8174
8192
  stories
8175
8193
  .command("block")
8176
8194
  .description("Mark one carried story blocked")
@@ -128,6 +128,11 @@ export function registerRunCommand(program) {
128
128
  let state;
129
129
  const graph = await internal.loadEnvGraph({
130
130
  entryFilePaths: paths,
131
+ // hq run owns its short-lived in-memory resolution. Varlock's default
132
+ // cache auto-detection can write ~/.config/varlock and initialize
133
+ // the native local-encryption helper, neither of which belongs on
134
+ // this command path.
135
+ skipCache: true,
131
136
  afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
132
137
  });
133
138
  // Refuse to resolve a graph varlock never finished loading (a malformed
@@ -44,6 +44,7 @@
44
44
  */
45
45
  import { appendThreadEvent, clamp, ensureLaneMeshThread, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
46
46
  import { LanesError } from "./errors.js";
47
+ import { terminalEnvelopeStoryStatus } from "./stories.js";
47
48
  export const LANE_MESH_EVENTS = [
48
49
  "lane_started",
49
50
  "lane_progress",
@@ -454,6 +455,8 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
454
455
  const patchStoryStatus = async (status) => {
455
456
  if (!lane.project_id?.trim() || !lane.story_id?.trim())
456
457
  return;
458
+ if (terminalEnvelopeStoryStatus(lane, envelope.decision) === undefined)
459
+ return;
457
460
  const suppressions = opts.storyStatusSuppressions;
458
461
  const key = laneStoryStatusSuppressionKey(company, lane.project_id, lane.story_id);
459
462
  if (suppressions?.has(key))
@@ -31,7 +31,7 @@ import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./qu
31
31
  import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
32
32
  import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
33
33
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
34
- import { activeLaneStories, laneStories, storyStatusForEnvelope } from "./stories.js";
34
+ import { activeLaneStories, laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
35
35
  import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
36
36
  import { PROVIDER_REJECTED_REASON, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
37
37
  /** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
@@ -50,6 +50,7 @@ export const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
50
50
  function spawnDepsForHost(deps) {
51
51
  return {
52
52
  ...(deps.spawnLaneDeps ?? {}),
53
+ env: deps.spawnLaneDeps?.env ?? deps.env ?? process.env,
53
54
  hostname: deps.host.hostname,
54
55
  admissionHost: deps.host,
55
56
  };
@@ -1270,24 +1271,22 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1270
1271
  }
1271
1272
  if (terminalEnvelope && latest) {
1272
1273
  const envelope = terminalEnvelope;
1273
- next.story_status = envelope.story_status;
1274
- next.story_complete = envelope.story_complete;
1275
- next.story_complete_reason = envelope.story_complete_reason;
1276
- const storyStatus = envelope.decision === "blocked"
1277
- ? "blocked"
1278
- : envelope.decision === "done"
1279
- ? "review"
1280
- : storyStatusForEnvelope(envelope.story_status);
1281
- next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
1282
- ? {
1283
- ...story,
1284
- status: storyStatus,
1285
- updated_at: nowIso,
1286
- ...(envelope.decision === "blocked"
1287
- ? { blocked_reason: envelope.blocked_reason ?? "" }
1288
- : { blocked_reason: undefined }),
1289
- }
1290
- : story);
1274
+ const storyStatus = terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
1275
+ if (storyStatus !== undefined) {
1276
+ next.story_status = envelope.story_status;
1277
+ next.story_complete = envelope.story_complete;
1278
+ next.story_complete_reason = envelope.story_complete_reason;
1279
+ next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
1280
+ ? {
1281
+ ...story,
1282
+ status: storyStatus,
1283
+ updated_at: nowIso,
1284
+ ...(envelope.decision === "blocked"
1285
+ ? { blocked_reason: envelope.blocked_reason ?? "" }
1286
+ : { blocked_reason: undefined }),
1287
+ }
1288
+ : story);
1289
+ }
1291
1290
  if (approvalGate?.violation) {
1292
1291
  next.last_envelope = {
1293
1292
  ref: latest.ref,
@@ -2682,6 +2681,10 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
2682
2681
  if (!deps.dryRun)
2683
2682
  writeAnsweredQuestions(hqRoot, current, questions);
2684
2683
  if (queueReason !== undefined) {
2684
+ const answerEnv = deps.spawnLaneDeps?.env ?? deps.env ?? process.env;
2685
+ const answerCaller = typeof answerEnv.HQ_LANE_ID === "string"
2686
+ ? answerEnv.HQ_LANE_ID.trim() || "user"
2687
+ : "user";
2685
2688
  const queued = casLaneIf(hqRoot, current.lane_id, (latest) => latest.state === "awaiting_input", (latest) => ({
2686
2689
  ...latest,
2687
2690
  state: "queued",
@@ -2693,6 +2696,7 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
2693
2696
  ? { session_id: latest.provider_session_id }
2694
2697
  : {}),
2695
2698
  trigger: "answered_questions",
2699
+ caller: answerCaller,
2696
2700
  at: answeredAt,
2697
2701
  },
2698
2702
  }), deps.dryRun);
@@ -2005,6 +2005,18 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2005
2005
  // Claim running before spawning. Two callers that both observe queued must
2006
2006
  // not both start processes; the CAS loser returns without spawn.
2007
2007
  const nowIso = (deps.now ?? (() => new Date()))().toISOString();
2008
+ const resumeHistoryTrigger = pendingResume?.keep_alive === true
2009
+ ? "keep_alive"
2010
+ : pendingResume?.trigger === "message" || trigger.kind === "message"
2011
+ ? "message"
2012
+ : pendingResume?.trigger === "resume" || trigger.kind === "resume"
2013
+ ? "resume_cli"
2014
+ : "reconcile";
2015
+ const resumeHistoryCaller = pendingResume?.caller ||
2016
+ (typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "") ||
2017
+ (trigger.kind === "answered_questions" || trigger.kind === "resume" || trigger.kind === "message"
2018
+ ? "user"
2019
+ : undefined);
2008
2020
  let statusGateHeartbeat;
2009
2021
  try {
2010
2022
  statusGateHeartbeat = managerStatusGateHeartbeat(hqRoot, existing, laneTurnId, nowIso);
@@ -2016,6 +2028,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2016
2028
  statusGateHeartbeat = undefined;
2017
2029
  }
2018
2030
  let claimed = false;
2031
+ let priorStartRecorded = false;
2019
2032
  const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
2020
2033
  claimed = false;
2021
2034
  if (current.state === "running" && current.account === resolved.id) {
@@ -2027,6 +2040,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2027
2040
  if (!claimableStatesFor(trigger, current).includes(current.state)) {
2028
2041
  throw new LanesError("spawn_state_mismatch", `Cannot mark ${current.lane_id} running from ${current.state}`, { lane_id: current.lane_id, state: current.state, trigger: trigger.kind });
2029
2042
  }
2043
+ // The host report is written in the claim CAS before exec proves that a
2044
+ // worker started. Use the prior spawn manifest's turn id (written only
2045
+ // after detachment is proven), with last_resume for legacy manifests.
2046
+ priorStartRecorded = previousSpawn.turn_id !== undefined ||
2047
+ current.last_resume !== undefined ||
2048
+ (current.resume_history?.length ?? 0) > 0;
2030
2049
  if (failedResumeClaim &&
2031
2050
  (failedResumeExpectedSenior === undefined ||
2032
2051
  !sameSeniorRef(current.senior, failedResumeExpectedSenior))) {
@@ -2260,6 +2279,16 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2260
2279
  }
2261
2280
  throw err;
2262
2281
  }
2282
+ if (priorStartRecorded) {
2283
+ retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => ({
2284
+ ...current,
2285
+ resume_history: appendCappedHistory(current.resume_history ?? [], {
2286
+ at: nowIso,
2287
+ trigger: resumeHistoryTrigger,
2288
+ ...(resumeHistoryCaller ? { caller: resumeHistoryCaller } : {}),
2289
+ }),
2290
+ }));
2291
+ }
2263
2292
  return laneLaunchResult(readLane(hqRoot, lane.lane_id));
2264
2293
  }
2265
2294
  /**
@@ -2563,6 +2592,16 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2563
2592
  killQuietly(killProcess, workerPid, "worker");
2564
2593
  throw err;
2565
2594
  }
2595
+ const startedAt = (deps.now ?? (() => new Date()))().toISOString();
2596
+ const callerLaneId = typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "";
2597
+ retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => ({
2598
+ ...current,
2599
+ resume_history: appendCappedHistory(current.resume_history ?? [], {
2600
+ at: startedAt,
2601
+ trigger: "envelope_repair",
2602
+ ...(callerLaneId ? { caller: callerLaneId } : {}),
2603
+ }),
2604
+ }));
2566
2605
  return workerPid;
2567
2606
  }
2568
2607
  export { PROVIDER_HOME_ENV, PROVIDER_COMMAND };
@@ -751,6 +751,10 @@ export function parseLaneRecord(raw, filePath) {
751
751
  path: filePath,
752
752
  });
753
753
  }
754
+ if (isPlainObject(raw.pending_resume) &&
755
+ raw.pending_resume.caller !== undefined) {
756
+ requireString(raw.pending_resume.caller, "pending_resume.caller");
757
+ }
754
758
  if (raw.keep_alive_history !== undefined) {
755
759
  if (!Array.isArray(raw.keep_alive_history) || raw.keep_alive_history.length > HISTORY_CAP) {
756
760
  throw new LaneSchemaError(`keep_alive_history must be an array capped at ${HISTORY_CAP} entries`, {
@@ -773,6 +777,29 @@ export function parseLaneRecord(raw, filePath) {
773
777
  requireTimestamp(value.at, `${field}.at`, filePath);
774
778
  }
775
779
  }
780
+ if (raw.resume_history !== undefined) {
781
+ if (!Array.isArray(raw.resume_history) || raw.resume_history.length > HISTORY_CAP) {
782
+ throw new LaneSchemaError(`resume_history must be an array capped at ${HISTORY_CAP} entries`, {
783
+ path: filePath,
784
+ field: "resume_history",
785
+ });
786
+ }
787
+ for (const [index, value] of raw.resume_history.entries()) {
788
+ const field = `resume_history[${index}]`;
789
+ if (!isPlainObject(value) ||
790
+ Object.keys(value).some((key) => !["at", "trigger", "caller"].includes(key))) {
791
+ throw new LaneSchemaError(`${field} must be a resume history entry`, {
792
+ path: filePath,
793
+ field,
794
+ value,
795
+ });
796
+ }
797
+ requireTimestamp(value.at, `${field}.at`, filePath);
798
+ requireString(value.trigger, `${field}.trigger`);
799
+ if (value.caller !== undefined)
800
+ requireString(value.caller, `${field}.caller`);
801
+ }
802
+ }
776
803
  if (raw.stop_history !== undefined) {
777
804
  if (!Array.isArray(raw.stop_history)) {
778
805
  throw new LaneSchemaError("stop_history must be an array when present", {
@@ -1163,6 +1190,11 @@ export function mergeLaneRecords(live, sidecars) {
1163
1190
  failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
1164
1191
  }
1165
1192
  : {}),
1193
+ ...(all.some((record) => record.resume_history !== undefined)
1194
+ ? {
1195
+ resume_history: unionHistory(all.map((record) => record.resume_history ?? []), entryIdentity, base.resume_history ?? []),
1196
+ }
1197
+ : {}),
1166
1198
  ...(all.some((record) => record.settings_history !== undefined)
1167
1199
  ? {
1168
1200
  settings_history: unionHistory(all.map((record) => record.settings_history ?? []), entryIdentity, base.settings_history ?? []),
@@ -1,4 +1,4 @@
1
- import type { LaneRecord, LaneStory } from "./types.js";
1
+ import type { LaneRecord, LaneStory, LaneStoryStatus } from "./types.js";
2
2
  /** Board-facing states for the stories a lane currently carries. */
3
3
  export declare const ACTIVE_LANE_STORY_STATUSES: readonly ["in_progress", "blocked", "review"];
4
4
  export declare function storyKey(projectId: string, storyId: string): string;
@@ -14,4 +14,10 @@ export declare function storyWarning(lane: LaneRecord): string | undefined;
14
14
  export declare function withCurrentStory(lane: LaneRecord, projectId: string, storyId: string): LaneRecord;
15
15
  export declare function withLaneStories(lane: LaneRecord, stories: LaneStory[]): LaneRecord;
16
16
  export declare function storyStatusForEnvelope(status: string): "in_progress" | "review";
17
+ /**
18
+ * Resolve the story status requested by a terminal envelope. Already-final
19
+ * carried stories are immutable through the envelope path; senior verification
20
+ * and explicit story commands own those transitions.
21
+ */
22
+ export declare function terminalEnvelopeStoryStatus(lane: LaneRecord, decision: "done" | "blocked" | "ask", envelopeStatus?: string): LaneStoryStatus | undefined;
17
23
  //# sourceMappingURL=stories.d.ts.map
@@ -73,4 +73,20 @@ export function storyStatusForEnvelope(status) {
73
73
  return "review";
74
74
  return "in_progress";
75
75
  }
76
+ /**
77
+ * Resolve the story status requested by a terminal envelope. Already-final
78
+ * carried stories are immutable through the envelope path; senior verification
79
+ * and explicit story commands own those transitions.
80
+ */
81
+ export function terminalEnvelopeStoryStatus(lane, decision, envelopeStatus = "in_progress") {
82
+ const primary = laneStory(lane, lane.project_id, lane.story_id);
83
+ if (primary && (String(primary.status) === "done" || String(primary.status) === "dropped")) {
84
+ return undefined;
85
+ }
86
+ if (decision === "blocked")
87
+ return "blocked";
88
+ if (decision === "done")
89
+ return "review";
90
+ return storyStatusForEnvelope(envelopeStatus);
91
+ }
76
92
  //# sourceMappingURL=stories.js.map
@@ -412,6 +412,14 @@ export interface KeepAliveResumeHistoryEntry {
412
412
  resumed_from: "done" | "interrupted" | "blocked";
413
413
  account: string | null;
414
414
  }
415
+ /** Bounded audit entry for every lane worker start after its first start. */
416
+ export interface LaneResumeHistoryEntry {
417
+ at: string;
418
+ /** Stable trigger class; path-specific starts may use their path name. */
419
+ trigger: string;
420
+ /** HQ lane id for an agent caller, or `user` for an explicit human CLI action. */
421
+ caller?: string;
422
+ }
415
423
  export interface KrFreshnessHistoryEntry {
416
424
  event: "kr_stalled";
417
425
  turn_id: string;
@@ -659,6 +667,8 @@ export interface LaneRecord {
659
667
  provider_home_at_spawn?: string;
660
668
  /** Set at spawn when this run continued a recorded session instead of starting fresh. */
661
669
  last_resume?: LaneResumeRecord;
670
+ /** Last 50 lane worker starts after the initial start, including actor provenance. */
671
+ resume_history?: LaneResumeHistoryEntry[];
662
672
  /** Cache counters from the last exited run's result. Nothing else from the payload. */
663
673
  last_run_usage?: LaneRunUsage;
664
674
  /** Completion time paired with last_run_usage; written by the watcher. */
@@ -723,6 +733,8 @@ export interface PendingResume {
723
733
  message_id?: string;
724
734
  /** The text is already framed as untrusted monitor data, not an operator instruction. */
725
735
  monitor_event?: true;
736
+ /** Caller identity captured when a deferred start was requested, when known. */
737
+ caller?: string;
726
738
  /** False when the command passed --no-compact; the deferred spawn honours it. */
727
739
  compact?: boolean;
728
740
  at: string;
@@ -233,7 +233,7 @@ export const LANE_FIELD_WRITERS = {
233
233
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
234
234
  admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command", "reconcile"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
235
235
  quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
236
- provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
236
+ provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], resume_history: ["spawn", "watcher"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
237
237
  };
238
238
  function isPlainObject(value) {
239
239
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -36,7 +36,7 @@ import * as path from "node:path";
36
36
  import { LanesError } from "./errors.js";
37
37
  import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
38
38
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, LANE_PROVIDERS, } from "./types.js";
39
- import { laneStories, storyStatusForEnvelope } from "./stories.js";
39
+ import { laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
40
40
  import { appendCappedHistory, listQuestionIds, readLane, readQuestion, retryUpdateLane, withLaneHireLocks } from "./store.js";
41
41
  import { enforceRegistryApprovalGate } from "./approval-gate.js";
42
42
  import { readWorkerRunResult } from "./delivery.js";
@@ -423,27 +423,25 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
423
423
  artifacts: envelope.artifacts,
424
424
  ...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
425
425
  };
426
- next.story_status = envelope.story_status;
427
- next.story_complete = envelope.story_complete;
428
- next.story_complete_reason = envelope.story_complete_reason;
429
- const currentProject = current.project_id;
430
- const currentStory = current.story_id;
431
- const storyStatus = envelope.decision === "blocked"
432
- ? "blocked"
433
- : envelope.decision === "done"
434
- ? "review"
435
- : storyStatusForEnvelope(envelope.story_status);
436
- const updatedStories = laneStories(current).map((story) => story.project_id === currentProject && story.story_id === currentStory
437
- ? {
438
- ...story,
439
- status: storyStatus,
440
- updated_at: at,
441
- ...(envelope.decision === "blocked"
442
- ? { blocked_reason: envelope.blocked_reason ?? "" }
443
- : { blocked_reason: undefined }),
444
- }
445
- : story);
446
- next.stories = updatedStories;
426
+ const storyStatus = terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
427
+ if (storyStatus !== undefined) {
428
+ next.story_status = envelope.story_status;
429
+ next.story_complete = envelope.story_complete;
430
+ next.story_complete_reason = envelope.story_complete_reason;
431
+ const currentProject = current.project_id;
432
+ const currentStory = current.story_id;
433
+ const updatedStories = laneStories(current).map((story) => story.project_id === currentProject && story.story_id === currentStory
434
+ ? {
435
+ ...story,
436
+ status: storyStatus,
437
+ updated_at: at,
438
+ ...(envelope.decision === "blocked"
439
+ ? { blocked_reason: envelope.blocked_reason ?? "" }
440
+ : { blocked_reason: undefined }),
441
+ }
442
+ : story);
443
+ next.stories = updatedStories;
444
+ }
447
445
  }
448
446
  return next;
449
447
  });
@@ -1,5 +1,5 @@
1
1
  /** The varlock version hq-cli is pinned to (package.json `dependencies`). */
2
- export declare const VARLOCK_PINNED_VERSION = "1.0.0";
2
+ export declare const VARLOCK_PINNED_VERSION = "1.21.1";
3
3
  /** A schema/plugin source varlock could not finish loading. */
4
4
  interface GuardDataSource {
5
5
  readonly isValid: boolean;
@@ -68,7 +68,7 @@ export declare function assertEnvGraphLoaded(graph: LoadedEnvGraph): void;
68
68
  * rejection into a thrown EnvResolutionError instead of hanging on the promise
69
69
  * that never settles.
70
70
  *
71
- * varlock@1.0.0's resolveEnvValues dispatches its per-item resolver un-awaited
71
+ * varlock's resolveEnvValues dispatches its per-item resolver un-awaited
72
72
  * and with a dead `_reject`, so a throw inside it becomes a process-level
73
73
  * unhandled rejection AND leaves the returned promise permanently pending. We
74
74
  * race the resolve against a scoped `unhandledRejection` listener: whichever
@@ -34,7 +34,7 @@
34
34
  // actionable `hq: …` line WITHOUT a Sentry capture (see isVarlockEnvError).
35
35
  import { claimRejection, releaseRejection, } from "../unhandled-rejection-boundary.js";
36
36
  /** The varlock version hq-cli is pinned to (package.json `dependencies`). */
37
- export const VARLOCK_PINNED_VERSION = "1.0.0";
37
+ export const VARLOCK_PINNED_VERSION = "1.21.1";
38
38
  /**
39
39
  * A schema/entry file could not be loaded, so the env graph never finished
40
40
  * processing. Carries an actionable, file-naming message; classified by
@@ -114,7 +114,7 @@ export function assertEnvGraphLoaded(graph) {
114
114
  * rejection into a thrown EnvResolutionError instead of hanging on the promise
115
115
  * that never settles.
116
116
  *
117
- * varlock@1.0.0's resolveEnvValues dispatches its per-item resolver un-awaited
117
+ * varlock's resolveEnvValues dispatches its per-item resolver un-awaited
118
118
  * and with a dead `_reject`, so a throw inside it becomes a process-level
119
119
  * unhandled rejection AND leaves the returned promise permanently pending. We
120
120
  * race the resolve against a scoped `unhandledRejection` listener: whichever
@@ -143,6 +143,9 @@ export async function resolveEnvValuesOrThrow(graph) {
143
143
  });
144
144
  process.prependOnceListener("unhandledRejection", onRejection);
145
145
  try {
146
+ // Preserve current varlock's direct rejection unchanged. The race only
147
+ // turns a floated rejection (the 1.0.0 behavior) into a typed error rather
148
+ // than leaving the caller waiting on a promise that never settles.
146
149
  await Promise.race([graph.resolveEnvValues(), rejection]);
147
150
  }
148
151
  finally {
@@ -23,8 +23,9 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
23
23
  errorsByName: new Map(),
24
24
  loadedSecretsByName: new Map(),
25
25
  };
26
- // varlock@1.0.0's plugin-lib.js omits the Resolver export (d.ts/JS mismatch);
27
- // extract it at runtime from any already-registered built-in resolver's prototype.
26
+ // In varlock@1.21.1, Resolver remains declared in plugin-lib's .d.mts but
27
+ // is not exported from its runtime module. Derive the runtime base class from
28
+ // a built-in resolver registered on the graph; the shape test pins this API.
28
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
30
  let RuntimeResolver;
30
31
  try {
@@ -39,8 +40,8 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
39
40
  RuntimeResolver = proto;
40
41
  }
41
42
  catch (e) {
42
- throw new Error('varlock Resolver base class extraction failed — the varlock@1.0.0 d.ts/JS mismatch ' +
43
- 'may have been resolved; switch to `import { Resolver } from "varlock/plugin-lib"`. ' +
43
+ throw new Error('varlock Resolver base class extraction failed — varlock@1.21.1 still declares ' +
44
+ 'Resolver in plugin-lib types without exporting it at runtime. ' +
44
45
  `Underlying: ${e instanceof Error ? e.message : String(e)}`);
45
46
  }
46
47
  // HqResolver is declared INSIDE installHqPlugin so its static def.resolve
@@ -96,7 +97,7 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
96
97
  }
97
98
  }
98
99
  }
99
- // varlock@1.0.0's env-spec parser rejects dots in decorator names ([a-zA-Z0-9_] only),
100
+ // Varlock's env-spec parser rejects dots in decorator names ([a-zA-Z0-9_] only),
100
101
  // so `@hq.company` is not valid syntax. We register as `@hqCompany` (camelCase) instead.
101
102
  // Schema files must use `# @hqCompany("slug")` followed by a blank line so the parser
102
103
  // treats it as a file-level root decorator rather than an item decorator for the next var.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.295.0",
3
+ "version": "5.296.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -58,7 +58,7 @@
58
58
  "semver": "^7.6.3",
59
59
  "simple-git": "^3.27.0",
60
60
  "smol-toml": "1.6.1",
61
- "varlock": "1.0.0"
61
+ "varlock": "1.21.1"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@eslint/js": "^10.0.1",