@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.
- package/CHANGELOG.md +37 -0
- package/assets/scaffold/.claude/hooks/validate-policy-frontmatter.sh +485 -0
- package/assets/scaffold/core/scripts/eval-trigger.sh +193 -0
- package/assets/scaffold/core/scripts/policy-age-report.sh +156 -0
- package/assets/scaffold/core/scripts/policy-retire.sh +199 -0
- package/assets/scaffold/core/scripts/read-policy-frontmatter.sh +32 -0
- package/dist/command-catalog.generated.d.ts +67 -0
- package/dist/command-catalog.generated.js +83 -0
- package/dist/commands/core-knowledge-prefs.d.ts +10 -0
- package/dist/commands/core-knowledge-prefs.js +125 -0
- package/dist/commands/core-native-policy-flag.d.ts +9 -0
- package/dist/commands/core-native-policy-flag.js +12 -0
- package/dist/commands/core-ontology-native.d.ts +0 -1
- package/dist/commands/core-ontology-native.js +2 -30
- package/dist/commands/core-ontology.d.ts +4 -0
- package/dist/commands/core-ontology.js +8 -3
- package/dist/commands/core-policy-frontmatter.d.ts +8 -0
- package/dist/commands/core-policy-frontmatter.js +52 -0
- package/dist/commands/core-policy.d.ts +15 -0
- package/dist/commands/core-policy.js +982 -0
- package/dist/commands/core.js +14 -1
- package/dist/commands/lanes-message.js +1 -0
- package/dist/commands/lanes-move.js +2 -0
- package/dist/commands/lanes-resume.js +1 -0
- package/dist/commands/lanes-shared.d.ts +10 -1
- package/dist/commands/lanes-shared.js +2 -1
- package/dist/commands/lanes.d.ts +8 -0
- package/dist/commands/lanes.js +324 -93
- package/dist/fast-lanes-list-route.d.ts +8 -0
- package/dist/fast-lanes-list-route.js +30 -0
- package/dist/fast-lanes-list.d.ts +3 -0
- package/dist/fast-lanes-list.js +252 -0
- package/dist/fast-scaffold.d.ts +5 -0
- package/dist/fast-scaffold.js +10 -0
- package/dist/index.js +10 -1
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +24 -1
- package/dist/lib/checkpoint-ontology/merge-copies.js +70 -1
- package/dist/lib/lanes/flag-keys.d.ts +3 -0
- package/dist/lib/lanes/flag-keys.js +3 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +1 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
- package/dist/lib/lanes/outpost-dispatch.js +4 -2
- package/dist/lib/lanes/outpost-hire-cache.d.ts +3 -0
- package/dist/lib/lanes/outpost-hire-cache.js +11 -3
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +26 -0
- package/dist/lib/lanes/outpost-snapshot-cache.js +215 -0
- package/dist/lib/lanes/reconcile.d.ts +7 -1
- package/dist/lib/lanes/reconcile.js +88 -16
- package/dist/lib/lanes/spawn.js +5 -0
- package/dist/lib/lanes/store.d.ts +4 -0
- package/dist/lib/lanes/store.js +66 -0
- package/dist/lib/lanes/throttle.d.ts +29 -2
- package/dist/lib/lanes/throttle.js +145 -5
- package/dist/lib/lanes/types.d.ts +8 -1
- package/dist/lib/lanes/types.js +2 -1
- package/dist/lib/lanes/watcher.js +1 -0
- package/dist/lib/policy-validator.d.ts +2 -0
- package/dist/lib/policy-validator.js +136 -0
- package/dist/native-fast-route.d.ts +2 -0
- package/dist/native-fast-route.js +6 -2
- package/dist/utils/contribution-table.d.ts +1 -1
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
423
|
+
cpuCheckEnabled,
|
|
289
424
|
state,
|
|
290
425
|
throttle,
|
|
291
426
|
nowMs,
|
|
292
427
|
});
|
|
293
|
-
const
|
|
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. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -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,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
|
-
|
|
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[
|
|
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" | "
|
|
102
|
+
export declare const SYMLINK_KEYS: ("workers" | "mcp" | "commands" | "hooks" | "skills" | "policies" | "knowledge" | "scripts")[];
|
|
103
103
|
//# sourceMappingURL=contribution-table.d.ts.map
|