@indigoai-us/hq-cli 5.97.3-rc.1 → 5.98.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.
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,24 @@
|
|
|
7
7
|
* I/O itself.
|
|
8
8
|
*/
|
|
9
9
|
import { Command } from "commander";
|
|
10
|
+
type Backend = "claude" | "codex" | "grok" | "none";
|
|
11
|
+
type SpawnableBackend = Exclude<Backend, "none">;
|
|
10
12
|
/**
|
|
11
13
|
* Kept in TypeScript rather than in a bundled asset: it is an instruction to
|
|
12
14
|
* a locally-installed agent, not a scaffold script that should be packaged.
|
|
13
15
|
*/
|
|
14
16
|
export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)\n both for session context and to extract additional reusable learnings/insights\n the parent did not pass explicitly. Never quote secrets\n or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists\n under this HQ root, read it and follow it wherever it goes beyond these instructions;\n the write bounds below always win over the skill text.\n2. Upgrade the thread file named in the payload IN PLACE: verify/repair its\n JSON; fill git.remote_url, git.initial_commit, git.commits_made, and\n git.knowledge_repos by scanning core/knowledge/public/*,\n core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge\n for symlinks or directories containing .git, recording dirty repositories\n as {\"<name>\": {\"commit\": \"<short>\", \"dirty\": true}}. Fill worker,\n next_steps, and insights; set type to \"checkpoint\"; then rename the file to\n drop -auto- from its filename. Use the renamed path in every reference you\n write afterwards.\n3. For every explicit or transcript-derived learning that is a reusable rule,\n FIRST search the existing policies for one the learning refines, contradicts\n or duplicates. Then take exactly one of these actions and name it in the\n report \u2014 the policy set is curated, not append-only:\n - AMEND an existing policy in place when the learning sharpens it, narrows\n its scope, or adds a case, and the rule as written is still correct.\n - SUPERSEDE it when the learning contradicts it: rewrite the rule to what is\n now true and record inside the file what changed and why.\n - MERGE near-duplicates into the single best-named file, then delete the\n files you merged away.\n - CREATE a new policy only when no existing policy covers the rule.\n Follow core/knowledge/public/hq-core/policies-spec.md. Write under\n personal/policies/ or, only when the payload names a company and the rule is\n company-specific, companies/<company>/policies/.\n DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,\n only inside those two directories, and never one whose body marks it HARD \u2014\n if a hard policy now looks wrong, leave it untouched and flag it in the\n report for a human to decide. Never delete a file you have not read.\n Apply the same curation to durable facts (not rules) under\n personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact\n in place rather than appending a second, contradictory copy of it.\n Store up to two explicit or transcript-derived insights per\n core/knowledge/public/hq-core/insights-spec.md when present, otherwise\n workspace/insights/.\n4. Close an active session journal fail-soft with\n bash .claude/skills/_shared/journal.sh close \"<project_dir>\" \"<one-line synthesis>\".\n Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with\n id, created_at, summary, files, and next_steps for backward compatibility.\n5. Update workspace/threads/recent.md and regenerate\n workspace/threads/INDEX.md. For each company whose knowledge path appears\n in files_touched, regenerate companies/<company>/knowledge/INDEX.md under\n core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation\n is allowed for those companies, but knowledge/policy content writes remain\n restricted to the payload's named company.\n6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;\n skip silently on any failure. Hook or automation improvements go ONLY under\n personal/hooks/ as proposals.\n7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.\n8. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), and what you skipped and why. List every policy or knowledge file\n you amended, superseded, merged or deleted with the reason, so a human can\n audit and reverse it; a deletion you do not name in the report is a defect.\n Then drain the queue: while\n workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim\n it atomically by renaming it aside \u2014\n mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl\n (N counting up from 2) \u2014 and process the claimed payloads with this same\n flow. Repeat until a claim finds nothing left, then update the report.\n NEVER read the queue and truncate it in place: a payload appended between\n your read and the truncate is lost, and its checkpoint is never enriched.\n";
|
|
15
17
|
export declare function renderSiblingPrompt(runDir: string, payloadPath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Preference order for `--backend auto`: the caller's own backend first, so a
|
|
20
|
+
* claude session keeps using claude and a codex session keeps using codex
|
|
21
|
+
* (same-engine continuity), then the fallback priority claude -> codex -> grok
|
|
22
|
+
* for whatever the caller is not. Deterministic and env-only, so it is unit
|
|
23
|
+
* tested directly.
|
|
24
|
+
*/
|
|
25
|
+
export declare function autoBackendPreference(): SpawnableBackend[];
|
|
26
|
+
export declare function siblingArgs(backend: SpawnableBackend, prompt: string): string[];
|
|
16
27
|
/** Attach the native checkpoint command to the hidden `hq core` group. */
|
|
17
28
|
export declare function registerCoreCheckpointCommand(core: Command): void;
|
|
29
|
+
export {};
|
|
18
30
|
//# sourceMappingURL=core-checkpoint.d.ts.map
|
|
@@ -26,8 +26,12 @@ const GROK_SIBLING_EFFORT = "high";
|
|
|
26
26
|
* plan and exits, having done nothing. The maintenance flow needs many turns.
|
|
27
27
|
*/
|
|
28
28
|
const GROK_SIBLING_MAX_TURNS = "100";
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Fallback priority for `--backend auto` once the caller's own backend is
|
|
31
|
+
* unavailable, unresponsive, out of credits (surfaces as a crashed run), or
|
|
32
|
+
* simply unknown. First healthy candidate wins.
|
|
33
|
+
*/
|
|
34
|
+
const FALLBACK_BACKEND_ORDER = ["claude", "codex", "grok"];
|
|
31
35
|
/** A backend that cannot answer `--version` this fast is treated as broken. */
|
|
32
36
|
const BACKEND_PROBE_TIMEOUT_MS = 10_000;
|
|
33
37
|
/** A sibling still holding the lock after this long is treated as abandoned. */
|
|
@@ -492,6 +496,32 @@ function crashedBackends(siblingRoot) {
|
|
|
492
496
|
}
|
|
493
497
|
return crashed;
|
|
494
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* The backend of the agent that invoked this checkpoint, when known.
|
|
501
|
+
*
|
|
502
|
+
* Reuses the same runtime signal the Stop gate reads (`HQ_CHECKPOINT_RUNTIME`,
|
|
503
|
+
* set to `codex` by the codex hook adapter and defaulted to `claude` by the
|
|
504
|
+
* claude Stop hook). Grok runs no HQ hooks, so it never auto-invokes a
|
|
505
|
+
* checkpoint and has no reliable caller signal — an `other`/unknown runtime
|
|
506
|
+
* returns null and falls through to the fallback order.
|
|
507
|
+
*/
|
|
508
|
+
function callerBackend() {
|
|
509
|
+
const runtime = checkpointGateRuntime();
|
|
510
|
+
return runtime === "claude" || runtime === "codex" ? runtime : null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Preference order for `--backend auto`: the caller's own backend first, so a
|
|
514
|
+
* claude session keeps using claude and a codex session keeps using codex
|
|
515
|
+
* (same-engine continuity), then the fallback priority claude -> codex -> grok
|
|
516
|
+
* for whatever the caller is not. Deterministic and env-only, so it is unit
|
|
517
|
+
* tested directly.
|
|
518
|
+
*/
|
|
519
|
+
export function autoBackendPreference() {
|
|
520
|
+
const caller = callerBackend();
|
|
521
|
+
if (!caller)
|
|
522
|
+
return [...FALLBACK_BACKEND_ORDER];
|
|
523
|
+
return [caller, ...FALLBACK_BACKEND_ORDER.filter((name) => name !== caller)];
|
|
524
|
+
}
|
|
495
525
|
function resolveBackend(requested, liveRoot) {
|
|
496
526
|
const value = requested ?? "auto";
|
|
497
527
|
if (!BACKENDS.has(value))
|
|
@@ -499,7 +529,7 @@ function resolveBackend(requested, liveRoot) {
|
|
|
499
529
|
// An explicitly named backend is honoured as given; only `auto` shops around.
|
|
500
530
|
if (value !== "auto")
|
|
501
531
|
return value;
|
|
502
|
-
const available =
|
|
532
|
+
const available = autoBackendPreference().filter((name) => backendOnPath(name) && backendResponds(name));
|
|
503
533
|
if (available.length === 0)
|
|
504
534
|
return "none";
|
|
505
535
|
const crashed = crashedBackends(path.join(liveRoot, "workspace", "checkpoints", "sibling"));
|
|
@@ -507,7 +537,7 @@ function resolveBackend(requested, liveRoot) {
|
|
|
507
537
|
// failure is a weaker signal than running no maintenance sibling at all.
|
|
508
538
|
return available.find((name) => !crashed.has(name)) ?? available[0];
|
|
509
539
|
}
|
|
510
|
-
function siblingArgs(backend, prompt) {
|
|
540
|
+
export function siblingArgs(backend, prompt) {
|
|
511
541
|
switch (backend) {
|
|
512
542
|
case "claude":
|
|
513
543
|
return [
|
|
@@ -517,8 +547,16 @@ function siblingArgs(backend, prompt) {
|
|
|
517
547
|
CLAUDE_SIBLING_MODEL,
|
|
518
548
|
"--effort",
|
|
519
549
|
CLAUDE_SIBLING_EFFORT,
|
|
550
|
+
// `acceptEdits` auto-approves edits only. At the HQ root the scaffold
|
|
551
|
+
// settings make Bash "ask", so a terminal command — the git scan, the
|
|
552
|
+
// journal helper, the queue-draining `mv` — hits an approval gate with
|
|
553
|
+
// no human attached; headless `claude -p` then denies it and the run
|
|
554
|
+
// stalls having written no report. `bypassPermissions` is the same
|
|
555
|
+
// unattended posture codex gets from `-s workspace-write` and grok from
|
|
556
|
+
// `--always-approve --sandbox workspace`. Hooks still fire natively —
|
|
557
|
+
// this only removes the interactive approval gate, not the hook chain.
|
|
520
558
|
"--permission-mode",
|
|
521
|
-
"
|
|
559
|
+
"bypassPermissions",
|
|
522
560
|
];
|
|
523
561
|
case "grok":
|
|
524
562
|
return [
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StdioOptions } from 'node:child_process';
|
|
1
2
|
import { type QmdProcessResult, type RunQmdOptions } from './index.js';
|
|
2
3
|
export type BackgroundResult = {
|
|
3
4
|
state: 'skipped-agent' | 'skipped' | 'quiet' | 'busy' | 'completed' | 'update-failed' | 'terminated';
|
|
@@ -17,6 +18,7 @@ export type BackgroundDependencies = {
|
|
|
17
18
|
runQmd: (args: string[], options?: RunQmdOptions) => QmdProcessResult;
|
|
18
19
|
spawnWorker: (options: {
|
|
19
20
|
logPath: string;
|
|
21
|
+
fallbackLogPath?: string;
|
|
20
22
|
}) => number;
|
|
21
23
|
/** Test seam for simulating a competing owner replacing the atomic record. */
|
|
22
24
|
afterOwnerPublish?: (ownerFile: string) => void;
|
|
@@ -28,6 +30,60 @@ export type BackgroundStatus = {
|
|
|
28
30
|
lock: 'held' | 'stale' | 'free';
|
|
29
31
|
completedAt?: number;
|
|
30
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Injectable seams for the launcher's worker-log open and detached spawn. Split
|
|
35
|
+
* out purely as a test seam so a synthetic open failure can be forced without
|
|
36
|
+
* touching real files or the invoking uid.
|
|
37
|
+
*/
|
|
38
|
+
export type SpawnWorkerIo = {
|
|
39
|
+
mkdirSync: (directory: string) => void;
|
|
40
|
+
openSync: (file: string) => number;
|
|
41
|
+
closeSync: (fd: number) => void;
|
|
42
|
+
spawn: (command: string, args: string[], options: {
|
|
43
|
+
detached: boolean;
|
|
44
|
+
stdio: StdioOptions;
|
|
45
|
+
}) => {
|
|
46
|
+
pid?: number;
|
|
47
|
+
unref: () => void;
|
|
48
|
+
};
|
|
49
|
+
/** Report a degraded open without swallowing it (stderr notice + breadcrumb). */
|
|
50
|
+
report: (info: WorkerLogDegradation) => void;
|
|
51
|
+
};
|
|
52
|
+
export type WorkerLogDegradation = {
|
|
53
|
+
requested: string;
|
|
54
|
+
used: string | null;
|
|
55
|
+
usedFallback: boolean;
|
|
56
|
+
code?: string;
|
|
57
|
+
syscall?: string;
|
|
58
|
+
};
|
|
59
|
+
type WorkerLogOpen = {
|
|
60
|
+
fd: number | null;
|
|
61
|
+
logPath: string | null;
|
|
62
|
+
usedFallback: boolean;
|
|
63
|
+
error?: {
|
|
64
|
+
code?: string;
|
|
65
|
+
syscall?: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Open the detached worker's log, degrading instead of crashing. Try the
|
|
70
|
+
* requested path; on ANY open failure retry once against a per-user fallback
|
|
71
|
+
* under the caller's own $HOME; if that also fails, return a no-log result so
|
|
72
|
+
* the launcher still spawns the worker.
|
|
73
|
+
*
|
|
74
|
+
* This is the fix for Sentry indigo-d0/hq-cli 7663380187: HQ's /handoff always
|
|
75
|
+
* points --log at the fixed, shared, world-writable /tmp/qmd-handoff.log, and an
|
|
76
|
+
* unguarded fs.openSync(logPath, 'a') here threw EACCES whenever that file
|
|
77
|
+
* already existed owned by another uid, so the reindex worker never spawned and
|
|
78
|
+
* the raw errno reached Sentry. A diagnostic side-channel must not take down the
|
|
79
|
+
* feature it exists to observe — every other worker-log writer in this module
|
|
80
|
+
* (appendWorkerLog, capWorkerLog) already swallows I/O failures the same way.
|
|
81
|
+
*/
|
|
82
|
+
export declare function openWorkerLog(logPath: string, fallbackLogPath: string | undefined, io: Pick<SpawnWorkerIo, 'mkdirSync' | 'openSync'>): WorkerLogOpen;
|
|
83
|
+
export declare function defaultSpawnWorker({ logPath, fallbackLogPath }: {
|
|
84
|
+
logPath: string;
|
|
85
|
+
fallbackLogPath?: string;
|
|
86
|
+
}, io?: SpawnWorkerIo): number;
|
|
31
87
|
/** Defaults used by the CLI; tests supply every nondeterministic dependency. */
|
|
32
88
|
export declare function defaultBackgroundDependencies(hqRoot: string): BackgroundDependencies;
|
|
33
89
|
/** Match the shell forwarder's hosted-agent markers before looking up qmd. */
|
|
@@ -39,4 +95,5 @@ export declare function runBackgroundLauncher(dependencies: BackgroundDependenci
|
|
|
39
95
|
export declare function runBackgroundWorker(dependencies: BackgroundDependencies): Promise<BackgroundResult>;
|
|
40
96
|
/** Report the background lock and latest successful completion for `hq index status`. */
|
|
41
97
|
export declare function backgroundStatus(dependencies: BackgroundDependencies): BackgroundStatus;
|
|
98
|
+
export {};
|
|
42
99
|
//# sourceMappingURL=background.d.ts.map
|
|
@@ -1,20 +1,99 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { Sentry } from '../../sentry.js';
|
|
4
5
|
import { reconcileCollections as defaultReconcileCollections, resolveQmdBin as defaultResolveQmdBin, runQmd as defaultRunQmd, } from './index.js';
|
|
5
6
|
const LOCK_NAME = 'qmd-reindex-bg.lock';
|
|
6
7
|
const COMPLETE_NAME = 'qmd-reindex-bg.completed';
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function errnoInfo(error) {
|
|
9
|
+
const e = error;
|
|
10
|
+
return { code: e?.code, syscall: e?.syscall };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Open the detached worker's log, degrading instead of crashing. Try the
|
|
14
|
+
* requested path; on ANY open failure retry once against a per-user fallback
|
|
15
|
+
* under the caller's own $HOME; if that also fails, return a no-log result so
|
|
16
|
+
* the launcher still spawns the worker.
|
|
17
|
+
*
|
|
18
|
+
* This is the fix for Sentry indigo-d0/hq-cli 7663380187: HQ's /handoff always
|
|
19
|
+
* points --log at the fixed, shared, world-writable /tmp/qmd-handoff.log, and an
|
|
20
|
+
* unguarded fs.openSync(logPath, 'a') here threw EACCES whenever that file
|
|
21
|
+
* already existed owned by another uid, so the reindex worker never spawned and
|
|
22
|
+
* the raw errno reached Sentry. A diagnostic side-channel must not take down the
|
|
23
|
+
* feature it exists to observe — every other worker-log writer in this module
|
|
24
|
+
* (appendWorkerLog, capWorkerLog) already swallows I/O failures the same way.
|
|
25
|
+
*/
|
|
26
|
+
export function openWorkerLog(logPath, fallbackLogPath, io) {
|
|
27
|
+
try {
|
|
28
|
+
io.mkdirSync(path.dirname(logPath));
|
|
29
|
+
return { fd: io.openSync(logPath), logPath, usedFallback: false };
|
|
30
|
+
}
|
|
31
|
+
catch (primaryError) {
|
|
32
|
+
const error = errnoInfo(primaryError);
|
|
33
|
+
if (fallbackLogPath && fallbackLogPath !== logPath) {
|
|
34
|
+
try {
|
|
35
|
+
io.mkdirSync(path.dirname(fallbackLogPath));
|
|
36
|
+
return { fd: io.openSync(fallbackLogPath), logPath: fallbackLogPath, usedFallback: true, error };
|
|
37
|
+
}
|
|
38
|
+
catch { /* fall through to the no-log result below */ }
|
|
39
|
+
}
|
|
40
|
+
return { fd: null, logPath: null, usedFallback: false, error };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Surface a degraded worker-log open without swallowing it: one best-effort
|
|
45
|
+
* stderr line naming the rejected path and where output went, plus a Sentry
|
|
46
|
+
* breadcrumb carrying only the errno — never the fallback path, which lives
|
|
47
|
+
* under $HOME. The condition is now handled and degraded, so it is deliberately
|
|
48
|
+
* NOT captured as an exception; the breadcrumb just gives the next genuine
|
|
49
|
+
* failure at this site the errno evidence this event lacked.
|
|
50
|
+
*/
|
|
51
|
+
function reportWorkerLogDegradation(info) {
|
|
52
|
+
const reason = info.code ? ` (${info.code}${info.syscall ? ` on ${info.syscall}` : ''})` : '';
|
|
53
|
+
const destination = info.used ? `writing worker output to ${info.used} instead` : 'disabling worker output';
|
|
54
|
+
try {
|
|
55
|
+
process.stderr.write(`hq: cannot open background reindex log ${info.requested}${reason}; ${destination}.\n`);
|
|
56
|
+
}
|
|
57
|
+
catch { /* the notice itself is best-effort and must never crash the launcher */ }
|
|
58
|
+
try {
|
|
59
|
+
Sentry.addBreadcrumb({
|
|
60
|
+
category: 'qmd.background',
|
|
61
|
+
level: 'warning',
|
|
62
|
+
message: 'worker log open degraded',
|
|
63
|
+
data: { code: info.code, syscall: info.syscall, usedFallback: info.usedFallback },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch { /* breadcrumb is best-effort; no Sentry client is active in tests */ }
|
|
67
|
+
}
|
|
68
|
+
const defaultSpawnWorkerIo = {
|
|
69
|
+
mkdirSync: (directory) => { fs.mkdirSync(directory, { recursive: true }); },
|
|
70
|
+
openSync: (file) => fs.openSync(file, 'a'),
|
|
71
|
+
closeSync: (fd) => { fs.closeSync(fd); },
|
|
72
|
+
spawn: (command, args, options) => spawn(command, args, options),
|
|
73
|
+
report: reportWorkerLogDegradation,
|
|
74
|
+
};
|
|
75
|
+
export function defaultSpawnWorker({ logPath, fallbackLogPath }, io = defaultSpawnWorkerIo) {
|
|
76
|
+
const opened = openWorkerLog(logPath, fallbackLogPath, io);
|
|
77
|
+
if (opened.error) {
|
|
78
|
+
io.report({ requested: logPath, used: opened.logPath, usedFallback: opened.usedFallback, ...opened.error });
|
|
79
|
+
}
|
|
10
80
|
const entry = process.argv[1];
|
|
11
81
|
if (!entry)
|
|
12
82
|
throw new Error('Cannot determine hq CLI entrypoint for background worker');
|
|
13
|
-
|
|
83
|
+
// Hand the child the log path we actually opened so its own appendWorkerLog /
|
|
84
|
+
// capWorkerLog write to the same file. When nothing could be opened, omit
|
|
85
|
+
// --log entirely so the child resolves its own default rather than re-failing
|
|
86
|
+
// on the path we already rejected — the reindex itself must still run.
|
|
87
|
+
const logArgs = opened.logPath ? ['--log', opened.logPath] : [];
|
|
88
|
+
const stdio = opened.fd === null
|
|
89
|
+
? ['ignore', 'ignore', 'ignore']
|
|
90
|
+
: ['ignore', opened.fd, opened.fd];
|
|
91
|
+
const child = io.spawn(process.execPath, [entry, 'index', 'background', '--worker', ...logArgs], {
|
|
14
92
|
detached: true,
|
|
15
|
-
stdio
|
|
93
|
+
stdio,
|
|
16
94
|
});
|
|
17
|
-
|
|
95
|
+
if (opened.fd !== null)
|
|
96
|
+
io.closeSync(opened.fd);
|
|
18
97
|
child.unref();
|
|
19
98
|
if (!child.pid)
|
|
20
99
|
throw new Error('Unable to start qmd background worker');
|
|
@@ -358,7 +437,15 @@ export function runBackgroundLauncher(dependencies) {
|
|
|
358
437
|
catch {
|
|
359
438
|
return { state: 'skipped' };
|
|
360
439
|
}
|
|
361
|
-
|
|
440
|
+
// Fall back to a per-user log under the caller's own $HOME when the requested
|
|
441
|
+
// (often the shared, world-writable /tmp) log cannot be opened — see
|
|
442
|
+
// openWorkerLog. `home` is validated non-empty above, so the fallback is
|
|
443
|
+
// always inside a directory this user owns.
|
|
444
|
+
const fallbackLogPath = path.join(home, '.hq', 'logs', 'qmd-handoff.log');
|
|
445
|
+
return {
|
|
446
|
+
state: 'launched',
|
|
447
|
+
pid: dependencies.spawnWorker({ logPath: workerLogPath(dependencies.env), fallbackLogPath }),
|
|
448
|
+
};
|
|
362
449
|
}
|
|
363
450
|
/** Run the single-flight cleanup → update → embed pipeline in a worker only. */
|
|
364
451
|
export async function runBackgroundWorker(dependencies) {
|