@indigoai-us/hq-cli 5.295.0 → 5.297.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 +12 -0
- package/dist/command-catalog.generated.d.ts +58 -0
- package/dist/command-catalog.generated.js +73 -0
- package/dist/commands/core-job-native.d.ts +7 -0
- package/dist/commands/core-job-native.js +20 -0
- package/dist/commands/core-job-probe-native.d.ts +45 -0
- package/dist/commands/core-job-probe-native.js +564 -0
- package/dist/commands/core-job-remote-launcher.d.ts +7 -0
- package/dist/commands/core-job-remote-launcher.js +51 -0
- package/dist/commands/core-job-run-native.d.ts +47 -0
- package/dist/commands/core-job-run-native.js +1215 -0
- package/dist/commands/core.js +1 -1
- package/dist/commands/lanes-message.js +1 -0
- package/dist/commands/lanes-resume.js +1 -0
- package/dist/commands/lanes-stories.d.ts +1 -0
- package/dist/commands/lanes-stories.js +40 -1
- package/dist/commands/lanes.js +20 -2
- package/dist/commands/mesh.js +3 -1
- package/dist/commands/run.js +5 -0
- package/dist/lib/daemon/handoff.d.ts +1 -0
- package/dist/lib/daemon/handoff.js +18 -2
- package/dist/lib/daemon/legacy.d.ts +7 -0
- package/dist/lib/daemon/legacy.js +6 -0
- package/dist/lib/daemon/lock.js +1 -1
- package/dist/lib/daemon/run.js +57 -2
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +26 -0
- package/dist/lib/doctor/checks/work-context.js +5 -1
- package/dist/lib/lanes/mesh-events.js +3 -0
- package/dist/lib/lanes/reconcile.js +23 -19
- package/dist/lib/lanes/spawn.js +39 -0
- package/dist/lib/lanes/store.js +32 -0
- package/dist/lib/lanes/stories.d.ts +7 -1
- package/dist/lib/lanes/stories.js +16 -0
- package/dist/lib/lanes/types.d.ts +12 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +20 -22
- package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/doctor.js +10 -0
- package/dist/lib/work-context/outbox-quarantine-status-flag.d.ts +4 -0
- package/dist/lib/work-context/outbox-quarantine-status-flag.js +7 -0
- package/dist/lib/work-context/outbox.d.ts +1 -0
- package/dist/lib/work-context/outbox.js +7 -1
- package/dist/run/env-graph-guard.d.ts +2 -2
- package/dist/run/env-graph-guard.js +5 -2
- package/dist/run/hq-plugin.js +6 -5
- package/package.json +2 -2
package/dist/commands/core.js
CHANGED
|
@@ -36,7 +36,7 @@ import { registerCoreHandoffCommand } from "./core-handoff.js";
|
|
|
36
36
|
import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
37
37
|
import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
|
|
38
38
|
import { registerCorePolicyCommand } from "./core-policy.js";
|
|
39
|
-
import { registerCoreJobsNativeCommand } from "./core-
|
|
39
|
+
import { registerCoreJobsNativeCommand } from "./core-job-native.js";
|
|
40
40
|
import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
|
|
41
41
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
42
42
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
@@ -530,6 +530,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
530
530
|
pending_resume: {
|
|
531
531
|
session_id: sessionId,
|
|
532
532
|
trigger: "message",
|
|
533
|
+
caller: ctx.env.HQ_LANE_ID?.trim() || "user",
|
|
533
534
|
text: deliveryText,
|
|
534
535
|
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
535
536
|
message_id: messageId,
|
|
@@ -359,6 +359,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
359
359
|
pending_resume: {
|
|
360
360
|
session_id: sessionId,
|
|
361
361
|
trigger: "resume",
|
|
362
|
+
caller: ctx.env.HQ_LANE_ID?.trim() || "user",
|
|
362
363
|
resumed_from: resumedFrom,
|
|
363
364
|
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
364
365
|
...(failedResumeExpectedSenior !== undefined
|
|
@@ -8,6 +8,7 @@ export declare function createLaneStoryHandlers(deps: LaneStoryHandlerDeps): {
|
|
|
8
8
|
runStoryList: (ctx: Ctx, laneId: string) => Promise<void>;
|
|
9
9
|
runStoryAdd: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
|
|
10
10
|
runStoryStart: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
|
|
11
|
+
runStoryReview: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
|
|
11
12
|
runStoryBlock: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
|
|
12
13
|
runStoryDone: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
|
|
13
14
|
};
|
|
@@ -18,6 +18,13 @@ export function createLaneStoryHandlers(deps) {
|
|
|
18
18
|
}
|
|
19
19
|
return { projectId, storyId: normalizedStoryId, story };
|
|
20
20
|
}
|
|
21
|
+
function reviewableStoryTarget(lane, storyId, project) {
|
|
22
|
+
const target = storyCommandTarget(lane, storyId, project);
|
|
23
|
+
if (target.story.status === "done") {
|
|
24
|
+
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be moved to review`, { lane_id: lane.lane_id, project_id: target.projectId, story_id: target.storyId });
|
|
25
|
+
}
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
21
28
|
function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
|
|
22
29
|
const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
|
|
23
30
|
? {
|
|
@@ -229,6 +236,38 @@ export function createLaneStoryHandlers(deps) {
|
|
|
229
236
|
const finalLane = readLane(hqRoot, laneId);
|
|
230
237
|
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
231
238
|
}
|
|
239
|
+
async function runStoryReview(ctx, laneId, storyId, opts) {
|
|
240
|
+
const hqRoot = ctx.hqRoot();
|
|
241
|
+
let lane = readLane(hqRoot, laneId);
|
|
242
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
243
|
+
const target = reviewableStoryTarget(lane, storyId, opts.project);
|
|
244
|
+
const at = ctx.now().toISOString();
|
|
245
|
+
const warnings = [];
|
|
246
|
+
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
247
|
+
lane = thread.lane;
|
|
248
|
+
if (thread.warning !== undefined)
|
|
249
|
+
warnings.push(thread.warning);
|
|
250
|
+
const before = lane;
|
|
251
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
252
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
253
|
+
reviewableStoryTarget(current, target.storyId, target.projectId);
|
|
254
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "review", at);
|
|
255
|
+
});
|
|
256
|
+
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_progress", "review", { detail: "Story moved to review" });
|
|
257
|
+
if (transitionWarning !== undefined)
|
|
258
|
+
warnings.push(transitionWarning);
|
|
259
|
+
const finalLane = readLane(hqRoot, laneId);
|
|
260
|
+
printJson({
|
|
261
|
+
ok: true,
|
|
262
|
+
action: "story.review",
|
|
263
|
+
lane_id: laneId,
|
|
264
|
+
project_id: target.projectId,
|
|
265
|
+
story_id: target.storyId,
|
|
266
|
+
status: "review",
|
|
267
|
+
story_warning: combinedStoryWarning(finalLane, warnings),
|
|
268
|
+
lane: finalLane,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
232
271
|
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
233
272
|
const hqRoot = ctx.hqRoot();
|
|
234
273
|
let lane = readLane(hqRoot, laneId);
|
|
@@ -303,6 +342,6 @@ export function createLaneStoryHandlers(deps) {
|
|
|
303
342
|
const finalLane = readLane(hqRoot, laneId);
|
|
304
343
|
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
305
344
|
}
|
|
306
|
-
return { runStoryList, runStoryAdd, runStoryStart, runStoryBlock, runStoryDone };
|
|
345
|
+
return { runStoryList, runStoryAdd, runStoryStart, runStoryReview, runStoryBlock, runStoryDone };
|
|
307
346
|
}
|
|
308
347
|
//# sourceMappingURL=lanes-stories.js.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -5062,10 +5062,12 @@ async function runShow(ctx, laneId) {
|
|
|
5062
5062
|
})
|
|
5063
5063
|
.slice(0, 20)
|
|
5064
5064
|
.map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
|
|
5065
|
+
const laneForShow = { ...lane };
|
|
5066
|
+
delete laneForShow.resume_history;
|
|
5065
5067
|
printJson({
|
|
5066
5068
|
ok: true,
|
|
5067
5069
|
action: "show",
|
|
5068
|
-
lane,
|
|
5070
|
+
lane: laneForShow,
|
|
5069
5071
|
...(laneIsSenior ? { reviews: recentReviews, reviews_skipped: reviewListing.skipped } : {}),
|
|
5070
5072
|
...hire,
|
|
5071
5073
|
overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, ctx.now()),
|
|
@@ -5079,6 +5081,8 @@ async function runShow(ctx, laneId) {
|
|
|
5079
5081
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
5080
5082
|
envelope_validation_errors: lane.envelope_validation_errors ?? [],
|
|
5081
5083
|
last_resume: lane.last_resume ?? null,
|
|
5084
|
+
// Public JSON key for the bounded start audit; `last_resume` remains unchanged.
|
|
5085
|
+
resume_history: lane.resume_history ?? [],
|
|
5082
5086
|
keep_alive_history: lane.keep_alive_history ?? [],
|
|
5083
5087
|
account: lane.account ?? null,
|
|
5084
5088
|
account_history: lane.account_history ?? [],
|
|
@@ -6191,7 +6195,12 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
6191
6195
|
}),
|
|
6192
6196
|
pending_resume: input.deferTurnStart
|
|
6193
6197
|
? undefined
|
|
6194
|
-
: {
|
|
6198
|
+
: {
|
|
6199
|
+
session_id: sessionId,
|
|
6200
|
+
at,
|
|
6201
|
+
caller: ctx.env.HQ_LANE_ID?.trim() || "user",
|
|
6202
|
+
...input.pending,
|
|
6203
|
+
},
|
|
6195
6204
|
};
|
|
6196
6205
|
applyResumeProfileAndEffort(current, next, {
|
|
6197
6206
|
...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
|
|
@@ -8171,6 +8180,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8171
8180
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8172
8181
|
await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
|
|
8173
8182
|
}));
|
|
8183
|
+
stories
|
|
8184
|
+
.command("review")
|
|
8185
|
+
.description("Move a carried story to review")
|
|
8186
|
+
.argument("<lane-id>", "Lane id")
|
|
8187
|
+
.argument("<story-id>", "Story id")
|
|
8188
|
+
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8189
|
+
.action(wrap(async (laneId, storyId, opts) => {
|
|
8190
|
+
await storyHandlers.runStoryReview(ctx, laneId, storyId, opts);
|
|
8191
|
+
}));
|
|
8174
8192
|
stories
|
|
8175
8193
|
.command("block")
|
|
8176
8194
|
.description("Mark one carried story blocked")
|
package/dist/commands/mesh.js
CHANGED
|
@@ -2129,7 +2129,9 @@ async function runDaemonInstall(opts) {
|
|
|
2129
2129
|
// `hq daemon` runs mesh on this machine: installing a second unit would run it twice.
|
|
2130
2130
|
// An explicit --hq-root is verified the same way as below and saved for the daemon.
|
|
2131
2131
|
const explicitRoot = opts.hqRoot ? resolveInstallHqRoot({ explicit: opts.hqRoot }).root : undefined;
|
|
2132
|
-
const handedOff = opts.systemUser
|
|
2132
|
+
const handedOff = opts.systemUser
|
|
2133
|
+
? null
|
|
2134
|
+
: handOffInstallHere(["mesh"], process.env, { hqRoot: explicitRoot, ifStale: opts.ifStale });
|
|
2133
2135
|
if (handedOff) {
|
|
2134
2136
|
console.log(opts.json ? JSON.stringify({ ok: true, action: "hq-daemon", reinstalled: false, message: handedOff }) : handedOff);
|
|
2135
2137
|
return;
|
package/dist/commands/run.js
CHANGED
|
@@ -128,6 +128,11 @@ export function registerRunCommand(program) {
|
|
|
128
128
|
let state;
|
|
129
129
|
const graph = await internal.loadEnvGraph({
|
|
130
130
|
entryFilePaths: paths,
|
|
131
|
+
// hq run owns its short-lived in-memory resolution. Varlock's default
|
|
132
|
+
// cache auto-detection can write ~/.config/varlock and initialize
|
|
133
|
+
// the native local-encryption helper, neither of which belongs on
|
|
134
|
+
// this command path.
|
|
135
|
+
skipCache: true,
|
|
131
136
|
afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
|
|
132
137
|
});
|
|
133
138
|
// Refuse to resolve a graph varlock never finished loading (a malformed
|
|
@@ -51,5 +51,6 @@ export declare function handOffInstall(opts: {
|
|
|
51
51
|
/** handOffInstall for this machine and user, for the older install commands. */
|
|
52
52
|
export declare function handOffInstallHere(services: readonly ServiceName[], env?: NodeJS.ProcessEnv, opts?: {
|
|
53
53
|
hqRoot?: string;
|
|
54
|
+
ifStale?: boolean;
|
|
54
55
|
}): string | null;
|
|
55
56
|
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -17,6 +17,15 @@ import { daemonPaths } from "./paths.js";
|
|
|
17
17
|
import { writeControlRequest } from "./requests.js";
|
|
18
18
|
/** Statuses where the daemon itself owns the unit's process. */
|
|
19
19
|
const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped"]);
|
|
20
|
+
const RECENT_HOSTED_DAEMON_MS = 5 * 60_000;
|
|
21
|
+
/** The updater's postinstall may run in the short gap before the desktop app relaunches its daemon. */
|
|
22
|
+
function recentlyHostedDaemon(paths) {
|
|
23
|
+
if (!fs.existsSync(paths.statePath))
|
|
24
|
+
return false;
|
|
25
|
+
const state = JSON.parse(fs.readFileSync(paths.statePath, "utf8"));
|
|
26
|
+
const age = Date.now() - (state.updatedAt ?? 0);
|
|
27
|
+
return state.host === "desktop" && age >= 0 && age <= RECENT_HOSTED_DAEMON_MS;
|
|
28
|
+
}
|
|
20
29
|
/** The live daemon's state, or null when no daemon is running. */
|
|
21
30
|
function liveState(paths, alive) {
|
|
22
31
|
if (runningDaemonPid(paths.lockPath, alive) === null)
|
|
@@ -124,14 +133,21 @@ export function handOffInstall(opts) {
|
|
|
124
133
|
export function handOffInstallHere(services, env = process.env, opts = {}) {
|
|
125
134
|
const home = os.homedir();
|
|
126
135
|
const paths = daemonPaths(home, env);
|
|
136
|
+
const config = readDaemonConfig(paths.configPath);
|
|
137
|
+
if (opts.ifStale && services.every((service) => config.disable.includes(service))) {
|
|
138
|
+
return `hq daemon has ${services.join(", ")} disabled, so the older service will not be installed.`;
|
|
139
|
+
}
|
|
127
140
|
const installed = daemonUnitInstalled({
|
|
128
141
|
platform: process.platform,
|
|
129
142
|
home,
|
|
130
143
|
windowsTaskExists: (task) => spawnSync("schtasks.exe", ["/Query", "/TN", task], { encoding: "utf8", windowsHide: true }).status === 0,
|
|
131
144
|
}) || hostedDaemonRunning(paths);
|
|
132
|
-
if (!installed)
|
|
145
|
+
if (!installed) {
|
|
146
|
+
if (opts.ifStale && services.includes("mesh") && recentlyHostedDaemon(paths)) {
|
|
147
|
+
return `hq daemon recently hosted mesh on this machine, so the older mesh service will not be installed during its restart.`;
|
|
148
|
+
}
|
|
133
149
|
return null;
|
|
134
|
-
|
|
150
|
+
}
|
|
135
151
|
const machineType = detectMachineType(undefined, {
|
|
136
152
|
platform: process.platform,
|
|
137
153
|
env,
|
|
@@ -32,4 +32,11 @@ export declare function findLegacyUnits(deps: LegacyProbeDeps): LegacyUnit[];
|
|
|
32
32
|
* own checks, because `hq daemon migrate` will never take them over.
|
|
33
33
|
*/
|
|
34
34
|
export declare function legacyOwnerLookup(units: readonly LegacyUnit[]): (unitId: string) => string | null;
|
|
35
|
+
/** A daemon-managed macOS mesh service takes over the old LaunchAgent on startup. */
|
|
36
|
+
export declare function legacyMeshUnitsToRetire(units: readonly LegacyUnit[], opts: {
|
|
37
|
+
platform: NodeJS.Platform;
|
|
38
|
+
managed: boolean;
|
|
39
|
+
hosted: boolean;
|
|
40
|
+
meshEnabled: boolean;
|
|
41
|
+
}): LegacyUnit[];
|
|
35
42
|
//# sourceMappingURL=legacy.d.ts.map
|
|
@@ -122,4 +122,10 @@ export function legacyOwnerLookup(units) {
|
|
|
122
122
|
return hit ? describeLegacyUnit(hit) : null;
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
/** A daemon-managed macOS mesh service takes over the old LaunchAgent on startup. */
|
|
126
|
+
export function legacyMeshUnitsToRetire(units, opts) {
|
|
127
|
+
if (opts.platform !== "darwin" || (!opts.managed && !opts.hosted) || !opts.meshEnabled)
|
|
128
|
+
return [];
|
|
129
|
+
return units.filter((unit) => unit.service === "mesh" && unit.kind === "launchd" && unit.migratable);
|
|
130
|
+
}
|
|
125
131
|
//# sourceMappingURL=legacy.js.map
|
package/dist/lib/daemon/lock.js
CHANGED
|
@@ -43,7 +43,7 @@ export function processCommandLine(pid, deps = defaultCommandLineDeps()) {
|
|
|
43
43
|
"-Command",
|
|
44
44
|
`(Get-CimInstance Win32_Process -Filter "ProcessId=${Math.trunc(pid)}").CommandLine`,
|
|
45
45
|
])
|
|
46
|
-
: deps.run("ps", ["-o", "
|
|
46
|
+
: deps.run("ps", ["-ww", "-o", "args=", "-p", String(pid)]);
|
|
47
47
|
if (r.status !== 0)
|
|
48
48
|
return null;
|
|
49
49
|
const line = r.stdout.trim();
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -19,9 +19,12 @@ import { botsRoot } from "../bot/paths.js";
|
|
|
19
19
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
20
20
|
import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
|
|
21
21
|
import { SUPERVISOR_HANDOFF_FLAG, readDaemonFlag } from "./flag.js";
|
|
22
|
-
import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
|
|
22
|
+
import { findLegacyUnits, legacyMeshUnitsToRetire, legacyOwnerLookup } from "./legacy.js";
|
|
23
23
|
import { readLegacySyncEnv } from "./legacy-sync-env.js";
|
|
24
|
-
import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
|
|
24
|
+
import { acquireDaemonLock, isProcessAlive, processCommandLine, releaseDaemonLock } from "./lock.js";
|
|
25
|
+
import { adoptLegacyUnits } from "./migrate.js";
|
|
26
|
+
import { daemonDir as meshDaemonDir, daemonPidPath as meshDaemonPidPath } from "../mesh/live/daemon/paths.js";
|
|
27
|
+
import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
25
28
|
import { daemonRootRefusal } from "./install.js";
|
|
26
29
|
import { detectMachineType } from "./machine-type.js";
|
|
27
30
|
import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
|
|
@@ -332,6 +335,54 @@ export async function runDaemon(opts) {
|
|
|
332
335
|
}
|
|
333
336
|
return legacyCache.units;
|
|
334
337
|
};
|
|
338
|
+
const legacyMesh = legacyMeshUnitsToRetire(legacyUnits(), {
|
|
339
|
+
platform,
|
|
340
|
+
managed: opts.managed === true,
|
|
341
|
+
hosted: opts.host !== undefined,
|
|
342
|
+
meshEnabled: enabledServices(machineType, initial).includes("mesh"),
|
|
343
|
+
});
|
|
344
|
+
if (legacyMesh.length > 0) {
|
|
345
|
+
try {
|
|
346
|
+
const uid = process.getuid?.() ?? 0;
|
|
347
|
+
const result = adoptLegacyUnits(legacyMesh, {
|
|
348
|
+
run: (command, args) => {
|
|
349
|
+
const r = spawnSync(command, args, { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
350
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
351
|
+
},
|
|
352
|
+
uid,
|
|
353
|
+
isRoot: uid === 0,
|
|
354
|
+
adoptedDir: path.join(paths.root, "adopted"),
|
|
355
|
+
recordPath: path.join(paths.root, "adopted.json"),
|
|
356
|
+
systemAdoptedDir: "/var/lib/hq/daemon/adopted",
|
|
357
|
+
systemRecordPath: "/var/lib/hq/daemon/adopted.json",
|
|
358
|
+
systemUnitDir: "/etc/systemd/system",
|
|
359
|
+
});
|
|
360
|
+
for (const unit of result.adopted)
|
|
361
|
+
log("info", `retired legacy ${unit.unitId} unit ${unit.name}; hq daemon now owns mesh`);
|
|
362
|
+
for (const failure of result.failed)
|
|
363
|
+
log("warn", `could not retire legacy ${failure.unit.unitId} unit ${failure.unit.name}: ${failure.error}`);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
log("error", `could not retire legacy mesh units: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
legacyCache = null;
|
|
369
|
+
}
|
|
370
|
+
const meshOwnerReason = () => {
|
|
371
|
+
const lockPath = meshDaemonPidPath(meshDaemonDir(undefined, home, env));
|
|
372
|
+
const owner = readPidLock(lockPath);
|
|
373
|
+
if (!owner || owner.pid === process.pid || !isProcessAlive(owner.pid))
|
|
374
|
+
return undefined;
|
|
375
|
+
const command = processCommandLine(owner.pid, {
|
|
376
|
+
platform,
|
|
377
|
+
run: (file, args) => {
|
|
378
|
+
const r = spawnSync(file, args, { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
379
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
if (command && !/\bmesh\s+daemon\s+run\b/.test(command))
|
|
383
|
+
return undefined;
|
|
384
|
+
return `mesh daemon already running (pid ${owner.pid}); waiting for it to exit`;
|
|
385
|
+
};
|
|
335
386
|
let warnedMissingPin = null;
|
|
336
387
|
const daemonSyncRunnerPids = new Set();
|
|
337
388
|
const plan = () => {
|
|
@@ -554,6 +605,7 @@ export async function runDaemon(opts) {
|
|
|
554
605
|
return undefined;
|
|
555
606
|
}
|
|
556
607
|
},
|
|
608
|
+
meshOwnerReason,
|
|
557
609
|
cliVersion: CLI_VERSION,
|
|
558
610
|
adopt,
|
|
559
611
|
});
|
|
@@ -603,6 +655,9 @@ export async function runDaemon(opts) {
|
|
|
603
655
|
log("info", "all services stopped");
|
|
604
656
|
releaseDaemonLock(paths.lockPath);
|
|
605
657
|
resolve(exitCode);
|
|
658
|
+
// Pipes, sockets and unrelated timers can outlive a clean shutdown.
|
|
659
|
+
// Keep restart and signal exits bounded just like the handoff path.
|
|
660
|
+
setTimeout(() => process.exit(exitCode), 5_000).unref();
|
|
606
661
|
});
|
|
607
662
|
};
|
|
608
663
|
/** Exit for a restart but leave the services running for the next daemon to adopt. */
|
|
@@ -107,6 +107,8 @@ export interface SupervisorDeps {
|
|
|
107
107
|
requestPollMs?: number;
|
|
108
108
|
/** Read hq-cloud's lease status after it exits with the lease-conflict code. */
|
|
109
109
|
syncOwnerBlockReason?: () => string | undefined;
|
|
110
|
+
/** A separate mesh daemon currently owns its pid lock after our mesh child exits. */
|
|
111
|
+
meshOwnerReason?: () => string | undefined;
|
|
110
112
|
/** The running CLI's version; part of the key of units that run `hq` commands. */
|
|
111
113
|
cliVersion?: string;
|
|
112
114
|
/** Services to take over from the previous daemon instead of starting them. */
|
|
@@ -27,6 +27,7 @@ export const BACKOFF_BASE_MS = 1_000;
|
|
|
27
27
|
export const BACKOFF_MAX_MS = 60_000;
|
|
28
28
|
export const HEALTHY_RUN_MS = 60_000;
|
|
29
29
|
export const STOP_ESCALATION_MS = 10_000;
|
|
30
|
+
const MESH_OWNER_RECHECK_MS = 15_000;
|
|
30
31
|
/** After a handoff, how long a replaced unit must stay up before the next one is replaced. */
|
|
31
32
|
export const HANDOFF_STAGGER_MS = 15_000;
|
|
32
33
|
/** How long a draining unit may take to finish its work before it gets SIGTERM. */
|
|
@@ -294,6 +295,31 @@ export function createSupervisor(deps) {
|
|
|
294
295
|
emit();
|
|
295
296
|
return;
|
|
296
297
|
}
|
|
298
|
+
const meshOwnerReason = slot.plan.service === "mesh" ? deps.meshOwnerReason?.() : undefined;
|
|
299
|
+
if (meshOwnerReason) {
|
|
300
|
+
const waitForOwner = (reason) => {
|
|
301
|
+
slot.state.status = "waiting";
|
|
302
|
+
slot.state.reason = reason;
|
|
303
|
+
slot.state.nextRunAt = now() + MESH_OWNER_RECHECK_MS;
|
|
304
|
+
deps.log("warn", `${reason}; retrying mesh ownership check in ${MESH_OWNER_RECHECK_MS}ms`);
|
|
305
|
+
slot.timer = setT(() => {
|
|
306
|
+
slot.timer = undefined;
|
|
307
|
+
const current = deps.meshOwnerReason?.();
|
|
308
|
+
if (current)
|
|
309
|
+
waitForOwner(current);
|
|
310
|
+
else {
|
|
311
|
+
slot.attempts = 0;
|
|
312
|
+
delete slot.state.reason;
|
|
313
|
+
delete slot.state.nextRunAt;
|
|
314
|
+
startLong(slot);
|
|
315
|
+
}
|
|
316
|
+
emit();
|
|
317
|
+
}, MESH_OWNER_RECHECK_MS);
|
|
318
|
+
};
|
|
319
|
+
waitForOwner(meshOwnerReason);
|
|
320
|
+
emit();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
297
323
|
deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
|
|
298
324
|
if (ranFor >= HEALTHY_RUN_MS)
|
|
299
325
|
slot.attempts = 0;
|
|
@@ -8,6 +8,7 @@ import * as os from "node:os";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { readDeviceConfig, } from "../../work-context/config.js";
|
|
10
10
|
import { outboxStats, listOutboxOperations } from "../../work-context/outbox.js";
|
|
11
|
+
import { outboxQuarantineStatusEnabled } from "../../work-context/outbox-quarantine-status-flag.js";
|
|
11
12
|
import { workContextRoot, workContextSessionsDir } from "../../work-context/paths.js";
|
|
12
13
|
import { countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
13
14
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
|
|
@@ -87,11 +88,14 @@ function runChecks(deps) {
|
|
|
87
88
|
target: path.join(root, "outbox"),
|
|
88
89
|
message: `Outbox queued depth=${stats.depth} (acked=${stats.acked}).`,
|
|
89
90
|
});
|
|
91
|
+
const showQuarantineReason = stats.quarantined > 0 && outboxQuarantineStatusEnabled();
|
|
90
92
|
results.push({
|
|
91
93
|
status: stats.quarantined > 0 ? "WARN" : "PASS",
|
|
92
94
|
checkId: "work-context.outbox-quarantined",
|
|
93
95
|
target: path.join(root, "outbox"),
|
|
94
|
-
message: `Outbox quarantined count=${stats.quarantined}
|
|
96
|
+
message: `Outbox quarantined count=${stats.quarantined}${showQuarantineReason
|
|
97
|
+
? ` (last reason=${stats.lastQuarantineReason ?? "unknown"})`
|
|
98
|
+
: ""}.`,
|
|
95
99
|
});
|
|
96
100
|
// Default-company mode + migration capability (US-017B)
|
|
97
101
|
try {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import { appendThreadEvent, clamp, ensureLaneMeshThread, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
|
|
46
46
|
import { LanesError } from "./errors.js";
|
|
47
|
+
import { terminalEnvelopeStoryStatus } from "./stories.js";
|
|
47
48
|
export const LANE_MESH_EVENTS = [
|
|
48
49
|
"lane_started",
|
|
49
50
|
"lane_progress",
|
|
@@ -454,6 +455,8 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
454
455
|
const patchStoryStatus = async (status) => {
|
|
455
456
|
if (!lane.project_id?.trim() || !lane.story_id?.trim())
|
|
456
457
|
return;
|
|
458
|
+
if (terminalEnvelopeStoryStatus(lane, envelope.decision) === undefined)
|
|
459
|
+
return;
|
|
457
460
|
const suppressions = opts.storyStatusSuppressions;
|
|
458
461
|
const key = laneStoryStatusSuppressionKey(company, lane.project_id, lane.story_id);
|
|
459
462
|
if (suppressions?.has(key))
|
|
@@ -31,7 +31,7 @@ import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./qu
|
|
|
31
31
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
32
32
|
import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
33
33
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
|
|
34
|
-
import { activeLaneStories, laneStories,
|
|
34
|
+
import { activeLaneStories, laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
|
|
35
35
|
import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
|
|
36
36
|
import { PROVIDER_REJECTED_REASON, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
37
37
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
@@ -50,6 +50,7 @@ export const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
|
|
|
50
50
|
function spawnDepsForHost(deps) {
|
|
51
51
|
return {
|
|
52
52
|
...(deps.spawnLaneDeps ?? {}),
|
|
53
|
+
env: deps.spawnLaneDeps?.env ?? deps.env ?? process.env,
|
|
53
54
|
hostname: deps.host.hostname,
|
|
54
55
|
admissionHost: deps.host,
|
|
55
56
|
};
|
|
@@ -1270,24 +1271,22 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1270
1271
|
}
|
|
1271
1272
|
if (terminalEnvelope && latest) {
|
|
1272
1273
|
const envelope = terminalEnvelope;
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
?
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1290
|
-
: story);
|
|
1274
|
+
const storyStatus = terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
|
|
1275
|
+
if (storyStatus !== undefined) {
|
|
1276
|
+
next.story_status = envelope.story_status;
|
|
1277
|
+
next.story_complete = envelope.story_complete;
|
|
1278
|
+
next.story_complete_reason = envelope.story_complete_reason;
|
|
1279
|
+
next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
|
|
1280
|
+
? {
|
|
1281
|
+
...story,
|
|
1282
|
+
status: storyStatus,
|
|
1283
|
+
updated_at: nowIso,
|
|
1284
|
+
...(envelope.decision === "blocked"
|
|
1285
|
+
? { blocked_reason: envelope.blocked_reason ?? "" }
|
|
1286
|
+
: { blocked_reason: undefined }),
|
|
1287
|
+
}
|
|
1288
|
+
: story);
|
|
1289
|
+
}
|
|
1291
1290
|
if (approvalGate?.violation) {
|
|
1292
1291
|
next.last_envelope = {
|
|
1293
1292
|
ref: latest.ref,
|
|
@@ -2682,6 +2681,10 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
2682
2681
|
if (!deps.dryRun)
|
|
2683
2682
|
writeAnsweredQuestions(hqRoot, current, questions);
|
|
2684
2683
|
if (queueReason !== undefined) {
|
|
2684
|
+
const answerEnv = deps.spawnLaneDeps?.env ?? deps.env ?? process.env;
|
|
2685
|
+
const answerCaller = typeof answerEnv.HQ_LANE_ID === "string"
|
|
2686
|
+
? answerEnv.HQ_LANE_ID.trim() || "user"
|
|
2687
|
+
: "user";
|
|
2685
2688
|
const queued = casLaneIf(hqRoot, current.lane_id, (latest) => latest.state === "awaiting_input", (latest) => ({
|
|
2686
2689
|
...latest,
|
|
2687
2690
|
state: "queued",
|
|
@@ -2693,6 +2696,7 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
2693
2696
|
? { session_id: latest.provider_session_id }
|
|
2694
2697
|
: {}),
|
|
2695
2698
|
trigger: "answered_questions",
|
|
2699
|
+
caller: answerCaller,
|
|
2696
2700
|
at: answeredAt,
|
|
2697
2701
|
},
|
|
2698
2702
|
}), deps.dryRun);
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -2005,6 +2005,18 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2005
2005
|
// Claim running before spawning. Two callers that both observe queued must
|
|
2006
2006
|
// not both start processes; the CAS loser returns without spawn.
|
|
2007
2007
|
const nowIso = (deps.now ?? (() => new Date()))().toISOString();
|
|
2008
|
+
const resumeHistoryTrigger = pendingResume?.keep_alive === true
|
|
2009
|
+
? "keep_alive"
|
|
2010
|
+
: pendingResume?.trigger === "message" || trigger.kind === "message"
|
|
2011
|
+
? "message"
|
|
2012
|
+
: pendingResume?.trigger === "resume" || trigger.kind === "resume"
|
|
2013
|
+
? "resume_cli"
|
|
2014
|
+
: "reconcile";
|
|
2015
|
+
const resumeHistoryCaller = pendingResume?.caller ||
|
|
2016
|
+
(typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "") ||
|
|
2017
|
+
(trigger.kind === "answered_questions" || trigger.kind === "resume" || trigger.kind === "message"
|
|
2018
|
+
? "user"
|
|
2019
|
+
: undefined);
|
|
2008
2020
|
let statusGateHeartbeat;
|
|
2009
2021
|
try {
|
|
2010
2022
|
statusGateHeartbeat = managerStatusGateHeartbeat(hqRoot, existing, laneTurnId, nowIso);
|
|
@@ -2016,6 +2028,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2016
2028
|
statusGateHeartbeat = undefined;
|
|
2017
2029
|
}
|
|
2018
2030
|
let claimed = false;
|
|
2031
|
+
let priorStartRecorded = false;
|
|
2019
2032
|
const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
|
|
2020
2033
|
claimed = false;
|
|
2021
2034
|
if (current.state === "running" && current.account === resolved.id) {
|
|
@@ -2027,6 +2040,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2027
2040
|
if (!claimableStatesFor(trigger, current).includes(current.state)) {
|
|
2028
2041
|
throw new LanesError("spawn_state_mismatch", `Cannot mark ${current.lane_id} running from ${current.state}`, { lane_id: current.lane_id, state: current.state, trigger: trigger.kind });
|
|
2029
2042
|
}
|
|
2043
|
+
// The host report is written in the claim CAS before exec proves that a
|
|
2044
|
+
// worker started. Use the prior spawn manifest's turn id (written only
|
|
2045
|
+
// after detachment is proven), with last_resume for legacy manifests.
|
|
2046
|
+
priorStartRecorded = previousSpawn.turn_id !== undefined ||
|
|
2047
|
+
current.last_resume !== undefined ||
|
|
2048
|
+
(current.resume_history?.length ?? 0) > 0;
|
|
2030
2049
|
if (failedResumeClaim &&
|
|
2031
2050
|
(failedResumeExpectedSenior === undefined ||
|
|
2032
2051
|
!sameSeniorRef(current.senior, failedResumeExpectedSenior))) {
|
|
@@ -2260,6 +2279,16 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2260
2279
|
}
|
|
2261
2280
|
throw err;
|
|
2262
2281
|
}
|
|
2282
|
+
if (priorStartRecorded) {
|
|
2283
|
+
retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => ({
|
|
2284
|
+
...current,
|
|
2285
|
+
resume_history: appendCappedHistory(current.resume_history ?? [], {
|
|
2286
|
+
at: nowIso,
|
|
2287
|
+
trigger: resumeHistoryTrigger,
|
|
2288
|
+
...(resumeHistoryCaller ? { caller: resumeHistoryCaller } : {}),
|
|
2289
|
+
}),
|
|
2290
|
+
}));
|
|
2291
|
+
}
|
|
2263
2292
|
return laneLaunchResult(readLane(hqRoot, lane.lane_id));
|
|
2264
2293
|
}
|
|
2265
2294
|
/**
|
|
@@ -2563,6 +2592,16 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2563
2592
|
killQuietly(killProcess, workerPid, "worker");
|
|
2564
2593
|
throw err;
|
|
2565
2594
|
}
|
|
2595
|
+
const startedAt = (deps.now ?? (() => new Date()))().toISOString();
|
|
2596
|
+
const callerLaneId = typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "";
|
|
2597
|
+
retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => ({
|
|
2598
|
+
...current,
|
|
2599
|
+
resume_history: appendCappedHistory(current.resume_history ?? [], {
|
|
2600
|
+
at: startedAt,
|
|
2601
|
+
trigger: "envelope_repair",
|
|
2602
|
+
...(callerLaneId ? { caller: callerLaneId } : {}),
|
|
2603
|
+
}),
|
|
2604
|
+
}));
|
|
2566
2605
|
return workerPid;
|
|
2567
2606
|
}
|
|
2568
2607
|
export { PROVIDER_HOME_ENV, PROVIDER_COMMAND };
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -751,6 +751,10 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
751
751
|
path: filePath,
|
|
752
752
|
});
|
|
753
753
|
}
|
|
754
|
+
if (isPlainObject(raw.pending_resume) &&
|
|
755
|
+
raw.pending_resume.caller !== undefined) {
|
|
756
|
+
requireString(raw.pending_resume.caller, "pending_resume.caller");
|
|
757
|
+
}
|
|
754
758
|
if (raw.keep_alive_history !== undefined) {
|
|
755
759
|
if (!Array.isArray(raw.keep_alive_history) || raw.keep_alive_history.length > HISTORY_CAP) {
|
|
756
760
|
throw new LaneSchemaError(`keep_alive_history must be an array capped at ${HISTORY_CAP} entries`, {
|
|
@@ -773,6 +777,29 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
773
777
|
requireTimestamp(value.at, `${field}.at`, filePath);
|
|
774
778
|
}
|
|
775
779
|
}
|
|
780
|
+
if (raw.resume_history !== undefined) {
|
|
781
|
+
if (!Array.isArray(raw.resume_history) || raw.resume_history.length > HISTORY_CAP) {
|
|
782
|
+
throw new LaneSchemaError(`resume_history must be an array capped at ${HISTORY_CAP} entries`, {
|
|
783
|
+
path: filePath,
|
|
784
|
+
field: "resume_history",
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
for (const [index, value] of raw.resume_history.entries()) {
|
|
788
|
+
const field = `resume_history[${index}]`;
|
|
789
|
+
if (!isPlainObject(value) ||
|
|
790
|
+
Object.keys(value).some((key) => !["at", "trigger", "caller"].includes(key))) {
|
|
791
|
+
throw new LaneSchemaError(`${field} must be a resume history entry`, {
|
|
792
|
+
path: filePath,
|
|
793
|
+
field,
|
|
794
|
+
value,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
requireTimestamp(value.at, `${field}.at`, filePath);
|
|
798
|
+
requireString(value.trigger, `${field}.trigger`);
|
|
799
|
+
if (value.caller !== undefined)
|
|
800
|
+
requireString(value.caller, `${field}.caller`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
776
803
|
if (raw.stop_history !== undefined) {
|
|
777
804
|
if (!Array.isArray(raw.stop_history)) {
|
|
778
805
|
throw new LaneSchemaError("stop_history must be an array when present", {
|
|
@@ -1163,6 +1190,11 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
1163
1190
|
failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
|
|
1164
1191
|
}
|
|
1165
1192
|
: {}),
|
|
1193
|
+
...(all.some((record) => record.resume_history !== undefined)
|
|
1194
|
+
? {
|
|
1195
|
+
resume_history: unionHistory(all.map((record) => record.resume_history ?? []), entryIdentity, base.resume_history ?? []),
|
|
1196
|
+
}
|
|
1197
|
+
: {}),
|
|
1166
1198
|
...(all.some((record) => record.settings_history !== undefined)
|
|
1167
1199
|
? {
|
|
1168
1200
|
settings_history: unionHistory(all.map((record) => record.settings_history ?? []), entryIdentity, base.settings_history ?? []),
|