@indigoai-us/hq-cli 5.159.0 → 5.161.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.
@@ -10,10 +10,10 @@ import { type LaneProvider } from "./types.js";
10
10
  export interface WorkerProfileMeta {
11
11
  version?: string;
12
12
  provider: LaneProvider;
13
- model: string;
14
- account: string;
15
- effort: string;
16
- fast_mode: boolean;
13
+ model?: string;
14
+ account?: string;
15
+ effort?: string;
16
+ fast_mode?: boolean;
17
17
  max_concurrent?: number;
18
18
  source?: string;
19
19
  accounts?: string[];
@@ -36,7 +36,7 @@ export declare function personalWorkerProfileDir(hqRoot: string, id: string): st
36
36
  /** A meta.yaml file is the explicit marker for a personal worker profile. */
37
37
  export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
38
38
  export declare function loadWorkerProfile(hqRoot: string, id: string): WorkerProfile;
39
- export declare function workerProfilePrompt(profile: WorkerProfile): string;
39
+ export declare function workerProfilePrompt(profile: WorkerProfile, delivery?: "hook" | "prompt"): string;
40
40
  export declare function ignoredWorkerProfileFields(provider: LaneProvider): string[];
41
41
  /**
42
42
  * Materialize provider settings and hook files in the lane run directory.
@@ -135,20 +135,22 @@ function parseMeta(id, dir, raw) {
135
135
  });
136
136
  }
137
137
  const providerValue = provider.trim();
138
- const requiredString = (field) => {
138
+ const optionalString = (field) => {
139
139
  const value = raw[field];
140
+ if (value === undefined)
141
+ return undefined;
140
142
  if (typeof value !== "string" || value.trim().length === 0) {
141
- throw profileError(id, dir, `meta.yaml ${field} must be a non-empty string`, {
143
+ throw profileError(id, dir, `meta.yaml ${field} must be a non-empty string when present`, {
142
144
  field,
143
145
  value,
144
146
  });
145
147
  }
146
148
  return value.trim();
147
149
  };
148
- const model = requiredString("model");
149
- const account = requiredString("account");
150
- const effort = requiredString("effort");
151
- if (!PROVIDER_EFFORT_LEVELS[providerValue].includes(effort)) {
150
+ const model = optionalString("model");
151
+ const account = optionalString("account");
152
+ const effort = optionalString("effort");
153
+ if (effort !== undefined && !PROVIDER_EFFORT_LEVELS[providerValue].includes(effort)) {
152
154
  throw profileError(id, dir, `meta.yaml effort ${JSON.stringify(effort)} is invalid for ${providerValue}`, {
153
155
  field: "effort",
154
156
  provider: providerValue,
@@ -156,8 +158,8 @@ function parseMeta(id, dir, raw) {
156
158
  accepted: [...PROVIDER_EFFORT_LEVELS[providerValue]],
157
159
  });
158
160
  }
159
- if (typeof raw.fast_mode !== "boolean") {
160
- throw profileError(id, dir, "meta.yaml fast_mode must be a boolean", {
161
+ if (raw.fast_mode !== undefined && typeof raw.fast_mode !== "boolean") {
162
+ throw profileError(id, dir, "meta.yaml fast_mode must be a boolean when present", {
161
163
  field: "fast_mode",
162
164
  value: raw.fast_mode,
163
165
  });
@@ -205,15 +207,45 @@ function parseMeta(id, dir, raw) {
205
207
  return {
206
208
  ...(version !== undefined ? { version } : {}),
207
209
  provider: providerValue,
208
- model,
209
- account,
210
- effort,
211
- fast_mode: raw.fast_mode,
210
+ ...(model !== undefined ? { model } : {}),
211
+ ...(account !== undefined ? { account } : {}),
212
+ ...(effort !== undefined ? { effort } : {}),
213
+ ...(raw.fast_mode !== undefined ? { fast_mode: raw.fast_mode } : {}),
212
214
  ...(maxConcurrent !== undefined ? { max_concurrent: maxConcurrent } : {}),
213
215
  ...(source !== undefined ? { source } : {}),
214
216
  ...(accounts !== undefined ? { accounts } : {}),
215
217
  };
216
218
  }
219
+ function validatePolicyFrontmatter(id, profileDir, relative) {
220
+ const filePath = path.join(profileDir, relative);
221
+ const content = readRequiredText(id, profileDir, filePath, relative);
222
+ const lines = content.split(/\r?\n/);
223
+ if (lines[0] !== "---") {
224
+ throw new WorkerResolutionError("worker_policy_invalid", `Worker policy ${filePath} must start with YAML frontmatter`, { worker: id, file: filePath, field: "frontmatter" });
225
+ }
226
+ const closing = lines.findIndex((line, index) => index > 0 && line === "---");
227
+ if (closing < 0 || closing === 1) {
228
+ throw new WorkerResolutionError("worker_policy_invalid", `Worker policy ${filePath} must contain a non-empty YAML frontmatter block`, { worker: id, file: filePath, field: "frontmatter" });
229
+ }
230
+ let frontmatter;
231
+ try {
232
+ frontmatter = parseUserYaml(lines.slice(1, closing).join("\n"), filePath);
233
+ }
234
+ catch (err) {
235
+ throw new WorkerResolutionError("worker_policy_invalid", `Worker policy ${filePath} has invalid YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, { worker: id, file: filePath, field: "frontmatter" });
236
+ }
237
+ if (!isPlainObject(frontmatter)) {
238
+ throw new WorkerResolutionError("worker_policy_invalid", `Worker policy ${filePath} frontmatter must contain a YAML object`, { worker: id, file: filePath, field: "frontmatter" });
239
+ }
240
+ if (typeof frontmatter.when !== "string" || frontmatter.when.trim().length === 0) {
241
+ throw new WorkerResolutionError("worker_policy_invalid", `Worker policy ${filePath} frontmatter must include a non-empty when field`, { worker: id, file: filePath, field: "when" });
242
+ }
243
+ if (!Array.isArray(frontmatter.on) ||
244
+ frontmatter.on.length === 0 ||
245
+ frontmatter.on.some((value) => typeof value !== "string" || value.trim().length === 0)) {
246
+ throw new WorkerResolutionError("worker_policy_invalid", `Worker policy ${filePath} frontmatter must include a non-empty on string array`, { worker: id, file: filePath, field: "on" });
247
+ }
248
+ }
217
249
  function readSettings(id, dir) {
218
250
  const filePath = path.join(dir, PROFILE_SETTINGS);
219
251
  const stat = lstatOrAbsent(filePath);
@@ -308,6 +340,9 @@ export function loadWorkerProfile(hqRoot, id) {
308
340
  }
309
341
  const settingsResult = readSettings(id, dir);
310
342
  const policyFiles = collectFiles(id, dir, PROFILE_POLICIES);
343
+ for (const relative of policyFiles.filter((file) => file.toLowerCase().endsWith(".md"))) {
344
+ validatePolicyFrontmatter(id, dir, relative);
345
+ }
311
346
  const hookFiles = collectFiles(id, dir, PROFILE_HOOKS);
312
347
  const hashFiles = [
313
348
  PROFILE_META,
@@ -327,8 +362,10 @@ export function loadWorkerProfile(hqRoot, id) {
327
362
  contentHash: hashProfileFiles(dir, hashFiles),
328
363
  };
329
364
  }
330
- export function workerProfilePrompt(profile) {
365
+ export function workerProfilePrompt(profile, delivery = "prompt") {
331
366
  const parts = [profile.systemPrompt.trimEnd()];
367
+ if (delivery === "hook")
368
+ return parts.join("\n");
332
369
  for (const relative of profile.policyFiles) {
333
370
  const content = readRequiredText(profile.id, profile.dir, path.join(profile.dir, relative), relative);
334
371
  parts.push(`\n## Profile policy: ${relative}\n${content.trimEnd()}`);
@@ -15,12 +15,12 @@ import { MAX_LANE_DEPTH } from "./constants.js";
15
15
  import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
16
16
  import { loadWorkerProfile } from "./profile.js";
17
17
  import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, } from "./liveness.js";
18
- import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
18
+ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
19
19
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
20
20
  import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
21
21
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
22
22
  import { answeredQuestionEntries, raiseEnvelopeQuestions } from "./questions.js";
23
- import { SILENCE_MS, classifyLaneExit, isPidAlive, readLatestEnvelope, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
23
+ import { SILENCE_MS, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
24
24
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
25
25
  import { laneStories, storyStatusForEnvelope } from "./stories.js";
26
26
  import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
@@ -570,9 +570,11 @@ async function withReconcileLock(hqRoot, fn) {
570
570
  }
571
571
  }
572
572
  function classifyDeadLocalLane(hqRoot, laneId) {
573
- const latest = readLatestEnvelopeRecord(hqRoot, laneId);
574
- const envelope = latest?.envelope ?? readLatestEnvelope(hqRoot, laneId);
575
573
  const runDir = laneRunDir(hqRoot, laneId);
574
+ const manifest = readSpawnManifest(runDir);
575
+ const minEnvelopeName = manifest.envelope_path ? path.basename(manifest.envelope_path) : undefined;
576
+ const latest = readLatestEnvelopeRecord(hqRoot, laneId, minEnvelopeName);
577
+ const envelope = latest?.envelope ?? null;
576
578
  const hasPartialOutput = [
577
579
  path.join(runDir, "logs", "worker.out"),
578
580
  path.join(runDir, "logs", "worker.err"),
@@ -896,22 +898,31 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
896
898
  // Match the watcher's one-repair decision before classifying an
897
899
  // unparseable envelope after both processes have died. As in the
898
900
  // watcher, a provider rejection or a quota signal skips the repair.
899
- const rawLatest = readLatestEnvelopeFile(hqRoot, lane.lane_id);
901
+ const runDir = laneRunDir(hqRoot, lane.lane_id);
902
+ const manifest = readSpawnManifest(runDir);
903
+ const minEnvelopeName = manifest.envelope_path ? path.basename(manifest.envelope_path) : undefined;
904
+ const rawLatest = readLatestEnvelopeFile(hqRoot, lane.lane_id, minEnvelopeName);
905
+ const missingExpected = rawLatest === null
906
+ ? missingExpectedEnvelopeFile(hqRoot, lane.lane_id, manifest.envelope_path)
907
+ : null;
908
+ const repairCandidate = rawLatest ?? missingExpected;
900
909
  let invalidEnvelope = null;
901
910
  if (!deps.dryRun &&
902
911
  !providerRejection &&
903
912
  !quota &&
904
- rawLatest &&
905
- rawLatest.envelope === null) {
913
+ repairCandidate &&
914
+ repairCandidate.envelope === null &&
915
+ (!repairCandidate.missing || envelopeRepairMode(classified.provider) === "resume")) {
906
916
  const spawnRepair = deps.spawnRepair ??
907
917
  ((root, repairLane, input) => spawnEnvelopeRepair(root, repairLane, input, deps.spawnLaneDeps));
908
- const repair = decideEnvelopeRepair(hqRoot, lane.lane_id, rawLatest, nowMs, spawnRepair, () => undefined);
918
+ const repair = decideEnvelopeRepair(hqRoot, lane.lane_id, repairCandidate, nowMs, spawnRepair, () => undefined);
909
919
  if (repair.action === "spawned") {
910
920
  armLaneWatcher(hqRoot, lane.lane_id, deps.spawnLaneDeps);
911
921
  summary.heartbeats.push(`${lane.lane_id}:envelope-repair`);
912
922
  continue;
913
923
  }
914
- invalidEnvelope = { errors: repair.errors };
924
+ if (!repairCandidate.missing)
925
+ invalidEnvelope = { errors: repair.errors };
915
926
  }
916
927
  const repairedClassification = invalidEnvelope
917
928
  ? classifyLaneExit({ envelope: null, hasPartialOutput: false, invalidEnvelope })
@@ -198,6 +198,7 @@ export declare function envelopeRepairPrompt(input: {
198
198
  envelopePath: string;
199
199
  error: string;
200
200
  rawText: string;
201
+ missing?: boolean;
201
202
  }): string;
202
203
  export interface ProviderResumeInput {
203
204
  session_id: string;
@@ -228,7 +229,10 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
228
229
  export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
229
230
  envVar: string;
230
231
  home: string;
231
- }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef): NodeJS.ProcessEnv;
232
+ }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string): NodeJS.ProcessEnv;
233
+ export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
234
+ /** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
235
+ export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
232
236
  export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonRaw: string, details: Record<string, unknown>): LaneRecord;
233
237
  /**
234
238
  * Spec §5.2 executing-side check. Compares this machine's identity to
@@ -251,6 +255,11 @@ export interface SpawnManifestFile {
251
255
  worker_profile_hash?: string;
252
256
  worker_profile_ignored?: string[];
253
257
  worker_profile_settings?: string;
258
+ worker_policy_delivery?: "hook" | "prompt";
259
+ worker_policy_dir?: string;
260
+ worker_policy_env?: {
261
+ HQ_POLICY_WORKER_DIR: string;
262
+ };
254
263
  }
255
264
  /** Read the per-run manifest. Missing file or field reads as absent. */
256
265
  export declare function readSpawnManifest(runDir: string): SpawnManifestFile;
@@ -282,6 +291,8 @@ export interface EnvelopeRepairSpawnInput {
282
291
  envelopePath: string;
283
292
  error: string;
284
293
  rawText: string;
294
+ /** True when this run never wrote its expected envelope file. */
295
+ missing?: boolean;
285
296
  }
286
297
  /** How the spec §4 repair turn reaches the provider (recorded as `envelope_repair.mode`). */
287
298
  export type EnvelopeRepairMode = "resume" | "fresh_turn";
@@ -1013,6 +1013,17 @@ export function envelopeRepairPrompt(input) {
1013
1013
  const rawText = input.rawText.length <= maxRawTextChars
1014
1014
  ? input.rawText
1015
1015
  : `${input.rawText.slice(0, 12_000)}\n\n[... raw envelope truncated; inspect ${input.envelopePath} for the complete file ...]\n\n${input.rawText.slice(-12_000)}`;
1016
+ if (input.missing) {
1017
+ return [
1018
+ `Missing expected envelope file: ${input.envelopePath}`,
1019
+ "The previous envelope is stale and must not be reused or copied into this run's result.",
1020
+ "Printing an envelope in the conversation or writing it to last-message.txt is not enough; write the JSON object directly to the expected file.",
1021
+ "Schema:",
1022
+ JSON.stringify(LANE_ENVELOPE_JSON_SCHEMA, null, 2),
1023
+ `Write the corrected JSON object to ${input.envelopePath}, overwriting it. ` +
1024
+ `Output only that JSON. Do not add keys the schema does not name, and do not resume the task.`,
1025
+ ].join("\n\n");
1026
+ }
1016
1027
  return [
1017
1028
  `The file you wrote at ${input.envelopePath} is not a valid hq-lane-envelope.v1. ` +
1018
1029
  `This is the single repair turn: rewrite that file so it validates, and do nothing else.`,
@@ -1119,7 +1130,7 @@ export function buildProviderCommand(provider, input) {
1119
1130
  args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, appendProviderProfileFlags(parsed, args, input.profile), input.model), input.effort), input.fastMode),
1120
1131
  };
1121
1132
  }
1122
- export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany) {
1133
+ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir) {
1123
1134
  const accepted = Object.values(PROVIDER_HOME_ENV);
1124
1135
  if (!accepted.includes(resolved.envVar)) {
1125
1136
  throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
@@ -1127,6 +1138,7 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
1127
1138
  const env = { ...base };
1128
1139
  delete env.HQ_PROJECT_ID;
1129
1140
  delete env.HQ_STORY_ID;
1141
+ delete env.HQ_POLICY_WORKER_DIR;
1130
1142
  env[resolved.envVar] = resolved.home;
1131
1143
  delete env[CLAUDE_AUTO_COMPACT_ENV];
1132
1144
  if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
@@ -1134,6 +1146,10 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
1134
1146
  }
1135
1147
  env.HQ_LANE_ID = laneId;
1136
1148
  env.HQ_ROOT = hqRoot;
1149
+ if (workerPolicyDir !== undefined &&
1150
+ (resolved.envVar === PROVIDER_HOME_ENV.claude || resolved.envVar === PROVIDER_HOME_ENV.codex)) {
1151
+ env.HQ_POLICY_WORKER_DIR = workerPolicyDir;
1152
+ }
1137
1153
  if (meshCompany) {
1138
1154
  env.HQ_SPAWN_COMPANY = meshCompany.slug || meshCompany.uid;
1139
1155
  }
@@ -1142,6 +1158,28 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
1142
1158
  }
1143
1159
  return env;
1144
1160
  }
1161
+ export const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
1162
+ function installedWorkerPolicyHookSupportsDirectory(hqRoot) {
1163
+ const hookPath = path.join(hqRoot, ".claude", "hooks", "inject-policy-on-trigger.sh");
1164
+ try {
1165
+ return fs.readFileSync(hookPath, "utf8")
1166
+ .split(/\r?\n/)
1167
+ .some((line) => line === WORKER_POLICY_HOOK_CAPABILITY);
1168
+ }
1169
+ catch (err) {
1170
+ const code = err.code;
1171
+ if (code === "ENOENT")
1172
+ return false;
1173
+ process.stderr.write(`hq lanes: cannot inspect worker-policy hook capability at ${hookPath}: ${err instanceof Error ? err.message : String(err)}; keeping policy prompt delivery\n`);
1174
+ return false;
1175
+ }
1176
+ }
1177
+ /** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
1178
+ export function workerPolicyDeliveryMode(hqRoot, provider) {
1179
+ return provider !== "grok" && installedWorkerPolicyHookSupportsDirectory(hqRoot)
1180
+ ? "hook"
1181
+ : "prompt";
1182
+ }
1145
1183
  function ensureDir(dir) {
1146
1184
  fs.mkdirSync(dir, { recursive: true });
1147
1185
  }
@@ -1331,6 +1369,13 @@ function writeSpawnManifest(runDir, input) {
1331
1369
  worker_profile: input.profile.id,
1332
1370
  worker_profile_hash: input.profile.hash,
1333
1371
  worker_profile_ignored: [...input.profile.ignoredFields],
1372
+ worker_policy_delivery: input.profile.policyDelivery,
1373
+ ...(input.profile.policyDir !== undefined
1374
+ ? {
1375
+ worker_policy_dir: input.profile.policyDir,
1376
+ worker_policy_env: { HQ_POLICY_WORKER_DIR: input.profile.policyDir },
1377
+ }
1378
+ : {}),
1334
1379
  ...(input.profile.settingsPath
1335
1380
  ? { worker_profile_settings: input.profile.settingsPath }
1336
1381
  : {}),
@@ -1513,12 +1558,18 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1513
1558
  const ignoredProfileFields = profile
1514
1559
  ? ignoredWorkerProfileFields(existing.provider)
1515
1560
  : [];
1561
+ const policyDelivery = profile
1562
+ ? workerPolicyDeliveryMode(hqRoot, existing.provider)
1563
+ : undefined;
1564
+ const workerPolicyDir = profile !== undefined && existing.provider !== "grok"
1565
+ ? path.relative(hqRoot, profile.dir).split(path.sep).join("/")
1566
+ : undefined;
1516
1567
  const profileRuntime = profile
1517
1568
  ? materializeWorkerProfile(profile, runDir)
1518
1569
  : undefined;
1519
1570
  const profileLaunch = profile
1520
1571
  ? {
1521
- systemPrompt: workerProfilePrompt(profile),
1572
+ systemPrompt: workerProfilePrompt(profile, policyDelivery ?? "prompt"),
1522
1573
  ...(profileRuntime?.settingsPath !== undefined
1523
1574
  ? { settingsPath: profileRuntime.settingsPath }
1524
1575
  : {}),
@@ -1637,7 +1688,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1637
1688
  const autoCompactTokens = lane.provider === "claude"
1638
1689
  ? providerAutoCompactTokens(config.providers.claude)
1639
1690
  : undefined;
1640
- const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined);
1691
+ const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
1641
1692
  const model = resolveLaneModel({
1642
1693
  cliModel: pinnedModel,
1643
1694
  profileModel,
@@ -1652,10 +1703,8 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1652
1703
  model,
1653
1704
  });
1654
1705
  const fastMode = lane.provider === "codex"
1655
- ? profileDefaultsApply
1656
- ? profile?.meta.fast_mode === true
1657
- : model !== undefined &&
1658
- (config.providers.codex.fast_mode_models ?? []).includes(model)
1706
+ ? (profileDefaultsApply ? profile?.meta.fast_mode : undefined) ??
1707
+ (model !== undefined && (config.providers.codex.fast_mode_models ?? []).includes(model))
1659
1708
  : false;
1660
1709
  const resume = plan.mode === "resume"
1661
1710
  ? {
@@ -1786,7 +1835,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1786
1835
  ? {
1787
1836
  model_source: pinnedModel !== undefined
1788
1837
  ? "cli"
1789
- : profileDefaultsApply
1838
+ : profileModel !== undefined
1790
1839
  ? "profile"
1791
1840
  : accountCfg?.model !== undefined
1792
1841
  ? "account"
@@ -1801,8 +1850,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1801
1850
  ? {
1802
1851
  worker_profile_hash: profile.contentHash,
1803
1852
  worker_profile_ignored: [...ignoredProfileFields],
1853
+ worker_policy_delivery: policyDelivery,
1804
1854
  }
1805
- : {}),
1855
+ : { worker_policy_delivery: undefined }),
1806
1856
  };
1807
1857
  if (resume !== undefined) {
1808
1858
  next.provider_session_id = resume.session_id;
@@ -1901,6 +1951,8 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1901
1951
  id: profile.id,
1902
1952
  hash: profile.contentHash,
1903
1953
  ignoredFields: ignoredProfileFields,
1954
+ policyDelivery: policyDelivery ?? "prompt",
1955
+ ...(workerPolicyDir !== undefined ? { policyDir: workerPolicyDir } : {}),
1904
1956
  ...(profileRuntime?.settingsPath !== undefined
1905
1957
  ? { settingsPath: profileRuntime.settingsPath }
1906
1958
  : {}),
@@ -1985,12 +2037,18 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
1985
2037
  const ignoredProfileFields = profile
1986
2038
  ? ignoredWorkerProfileFields(lane.provider)
1987
2039
  : [];
2040
+ const policyDelivery = profile
2041
+ ? workerPolicyDeliveryMode(hqRoot, lane.provider)
2042
+ : undefined;
2043
+ const workerPolicyDir = profile !== undefined && lane.provider !== "grok"
2044
+ ? path.relative(hqRoot, profile.dir).split(path.sep).join("/")
2045
+ : undefined;
1988
2046
  const profileRuntime = profile
1989
2047
  ? materializeWorkerProfile(profile, runDir)
1990
2048
  : undefined;
1991
2049
  const profileLaunch = profile
1992
2050
  ? {
1993
- systemPrompt: workerProfilePrompt(profile),
2051
+ systemPrompt: workerProfilePrompt(profile, policyDelivery ?? "prompt"),
1994
2052
  ...(profileRuntime?.settingsPath !== undefined
1995
2053
  ? { settingsPath: profileRuntime.settingsPath }
1996
2054
  : {}),
@@ -2014,7 +2072,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2014
2072
  const autoCompactTokens = lane.provider === "claude"
2015
2073
  ? providerAutoCompactTokens(config.providers.claude)
2016
2074
  : undefined;
2017
- const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined);
2075
+ const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
2018
2076
  const accountCfg = config.providers[lane.provider].accounts[resolved.id];
2019
2077
  const model = resolveLaneModel({
2020
2078
  cliModel: pinnedModel,
@@ -2030,15 +2088,14 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2030
2088
  model,
2031
2089
  });
2032
2090
  const fastMode = lane.provider === "codex"
2033
- ? profileDefaultsApply
2034
- ? profile?.meta.fast_mode === true
2035
- : model !== undefined &&
2036
- (config.providers.codex.fast_mode_models ?? []).includes(model)
2091
+ ? (profileDefaultsApply ? profile?.meta.fast_mode : undefined) ??
2092
+ (model !== undefined && (config.providers.codex.fast_mode_models ?? []).includes(model))
2037
2093
  : false;
2038
2094
  const repairPrompt = envelopeRepairPrompt({
2039
2095
  envelopePath: input.envelopePath,
2040
2096
  error: input.error,
2041
2097
  rawText: input.rawText,
2098
+ missing: input.missing,
2042
2099
  });
2043
2100
  const command = buildProviderCommand(lane.provider, {
2044
2101
  hqRoot,
@@ -2081,6 +2138,8 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2081
2138
  id: profile.id,
2082
2139
  hash: profile.contentHash,
2083
2140
  ignoredFields: ignoredProfileFields,
2141
+ policyDelivery: policyDelivery ?? "prompt",
2142
+ ...(workerPolicyDir !== undefined ? { policyDir: workerPolicyDir } : {}),
2084
2143
  ...(profileRuntime?.settingsPath !== undefined
2085
2144
  ? { settingsPath: profileRuntime.settingsPath }
2086
2145
  : {}),
@@ -2102,11 +2161,12 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2102
2161
  if (profile !== undefined) {
2103
2162
  next.worker_profile_hash = profile.contentHash;
2104
2163
  next.worker_profile_ignored = [...ignoredProfileFields];
2164
+ next.worker_policy_delivery = policyDelivery;
2105
2165
  if (model !== undefined) {
2106
2166
  next.model = model;
2107
2167
  next.model_source = pinnedModel !== undefined
2108
2168
  ? "cli"
2109
- : profileDefaultsApply
2169
+ : profileModel !== undefined
2110
2170
  ? "profile"
2111
2171
  : accountCfg?.model !== undefined
2112
2172
  ? "account"
@@ -2124,6 +2184,9 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2124
2184
  else
2125
2185
  delete next.effort;
2126
2186
  }
2187
+ else {
2188
+ next.worker_policy_delivery = undefined;
2189
+ }
2127
2190
  if (fastMode)
2128
2191
  next.fast_mode = true;
2129
2192
  else
@@ -421,6 +421,11 @@ export function parseLaneRecord(raw, filePath) {
421
421
  if (!isLaneProvider(raw.provider)) {
422
422
  throw new LaneSchemaError(`Unknown provider ${JSON.stringify(raw.provider)}`, { path: filePath, value: raw.provider });
423
423
  }
424
+ if (raw.worker_policy_delivery !== undefined &&
425
+ raw.worker_policy_delivery !== "hook" &&
426
+ raw.worker_policy_delivery !== "prompt") {
427
+ 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 });
428
+ }
424
429
  parseLaneStories(raw, filePath);
425
430
  parseOptionalCompanyField(raw, filePath);
426
431
  parseOptionalMeshCompanyField(raw, filePath);
@@ -296,6 +296,8 @@ export interface LaneRecord {
296
296
  worker_profile_hash?: string;
297
297
  /** Profile fields intentionally unsupported by the selected provider. */
298
298
  worker_profile_ignored?: string[];
299
+ /** How policy files were delivered on the most recent profile spawn. */
300
+ worker_policy_delivery?: "hook" | "prompt";
299
301
  provider: LaneProvider;
300
302
  account?: string;
301
303
  /** Eligible accounts for a queue entry created without an explicit account. */
@@ -400,9 +402,9 @@ export interface LaneRecord {
400
402
  /** Cache counters from the last exited run's result. Nothing else from the payload. */
401
403
  last_run_usage?: LaneRunUsage;
402
404
  /**
403
- * Spec §4: one repair turn when the final output fails parseLaneEnvelope.
404
- * Written by the watcher before the repair spawn, updated on the repair
405
- * run's exit. Absent when every envelope this lane wrote parsed first time.
405
+ * Spec §4: one repair turn per expected envelope slot when the final output
406
+ * is invalid or missing. Written before repair spawn and updated on repair
407
+ * exit. Absent when each slot's first envelope parsed successfully.
406
408
  */
407
409
  envelope_repair?: LaneEnvelopeRepair;
408
410
  /** Last successful lane_progress mesh delivery, preserved across watcher restarts. */
@@ -214,12 +214,12 @@ export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spaw
214
214
  /** Field ownership at the durable-record mutation boundary (spec §10.1). */
215
215
  export const LANE_FIELD_WRITERS = {
216
216
  schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
217
- project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator"],
217
+ project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
218
218
  depth: ["command"], senior: ["command"], senior_history: ["command"],
219
219
  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"],
220
220
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
221
221
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
222
- priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"], worker_profile: ["creator"], worker_profile_hash: ["creator", "spawn"], worker_profile_ignored: ["creator", "spawn"],
222
+ 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_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
223
223
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
224
224
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
225
225
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
@@ -174,8 +174,10 @@ export interface LatestEnvelopeFile {
174
174
  path: string;
175
175
  text: string;
176
176
  envelope: LaneEnvelope | null;
177
- /** Parser message when `envelope` is null. */
177
+ /** Parser message, or the missing-slot reason, when `envelope` is null. */
178
178
  error: string | null;
179
+ /** True when the manifest's expected envelope slot was never created. */
180
+ missing?: boolean;
179
181
  }
180
182
  export declare function readLatestEnvelope(hqRoot: string, laneId: string, minName?: string): LaneEnvelope | null;
181
183
  /** Valid-only view over `readLatestEnvelopeFile`; an unparseable file reads as absent. */
@@ -187,6 +189,8 @@ export declare function readLatestEnvelopeRecord(hqRoot: string, laneId: string,
187
189
  * would otherwise be marked done again from the previous one.
188
190
  */
189
191
  export declare function readLatestEnvelopeFile(hqRoot: string, laneId: string, minName?: string): LatestEnvelopeFile | null;
192
+ /** A descriptor for this run's manifest slot when no envelope file was written. */
193
+ export declare function missingExpectedEnvelopeFile(hqRoot: string, laneId: string, envelopePath: string | undefined): LatestEnvelopeFile | null;
190
194
  export interface WatcherLoopDeps {
191
195
  sleep?: (ms: number) => Promise<void>;
192
196
  now?: () => number;
@@ -230,13 +234,15 @@ export type EnvelopeRepairDecision = {
230
234
  errors: string[];
231
235
  };
232
236
  /**
233
- * Spec §4 repair decision for an exited worker whose envelope file does not
234
- * parse. First failure on a resumable provider with a recorded session id:
235
- * record `envelope_repair` on the lane and spawn the repair turn. A provider
236
- * with no session resume gets the same single attempt as a fresh turn. Anything
237
- * else (already attempted, no session id on a resumable provider, spawn
238
- * failure): give up with every error collected, and the caller classifies
239
- * the lane `failed` with reason `envelope_invalid`.
237
+ * Spec §4 repair decision for an exited worker whose expected envelope is
238
+ * invalid or missing. First failure on a resumable provider with a recorded
239
+ * session id: record `envelope_repair` on the lane and spawn the repair turn.
240
+ * A provider with no session resume gets the same single attempt as a fresh
241
+ * turn for invalid files. Anything else (already attempted, no session id on a
242
+ * resumable provider, non-running lane, spawn failure): give up with every
243
+ * error collected. The caller preserves the normal interrupted classification
244
+ * for a missing file and uses `envelope_invalid` only for an existing invalid
245
+ * file.
240
246
  */
241
247
  export declare function decideEnvelopeRepair(hqRoot: string, laneId: string, latest: LatestEnvelopeFile, nowMs: number, spawnRepair: NonNullable<WatcherLoopDeps["spawnRepair"]>, logDecision: (record: Record<string, unknown>) => void): EnvelopeRepairDecision;
242
248
  /** Exported so tests can prove the watcher reads the live record, not spawn env. */