@indigoai-us/hq-cli 5.345.114 → 5.345.115

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 (54) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
  3. package/dist/commands/bot-jobs.d.ts +1 -11
  4. package/dist/commands/bot-jobs.js +13 -34
  5. package/dist/commands/core-audit-log-native.js +1 -1
  6. package/dist/commands/core-checkpoint-native.d.ts +27 -0
  7. package/dist/commands/core-checkpoint-native.js +85 -2
  8. package/dist/commands/core-dm-bind-native.js +1 -1
  9. package/dist/commands/core-git-pack-extension-check-native.js +1 -1
  10. package/dist/commands/core-job-native-utils.d.ts +2 -2
  11. package/dist/commands/core-job-native-utils.js +3 -11
  12. package/dist/commands/core-job-notify-native.js +1 -2
  13. package/dist/commands/core-job-probe-native.d.ts +2 -3
  14. package/dist/commands/core-job-probe-native.js +14 -22
  15. package/dist/commands/core-job-remediate-native.js +1 -2
  16. package/dist/commands/core-job-run-native.js +58 -54
  17. package/dist/commands/core-jobs-native.d.ts +1 -3
  18. package/dist/commands/core-jobs-native.js +16 -271
  19. package/dist/commands/core-jobs-schedule.d.ts +7 -0
  20. package/dist/commands/core-jobs-schedule.js +243 -0
  21. package/dist/commands/core-native-utilities.d.ts +2 -3
  22. package/dist/commands/core-native-utilities.js +2 -19
  23. package/dist/commands/core-pin-native.js +1 -1
  24. package/dist/commands/core-refresh-vault-access-native.js +1 -1
  25. package/dist/commands/core-resolve-company-command.js +2 -17
  26. package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
  27. package/dist/commands/core.js +1 -17
  28. package/dist/fast-scaffold.d.ts +1 -1
  29. package/dist/fast-scaffold.js +0 -38
  30. package/dist/lib/daemon/catalog.d.ts +0 -2
  31. package/dist/lib/daemon/catalog.js +3 -6
  32. package/dist/lib/daemon/run.js +0 -7
  33. package/dist/utils/resident-package-preload.js +2 -7
  34. package/package.json +1 -1
  35. package/assets/scaffold/core/scripts/audit-log.sh +0 -384
  36. package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
  37. package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
  38. package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
  39. package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
  40. package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
  41. package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
  42. package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
  43. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
  44. package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
  45. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
  46. package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
  47. package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
  48. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
  49. package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
  50. package/dist/commands/core-job-post-exit-assets.js +0 -37
  51. package/dist/commands/core-native-jobs-flag.d.ts +0 -4
  52. package/dist/commands/core-native-jobs-flag.js +0 -7
  53. package/dist/commands/core-native-utilities-flag.d.ts +0 -4
  54. package/dist/commands/core-native-utilities-flag.js +0 -7
@@ -7,14 +7,11 @@ import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { JSON_SCHEMA, load } from "js-yaml";
9
9
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
10
- import { resolveLiveRoot, resolveBundledAsset } from "../utils/hq-roots.js";
11
- import { runBundledScript } from "../utils/run-bundled-script.js";
10
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
12
11
  import { describeCoreCommand } from "./core-command-catalog.js";
13
- import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
14
12
  import { releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
15
- import { materializeCoreJobPostExitHelpers } from "./core-job-post-exit-assets.js";
16
- const RUN_ASSET = "core/scripts/hq-job-run.sh";
17
13
  const RUNNER_NAME = "hq-job-run";
14
+ const RUNNER_PATH = fileURLToPath(import.meta.url);
18
15
  export const OUTPOST_IDENTITY_FILE = "/etc/outpost/codex-identity.env";
19
16
  export const OUTPOST_RUNNER_FILE = "/usr/local/bin/outpost-runner.sh";
20
17
  const REMOTE_LAUNCHER = fileURLToPath(new URL("./core-job-remote-launcher.js", import.meta.url));
@@ -148,6 +145,15 @@ export async function spawnProcess(request) {
148
145
  if (helper === "remediate.sh" || helper === "hq-job-remediate.sh")
149
146
  appendArgvRecord(request.env.HQ_JOB_RUN_REMEDIATE_ARGV_RECORD, "hq-job-remediate.sh", helperArgs);
150
147
  }
148
+ if (request.file === "hq") {
149
+ const jobIndex = request.args.indexOf("job");
150
+ const helper = request.args[jobIndex + 1];
151
+ const helperArgs = request.args.slice(jobIndex + 2);
152
+ if (helper === "notify")
153
+ appendArgvRecord(request.env.HQ_JOB_RUN_NOTIFY_ARGV_RECORD, "hq-job-notify.sh", helperArgs);
154
+ if (helper === "remediate")
155
+ appendArgvRecord(request.env.HQ_JOB_RUN_REMEDIATE_ARGV_RECORD, "hq-job-remediate.sh", helperArgs);
156
+ }
151
157
  if (request.file === "tmux" && request.args[0] === "new-session") {
152
158
  const doneFile = request.env.HQ_JOB_RUN_REMOTE_DONE_FILE;
153
159
  const exitFile = request.env.HQ_JOB_RUN_REMOTE_EXIT_FILE;
@@ -165,7 +171,8 @@ export async function spawnProcess(request) {
165
171
  ?? '{"retry_recommended":false,"attempted":false,"notes":"stubbed"}';
166
172
  const code = nestedAgent ? Number(request.env.HQ_JOB_RUN_STUB_EXIT_CODE ?? "0") : 0;
167
173
  const helperName = path.basename(request.args[0] ?? "");
168
- const isRemediation = request.file === "bash" && (helperName === "remediate.sh" || helperName === "hq-job-remediate.sh");
174
+ const isRemediation = (request.file === "bash" && (helperName === "remediate.sh" || helperName === "hq-job-remediate.sh"))
175
+ || (request.file === "hq" && request.args.includes("job") && request.args[request.args.indexOf("job") + 1] === "remediate");
169
176
  const stdout = isRemediation ? Buffer.from(response) : Buffer.alloc(0);
170
177
  return { code: Number.isInteger(code) ? code : 0, stdout, stderr: Buffer.alloc(0) };
171
178
  }
@@ -359,7 +366,7 @@ function defaultDependencies(env) {
359
366
  }
360
367
  : (file) => acquireFlock(file, env),
361
368
  sleep: stubbed ? async () => undefined : (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
362
- scriptPath: resolveBundledAsset(RUN_ASSET),
369
+ scriptPath: RUNNER_PATH,
363
370
  };
364
371
  }
365
372
  function yqText(value, fallback = "") {
@@ -711,23 +718,25 @@ async function callNotify(jobId, jobName, outcome, summary, duration, logPath, f
711
718
  emitLog("notify skipped (HQ_JOB_RUN_NO_NOTIFY=1)");
712
719
  return;
713
720
  }
714
- try {
715
- const info = deps.stat(notifyBin);
716
- if (!(info.mode & 0o111)) {
717
- try {
718
- deps.chmod(notifyBin, info.mode | 0o111);
721
+ if (notifyBin) {
722
+ try {
723
+ const info = deps.stat(notifyBin);
724
+ if (!(info.mode & 0o111)) {
725
+ try {
726
+ deps.chmod(notifyBin, info.mode | 0o111);
727
+ }
728
+ catch { /* shell ignores chmod failure */ }
719
729
  }
720
- catch { /* shell ignores chmod failure */ }
721
730
  }
722
- }
723
- catch (error) {
724
- if (error.code === "ENOENT") {
725
- emitLog(`notify skipped — missing ${notifyBin}`);
726
- return;
731
+ catch (error) {
732
+ if (error.code === "ENOENT") {
733
+ emitLog(`notify skipped — missing ${notifyBin}`);
734
+ return;
735
+ }
736
+ throw error;
727
737
  }
728
- throw error;
729
738
  }
730
- const args = [notifyBin,
739
+ const helperArgs = [
731
740
  "--hq-root", hqRoot,
732
741
  "--job-id", jobId,
733
742
  "--job-name", jobName,
@@ -740,27 +749,37 @@ async function callNotify(jobId, jobName, outcome, summary, duration, logPath, f
740
749
  "--owner", owner,
741
750
  "--notify", notifyMode,
742
751
  ];
743
- const result = await runHelper("bash", args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot }, deps, logPath);
752
+ const command = notifyBin ? "bash" : "hq";
753
+ const args = notifyBin
754
+ ? [notifyBin, ...helperArgs]
755
+ : ["core", "--hq-root", hqRoot, "job", "notify", ...helperArgs];
756
+ const result = await runHelper(command, args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot, HQ_NO_UPDATE_CHECK: "1" }, deps, logPath);
744
757
  if (result.code !== 0)
745
758
  emitLog(`notify helper exited ${result.code} (ignored)`);
746
759
  }
747
760
  async function callRemediate(failureClass, jobId, company, hqRoot, remediateBin, logFile, deps) {
748
- try {
749
- const info = deps.stat(remediateBin);
750
- if (!(info.mode & 0o111)) {
751
- try {
752
- deps.chmod(remediateBin, info.mode | 0o111);
761
+ if (remediateBin) {
762
+ try {
763
+ const info = deps.stat(remediateBin);
764
+ if (!(info.mode & 0o111)) {
765
+ try {
766
+ deps.chmod(remediateBin, info.mode | 0o111);
767
+ }
768
+ catch { /* shell ignores chmod failure */ }
753
769
  }
754
- catch { /* shell ignores chmod failure */ }
770
+ }
771
+ catch (error) {
772
+ if (error.code === "ENOENT")
773
+ return '{"retry_recommended":false,"attempted":false,"notes":"remediate missing"}';
774
+ throw error;
755
775
  }
756
776
  }
757
- catch (error) {
758
- if (error.code === "ENOENT")
759
- return '{"retry_recommended":false,"attempted":false,"notes":"remediate missing"}';
760
- throw error;
761
- }
762
- const args = [remediateBin, "--hq-root", hqRoot, "--failure-class", failureClass, "--job-id", jobId, "--company", company];
763
- const result = await runHelper("bash", args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot, HQ_JOB_REMEDIATE_LOG: logFile }, deps);
777
+ const helperArgs = ["--hq-root", hqRoot, "--failure-class", failureClass, "--job-id", jobId, "--company", company];
778
+ const command = remediateBin ? "bash" : "hq";
779
+ const args = remediateBin
780
+ ? [remediateBin, ...helperArgs]
781
+ : ["core", "--hq-root", hqRoot, "job", "remediate", ...helperArgs];
782
+ const result = await runHelper(command, args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot, HQ_JOB_REMEDIATE_LOG: logFile, HQ_NO_UPDATE_CHECK: "1" }, deps);
764
783
  if (result.stderr.length)
765
784
  deps.appendFile(logFile, result.stderr);
766
785
  const output = result.stdout.toString("utf8").replace(/\n+$/, "");
@@ -1113,19 +1132,12 @@ export async function runJob(argv, hqRootOverride, injected) {
1113
1132
  catch (error) {
1114
1133
  return emitDie(`failed to acquire global run lock: ${lockPath}${error instanceof Error && error.message.includes("flock") ? ` (${error.message})` : ""}`);
1115
1134
  }
1116
- let postExitHelpers = null;
1117
1135
  try {
1118
1136
  const startedAt = isoNow(deps);
1119
1137
  const startedEpoch = deps.epochNow();
1120
1138
  deps.appendFile(logFile, `=== hq-job-run ${jobId} ===\nstarted_at=${startedAt}\nruntime=${runtime}\ntimeout_seconds=${timeoutSeconds}\nsurface=${surface}\njob_file=${jobFile}\nhq_root=${hqRoot}\n`);
1121
- try {
1122
- postExitHelpers = materializeCoreJobPostExitHelpers(deps.scriptPath, env);
1123
- }
1124
- catch (error) {
1125
- deps.appendFile(logFile, `post_exit_helpers_unavailable=${error instanceof Error ? error.name : "Error"}; package lease retained\n`);
1126
- }
1127
- const notifyBin = postExitHelpers?.notifyBin ?? env.HQ_JOB_NOTIFY_BIN ?? path.join(path.dirname(deps.scriptPath), "hq-job-notify.sh");
1128
- const remediateBin = postExitHelpers?.remediateBin ?? env.HQ_JOB_REMEDIATE_BIN ?? path.join(path.dirname(deps.scriptPath), "hq-job-remediate.sh");
1139
+ const notifyBin = env.HQ_JOB_NOTIFY_BIN ?? "";
1140
+ const remediateBin = env.HQ_JOB_REMEDIATE_BIN ?? "";
1129
1141
  const authStatus = readAuthStatus(runtime, env, deps.home, deps);
1130
1142
  if (authStatus !== "ok") {
1131
1143
  const reason = authStatus === "no_cli"
@@ -1147,7 +1159,7 @@ export async function runJob(argv, hqRootOverride, injected) {
1147
1159
  return emitDie(error instanceof Error ? error.message : String(error));
1148
1160
  }
1149
1161
  const secretNames = jobSecretNames(job);
1150
- let exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, postExitHelpers !== null);
1162
+ let exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, true);
1151
1163
  let failureClass = "";
1152
1164
  let nextAction = "";
1153
1165
  let retried = 0;
@@ -1169,7 +1181,7 @@ export async function runJob(argv, hqRootOverride, injected) {
1169
1181
  retried = 1;
1170
1182
  deps.appendFile(logFile, "remediate_retry=1\n");
1171
1183
  emitLog(`self-remediate recommended retry for ${failureClass} — retrying once`);
1172
- exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, postExitHelpers !== null);
1184
+ exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, true);
1173
1185
  if (exitCode !== 0) {
1174
1186
  failureClass = classifyFailure(exitCode, logFile, "", deps);
1175
1187
  deps.appendFile(logFile, `failure_class_after_retry=${failureClass}\n`);
@@ -1203,7 +1215,6 @@ export async function runJob(argv, hqRootOverride, injected) {
1203
1215
  }
1204
1216
  finally {
1205
1217
  await lease.release();
1206
- postExitHelpers?.cleanup();
1207
1218
  }
1208
1219
  }
1209
1220
  export function registerCoreJobRunCommand(job) {
@@ -1216,18 +1227,11 @@ export function registerCoreJobRunCommand(job) {
1216
1227
  .action(async (args, _options, current) => {
1217
1228
  const argv = rawArgumentsForCommand(current, args);
1218
1229
  const root = current.parent?.parent?.opts().hqRoot ?? process.env.HQ_ROOT;
1219
- if (!await coreNativeJobsEnabled()) {
1220
- const resolvedRoot = resolveLiveRoot({ hqRoot: root });
1221
- const result = runBundledScript({ asset: RUN_ASSET, args: argv, cwd: process.cwd(), hqRoot: resolvedRoot });
1222
- if (result.code !== 0)
1223
- process.exit(result.code);
1224
- return;
1225
- }
1226
1230
  const result = await runJob(argv, root);
1227
1231
  if (result !== 0)
1228
1232
  process.exit(result);
1229
1233
  });
1230
- describeCoreCommand(command, { execution: "native", asset: RUN_ASSET, root: "live" });
1234
+ describeCoreCommand(command, { execution: "native", root: "live" });
1231
1235
  }
1232
1236
  function rawArgumentsForCommand(command, fallback, raw = process.argv) {
1233
1237
  const names = [];
@@ -1,7 +1,6 @@
1
1
  /** Native TypeScript ports for the Wave 4a scheduled-jobs scaffold tools. */
2
2
  import { Command } from "commander";
3
- export declare function detectHostTimezone(override: string, resolveLocaltime?: (file: string) => string, readTimedatectl?: () => string): string;
4
- export declare function runScheduleParse(argv: string[]): number;
3
+ export { detectHostTimezone, runScheduleParse, type ScheduleParseWriters } from "./core-jobs-schedule.js";
5
4
  export declare function validateJobs(args: string[]): number;
6
5
  type ReconcileCommandResult = {
7
6
  status: number;
@@ -21,5 +20,4 @@ export declare function registerCoreJobsNativeCommand(core: Command): void;
21
20
  export declare function rawArgumentsForCommand(command: Command, fallback: string[], raw?: string[]): string[];
22
21
  /** Fast-entry adapter; the shared family flag is resolved before this module loads. */
23
22
  export declare function runJobsFast(args: readonly string[], _stdin: string, _cwd: string, _liveRoot: string | undefined, commandName: string, hqRootArg: string | undefined): Promise<number>;
24
- export {};
25
23
  //# sourceMappingURL=core-jobs-native.d.ts.map
@@ -4,248 +4,11 @@ import { spawnSync } from "node:child_process";
4
4
  import { JSON_SCHEMA, load, loadAll } from "js-yaml";
5
5
  import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
6
6
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
- import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
8
7
  import { resolveLiveRoot } from "../utils/hq-roots.js";
9
- import { runBundledScript } from "../utils/run-bundled-script.js";
10
8
  import { describeCoreCommand } from "./core-command-catalog.js";
11
- const SCHEDULE_PARSE_ASSET = "core/scripts/jobs-schedule-parse.sh";
12
- const VALIDATE_ASSET = "core/scripts/jobs-validate.sh";
13
- const RECONCILE_ASSET = "core/scripts/outpost-jobs-reconcile.sh";
14
- const SCHEDULE_PARSE_USAGE = 'usage: jobs-schedule-parse.sh [--tz IANA] "<nl or cron schedule>"\n';
9
+ import { runScheduleParse } from "./core-jobs-schedule.js";
10
+ export { detectHostTimezone, runScheduleParse } from "./core-jobs-schedule.js";
15
11
  const VALIDATE_USAGE = "usage: jobs-validate.sh <file-or-dir> [file-or-dir...]\n";
16
- const CRON_RE = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/;
17
- const DAY_NAMES = {
18
- sun: 0, sunday: 0, mon: 1, monday: 1, tue: 2, tues: 2, tuesday: 2,
19
- wed: 3, wednesday: 3, thu: 4, thur: 4, thurs: 4, thursday: 4,
20
- fri: 5, friday: 5, sat: 6, saturday: 6,
21
- };
22
- const DAY_LABELS = {
23
- "0": "Sunday", "1": "Monday", "2": "Tuesday", "3": "Wednesday",
24
- "4": "Thursday", "5": "Friday", "6": "Saturday", "1-5": "weekday",
25
- "*": "day",
26
- };
27
- function parseClock(text) {
28
- const match = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(text.trim().toLowerCase().replace(/ /g, ""));
29
- if (!match)
30
- return undefined;
31
- let hour = Number.parseInt(match[1], 10);
32
- const minute = Number.parseInt(match[2] || "0", 10);
33
- const meridiem = match[3];
34
- if (minute > 59)
35
- return undefined;
36
- if (meridiem) {
37
- if (hour < 1 || hour > 12)
38
- return undefined;
39
- hour = meridiem === "am"
40
- ? (hour === 12 ? 0 : hour)
41
- : (hour === 12 ? 12 : hour + 12);
42
- }
43
- else if (hour > 23) {
44
- return undefined;
45
- }
46
- return [hour, minute];
47
- }
48
- function formatClock(hour, minute) {
49
- const hour12 = hour % 12 || 12;
50
- return `${hour12}:${String(minute).padStart(2, "0")} ${hour < 12 ? "AM" : "PM"}`;
51
- }
52
- function humanizeCron(cron, timezone) {
53
- const fields = cron.split(/\s+/);
54
- if (fields.length !== 5)
55
- return `Cron \`${cron}\` (${timezone})`;
56
- const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
57
- if (/^\d+$/.test(minute) && /^\d+$/.test(hour) && dayOfMonth === "*"
58
- && month === "*" && (dayOfWeek === "*" || dayOfWeek === "1-5")) {
59
- const day = dayOfWeek === "1-5" ? "Every weekday" : "Every day";
60
- return `${day} at ${formatClock(+hour, +minute)} (${timezone})`;
61
- }
62
- if (/^\d+$/.test(minute) && /^\d+$/.test(hour) && dayOfMonth === "*"
63
- && month === "*" && /^\d+$/.test(dayOfWeek)) {
64
- const day = DAY_LABELS[dayOfWeek] || `day ${dayOfWeek}`;
65
- return `Every ${day} at ${formatClock(+hour, +minute)} (${timezone})`;
66
- }
67
- if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*"
68
- && month === "*" && dayOfWeek === "*") {
69
- return `Every ${minute.slice(2)} minutes (${timezone})`;
70
- }
71
- if (minute === "0" && hour.startsWith("*/") && dayOfMonth === "*"
72
- && month === "*" && dayOfWeek === "*") {
73
- return `Every ${hour.slice(2)} hours (${timezone})`;
74
- }
75
- if (minute === "0" && hour === "*" && dayOfMonth === "*"
76
- && month === "*" && dayOfWeek === "*") {
77
- return `Every hour on the hour (${timezone})`;
78
- }
79
- return `Cron \`${cron}\` (${timezone})`;
80
- }
81
- function parseNaturalLanguage(text) {
82
- const normalized = text.trim().toLowerCase();
83
- let match = /^every\s+(\d+)\s+minutes?$/.exec(normalized);
84
- if (match) {
85
- const minutes = Number(match[1]);
86
- if (minutes < 1 || minutes > 59)
87
- return [null, "minute interval must be 1..59"];
88
- return [`*/${minutes} * * * *`, `Every ${minutes} minutes`];
89
- }
90
- if (normalized === "every hour" || normalized === "hourly") {
91
- return ["0 * * * *", "Every hour on the hour"];
92
- }
93
- match = /^every\s+(\d+)\s+hours?$/.exec(normalized);
94
- if (match) {
95
- const hours = Number(match[1]);
96
- if (hours < 1 || hours > 23)
97
- return [null, "hour interval must be 1..23"];
98
- return [`0 */${hours} * * *`, `Every ${hours} hours`];
99
- }
100
- match = /^(?:every\s+)?(weekday|weekdays|day|daily)\s+(?:at\s+)?(.+)$/.exec(normalized);
101
- if (match) {
102
- const clock = parseClock(match[2]);
103
- if (!clock)
104
- return [null, `could not parse time: ${match[2]}`];
105
- const [hour, minute] = clock;
106
- const weekdays = match[1] === "weekday" || match[1] === "weekdays";
107
- return [
108
- `${minute} ${hour} * * ${weekdays ? "1-5" : "*"}`,
109
- `${weekdays ? "Every weekday" : "Every day"} at ${formatClock(hour, minute)}`,
110
- ];
111
- }
112
- match = /^(?:every\s+)?(sun(?:day)?|mon(?:day)?|tue(?:s|sday)?|wed(?:nesday)?|thu(?:r|rs|rsday)?|fri(?:day)?|sat(?:urday)?)s?\s+(?:at\s+)?(.+)$/.exec(normalized);
113
- if (match) {
114
- const day = DAY_NAMES[match[1]];
115
- const clock = parseClock(match[2]);
116
- if (!clock)
117
- return [null, `could not parse time: ${match[2]}`];
118
- return [
119
- `${clock[1]} ${clock[0]} * * ${day}`,
120
- `Every ${DAY_LABELS[String(day)]} at ${formatClock(...clock)}`,
121
- ];
122
- }
123
- match = /^at\s+(.+?)\s+every\s+(weekday|weekdays|day|daily)$/.exec(normalized);
124
- if (match) {
125
- const clock = parseClock(match[1]);
126
- if (!clock)
127
- return [null, `could not parse time: ${match[1]}`];
128
- const weekdays = match[2] === "weekday" || match[2] === "weekdays";
129
- return [
130
- `${clock[1]} ${clock[0]} * * ${weekdays ? "1-5" : "*"}`,
131
- `${weekdays ? "Every weekday" : "Every day"} at ${formatClock(...clock)}`,
132
- ];
133
- }
134
- return [
135
- null,
136
- "unrecognized schedule — try 'every weekday at 9am', 'every day at 14:30', "
137
- + "'every monday at 9am', a 5-field cron, or 'every 15 minutes'",
138
- ];
139
- }
140
- function isValidTimezone(timezone) {
141
- const wellFormed = /^(UTC|[A-Za-z0-9_+-]+(\/[A-Za-z0-9_+-]+)+)$/;
142
- if (!wellFormed.test(timezone))
143
- return false;
144
- try {
145
- new Intl.DateTimeFormat(undefined, { timeZone: timezone });
146
- return true;
147
- }
148
- catch {
149
- // Hosts without tzdata accept the same well-formed names as the shell.
150
- return wellFormed.test(timezone);
151
- }
152
- }
153
- export function detectHostTimezone(override, resolveLocaltime = fs.realpathSync, readTimedatectl = () => {
154
- const result = spawnSync("timedatectl", ["show", "-p", "Timezone", "--value"], { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] });
155
- if (result.error || result.status !== 0)
156
- return "";
157
- return result.stdout || "";
158
- }) {
159
- if (override)
160
- return override;
161
- const environmentTimezone = (process.env.TZ || "").trim();
162
- if (environmentTimezone && environmentTimezone.includes("/"))
163
- return environmentTimezone;
164
- try {
165
- const realpath = resolveLocaltime("/etc/localtime");
166
- const parts = realpath.split(path.sep).filter(Boolean);
167
- const zoneinfo = parts.indexOf("zoneinfo");
168
- if (zoneinfo >= 0 && parts.slice(zoneinfo + 1).length) {
169
- return parts.slice(zoneinfo + 1).join("/");
170
- }
171
- }
172
- catch {
173
- // Ignore an unavailable local timezone link.
174
- }
175
- try {
176
- const timezone = readTimedatectl().trim();
177
- if (timezone && timezone.includes("/"))
178
- return timezone;
179
- }
180
- catch {
181
- // Ignore unavailable timedatectl.
182
- }
183
- return "UTC";
184
- }
185
- export function runScheduleParse(argv) {
186
- let timezoneOverride = "";
187
- const args = [...argv];
188
- while (args.length) {
189
- const argument = args[0];
190
- if (argument === "--tz") {
191
- if (args.length < 2) {
192
- process.stderr.write(SCHEDULE_PARSE_USAGE);
193
- return 2;
194
- }
195
- timezoneOverride = args[1];
196
- args.splice(0, 2);
197
- }
198
- else if (argument === "--json") {
199
- args.shift();
200
- }
201
- else if (argument === "-h" || argument === "--help") {
202
- process.stderr.write(SCHEDULE_PARSE_USAGE);
203
- return 2;
204
- }
205
- else if (argument === "--") {
206
- args.shift();
207
- break;
208
- }
209
- else if (argument.startsWith("-")) {
210
- process.stderr.write(`jobs-schedule-parse: unknown flag: ${argument}\n${SCHEDULE_PARSE_USAGE}`);
211
- return 2;
212
- }
213
- else {
214
- break;
215
- }
216
- }
217
- if (!args.length) {
218
- process.stderr.write(SCHEDULE_PARSE_USAGE);
219
- return 2;
220
- }
221
- const input = args.join(" ").trim();
222
- if (!input) {
223
- process.stdout.write('{"ok":false,"error":"empty schedule"}\n');
224
- return 1;
225
- }
226
- const timezone = detectHostTimezone(timezoneOverride);
227
- if (!isValidTimezone(timezone)) {
228
- process.stdout.write(`${JSON.stringify({ ok: false, error: `invalid IANA timezone: ${timezone}` })}\n`);
229
- return 1;
230
- }
231
- if (CRON_RE.test(input) && !/[A-Za-z]/.test(input)) {
232
- process.stdout.write(`${JSON.stringify({ ok: true, cron: input, timezone, human: humanizeCron(input, timezone), source: "cron" })}\n`);
233
- return 0;
234
- }
235
- const namedCron = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(mon-fri|monday-friday)$/i.exec(input);
236
- if (namedCron) {
237
- const cron = `${namedCron[1]} ${namedCron[2]} ${namedCron[3]} ${namedCron[4]} 1-5`;
238
- process.stdout.write(`${JSON.stringify({ ok: true, cron, timezone, human: humanizeCron(cron, timezone), source: "cron" })}\n`);
239
- return 0;
240
- }
241
- const [cron, message] = parseNaturalLanguage(input);
242
- if (cron === null) {
243
- process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
244
- return 1;
245
- }
246
- process.stdout.write(`${JSON.stringify({ ok: true, cron, timezone, human: `${message} (${timezone})`, source: "nl" })}\n`);
247
- return 0;
248
- }
249
12
  function isValidCronField(value, minimum, maximum) {
250
13
  return value.split(",").every((part) => {
251
14
  if (part === "*")
@@ -971,9 +734,10 @@ function servicePathValue() {
971
734
  function systemdQuote(value) {
972
735
  return `"${value.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
973
736
  }
974
- function serviceUnit(root, runner, id) {
737
+ function serviceUnit(root, id) {
975
738
  const pathValue = systemdQuote(`PATH=${servicePathValue()}`);
976
- return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${shellDisplayPath(runner)} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
739
+ const runner = path.resolve(root, "core/scripts/hq-job-run.sh");
740
+ return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${runner} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
977
741
  }
978
742
  function timerUnit(id, onCalendar, randomize, root) {
979
743
  return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job timer: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\n\n[Timer]\nOnCalendar=${onCalendar}\nPersistent=true\nRandomizedDelaySec=${randomize}\nUnit=hq-job-${id}.service\n\n[Install]\nWantedBy=timers.target\n`;
@@ -1010,9 +774,8 @@ function installSyncHook(root) {
1010
774
  "# hq-sync-runner executes personal/hooks/Stop/* after each sync cycle.",
1011
775
  "set -u",
1012
776
  'HQ_ROOT="${OUTPOST_HQ_ROOT:-${HQ_ROOT:-/home/ec2-user/hq}}"',
1013
- 'RECONCILE="$HQ_ROOT/core/scripts/outpost-jobs-reconcile.sh"',
1014
- 'if [ -x "$RECONCILE" ] || [ -f "$RECONCILE" ]; then',
1015
- ' bash "$RECONCILE" --hq-root "$HQ_ROOT" --after-sync >>"${HOME}/.hq/jobs/reconcile/after-sync.log" 2>&1 || true',
777
+ 'if command -v hq >/dev/null 2>&1; then',
778
+ ' HQ_NO_UPDATE_CHECK=1 hq core --hq-root "$HQ_ROOT" jobs reconcile --after-sync >>"${HOME}/.hq/jobs/reconcile/after-sync.log" 2>&1 || true',
1016
779
  "fi",
1017
780
  "",
1018
781
  ].join("\n");
@@ -1087,15 +850,8 @@ export async function runReconcile(argv, rootArg, options = {}) {
1087
850
  }
1088
851
  if (!root)
1089
852
  root = resolveLiveRoot();
1090
- const validator = path.join(root, "core", "scripts", "jobs-validate.sh");
1091
- const runner = path.join(root, "core", "scripts", "hq-job-run.sh");
1092
- const probe = path.join(root, "core", "scripts", "hq-job-probe.sh");
1093
- if (!fs.existsSync(validator))
1094
- return reconcileDie("missing jobs-validate.sh");
1095
- if (!fs.existsSync(runner))
1096
- return reconcileDie("missing hq-job-run.sh");
1097
- if (!fs.existsSync(probe))
1098
- return reconcileDie("missing hq-job-probe.sh");
853
+ const servicePath = servicePathValue();
854
+ const hq = findExecutable("hq", servicePath) || "hq";
1099
855
  const home = process.env.HOME ?? "";
1100
856
  const unitDir = process.env.HQ_JOB_UNIT_DIR || path.join(home, ".config", "systemd", "user");
1101
857
  const cacheDir = process.env.HQ_JOB_STATUS_CACHE_DIR || path.join(home, ".hq", "jobs", "status-cache");
@@ -1131,7 +887,7 @@ export async function runReconcile(argv, rootArg, options = {}) {
1131
887
  if (readiness === "ready")
1132
888
  continue;
1133
889
  log(`probe ${id} (cached readiness=${readiness})`);
1134
- const result = commandRunner("bash", [probe, "--hq-root", root, file]);
890
+ const result = commandRunner(hq, ["core", "--hq-root", root, "job", "probe", "--hq-root", root, file]);
1135
891
  probed += 1;
1136
892
  const probeReadiness = probeValue(result.stdout, "readiness");
1137
893
  if (probeReadiness) {
@@ -1209,7 +965,7 @@ export async function runReconcile(argv, rootArg, options = {}) {
1209
965
  if (timezone && !` ${onCalendar} `.includes(` ${timezone} `))
1210
966
  onCalendar = `${onCalendar} ${timezone}`;
1211
967
  desired.add(id);
1212
- const service = serviceUnit(root, runner, id);
968
+ const service = serviceUnit(root, id);
1213
969
  const timer = timerUnit(id, onCalendar, randomize, root);
1214
970
  const unchanged = fs.existsSync(servicePath) && fs.existsSync(timerPath)
1215
971
  && fs.readFileSync(servicePath, "utf8") === service
@@ -1282,7 +1038,7 @@ export async function runReconcile(argv, rootArg, options = {}) {
1282
1038
  }
1283
1039
  export function registerCoreJobsNativeCommand(core) {
1284
1040
  const jobs = core.command("jobs").description("Scheduled jobs helpers");
1285
- const register = (name, asset, summary, run) => {
1041
+ const register = (name, summary, run) => {
1286
1042
  const command = jobs.command(name)
1287
1043
  .description(summary)
1288
1044
  .helpOption(false)
@@ -1292,26 +1048,15 @@ export function registerCoreJobsNativeCommand(core) {
1292
1048
  .action(async (args, _options, current) => {
1293
1049
  const argv = rawArgumentsForCommand(current, args);
1294
1050
  const groupHqRoot = core.opts().hqRoot;
1295
- const { coreNativeJobsEnabled } = await import("./core-native-jobs-flag.js");
1296
- if (!await coreNativeJobsEnabled()) {
1297
- const hqRoot = asset === RECONCILE_ASSET && !argv.includes("--hq-root")
1298
- && !argv.includes("--help") && !argv.includes("-h")
1299
- ? resolveLiveRoot({ hqRoot: groupHqRoot })
1300
- : undefined;
1301
- const result = runBundledScript({ asset, args: argv, cwd: process.cwd(), ...(hqRoot ? { hqRoot } : {}) });
1302
- if (result.code !== 0)
1303
- process.exit(result.code);
1304
- return;
1305
- }
1306
1051
  const result = await run(argv, groupHqRoot ?? process.env.HQ_ROOT);
1307
1052
  if (result !== 0)
1308
1053
  process.exit(result);
1309
1054
  });
1310
- describeCoreCommand(command, { execution: "native", asset, root: "live" });
1055
+ describeCoreCommand(command, { execution: "native", root: "live" });
1311
1056
  };
1312
- register("schedule-parse", SCHEDULE_PARSE_ASSET, "Resolve natural-language or raw cron schedules", runScheduleParse);
1313
- register("validate", VALIDATE_ASSET, "Validate scheduled-job YAML files", validateJobs);
1314
- register("reconcile", RECONCILE_ASSET, "Materialize scheduled jobs on this outpost", runReconcile);
1057
+ register("schedule-parse", "Resolve natural-language or raw cron schedules", (args) => runScheduleParse(args));
1058
+ register("validate", "Validate scheduled-job YAML files", validateJobs);
1059
+ register("reconcile", "Materialize scheduled jobs on this outpost", runReconcile);
1315
1060
  }
1316
1061
  export function rawArgumentsForCommand(command, fallback, raw = process.argv) {
1317
1062
  const names = [];
@@ -0,0 +1,7 @@
1
+ export type ScheduleParseWriters = {
2
+ stdout: (value: string) => void;
3
+ stderr: (value: string) => void;
4
+ };
5
+ export declare function detectHostTimezone(override: string, resolveLocaltime?: (file: string) => string, readTimedatectl?: () => string): string;
6
+ export declare function runScheduleParse(argv: string[], writers?: ScheduleParseWriters): number;
7
+ //# sourceMappingURL=core-jobs-schedule.d.ts.map