@indigoai-us/hq-cli 5.281.0 → 5.283.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/assets/scaffold/.claude/hooks/validate-policy-frontmatter.sh +485 -0
  3. package/assets/scaffold/core/scripts/eval-trigger.sh +193 -0
  4. package/assets/scaffold/core/scripts/policy-age-report.sh +156 -0
  5. package/assets/scaffold/core/scripts/policy-retire.sh +199 -0
  6. package/assets/scaffold/core/scripts/read-policy-frontmatter.sh +32 -0
  7. package/dist/command-catalog.generated.d.ts +67 -0
  8. package/dist/command-catalog.generated.js +83 -0
  9. package/dist/commands/core-knowledge-prefs.d.ts +10 -0
  10. package/dist/commands/core-knowledge-prefs.js +125 -0
  11. package/dist/commands/core-native-policy-flag.d.ts +9 -0
  12. package/dist/commands/core-native-policy-flag.js +12 -0
  13. package/dist/commands/core-ontology-native.d.ts +0 -1
  14. package/dist/commands/core-ontology-native.js +2 -30
  15. package/dist/commands/core-ontology.d.ts +4 -0
  16. package/dist/commands/core-ontology.js +8 -3
  17. package/dist/commands/core-policy-frontmatter.d.ts +8 -0
  18. package/dist/commands/core-policy-frontmatter.js +52 -0
  19. package/dist/commands/core-policy.d.ts +15 -0
  20. package/dist/commands/core-policy.js +982 -0
  21. package/dist/commands/core.js +14 -1
  22. package/dist/commands/lanes-message.js +1 -0
  23. package/dist/commands/lanes-move.js +2 -0
  24. package/dist/commands/lanes-resume.js +1 -0
  25. package/dist/commands/lanes-shared.d.ts +10 -1
  26. package/dist/commands/lanes-shared.js +2 -1
  27. package/dist/commands/lanes.d.ts +8 -0
  28. package/dist/commands/lanes.js +324 -93
  29. package/dist/fast-lanes-list-route.d.ts +8 -0
  30. package/dist/fast-lanes-list-route.js +30 -0
  31. package/dist/fast-lanes-list.d.ts +3 -0
  32. package/dist/fast-lanes-list.js +252 -0
  33. package/dist/fast-scaffold.d.ts +5 -0
  34. package/dist/fast-scaffold.js +10 -0
  35. package/dist/index.js +10 -1
  36. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +24 -1
  37. package/dist/lib/checkpoint-ontology/merge-copies.js +70 -1
  38. package/dist/lib/lanes/flag-keys.d.ts +3 -0
  39. package/dist/lib/lanes/flag-keys.js +3 -0
  40. package/dist/lib/lanes/index.d.ts +1 -1
  41. package/dist/lib/lanes/index.js +1 -1
  42. package/dist/lib/lanes/keep-alive.js +1 -0
  43. package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
  44. package/dist/lib/lanes/outpost-dispatch.js +4 -2
  45. package/dist/lib/lanes/outpost-hire-cache.d.ts +3 -0
  46. package/dist/lib/lanes/outpost-hire-cache.js +11 -3
  47. package/dist/lib/lanes/outpost-snapshot-cache.d.ts +26 -0
  48. package/dist/lib/lanes/outpost-snapshot-cache.js +215 -0
  49. package/dist/lib/lanes/reconcile.d.ts +7 -1
  50. package/dist/lib/lanes/reconcile.js +88 -16
  51. package/dist/lib/lanes/spawn.js +5 -0
  52. package/dist/lib/lanes/store.d.ts +4 -0
  53. package/dist/lib/lanes/store.js +66 -0
  54. package/dist/lib/lanes/throttle.d.ts +29 -2
  55. package/dist/lib/lanes/throttle.js +145 -5
  56. package/dist/lib/lanes/types.d.ts +8 -1
  57. package/dist/lib/lanes/types.js +2 -1
  58. package/dist/lib/lanes/watcher.js +1 -0
  59. package/dist/lib/policy-validator.d.ts +2 -0
  60. package/dist/lib/policy-validator.js +136 -0
  61. package/dist/native-fast-route.d.ts +2 -0
  62. package/dist/native-fast-route.js +6 -2
  63. package/dist/utils/contribution-table.d.ts +1 -1
  64. package/package.json +1 -1
@@ -12,7 +12,8 @@
12
12
  import * as fs from "node:fs";
13
13
  import * as os from "node:os";
14
14
  import type { ThrottleConfig } from "./types.js";
15
- export declare const CPU_SAMPLE_MS = 200;
15
+ export declare const CPU_SAMPLE_MS = 5000;
16
+ export declare const CPU_CUMULATIVE_MAX_AGE_MS: number;
16
17
  export declare const STARVATION_MAX_DEFERRALS = 10;
17
18
  export declare const STARVATION_MAX_MS: number;
18
19
  export declare const PROC_MEMINFO = "/proc/meminfo";
@@ -39,12 +40,34 @@ export interface ThrottleDeps {
39
40
  renameSync: typeof fs.renameSync;
40
41
  unlinkSync: typeof fs.unlinkSync;
41
42
  }
43
+ export type CpuBusySource = "interval" | "window" | "override";
44
+ export interface CpuCumulativeSnapshot {
45
+ idle: number;
46
+ total: number;
47
+ sampled_at: string;
48
+ }
49
+ export interface CpuBusyObservation {
50
+ busy: number | null;
51
+ source: CpuBusySource | undefined;
52
+ snapshot: CpuCumulativeSnapshot | undefined;
53
+ }
42
54
  export declare function defaultThrottleDeps(env?: NodeJS.ProcessEnv): ThrottleDeps;
43
55
  export declare function cpuTimesSnapshot(cpus: os.CpuInfo[]): CpuTimes;
44
56
  export declare function busyFractionFromDeltas(before: CpuTimes, after: CpuTimes): number | null;
45
57
  export declare function sampleCpuBusyFraction(deps?: ThrottleDeps, sampleMs?: number): Promise<number | null>;
46
58
  export declare function isCpuCheckEnabled(env?: NodeJS.ProcessEnv): boolean;
47
59
  export declare function resolveCpuBusyFraction(deps?: ThrottleDeps): Promise<number | null>;
60
+ /**
61
+ * Measure CPU busy against the previous reconcile snapshot when it is fresh.
62
+ * A short stable window is used only to establish a baseline or recover from
63
+ * stale/backwards counters (for example, after a reboot).
64
+ */
65
+ export declare function sampleCpuBusyObservation(deps: ThrottleDeps, previous: CpuCumulativeSnapshot | undefined, options?: {
66
+ measureBusy?: boolean;
67
+ }): Promise<CpuBusyObservation>;
68
+ export declare function recordCpuBusyObservation(state: ThrottleState, observation: CpuBusyObservation, options?: {
69
+ advanceSnapshot?: boolean;
70
+ }): ThrottleState;
48
71
  /** Parse MemAvailable kB from /proc/meminfo. */
49
72
  export declare function parseMemAvailableKb(contents: string): number | null;
50
73
  /**
@@ -56,6 +79,10 @@ export interface ThrottleState {
56
79
  consecutiveDeferrals: number;
57
80
  lastAdmissionAt: string | null;
58
81
  cpuPaused: boolean;
82
+ cpu_snapshot?: CpuCumulativeSnapshot;
83
+ last_busy?: number | null;
84
+ busy_source?: CpuBusySource;
85
+ sampled_at?: string;
59
86
  }
60
87
  export type ThrottleAdmitReason = "ok" | "starvation_bound" | "unreadable" | "cpu_check_disabled";
61
88
  export type ThrottleDeferReason = "cpu" | "memory";
@@ -82,7 +109,7 @@ export declare function applyThrottleDecision(state: ThrottleState, decision: Th
82
109
  export declare function throttleStatePath(hqRoot: string): string;
83
110
  export declare function readThrottleState(filePath: string, deps?: ThrottleDeps): ThrottleState | "unreadable" | "missing";
84
111
  export declare function writeThrottleState(filePath: string, state: ThrottleState, deps?: ThrottleDeps): void;
85
- export declare function decideAdmission(hqRoot: string, throttle?: ThrottleConfig, deps?: ThrottleDeps): Promise<{
112
+ export declare function decideAdmission(hqRoot: string, throttle?: ThrottleConfig, deps?: ThrottleDeps, cpuBusyFraction?: number | null, cpuObservation?: CpuBusyObservation): Promise<{
86
113
  decision: ThrottleDecision;
87
114
  state: ThrottleState;
88
115
  }>;
@@ -13,7 +13,10 @@ import * as fs from "node:fs";
13
13
  import * as os from "node:os";
14
14
  import * as path from "node:path";
15
15
  import { DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_MIN_AVAILABLE_MEM_MB, } from "./config.js";
16
- export const CPU_SAMPLE_MS = 200;
16
+ // Reconcile normally measures the interval between cumulative CPU snapshots.
17
+ // A five-second window establishes a baseline or recovers from stale counters.
18
+ export const CPU_SAMPLE_MS = 5_000;
19
+ export const CPU_CUMULATIVE_MAX_AGE_MS = 5 * 60 * 1000;
17
20
  export const STARVATION_MAX_DEFERRALS = 10;
18
21
  export const STARVATION_MAX_MS = 30 * 60 * 1000;
19
22
  export const PROC_MEMINFO = "/proc/meminfo";
@@ -83,6 +86,93 @@ export async function resolveCpuBusyFraction(deps = defaultThrottleDeps()) {
83
86
  }
84
87
  return sampleCpuBusyFraction(deps);
85
88
  }
89
+ function snapshotFromDeps(deps) {
90
+ return {
91
+ ...cpuTimesSnapshot(deps.cpus()),
92
+ sampled_at: new Date(deps.now()).toISOString(),
93
+ };
94
+ }
95
+ /**
96
+ * Measure CPU busy against the previous reconcile snapshot when it is fresh.
97
+ * A short stable window is used only to establish a baseline or recover from
98
+ * stale/backwards counters (for example, after a reboot).
99
+ */
100
+ export async function sampleCpuBusyObservation(deps, previous, options = {}) {
101
+ let start;
102
+ try {
103
+ start = snapshotFromDeps(deps);
104
+ }
105
+ catch (err) {
106
+ process.stderr.write(`lanes throttle: CPU counters failed: ${err instanceof Error ? err.message : String(err)}\n`);
107
+ return { busy: null, source: undefined, snapshot: previous };
108
+ }
109
+ if (options.measureBusy === false || !isCpuCheckEnabled(deps.env)) {
110
+ return { busy: null, source: undefined, snapshot: start };
111
+ }
112
+ const rawOverride = deps.env[LANES_CPU_BUSY_OVERRIDE];
113
+ if (rawOverride !== undefined && rawOverride !== "") {
114
+ const override = Number(rawOverride);
115
+ if (Number.isFinite(override)) {
116
+ return {
117
+ busy: Math.max(0, Math.min(1, override)),
118
+ source: "override",
119
+ snapshot: start,
120
+ };
121
+ }
122
+ process.stderr.write(`lanes throttle: ${LANES_CPU_BUSY_OVERRIDE} is not a finite number (${JSON.stringify(rawOverride)}); sampling instead\n`);
123
+ }
124
+ const nowMs = Date.parse(start.sampled_at);
125
+ const previousAtMs = previous ? Date.parse(previous.sampled_at) : Number.NaN;
126
+ if (previous !== undefined &&
127
+ Number.isFinite(previousAtMs) &&
128
+ nowMs >= previousAtMs &&
129
+ nowMs - previousAtMs <= CPU_CUMULATIVE_MAX_AGE_MS &&
130
+ start.idle >= previous.idle &&
131
+ start.total >= previous.total) {
132
+ return {
133
+ busy: busyFractionFromDeltas(previous, start),
134
+ source: "interval",
135
+ snapshot: start,
136
+ };
137
+ }
138
+ try {
139
+ await deps.sleep(CPU_SAMPLE_MS);
140
+ const end = snapshotFromDeps(deps);
141
+ return {
142
+ busy: busyFractionFromDeltas(start, end),
143
+ source: "window",
144
+ snapshot: end,
145
+ };
146
+ }
147
+ catch (err) {
148
+ process.stderr.write(`lanes throttle: CPU window sample failed: ${err instanceof Error ? err.message : String(err)}\n`);
149
+ return { busy: null, source: "window", snapshot: start };
150
+ }
151
+ }
152
+ export function recordCpuBusyObservation(state, observation, options = {}) {
153
+ const observationAt = observation.snapshot === undefined
154
+ ? Number.NaN
155
+ : Date.parse(observation.snapshot.sampled_at);
156
+ const currentSnapshotAt = state.cpu_snapshot === undefined
157
+ ? Number.NaN
158
+ : Date.parse(state.cpu_snapshot.sampled_at);
159
+ const snapshotIsCurrent = options.advanceSnapshot !== false && observation.snapshot !== undefined && (!Number.isFinite(currentSnapshotAt) ||
160
+ observationAt > currentSnapshotAt ||
161
+ (observationAt === currentSnapshotAt && observation.snapshot.total >= (state.cpu_snapshot?.total ?? 0)));
162
+ const currentMetricAt = state.sampled_at === undefined ? Number.NaN : Date.parse(state.sampled_at);
163
+ const metricIsCurrent = observation.source !== undefined && Number.isFinite(observationAt) && (!Number.isFinite(currentMetricAt) || observationAt >= currentMetricAt);
164
+ return {
165
+ ...state,
166
+ ...(snapshotIsCurrent ? { cpu_snapshot: observation.snapshot } : {}),
167
+ ...(metricIsCurrent
168
+ ? {
169
+ last_busy: observation.busy,
170
+ busy_source: observation.source,
171
+ sampled_at: observation.snapshot.sampled_at,
172
+ }
173
+ : {}),
174
+ };
175
+ }
86
176
  /** Parse MemAvailable kB from /proc/meminfo. */
87
177
  export function parseMemAvailableKb(contents) {
88
178
  for (const line of contents.split("\n")) {
@@ -184,12 +274,14 @@ export function evaluateThrottle(input) {
184
274
  export function applyThrottleDecision(state, decision, nowMs) {
185
275
  if (decision.admit) {
186
276
  return {
277
+ ...state,
187
278
  consecutiveDeferrals: 0,
188
279
  lastAdmissionAt: new Date(nowMs).toISOString(),
189
280
  cpuPaused: false,
190
281
  };
191
282
  }
192
283
  return {
284
+ ...state,
193
285
  consecutiveDeferrals: state.consecutiveDeferrals + 1,
194
286
  lastAdmissionAt: state.lastAdmissionAt,
195
287
  cpuPaused: decision.reason === "cpu" ? true : state.cpuPaused,
@@ -231,10 +323,47 @@ export function readThrottleState(filePath, deps = defaultThrottleDeps()) {
231
323
  process.stderr.write(`lanes throttle: state ${filePath} has invalid cpuPaused\n`);
232
324
  return "unreadable";
233
325
  }
326
+ const optionalTimestamp = (value) => typeof value === "string" && value.trim() !== "" && Number.isFinite(Date.parse(value));
327
+ let cpuSnapshot;
328
+ if (obj.cpu_snapshot !== undefined) {
329
+ const rawSnapshot = obj.cpu_snapshot;
330
+ if (rawSnapshot === null || typeof rawSnapshot !== "object" || Array.isArray(rawSnapshot)) {
331
+ process.stderr.write(`lanes throttle: state ${filePath} has invalid cpu_snapshot\n`);
332
+ return "unreadable";
333
+ }
334
+ const snapshot = rawSnapshot;
335
+ if (typeof snapshot.idle !== "number" || !Number.isFinite(snapshot.idle) || snapshot.idle < 0 ||
336
+ typeof snapshot.total !== "number" || !Number.isFinite(snapshot.total) || snapshot.total < 0 ||
337
+ !optionalTimestamp(snapshot.sampled_at)) {
338
+ process.stderr.write(`lanes throttle: state ${filePath} has invalid cpu_snapshot fields\n`);
339
+ return "unreadable";
340
+ }
341
+ cpuSnapshot = {
342
+ idle: snapshot.idle,
343
+ total: snapshot.total,
344
+ sampled_at: snapshot.sampled_at,
345
+ };
346
+ }
347
+ if (obj.last_busy !== undefined && obj.last_busy !== null && (typeof obj.last_busy !== "number" || !Number.isFinite(obj.last_busy) || obj.last_busy < 0 || obj.last_busy > 1)) {
348
+ process.stderr.write(`lanes throttle: state ${filePath} has invalid last_busy\n`);
349
+ return "unreadable";
350
+ }
351
+ if (obj.busy_source !== undefined && !["interval", "window", "override"].includes(String(obj.busy_source))) {
352
+ process.stderr.write(`lanes throttle: state ${filePath} has invalid busy_source\n`);
353
+ return "unreadable";
354
+ }
355
+ if (obj.sampled_at !== undefined && !optionalTimestamp(obj.sampled_at)) {
356
+ process.stderr.write(`lanes throttle: state ${filePath} has invalid sampled_at\n`);
357
+ return "unreadable";
358
+ }
234
359
  return {
235
360
  consecutiveDeferrals: obj.consecutiveDeferrals,
236
361
  lastAdmissionAt: obj.lastAdmissionAt,
237
362
  cpuPaused: obj.cpuPaused,
363
+ ...(cpuSnapshot !== undefined ? { cpu_snapshot: cpuSnapshot } : {}),
364
+ ...(obj.last_busy !== undefined ? { last_busy: obj.last_busy } : {}),
365
+ ...(obj.busy_source !== undefined ? { busy_source: obj.busy_source } : {}),
366
+ ...(obj.sampled_at !== undefined ? { sampled_at: obj.sampled_at } : {}),
238
367
  };
239
368
  }
240
369
  catch (err) {
@@ -269,7 +398,7 @@ export async function decideAdmission(hqRoot, throttle = {
269
398
  cpu_busy_pause: DEFAULT_CPU_BUSY_PAUSE,
270
399
  cpu_busy_resume: DEFAULT_CPU_BUSY_RESUME,
271
400
  min_available_mem_mb: DEFAULT_MIN_AVAILABLE_MEM_MB,
272
- }, deps = defaultThrottleDeps()) {
401
+ }, deps = defaultThrottleDeps(), cpuBusyFraction, cpuObservation) {
273
402
  const filePath = throttleStatePath(hqRoot);
274
403
  const loaded = readThrottleState(filePath, deps);
275
404
  if (loaded === "unreadable") {
@@ -279,18 +408,29 @@ export async function decideAdmission(hqRoot, throttle = {
279
408
  };
280
409
  }
281
410
  const state = loaded === "missing" ? emptyThrottleState() : loaded;
282
- const busy = await resolveCpuBusyFraction(deps);
411
+ const cpuCheckEnabled = isCpuCheckEnabled(deps.env);
412
+ const observation = cpuObservation ?? (cpuBusyFraction === undefined && cpuCheckEnabled
413
+ ? await sampleCpuBusyObservation(deps, state.cpu_snapshot)
414
+ : undefined);
415
+ const busy = cpuBusyFraction === undefined
416
+ ? observation?.busy ?? null
417
+ : cpuBusyFraction;
283
418
  const availableMemMb = readAvailableMemMb(deps);
284
419
  const nowMs = deps.now();
285
420
  const decision = evaluateThrottle({
286
421
  busy,
287
422
  availableMemMb,
288
- cpuCheckEnabled: isCpuCheckEnabled(deps.env),
423
+ cpuCheckEnabled,
289
424
  state,
290
425
  throttle,
291
426
  nowMs,
292
427
  });
293
- const next = applyThrottleDecision(state, decision, nowMs);
428
+ const observedState = observation
429
+ ? recordCpuBusyObservation(state, observation, {
430
+ advanceSnapshot: cpuObservation !== undefined,
431
+ })
432
+ : state;
433
+ const next = applyThrottleDecision(observedState, decision, nowMs);
294
434
  writeThrottleState(filePath, next, deps);
295
435
  return { decision, state: next };
296
436
  }
@@ -173,8 +173,14 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
173
173
  export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
174
174
  export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
175
175
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
176
- export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry"];
176
+ export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed"];
177
177
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
178
+ export interface QueuedDetails {
179
+ reason: "worker_profile_changed";
180
+ expected_hash: string | null;
181
+ actual_hash: string;
182
+ recovery_command: string;
183
+ }
178
184
  export declare function isQueuedReason(value: unknown): value is QueuedReason;
179
185
  export declare const HOST_KINDS: readonly ["local", "outpost"];
180
186
  export type HostKind = (typeof HOST_KINDS)[number];
@@ -442,6 +448,7 @@ export interface LaneRecord {
442
448
  senior_history: SeniorHistoryEntry[];
443
449
  state: LaneState;
444
450
  queued_reason?: QueuedReason;
451
+ queued_details?: QueuedDetails;
445
452
  /** Time the current queue turn began; old records fall back to created_at. */
446
453
  queued_at?: string;
447
454
  /** Short-lived slot reservation between create persistence and worker spawn. */
@@ -107,6 +107,7 @@ export const QUEUED_REASONS = [
107
107
  "throttle",
108
108
  "provider_binary_missing",
109
109
  "account_retry",
110
+ "worker_profile_changed",
110
111
  ];
111
112
  export function isQueuedReason(value) {
112
113
  return typeof value === "string" && QUEUED_REASONS.includes(value);
@@ -220,7 +221,7 @@ export const LANE_FIELD_WRITERS = {
220
221
  project_id: ["command"], story_id: ["command"], no_story_reason: ["creator", "command"], ledger_sync_error: ["story-ledger"], pending_question_ids: ["story-ledger"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
221
222
  depth: ["command"], senior: ["command"], senior_history: ["command"],
222
223
  hire_grant: ["creator", "command"],
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
+ state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_details: ["command", "reconcile", "spawn", "watcher"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
224
225
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
225
226
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
226
227
  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"],
@@ -380,6 +380,7 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
380
380
  ...runResultFields(hqRoot, current, at),
381
381
  state: effectiveClassification.state,
382
382
  queued_reason: undefined,
383
+ queued_details: undefined,
383
384
  stalled: undefined,
384
385
  stalled_at: undefined,
385
386
  stalled_reason: undefined,
@@ -0,0 +1,2 @@
1
+ export declare function policyValidatorDiagnostic(content: string, filePath: string, env: NodeJS.ProcessEnv): string | undefined;
2
+ //# sourceMappingURL=policy-validator.d.ts.map
@@ -0,0 +1,136 @@
1
+ const VALIDATION_NOTE = '\n(The HQ_ALLOW_POLICY_NO_TRIGGER override requires explicit human permission.\nAn agent must never set, export, or write it on its own initiative.)\n';
2
+ function isPolicyPath(file) {
3
+ const low = file.toLowerCase().replaceAll('\\', '/');
4
+ const basename = low.slice(low.lastIndexOf('/') + 1);
5
+ return low.endsWith('.md')
6
+ && (low.includes('/policies/') || low.startsWith('policies/'))
7
+ && basename !== 'readme.md'
8
+ && !low.includes('/audit/');
9
+ }
10
+ function normalizeValidatorText(content) {
11
+ return content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
12
+ }
13
+ function hasValidTriggerGrammar(expression) {
14
+ let input = expression.replace(/[A-Za-z0-9_./][A-Za-z0-9_./-]*/g, '0');
15
+ if (/[^01&|!() \t]/.test(input))
16
+ return false;
17
+ let position = 0;
18
+ let valid = true;
19
+ const skip = () => { while (input[position] === ' ' || input[position] === '\t')
20
+ position += 1; };
21
+ let parseOr;
22
+ let parseAnd;
23
+ let parseNot;
24
+ let parseAtom;
25
+ parseAtom = () => {
26
+ skip();
27
+ const current = input[position];
28
+ if (current === '(') {
29
+ position += 1;
30
+ const result = parseOr();
31
+ skip();
32
+ if (input[position] !== ')') {
33
+ valid = false;
34
+ return false;
35
+ }
36
+ position += 1;
37
+ return result;
38
+ }
39
+ if (current === '0' || current === '1') {
40
+ position += 1;
41
+ return current === '1';
42
+ }
43
+ valid = false;
44
+ return false;
45
+ };
46
+ parseNot = () => {
47
+ skip();
48
+ if (input[position] === '!') {
49
+ position += 1;
50
+ return !parseNot();
51
+ }
52
+ return parseAtom();
53
+ };
54
+ parseAnd = () => {
55
+ let result = parseNot();
56
+ skip();
57
+ while (valid && input.slice(position, position + 2) === '&&') {
58
+ position += 2;
59
+ const rhs = parseNot();
60
+ result = result && rhs;
61
+ skip();
62
+ }
63
+ return result;
64
+ };
65
+ parseOr = () => {
66
+ let result = parseAnd();
67
+ skip();
68
+ while (valid && input.slice(position, position + 2) === '||') {
69
+ position += 2;
70
+ const rhs = parseAnd();
71
+ result = result || rhs;
72
+ skip();
73
+ }
74
+ return result;
75
+ };
76
+ input = input.replace(/\r/g, '');
77
+ if (input.replace(/[ \t]/g, '') === '')
78
+ return false;
79
+ parseOr();
80
+ skip();
81
+ return valid && position === input.length;
82
+ }
83
+ export function policyValidatorDiagnostic(content, filePath, env) {
84
+ if (!isPolicyPath(filePath))
85
+ return undefined;
86
+ const normalized = normalizeValidatorText(content);
87
+ const match = /^\s*---[ \t]*\n([\s\S]*?)\n---[ \t]*(\n|$)/.exec(normalized);
88
+ if (!match)
89
+ return `BLOCKED: policy file is missing required trigger frontmatter (missing: no-frontmatter).\n\nEvery policy under */policies/ must declare BOTH:\n when: <expression> # e.g. always | git && push | deploy || share\n on: [<events>] # any of PreToolUse, PostToolUse, UserPromptSubmit, AssistantIntent, SessionStart\nThese drive just-in-time policy injection. See\ncore/knowledge/public/hq-core/policies-spec.md ("Trigger Expressions").\nAdd both fields to the frontmatter, then retry.\n${VALIDATION_NOTE}`;
90
+ const frontmatter = match[1];
91
+ const whenLines = [...frontmatter.matchAll(/^[ \t]*when:[ \t]*(.*)$/gm)].map((entry) => entry[1] ?? '');
92
+ const hasWhen = whenLines.some((line) => /\S/.test(line));
93
+ const onMatch = /^[ \t]*on:[ \t]*\S/m.test(frontmatter);
94
+ const missing = [];
95
+ if (!hasWhen)
96
+ missing.push('when');
97
+ if (!onMatch)
98
+ missing.push('on');
99
+ if (missing.length > 0)
100
+ return `BLOCKED: policy file is missing required trigger frontmatter (missing: ${missing.join(',')}).\n\nEvery policy under */policies/ must declare BOTH:\n when: <expression> # e.g. always | git && push | deploy || share\n on: [<events>] # any of PreToolUse, PostToolUse, UserPromptSubmit, AssistantIntent, SessionStart\nThese drive just-in-time policy injection. See\ncore/knowledge/public/hq-core/policies-spec.md ("Trigger Expressions").\nAdd both fields to the frontmatter, then retry.\n${VALIDATION_NOTE}`;
101
+ const enforcement = /^[ \t]*enforcement:[ \t]*["']?([A-Za-z]+)["']?/m.exec(frontmatter)?.[1]?.toLowerCase();
102
+ if (enforcement === 'hard') {
103
+ const tautological = whenLines.some((line) => !/[&!]/.test(line.toLowerCase()) && /(^|[^A-Za-z0-9_./-])always([^A-Za-z0-9_./-]|$)/i.test(line));
104
+ const onLine = /^[ \t]*on:[ \t]*(.*)$/m.exec(frontmatter)?.[1] ?? '';
105
+ const reactive = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'AssistantIntent'].filter((event) => onLine.includes(event));
106
+ if (tautological && reactive.length > 0) {
107
+ const events = reactive.join(', ');
108
+ return `BLOCKED: an enforcement: hard policy pairs an unconditional \`when:\` with a\nreactive event (${events}).\n\n\`when: always\` is TRUE for every command, prompt, and message. On a reactive\nevent the injector ranks that as an event-specific match, so it takes the\nsession cap slot of a policy that genuinely matched the thing you are doing —\nthe loose trigger crowds out the precise one.\n\nPick one:\n when: always + on: [SessionStart] # ambient governance rule\n when: <real signal> + on: [PreToolUse, ...] # e.g. deploy || publish\n\nName the word(s) that actually appear when the rule is relevant. See\ncore/knowledge/public/hq-core/policies-spec.md ("Trigger Expressions").\n${VALIDATION_NOTE}`;
109
+ }
110
+ const after = normalized.slice(match.index + match[0].length);
111
+ const binding = [];
112
+ const stop = /^#+[ \t]*(rationale|rationale and context|background|change history|changelog|history|examples?|references?|related|see also|sources?|provenance|evidence)[ \t]*$/i;
113
+ for (const line of after.split('\n')) {
114
+ if (stop.test(line.trim().toLowerCase()))
115
+ break;
116
+ binding.push(line);
117
+ }
118
+ const byteCount = Buffer.byteLength(binding.join('\n').trim(), 'utf8');
119
+ const rawMax = env.HQ_POLICY_HARD_RULE_MAX_BYTES || '6144';
120
+ const max = Number.parseInt(rawMax, 10);
121
+ if (Number.isFinite(max) && max > 0 && byteCount > max) {
122
+ return `BLOCKED: enforcement: hard policy body is ${byteCount} bytes; the limit is ${rawMax}.\n\nA hard policy is injected VERBATIM into the session, so its length is a\nrecurring context cost for every session it fires in. Keep the binding rule\ntight and move the reasoning out of the injected span: everything from the\nfirst \`## Rationale\` / \`## Background\` / \`## Change history\` /\n\`## Examples\` / \`## References\` heading onward is NOT counted and NOT\ninjected, so long-form context belongs there.\n\nIf the rule genuinely needs more than that, either split it into separate\npolicies with distinct triggers, or raise HQ_POLICY_HARD_RULE_MAX_BYTES in\n.claude/settings.local.json "env".\n${VALIDATION_NOTE}`;
123
+ }
124
+ }
125
+ let malformedWhen;
126
+ for (const when of whenLines) {
127
+ if (!hasValidTriggerGrammar(when))
128
+ malformedWhen = when;
129
+ }
130
+ if (malformedWhen !== undefined) {
131
+ const when = malformedWhen;
132
+ return `BLOCKED: policy file has a malformed when: trigger expression:\n\n when: ${when}\n\nThe canonical trigger evaluator rejected it. A space is NOT an AND, and quoted\nphrases are NOT tokens. Use explicit boolean operators instead:\n "pull request" -> (pull && request)\n "force-push" -> force-push\n\nUse only identifiers joined by the documented boolean grammar:\n when: <identifier> # e.g. always | git | /deep-plan\n when: <expr> && <expr> # AND\n when: <expr> || <expr> # OR\n when: ! <expr> # NOT\n when: ( <expr> ) # grouping\nIdentifiers may contain letters, digits, _, ., /, and internal -. Quotes,\nYAML block scalars, adjacent identifiers without an operator, and other\npunctuation are not valid. See core/knowledge/public/hq-core/policies-spec.md\n("Trigger Expressions"). Fix the expression, then retry.\n${VALIDATION_NOTE}`;
133
+ }
134
+ return undefined;
135
+ }
136
+ //# sourceMappingURL=policy-validator.js.map
@@ -1,3 +1,5 @@
1
1
  /** Default-off flag check for native entrypoint routes. */
2
2
  export declare function nativeFastRouteEnabled(argv: readonly string[], env?: NodeJS.ProcessEnv): Promise<boolean>;
3
+ /** Reads a default-off fast-route gate from its local override or fresh snapshot. */
4
+ export declare function fastFlagEnabled(flagKey: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
3
5
  //# sourceMappingURL=native-fast-route.d.ts.map
@@ -5,7 +5,11 @@ export async function nativeFastRouteEnabled(argv, env = process.env) {
5
5
  const parsed = parseFastCore(argv);
6
6
  if (!parsed || parsed.entry.kind !== "native")
7
7
  return false;
8
- const override = nativeRouteOverride(parsed.entry.flagKey, env);
8
+ return fastFlagEnabled(parsed.entry.flagKey, env);
9
+ }
10
+ /** Reads a default-off fast-route gate from its local override or fresh snapshot. */
11
+ export async function fastFlagEnabled(flagKey, env = process.env) {
12
+ const override = nativeRouteOverride(flagKey, env);
9
13
  if (override !== null)
10
14
  return override;
11
15
  try {
@@ -16,7 +20,7 @@ export async function nativeFastRouteEnabled(argv, env = process.env) {
16
20
  const { companyUid } = resolveProcessFlagCompanyContext(env);
17
21
  if (!companyUid)
18
22
  return false;
19
- return readFlagSnapshotFile(companyUid, { env })?.flags[parsed.entry.flagKey] === true;
23
+ return readFlagSnapshotFile(companyUid, { env })?.flags[flagKey] === true;
20
24
  }
21
25
  catch {
22
26
  // The new path is a rollout optimization. Any unavailable local scope or
@@ -99,5 +99,5 @@ export declare const CONTRIBUTION_KEYS: ContributionKey[];
99
99
  */
100
100
  export declare function payloadFor(key: ContributionKey, item: string): string;
101
101
  /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
102
- export declare const SYMLINK_KEYS: ("workers" | "mcp" | "commands" | "skills" | "knowledge" | "hooks" | "policies" | "scripts")[];
102
+ export declare const SYMLINK_KEYS: ("workers" | "mcp" | "commands" | "hooks" | "skills" | "policies" | "knowledge" | "scripts")[];
103
103
  //# sourceMappingURL=contribution-table.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.281.0",
3
+ "version": "5.283.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {