@indigoai-us/hq-cli 5.290.0 → 5.292.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 (83) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
  5. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  9. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  10. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  11. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  12. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  13. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  14. package/assets/scaffold/core/scripts/hq-session.sh +182 -21
  15. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
  16. package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
  17. package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
  18. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
  19. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  20. package/dist/command-catalog.generated.d.ts +165 -1
  21. package/dist/command-catalog.generated.js +205 -1
  22. package/dist/commands/cloud.d.ts +26 -14
  23. package/dist/commands/cloud.js +207 -56
  24. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  25. package/dist/commands/core-delegate-w1b.js +1049 -0
  26. package/dist/commands/core-delegate.d.ts +28 -0
  27. package/dist/commands/core-delegate.js +15 -13
  28. package/dist/commands/core-handoff.js +152 -23
  29. package/dist/commands/core-jobs-native.d.ts +23 -0
  30. package/dist/commands/core-jobs-native.js +1331 -0
  31. package/dist/commands/core-native-jobs-flag.d.ts +4 -0
  32. package/dist/commands/core-native-jobs-flag.js +7 -0
  33. package/dist/commands/core-ontology.d.ts +22 -4
  34. package/dist/commands/core-ontology.js +90 -8
  35. package/dist/commands/core-policy.js +24 -5
  36. package/dist/commands/core.js +17 -0
  37. package/dist/commands/integrations-core.js +7 -0
  38. package/dist/commands/lanes.js +111 -35
  39. package/dist/commands/members.js +5 -0
  40. package/dist/commands/mesh.js +46 -25
  41. package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
  42. package/dist/commands/personal-targeted-push-flag.js +7 -0
  43. package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
  44. package/dist/commands/personal-targeted-push-paths.js +67 -0
  45. package/dist/commands/secrets.js +9 -0
  46. package/dist/commands/skill.d.ts +3 -0
  47. package/dist/commands/skill.js +28 -0
  48. package/dist/fast-core.js +1 -0
  49. package/dist/lib/billing/free-month.d.ts +18 -0
  50. package/dist/lib/billing/free-month.js +31 -0
  51. package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
  52. package/dist/lib/billing/plan-limit-refusals.js +187 -0
  53. package/dist/lib/billing/plan-lock.d.ts +2 -6
  54. package/dist/lib/billing/plan-lock.js +5 -16
  55. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  56. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  57. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  58. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  59. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  60. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  61. package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
  62. package/dist/lib/core-utils/secret-patterns.js +3 -2
  63. package/dist/lib/daemon/flag.d.ts +2 -0
  64. package/dist/lib/daemon/flag.js +2 -0
  65. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  66. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  67. package/dist/lib/lanes/autopilot.d.ts +69 -0
  68. package/dist/lib/lanes/autopilot.js +418 -0
  69. package/dist/lib/lanes/mesh-events.js +2 -0
  70. package/dist/lib/lanes/store.js +11 -6
  71. package/dist/lib/mesh/api.js +4 -0
  72. package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
  73. package/dist/lib/mesh/live/daemon/state.js +34 -2
  74. package/dist/main.js +23 -12
  75. package/dist/utils/plan-gate-error.d.ts +24 -0
  76. package/dist/utils/plan-gate-error.js +219 -9
  77. package/dist/utils/scaffold-sibling-references.js +4 -4
  78. package/dist/utils/self-update.d.ts +6 -0
  79. package/dist/utils/self-update.js +28 -6
  80. package/dist/utils/staged-install.d.ts +34 -0
  81. package/dist/utils/staged-install.js +296 -0
  82. package/package.json +2 -2
  83. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -0,0 +1,4 @@
1
+ /** Default-off rollout gate for the native scheduled-jobs helpers. */
2
+ export declare const CORE_NATIVE_JOBS_FLAG = "core-native-jobs";
3
+ export declare function coreNativeJobsEnabled(): Promise<boolean>;
4
+ //# sourceMappingURL=core-native-jobs-flag.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Default-off rollout gate for the native scheduled-jobs helpers. */
2
+ export const CORE_NATIVE_JOBS_FLAG = "core-native-jobs";
3
+ export async function coreNativeJobsEnabled() {
4
+ const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
+ return resolveProcessFlagGate(CORE_NATIVE_JOBS_FLAG, { fallback: false }, () => false);
6
+ }
7
+ //# sourceMappingURL=core-native-jobs-flag.js.map
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { Command } from "commander";
11
11
  import { type FlagReader } from "../lib/checkpoint-ontology/plan.js";
12
- import { type DeleteVaultKey, type ListedPair } from "../lib/checkpoint-ontology/merge-copies.js";
12
+ import { type AliasDrop } from "../lib/checkpoint-ontology/drop-aliases.js";
13
+ import { type DeleteVaultKey, type ListedPair, type MergeCopiesReport } from "../lib/checkpoint-ontology/merge-copies.js";
13
14
  export declare const ONTOLOGY_PAYLOAD_FILE = "payload.json";
14
15
  /** The hq-flags read, scoped to the payload company. */
15
16
  export declare const registryFlagReader: FlagReader;
@@ -31,12 +32,15 @@ export interface MergeCopiesCommandOptions {
31
32
  apply?: boolean;
32
33
  delete?: boolean;
33
34
  variants?: boolean;
35
+ /** Merge exactly the `--only` pairs, whatever their names (flag merge-listed-pairs). */
36
+ listed?: boolean;
34
37
  limit?: number;
35
38
  only?: string;
36
39
  /** Also re-point refs and links held today (flag merge-rewrite-refs). */
37
40
  rewriteRefs?: boolean;
38
41
  json?: boolean;
39
42
  }
43
+ export declare function mergeCopiesSummary(report: MergeCopiesReport): string;
40
44
  /** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
41
45
  export declare function readListedPairs(file: string): ListedPair[];
42
46
  /**
@@ -47,10 +51,24 @@ export declare function readListedPairs(file: string): ListedPair[];
47
51
  * With `--variants`, merges two-file duplicate groups whose names differ only
48
52
  * in case, spacing or punctuation instead, gated by
49
53
  * `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
50
- * `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
51
- * invoice and bill links, source and knowledge links, and carries external
52
- * ids and web domains.
54
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points invoice and
55
+ * knowledge links to the survivor and carries external ids and web domains;
56
+ * pairs named by signals or sources stay held. `--listed` (flag
57
+ * `checkpoint.ontology.merge-listed-pairs`) merges exactly the `--only` pairs,
58
+ * which a person reviewed, instead of the pairs a finder produces.
53
59
  */
54
60
  export declare function runOntologyMergeCopies(hqRoot: string, slug: string | undefined, options: MergeCopiesCommandOptions, deps?: OntologyCommandDeps): Promise<string>;
61
+ /** Reads a `drop-aliases --only` file: a JSON array of `{ entity, alias }`. */
62
+ export declare function readAliasDrops(file: string): AliasDrop[];
63
+ /**
64
+ * `hq core ontology drop-aliases`: remove the reviewed `{ entity, alias }`
65
+ * pairs in `--only` from entity files (`--apply`); without it only reports.
66
+ * Gated by `checkpoint.ontology.drop-aliases` and `checkpoint.ontology.multi-writer`.
67
+ */
68
+ export declare function runOntologyDropAliases(hqRoot: string, slug: string | undefined, options: {
69
+ only?: string;
70
+ apply?: boolean;
71
+ json?: boolean;
72
+ }, deps?: OntologyCommandDeps): Promise<string>;
55
73
  export declare function registerCoreOntologyCommand(core: Command): void;
56
74
  //# sourceMappingURL=core-ontology.d.ts.map
@@ -17,7 +17,8 @@ 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_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
20
+ import { DROP_ALIASES_FLAG, dropAliases } from "../lib/checkpoint-ontology/drop-aliases.js";
21
+ import { MERGE_COPIES_FLAG, MERGE_LISTED_PAIRS_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
21
22
  import { callDeleteEndpoint } from "./files.js";
22
23
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
23
24
  import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
@@ -142,7 +143,7 @@ async function companyVaultDeleter(companyUid) {
142
143
  return (await callDeleteEndpoint({ token, companyUid, prefix: key, dryRun: false })).deleted;
143
144
  };
144
145
  }
145
- function mergeCopiesSummary(report) {
146
+ export function mergeCopiesSummary(report) {
146
147
  const held = report.plans.filter((plan) => plan.held).length;
147
148
  const lines = [
148
149
  `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`,
@@ -150,7 +151,7 @@ function mergeCopiesSummary(report) {
150
151
  ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}; broken entity links ${report.broken_links_before} -> ${report.broken_links_after}`,
151
152
  ];
152
153
  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
+ lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" || plan.held === "server_owned_refs" ? ` (${plan.detail})` : ""}`);
154
155
  }
155
156
  for (const error of report.errors)
156
157
  lines.push(` error ${error.path}: ${error.error}`);
@@ -216,9 +217,11 @@ function readResolution(copy, raw) {
216
217
  * With `--variants`, merges two-file duplicate groups whose names differ only
217
218
  * in case, spacing or punctuation instead, gated by
218
219
  * `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.
220
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points invoice and
221
+ * knowledge links to the survivor and carries external ids and web domains;
222
+ * pairs named by signals or sources stay held. `--listed` (flag
223
+ * `checkpoint.ontology.merge-listed-pairs`) merges exactly the `--only` pairs,
224
+ * which a person reviewed, instead of the pairs a finder produces.
222
225
  */
223
226
  export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
224
227
  if (options.apply && options.delete) {
@@ -227,13 +230,20 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
227
230
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1)) {
228
231
  throw new OntologyUsageError("ontology: --limit must be a positive whole number");
229
232
  }
233
+ if (options.listed && options.variants) {
234
+ throw new OntologyUsageError("ontology: use either --listed or --variants");
235
+ }
236
+ if (options.listed && options.only === undefined) {
237
+ throw new OntologyUsageError("ontology: --listed needs --only <file>");
238
+ }
230
239
  const only = options.only !== undefined ? readListedPairs(options.only) : undefined;
231
240
  const resolution = resolveOntologyCompany(hqRoot, slug);
232
241
  if (!resolution.ok)
233
242
  throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
234
243
  const company = resolution.company;
235
244
  const readFlag = deps.readFlag ?? registryFlagReader;
236
- for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
245
+ const kindFlag = options.listed ? MERGE_LISTED_PAIRS_FLAG : options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG;
246
+ for (const flag of [kindFlag, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
237
247
  if (!(await readFlag(flag, company))) {
238
248
  return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology merge-copies: skipped (${flag} is off)`;
239
249
  }
@@ -251,6 +261,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
251
261
  mode,
252
262
  now: deps.now?.() ?? new Date(),
253
263
  ...(options.variants ? { pairKind: "variants" } : {}),
264
+ ...(options.listed ? { pairKind: "listed" } : {}),
254
265
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
255
266
  ...(only ? { only } : {}),
256
267
  ...(options.rewriteRefs ? { rewriteRefs: true } : {}),
@@ -258,6 +269,59 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
258
269
  });
259
270
  return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
260
271
  }
272
+ /** Reads a `drop-aliases --only` file: a JSON array of `{ entity, alias }`. */
273
+ export function readAliasDrops(file) {
274
+ let parsed;
275
+ try {
276
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
277
+ }
278
+ catch (error) {
279
+ throw new OntologyUsageError(`ontology: --only file ${file} is not readable JSON: ${error.message}`);
280
+ }
281
+ const usage = "ontology: --only must be a non-empty JSON array of { entity, alias } strings";
282
+ if (!Array.isArray(parsed) || parsed.length === 0)
283
+ throw new OntologyUsageError(usage);
284
+ return parsed.map((item) => {
285
+ const record = item;
286
+ if (!record || typeof record.entity !== "string" || typeof record.alias !== "string" || record.alias === "") {
287
+ throw new OntologyUsageError(usage);
288
+ }
289
+ return { entity: record.entity, alias: record.alias };
290
+ });
291
+ }
292
+ /**
293
+ * `hq core ontology drop-aliases`: remove the reviewed `{ entity, alias }`
294
+ * pairs in `--only` from entity files (`--apply`); without it only reports.
295
+ * Gated by `checkpoint.ontology.drop-aliases` and `checkpoint.ontology.multi-writer`.
296
+ */
297
+ export async function runOntologyDropAliases(hqRoot, slug, options, deps = {}) {
298
+ if (options.only === undefined)
299
+ throw new OntologyUsageError("ontology: drop-aliases needs --only <file>");
300
+ const drops = readAliasDrops(options.only);
301
+ const resolution = resolveOntologyCompany(hqRoot, slug);
302
+ if (!resolution.ok)
303
+ throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
304
+ const company = resolution.company;
305
+ const readFlag = deps.readFlag ?? registryFlagReader;
306
+ for (const flag of [DROP_ALIASES_FLAG, MULTI_WRITER_FLAG]) {
307
+ if (!(await readFlag(flag, company))) {
308
+ return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology drop-aliases: skipped (${flag} is off)`;
309
+ }
310
+ }
311
+ const report = dropAliases({ hqRoot, company, mode: options.apply ? "apply" : "dry_run", drops, now: deps.now?.() ?? new Date() });
312
+ if (options.json)
313
+ return JSON.stringify(report, null, 2);
314
+ const planned = Object.values(report.planned).reduce((sum, list) => sum + list.length, 0);
315
+ const lines = [
316
+ `ontology drop-aliases (${report.mode.replace("_", " ")}) in ${report.company}: ${planned} alias(es) on ${Object.keys(report.planned).length} entities, written ${report.written.length}, skipped ${report.skipped.length}, errors ${report.errors.length}`,
317
+ ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}`,
318
+ ];
319
+ for (const skip of report.skipped)
320
+ lines.push(` skipped ${skip.entity} "${skip.alias}": ${skip.reason}${skip.detail ? ` (${skip.detail})` : ""}`);
321
+ for (const error of report.errors)
322
+ lines.push(` error ${error.path}: ${error.error}`);
323
+ return lines.join("\n");
324
+ }
261
325
  function fail(error) {
262
326
  if (error instanceof OntologyUsageError || error instanceof ApplyError || error instanceof WriteBoundsError) {
263
327
  process.stderr.write(`${error.message}\n`);
@@ -306,9 +370,10 @@ export function registerCoreOntologyCommand(core) {
306
370
  .option("--apply", "merge each copy into its original and re-point links (writes entity files)")
307
371
  .option("--delete", "delete fully merged copies from the vault by exact key (after sync pushed the merge)")
308
372
  .option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
373
+ .option("--listed", "merge exactly the pairs in the --only file, whatever their names (checkpoint.ontology.merge-listed-pairs)")
309
374
  .option("--limit <n>", "handle at most this many copies", (value) => Number(value))
310
375
  .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)")
376
+ .option("--rewrite-refs", "also re-point invoice and knowledge links to the survivor and carry external ids and web domains; pairs named by signals or sources stay held (checkpoint.ontology.merge-rewrite-refs)")
312
377
  .option("--json", "print JSON")
313
378
  .option("--hq-root <path>", "HQ installation to operate on")
314
379
  .action(async (options) => {
@@ -320,6 +385,23 @@ export function registerCoreOntologyCommand(core) {
320
385
  }
321
386
  });
322
387
  describeCoreCommand(mergeCopies, { execution: "native", root: "live" });
388
+ const dropAliasesCommand = ontology
389
+ .command("drop-aliases")
390
+ .description("Remove reviewed wrong aliases from entity files (dry run by default)")
391
+ .requiredOption("--company <slug>", "company to change")
392
+ .option("--only <file>", "JSON array of { entity, alias } to remove; entity is a company-relative path")
393
+ .option("--apply", "write the entity files")
394
+ .option("--json", "print JSON")
395
+ .option("--hq-root <path>", "HQ installation to operate on")
396
+ .action(async (options) => {
397
+ try {
398
+ process.stdout.write(`${await runOntologyDropAliases(rootFor(options), options.company, options)}\n`);
399
+ }
400
+ catch (error) {
401
+ fail(error);
402
+ }
403
+ });
404
+ describeCoreCommand(dropAliasesCommand, { execution: "native", root: "live" });
323
405
  const apply = ontology
324
406
  .command("apply")
325
407
  .description("Validate the run's entity candidates and write them to the payload company")
@@ -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
@@ -35,6 +35,7 @@ import { registerCoreHandoffCommand } from "./core-handoff.js";
35
35
  import { registerCoreOntologyCommand } from "./core-ontology.js";
36
36
  import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
37
37
  import { registerCorePolicyCommand } from "./core-policy.js";
38
+ import { registerCoreJobsNativeCommand } from "./core-jobs-native.js";
38
39
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
39
40
  import { resolveLiveRoot } from "../utils/hq-roots.js";
40
41
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
@@ -211,6 +212,13 @@ export const SCAFFOLD_COMMANDS = [
211
212
  * direct `hq core` command.
212
213
  */
213
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" },
217
+ { asset: "core/scripts/outpost-jobs-reconcile.sh", root: "live", summary: "Rollback companion for native jobs reconcile" },
218
+ { asset: "core/scripts/hq-job-probe.sh", root: "live", summary: "Scheduled jobs readiness probe used by the reconcile rollback path" },
219
+ { asset: "core/scripts/hq-job-run.sh", root: "live", summary: "Scheduled jobs runner used by generated systemd units" },
220
+ { asset: "core/scripts/hq-job-notify.sh", root: "live", summary: "Scheduled jobs failure notification helper" },
221
+ { asset: "core/scripts/hq-job-remediate.sh", root: "live", summary: "Scheduled jobs bounded remediation helper" },
214
222
  // Shared shell libs sourced by the two relocated scripts (and hooks). Never
215
223
  // dispatched on their own (so no `hq core` command), but they MUST ship in the
216
224
  // tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
@@ -232,6 +240,11 @@ export const SCAFFOLD_ONLY_ASSETS = [
232
240
  { asset: "core/scripts/hq-delegate-repo.sh", root: "live", summary: "Rollback shell for native delegate repo" },
233
241
  { asset: "core/scripts/hq-delegate-secrets.sh", root: "live", summary: "Rollback shell for native delegate secrets" },
234
242
  { asset: "core/scripts/hq-delegate-transfer.sh", root: "live", summary: "Rollback shell for native delegate transfer" },
243
+ { asset: "core/scripts/hq-delegate-bundle.sh", root: "live", summary: "Rollback shell for native delegate bundle" },
244
+ { asset: "core/scripts/hq-delegate-grant.sh", root: "live", summary: "Rollback shell for native delegate grant" },
245
+ { asset: "core/scripts/hq-delegate-publish.sh", root: "live", summary: "Rollback shell for native delegate publish" },
246
+ { asset: "core/scripts/hq-delegate-send.sh", root: "live", summary: "Rollback shell for native delegate send" },
247
+ { asset: "core/scripts/hq-delegate-verify.sh", root: "live", summary: "Rollback shell for native delegate verify" },
235
248
  { asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
236
249
  // Retained rollback companions for the gated native ontology commands. These
237
250
  // are loose HQ-tree assets, but `hq core ontology` dispatches natively.
@@ -308,6 +321,9 @@ function runEntry(entry, scope, args) {
308
321
  args,
309
322
  cwd,
310
323
  hqRoot,
324
+ env: entry.asset === "core/scripts/hq-session.sh"
325
+ ? { HQ_HQ_SESSION_NO_CLI: "1" }
326
+ : undefined,
311
327
  });
312
328
  // Propagate verbatim. Several of these scripts use their exit code as their
313
329
  // interface (drift detected, lint failed), so remapping it would break the
@@ -335,6 +351,7 @@ export function registerCoreCommands(program) {
335
351
  registerCoreHandoffCommand(core);
336
352
  registerCoreKnowledgePrefsCommand(core);
337
353
  registerCorePolicyCommand(core);
354
+ registerCoreJobsNativeCommand(core);
338
355
  // Wave 2 handoff/worktree commands retain their byte-identical shell assets
339
356
  // as a default-off rollback path for one release. Keep the gate helper small
340
357
  // 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