@indigoai-us/hq-cli 5.238.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 +21 -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/checkpoint-ontology/apply.js +26 -1
- package/dist/lib/checkpoint-ontology/plan.d.ts +12 -0
- package/dist/lib/checkpoint-ontology/plan.js +12 -0
- 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
|
@@ -34,6 +34,6 @@ export declare class AccountResolutionError extends LanesError {
|
|
|
34
34
|
constructor(error: "unknown_account" | "account_disabled" | "no_enabled_account" | "account_model_unavailable" | "account_quota_exhausted", message: string, details?: Record<string, unknown>);
|
|
35
35
|
}
|
|
36
36
|
export declare class WorkerResolutionError extends LanesError {
|
|
37
|
-
constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "worker_profile_invalid" | "worker_policy_invalid" | "worker_profile_ambiguous" | "package_workers_unmounted" | "worker_registry_unreadable", message: string, details?: Record<string, unknown>);
|
|
37
|
+
constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "worker_profile_invalid" | "worker_policy_invalid" | "worker_profile_ambiguous" | "package_workers_unmounted" | "worker_registry_unreadable" | "registry_worker_not_found" | "registry_worker_company_mismatch" | "registry_worker_inactive" | "registry_worker_invalid" | "registry_worker_unreadable" | "registry_worker_ambiguous", message: string, details?: Record<string, unknown>);
|
|
38
38
|
}
|
|
39
39
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -56,8 +56,10 @@ export type { KnownModels, KnownModelsDeps, ProbeCommand, ProbeResult } from "./
|
|
|
56
56
|
export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
|
|
57
57
|
export type { SessionCopyPlan, SessionCopyResult } from "./move-account.js";
|
|
58
58
|
export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
|
|
59
|
-
export { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, personalWorkerProfileDir, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
|
|
59
|
+
export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
|
|
60
|
+
export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
|
|
60
61
|
export type { WorkerProfile, WorkerProfileMeta, WorkerProfileRuntime, } from "./profile.js";
|
|
62
|
+
export type { RegistryPersona } from "./registry-persona.js";
|
|
61
63
|
export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
|
|
62
64
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
63
65
|
export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -31,7 +31,8 @@ export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeRe
|
|
|
31
31
|
export { CODEX_MODELS_CACHE_NAME, MODEL_PROBE_TIMEOUT_MS, claudeKnownModels, codexKnownModels, defaultProbe, grokKnownModels, knownModels, modelIsKnown, parseClaudeHelpModels, parseGrokModelsOutput, validateLaneModel, } from "./models.js";
|
|
32
32
|
export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
|
|
33
33
|
export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
|
|
34
|
-
export { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, personalWorkerProfileDir, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
|
|
34
|
+
export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
|
|
35
|
+
export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
|
|
35
36
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
36
37
|
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
37
38
|
export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
|
|
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
|
|
|
27
27
|
stderr: string;
|
|
28
28
|
}
|
|
29
29
|
export interface OutpostLaneCommand {
|
|
30
|
-
action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target" | "reparent-target";
|
|
30
|
+
action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target" | "reparent-target" | "approve-target";
|
|
31
31
|
laneId: string;
|
|
32
32
|
hqBinary?: string;
|
|
33
33
|
text?: string;
|
|
@@ -43,6 +43,7 @@ export interface OutpostLaneCommand {
|
|
|
43
43
|
hire?: string;
|
|
44
44
|
grantContext?: string;
|
|
45
45
|
profileRepinContext?: string;
|
|
46
|
+
approvalContext?: string;
|
|
46
47
|
senior?: string;
|
|
47
48
|
reason?: string;
|
|
48
49
|
compact?: boolean;
|
|
@@ -151,6 +151,7 @@ export function buildOutpostLaneCommand(input) {
|
|
|
151
151
|
["--hire", input.hire],
|
|
152
152
|
["--grant-context", input.grantContext],
|
|
153
153
|
["--profile-repin-context", input.profileRepinContext],
|
|
154
|
+
["--approval-context", input.approvalContext],
|
|
154
155
|
["--failed-resume-context", input.failedResumeContext],
|
|
155
156
|
["--senior", input.senior],
|
|
156
157
|
["--reason", input.reason],
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SeniorRef } from "./types.js";
|
|
2
|
+
declare const APPROVAL_CONTEXT_SCHEMA: "hq-lane-registry-worker-approval-context.v1";
|
|
3
|
+
export interface OutpostRegistryWorkerApprovalContext {
|
|
4
|
+
schema_version: typeof APPROVAL_CONTEXT_SCHEMA;
|
|
5
|
+
lane_id: string;
|
|
6
|
+
expected_senior: SeniorRef;
|
|
7
|
+
actor: string;
|
|
8
|
+
checkpoint: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
approval_id: string;
|
|
11
|
+
registry_worker_id: string;
|
|
12
|
+
worker_yaml_sha256: string;
|
|
13
|
+
at: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createOutpostRegistryWorkerApprovalContext(input: Omit<OutpostRegistryWorkerApprovalContext, "schema_version">): string;
|
|
16
|
+
export declare function parseOutpostRegistryWorkerApprovalContext(encoded: string, expectedLaneId: string): OutpostRegistryWorkerApprovalContext;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=outpost-registry-worker-approval.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { LanesError } from "./errors.js";
|
|
2
|
+
import { isSeniorKind } from "./types.js";
|
|
3
|
+
const APPROVAL_CONTEXT_SCHEMA = "hq-lane-registry-worker-approval-context.v1";
|
|
4
|
+
const MAX_CONTEXT_BYTES = 16 * 1024;
|
|
5
|
+
function contextError(message) {
|
|
6
|
+
return new LanesError("registry_worker_approval_context_invalid", message, {});
|
|
7
|
+
}
|
|
8
|
+
function copySenior(senior) {
|
|
9
|
+
return senior.id === undefined
|
|
10
|
+
? { kind: senior.kind }
|
|
11
|
+
: { kind: senior.kind, id: senior.id };
|
|
12
|
+
}
|
|
13
|
+
function parseSenior(value) {
|
|
14
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
15
|
+
return null;
|
|
16
|
+
const record = value;
|
|
17
|
+
if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
|
|
18
|
+
return null;
|
|
19
|
+
if (!isSeniorKind(record.kind))
|
|
20
|
+
return null;
|
|
21
|
+
if (record.id !== undefined &&
|
|
22
|
+
(typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
|
|
23
|
+
return null;
|
|
24
|
+
if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
if (record.kind === "user" && record.id !== undefined)
|
|
27
|
+
return null;
|
|
28
|
+
return copySenior({ kind: record.kind, ...(record.id === undefined ? {} : { id: record.id }) });
|
|
29
|
+
}
|
|
30
|
+
function nonEmpty(value) {
|
|
31
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
32
|
+
}
|
|
33
|
+
export function createOutpostRegistryWorkerApprovalContext(input) {
|
|
34
|
+
if (!nonEmpty(input.lane_id) ||
|
|
35
|
+
!nonEmpty(input.actor) ||
|
|
36
|
+
!nonEmpty(input.checkpoint) ||
|
|
37
|
+
!nonEmpty(input.reason) ||
|
|
38
|
+
!nonEmpty(input.approval_id) ||
|
|
39
|
+
!nonEmpty(input.registry_worker_id) ||
|
|
40
|
+
!/^sha256:[a-f0-9]{64}$/.test(input.worker_yaml_sha256) ||
|
|
41
|
+
!Number.isFinite(Date.parse(input.at))) {
|
|
42
|
+
throw contextError("Outpost registry worker approval fields are incomplete");
|
|
43
|
+
}
|
|
44
|
+
const context = {
|
|
45
|
+
schema_version: APPROVAL_CONTEXT_SCHEMA,
|
|
46
|
+
...input,
|
|
47
|
+
expected_senior: copySenior(input.expected_senior),
|
|
48
|
+
};
|
|
49
|
+
const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
|
|
50
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
|
|
51
|
+
throw contextError(`Registry worker approval context for lane ${input.lane_id} is too large`);
|
|
52
|
+
}
|
|
53
|
+
return encoded;
|
|
54
|
+
}
|
|
55
|
+
export function parseOutpostRegistryWorkerApprovalContext(encoded, expectedLaneId) {
|
|
56
|
+
if (encoded.length === 0 ||
|
|
57
|
+
encoded.length > MAX_CONTEXT_BYTES ||
|
|
58
|
+
!/^[A-Za-z0-9_-]+$/.test(encoded) ||
|
|
59
|
+
Buffer.from(encoded, "base64url").toString("base64url") !== encoded) {
|
|
60
|
+
throw contextError("Registry worker approval context must be bounded canonical base64url text");
|
|
61
|
+
}
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(encoded, "base64url"));
|
|
65
|
+
raw = JSON.parse(decoded);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw contextError("Registry worker approval context is not valid UTF-8 JSON");
|
|
69
|
+
}
|
|
70
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
71
|
+
throw contextError("Registry worker approval context must be a JSON object");
|
|
72
|
+
}
|
|
73
|
+
const record = raw;
|
|
74
|
+
const allowed = [
|
|
75
|
+
"schema_version", "lane_id", "expected_senior", "actor", "checkpoint", "reason",
|
|
76
|
+
"approval_id", "registry_worker_id", "worker_yaml_sha256", "at",
|
|
77
|
+
];
|
|
78
|
+
if (Object.keys(record).some((key) => !allowed.includes(key))) {
|
|
79
|
+
throw contextError("Registry worker approval context contains unknown fields");
|
|
80
|
+
}
|
|
81
|
+
const expectedSenior = parseSenior(record.expected_senior);
|
|
82
|
+
if (record.schema_version !== APPROVAL_CONTEXT_SCHEMA ||
|
|
83
|
+
record.lane_id !== expectedLaneId ||
|
|
84
|
+
expectedSenior === null ||
|
|
85
|
+
!nonEmpty(record.actor) ||
|
|
86
|
+
!nonEmpty(record.checkpoint) ||
|
|
87
|
+
!nonEmpty(record.reason) ||
|
|
88
|
+
!nonEmpty(record.approval_id) ||
|
|
89
|
+
!nonEmpty(record.registry_worker_id) ||
|
|
90
|
+
typeof record.worker_yaml_sha256 !== "string" ||
|
|
91
|
+
!/^sha256:[a-f0-9]{64}$/.test(record.worker_yaml_sha256) ||
|
|
92
|
+
typeof record.at !== "string" ||
|
|
93
|
+
!Number.isFinite(Date.parse(record.at))) {
|
|
94
|
+
throw contextError(`Registry worker approval context is invalid for lane ${expectedLaneId}`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
schema_version: APPROVAL_CONTEXT_SCHEMA,
|
|
98
|
+
lane_id: expectedLaneId,
|
|
99
|
+
expected_senior: expectedSenior,
|
|
100
|
+
actor: record.actor,
|
|
101
|
+
checkpoint: record.checkpoint,
|
|
102
|
+
reason: record.reason,
|
|
103
|
+
approval_id: record.approval_id,
|
|
104
|
+
registry_worker_id: record.registry_worker_id,
|
|
105
|
+
worker_yaml_sha256: record.worker_yaml_sha256,
|
|
106
|
+
at: record.at,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=outpost-registry-worker-approval.js.map
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* must be re-read for every spawn.
|
|
8
8
|
*/
|
|
9
9
|
import { type LaneRecord, type LaneProvider } from "./types.js";
|
|
10
|
+
import type { RegistryWorkerRecord } from "./types.js";
|
|
11
|
+
import { type RegistryPersona } from "./registry-persona.js";
|
|
12
|
+
export declare const REGISTRY_PERSONA_PROMPT_MAX_CHARS = 12000;
|
|
10
13
|
export interface WorkerProfileMeta {
|
|
11
14
|
version?: string;
|
|
12
15
|
provider: LaneProvider;
|
|
@@ -18,6 +21,7 @@ export interface WorkerProfileMeta {
|
|
|
18
21
|
source?: string;
|
|
19
22
|
accounts?: string[];
|
|
20
23
|
keep_alive?: boolean;
|
|
24
|
+
registry_worker?: string;
|
|
21
25
|
}
|
|
22
26
|
export interface WorkerProfile {
|
|
23
27
|
id: string;
|
|
@@ -28,6 +32,7 @@ export interface WorkerProfile {
|
|
|
28
32
|
policyFiles: string[];
|
|
29
33
|
hookFiles: string[];
|
|
30
34
|
contentHash: string;
|
|
35
|
+
registryWorker?: RegistryPersona;
|
|
31
36
|
}
|
|
32
37
|
export interface WorkerProfileRuntime {
|
|
33
38
|
settingsPath?: string;
|
|
@@ -41,6 +46,11 @@ export declare function personalWorkerProfileDir(hqRoot: string, id: string): st
|
|
|
41
46
|
/** A meta.yaml file is the explicit marker for a personal worker profile. */
|
|
42
47
|
export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
|
|
43
48
|
export declare function loadWorkerProfile(hqRoot: string, id: string, options?: LoadWorkerProfileOptions): WorkerProfile;
|
|
49
|
+
export declare function loadWorkerProfileWithRegistry(hqRoot: string, id: string, options?: {
|
|
50
|
+
laneCompany?: string;
|
|
51
|
+
registryWorkerRecord?: RegistryWorkerRecord;
|
|
52
|
+
onUnknownMetaFields?: (fields: readonly string[]) => void;
|
|
53
|
+
}): WorkerProfile;
|
|
44
54
|
export interface EffectiveLaneKeepAlive {
|
|
45
55
|
enabled: boolean;
|
|
46
56
|
source: "lane" | "none";
|
|
@@ -13,11 +13,13 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
|
13
13
|
import { WorkerResolutionError } from "./errors.js";
|
|
14
14
|
import { PROVIDER_EFFORT_LEVELS } from "./effort.js";
|
|
15
15
|
import { LANE_PROVIDERS, isLaneProvider, } from "./types.js";
|
|
16
|
+
import { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
|
|
16
17
|
const PROFILE_META = "meta.yaml";
|
|
17
18
|
const PROFILE_SYSTEM_PROMPT = "system-prompt.md";
|
|
18
19
|
const PROFILE_SETTINGS = "settings.json";
|
|
19
20
|
const PROFILE_POLICIES = "policies";
|
|
20
21
|
const PROFILE_HOOKS = "hooks";
|
|
22
|
+
export const REGISTRY_PERSONA_PROMPT_MAX_CHARS = 12_000;
|
|
21
23
|
const PROFILE_META_KEYS = new Set([
|
|
22
24
|
"version",
|
|
23
25
|
"provider",
|
|
@@ -29,6 +31,7 @@ const PROFILE_META_KEYS = new Set([
|
|
|
29
31
|
"source",
|
|
30
32
|
"accounts",
|
|
31
33
|
"keep_alive",
|
|
34
|
+
"registry_worker",
|
|
32
35
|
]);
|
|
33
36
|
function isPlainObject(value) {
|
|
34
37
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -214,6 +217,7 @@ function parseMeta(id, dir, raw, options = {}) {
|
|
|
214
217
|
}
|
|
215
218
|
accounts = raw.accounts.map((item) => item.trim());
|
|
216
219
|
}
|
|
220
|
+
const registryWorker = optionalString("registry_worker");
|
|
217
221
|
return {
|
|
218
222
|
...(version !== undefined ? { version } : {}),
|
|
219
223
|
provider: providerValue,
|
|
@@ -225,6 +229,7 @@ function parseMeta(id, dir, raw, options = {}) {
|
|
|
225
229
|
...(maxConcurrent !== undefined ? { max_concurrent: maxConcurrent } : {}),
|
|
226
230
|
...(source !== undefined ? { source } : {}),
|
|
227
231
|
...(accounts !== undefined ? { accounts } : {}),
|
|
232
|
+
...(registryWorker !== undefined ? { registry_worker: registryWorker } : {}),
|
|
228
233
|
};
|
|
229
234
|
}
|
|
230
235
|
function validatePolicyFrontmatter(id, profileDir, relative) {
|
|
@@ -373,6 +378,39 @@ export function loadWorkerProfile(hqRoot, id, options = {}) {
|
|
|
373
378
|
contentHash: hashProfileFiles(dir, hashFiles),
|
|
374
379
|
};
|
|
375
380
|
}
|
|
381
|
+
export function loadWorkerProfileWithRegistry(hqRoot, id, options = {}) {
|
|
382
|
+
const profile = loadWorkerProfile(hqRoot, id, options);
|
|
383
|
+
const requestedId = profile.meta.registry_worker;
|
|
384
|
+
if (requestedId === undefined) {
|
|
385
|
+
if (options.registryWorkerRecord !== undefined) {
|
|
386
|
+
throw new WorkerResolutionError("registry_worker_invalid", `Worker profile ${JSON.stringify(id)} no longer names its recorded registry worker`, { worker: id, registry_worker: options.registryWorkerRecord.id });
|
|
387
|
+
}
|
|
388
|
+
return profile;
|
|
389
|
+
}
|
|
390
|
+
const persona = options.registryWorkerRecord !== undefined
|
|
391
|
+
? (() => {
|
|
392
|
+
if (options.registryWorkerRecord?.id !== requestedId) {
|
|
393
|
+
throw new WorkerResolutionError("registry_worker_invalid", `Worker profile ${JSON.stringify(id)} names registry worker ${JSON.stringify(requestedId)}, but the lane records ${JSON.stringify(options.registryWorkerRecord?.id)}`, {
|
|
394
|
+
worker: id,
|
|
395
|
+
profile_registry_worker: requestedId,
|
|
396
|
+
lane_registry_worker: options.registryWorkerRecord?.id,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return loadRegistryPersonaFromRecord(hqRoot, options.registryWorkerRecord);
|
|
400
|
+
})()
|
|
401
|
+
: resolveRegistryPersona(hqRoot, requestedId, options.laneCompany);
|
|
402
|
+
const record = registryWorkerRecord(persona);
|
|
403
|
+
const hash = createHash("sha256")
|
|
404
|
+
.update(profile.contentHash)
|
|
405
|
+
.update("\0registry_worker\0")
|
|
406
|
+
.update(JSON.stringify({ id: record.id, path: record.path, worker_yaml_sha256: record.worker_yaml_sha256 }))
|
|
407
|
+
.digest("hex");
|
|
408
|
+
return {
|
|
409
|
+
...profile,
|
|
410
|
+
registryWorker: persona,
|
|
411
|
+
contentHash: `sha256:${hash}`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
376
414
|
/** Resolve keep-alive from the lane record; profile metadata only seeds new spawns. */
|
|
377
415
|
export function resolveLaneKeepAlive(_hqRoot, lane) {
|
|
378
416
|
return lane.keep_alive === undefined
|
|
@@ -381,6 +419,9 @@ export function resolveLaneKeepAlive(_hqRoot, lane) {
|
|
|
381
419
|
}
|
|
382
420
|
export function workerProfilePrompt(profile, delivery = "prompt") {
|
|
383
421
|
const parts = [profile.systemPrompt.trimEnd()];
|
|
422
|
+
if (profile.registryWorker !== undefined) {
|
|
423
|
+
parts.push(formatRegistryWorkerPrompt(profile.registryWorker));
|
|
424
|
+
}
|
|
384
425
|
if (delivery === "hook")
|
|
385
426
|
return parts.join("\n");
|
|
386
427
|
for (const relative of profile.policyFiles) {
|
|
@@ -389,6 +430,53 @@ export function workerProfilePrompt(profile, delivery = "prompt") {
|
|
|
389
430
|
}
|
|
390
431
|
return parts.join("\n");
|
|
391
432
|
}
|
|
433
|
+
function formatRegistryWorkerPrompt(persona) {
|
|
434
|
+
const lines = [
|
|
435
|
+
"## Registry worker",
|
|
436
|
+
`Description: ${persona.description}`,
|
|
437
|
+
"",
|
|
438
|
+
"Instructions:",
|
|
439
|
+
persona.instructions || "No additional instructions were supplied.",
|
|
440
|
+
];
|
|
441
|
+
if (persona.skillFiles.length > 0) {
|
|
442
|
+
lines.push("", "Skills (path references only; do not inline these files):");
|
|
443
|
+
for (const file of persona.skillFiles)
|
|
444
|
+
lines.push(`- ${file}`);
|
|
445
|
+
}
|
|
446
|
+
if (persona.postExecute.length > 0) {
|
|
447
|
+
lines.push("", "Done criteria:");
|
|
448
|
+
for (const command of persona.postExecute)
|
|
449
|
+
lines.push(`- ${command}`);
|
|
450
|
+
}
|
|
451
|
+
const safetyLines = [];
|
|
452
|
+
if (persona.humanCheckpoints.length > 0) {
|
|
453
|
+
safetyLines.push("Human checkpoints:");
|
|
454
|
+
for (const checkpoint of persona.humanCheckpoints) {
|
|
455
|
+
safetyLines.push(`- STOP before proceeding with ${checkpoint}. Report that this checkpoint is reached to the senior and wait for approval. Do not perform the action before it is approved.`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else if (persona.approvalRequired) {
|
|
459
|
+
safetyLines.push("Human checkpoint:", "- STOP before any external publish action. Report checkpoint external_publish to the senior and wait for approval. Do not perform the action before it is approved.");
|
|
460
|
+
}
|
|
461
|
+
if (persona.approvalRequired || persona.humanCheckpoints.length > 0) {
|
|
462
|
+
safetyLines.push("For an approved external action, include the exact approved checkpoint in the artifact's checkpoint field. Each approval applies to the next matching envelope only.");
|
|
463
|
+
}
|
|
464
|
+
if (safetyLines.length > 0)
|
|
465
|
+
lines.push("");
|
|
466
|
+
const complete = [...lines, ...safetyLines].join("\n");
|
|
467
|
+
if (complete.length <= REGISTRY_PERSONA_PROMPT_MAX_CHARS)
|
|
468
|
+
return complete;
|
|
469
|
+
const suffix = [
|
|
470
|
+
"",
|
|
471
|
+
`[Registry worker section truncated at ${REGISTRY_PERSONA_PROMPT_MAX_CHARS} characters; descriptive material, skill paths, or done criteria may be omitted.]`,
|
|
472
|
+
...(safetyLines.length > 0 ? ["", ...safetyLines] : []),
|
|
473
|
+
].join("\n");
|
|
474
|
+
const prefixBudget = REGISTRY_PERSONA_PROMPT_MAX_CHARS - suffix.length;
|
|
475
|
+
if (prefixBudget < 0) {
|
|
476
|
+
throw new WorkerResolutionError("registry_worker_invalid", `Registry worker ${JSON.stringify(persona.id)} checkpoint instructions exceed the bounded prompt limit`, { worker: persona.id, max_chars: REGISTRY_PERSONA_PROMPT_MAX_CHARS });
|
|
477
|
+
}
|
|
478
|
+
return `${lines.join("\n").slice(0, prefixBudget).trimEnd()}${suffix}`;
|
|
479
|
+
}
|
|
392
480
|
export function ignoredWorkerProfileFields(provider) {
|
|
393
481
|
return provider === "claude" ? [] : ["settings", "hooks"];
|
|
394
482
|
}
|
|
@@ -11,6 +11,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
|
11
11
|
import { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
|
|
12
12
|
import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
|
|
13
13
|
import { effectiveAccountHome } from "./account-home.js";
|
|
14
|
+
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
14
15
|
import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
|
|
15
16
|
import { MAX_LANE_DEPTH } from "./constants.js";
|
|
16
17
|
import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
|
|
@@ -1059,13 +1060,22 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1059
1060
|
process.stderr.write(`hq lanes reconcile: terminal ask refused for ${lane.lane_id}: ${questionRefusalCode}\n`);
|
|
1060
1061
|
}
|
|
1061
1062
|
}
|
|
1063
|
+
let terminalEnvelope = latest?.envelope;
|
|
1064
|
+
let terminalClassification = repairedClassification;
|
|
1062
1065
|
const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) => {
|
|
1066
|
+
const approvalGate = latest?.envelope
|
|
1067
|
+
? enforceRegistryApprovalGate(current, latest.envelope)
|
|
1068
|
+
: undefined;
|
|
1069
|
+
terminalEnvelope = approvalGate?.envelope ?? latest?.envelope;
|
|
1070
|
+
terminalClassification = approvalGate?.violation
|
|
1071
|
+
? { state: "blocked", reason: "approval_gate_violated" }
|
|
1072
|
+
: repairedClassification;
|
|
1063
1073
|
const next = {
|
|
1064
1074
|
...current,
|
|
1065
|
-
state:
|
|
1075
|
+
state: terminalClassification.state,
|
|
1066
1076
|
queued_reason: undefined,
|
|
1067
|
-
terminal_reason:
|
|
1068
|
-
interrupted_at:
|
|
1077
|
+
terminal_reason: terminalClassification.reason ?? "host_lost",
|
|
1078
|
+
interrupted_at: terminalClassification.state === "interrupted" ? nowIso : undefined,
|
|
1069
1079
|
...(providerRejection
|
|
1070
1080
|
? { terminal_details: providerRejectionDetails(providerRejection, current) }
|
|
1071
1081
|
: {}),
|
|
@@ -1073,8 +1083,16 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1073
1083
|
...(quota?.marker ? { model_quota_marker: quota.marker } : {}),
|
|
1074
1084
|
last_host_report_at: nowIso,
|
|
1075
1085
|
};
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1086
|
+
if (approvalGate && !approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
|
|
1087
|
+
next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
|
|
1088
|
+
event: "consumed",
|
|
1089
|
+
approval_id: approvalId,
|
|
1090
|
+
envelope_ref: latest.ref,
|
|
1091
|
+
at: nowIso,
|
|
1092
|
+
}), current.registry_worker_approval_history ?? []);
|
|
1093
|
+
}
|
|
1094
|
+
if (terminalEnvelope && latest) {
|
|
1095
|
+
const envelope = terminalEnvelope;
|
|
1078
1096
|
next.story_status = envelope.story_status;
|
|
1079
1097
|
if (envelope.story_complete !== undefined) {
|
|
1080
1098
|
next.story_complete = envelope.story_complete;
|
|
@@ -1095,6 +1113,15 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1095
1113
|
: { blocked_reason: undefined }),
|
|
1096
1114
|
}
|
|
1097
1115
|
: story);
|
|
1116
|
+
if (approvalGate?.violation) {
|
|
1117
|
+
next.last_envelope = {
|
|
1118
|
+
ref: latest.ref,
|
|
1119
|
+
decision: envelope.decision,
|
|
1120
|
+
at: nowIso,
|
|
1121
|
+
artifacts: envelope.artifacts,
|
|
1122
|
+
blocked_reason: envelope.blocked_reason,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1098
1125
|
}
|
|
1099
1126
|
return next;
|
|
1100
1127
|
}, deps.dryRun);
|
|
@@ -1102,20 +1129,20 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1102
1129
|
summary.dead_host.push({
|
|
1103
1130
|
id: lane.lane_id,
|
|
1104
1131
|
from: "running",
|
|
1105
|
-
to:
|
|
1106
|
-
reason:
|
|
1132
|
+
to: terminalClassification.state,
|
|
1133
|
+
reason: terminalClassification.reason ?? "host_lost",
|
|
1107
1134
|
});
|
|
1108
|
-
if (latest
|
|
1135
|
+
if (terminalEnvelope && latest && questionRefusalCode === null) {
|
|
1109
1136
|
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
1110
1137
|
if (mesh) {
|
|
1111
1138
|
const meshEmitTimeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
|
|
1112
1139
|
let timer;
|
|
1113
1140
|
try {
|
|
1114
|
-
const questionIds =
|
|
1115
|
-
? envelopeQuestionIds(lane.lane_id, latest.ref,
|
|
1141
|
+
const questionIds = terminalEnvelope.decision === "ask"
|
|
1142
|
+
? envelopeQuestionIds(lane.lane_id, latest.ref, terminalEnvelope)
|
|
1116
1143
|
: undefined;
|
|
1117
1144
|
await Promise.race([
|
|
1118
|
-
emitLaneDecisionEvents(mesh, result.record,
|
|
1145
|
+
emitLaneDecisionEvents(mesh, result.record, terminalEnvelope, { questionIds }),
|
|
1119
1146
|
new Promise((_resolve, reject) => {
|
|
1120
1147
|
timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${meshEmitTimeoutMs}ms`)), meshEmitTimeoutMs);
|
|
1121
1148
|
}),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RegistryWorkerRecord } from "./types.js";
|
|
2
|
+
export interface RegistryPersona {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Registry-relative worker directory. Kept for stable resume resolution. */
|
|
5
|
+
path: string;
|
|
6
|
+
workerYamlSha256: string;
|
|
7
|
+
description: string;
|
|
8
|
+
instructions: string;
|
|
9
|
+
/** Skill files are path references only. Their contents are never inlined. */
|
|
10
|
+
skillFiles: string[];
|
|
11
|
+
postExecute: string[];
|
|
12
|
+
approvalRequired: boolean;
|
|
13
|
+
humanCheckpoints: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Resolve an active registry worker for a lane without company fallback. */
|
|
16
|
+
export declare function resolveRegistryPersona(hqRoot: string, id: string, laneCompany: string | undefined): RegistryPersona;
|
|
17
|
+
/** Re-read the recorded path on resume; registry changes cannot redirect a lane. */
|
|
18
|
+
export declare function loadRegistryPersonaFromRecord(hqRoot: string, record: RegistryWorkerRecord): RegistryPersona;
|
|
19
|
+
export declare function registryWorkerRecord(persona: RegistryPersona): RegistryWorkerRecord;
|
|
20
|
+
//# sourceMappingURL=registry-persona.d.ts.map
|