@indigoai-us/hq-cli 5.239.0 → 5.240.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 +10 -0
- package/dist/command-catalog.generated.d.ts +41 -0
- package/dist/command-catalog.generated.js +54 -0
- package/dist/commands/lanes.js +187 -16
- package/dist/lib/lanes/approval-gate.d.ts +8 -0
- package/dist/lib/lanes/approval-gate.js +90 -0
- package/dist/lib/lanes/errors.d.ts +1 -1
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/outpost-registry-worker-approval.d.ts +18 -0
- package/dist/lib/lanes/outpost-registry-worker-approval.js +109 -0
- package/dist/lib/lanes/profile.d.ts +10 -0
- package/dist/lib/lanes/profile.js +88 -0
- package/dist/lib/lanes/reconcile.js +38 -11
- package/dist/lib/lanes/registry-persona.d.ts +20 -0
- package/dist/lib/lanes/registry-persona.js +329 -0
- package/dist/lib/lanes/registry.d.ts +4 -1
- package/dist/lib/lanes/registry.js +8 -3
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +28 -7
- package/dist/lib/lanes/store.js +90 -0
- package/dist/lib/lanes/types.d.ts +29 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +46 -15
- package/package.json +1 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
5
|
+
import { WorkerResolutionError } from "./errors.js";
|
|
6
|
+
const REGISTRY_RELATIVE_PATH = path.join("core", "workers", "registry.yaml");
|
|
7
|
+
function fail(code, message, details = {}) {
|
|
8
|
+
throw new WorkerResolutionError(code, message, details);
|
|
9
|
+
}
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function withinRoot(root, target) {
|
|
14
|
+
const relative = path.relative(root, target);
|
|
15
|
+
return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
16
|
+
}
|
|
17
|
+
function resolveWorkerDirectory(hqRoot, workerPath, id) {
|
|
18
|
+
const relative = workerPath.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
19
|
+
if (relative.length === 0 ||
|
|
20
|
+
path.posix.isAbsolute(relative) ||
|
|
21
|
+
relative.split("/").some((part) => part === ".." || part === ".")) {
|
|
22
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} has an unsafe path`, {
|
|
23
|
+
worker: id,
|
|
24
|
+
path: workerPath,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const root = path.resolve(hqRoot);
|
|
28
|
+
const abs = path.resolve(root, ...relative.split("/"));
|
|
29
|
+
if (!withinRoot(root, abs)) {
|
|
30
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} escapes HQ_ROOT`, {
|
|
31
|
+
worker: id,
|
|
32
|
+
path: workerPath,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
let realRoot;
|
|
36
|
+
let realWorkerDir;
|
|
37
|
+
try {
|
|
38
|
+
realRoot = fs.realpathSync(root);
|
|
39
|
+
realWorkerDir = fs.realpathSync(abs);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
fail("registry_worker_unreadable", `Cannot resolve registry worker ${JSON.stringify(id)} at ${abs}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
43
|
+
worker: id,
|
|
44
|
+
path: abs,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!withinRoot(realRoot, realWorkerDir)) {
|
|
48
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} resolves outside HQ_ROOT`, {
|
|
49
|
+
worker: id,
|
|
50
|
+
path: workerPath,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { abs: realWorkerDir, relative };
|
|
54
|
+
}
|
|
55
|
+
function readWorkerYaml(hqRoot, workerPath, id) {
|
|
56
|
+
const directory = resolveWorkerDirectory(hqRoot, workerPath, id);
|
|
57
|
+
const filePath = path.join(directory.abs, "worker.yaml");
|
|
58
|
+
let stat;
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
stat = fs.lstatSync(filePath);
|
|
62
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
63
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml must be a regular file`, {
|
|
64
|
+
worker: id,
|
|
65
|
+
path: filePath,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
text = fs.readFileSync(filePath, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof WorkerResolutionError)
|
|
72
|
+
throw err;
|
|
73
|
+
fail("registry_worker_unreadable", `Cannot read registry worker ${JSON.stringify(id)} at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
74
|
+
worker: id,
|
|
75
|
+
path: filePath,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = parseUserYaml(text, filePath);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} has invalid worker.yaml at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
84
|
+
worker: id,
|
|
85
|
+
path: filePath,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!isPlainObject(parsed)) {
|
|
89
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml must contain an object`, {
|
|
90
|
+
worker: id,
|
|
91
|
+
path: filePath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const realFile = fs.realpathSync(filePath);
|
|
95
|
+
const realRoot = fs.realpathSync(path.resolve(hqRoot));
|
|
96
|
+
if (!withinRoot(realRoot, realFile)) {
|
|
97
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml resolves outside HQ_ROOT`, {
|
|
98
|
+
worker: id,
|
|
99
|
+
path: filePath,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
filePath,
|
|
104
|
+
text,
|
|
105
|
+
yaml: parsed,
|
|
106
|
+
sha256: `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function textValue(value, field, id) {
|
|
110
|
+
if (value === undefined || value === null)
|
|
111
|
+
return "";
|
|
112
|
+
if (typeof value === "string")
|
|
113
|
+
return value.trim();
|
|
114
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
115
|
+
return value.map((item) => item.trim()).filter(Boolean).join("\n");
|
|
116
|
+
}
|
|
117
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must be a string or string array`, {
|
|
118
|
+
worker: id,
|
|
119
|
+
field,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function stringList(value, field, id) {
|
|
123
|
+
if (value === undefined || value === null)
|
|
124
|
+
return [];
|
|
125
|
+
const values = typeof value === "string" ? [value] : value;
|
|
126
|
+
if (!Array.isArray(values) || values.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
127
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must be an array of non-empty strings`, {
|
|
128
|
+
worker: id,
|
|
129
|
+
field,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const cleaned = values.map((item) => item.trim());
|
|
133
|
+
if (new Set(cleaned).size !== cleaned.length) {
|
|
134
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must not contain duplicates`, {
|
|
135
|
+
worker: id,
|
|
136
|
+
field,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return cleaned;
|
|
140
|
+
}
|
|
141
|
+
function verificationCommands(value, id) {
|
|
142
|
+
if (value === undefined || value === null)
|
|
143
|
+
return [];
|
|
144
|
+
if (!Array.isArray(value)) {
|
|
145
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute must be an array`, {
|
|
146
|
+
worker: id,
|
|
147
|
+
field: "verification.post_execute",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const commands = value.map((entry, index) => {
|
|
151
|
+
if (typeof entry === "string" && entry.trim() !== "")
|
|
152
|
+
return entry.trim();
|
|
153
|
+
if (isPlainObject(entry) && typeof entry.command === "string" && entry.command.trim() !== "") {
|
|
154
|
+
return entry.command.trim();
|
|
155
|
+
}
|
|
156
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute[${index}] must contain a non-empty command`, {
|
|
157
|
+
worker: id,
|
|
158
|
+
field: `verification.post_execute[${index}]`,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
if (new Set(commands).size !== commands.length) {
|
|
162
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute must not contain duplicate commands`, {
|
|
163
|
+
worker: id,
|
|
164
|
+
field: "verification.post_execute",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return commands;
|
|
168
|
+
}
|
|
169
|
+
function personaFromYaml(hqRoot, id, workerPath, yaml, sha256, registryDescription, pinnedApproval) {
|
|
170
|
+
if (yaml.worker?.id !== undefined && yaml.worker.id !== id) {
|
|
171
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml declares a different id`, {
|
|
172
|
+
worker: id,
|
|
173
|
+
declared_id: yaml.worker.id,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const workerDir = resolveWorkerDirectory(hqRoot, workerPath, id).abs;
|
|
177
|
+
const description = textValue(yaml.worker?.description ?? yaml.description ?? registryDescription, "description", id) || id;
|
|
178
|
+
const instructions = textValue(yaml.instructions, "instructions", id);
|
|
179
|
+
const skillFiles = [];
|
|
180
|
+
if (yaml.skills !== undefined) {
|
|
181
|
+
if (!Array.isArray(yaml.skills)) {
|
|
182
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} skills must be an array`, {
|
|
183
|
+
worker: id,
|
|
184
|
+
field: "skills",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (const [index, skill] of yaml.skills.entries()) {
|
|
188
|
+
if (!isPlainObject(skill) || typeof skill.file !== "string" || skill.file.trim().length === 0) {
|
|
189
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} skills[${index}].file must be a non-empty string`, {
|
|
190
|
+
worker: id,
|
|
191
|
+
field: `skills[${index}].file`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
skillFiles.push(path.resolve(workerDir, skill.file.trim()));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
let verification = {};
|
|
198
|
+
if (yaml.verification !== undefined) {
|
|
199
|
+
if (!isPlainObject(yaml.verification)) {
|
|
200
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification must be an object`, {
|
|
201
|
+
worker: id,
|
|
202
|
+
field: "verification",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
verification = yaml.verification;
|
|
206
|
+
}
|
|
207
|
+
if (verification.approval_required !== undefined && typeof verification.approval_required !== "boolean") {
|
|
208
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.approval_required must be a boolean`, {
|
|
209
|
+
worker: id,
|
|
210
|
+
field: "verification.approval_required",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const humanCheckpoints = pinnedApproval?.human_checkpoints ??
|
|
214
|
+
stringList(verification.human_checkpoints, "verification.human_checkpoints", id);
|
|
215
|
+
return {
|
|
216
|
+
id,
|
|
217
|
+
path: workerPath,
|
|
218
|
+
workerYamlSha256: sha256,
|
|
219
|
+
description,
|
|
220
|
+
instructions,
|
|
221
|
+
skillFiles,
|
|
222
|
+
postExecute: verificationCommands(verification.post_execute, id),
|
|
223
|
+
approvalRequired: pinnedApproval?.approval_required ?? verification.approval_required === true,
|
|
224
|
+
humanCheckpoints: [...humanCheckpoints],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function registryEntries(hqRoot) {
|
|
228
|
+
const registryPath = path.join(hqRoot, REGISTRY_RELATIVE_PATH);
|
|
229
|
+
let text;
|
|
230
|
+
try {
|
|
231
|
+
text = fs.readFileSync(registryPath, "utf8");
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
fail("registry_worker_unreadable", `Cannot read worker registry at ${registryPath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
235
|
+
path: registryPath,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = parseUserYaml(text, registryPath);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
fail("registry_worker_unreadable", `Cannot parse worker registry at ${registryPath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
244
|
+
path: registryPath,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.workers)) {
|
|
248
|
+
fail("registry_worker_unreadable", `Worker registry at ${registryPath} must contain a workers array`, {
|
|
249
|
+
path: registryPath,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return parsed.workers.filter(isPlainObject);
|
|
253
|
+
}
|
|
254
|
+
/** Resolve an active registry worker for a lane without company fallback. */
|
|
255
|
+
export function resolveRegistryPersona(hqRoot, id, laneCompany) {
|
|
256
|
+
const workerId = id.trim();
|
|
257
|
+
if (!workerId) {
|
|
258
|
+
fail("registry_worker_invalid", "registry_worker must be a non-empty worker id", { worker: id });
|
|
259
|
+
}
|
|
260
|
+
const candidates = registryEntries(hqRoot).filter((entry) => entry.id === workerId);
|
|
261
|
+
if (candidates.length === 0) {
|
|
262
|
+
fail("registry_worker_not_found", `Registry worker ${JSON.stringify(workerId)} was not found`, {
|
|
263
|
+
worker: workerId,
|
|
264
|
+
path: path.join(hqRoot, REGISTRY_RELATIVE_PATH),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (candidates.length > 1) {
|
|
268
|
+
fail("registry_worker_ambiguous", `Registry worker ${JSON.stringify(workerId)} appears more than once`, {
|
|
269
|
+
worker: workerId,
|
|
270
|
+
paths: candidates.map((entry) => entry.path),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const entry = candidates[0];
|
|
274
|
+
if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
275
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} has no path`, {
|
|
276
|
+
worker: workerId,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (entry.company !== undefined && typeof entry.company !== "string") {
|
|
280
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} company must be a string`, {
|
|
281
|
+
worker: workerId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (typeof entry.company === "string" && entry.company.trim().length === 0) {
|
|
285
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} company must be non-empty when supplied`, {
|
|
286
|
+
worker: workerId,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const workerCompany = typeof entry.company === "string" && entry.company.trim().length > 0
|
|
290
|
+
? entry.company.trim()
|
|
291
|
+
: undefined;
|
|
292
|
+
if (workerCompany !== undefined && workerCompany !== laneCompany) {
|
|
293
|
+
fail("registry_worker_company_mismatch", `Registry worker ${JSON.stringify(workerId)} belongs to company ${workerCompany} and is not available to ${laneCompany ?? "(none)"}`, {
|
|
294
|
+
worker: workerId,
|
|
295
|
+
worker_company: workerCompany,
|
|
296
|
+
lane_company: laneCompany ?? null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const status = typeof entry.status === "string" && entry.status.trim() !== ""
|
|
300
|
+
? entry.status.trim()
|
|
301
|
+
: "active";
|
|
302
|
+
if (status !== "active") {
|
|
303
|
+
fail("registry_worker_inactive", `Registry worker ${JSON.stringify(workerId)} has status ${JSON.stringify(status)} (need active)`, {
|
|
304
|
+
worker: workerId,
|
|
305
|
+
status,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const worker = readWorkerYaml(hqRoot, entry.path.trim(), workerId);
|
|
309
|
+
const persona = personaFromYaml(hqRoot, workerId, entry.path.trim(), worker.yaml, worker.sha256, entry.description);
|
|
310
|
+
return persona;
|
|
311
|
+
}
|
|
312
|
+
/** Re-read the recorded path on resume; registry changes cannot redirect a lane. */
|
|
313
|
+
export function loadRegistryPersonaFromRecord(hqRoot, record) {
|
|
314
|
+
const worker = readWorkerYaml(hqRoot, record.path, record.id);
|
|
315
|
+
return personaFromYaml(hqRoot, record.id, record.path, worker.yaml, worker.sha256, undefined, {
|
|
316
|
+
approval_required: record.approval_required,
|
|
317
|
+
human_checkpoints: record.human_checkpoints,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
export function registryWorkerRecord(persona) {
|
|
321
|
+
return {
|
|
322
|
+
id: persona.id,
|
|
323
|
+
path: persona.path,
|
|
324
|
+
worker_yaml_sha256: persona.workerYamlSha256,
|
|
325
|
+
approval_required: persona.approvalRequired,
|
|
326
|
+
human_checkpoints: [...persona.humanCheckpoints],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=registry-persona.js.map
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import * as fs from "node:fs";
|
|
26
26
|
import { type WorkerProfile } from "./profile.js";
|
|
27
|
+
import type { RegistryWorkerRecord } from "./types.js";
|
|
27
28
|
export declare const WORKER_REGISTRY_REL: string;
|
|
28
29
|
export interface RegistryWorkerEntry {
|
|
29
30
|
id: string;
|
|
@@ -115,5 +116,7 @@ export declare function tenancyFromRegistryPath(entry: RegistryWorkerEntry): Wor
|
|
|
115
116
|
*/
|
|
116
117
|
export declare function resolveContextBasePaths(hqRoot: string, workerDir: string, bases: string[] | undefined, deps?: RegistryDeps): string[];
|
|
117
118
|
export declare function resolveSkillFiles(workerDir: string, skills: WorkerYamlSkill[] | undefined, deps?: RegistryDeps): string[];
|
|
118
|
-
export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps
|
|
119
|
+
export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps, options?: {
|
|
120
|
+
registryWorkerRecord?: RegistryWorkerRecord;
|
|
121
|
+
}): ResolvedLaneWorker;
|
|
119
122
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -28,7 +28,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
|
28
28
|
import { readContributes } from "../scan-packages/index.js";
|
|
29
29
|
import { contributionLinks, linkStatus, } from "../../utils/pack-contributions.js";
|
|
30
30
|
import { WorkerResolutionError } from "./errors.js";
|
|
31
|
-
import {
|
|
31
|
+
import { loadWorkerProfileWithRegistry, workerProfileMarker, } from "./profile.js";
|
|
32
32
|
export const WORKER_REGISTRY_REL = path.join("core", "workers", "registry.yaml");
|
|
33
33
|
function defaultRegistryDeps() {
|
|
34
34
|
return {
|
|
@@ -371,10 +371,15 @@ function readRegistryOrEmpty(hqRoot, deps) {
|
|
|
371
371
|
return [];
|
|
372
372
|
return readWorkerRegistryFile(hqRoot, deps);
|
|
373
373
|
}
|
|
374
|
-
export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps()) {
|
|
374
|
+
export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps(), options = {}) {
|
|
375
375
|
const personalRel = path.posix.join("personal", "workers", workerId);
|
|
376
376
|
const profile = workerProfileMarker(hqRoot, workerId) === "present"
|
|
377
|
-
?
|
|
377
|
+
? loadWorkerProfileWithRegistry(hqRoot, workerId, {
|
|
378
|
+
laneCompany,
|
|
379
|
+
...(options.registryWorkerRecord !== undefined
|
|
380
|
+
? { registryWorkerRecord: options.registryWorkerRecord }
|
|
381
|
+
: {}),
|
|
382
|
+
})
|
|
378
383
|
: undefined;
|
|
379
384
|
const personalClaim = profile ? "profile" : personalDirClaim(hqRoot, workerId, deps);
|
|
380
385
|
if (personalClaim === "missing_yaml") {
|
|
@@ -239,7 +239,7 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
|
|
|
239
239
|
export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
|
|
240
240
|
envVar: string;
|
|
241
241
|
home: string;
|
|
242
|
-
}, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string): NodeJS.ProcessEnv;
|
|
242
|
+
}, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"]): NodeJS.ProcessEnv;
|
|
243
243
|
export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
|
|
244
244
|
/** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
|
|
245
245
|
export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -23,7 +23,8 @@ import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
|
|
|
23
23
|
import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
|
|
24
24
|
import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
|
|
25
25
|
import { AccountResolutionError, LanesError } from "./errors.js";
|
|
26
|
-
import { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
|
|
26
|
+
import { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
|
|
27
|
+
import { registryWorkerRecord } from "./registry-persona.js";
|
|
27
28
|
import { resolveMachineIdentity, } from "./machine-identity.js";
|
|
28
29
|
import { appendCappedHistory, listLaneIds, readLane, retryUpdateLane } from "./store.js";
|
|
29
30
|
import { copySessionFiles, planSessionCopy } from "./move-account.js";
|
|
@@ -1236,7 +1237,7 @@ export function buildProviderCommand(provider, input) {
|
|
|
1236
1237
|
args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, appendCodexWorkerPolicyDirFlag(parsed, appendProviderProfileFlags(parsed, args, input.profile), input.workerPolicyDir), input.model), input.effort), input.fastMode),
|
|
1237
1238
|
};
|
|
1238
1239
|
}
|
|
1239
|
-
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir) {
|
|
1240
|
+
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker) {
|
|
1240
1241
|
const accepted = Object.values(PROVIDER_HOME_ENV);
|
|
1241
1242
|
if (!accepted.includes(resolved.envVar)) {
|
|
1242
1243
|
throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
|
|
@@ -1245,6 +1246,8 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
|
|
|
1245
1246
|
delete env.HQ_PROJECT_ID;
|
|
1246
1247
|
delete env.HQ_STORY_ID;
|
|
1247
1248
|
delete env.HQ_POLICY_WORKER_DIR;
|
|
1249
|
+
delete env.HQ_LANE_APPROVAL_REQUIRED;
|
|
1250
|
+
delete env.HQ_LANE_HUMAN_CHECKPOINTS;
|
|
1248
1251
|
env[resolved.envVar] = resolved.home;
|
|
1249
1252
|
delete env[CLAUDE_AUTO_COMPACT_ENV];
|
|
1250
1253
|
if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
|
|
@@ -1252,6 +1255,12 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
|
|
|
1252
1255
|
}
|
|
1253
1256
|
env.HQ_LANE_ID = laneId;
|
|
1254
1257
|
env.HQ_ROOT = hqRoot;
|
|
1258
|
+
if (registryWorker && (registryWorker.approval_required || registryWorker.human_checkpoints.length > 0)) {
|
|
1259
|
+
env.HQ_LANE_APPROVAL_REQUIRED = "1";
|
|
1260
|
+
env.HQ_LANE_HUMAN_CHECKPOINTS = registryWorker.human_checkpoints.length > 0
|
|
1261
|
+
? registryWorker.human_checkpoints.join(",")
|
|
1262
|
+
: "external_publish";
|
|
1263
|
+
}
|
|
1255
1264
|
if (workerPolicyDir !== undefined &&
|
|
1256
1265
|
(resolved.envVar === PROVIDER_HOME_ENV.claude || resolved.envVar === PROVIDER_HOME_ENV.codex)) {
|
|
1257
1266
|
env.HQ_POLICY_WORKER_DIR = workerPolicyDir;
|
|
@@ -1659,7 +1668,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1659
1668
|
ensureDir(logsDir);
|
|
1660
1669
|
ensureDir(path.join(runDir, "envelopes"));
|
|
1661
1670
|
const profile = existing.worker_profile
|
|
1662
|
-
?
|
|
1671
|
+
? loadWorkerProfileWithRegistry(hqRoot, existing.worker_profile, {
|
|
1672
|
+
laneCompany: existing.company?.slug ?? existing.mesh_company?.slug,
|
|
1673
|
+
...(existing.registry_worker !== undefined
|
|
1674
|
+
? { registryWorkerRecord: existing.registry_worker }
|
|
1675
|
+
: {}),
|
|
1676
|
+
})
|
|
1663
1677
|
: undefined;
|
|
1664
1678
|
if (existing.host.kind === "outpost" &&
|
|
1665
1679
|
profile !== undefined &&
|
|
@@ -1827,7 +1841,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1827
1841
|
const autoCompactTokens = lane.provider === "claude"
|
|
1828
1842
|
? providerAutoCompactTokens(config.providers.claude)
|
|
1829
1843
|
: undefined;
|
|
1830
|
-
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
|
|
1844
|
+
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker);
|
|
1831
1845
|
const laneTurnId = randomUUID();
|
|
1832
1846
|
childEnv.HQ_LANE_TURN_ID = laneTurnId;
|
|
1833
1847
|
const model = resolveLaneModel({
|
|
@@ -2023,10 +2037,13 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2023
2037
|
...(profile
|
|
2024
2038
|
? {
|
|
2025
2039
|
worker_profile_hash: profile.contentHash,
|
|
2040
|
+
registry_worker: profile.registryWorker !== undefined
|
|
2041
|
+
? registryWorkerRecord(profile.registryWorker)
|
|
2042
|
+
: undefined,
|
|
2026
2043
|
worker_profile_ignored: [...ignoredProfileFields],
|
|
2027
2044
|
worker_policy_delivery: policyDelivery,
|
|
2028
2045
|
}
|
|
2029
|
-
: { worker_policy_delivery: undefined }),
|
|
2046
|
+
: { registry_worker: undefined, worker_policy_delivery: undefined }),
|
|
2030
2047
|
};
|
|
2031
2048
|
if (resume !== undefined) {
|
|
2032
2049
|
next.provider_session_id = resume.session_id;
|
|
@@ -2240,7 +2257,11 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2240
2257
|
ensureDir(logsDir);
|
|
2241
2258
|
const config = loadLanesConfig(hqRoot);
|
|
2242
2259
|
const profile = lane.worker_profile
|
|
2243
|
-
?
|
|
2260
|
+
? loadWorkerProfileWithRegistry(hqRoot, lane.worker_profile, {
|
|
2261
|
+
laneCompany: lane.company?.slug ?? lane.mesh_company?.slug,
|
|
2262
|
+
...(lane.registry_worker !== undefined
|
|
2263
|
+
? { registryWorkerRecord: lane.registry_worker }
|
|
2264
|
+
: {}),
|
|
2244
2265
|
onUnknownMetaFields: (fields) => {
|
|
2245
2266
|
process.stderr.write(`hq lanes watcher: envelope repair ignored unknown worker profile meta field(s) for ${lane.worker_profile}: ${fields.join(", ")}\n`);
|
|
2246
2267
|
input.onProfileWarning?.(fields);
|
|
@@ -2289,7 +2310,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2289
2310
|
const autoCompactTokens = lane.provider === "claude"
|
|
2290
2311
|
? providerAutoCompactTokens(config.providers.claude)
|
|
2291
2312
|
: undefined;
|
|
2292
|
-
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
|
|
2313
|
+
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker);
|
|
2293
2314
|
const accountCfg = config.providers[lane.provider].accounts[resolved.id];
|
|
2294
2315
|
const model = resolveLaneModel({
|
|
2295
2316
|
cliModel: pinnedModel,
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -543,6 +543,91 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
543
543
|
raw.worker_policy_delivery !== "prompt") {
|
|
544
544
|
throw new LaneSchemaError(`Unknown worker policy delivery mode ${JSON.stringify(raw.worker_policy_delivery)}`, { path: filePath, field: "worker_policy_delivery", value: raw.worker_policy_delivery });
|
|
545
545
|
}
|
|
546
|
+
if (raw.registry_worker !== undefined) {
|
|
547
|
+
const worker = raw.registry_worker;
|
|
548
|
+
if (!isPlainObject(worker) || Object.keys(worker).some((key) => !["id", "path", "worker_yaml_sha256", "approval_required", "human_checkpoints"].includes(key))) {
|
|
549
|
+
throw new LaneSchemaError("registry_worker must be a pinned registry worker record", {
|
|
550
|
+
path: filePath,
|
|
551
|
+
field: "registry_worker",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
for (const key of ["id", "path"]) {
|
|
555
|
+
if (typeof worker[key] !== "string" || worker[key].trim().length === 0) {
|
|
556
|
+
throw new LaneSchemaError(`registry_worker.${key} must be a non-empty string`, {
|
|
557
|
+
path: filePath,
|
|
558
|
+
field: `registry_worker.${key}`,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const registryPath = worker.path.replaceAll("\\", "/");
|
|
563
|
+
if (path.posix.isAbsolute(registryPath) || registryPath.split("/").some((part) => part === ".." || part === ".")) {
|
|
564
|
+
throw new LaneSchemaError("registry_worker.path must stay within HQ_ROOT", {
|
|
565
|
+
path: filePath,
|
|
566
|
+
field: "registry_worker.path",
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (typeof worker.worker_yaml_sha256 !== "string" || !/^sha256:[a-f0-9]{64}$/.test(worker.worker_yaml_sha256)) {
|
|
570
|
+
throw new LaneSchemaError("registry_worker.worker_yaml_sha256 must be a SHA-256 hash", {
|
|
571
|
+
path: filePath,
|
|
572
|
+
field: "registry_worker.worker_yaml_sha256",
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (typeof worker.approval_required !== "boolean") {
|
|
576
|
+
throw new LaneSchemaError("registry_worker.approval_required must be a boolean", {
|
|
577
|
+
path: filePath,
|
|
578
|
+
field: "registry_worker.approval_required",
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (!Array.isArray(worker.human_checkpoints) || worker.human_checkpoints.some((checkpoint) => typeof checkpoint !== "string" || checkpoint.trim().length === 0) ||
|
|
582
|
+
new Set(worker.human_checkpoints).size !== worker.human_checkpoints.length) {
|
|
583
|
+
throw new LaneSchemaError("registry_worker.human_checkpoints must be unique non-empty strings", {
|
|
584
|
+
path: filePath,
|
|
585
|
+
field: "registry_worker.human_checkpoints",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (raw.registry_worker_approval_history !== undefined) {
|
|
590
|
+
if (!Array.isArray(raw.registry_worker_approval_history)) {
|
|
591
|
+
throw new LaneSchemaError("registry_worker_approval_history must be an array when present", {
|
|
592
|
+
path: filePath,
|
|
593
|
+
field: "registry_worker_approval_history",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
for (const [index, entry] of raw.registry_worker_approval_history.entries()) {
|
|
597
|
+
const field = `registry_worker_approval_history[${index}]`;
|
|
598
|
+
if (!isPlainObject(entry)) {
|
|
599
|
+
throw new LaneSchemaError(`${field} must be an approval audit event`, { path: filePath, field });
|
|
600
|
+
}
|
|
601
|
+
const expected = entry.event === "approved"
|
|
602
|
+
? ["event", "approval_id", "checkpoint", "registry_worker_id", "worker_yaml_sha256", "reason", "actor", "at"]
|
|
603
|
+
: entry.event === "consumed"
|
|
604
|
+
? ["event", "approval_id", "envelope_ref", "at"]
|
|
605
|
+
: [];
|
|
606
|
+
if (expected.length === 0 || Object.keys(entry).some((key) => !expected.includes(key))) {
|
|
607
|
+
throw new LaneSchemaError(`${field} has an unknown event or field`, { path: filePath, field });
|
|
608
|
+
}
|
|
609
|
+
for (const key of ["approval_id", ...(entry.event === "approved" ? ["checkpoint", "reason", "actor"] : ["envelope_ref"])]) {
|
|
610
|
+
if (typeof entry[key] !== "string" || entry[key].trim().length === 0) {
|
|
611
|
+
throw new LaneSchemaError(`${field}.${key} must be a non-empty string`, { path: filePath, field });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (entry.event === "approved" && entry.worker_yaml_sha256 !== undefined &&
|
|
615
|
+
(typeof entry.worker_yaml_sha256 !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.worker_yaml_sha256))) {
|
|
616
|
+
throw new LaneSchemaError(`${field}.worker_yaml_sha256 must be a SHA-256 hash`, {
|
|
617
|
+
path: filePath,
|
|
618
|
+
field: `${field}.worker_yaml_sha256`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (entry.event === "approved" && entry.registry_worker_id !== undefined &&
|
|
622
|
+
(typeof entry.registry_worker_id !== "string" || entry.registry_worker_id.trim().length === 0)) {
|
|
623
|
+
throw new LaneSchemaError(`${field}.registry_worker_id must be a non-empty string`, {
|
|
624
|
+
path: filePath,
|
|
625
|
+
field: `${field}.registry_worker_id`,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
546
631
|
parseLaneStories(raw, filePath);
|
|
547
632
|
if (raw.last_envelope !== undefined) {
|
|
548
633
|
if (!isPlainObject(raw.last_envelope) || typeof raw.last_envelope.ref !== "string" ||
|
|
@@ -920,6 +1005,11 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
920
1005
|
worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
|
|
921
1006
|
}
|
|
922
1007
|
: {}),
|
|
1008
|
+
...(all.some((record) => record.registry_worker_approval_history !== undefined)
|
|
1009
|
+
? {
|
|
1010
|
+
registry_worker_approval_history: unionHistory(all.map((record) => record.registry_worker_approval_history ?? []), entryIdentity, base.registry_worker_approval_history ?? []),
|
|
1011
|
+
}
|
|
1012
|
+
: {}),
|
|
923
1013
|
...(all.some((record) => record.failed_resume_history !== undefined)
|
|
924
1014
|
? {
|
|
925
1015
|
failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
|
|
@@ -295,6 +295,31 @@ export interface WorkerProfileRepinHistoryEntry {
|
|
|
295
295
|
at: string;
|
|
296
296
|
reason: string;
|
|
297
297
|
}
|
|
298
|
+
/** The registry worker identity and approval contract pinned at lane creation. */
|
|
299
|
+
export interface RegistryWorkerRecord {
|
|
300
|
+
id: string;
|
|
301
|
+
path: string;
|
|
302
|
+
worker_yaml_sha256: string;
|
|
303
|
+
approval_required: boolean;
|
|
304
|
+
human_checkpoints: string[];
|
|
305
|
+
}
|
|
306
|
+
/** Append-only approval and consumption events for registry worker checkpoints. */
|
|
307
|
+
export type RegistryWorkerApprovalHistoryEntry = {
|
|
308
|
+
event: "approved";
|
|
309
|
+
approval_id: string;
|
|
310
|
+
checkpoint: string;
|
|
311
|
+
/** Pins this approval to the registry worker version that received it. */
|
|
312
|
+
registry_worker_id?: string;
|
|
313
|
+
worker_yaml_sha256?: string;
|
|
314
|
+
reason: string;
|
|
315
|
+
actor: string;
|
|
316
|
+
at: string;
|
|
317
|
+
} | {
|
|
318
|
+
event: "consumed";
|
|
319
|
+
approval_id: string;
|
|
320
|
+
envelope_ref: string;
|
|
321
|
+
at: string;
|
|
322
|
+
};
|
|
298
323
|
export interface AttributionHistoryEntry {
|
|
299
324
|
project_id: string;
|
|
300
325
|
story_id: string;
|
|
@@ -390,6 +415,10 @@ export interface LaneRecord {
|
|
|
390
415
|
hire_grant?: number;
|
|
391
416
|
/** Content hash of the profile last applied to this lane. */
|
|
392
417
|
worker_profile_hash?: string;
|
|
418
|
+
/** Registry persona selected by the profile, pinned at lane creation. */
|
|
419
|
+
registry_worker?: RegistryWorkerRecord;
|
|
420
|
+
/** Append-only approvals and one-envelope consumption records. */
|
|
421
|
+
registry_worker_approval_history?: RegistryWorkerApprovalHistoryEntry[];
|
|
393
422
|
/** Bounded audit history of authorized Outpost profile hash re-pins. */
|
|
394
423
|
worker_profile_repin_history?: WorkerProfileRepinHistoryEntry[];
|
|
395
424
|
/** Profile fields intentionally unsupported by the selected provider. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -223,7 +223,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
223
223
|
state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
|
|
224
224
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
225
225
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
226
|
-
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
226
|
+
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
227
227
|
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
|
|
228
228
|
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
229
229
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
@@ -164,6 +164,8 @@ export declare function applyTerminalToLane(hqRoot: string, laneId: string, clas
|
|
|
164
164
|
export declare function applyTerminalToLaneWithResult(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): {
|
|
165
165
|
record: LaneRecord;
|
|
166
166
|
changed: boolean;
|
|
167
|
+
envelope?: LaneEnvelope;
|
|
168
|
+
classification: TerminalClassification;
|
|
167
169
|
};
|
|
168
170
|
export declare function isPidAlive(pid: number): boolean;
|
|
169
171
|
export interface LatestEnvelope {
|