@indigoai-us/hq-cli 5.280.0 → 5.281.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.
@@ -20,6 +20,7 @@ import { buildEntityIndex } from "../lib/checkpoint-ontology/tree.js";
20
20
  import { MERGE_COPIES_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
21
21
  import { callDeleteEndpoint } from "./files.js";
22
22
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
23
+ import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
23
24
  export const ONTOLOGY_PAYLOAD_FILE = "payload.json";
24
25
  class OntologyUsageError extends Error {
25
26
  }
@@ -267,5 +268,15 @@ export function registerCoreOntologyCommand(core) {
267
268
  }
268
269
  });
269
270
  describeCoreCommand(apply, { execution: "native", root: "live" });
271
+ registerCoreOntologyNativeCommands(ontology, () => {
272
+ const argv = process.argv.slice(2);
273
+ const coreIndex = argv.indexOf("core");
274
+ const ontologyIndex = argv.indexOf("ontology", coreIndex + 1);
275
+ const rootOptionIndex = argv.indexOf("--hq-root", coreIndex + 1);
276
+ const groupRoot = rootOptionIndex >= 0 && (ontologyIndex < 0 || rootOptionIndex < ontologyIndex)
277
+ ? argv[rootOptionIndex + 1]
278
+ : undefined;
279
+ return groupRoot ? rootFor({ hqRoot: groupRoot }) : process.env.HQ_ROOT || rootFor({});
280
+ });
270
281
  }
271
282
  //# sourceMappingURL=core-ontology.js.map
@@ -222,6 +222,15 @@ export const SCAFFOLD_ONLY_ASSETS = [
222
222
  { asset: "core/scripts/hq-delegate-secrets.sh", root: "live", summary: "Rollback shell for native delegate secrets" },
223
223
  { asset: "core/scripts/hq-delegate-transfer.sh", root: "live", summary: "Rollback shell for native delegate transfer" },
224
224
  { asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
225
+ // Retained rollback companions for the gated native ontology commands. These
226
+ // are loose HQ-tree assets, but `hq core ontology` dispatches natively.
227
+ { asset: "core/scripts/ontology-audience-grant.sh", root: "live", summary: "Rollback companion for native ontology audience grant" },
228
+ { asset: "core/scripts/ontology-brief.mjs", root: "live", summary: "Rollback companion for native ontology brief" },
229
+ { asset: "core/scripts/ontology-candidate.sh", root: "live", summary: "Rollback companion for native ontology candidate" },
230
+ { asset: "core/scripts/ontology-garden.mjs", root: "live", summary: "Rollback companion for native ontology garden" },
231
+ { asset: "core/scripts/ontology-source-worklist.sh", root: "live", summary: "Rollback companion for native ontology source worklist" },
232
+ { asset: "core/scripts/source-yaml-validate.sh", root: "cwd", summary: "Rollback companion for native source YAML validation" },
233
+ { asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "Sibling dependency for ontology candidate rollback companion" },
225
234
  ...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
226
235
  asset,
227
236
  root,
@@ -242,8 +242,9 @@ export function registerGroupGrantsCommand(program) {
242
242
  });
243
243
  grants
244
244
  .command("outbound")
245
- .description("List grants a source-company group holds on other companies")
246
- .requiredOption("--group <groupId>", "Group id to inspect")
245
+ .description("List grants a source-company group holds on other companies; " +
246
+ "requires --group <groupId>.")
247
+ .requiredOption("--group <groupId>", "Group id to inspect (required)")
247
248
  .action(async (opts) => {
248
249
  try {
249
250
  const token = await ensureCognitoToken();
@@ -25,6 +25,7 @@ export { spawnLaneWorker };
25
25
  export { questionBodyAbsPath };
26
26
  export { MAX_LANE_DEPTH };
27
27
  export declare const LANES_RUNS_REL: string;
28
+ export declare const LANE_REMOTE_MIRROR_SYNC_FLAG = "lanes.remote-done-mirror";
28
29
  /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
29
30
  export { emitLaneDecisionEvents };
30
31
  export interface LanesCommandDeps {
@@ -61,6 +61,7 @@ import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
61
61
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
62
62
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
63
63
  import { LanesError } from "../lib/lanes/errors.js";
64
+ import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
64
65
  import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
65
66
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
66
67
  import { managerStatusGateEnabledForLane, resolveManagerStatusGateForLane, } from "../lib/lanes/manager-status-gate.js";
@@ -74,6 +75,8 @@ export { spawnLaneWorker };
74
75
  export { questionBodyAbsPath };
75
76
  export { MAX_LANE_DEPTH };
76
77
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
78
+ export const LANE_REMOTE_MIRROR_SYNC_FLAG = "lanes.remote-done-mirror";
79
+ const REMOTE_MIRROR_REFRESH_BATCH_SIZE = 100;
77
80
  /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
78
81
  export { emitLaneDecisionEvents };
79
82
  function withoutProviderHome(value) {
@@ -714,7 +717,7 @@ async function dispatchOutpostLaneList(ctx, outpostId, query, timeoutSeconds, wa
714
717
  timeoutSeconds,
715
718
  waitMs,
716
719
  });
717
- if (!rejectedLaneListFlag(filtered, query))
720
+ if (query.strictSnapshot || !rejectedLaneListFlag(filtered, query))
718
721
  return filtered;
719
722
  return ctx.dispatchOutpostExec({
720
723
  outpostId,
@@ -874,6 +877,7 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
874
877
  !listLaneIds(hqRoot).includes(lane.senior.id)) {
875
878
  throw new LanesError("hire_grant_context_required", `Resume of Outpost lane ${lane.lane_id} requires the primary senior's hire-grant context`, { lane_id: lane.lane_id, senior_lane_id: lane.senior.id });
876
879
  }
880
+ await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), new Set([lane.senior.id]));
877
881
  return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
878
882
  const current = readLane(hqRoot, lane.lane_id);
879
883
  if (grantContext !== undefined &&
@@ -1054,6 +1058,7 @@ function normalizedLaneListQuery(query) {
1054
1058
  ...(query.active ? { active: true } : {}),
1055
1059
  ...(query.hierarchy === undefined ? {} : { hierarchy: query.hierarchy }),
1056
1060
  ...(query.snapshot ? { snapshot: true } : {}),
1061
+ ...(query.strictSnapshot ? { strictSnapshot: true } : {}),
1057
1062
  };
1058
1063
  }
1059
1064
  async function readOutpostLaneRecordsUncached(ctx, outpostId, query) {
@@ -1144,6 +1149,156 @@ async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}) {
1144
1149
  byOutpost.set(outpostId, { queryKey, promise });
1145
1150
  return promise;
1146
1151
  }
1152
+ /** Refresh only primary-created, non-terminal Outpost mirrors in one bounded owner query per Outpost. */
1153
+ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
1154
+ const result = {
1155
+ updated: new Map(),
1156
+ unverifiedRemote: new Map(),
1157
+ };
1158
+ const host = reconcileHostFromCtx(ctx);
1159
+ if (host.outpostId !== null || parentLaneIds?.size === 0)
1160
+ return result;
1161
+ let primaryIdentity;
1162
+ const originMatchesThisPrimary = (lane) => {
1163
+ if (lane.origin_host_source !== "imds") {
1164
+ return lane.origin_host === host.hostname;
1165
+ }
1166
+ primaryIdentity ??= ctx.resolveMachineIdentity();
1167
+ if (primaryIdentity.status !== "ok")
1168
+ return false;
1169
+ const primaryImdsId = primaryIdentity.imds ??
1170
+ (primaryIdentity.source === "imds" ? primaryIdentity.id : null);
1171
+ return primaryImdsId !== null && primaryImdsId === lane.origin_host;
1172
+ };
1173
+ const candidates = records
1174
+ .filter((lane) => lane.host.kind === "outpost" &&
1175
+ originMatchesThisPrimary(lane) &&
1176
+ (parentLaneIds === undefined || (lane.senior.kind === "lane" &&
1177
+ lane.senior.id !== undefined &&
1178
+ parentLaneIds.has(lane.senior.id))) &&
1179
+ !isTerminalLaneState(lane.state))
1180
+ .sort((a, b) => a.lane_id.localeCompare(b.lane_id));
1181
+ if (candidates.length === 0)
1182
+ return result;
1183
+ const enabledByCompany = new Map();
1184
+ const enabledCandidates = [];
1185
+ for (const lane of candidates) {
1186
+ const companyKey = JSON.stringify([lane.company?.uid ?? null, lane.company?.slug ?? null]);
1187
+ let enabled = enabledByCompany.get(companyKey);
1188
+ if (enabled === undefined) {
1189
+ enabled = await readLanesRegistryFlag(LANE_REMOTE_MIRROR_SYNC_FLAG, {
1190
+ company: lane.company,
1191
+ env: ctx.env,
1192
+ });
1193
+ enabledByCompany.set(companyKey, enabled);
1194
+ }
1195
+ if (enabled)
1196
+ enabledCandidates.push(lane);
1197
+ }
1198
+ if (enabledCandidates.length === 0)
1199
+ return result;
1200
+ const byOutpost = new Map();
1201
+ for (const lane of enabledCandidates) {
1202
+ const group = byOutpost.get(lane.host.id) ?? [];
1203
+ group.push(lane);
1204
+ byOutpost.set(lane.host.id, group);
1205
+ }
1206
+ for (const [outpostId, group] of byOutpost) {
1207
+ const batch = group.slice(0, REMOTE_MIRROR_REFRESH_BATCH_SIZE);
1208
+ const overflow = group.slice(REMOTE_MIRROR_REFRESH_BATCH_SIZE);
1209
+ if (overflow.length > 0) {
1210
+ const ids = overflow.map((lane) => lane.lane_id);
1211
+ for (const lane of overflow) {
1212
+ result.unverifiedRemote.set(lane.lane_id, "refresh_batch_limit");
1213
+ }
1214
+ process.stderr.write(`hq lanes: unverified remote mirrors from Outpost ${outpostId} remain counted; refresh batch limit left lanes ${ids.join(", ")} unread\n`);
1215
+ }
1216
+ let ownerRecords;
1217
+ try {
1218
+ ownerRecords = await readOutpostLaneRecords(ctx, outpostId, {
1219
+ laneIds: batch.map((lane) => lane.lane_id),
1220
+ snapshot: true,
1221
+ strictSnapshot: true,
1222
+ });
1223
+ }
1224
+ catch (error) {
1225
+ const detail = isLanesError(error)
1226
+ ? `${error.error}: ${error.message}`
1227
+ : dispatchErrorMessage(error);
1228
+ for (const lane of batch)
1229
+ result.unverifiedRemote.set(lane.lane_id, detail);
1230
+ process.stderr.write(`hq lanes: unverified remote mirrors from Outpost ${outpostId} remain counted: ${detail}; lanes ${batch.map((lane) => lane.lane_id).join(", ")}\n`);
1231
+ continue;
1232
+ }
1233
+ for (const mirror of batch) {
1234
+ const matches = ownerRecords.filter((lane) => lane.lane_id === mirror.lane_id);
1235
+ const owner = matches[0];
1236
+ const ownerIsHostedByOutpost = owner !== undefined && ((owner.host.kind === "outpost" && owner.host.id === outpostId) ||
1237
+ (owner.host.kind === "local" && owner.host.id.trim() !== ""));
1238
+ if (matches.length !== 1 ||
1239
+ owner === undefined ||
1240
+ !ownerIsHostedByOutpost ||
1241
+ (owner.company?.uid ?? null) !== (mirror.company?.uid ?? null) ||
1242
+ (owner.company?.slug ?? null) !== (mirror.company?.slug ?? null)) {
1243
+ const detail = matches.length === 0
1244
+ ? "owner did not return the requested lane"
1245
+ : matches.length > 1
1246
+ ? "owner returned duplicate lane rows"
1247
+ : "owner lane identity did not match its mirror";
1248
+ result.unverifiedRemote.set(mirror.lane_id, detail);
1249
+ process.stderr.write(`hq lanes: unverified remote mirror ${mirror.lane_id} from Outpost ${outpostId} remains counted: ${detail}\n`);
1250
+ continue;
1251
+ }
1252
+ const endedAt = isTerminalLaneState(owner.state)
1253
+ ? owner.last_envelope?.at ?? owner.updated_at
1254
+ : undefined;
1255
+ const envelopeSummary = isTerminalLaneState(owner.state)
1256
+ ? owner.envelope_summary
1257
+ : undefined;
1258
+ const before = readLane(hqRoot, mirror.lane_id);
1259
+ if (before.host.kind !== "outpost" || before.host.id !== outpostId ||
1260
+ !originMatchesThisPrimary(before) || isTerminalLaneState(before.state)) {
1261
+ result.updated.set(mirror.lane_id, before);
1262
+ continue;
1263
+ }
1264
+ const needsUpdate = before.state !== owner.state ||
1265
+ before.queued_reason !== (owner.state === "queued" ? owner.queued_reason : undefined) ||
1266
+ before.ended_at !== endedAt ||
1267
+ before.envelope_summary !== envelopeSummary ||
1268
+ (isTerminalLaneState(owner.state) && (JSON.stringify(before.last_envelope) !== JSON.stringify(owner.last_envelope) ||
1269
+ before.story_complete !== owner.story_complete ||
1270
+ before.story_complete_reason !== owner.story_complete_reason ||
1271
+ before.story_status !== owner.story_status ||
1272
+ before.terminal_reason !== owner.terminal_reason));
1273
+ if (!needsUpdate) {
1274
+ result.updated.set(mirror.lane_id, before);
1275
+ continue;
1276
+ }
1277
+ const updated = retryUpdateLane(hqRoot, mirror.lane_id, "reconcile", (current) => {
1278
+ if (current.host.kind !== "outpost" || current.host.id !== outpostId ||
1279
+ !originMatchesThisPrimary(current) || isTerminalLaneState(current.state))
1280
+ return current;
1281
+ return {
1282
+ ...current,
1283
+ state: owner.state,
1284
+ queued_reason: owner.state === "queued" ? owner.queued_reason : undefined,
1285
+ ended_at: endedAt,
1286
+ envelope_summary: envelopeSummary,
1287
+ ...(isTerminalLaneState(owner.state) ? {
1288
+ last_envelope: owner.last_envelope,
1289
+ story_complete: owner.story_complete,
1290
+ story_complete_reason: owner.story_complete_reason,
1291
+ story_status: owner.story_status,
1292
+ terminal_reason: owner.terminal_reason,
1293
+ terminal_details: owner.terminal_details,
1294
+ } : {}),
1295
+ };
1296
+ });
1297
+ result.updated.set(mirror.lane_id, updated);
1298
+ }
1299
+ }
1300
+ return result;
1301
+ }
1147
1302
  async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1148
1303
  try {
1149
1304
  return { lane: readLane(ctx.hqRoot(), laneId), remoteOnly: false };
@@ -3035,6 +3190,9 @@ async function runCreate(ctx, opts) {
3035
3190
  : {}),
3036
3191
  attribution_history: [],
3037
3192
  };
3193
+ if (senior.kind === "lane" && senior.id) {
3194
+ await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), new Set([senior.id]));
3195
+ }
3038
3196
  const lane = senior.kind === "lane"
3039
3197
  ? withHireGrantAdmission(hqRoot, senior.id, ({ parent, children, parentGrant, availableChildGrant }) => {
3040
3198
  if (isTerminalLaneState(parent.state)) {
@@ -4228,7 +4386,7 @@ async function runList(ctx, opts) {
4228
4386
  const hierarchyIds = opts.hierarchy === undefined
4229
4387
  ? undefined
4230
4388
  : new Set(selectOutpostHireGrantHierarchy(opts.hierarchy, laneSource).map((lane) => lane.lane_id));
4231
- const selectedLanes = laneSource
4389
+ let selectedLanes = laneSource
4232
4390
  .filter((lane) => {
4233
4391
  if (requestedLaneIds.length > 0 && !requestedIds.has(lane.lane_id))
4234
4392
  return false;
@@ -4254,9 +4412,27 @@ async function runList(ctx, opts) {
4254
4412
  })
4255
4413
  .sort(compareLaneQueueOrder);
4256
4414
  if (opts.snapshot) {
4257
- printJson({ ok: true, action: "list", lanes: selectedLanes });
4415
+ printJson({
4416
+ ok: true,
4417
+ action: "list",
4418
+ lanes: selectedLanes.map((lane) => {
4419
+ if (!isTerminalLaneState(lane.state))
4420
+ return lane;
4421
+ const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
4422
+ return envelope?.summary ? { ...lane, envelope_summary: envelope.summary } : lane;
4423
+ }),
4424
+ });
4258
4425
  return;
4259
4426
  }
4427
+ const remoteRefresh = await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(selectedLanes
4428
+ .filter((lane) => !isTerminalLaneState(lane.state))
4429
+ .map((lane) => lane.lane_id)));
4430
+ for (const [laneId, updated] of remoteRefresh.updated) {
4431
+ const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
4432
+ if (index >= 0)
4433
+ laneSource[index] = updated;
4434
+ }
4435
+ selectedLanes = selectedLanes.map((lane) => remoteRefresh.updated.get(lane.lane_id) ?? lane);
4260
4436
  const now = ctx.now();
4261
4437
  const host = reconcileHostFromCtx(ctx);
4262
4438
  const allLanes = laneSource;
@@ -4313,6 +4489,7 @@ async function runList(ctx, opts) {
4313
4489
  return {
4314
4490
  ...lane,
4315
4491
  ...hireByLane.get(lane.lane_id),
4492
+ ...(remoteRefresh.unverifiedRemote.has(lane.lane_id) ? { unverified_remote: true } : {}),
4316
4493
  stories: laneStories(lane),
4317
4494
  story_ids: laneStories(lane).map((story) => story.story_id),
4318
4495
  no_story_reason: lane.no_story_reason ?? null,
@@ -4349,6 +4526,9 @@ async function runList(ctx, opts) {
4349
4526
  ok: true,
4350
4527
  action: "list",
4351
4528
  lanes,
4529
+ ...(remoteRefresh.unverifiedRemote.size > 0
4530
+ ? { unverified_remote_lanes: [...remoteRefresh.unverifiedRemote.keys()] }
4531
+ : {}),
4352
4532
  ...(host_capacity !== undefined ? { host_capacity } : {}),
4353
4533
  };
4354
4534
  if (opts.human)
@@ -4768,6 +4948,13 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
4768
4948
  ? [initial.senior.id]
4769
4949
  : []),
4770
4950
  ];
4951
+ if (authorization === "senior") {
4952
+ const parentLaneIds = new Set([initial.lane_id]);
4953
+ if (initial.senior.kind === "lane" && initial.senior.id) {
4954
+ parentLaneIds.add(initial.senior.id);
4955
+ }
4956
+ await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), parentLaneIds);
4957
+ }
4771
4958
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
4772
4959
  const current = readLane(hqRoot, laneId);
4773
4960
  const currentCallerAuthorization = authorization === "senior"
@@ -6221,6 +6408,12 @@ async function reparentOnOwner(ctx, laneId, opts) {
6221
6408
  const nextSenior = parseSeniorRef(opts.senior);
6222
6409
  assertSeniorExists(hqRoot, nextSenior, ctx);
6223
6410
  const initial = readLane(hqRoot, laneId);
6411
+ const refreshParents = new Set();
6412
+ if (initial.senior.kind === "lane" && initial.senior.id)
6413
+ refreshParents.add(initial.senior.id);
6414
+ if (nextSenior.kind === "lane" && nextSenior.id)
6415
+ refreshParents.add(nextSenior.id);
6416
+ await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), refreshParents);
6224
6417
  const lockIds = [
6225
6418
  laneId,
6226
6419
  ...(initial.senior.kind === "lane" && initial.senior.id ? [initial.senior.id] : []),
@@ -6380,12 +6573,58 @@ const CANONICAL_LOG_STREAMS = [
6380
6573
  ];
6381
6574
  async function runLogs(ctx, laneId, opts) {
6382
6575
  const hqRoot = ctx.hqRoot();
6383
- readLane(hqRoot, laneId);
6384
- const logsDir = path.join(runDir(hqRoot, laneId), "logs");
6576
+ const lane = readLane(hqRoot, laneId);
6385
6577
  const tail = parseOptionalInt(opts.tail, "--tail");
6386
6578
  if (tail === 0) {
6387
6579
  throw new LanesError("invalid_integer", "--tail must be a positive integer", { flag: "--tail", value: opts.tail });
6388
6580
  }
6581
+ if (lane.host.kind === "outpost" &&
6582
+ !laneRunsHere(lane, reconcileHostFromCtx(ctx)) &&
6583
+ await readLanesRegistryFlag(LANE_REMOTE_MIRROR_SYNC_FLAG, {
6584
+ company: lane.company,
6585
+ env: ctx.env,
6586
+ })) {
6587
+ const outpostId = lane.host.id;
6588
+ const waitMs = outpostSingleLaneWaitMs(ctx.env);
6589
+ let dispatched;
6590
+ try {
6591
+ dispatched = await ctx.dispatchOutpostExec({
6592
+ outpostId,
6593
+ command: buildOutpostLaneCommand({
6594
+ action: "logs",
6595
+ laneId,
6596
+ hqBinary: outpostCliOverride(ctx, outpostId),
6597
+ ...(tail === undefined ? {} : { tail: String(tail) }),
6598
+ json: true,
6599
+ }),
6600
+ timeoutSeconds: Math.ceil(waitMs / 1000),
6601
+ waitMs,
6602
+ });
6603
+ }
6604
+ catch (error) {
6605
+ throw new LanesError("outpost_lane_logs_unavailable", `Could not read logs for lane ${laneId} from Outpost ${outpostId}: ${dispatchErrorMessage(error)}`, { lane_id: laneId, outpost_id: outpostId, wait_ms: waitMs, wait_env: OUTPOST_SINGLE_LANE_WAIT_ENV });
6606
+ }
6607
+ if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
6608
+ throw new LanesError("outpost_lane_logs_unavailable", `Outpost ${outpostId} did not return logs for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, status: dispatched.status, exit_code: dispatched.exitCode });
6609
+ }
6610
+ let body;
6611
+ try {
6612
+ body = JSON.parse(dispatched.stdout);
6613
+ }
6614
+ catch (error) {
6615
+ throw new LanesError("outpost_lane_logs_invalid", `Outpost ${outpostId} returned invalid logs JSON for lane ${laneId}: ${dispatchErrorMessage(error)}`, { lane_id: laneId, outpost_id: outpostId });
6616
+ }
6617
+ if (body === null || typeof body !== "object" || Array.isArray(body) ||
6618
+ body.ok !== true ||
6619
+ body.action !== "logs" ||
6620
+ body.lane_id !== laneId ||
6621
+ !Array.isArray(body.streams)) {
6622
+ throw new LanesError("outpost_lane_logs_invalid", `Outpost ${outpostId} returned a malformed logs payload for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId });
6623
+ }
6624
+ printJson(body);
6625
+ return;
6626
+ }
6627
+ const logsDir = path.join(runDir(hqRoot, laneId), "logs");
6389
6628
  const names = new Set(CANONICAL_LOG_STREAMS);
6390
6629
  if (fs.existsSync(logsDir)) {
6391
6630
  for (const name of fs.readdirSync(logsDir)) {
@@ -7059,6 +7298,9 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
7059
7298
  async function runReconcile(ctx, opts, recoverQueuedMessages) {
7060
7299
  const fromEnv = ctx.env.HQ_ROOT?.trim();
7061
7300
  const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
7301
+ const mirrorRefresh = opts.dryRun
7302
+ ? { updated: new Map(), unverifiedRemote: new Map() }
7303
+ : await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot));
7062
7304
  const summary = await runReconcilePass(hqRoot, {
7063
7305
  now: ctx.now,
7064
7306
  host: reconcileHostFromCtx(ctx),
@@ -7174,7 +7416,12 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7174
7416
  }
7175
7417
  },
7176
7418
  });
7177
- printJson(summary);
7419
+ printJson({
7420
+ ...summary,
7421
+ ...(mirrorRefresh.unverifiedRemote.size > 0
7422
+ ? { unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()] }
7423
+ : {}),
7424
+ });
7178
7425
  if (!summary.ok) {
7179
7426
  process.exit(1);
7180
7427
  }
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target" | "reparent-target" | "approve-target";
30
+ action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target";
31
31
  laneId: string;
32
32
  hqBinary?: string;
33
33
  text?: string;
@@ -47,6 +47,8 @@ export interface OutpostLaneCommand {
47
47
  senior?: string;
48
48
  reason?: string;
49
49
  compact?: boolean;
50
+ tail?: string;
51
+ json?: boolean;
50
52
  }
51
53
  export declare function buildOutpostQuestionAnswerCommand(input: {
52
54
  questionId: string;
@@ -174,6 +174,10 @@ export function buildOutpostLaneCommand(input) {
174
174
  args.push("--keep-alive");
175
175
  if (input.compact === false)
176
176
  args.push("--no-compact");
177
+ if (input.action === "logs" && input.tail !== undefined)
178
+ args.push("--tail", input.tail);
179
+ if (input.action === "logs" && input.json === true)
180
+ args.push("--json");
177
181
  return withRemoteHqDir(joinCommandParts(args));
178
182
  }
179
183
  /** True when a remote CLI exited after rejecting the forwarded effort flag. */
@@ -416,6 +416,17 @@ export function parseLaneRecord(raw, filePath) {
416
416
  requireInt(raw.version, "version");
417
417
  requireInt(raw.depth, "depth");
418
418
  requireInt(raw.priority, "priority");
419
+ if (raw.ended_at !== undefined) {
420
+ requireTimestamp(raw.ended_at, "ended_at", filePath);
421
+ }
422
+ if (raw.envelope_summary !== undefined &&
423
+ (typeof raw.envelope_summary !== "string" || raw.envelope_summary.trim() === "")) {
424
+ throw new LaneSchemaError("envelope_summary must be a non-empty string when present", {
425
+ path: filePath,
426
+ field: "envelope_summary",
427
+ value: raw.envelope_summary,
428
+ });
429
+ }
419
430
  if (raw.hire_grant !== undefined &&
420
431
  (typeof raw.hire_grant !== "number" || !Number.isSafeInteger(raw.hire_grant) || raw.hire_grant < 0)) {
421
432
  throw new LaneSchemaError("Expected non-negative safe integer for hire_grant", {
@@ -526,6 +526,10 @@ export interface LaneRecord {
526
526
  version: number;
527
527
  created_at: string;
528
528
  updated_at: string;
529
+ /** Owner-host completion time copied to a primary mirror during reconcile. */
530
+ ended_at?: string;
531
+ /** Summary from the owner's latest terminal envelope, copied to a mirror. */
532
+ envelope_summary?: string;
529
533
  attribution_history: AttributionHistoryEntry[];
530
534
  last_envelope?: LastEnvelopeRef;
531
535
  /** Effective model id recorded at create/spawn. Omitted when the provider chooses. */
@@ -226,7 +226,7 @@ export const LANE_FIELD_WRITERS = {
226
226
  priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
227
  keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
228
228
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
229
- version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
229
+ version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command"],
230
230
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
231
231
  last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
232
232
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.280.0",
3
+ "version": "5.281.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {