@indigoai-us/hq-cli 5.148.0 → 5.149.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +8 -11
  3. package/dist/command-catalog.generated.d.ts +129 -0
  4. package/dist/command-catalog.generated.js +168 -0
  5. package/dist/commands/lanes.js +506 -23
  6. package/dist/commands/mesh.js +82 -11
  7. package/dist/lib/lanes/index.d.ts +3 -2
  8. package/dist/lib/lanes/index.js +2 -1
  9. package/dist/lib/lanes/mesh-events.d.ts +8 -0
  10. package/dist/lib/lanes/mesh-events.js +83 -6
  11. package/dist/lib/lanes/outpost-dispatch.d.ts +12 -0
  12. package/dist/lib/lanes/outpost-dispatch.js +17 -0
  13. package/dist/lib/lanes/reconcile.js +39 -12
  14. package/dist/lib/lanes/session-relink.d.ts +25 -0
  15. package/dist/lib/lanes/session-relink.js +216 -0
  16. package/dist/lib/lanes/spawn.d.ts +1 -0
  17. package/dist/lib/lanes/spawn.js +48 -5
  18. package/dist/lib/lanes/store.js +109 -1
  19. package/dist/lib/lanes/stories.d.ts +17 -0
  20. package/dist/lib/lanes/stories.js +73 -0
  21. package/dist/lib/lanes/types.d.ts +20 -0
  22. package/dist/lib/lanes/types.js +9 -2
  23. package/dist/lib/lanes/watcher.js +69 -9
  24. package/dist/lib/mesh/live/daemon/doctor.d.ts +27 -0
  25. package/dist/lib/mesh/live/daemon/doctor.js +84 -0
  26. package/dist/lib/mesh/live/daemon/index.d.ts +3 -3
  27. package/dist/lib/mesh/live/daemon/index.js +2 -2
  28. package/dist/lib/mesh/live/daemon/install.d.ts +40 -0
  29. package/dist/lib/mesh/live/daemon/install.js +88 -0
  30. package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
  31. package/dist/lib/mesh/live/daemon/run.js +12 -1
  32. package/dist/lib/mesh/live/daemon/state.d.ts +4 -0
  33. package/dist/lib/mesh/project-board.d.ts +15 -0
  34. package/dist/lib/mesh/project-board.js +84 -0
  35. package/dist/lib/work-context/prd-sync.d.ts +5 -1
  36. package/dist/lib/work-context/prd-sync.js +22 -1
  37. package/package.json +5 -3
  38. package/scripts/mesh-daemon-if-stale.mjs +16 -0
@@ -9,11 +9,12 @@ import { resolve as resolvePath } from "node:path";
9
9
  import * as os from "node:os";
10
10
  import { readFileSync } from "node:fs";
11
11
  import { findHqRoot } from "../utils/manifest.js";
12
+ import { CLI_VERSION } from "../cli-version.js";
12
13
  import chalk from "chalk";
13
14
  import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
14
15
  import * as readline from "node:readline/promises";
15
16
  import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
16
- import { closeSupersededProjectThread, ensureProjectBoard } from "../lib/mesh/project-board.js";
17
+ import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard } from "../lib/mesh/project-board.js";
17
18
  import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
18
19
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
19
20
  import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
@@ -34,7 +35,7 @@ import { generateUlid } from "../lib/mesh/live/ulid.js";
34
35
  import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
35
36
  import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
36
37
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
37
- import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
38
+ import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatCliIdentityLines, formatDaemonDoctor, installDaemonIfStale, installDaemonService, readDaemonState, resolveAbsoluteNodeBinary, resolveRunningCliEntry, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
38
39
  import { workContextRoot } from "../lib/work-context/paths.js";
39
40
  import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
40
41
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
@@ -575,6 +576,29 @@ async function runPrdSync(opts) {
575
576
  ...(bound?.projectId ? { project: bound.projectId } : {}),
576
577
  touchedPaths: [resolvePath(file)],
577
578
  };
579
+ if (bound?.companyUid && bound.projectId) {
580
+ try {
581
+ const source = parseProjectPlan(readFileSync(file, "utf8"), file);
582
+ await ensureMissingBoardStories({
583
+ token,
584
+ company: {
585
+ companyUid: bound.companyUid,
586
+ companySlug: bound.companySlug || bound.companyUid,
587
+ },
588
+ projectId: bound.projectId,
589
+ name: source.name || bound.projectId,
590
+ description: source.description ?? "",
591
+ stories: source.stories,
592
+ repos: source.repos ?? [],
593
+ requiredIds: plan.events.map((event) => event.taskId),
594
+ });
595
+ }
596
+ catch (err) {
597
+ const reason = err instanceof Error ? err.message : String(err);
598
+ console.error(`work-mesh: prd-sync ensure failed; emitting anyway (${reason.slice(0, 160)})`);
599
+ }
600
+ }
601
+ const meshRoot = workMeshHomeRoot();
578
602
  const seqFirst = reserveSessionSeq(sessionId, plan.events.length);
579
603
  const events = plan.events.map((event, index) => buildMeshEmitEvent({
580
604
  eventId: generateUlid(Date.now() + index),
@@ -592,7 +616,7 @@ async function runPrdSync(opts) {
592
616
  }));
593
617
  const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
594
618
  const emitted = await emitEvents({
595
- workMeshRoot: workMeshHomeRoot(),
619
+ workMeshRoot: meshRoot,
596
620
  poster,
597
621
  newEvents: events,
598
622
  });
@@ -601,8 +625,15 @@ async function runPrdSync(opts) {
601
625
  for (const line of formatEmitRejectionLines(emitted.rejections))
602
626
  console.error(line);
603
627
  }
604
- plan.commit();
605
- if (plan.taskId) {
628
+ const blocked = new Set(emitted.rejections.map((row) => row.eventId));
629
+ for (const pending of readEmitRetry(meshRoot)) {
630
+ const id = typeof pending.eventId === "string" ? pending.eventId : "";
631
+ if (id)
632
+ blocked.add(id);
633
+ }
634
+ const advanced = events.flatMap((event) => (event.eventId && event.taskId && !blocked.has(event.eventId) ? [event.taskId] : []));
635
+ plan.commit(advanced);
636
+ if (plan.taskId && advanced.includes(plan.taskId)) {
606
637
  updateSessionState(sessionId, root, (current) => {
607
638
  if (!current)
608
639
  return null;
@@ -639,6 +670,12 @@ async function runDoctor(opts) {
639
670
  fail("`hq mesh doctor --apply` (paced Board PUTs from local prd.json) is not in the CLI yet.\n" +
640
671
  "Omit --apply to warm the conversation cache (directory, inbox, pair DMs).");
641
672
  }
673
+ const identity = collectDaemonDoctor();
674
+ const identityLines = formatCliIdentityLines(identity);
675
+ if (!opts.json) {
676
+ for (const line of identityLines)
677
+ console.log(line);
678
+ }
642
679
  const token = await requireToken();
643
680
  const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
644
681
  if (!principalUid) {
@@ -647,7 +684,15 @@ async function runDoctor(opts) {
647
684
  const warmed = await warmMeshConversationCache(token, principalUid);
648
685
  const needsProject = listNeedsProjectSessions(workContextHomeRoot());
649
686
  if (opts.json) {
650
- console.log(JSON.stringify({ ok: true, action: "doctor", ...warmed, needsProject }, null, 2));
687
+ console.log(JSON.stringify({
688
+ ok: true,
689
+ action: "doctor",
690
+ ...warmed,
691
+ needsProject,
692
+ cliMismatchLine: identity.cliMismatchLine ?? null,
693
+ hqOnPath: identity.hqOnPath,
694
+ multipleHqOnPath: identity.multipleHqOnPath,
695
+ }, null, 2));
651
696
  return;
652
697
  }
653
698
  console.log(`Work mesh cache warmed at ${warmed.cacheRoot}` +
@@ -1938,6 +1983,7 @@ export function registerMeshCommand(program) {
1938
1983
  .description("Install LaunchAgent (macOS), systemd user unit (Linux), or Scheduled Task (Windows)")
1939
1984
  .option("--system-user <user>", "Write a root-owned unit template for agent boxes (e.g. ec2-user)")
1940
1985
  .option("--hq-root <path>", "HQ root to record for the service (else HQ_ROOT, else the nearest HQ root above cwd)")
1986
+ .option("--if-stale", "Reinstall and restart only when this CLI is newer than the daemon's recorded version")
1941
1987
  .option("--json", "Print machine-readable JSON")
1942
1988
  .action((opts) => wrap(() => runDaemonInstall(opts))());
1943
1989
  daemon
@@ -1971,13 +2017,38 @@ async function runDaemonInstall(opts) {
1971
2017
  // Verified root only: a repo's own .claude/ must never become the daemon's
1972
2018
  // HQ root (that path is persisted as trusted and used for attribution).
1973
2019
  const hqRoot = resolveInstallHqRoot({ explicit: opts.hqRoot });
2020
+ const paths = buildInstallPaths({
2021
+ systemUser: opts.systemUser,
2022
+ hqRoot: hqRoot.root,
2023
+ hqRootSource: hqRoot.source,
2024
+ hqBinary: resolveRunningCliEntry(),
2025
+ nodeBinary: resolveAbsoluteNodeBinary(),
2026
+ });
2027
+ if (opts.ifStale) {
2028
+ const stale = installDaemonIfStale({
2029
+ currentVersion: CLI_VERSION,
2030
+ platform: detectPlatform(),
2031
+ paths,
2032
+ systemUser: opts.systemUser,
2033
+ });
2034
+ if (opts.json) {
2035
+ console.log(JSON.stringify({
2036
+ ok: true,
2037
+ action: stale.reinstalled ? "install" : "unchanged",
2038
+ reinstalled: stale.reinstalled,
2039
+ message: stale.message,
2040
+ hqBinary: paths.hqBinary,
2041
+ nodeBinary: paths.nodeBinary,
2042
+ hqRoot: hqRoot.root,
2043
+ }, null, 2));
2044
+ return;
2045
+ }
2046
+ console.log(stale.message);
2047
+ return;
2048
+ }
1974
2049
  const result = installDaemonService({
1975
2050
  platform: detectPlatform(),
1976
- paths: buildInstallPaths({
1977
- systemUser: opts.systemUser,
1978
- hqRoot: hqRoot.root,
1979
- hqRootSource: hqRoot.source,
1980
- }),
2051
+ paths,
1981
2052
  systemUser: opts.systemUser,
1982
2053
  });
1983
2054
  if (opts.json) {
@@ -3,8 +3,9 @@
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 { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, 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";
7
- export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
6
+ 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";
7
+ export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
8
+ export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
8
9
  export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
9
10
  export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
10
11
  export { resolveLanesHqRoot } from "./hq-root.js";
@@ -3,7 +3,8 @@
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 { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, 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";
6
+ 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";
7
+ export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
7
8
  export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
8
9
  export { resolveLanesHqRoot } from "./hq-root.js";
9
10
  export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
@@ -78,6 +78,14 @@ export interface CreateLaneMeshOptions {
78
78
  interactive?: boolean;
79
79
  /** Test seam. Defaults to the shared `requireToken`. */
80
80
  token?: () => Promise<string>;
81
+ /** Repair a missing project view before retrying a story status patch. */
82
+ repairProjectView?: (input: {
83
+ token: string;
84
+ company: LaneCompanyRef;
85
+ projectId: string;
86
+ storyId: string;
87
+ status: string;
88
+ }) => Promise<void>;
81
89
  }
82
90
  export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
83
91
  /** Resolve the explicit mesh attribution before falling back to the lane tenant. */
@@ -42,7 +42,7 @@
42
42
  * of opening a browser nobody is watching, and the watcher records
43
43
  * `mesh_event_failed` and moves on. No credential file is read here.
44
44
  */
45
- import { appendThreadEvent, clamp, ensureProjectThread, patchStoryStatus, requireToken, resolveMeshCompany, } from "../mesh/api.js";
45
+ import { appendThreadEvent, clamp, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
46
46
  import { LanesError } from "./errors.js";
47
47
  export const LANE_MESH_EVENTS = [
48
48
  "lane_started",
@@ -163,9 +163,64 @@ export function toWireThreadEvent(event, payload) {
163
163
  }
164
164
  }
165
165
  }
166
+ function isProjectViewNotFound(error) {
167
+ if (!error || typeof error !== "object")
168
+ return false;
169
+ const candidate = error;
170
+ return (candidate.code === "PROJECT_VIEW_NOT_FOUND" ||
171
+ (typeof candidate.message === "string" && candidate.message.includes("PROJECT_VIEW_NOT_FOUND")));
172
+ }
173
+ function isAlreadyRegistered(error) {
174
+ if (!error || typeof error !== "object")
175
+ return false;
176
+ const candidate = error;
177
+ return (candidate.code === "PROJECT_ALREADY_REGISTERED" ||
178
+ (typeof candidate.message === "string" && candidate.message.includes("PROJECT_ALREADY_REGISTERED")));
179
+ }
180
+ async function repairProjectView(input) {
181
+ const company = { companyUid: input.company.uid, companySlug: input.company.slug };
182
+ await ensureProjectThread(input.token, company, input.projectId, {
183
+ summary: `HQ lane ${input.projectId}`,
184
+ });
185
+ let current;
186
+ try {
187
+ current = await getProjectView(input.token, input.company.uid, input.projectId);
188
+ }
189
+ catch (error) {
190
+ if (!isProjectViewNotFound(error))
191
+ throw error;
192
+ }
193
+ const stories = [...(current?.stories ?? [])];
194
+ const index = stories.findIndex((story) => story.id === input.storyId);
195
+ const nextStory = {
196
+ ...(index >= 0 ? stories[index] : {}),
197
+ id: input.storyId,
198
+ title: index >= 0 ? stories[index]?.title || input.storyId : input.storyId,
199
+ status: input.status,
200
+ };
201
+ if (index >= 0)
202
+ stories[index] = nextStory;
203
+ else
204
+ stories.push(nextStory);
205
+ await putProjectView(input.token, input.company.uid, input.projectId, {
206
+ name: current?.name || input.projectId,
207
+ description: current?.description || "HQ lane project",
208
+ stories,
209
+ repos: current?.repos ?? [],
210
+ ...(current?.version !== undefined ? { expectedVersion: current.version } : {}),
211
+ });
212
+ try {
213
+ await registerMeshProject(input.token, input.company.uid, input.projectId);
214
+ }
215
+ catch (error) {
216
+ if (!isAlreadyRegistered(error))
217
+ throw error;
218
+ }
219
+ }
166
220
  export function createLaneMesh(opts = {}) {
167
221
  const interactive = opts.interactive ?? true;
168
222
  const token = opts.token ?? (() => requireToken({ interactive }));
223
+ const repair = opts.repairProjectView ?? repairProjectView;
169
224
  return {
170
225
  async ensureProjectThread(company, projectId) {
171
226
  const tok = await token();
@@ -182,7 +237,21 @@ export function createLaneMesh(opts = {}) {
182
237
  },
183
238
  async patchStoryStatus(company, projectId, storyId, status) {
184
239
  const tok = await token();
185
- await patchStoryStatus(tok, company.uid, projectId, storyId, status);
240
+ try {
241
+ await patchStoryStatus(tok, company.uid, projectId, storyId, status);
242
+ }
243
+ catch (error) {
244
+ if (!isProjectViewNotFound(error))
245
+ throw error;
246
+ await repair({
247
+ token: tok,
248
+ company,
249
+ projectId,
250
+ storyId,
251
+ status,
252
+ });
253
+ await patchStoryStatus(tok, company.uid, projectId, storyId, status);
254
+ }
186
255
  },
187
256
  };
188
257
  }
@@ -232,10 +301,18 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
232
301
  return;
233
302
  }
234
303
  case "blocked":
235
- await mesh.appendThreadEvent(company, threadId, "lane_blocked", {
236
- ...baseIdentity(lane),
237
- blocked_reason: envelope.blocked_reason ?? "",
238
- });
304
+ {
305
+ const outcomes = await Promise.allSettled([
306
+ mesh.appendThreadEvent(company, threadId, "lane_blocked", {
307
+ ...baseIdentity(lane),
308
+ blocked_reason: envelope.blocked_reason ?? "",
309
+ }),
310
+ mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "in_progress"),
311
+ ]);
312
+ const failure = outcomes.find((outcome) => outcome.status === "rejected");
313
+ if (failure?.status === "rejected")
314
+ throw failure.reason;
315
+ }
239
316
  return;
240
317
  case "ask":
241
318
  // A mesh question event has one `questionId`; sending one event per id
@@ -26,6 +26,16 @@ export interface OutpostDispatchResult {
26
26
  stdout: string;
27
27
  stderr: string;
28
28
  }
29
+ export interface OutpostLaneCommand {
30
+ action: "message" | "resume";
31
+ laneId: string;
32
+ text?: string;
33
+ project?: string;
34
+ story?: string;
35
+ account?: string;
36
+ model?: string;
37
+ compact?: boolean;
38
+ }
29
39
  export interface OutpostExecPort {
30
40
  stageExecInput: (outpostId?: string) => Promise<{
31
41
  putUrl: string;
@@ -47,6 +57,8 @@ export interface OutpostExecPort {
47
57
  export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPayload;
48
58
  /** Same wrapping `hq outposts exec` applies: run from the box's HQ folder. */
49
59
  export declare function buildOutpostPickupCommand(payloadUrl: string): string;
60
+ /** Build the same `hq outposts exec` remote command used by lane pickup. */
61
+ export declare function buildOutpostLaneCommand(input: OutpostLaneCommand): string;
50
62
  export declare function stageOutpostPayload(port: Pick<OutpostExecPort, "stageExecInput">, outpostId: string, bytes: Uint8Array, put?: typeof fetch): Promise<{
51
63
  getUrl: string;
52
64
  }>;
@@ -42,6 +42,23 @@ export function parseOutpostPickupPayload(raw) {
42
42
  export function buildOutpostPickupCommand(payloadUrl) {
43
43
  return withRemoteHqDir(joinCommandParts(["hq", "lanes", "pickup", "--payload-url", payloadUrl]));
44
44
  }
45
+ /** Build the same `hq outposts exec` remote command used by lane pickup. */
46
+ export function buildOutpostLaneCommand(input) {
47
+ const args = ["hq", "lanes", input.action, input.laneId];
48
+ for (const [flag, value] of [
49
+ ["--text", input.text],
50
+ ["--project", input.project],
51
+ ["--story", input.story],
52
+ ["--account", input.account],
53
+ ["--model", input.model],
54
+ ]) {
55
+ if (value !== undefined)
56
+ args.push(flag, value);
57
+ }
58
+ if (input.compact === false)
59
+ args.push("--no-compact");
60
+ return withRemoteHqDir(joinCommandParts(args));
61
+ }
45
62
  export async function stageOutpostPayload(port, outpostId, bytes, put = fetch) {
46
63
  const staged = await port.stageExecInput(outpostId);
47
64
  const upload = await put(staged.putUrl, {
@@ -20,6 +20,7 @@ import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./qu
20
20
  import { answeredQuestionEntries, raiseEnvelopeQuestions } from "./questions.js";
21
21
  import { SILENCE_MS, classifyLaneExit, isPidAlive, readLatestEnvelope, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
22
22
  import { createLaneMesh, emitLaneDecisionEvents, laneMeshCompany } from "./mesh-events.js";
23
+ import { laneStories, storyStatusForEnvelope } from "./stories.js";
23
24
  import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
24
25
  import { providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
25
26
  /** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
@@ -882,18 +883,44 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
882
883
  (latest.envelope.questions?.length ?? 0) > 0) {
883
884
  raiseEnvelopeQuestions(hqRoot, readLane(hqRoot, lane.lane_id), latest.envelope, latest.ref, nowIso);
884
885
  }
885
- const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) => ({
886
- ...current,
887
- state: repairedClassification.state,
888
- queued_reason: undefined,
889
- terminal_reason: repairedClassification.reason ?? "host_lost",
890
- ...(providerRejection
891
- ? { terminal_details: providerRejectionDetails(providerRejection, current) }
892
- : {}),
893
- ...(quota?.quota_reset_at ? { quota_reset_at: quota.quota_reset_at } : {}),
894
- ...(quota?.marker ? { model_quota_marker: quota.marker } : {}),
895
- last_host_report_at: nowIso,
896
- }), deps.dryRun);
886
+ const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) => {
887
+ const next = {
888
+ ...current,
889
+ state: repairedClassification.state,
890
+ queued_reason: undefined,
891
+ terminal_reason: repairedClassification.reason ?? "host_lost",
892
+ ...(providerRejection
893
+ ? { terminal_details: providerRejectionDetails(providerRejection, current) }
894
+ : {}),
895
+ ...(quota?.quota_reset_at ? { quota_reset_at: quota.quota_reset_at } : {}),
896
+ ...(quota?.marker ? { model_quota_marker: quota.marker } : {}),
897
+ last_host_report_at: nowIso,
898
+ };
899
+ if (latest?.envelope) {
900
+ const envelope = latest.envelope;
901
+ next.story_status = envelope.story_status;
902
+ if (envelope.story_complete !== undefined) {
903
+ next.story_complete = envelope.story_complete;
904
+ next.story_complete_reason = envelope.story_complete_reason;
905
+ }
906
+ const storyStatus = envelope.decision === "blocked"
907
+ ? "blocked"
908
+ : envelope.decision === "done"
909
+ ? "review"
910
+ : storyStatusForEnvelope(envelope.story_status);
911
+ next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
912
+ ? {
913
+ ...story,
914
+ status: storyStatus,
915
+ updated_at: nowIso,
916
+ ...(envelope.decision === "blocked"
917
+ ? { blocked_reason: envelope.blocked_reason ?? "" }
918
+ : { blocked_reason: undefined }),
919
+ }
920
+ : story);
921
+ }
922
+ return next;
923
+ }, deps.dryRun);
897
924
  if (result.changed) {
898
925
  summary.dead_host.push({
899
926
  id: lane.lane_id,
@@ -0,0 +1,25 @@
1
+ import { type SessionCopyResult } from "./move-account.js";
2
+ import type { LaneProvider } from "./types.js";
3
+ export interface EnsureSessionHomeInput {
4
+ provider: Exclude<LaneProvider, "grok">;
5
+ sessionId: string;
6
+ cwd: string;
7
+ targetHome: string;
8
+ registeredHomes: string[];
9
+ providerHomeAtSpawn?: string;
10
+ }
11
+ export interface EnsureSessionHomeResult extends SessionCopyResult {
12
+ source_home: string;
13
+ transcript_path: string;
14
+ homes_searched: string[];
15
+ }
16
+ /**
17
+ * Older Claude account homes can share a registered `account-N/.claude`
18
+ * parent even after one account is re-registered elsewhere. Discover only
19
+ * those sibling homes, then inspect the exact session path; credential files
20
+ * are never opened.
21
+ */
22
+ export declare function discoverSiblingAccountHomes(provider: EnsureSessionHomeInput["provider"], registeredHomes: string[]): string[];
23
+ /** Find, copy, and verify a provider transcript in the home the next run uses. */
24
+ export declare function ensureSessionHome(input: EnsureSessionHomeInput): EnsureSessionHomeResult;
25
+ //# sourceMappingURL=session-relink.d.ts.map