@indigoai-us/hq-cli 5.289.0 → 5.291.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +670 -0
  3. package/assets/scaffold/core/scripts/handoff-open-steps.sh +81 -0
  4. package/assets/scaffold/core/scripts/handoff-post.sh +187 -0
  5. package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
  6. package/assets/scaffold/core/scripts/hq-session.sh +182 -21
  7. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
  8. package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
  9. package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
  10. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
  11. package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +40 -0
  12. package/dist/command-catalog.generated.d.ts +110 -0
  13. package/dist/command-catalog.generated.js +136 -0
  14. package/dist/commands/cloud.d.ts +26 -14
  15. package/dist/commands/cloud.js +207 -56
  16. package/dist/commands/core-handoff.d.ts +22 -0
  17. package/dist/commands/core-handoff.js +1240 -0
  18. package/dist/commands/core-jobs-native.d.ts +8 -0
  19. package/dist/commands/core-jobs-native.js +624 -0
  20. package/dist/commands/core-native-jobs-flag.d.ts +4 -0
  21. package/dist/commands/core-native-jobs-flag.js +7 -0
  22. package/dist/commands/core-ontology.d.ts +6 -1
  23. package/dist/commands/core-ontology.js +42 -7
  24. package/dist/commands/core-policy.js +24 -5
  25. package/dist/commands/core.js +15 -1
  26. package/dist/commands/integrations-core.js +7 -0
  27. package/dist/commands/lanes.js +111 -35
  28. package/dist/commands/members.js +5 -0
  29. package/dist/commands/mesh.js +46 -25
  30. package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
  31. package/dist/commands/personal-targeted-push-flag.js +7 -0
  32. package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
  33. package/dist/commands/personal-targeted-push-paths.js +67 -0
  34. package/dist/commands/secrets.js +9 -0
  35. package/dist/commands/skill.d.ts +3 -0
  36. package/dist/commands/skill.js +28 -0
  37. package/dist/fast-core.js +1 -0
  38. package/dist/lib/billing/free-month.d.ts +18 -0
  39. package/dist/lib/billing/free-month.js +31 -0
  40. package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
  41. package/dist/lib/billing/plan-limit-refusals.js +187 -0
  42. package/dist/lib/billing/plan-lock.d.ts +2 -6
  43. package/dist/lib/billing/plan-lock.js +5 -16
  44. package/dist/lib/checkpoint-ontology/entity-file.d.ts +21 -0
  45. package/dist/lib/checkpoint-ontology/entity-file.js +100 -0
  46. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +34 -2
  47. package/dist/lib/checkpoint-ontology/merge-copies.js +541 -27
  48. package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
  49. package/dist/lib/core-utils/secret-patterns.js +3 -2
  50. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  51. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  52. package/dist/lib/lanes/autopilot.d.ts +69 -0
  53. package/dist/lib/lanes/autopilot.js +418 -0
  54. package/dist/lib/lanes/mesh-events.js +2 -0
  55. package/dist/lib/lanes/store.d.ts +4 -2
  56. package/dist/lib/lanes/store.js +18 -9
  57. package/dist/lib/mesh/api.js +4 -0
  58. package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
  59. package/dist/lib/mesh/live/daemon/state.js +34 -2
  60. package/dist/main.js +23 -12
  61. package/dist/utils/plan-gate-error.d.ts +24 -0
  62. package/dist/utils/plan-gate-error.js +219 -9
  63. package/dist/utils/self-update.d.ts +3 -0
  64. package/dist/utils/self-update.js +232 -1
  65. package/package.json +2 -2
@@ -17,7 +17,7 @@ import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.j
17
17
  import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
18
18
  import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
19
19
  import { buildEntityIndex } from "../lib/checkpoint-ontology/tree.js";
20
- import { MERGE_COPIES_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
20
+ import { MERGE_COPIES_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_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
23
  import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
@@ -146,11 +146,12 @@ function mergeCopiesSummary(report) {
146
146
  const held = report.plans.filter((plan) => plan.held).length;
147
147
  const lines = [
148
148
  `ontology merge-copies (${report.mode.replace("_", " ")}) in ${report.company} (${report.pair_kind}): ${report.plans.length} of ${report.copies_found} copies, ${held} held, ${report.skipped.length} skipped`,
149
- ` merged ${report.merged.length}, relinked ${report.relinked.length}, deleted ${report.deleted.length}, not ready ${report.not_ready.length}, errors ${report.errors.length}`,
149
+ ` merged ${report.merged.length}, relinked ${report.relinked.length}${report.rewritten !== undefined ? `, rewritten ${report.rewritten.length}` : ""}, deleted ${report.deleted.length}, not ready ${report.not_ready.length}, errors ${report.errors.length}`,
150
150
  ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}; broken entity links ${report.broken_links_before} -> ${report.broken_links_after}`,
151
151
  ];
152
- for (const plan of report.plans.filter((entry) => entry.held))
153
- lines.push(` held ${plan.copy}: ${plan.held}`);
152
+ for (const plan of report.plans.filter((entry) => entry.held)) {
153
+ lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" ? ` (${plan.detail})` : ""}`);
154
+ }
154
155
  for (const error of report.errors)
155
156
  lines.push(` error ${error.path}: ${error.error}`);
156
157
  for (const entry of report.only_unmatched ?? [])
@@ -176,7 +177,36 @@ export function readListedPairs(file) {
176
177
  })) {
177
178
  throw new OntologyUsageError("ontology: --only must be a non-empty JSON array of { copy, original } paths");
178
179
  }
179
- return parsed.map((entry) => ({ copy: entry.copy, original: entry.original }));
180
+ return parsed.map((entry) => {
181
+ if (entry.resolve_external_ids === undefined)
182
+ return { copy: entry.copy, original: entry.original };
183
+ return { copy: entry.copy, original: entry.original, resolve_external_ids: readResolution(entry.copy, entry.resolve_external_ids) };
184
+ });
185
+ }
186
+ /** `{ provider: { primary, namespace, object_type? } }`, every value a non-empty string. */
187
+ function readResolution(copy, raw) {
188
+ const usage = `ontology: --only entry ${copy}: resolve_external_ids must map each provider to { primary, namespace, object_type? } strings`;
189
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).length === 0)
190
+ throw new OntologyUsageError(usage);
191
+ const out = {};
192
+ for (const [provider, value] of Object.entries(raw)) {
193
+ if (value === null || typeof value !== "object" || Array.isArray(value))
194
+ throw new OntologyUsageError(usage);
195
+ const record = value;
196
+ const nonEmpty = (field) => typeof field === "string" && field.length > 0;
197
+ if (!Object.keys(record).every((key) => key === "primary" || key === "namespace" || key === "object_type") ||
198
+ !nonEmpty(record.primary) ||
199
+ !nonEmpty(record.namespace) ||
200
+ (record.object_type !== undefined && !nonEmpty(record.object_type))) {
201
+ throw new OntologyUsageError(usage);
202
+ }
203
+ out[provider] = {
204
+ primary: record.primary,
205
+ namespace: record.namespace,
206
+ ...(record.object_type !== undefined ? { object_type: record.object_type } : {}),
207
+ };
208
+ }
209
+ return out;
180
210
  }
181
211
  /**
182
212
  * `hq core ontology merge-copies`: merge `<slug> <n>.md` copies into their
@@ -185,7 +215,10 @@ export function readListedPairs(file) {
185
215
  * `checkpoint.ontology.merge-copies` and `checkpoint.ontology.multi-writer`.
186
216
  * With `--variants`, merges two-file duplicate groups whose names differ only
187
217
  * in case, spacing or punctuation instead, gated by
188
- * `checkpoint.ontology.merge-variants`.
218
+ * `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
219
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
220
+ * invoice and bill links, source and knowledge links, and carries external
221
+ * ids and web domains.
189
222
  */
190
223
  export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
191
224
  if (options.apply && options.delete) {
@@ -200,7 +233,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
200
233
  throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
201
234
  const company = resolution.company;
202
235
  const readFlag = deps.readFlag ?? registryFlagReader;
203
- for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG]) {
236
+ for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
204
237
  if (!(await readFlag(flag, company))) {
205
238
  return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology merge-copies: skipped (${flag} is off)`;
206
239
  }
@@ -220,6 +253,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
220
253
  ...(options.variants ? { pairKind: "variants" } : {}),
221
254
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
222
255
  ...(only ? { only } : {}),
256
+ ...(options.rewriteRefs ? { rewriteRefs: true } : {}),
223
257
  ...(deleteVaultKey ? { deleteVaultKey } : {}),
224
258
  });
225
259
  return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
@@ -274,6 +308,7 @@ export function registerCoreOntologyCommand(core) {
274
308
  .option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
275
309
  .option("--limit <n>", "handle at most this many copies", (value) => Number(value))
276
310
  .option("--only <file>", "handle only the copy -> original pairs listed in this JSON file")
311
+ .option("--rewrite-refs", "also re-point signal refs, invoice, source and knowledge links to the survivor and carry external ids and web domains (checkpoint.ontology.merge-rewrite-refs)")
277
312
  .option("--json", "print JSON")
278
313
  .option("--hq-root <path>", "HQ installation to operate on")
279
314
  .action(async (options) => {
@@ -205,6 +205,28 @@ function extractRulePaths(file) {
205
205
  }
206
206
  return [...paths].sort();
207
207
  }
208
+ function shellReferenceStem(reference) {
209
+ const lastDot = reference.lastIndexOf('.');
210
+ return lastDot === -1 ? reference : reference.slice(0, lastDot);
211
+ }
212
+ function shellLsExtensionGlobHasMatch(root, stem) {
213
+ const patternPath = path.join(root, `${stem}.*`);
214
+ const directory = path.dirname(patternPath);
215
+ const basenamePattern = path.basename(patternPath);
216
+ const literalPrefix = basenamePattern.slice(0, -1);
217
+ let entries;
218
+ try {
219
+ entries = fs.readdirSync(directory);
220
+ }
221
+ catch (error) {
222
+ if (expectedFsMiss(error))
223
+ return false;
224
+ throw error;
225
+ }
226
+ // Bash 5.2 enables globskipdots by default, so `.*` only matches when the
227
+ // directory has an actual hidden entry. readdir omits `.` and `..`.
228
+ return entries.some((entry) => entry.startsWith(literalPrefix));
229
+ }
208
230
  function refSummary(file, root) {
209
231
  const refs = extractRulePaths(file);
210
232
  const judged = refs.filter((reference) => {
@@ -228,11 +250,8 @@ function refSummary(file, root) {
228
250
  }
229
251
  if (fs.existsSync(path.join(root, reference)))
230
252
  continue;
231
- const extension = path.posix.extname(reference);
232
- const stem = extension ? reference.slice(0, -extension.length) : reference;
233
- const parent = path.join(root, path.posix.dirname(stem));
234
- const basename = path.posix.basename(stem);
235
- const hasSibling = listFiles(parent).some((name) => name.startsWith(`${basename}.`));
253
+ const stem = shellReferenceStem(reference);
254
+ const hasSibling = shellLsExtensionGlobHasMatch(root, stem);
236
255
  if (hasSibling)
237
256
  renamed.push(reference);
238
257
  else
@@ -31,9 +31,11 @@
31
31
  import { Option } from "commander";
32
32
  import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
33
33
  import { registerCoreDelegateCommand } from "./core-delegate.js";
34
+ import { registerCoreHandoffCommand } from "./core-handoff.js";
34
35
  import { registerCoreOntologyCommand } from "./core-ontology.js";
35
36
  import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
36
37
  import { registerCorePolicyCommand } from "./core-policy.js";
38
+ import { registerCoreJobsNativeCommand } from "./core-jobs-native.js";
37
39
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
38
40
  import { resolveLiveRoot } from "../utils/hq-roots.js";
39
41
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
@@ -210,6 +212,8 @@ export const SCAFFOLD_COMMANDS = [
210
212
  * direct `hq core` command.
211
213
  */
212
214
  export const SCAFFOLD_ONLY_ASSETS = [
215
+ { asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
216
+ { asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
213
217
  // Shared shell libs sourced by the two relocated scripts (and hooks). Never
214
218
  // dispatched on their own (so no `hq core` command), but they MUST ship in the
215
219
  // tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
@@ -241,7 +245,7 @@ export const SCAFFOLD_ONLY_ASSETS = [
241
245
  { asset: "core/scripts/ontology-source-worklist.sh", root: "live", summary: "Rollback companion for native ontology source worklist" },
242
246
  { asset: "core/scripts/source-yaml-validate.sh", root: "cwd", summary: "Rollback companion for native source YAML validation" },
243
247
  // Shared by native knowledge-prefs and the ontology candidate shell fallback.
244
- { asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "Rollback companion for native knowledge-prefs and ontology candidate" },
248
+ { asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "Sibling dependency for ontology candidate rollback companion" },
245
249
  // Rollback companions for the default-off native knowledge and policy routes.
246
250
  // policy-retire also runs the scaffold validator in-process when native, but
247
251
  // its bundled shell fallback needs both helper assets at these exact paths.
@@ -250,6 +254,11 @@ export const SCAFFOLD_ONLY_ASSETS = [
250
254
  { asset: "core/scripts/read-policy-frontmatter.sh", root: "cwd", summary: "Rollback companion for native policy frontmatter" },
251
255
  { asset: "core/scripts/eval-trigger.sh", root: "live", summary: "Trigger grammar helper used by the policy validator companion" },
252
256
  { asset: ".claude/hooks/validate-policy-frontmatter.sh", root: "live", summary: "Policy validator companion used by policy-retire rollback" },
257
+ // Wave 2a handoff shells remain bundled as the default-off rollback path.
258
+ { asset: "core/scripts/handoff-finalize.sh", root: "live", summary: "Rollback shell for native handoff finalize" },
259
+ { asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
260
+ { asset: "core/scripts/handoff-open-steps.sh", root: "live", summary: "Rollback shell for native handoff open-steps" },
261
+ { asset: "core/scripts/qmd-reindex-bg.sh", root: "live", summary: "Rollback handoff reindex forwarder" },
253
262
  ...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
254
263
  asset,
255
264
  root,
@@ -302,6 +311,9 @@ function runEntry(entry, scope, args) {
302
311
  args,
303
312
  cwd,
304
313
  hqRoot,
314
+ env: entry.asset === "core/scripts/hq-session.sh"
315
+ ? { HQ_HQ_SESSION_NO_CLI: "1" }
316
+ : undefined,
305
317
  });
306
318
  // Propagate verbatim. Several of these scripts use their exit code as their
307
319
  // interface (drift detected, lint failed), so remapping it would break the
@@ -326,8 +338,10 @@ export function registerCoreCommands(program) {
326
338
  registerCoreOntologyCommand(core);
327
339
  registerCoreCheckpointGateSettingCommand(core);
328
340
  registerCoreDelegateCommand(core);
341
+ registerCoreHandoffCommand(core);
329
342
  registerCoreKnowledgePrefsCommand(core);
330
343
  registerCorePolicyCommand(core);
344
+ registerCoreJobsNativeCommand(core);
331
345
  // Wave 2 handoff/worktree commands retain their byte-identical shell assets
332
346
  // as a default-off rollback path for one release. Keep the gate helper small
333
347
  // so sibling Wave 2 work can reuse it without editing this command table.
@@ -12,6 +12,7 @@ import { randomUUID } from "node:crypto";
12
12
  import chalk from "chalk";
13
13
  import { vaultApiFetch } from "../utils/vault-api.js";
14
14
  import { AuthError } from "../utils/auth-error.js";
15
+ import { planGateErrorFromPayload } from "../utils/plan-gate-error.js";
15
16
  import { redactErrorText } from "../utils/redact-error-text.js";
16
17
  import { redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
17
18
  import { connectionHealthNotice } from "../lib/integrations/health.js";
@@ -195,6 +196,12 @@ export async function raiseForResponse(res, fallback) {
195
196
  raiseIfUnauthorized(res);
196
197
  raiseIfUpstreamUnavailable(res);
197
198
  const body = (await res.json().catch(() => ({})));
199
+ // A plan-limit 402 is normally decoded by vaultApiFetch before it gets here.
200
+ // Decode it again so no integration route can print the wire token
201
+ // `plan_limit_reached` as its whole message; main.ts renders the typed gate.
202
+ const planGate = planGateErrorFromPayload(res.status, body);
203
+ if (planGate)
204
+ throw planGate;
198
205
  // hq-pro mints this error text, so it is untrusted input to a message we
199
206
  // print verbatim — scrub credentials out before it can reach a terminal or a
200
207
  // Sentry crash report (same reasoning as the gateway path below).
@@ -32,6 +32,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
32
32
  import { startOutpostLaneSnapshotRefresh } from "../lib/lanes/outpost-snapshot-refresh.js";
33
33
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
34
34
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
35
+ import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
35
36
  import { getOrFetchOutpostSnapshot, OUTPOST_SNAPSHOT_CACHE_TTL_MS, outpostLaneListSnapshotKey, OutpostSnapshotInvocationMemo, } from "../lib/lanes/outpost-snapshot-cache.js";
36
37
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
37
38
  import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
@@ -2369,6 +2370,8 @@ async function emitLaneStartedForStories(ctx, company, lane, threadId, knownAbse
2369
2370
  failures.push(err);
2370
2371
  continue;
2371
2372
  }
2373
+ if (!story.project_id?.trim() || !story.story_id?.trim())
2374
+ continue;
2372
2375
  if (knownAbsentStories.has(storyBoardStoryKey(story.project_id, story.story_id)))
2373
2376
  continue;
2374
2377
  try {
@@ -3453,40 +3456,42 @@ async function runCreate(ctx, opts) {
3453
3456
  : undefined,
3454
3457
  });
3455
3458
  }
3456
- printJson({
3457
- ok: true,
3458
- action: "create",
3459
- lane_id: started.lane_id,
3460
- state: started.state,
3461
- queued_reason: started.queued_reason ?? null,
3462
- priority: started.priority,
3463
- provider: started.provider,
3464
- account: started.account ?? null,
3465
- model: started.model ?? null,
3466
- effort: started.effort ?? null,
3467
- effort_source: started.effort_source ?? null,
3468
- host: started.host,
3469
- ...(autoPlacementSkippedHosts.length > 0
3470
- ? { skipped_hosts: autoPlacementSkippedHosts }
3471
- : {}),
3472
- company: started.company,
3473
- project_id: started.project_id,
3474
- story_id: started.story_id,
3475
- stories: laneStories(started),
3476
- story_warning: combinedStoryWarning(started, createStoryWarnings),
3477
- thread_id: started.thread_id ?? null,
3478
- senior: started.senior,
3479
- depth: started.depth,
3480
- timeout_secs: timeoutSecs ?? null,
3481
- ...(validation ? { validation } : {}),
3482
- ...(started.outpost_command_id
3483
- ? {
3484
- outpost_command_id: started
3485
- .outpost_command_id,
3486
- }
3487
- : {}),
3488
- lane: started,
3489
- });
3459
+ if (!opts.quiet)
3460
+ printJson({
3461
+ ok: true,
3462
+ action: "create",
3463
+ lane_id: started.lane_id,
3464
+ state: started.state,
3465
+ queued_reason: started.queued_reason ?? null,
3466
+ priority: started.priority,
3467
+ provider: started.provider,
3468
+ account: started.account ?? null,
3469
+ model: started.model ?? null,
3470
+ effort: started.effort ?? null,
3471
+ effort_source: started.effort_source ?? null,
3472
+ host: started.host,
3473
+ ...(autoPlacementSkippedHosts.length > 0
3474
+ ? { skipped_hosts: autoPlacementSkippedHosts }
3475
+ : {}),
3476
+ company: started.company,
3477
+ project_id: started.project_id,
3478
+ story_id: started.story_id,
3479
+ stories: laneStories(started),
3480
+ story_warning: combinedStoryWarning(started, createStoryWarnings),
3481
+ thread_id: started.thread_id ?? null,
3482
+ senior: started.senior,
3483
+ depth: started.depth,
3484
+ timeout_secs: timeoutSecs ?? null,
3485
+ ...(validation ? { validation } : {}),
3486
+ ...(started.outpost_command_id
3487
+ ? {
3488
+ outpost_command_id: started
3489
+ .outpost_command_id,
3490
+ }
3491
+ : {}),
3492
+ lane: started,
3493
+ });
3494
+ return started;
3490
3495
  }
3491
3496
  async function dispatchCreatedOutpostLane(ctx, input) {
3492
3497
  const { hqRoot, lane, outpostId, briefText, timeout, preserveQueuedOnFailure = false, } = input;
@@ -6464,7 +6469,7 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
6464
6469
  ...payload,
6465
6470
  lane_status: lane.state,
6466
6471
  });
6467
- if (!skipStoryStatusPatch) {
6472
+ if (!skipStoryStatusPatch && projectId?.trim() && storyId?.trim()) {
6468
6473
  await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
6469
6474
  }
6470
6475
  }
@@ -7824,8 +7829,44 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7824
7829
  summary.ok = false;
7825
7830
  }
7826
7831
  const terminalizedRemoteIds = new Set(remoteLost.map(({ id }) => id));
7832
+ let autopilot;
7833
+ try {
7834
+ const host = reconcileHostFromCtx(ctx);
7835
+ autopilot = await runLeadAutopilotPass({
7836
+ hqRoot,
7837
+ host: host.outpostId ?? "local",
7838
+ hostname: host.hostname,
7839
+ outpostId: host.outpostId,
7840
+ now: ctx.now(),
7841
+ dryRun: Boolean(opts.dryRun),
7842
+ readFlag: readLanesRegistryFlag,
7843
+ keepAliveResumedLeads: new Set(summary.resumed.filter((entry) => entry.reason === "keep_alive").map((entry) => entry.id)),
7844
+ startWorker: async ({ lead, story }) => {
7845
+ const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
7846
+ const started = await runCreate(workerCtx, {
7847
+ company: lead.company?.slug,
7848
+ project: lead.project_id,
7849
+ story: story.id,
7850
+ briefFile: story.briefPath,
7851
+ provider: "codex",
7852
+ model: "gpt-6-luna",
7853
+ effort: "high",
7854
+ senior: `lane:${lead.lane_id}`,
7855
+ quiet: true,
7856
+ });
7857
+ return { lane_id: started.lane_id };
7858
+ },
7859
+ });
7860
+ }
7861
+ catch (error) {
7862
+ const message = error instanceof Error ? error.message : String(error);
7863
+ summary.ok = false;
7864
+ summary.errors.push({ code: "lead_autopilot_failed", message });
7865
+ process.stderr.write(`hq lanes autopilot: ${message}\n`);
7866
+ }
7827
7867
  printJson({
7828
7868
  ...summary,
7869
+ ...(autopilot ? { autopilot } : {}),
7829
7870
  ...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
7830
7871
  ...(mirrorRefresh.unverifiedRemote.size > 0
7831
7872
  ? {
@@ -8590,6 +8631,41 @@ export function registerLanesCommand(parent, deps = {}) {
8590
8631
  .action(wrap(async (opts) => {
8591
8632
  await runPickup(ctx, opts);
8592
8633
  }));
8634
+ lanes
8635
+ .command("autopilot")
8636
+ .description("Check registered team leads and wake them for ready work")
8637
+ .command("run")
8638
+ .description("Run one lead autopilot check pass")
8639
+ .option("--dry-run", "Report proposed wakes without writing them")
8640
+ .option("--json", "Emit JSON on stdout (default)")
8641
+ .action(wrap(async (opts) => {
8642
+ const host = reconcileHostFromCtx(ctx);
8643
+ const result = await runLeadAutopilotPass({
8644
+ hqRoot: ctx.hqRoot(),
8645
+ host: host.outpostId ?? "local",
8646
+ hostname: host.hostname,
8647
+ outpostId: host.outpostId,
8648
+ now: ctx.now(),
8649
+ dryRun: Boolean(opts.dryRun),
8650
+ readFlag: readLanesRegistryFlag,
8651
+ startWorker: async ({ lead, story }) => {
8652
+ const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
8653
+ const started = await runCreate(workerCtx, {
8654
+ company: lead.company?.slug,
8655
+ project: lead.project_id,
8656
+ story: story.id,
8657
+ briefFile: story.briefPath,
8658
+ provider: "codex",
8659
+ model: "gpt-6-luna",
8660
+ effort: "high",
8661
+ senior: `lane:${lead.lane_id}`,
8662
+ quiet: true,
8663
+ });
8664
+ return { lane_id: started.lane_id };
8665
+ },
8666
+ });
8667
+ printJson(result);
8668
+ }));
8593
8669
  const reconcile = lanes
8594
8670
  .command("reconcile", { hidden: true })
8595
8671
  .description("Run one machine-owned reconcile pass and exit")
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
3
3
  import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
4
4
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
5
+ import { isPlanGateError } from "../utils/plan-gate-error.js";
5
6
  const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
6
7
  const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
7
8
  const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
@@ -558,6 +559,10 @@ export function registerMembersCommand(program) {
558
559
  }
559
560
  }
560
561
  catch (err) {
562
+ // A seat-limit refusal goes to main.ts's shared plan-gate renderer:
563
+ // reason, usage, fix options and upgrade link, then a non-zero exit.
564
+ if (isPlanGateError(err))
565
+ throw err;
561
566
  // A 409 may refer to either an active membership or an existing
562
567
  // pending invite. Probe both views so email targets get actionable
563
568
  // guidance while preserving the terminal exit-0 behavior for bulk
@@ -11,7 +11,7 @@ import { readFileSync } from "node:fs";
11
11
  import { findHqRoot } from "../utils/manifest.js";
12
12
  import { CLI_VERSION } from "../cli-version.js";
13
13
  import chalk from "chalk";
14
- import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
14
+ import { ensureCognitoToken, isExpiring, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
15
15
  import * as readline from "node:readline/promises";
16
16
  import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
17
17
  import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
@@ -26,7 +26,9 @@ import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/i
26
26
  import { flushSessionEvents } from "../lib/mesh/live/flush.js";
27
27
  import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
28
28
  import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
29
- const MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
29
+ const ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
30
+ // Leave 500 ms of headroom inside SessionStart's 2 s process-group limit.
31
+ const OFFLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS = 1_500;
30
32
  import { buildEmitEvidence, machineEmitDisposition } from "../lib/mesh/live/emit-evidence.js";
31
33
  import { appendHeldLine } from "../lib/mesh/live/spool.js";
32
34
  import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
@@ -860,29 +862,45 @@ async function runContextReconcile(opts) {
860
862
  let offline = Boolean(opts.offline);
861
863
  let token;
862
864
  try {
863
- // Offline is outbox-only delivery. It still refreshes a device default's
864
- // token-backed membership so a revoked default cannot be preflighted.
865
- token = await requireToken();
866
- const sessionToken = token;
867
- validateMembership = async (candidate) => {
868
- const membership = await new Promise((resolve, reject) => {
869
- const timer = setTimeout(() => reject(new Error("membership validation timed out")), MEMBERSHIP_VALIDATION_TIMEOUT_MS);
870
- void resolveActiveMembershipCompany(sessionToken, candidate).then((value) => {
871
- clearTimeout(timer);
872
- resolve(value);
873
- }, (error) => {
874
- clearTimeout(timer);
875
- reject(error);
865
+ if (offline) {
866
+ if (isMachineIdentity()) {
867
+ token = await ensureCognitoToken({ tokenSource: "machine", interactive: false });
868
+ }
869
+ else {
870
+ const cached = loadCachedTokens();
871
+ if (cached && !isExpiring(cached, 120)) {
872
+ token = cached.accessToken;
873
+ }
874
+ }
875
+ }
876
+ else {
877
+ token = await requireToken();
878
+ }
879
+ if (token) {
880
+ const sessionToken = token;
881
+ const membershipValidationTimeoutMs = offline
882
+ ? OFFLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS
883
+ : ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS;
884
+ validateMembership = async (candidate) => {
885
+ const membership = await new Promise((resolve, reject) => {
886
+ const timer = setTimeout(() => reject(new Error("membership validation timed out")), membershipValidationTimeoutMs);
887
+ void resolveActiveMembershipCompany(sessionToken, candidate).then((value) => {
888
+ clearTimeout(timer);
889
+ resolve(value);
890
+ }, (error) => {
891
+ clearTimeout(timer);
892
+ reject(error);
893
+ });
876
894
  });
877
- });
878
- if (!membership || typeof membership.companyUid !== "string" || !membership.companyUid) {
879
- return false;
895
+ if (!membership || typeof membership.companyUid !== "string" || !membership.companyUid) {
896
+ return false;
897
+ }
898
+ return { uid: membership.companyUid };
899
+ };
900
+ if (!offline) {
901
+ deliver = createWorkSessionDeliverer({ token: sessionToken });
902
+ fetchCandidates = createCandidatesFetcher({ token: sessionToken });
880
903
  }
881
- return { uid: membership.companyUid };
882
- };
883
- if (!offline) {
884
- deliver = createWorkSessionDeliverer({ token: sessionToken });
885
- fetchCandidates = createCandidatesFetcher({ token: sessionToken });
886
904
  }
887
905
  }
888
906
  catch {
@@ -900,7 +918,10 @@ async function runContextReconcile(opts) {
900
918
  log: (message) => console.error(chalk.dim(message)),
901
919
  });
902
920
  if (activation.status === "needs_choice") {
903
- const interactive = !opts.machine && Boolean(process.stdin.isTTY && process.stdout.isTTY) && !isMachineIdentity();
921
+ const interactive = !offline &&
922
+ !opts.machine &&
923
+ Boolean(process.stdin.isTTY && process.stdout.isTTY) &&
924
+ !isMachineIdentity();
904
925
  if (interactive) {
905
926
  const choices = activation.memberships
906
927
  .map((membership, index) => `${index + 1}. ${membership.slug}`)
@@ -920,7 +941,7 @@ async function runContextReconcile(opts) {
920
941
  rl.close();
921
942
  }
922
943
  }
923
- else if (!opts.machine) {
944
+ else if (!offline && !opts.machine) {
924
945
  console.error(chalk.yellow("Multiple active memberships. Run `hq mesh context default set <slug>` to choose this device's default company."));
925
946
  }
926
947
  }
@@ -0,0 +1,4 @@
1
+ export declare const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
2
+ /** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
3
+ export declare function personalTargetedPushPathsEnabled(): boolean;
4
+ //# sourceMappingURL=personal-targeted-push-flag.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
2
+ export const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
3
+ /** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
4
+ export function personalTargetedPushPathsEnabled() {
5
+ return resolveProcessFlagGate(CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG, { fallback: false }, () => false);
6
+ }
7
+ //# sourceMappingURL=personal-targeted-push-flag.js.map
@@ -0,0 +1,3 @@
1
+ /** Resolve explicit --personal paths against the same roots as a full personal push. */
2
+ export declare function resolveExplicitPersonalPushPaths(hqRoot: string, requestedPaths: readonly string[], cwd?: string): string[];
3
+ //# sourceMappingURL=personal-targeted-push-paths.d.ts.map
@@ -0,0 +1,67 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { computePersonalVaultPaths, matchPersonalVaultExclusion, } from "@indigoai-us/hq-cloud";
4
+ function isWithin(parent, candidate) {
5
+ const relative = path.relative(parent, candidate);
6
+ return relative === "" ||
7
+ (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
8
+ }
9
+ function rejectPath(input, reason) {
10
+ throw new Error(`--personal cannot push path "${input}": ${reason}`);
11
+ }
12
+ /** Resolve explicit --personal paths against the same roots as a full personal push. */
13
+ export function resolveExplicitPersonalPushPaths(hqRoot, requestedPaths, cwd = process.cwd()) {
14
+ let realRoot;
15
+ try {
16
+ realRoot = fs.realpathSync.native(hqRoot);
17
+ }
18
+ catch (error) {
19
+ const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
20
+ throw new Error(`--personal cannot resolve HQ root "${hqRoot}" (${code})`);
21
+ }
22
+ const allowedRoots = computePersonalVaultPaths(realRoot)
23
+ .map((allowedPath) => {
24
+ try {
25
+ return fs.realpathSync.native(allowedPath);
26
+ }
27
+ catch (error) {
28
+ const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
29
+ throw new Error(`--personal personal-vault scope changed at "${allowedPath}" (${code})`);
30
+ }
31
+ })
32
+ .filter((allowedPath) => isWithin(realRoot, allowedPath));
33
+ const resolved = [];
34
+ for (const input of requestedPaths) {
35
+ const normalizedInput = path.resolve(cwd, input);
36
+ let realPath;
37
+ try {
38
+ realPath = fs.realpathSync.native(normalizedInput);
39
+ }
40
+ catch (error) {
41
+ const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
42
+ rejectPath(input, `path cannot be resolved (${code})`);
43
+ }
44
+ if (!isWithin(realRoot, realPath)) {
45
+ rejectPath(input, "resolved path escapes the HQ root");
46
+ }
47
+ let isDirectory;
48
+ try {
49
+ isDirectory = fs.statSync(realPath).isDirectory();
50
+ }
51
+ catch (error) {
52
+ const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
53
+ rejectPath(input, `path cannot be inspected (${code})`);
54
+ }
55
+ const relativePath = path.relative(realRoot, realPath).split(path.sep).join("/");
56
+ const exclusion = matchPersonalVaultExclusion(relativePath, isDirectory);
57
+ if (exclusion) {
58
+ rejectPath(input, `excluded by personal-vault rule ${exclusion.id}`);
59
+ }
60
+ if (!allowedRoots.some((allowedRoot) => isWithin(allowedRoot, realPath))) {
61
+ rejectPath(input, "resolved path is outside the personal-vault paths");
62
+ }
63
+ resolved.push(realPath);
64
+ }
65
+ return resolved;
66
+ }
67
+ //# sourceMappingURL=personal-targeted-push-paths.js.map