@indigoai-us/hq-cli 5.345.113 → 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.
- package/CHANGELOG.md +24 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
- package/dist/commands/bot-jobs.d.ts +1 -11
- package/dist/commands/bot-jobs.js +13 -34
- package/dist/commands/capacity.js +2 -0
- package/dist/commands/core-audit-log-native.js +1 -1
- package/dist/commands/core-checkpoint-native.d.ts +27 -0
- package/dist/commands/core-checkpoint-native.js +85 -2
- package/dist/commands/core-dm-bind-native.js +1 -1
- package/dist/commands/core-git-pack-extension-check-native.js +1 -1
- package/dist/commands/core-job-native-utils.d.ts +2 -2
- package/dist/commands/core-job-native-utils.js +3 -11
- package/dist/commands/core-job-notify-native.js +1 -2
- package/dist/commands/core-job-probe-native.d.ts +2 -3
- package/dist/commands/core-job-probe-native.js +14 -22
- package/dist/commands/core-job-remediate-native.js +1 -2
- package/dist/commands/core-job-run-native.js +58 -54
- package/dist/commands/core-jobs-native.d.ts +1 -3
- package/dist/commands/core-jobs-native.js +16 -271
- package/dist/commands/core-jobs-schedule.d.ts +7 -0
- package/dist/commands/core-jobs-schedule.js +243 -0
- package/dist/commands/core-native-utilities.d.ts +2 -3
- package/dist/commands/core-native-utilities.js +2 -19
- package/dist/commands/core-pin-native.js +1 -1
- package/dist/commands/core-refresh-vault-access-native.js +1 -1
- package/dist/commands/core-resolve-company-command.js +2 -17
- package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
- package/dist/commands/core.js +1 -17
- package/dist/commands/lanes.js +1 -0
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +0 -38
- package/dist/lib/capacity/lane-login.js +73 -6
- package/dist/lib/capacity/route.d.ts +1 -1
- package/dist/lib/capacity/route.js +5 -1
- package/dist/lib/capacity/sign-in.d.ts +12 -0
- package/dist/lib/capacity/sign-in.js +65 -16
- package/dist/lib/daemon/catalog.d.ts +0 -2
- package/dist/lib/daemon/catalog.js +3 -6
- package/dist/lib/daemon/run.js +0 -7
- package/dist/utils/resident-package-preload.js +2 -7
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/audit-log.sh +0 -384
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
- package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
- package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
- package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
- package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
- package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
- package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
- package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
- package/dist/commands/core-job-post-exit-assets.js +0 -37
- package/dist/commands/core-native-jobs-flag.d.ts +0 -4
- package/dist/commands/core-native-jobs-flag.js +0 -7
- package/dist/commands/core-native-utilities-flag.d.ts +0 -4
- package/dist/commands/core-native-utilities-flag.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.115] — 2026-10-13
|
|
6
|
+
- The bundled delegate-send shell now caches the Git Bash `jq --binary` capability probe for one invocation instead of starting `jq.exe` before every jq operation.
|
|
7
|
+
- Remove the bundled scheduled jobs shell copies and route scheduled job units, probes, validation, notifications, and reconciliation through native HQ core commands. Fixed shell-output fixtures retain differential coverage for all 238 cases, and the published E2E matrix now runs native-only probes for these commands.
|
|
8
|
+
|
|
9
|
+
- `hq core checkpoint` now runs its background sibling under a supervisor that
|
|
10
|
+
stops it after 30 minutes and kills any process it left behind, and the
|
|
11
|
+
sibling prompt forbids unbounded searches over the workspace folder.
|
|
12
|
+
|
|
13
|
+
## [5.345.114] — 2026-10-13
|
|
14
|
+
- The native `hq core` utilities no longer include bundled shell fallbacks. Published E2E now probes each command directly.
|
|
15
|
+
|
|
16
|
+
- `hq capacity login <provider> --account <id>` signs a registered account
|
|
17
|
+
in again into its existing home. The account id, home, and Keychain item
|
|
18
|
+
stay the same. `--account default` (or the id registered at `~/.claude`)
|
|
19
|
+
signs in the default home without setting `CLAUDE_CONFIG_DIR`. A reserved
|
|
20
|
+
id that names no registered account is refused before any sign-in runs.
|
|
21
|
+
- `hq capacity login --repair` signs in each signed-out account one at a
|
|
22
|
+
time, keeps going when one fails, and ends with a Fixed/Skipped report.
|
|
23
|
+
It exits 1 when any signed-out account was skipped.
|
|
24
|
+
- `hq capacity route` and `hq lanes create --account auto` list every
|
|
25
|
+
signed-out account under `skipped` with its reason and the exact
|
|
26
|
+
`hq capacity login <provider> --account <id>` fix, using the lanes
|
|
27
|
+
account id. The `no_account_with_room` error now carries `skipped`.
|
|
28
|
+
|
|
5
29
|
## [5.345.113] — 2026-10-13
|
|
6
30
|
- `hq daemon sync now --company <slug>` asks the running sync daemon to sync one company right away, without restarting sync. It needs hq-cloud 6.18.68 or later in the daemon; with an older daemon it falls back to the previous restart. `--json` reports which path it took. The desktop app uses this for Sync on the "Added to" banner.
|
|
7
31
|
|
|
@@ -45,11 +45,15 @@ command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
|
45
45
|
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
46
46
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
47
47
|
msys*:*|*:MINGW*)
|
|
48
|
+
_hq_jq_binary_supported=""
|
|
48
49
|
jq() {
|
|
49
50
|
local stdout_file stderr_file status binary_supported
|
|
50
51
|
stdout_file="$(mktemp)" || return $?
|
|
51
52
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
52
|
-
if
|
|
53
|
+
if [[ -z "$_hq_jq_binary_supported" ]]; then
|
|
54
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then _hq_jq_binary_supported=1; else _hq_jq_binary_supported=0; fi
|
|
55
|
+
fi
|
|
56
|
+
binary_supported=$_hq_jq_binary_supported
|
|
53
57
|
if [[ "$binary_supported" == 1 ]]; then
|
|
54
58
|
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
55
59
|
command cat "$stdout_file"
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* The routes are self-only per agent, so every call uses the BOT's own machine
|
|
15
15
|
* identity — the same credential path its turns use — not the owner's session.
|
|
16
16
|
*/
|
|
17
|
-
import { execFileSync } from "node:child_process";
|
|
18
17
|
import type { Command } from "commander";
|
|
19
18
|
import { BotApi, type BotJobRecord, type CreateJobInput } from "../lib/bot/api.js";
|
|
20
19
|
import { type BotConfig, type BotKind } from "../lib/bot/config.js";
|
|
@@ -55,18 +54,9 @@ export interface ParsedSchedule {
|
|
|
55
54
|
/** Plain words for the schedule, when the helper supplied them. */
|
|
56
55
|
human?: string;
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Plain English → cron, using HQ's OWN helper (`core/scripts/jobs-schedule-parse.sh`)
|
|
61
|
-
* so `hq bot jobs --every` and the `/schedule` command agree on what "every
|
|
62
|
-
* weekday morning" means. When the helper is not in this HQ root, the caller is
|
|
63
|
-
* told to pass `--cron` instead rather than getting a second, divergent parser.
|
|
64
|
-
*/
|
|
57
|
+
/** Plain English → cron, using the same native parser as `hq core`. */
|
|
65
58
|
export declare function parseScheduleText(text: string, opts: {
|
|
66
|
-
hqRoot: string;
|
|
67
59
|
timezone: string;
|
|
68
|
-
exec?: typeof execFileSync;
|
|
69
|
-
exists?: (p: string) => boolean;
|
|
70
60
|
}): ParsedSchedule;
|
|
71
61
|
/** The create body the cloud expects, with the client-side limits enforced first. */
|
|
72
62
|
export declare function buildCreateJobBody(input: {
|
|
@@ -15,14 +15,12 @@
|
|
|
15
15
|
* identity — the same credential path its turns use — not the owner's session.
|
|
16
16
|
*/
|
|
17
17
|
import chalk from "chalk";
|
|
18
|
-
import { execFileSync } from "node:child_process";
|
|
19
|
-
import * as fs from "node:fs";
|
|
20
|
-
import * as path from "node:path";
|
|
21
18
|
import { localJobsClient } from "../lib/bot/local-jobs.js";
|
|
22
19
|
import { BotApiError } from "../lib/bot/api.js";
|
|
23
20
|
import { effectiveBotKind } from "../lib/bot/config.js";
|
|
24
21
|
import { isQuietJobContext, JOB_RUN_OPTIONS_CONTEXT_KEY } from "../lib/bot/job-run.js";
|
|
25
22
|
import { BOT_JOB_QUIET_OPTION_FLAG, botJobQuietOptionEnabled } from "../lib/bot/job-quiet-flag.js";
|
|
23
|
+
import { runScheduleParse } from "./core-jobs-schedule.js";
|
|
26
24
|
// ─── Schedule translation ────────────────────────────────────────────────────
|
|
27
25
|
/** Mirrors hq-pro-agents `MIN_RECURRING_INTERVAL_SECONDS`. */
|
|
28
26
|
export const MIN_RECURRING_INTERVAL_SECONDS = 5 * 60;
|
|
@@ -190,43 +188,24 @@ export function assertScheduleWithinFloor(expr) {
|
|
|
190
188
|
refuse(`recurring jobs may not fire more often than every ${MIN_RECURRING_INTERVAL_SECONDS / 60} minutes`);
|
|
191
189
|
}
|
|
192
190
|
}
|
|
193
|
-
|
|
194
|
-
return path.join(hqRoot, "core", "scripts", "jobs-schedule-parse.sh");
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Plain English → cron, using HQ's OWN helper (`core/scripts/jobs-schedule-parse.sh`)
|
|
198
|
-
* so `hq bot jobs --every` and the `/schedule` command agree on what "every
|
|
199
|
-
* weekday morning" means. When the helper is not in this HQ root, the caller is
|
|
200
|
-
* told to pass `--cron` instead rather than getting a second, divergent parser.
|
|
201
|
-
*/
|
|
191
|
+
/** Plain English → cron, using the same native parser as `hq core`. */
|
|
202
192
|
export function parseScheduleText(text, opts) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
const exec = opts.exec ?? execFileSync;
|
|
210
|
-
let raw;
|
|
211
|
-
try {
|
|
212
|
-
raw = String(exec("bash", [helper, "--tz", opts.timezone, text], { encoding: "utf8" })).trim();
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
const out = err.stdout;
|
|
216
|
-
raw = typeof out === "string" ? out.trim() : "";
|
|
217
|
-
if (!raw) {
|
|
218
|
-
refuse(`I couldn't work out a schedule from "${text}". Pass --cron "<expression>" instead.`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
193
|
+
let raw = "";
|
|
194
|
+
let stderr = "";
|
|
195
|
+
const status = runScheduleParse(["--tz", opts.timezone, text], {
|
|
196
|
+
stdout: (value) => { raw += value; },
|
|
197
|
+
stderr: (value) => { stderr += value; },
|
|
198
|
+
});
|
|
221
199
|
let parsed;
|
|
222
200
|
try {
|
|
223
|
-
parsed = JSON.parse(raw);
|
|
201
|
+
parsed = JSON.parse(raw.trim());
|
|
224
202
|
}
|
|
225
203
|
catch {
|
|
226
204
|
refuse(`I couldn't work out a schedule from "${text}". Pass --cron "<expression>" instead.`);
|
|
227
205
|
}
|
|
228
|
-
if (!parsed.ok || !parsed.cron) {
|
|
229
|
-
|
|
206
|
+
if (status !== 0 || !parsed.ok || !parsed.cron) {
|
|
207
|
+
const reason = parsed.error || stderr.trim();
|
|
208
|
+
refuse(`I couldn't work out a schedule from "${text}"${reason ? ` (${reason})` : ""}. Pass --cron "<expression>" instead.`);
|
|
230
209
|
}
|
|
231
210
|
return {
|
|
232
211
|
cron: parsed.cron,
|
|
@@ -418,7 +397,7 @@ export async function runBotJobs(nameArg, opts, deps) {
|
|
|
418
397
|
const timezone = opts.tz?.trim() || (deps.timezone ?? machineTimezone)();
|
|
419
398
|
const parse = deps.parseSchedule ?? parseScheduleText;
|
|
420
399
|
const schedule = opts.every
|
|
421
|
-
? parse(opts.every, {
|
|
400
|
+
? parse(opts.every, { timezone })
|
|
422
401
|
: { cron: opts.cron, timezone };
|
|
423
402
|
const body = buildCreateJobBody({ prompt: opts.add, cron: schedule.cron, timezone: schedule.timezone, quiet: opts.quiet });
|
|
424
403
|
const api = await jobsApi();
|
|
@@ -533,9 +533,11 @@ export function registerCapacityCommand(program) {
|
|
|
533
533
|
});
|
|
534
534
|
if (wantsJson(opts, command)) {
|
|
535
535
|
console.log(JSON.stringify(result.json ?? loginJson(rows, clock.nowMs, provider), null, 2));
|
|
536
|
+
process.exitCode = result.exitCode;
|
|
536
537
|
return;
|
|
537
538
|
}
|
|
538
539
|
console.log(result.text);
|
|
540
|
+
process.exitCode = result.exitCode;
|
|
539
541
|
}
|
|
540
542
|
catch (err) {
|
|
541
543
|
failCapacity(err);
|
|
@@ -249,7 +249,7 @@ export function runAuditLog(args, deps) {
|
|
|
249
249
|
}
|
|
250
250
|
export function registerCoreAuditLogNative(core) {
|
|
251
251
|
registerNativeUtility(core, {
|
|
252
|
-
name: "audit-log",
|
|
252
|
+
name: "audit-log", root: "live", summary: "Append, query, or summarize the HQ audit log",
|
|
253
253
|
resolveRoot: (_cwd, hqRoot) => resolveLiveRoot({ hqRoot }),
|
|
254
254
|
run: (args, context) => runAuditLog(args, context),
|
|
255
255
|
});
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
+
/**
|
|
12
|
+
* Wall-clock cap on one sibling run. The supervisor kills the sibling and its
|
|
13
|
+
* descendants at this point. It stays under the lock TTL so a capped run
|
|
14
|
+
* always releases the queue before the lock would be presumed abandoned.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SIBLING_MAX_RUNTIME_MS: number;
|
|
11
17
|
type Backend = "claude" | "codex" | "grok" | "none";
|
|
12
18
|
type SpawnableBackend = Exclude<Backend, "none">;
|
|
13
19
|
/**
|
|
@@ -70,6 +76,27 @@ export declare function autoBackendPreference(preferGrok?: boolean): SpawnableBa
|
|
|
70
76
|
*/
|
|
71
77
|
export declare function companyPrefersGrok(liveRoot: string, companySlug: string | null | undefined): Promise<boolean>;
|
|
72
78
|
export declare function siblingArgs(backend: SpawnableBackend, prompt: string): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Supervisor for a detached sibling on POSIX. It forks the backend, records
|
|
81
|
+
* the backend's descendant tree every 2 seconds, kills that tree after the
|
|
82
|
+
* wall-clock cap, and on exit terminates any recorded descendant that was
|
|
83
|
+
* reparented to init. Without it, a shell command the agent started (an
|
|
84
|
+
* unbounded `find workspace` was the observed case) kept running after the
|
|
85
|
+
* agent exited, with PID 1 as its parent.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SIBLING_SUPERVISOR_SCRIPT: string;
|
|
88
|
+
/**
|
|
89
|
+
* The command that launches a sibling: the backend wrapped in the supervisor
|
|
90
|
+
* on POSIX hosts with perl, the bare backend otherwise.
|
|
91
|
+
*/
|
|
92
|
+
export declare function siblingLaunch(backend: SpawnableBackend, args: string[], options?: {
|
|
93
|
+
platform?: NodeJS.Platform;
|
|
94
|
+
perlPath?: string | null;
|
|
95
|
+
maxRuntimeMs?: number;
|
|
96
|
+
}): {
|
|
97
|
+
command: string;
|
|
98
|
+
args: string[];
|
|
99
|
+
};
|
|
73
100
|
/** Attach the native checkpoint command to the hidden `hq core` group. */
|
|
74
101
|
export declare function registerCoreCheckpointCommand(core: Command, preResolvedHqRoot?: string): void;
|
|
75
102
|
export {};
|
|
@@ -63,6 +63,12 @@ const FALLBACK_BACKEND_ORDER = ["claude", "codex", "grok"];
|
|
|
63
63
|
const BACKEND_PROBE_TIMEOUT_MS = 10_000;
|
|
64
64
|
/** A sibling still holding the lock after this long is treated as abandoned. */
|
|
65
65
|
const SIBLING_LOCK_TTL_MS = 60 * 60 * 1000;
|
|
66
|
+
/**
|
|
67
|
+
* Wall-clock cap on one sibling run. The supervisor kills the sibling and its
|
|
68
|
+
* descendants at this point. It stays under the lock TTL so a capped run
|
|
69
|
+
* always releases the queue before the lock would be presumed abandoned.
|
|
70
|
+
*/
|
|
71
|
+
export const SIBLING_MAX_RUNTIME_MS = 30 * 60 * 1000;
|
|
66
72
|
/** Newest-wins cap so a wedged sibling cannot grow an unbounded queue. */
|
|
67
73
|
const MAX_PENDING_PAYLOADS = 50;
|
|
68
74
|
class CheckpointUsageError extends Error {
|
|
@@ -115,6 +121,11 @@ HQ install. Your parent session's state is in <payloadPath>. Work
|
|
|
115
121
|
quietly and do not ask questions; if something is ambiguous, record it in the
|
|
116
122
|
report instead of guessing.
|
|
117
123
|
|
|
124
|
+
0. SEARCH BOUNDS: never run find, grep -r, rg, ls -R, or du over workspace/,
|
|
125
|
+
the HQ root, or repos/. workspace/worktrees holds hundreds of full checkouts
|
|
126
|
+
with node_modules, and an unbounded walk there runs for many minutes. Read
|
|
127
|
+
the exact paths named here or in the payload. If you must search, name one
|
|
128
|
+
narrow directory and use -maxdepth 3, pruning worktrees and node_modules.
|
|
118
129
|
1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)
|
|
119
130
|
both for session context and to extract additional reusable learnings/insights
|
|
120
131
|
the parent did not pass explicitly. Never quote secrets
|
|
@@ -156,6 +167,8 @@ report instead of guessing.
|
|
|
156
167
|
workspace/insights/.
|
|
157
168
|
4. Close an active session journal fail-soft with
|
|
158
169
|
bash .claude/skills/_shared/journal.sh close "<project_dir>" "<one-line synthesis>".
|
|
170
|
+
Locate the journal only with bash .claude/skills/_shared/journal.sh path or
|
|
171
|
+
the payload; if neither names one, skip this step. Do not search for it.
|
|
159
172
|
Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with
|
|
160
173
|
id, created_at, summary, files, and next_steps for backward compatibility.
|
|
161
174
|
5. Update workspace/threads/recent.md and regenerate
|
|
@@ -888,6 +901,76 @@ export function siblingArgs(backend, prompt) {
|
|
|
888
901
|
];
|
|
889
902
|
}
|
|
890
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* Supervisor for a detached sibling on POSIX. It forks the backend, records
|
|
906
|
+
* the backend's descendant tree every 2 seconds, kills that tree after the
|
|
907
|
+
* wall-clock cap, and on exit terminates any recorded descendant that was
|
|
908
|
+
* reparented to init. Without it, a shell command the agent started (an
|
|
909
|
+
* unbounded `find workspace` was the observed case) kept running after the
|
|
910
|
+
* agent exited, with PID 1 as its parent.
|
|
911
|
+
*/
|
|
912
|
+
export const SIBLING_SUPERVISOR_SCRIPT = [
|
|
913
|
+
"use strict; use warnings; use POSIX qw(:sys_wait_h);",
|
|
914
|
+
"my $secs = shift @ARGV;",
|
|
915
|
+
'my $pid = fork(); die "checkpoint sibling: fork failed\\n" unless defined $pid;',
|
|
916
|
+
"if ($pid == 0) { exec { $ARGV[0] } @ARGV; exit 127; }",
|
|
917
|
+
"my %seen;",
|
|
918
|
+
"sub tree {",
|
|
919
|
+
" my %kids;",
|
|
920
|
+
" for (qx{ps -axo pid=,ppid=}) { my ($p, $pp) = split ' '; push @{$kids{$pp}}, $p if defined $pp; }",
|
|
921
|
+
" my @out; my @q = ($pid);",
|
|
922
|
+
" while (@q) { my $x = shift @q; for my $c (@{$kids{$x} || []}) { push @out, $c; push @q, $c; } }",
|
|
923
|
+
" return @out;",
|
|
924
|
+
"}",
|
|
925
|
+
"my $deadline = time + $secs; my $rc = 0;",
|
|
926
|
+
"while (1) {",
|
|
927
|
+
" my $w = waitpid($pid, WNOHANG);",
|
|
928
|
+
" if ($w == $pid) { $rc = $?; last; }",
|
|
929
|
+
" $seen{$_} = 1 for tree();",
|
|
930
|
+
" if (time >= $deadline) {",
|
|
931
|
+
" my @d = tree(); $seen{$_} = 1 for @d;",
|
|
932
|
+
' print STDERR "checkpoint sibling: killed after $secs s wall-clock limit\\n";',
|
|
933
|
+
" kill 'TERM', @d, $pid; sleep 2;",
|
|
934
|
+
" kill 'KILL', grep { kill 0, $_ } (tree(), @d, $pid);",
|
|
935
|
+
" waitpid($pid, 0); $rc = 124 << 8; last;",
|
|
936
|
+
" }",
|
|
937
|
+
" sleep 2;",
|
|
938
|
+
"}",
|
|
939
|
+
"for my $p (keys %seen) {",
|
|
940
|
+
" my $pp = qx{ps -o ppid= -p $p 2>/dev/null}; $pp =~ s/\\s//g;",
|
|
941
|
+
" kill 'TERM', $p if $pp eq '1';",
|
|
942
|
+
"}",
|
|
943
|
+
"exit(($rc & 127) ? 128 + ($rc & 127) : ($rc >> 8));",
|
|
944
|
+
].join("\n");
|
|
945
|
+
/**
|
|
946
|
+
* The command that launches a sibling: the backend wrapped in the supervisor
|
|
947
|
+
* on POSIX hosts with perl, the bare backend otherwise.
|
|
948
|
+
*/
|
|
949
|
+
export function siblingLaunch(backend, args, options = {}) {
|
|
950
|
+
const platform = options.platform ?? process.platform;
|
|
951
|
+
if (platform === "win32")
|
|
952
|
+
return { command: backend, args };
|
|
953
|
+
const perlPath = options.perlPath === undefined ? findPerl() : options.perlPath;
|
|
954
|
+
if (!perlPath)
|
|
955
|
+
return { command: backend, args };
|
|
956
|
+
const seconds = Math.max(1, Math.ceil((options.maxRuntimeMs ?? SIBLING_MAX_RUNTIME_MS) / 1000));
|
|
957
|
+
return {
|
|
958
|
+
command: perlPath,
|
|
959
|
+
args: ["-e", SIBLING_SUPERVISOR_SCRIPT, String(seconds), backend, ...args],
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function findPerl() {
|
|
963
|
+
for (const candidate of ["/usr/bin/perl", "/bin/perl", "/opt/homebrew/bin/perl", "/usr/local/bin/perl"]) {
|
|
964
|
+
try {
|
|
965
|
+
if (fs.statSync(candidate).isFile())
|
|
966
|
+
return candidate;
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
// Try the next candidate.
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
891
974
|
function siblingPayload(input, threadPath, backend) {
|
|
892
975
|
return {
|
|
893
976
|
backend,
|
|
@@ -1047,10 +1130,10 @@ async function startSibling(liveRoot, input, threadPath, backend) {
|
|
|
1047
1130
|
fs.writeFileSync(path.join(runDir, "prompt.md"), prompt);
|
|
1048
1131
|
const logPath = path.join(runDir, "run.log");
|
|
1049
1132
|
const logFd = fs.openSync(logPath, "a");
|
|
1050
|
-
const
|
|
1133
|
+
const launch = siblingLaunch(backend, siblingArgs(backend, prompt));
|
|
1051
1134
|
let child;
|
|
1052
1135
|
try {
|
|
1053
|
-
child = spawn(
|
|
1136
|
+
child = spawn(launch.command, launch.args, {
|
|
1054
1137
|
detached: true,
|
|
1055
1138
|
windowsHide: true,
|
|
1056
1139
|
cwd: liveRoot,
|
|
@@ -672,7 +672,7 @@ export async function runDmBind(args, deps) {
|
|
|
672
672
|
}
|
|
673
673
|
export function registerCoreDmBindNative(core) {
|
|
674
674
|
registerNativeUtility(core, {
|
|
675
|
-
name: "dm-bind",
|
|
675
|
+
name: "dm-bind", root: "live", summary: "Bind this session to one HQ DM channel",
|
|
676
676
|
resolveRoot: (_cwd, hqRoot) => resolveLiveRoot({ hqRoot }),
|
|
677
677
|
run: (args, context) => runDmBind(args, createDmBindDeps(context.root, context.cwd, context.env, context.now)),
|
|
678
678
|
});
|
|
@@ -178,7 +178,7 @@ export function runGitPackExtensionCheck(args, deps) {
|
|
|
178
178
|
}
|
|
179
179
|
export function registerCoreGitPackExtensionCheckNative(core) {
|
|
180
180
|
registerNativeUtility(core, {
|
|
181
|
-
name: "git-pack-extension-check",
|
|
181
|
+
name: "git-pack-extension-check", root: "cwd", summary: "Find or repair extensionless Git pack files",
|
|
182
182
|
resolveRoot: (cwd) => cwd,
|
|
183
183
|
run: (args, context) => runGitPackExtensionCheck(args, {
|
|
184
184
|
cwd: context.cwd,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
/** Preserve raw argv (including the option terminator) through Commander. */
|
|
3
3
|
export declare function rawJobArgumentsForCommand(command: Command, fallback: string[], raw?: string[]): string[];
|
|
4
|
-
/** Shared
|
|
5
|
-
export declare function registerCoreJobLeaf(group: Command, core: Command, name: string,
|
|
4
|
+
/** Shared registration path for each leaf in the singular job group. */
|
|
5
|
+
export declare function registerCoreJobLeaf(group: Command, core: Command, name: string, summary: string, run: (args: string[], hqRoot: string) => number, canUseLiveRoot: (args: string[]) => boolean, valueOptions: readonly string[], booleanOptions?: readonly string[]): void;
|
|
6
6
|
//# sourceMappingURL=core-job-native-utils.d.ts.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
2
|
-
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
3
2
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
|
-
import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
|
|
5
3
|
/** Preserve raw argv (including the option terminator) through Commander. */
|
|
6
4
|
export function rawJobArgumentsForCommand(command, fallback, raw = process.argv) {
|
|
7
5
|
const names = [];
|
|
@@ -19,8 +17,8 @@ export function rawJobArgumentsForCommand(command, fallback, raw = process.argv)
|
|
|
19
17
|
}
|
|
20
18
|
return start >= 0 ? raw.slice(start) : (command.args.length ? command.args : fallback);
|
|
21
19
|
}
|
|
22
|
-
/** Shared
|
|
23
|
-
export function registerCoreJobLeaf(group, core, name,
|
|
20
|
+
/** Shared registration path for each leaf in the singular job group. */
|
|
21
|
+
export function registerCoreJobLeaf(group, core, name, summary, run, canUseLiveRoot, valueOptions, booleanOptions = []) {
|
|
24
22
|
const command = group.command(name)
|
|
25
23
|
.description(summary)
|
|
26
24
|
.helpOption(false)
|
|
@@ -33,17 +31,11 @@ export function registerCoreJobLeaf(group, core, name, asset, summary, run, canU
|
|
|
33
31
|
const hqRoot = canUseLiveRoot(argv)
|
|
34
32
|
? resolveLiveRoot({ hqRoot: leafRootOverride(argv, valueOptions, booleanOptions) ?? groupHqRoot })
|
|
35
33
|
: groupHqRoot ?? "";
|
|
36
|
-
if (!await coreNativeJobsEnabled()) {
|
|
37
|
-
const result = runBundledScript({ asset, args: argv, cwd: process.cwd(), hqRoot: hqRoot || undefined });
|
|
38
|
-
if (result.code !== 0)
|
|
39
|
-
process.exit(result.code);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
34
|
const result = run(argv, hqRoot);
|
|
43
35
|
if (result !== 0)
|
|
44
36
|
process.exit(result);
|
|
45
37
|
});
|
|
46
|
-
describeCoreCommand(command, { execution: "native",
|
|
38
|
+
describeCoreCommand(command, { execution: "native", root: "live" });
|
|
47
39
|
}
|
|
48
40
|
/**
|
|
49
41
|
* Find a leaf --hq-root without resolving anything until the leaf parser has
|
|
@@ -5,7 +5,6 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { load } from "js-yaml";
|
|
6
6
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
7
|
import { registerCoreJobLeaf } from "./core-job-native-utils.js";
|
|
8
|
-
const ASSET = "core/scripts/hq-job-notify.sh";
|
|
9
8
|
const USAGE = [
|
|
10
9
|
"hq-job-notify.sh — dm-only remediation alerts for Outpost scheduled jobs (US-006).",
|
|
11
10
|
"",
|
|
@@ -351,7 +350,7 @@ export function runJobNotify(argv, options = {}) {
|
|
|
351
350
|
return 0;
|
|
352
351
|
}
|
|
353
352
|
export function registerCoreJobNotifyNativeCommand(group, core) {
|
|
354
|
-
registerCoreJobLeaf(group, core, "notify",
|
|
353
|
+
registerCoreJobLeaf(group, core, "notify", "Send a scheduled-job status alert to its owner", (argv, hqRoot) => runJobNotify(argv, { hqRoot }), canUseLiveRootForArguments, [
|
|
355
354
|
"--hq-root", "--job-id", "--job-name", "--outcome", "--summary", "--duration", "--log-path",
|
|
356
355
|
"--failure-class", "--next-action", "--owner", "--notify",
|
|
357
356
|
]);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
export declare const JOB_PROBE_ASSET = "core/scripts/hq-job-probe.sh";
|
|
3
2
|
export declare function displayJobFile(value: string, platform?: NodeJS.Platform): string;
|
|
4
|
-
export declare function
|
|
3
|
+
export declare function displayShellRegistryError(value: string, platform?: NodeJS.Platform): string;
|
|
5
4
|
export declare const JOB_PROBE_USAGE: string;
|
|
6
5
|
export type Checks = {
|
|
7
6
|
runtime: string;
|
|
@@ -32,7 +31,7 @@ export type ProbeDeps = {
|
|
|
32
31
|
isDirectory: (file: string) => boolean;
|
|
33
32
|
realpath: (file: string) => string | undefined;
|
|
34
33
|
readText: (file: string) => string;
|
|
35
|
-
writeState: (file: string, body: string, mode: number) => void;
|
|
34
|
+
writeState: (file: string, body: string, mode: number, lineEnding?: "\n" | "\r\n") => void;
|
|
36
35
|
writeStderr: (line: string) => void;
|
|
37
36
|
listSecrets: (company?: string) => Promise<{
|
|
38
37
|
code: number;
|
|
@@ -5,23 +5,18 @@ import * as path from "node:path";
|
|
|
5
5
|
import { load, JSON_SCHEMA } from "js-yaml";
|
|
6
6
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
7
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
8
|
-
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
9
8
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
10
|
-
import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
|
|
11
9
|
import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
|
|
12
10
|
import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
|
|
13
|
-
export const JOB_PROBE_ASSET = "core/scripts/hq-job-probe.sh";
|
|
14
11
|
export function displayJobFile(value, platform = process.platform) {
|
|
15
12
|
if (platform !== "win32")
|
|
16
13
|
return value;
|
|
17
14
|
return path.win32.resolve(value).replaceAll("\\", "/");
|
|
18
15
|
}
|
|
19
|
-
export function
|
|
16
|
+
export function displayShellRegistryError(value, platform = process.platform) {
|
|
20
17
|
if (platform !== "win32")
|
|
21
18
|
return value;
|
|
22
|
-
|
|
23
|
-
const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
|
|
24
|
-
return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
|
|
19
|
+
return value.replace(/([A-Za-z]):[\\/]/gu, (_match, drive) => `/${drive.toLowerCase()}/`).replaceAll("\\", "/");
|
|
25
20
|
}
|
|
26
21
|
export const JOB_PROBE_USAGE = [
|
|
27
22
|
"hq-job-probe.sh — Outpost requirements probe → hq-pro jobs status (US-008).", "",
|
|
@@ -93,7 +88,7 @@ const realpath = (f) => { try {
|
|
|
93
88
|
catch {
|
|
94
89
|
return undefined;
|
|
95
90
|
} };
|
|
96
|
-
function writeState(file, body, mode) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${body}
|
|
91
|
+
function writeState(file, body, mode, lineEnding = "\n") { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${body}${lineEnding}`, { mode }); try {
|
|
97
92
|
fs.chmodSync(file, mode);
|
|
98
93
|
}
|
|
99
94
|
catch { /* legacy chmod is best-effort */ } }
|
|
@@ -286,7 +281,12 @@ function auth(runtime, d) {
|
|
|
286
281
|
return (d.statSize(file) ?? 0) > 0 ? "ok" : "missing";
|
|
287
282
|
}
|
|
288
283
|
function writeAttempt(d, id, data) { d.writeState(path.join(d.env.HQ_JOB_PROBE_STATE_DIR || path.join(d.home, ".hq/jobs/probes"), id, "last-attempt.json"), compact(data), 0o600); }
|
|
289
|
-
function writeCache(d, id, readiness, source) {
|
|
284
|
+
function writeCache(d, id, readiness, source) {
|
|
285
|
+
// The bundled shell redirects the Windows jq.exe output to this file. The
|
|
286
|
+
// Windows runtime writes CRLF, so preserve those bytes in the native port.
|
|
287
|
+
const lineEnding = process.platform === "win32" ? "\r\n" : "\n";
|
|
288
|
+
d.writeState(path.join(d.env.HQ_JOB_STATUS_CACHE_DIR || path.join(d.home, ".hq/jobs/status-cache"), `${id}.json`), compact({ job_id: id, readiness, updated_at: isoNow(d), source }), 0o600, lineEnding);
|
|
289
|
+
}
|
|
290
290
|
function resolveCwd(root, cwd, d) {
|
|
291
291
|
const canon = d.realpath(root) ?? path.resolve(root), candidate = path.resolve(root, cwd), file = d.realpath(candidate) ?? candidate;
|
|
292
292
|
return { file, allowed: file === canon || file.startsWith(`${canon}${path.sep}`) };
|
|
@@ -338,13 +338,13 @@ async function probeFile(file, dry, d) {
|
|
|
338
338
|
catch (e) {
|
|
339
339
|
const rawStderr = legacyYamlParseDiagnostics(file, d);
|
|
340
340
|
if (rawStderr)
|
|
341
|
-
throw new Fatal(`job missing id: ${
|
|
341
|
+
throw new Fatal(`job missing id: ${displayJobFile(file)}`, 2, "hq-job-probe", rawStderr);
|
|
342
342
|
throw new Fatal(`invalid job YAML: ${file}${e instanceof Error ? `: ${e.message}` : ""}`);
|
|
343
343
|
}
|
|
344
344
|
const id = scalar(job.id), name = scalar(job.name), runtime = scalar(nested(job, "requirements", "runtime")) || scalar(job.runtime);
|
|
345
345
|
const company = scalar(nested(job, "requirements", "company")), cwd = scalar(nested(job, "requirements", "cwd") ?? job.cwd);
|
|
346
346
|
if (!id)
|
|
347
|
-
throw new Fatal(`job missing id: ${
|
|
347
|
+
throw new Fatal(`job missing id: ${displayJobFile(file)}`);
|
|
348
348
|
if (!runtime)
|
|
349
349
|
throw new Fatal(`job missing runtime: ${file}`);
|
|
350
350
|
const at = isoNow(d), actions = [], authState = auth(runtime, d);
|
|
@@ -515,7 +515,7 @@ export async function runJobProbe(argv, injected) {
|
|
|
515
515
|
return { code: 2, stdout: "", stderr: "hq-job-probe: --all does not take a job argument\n" };
|
|
516
516
|
const files = allFiles(root);
|
|
517
517
|
if (!files.length)
|
|
518
|
-
return { code: 2, stdout: "", stderr: `hq-job-probe: no job YAML found under personal/jobs or companies/*/jobs in ${
|
|
518
|
+
return { code: 2, stdout: "", stderr: `hq-job-probe: no job YAML found under personal/jobs or companies/*/jobs in ${displayJobFile(root)}\n` };
|
|
519
519
|
let code = 0;
|
|
520
520
|
try {
|
|
521
521
|
for (const f of files) {
|
|
@@ -545,9 +545,7 @@ export async function runJobProbe(argv, injected) {
|
|
|
545
545
|
// then emits the ordinary not-found error after the process substitution
|
|
546
546
|
// closes. Preserve both stderr lines for byte parity.
|
|
547
547
|
if (x instanceof Fatal && x.message.startsWith("duplicate job id '")) {
|
|
548
|
-
const shellMessage =
|
|
549
|
-
? x.message.replace(/([A-Za-z]):[\\/]/gu, (_match, drive) => "/" + drive.toLowerCase() + "/").replaceAll("\\", "/")
|
|
550
|
-
: x.message;
|
|
548
|
+
const shellMessage = displayShellRegistryError(x.message);
|
|
551
549
|
return { code: x.code, stdout: "", stderr: `${x.prefix}: ${shellMessage}\n${x.prefix}: job id not found in registry: ${opts.target}\n` };
|
|
552
550
|
}
|
|
553
551
|
return failure(x);
|
|
@@ -593,12 +591,6 @@ export function registerCoreJobProbeCommand(job) {
|
|
|
593
591
|
const root = !forwarded.help && (!forwarded.hasRoot || Boolean(forwarded.root))
|
|
594
592
|
? resolveLiveRoot({ hqRoot: forwarded.hasRoot ? forwarded.root : core.opts().hqRoot })
|
|
595
593
|
: undefined;
|
|
596
|
-
if (!await coreNativeJobsEnabled()) {
|
|
597
|
-
const r = runBundledScript({ asset: JOB_PROBE_ASSET, args: argv, cwd: process.cwd(), ...(root ? { hqRoot: root } : {}) });
|
|
598
|
-
if (r.code)
|
|
599
|
-
process.exit(r.code);
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
594
|
const r = await runJobProbe(argv, defaultProbeDeps(root ?? (process.env.HQ_ROOT || process.cwd())));
|
|
603
595
|
if (r.stdout)
|
|
604
596
|
process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
|
|
@@ -607,6 +599,6 @@ export function registerCoreJobProbeCommand(job) {
|
|
|
607
599
|
if (r.code)
|
|
608
600
|
process.exit(r.code);
|
|
609
601
|
});
|
|
610
|
-
describeCoreCommand(command, { execution: "native",
|
|
602
|
+
describeCoreCommand(command, { execution: "native", root: "live" });
|
|
611
603
|
}
|
|
612
604
|
//# sourceMappingURL=core-job-probe-native.js.map
|
|
@@ -4,7 +4,6 @@ import * as os from "node:os";
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
6
6
|
import { registerCoreJobLeaf } from "./core-job-native-utils.js";
|
|
7
|
-
const ASSET = "core/scripts/hq-job-remediate.sh";
|
|
8
7
|
const USAGE = [
|
|
9
8
|
"hq-job-remediate.sh — bounded self-remediation for Outpost scheduled jobs (US-006).",
|
|
10
9
|
"",
|
|
@@ -367,7 +366,7 @@ export function runJobRemediate(argv, options = {}) {
|
|
|
367
366
|
}
|
|
368
367
|
}
|
|
369
368
|
export function registerCoreJobRemediateNativeCommand(group, core) {
|
|
370
|
-
registerCoreJobLeaf(group, core, "remediate",
|
|
369
|
+
registerCoreJobLeaf(group, core, "remediate", "Run bounded remediation for a scheduled-job failure", (argv, hqRoot) => runJobRemediate(argv, { hqRoot }), canUseLiveRootForArguments, [
|
|
371
370
|
"--hq-root", "--failure-class", "--job-id", "--company",
|
|
372
371
|
], ["--dry-run"]);
|
|
373
372
|
}
|