@dev-loops/core 1.0.0-rc.4 → 1.0.0-rc.6
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/package.json +6 -1
- package/src/claude/hook-decisions.mjs +172 -5
- package/src/cli/primitives.mjs +21 -0
- package/src/config/config.mjs +83 -2
- package/src/config/extension-defaults.yaml +5 -0
- package/src/github/comment-id-guard.mjs +70 -0
- package/src/github/copilot-helpers.mjs +31 -0
- package/src/github/issue-ops.mjs +6 -0
- package/src/loop/agent-stall.mjs +194 -0
- package/src/loop/bash-command-classify.mjs +277 -0
- package/src/loop/cache-telemetry-evidence.mjs +437 -0
- package/src/loop/copilot-ci-status.mjs +18 -7
- package/src/loop/default-branch-guard.mjs +1 -1
- package/src/loop/gate-fanin.mjs +244 -0
- package/src/loop/handoff-envelope.mjs +28 -1
- package/src/loop/issue-refinement-artifact.mjs +154 -18
- package/src/loop/main-checkout-ff.mjs +39 -0
- package/src/loop/primer-evidence.mjs +375 -0
- package/src/loop/public-dev-loop-routing.mjs +12 -0
- package/src/loop/retrospective-checkpoint.mjs +77 -13
- package/src/loop/review-dispatch-plan.mjs +595 -0
- package/src/loop/review-lineage.mjs +588 -0
- package/src/loop/worktree-guard.mjs +80 -0
- package/src/projects/move-queue-item.mjs +37 -1
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-dispatch-plan.mjs — cache-aware review dispatch plan + request-prefix
|
|
3
|
+
* fingerprinting + stable/volatile request separation (issue #1468 slices 1-2).
|
|
4
|
+
*
|
|
5
|
+
* This module is the mechanically-checkable foundation for the cache-efficient
|
|
6
|
+
* review dispatch design (Section A/B/C/D of the #1468 spec). It owns:
|
|
7
|
+
*
|
|
8
|
+
* 1. Harness capability model — explicit representation of what a harness can
|
|
9
|
+
* observe/control about provider prompt caching
|
|
10
|
+
* (breakpointControl / barrierSignal / cacheTtlControl / usageTelemetry).
|
|
11
|
+
* Opaque capabilities are represented as such and are NEVER described as
|
|
12
|
+
* verified cache hits.
|
|
13
|
+
* 2. Request-prefix fingerprinting — a deterministic sha256 over every
|
|
14
|
+
* cache-relevant value the dev-loops layer observes or controls (concrete
|
|
15
|
+
* model, tool definitions/order, system/project/agent instructions,
|
|
16
|
+
* thinking/tool-choice settings, content-block boundaries, shared artifact
|
|
17
|
+
* bytes, and breakpoint/TTL intent). Values owned opaquely by a harness are
|
|
18
|
+
* represented as a placeholder, never assumed identical.
|
|
19
|
+
* 3. Stable/volatile request separation — physically separates stable handoff
|
|
20
|
+
* content from volatile `gateState` at the request/artifact boundary, so a
|
|
21
|
+
* provider-visible cache boundary sits after the stable materialized
|
|
22
|
+
* briefing block and before the late volatile tail + angle suffix.
|
|
23
|
+
* 4. Dispatch-plan builder — one deterministic per-gate-round artifact that
|
|
24
|
+
* records the complete cache-relevant request shape without duplicating
|
|
25
|
+
* briefing content (Section A).
|
|
26
|
+
* 5. Primer-form default — deterministic default by harness capability
|
|
27
|
+
* (Section C/D): first-output-observable harnesses may let a lead reviewer
|
|
28
|
+
* prime; completion-only harnesses default to a short dedicated primer
|
|
29
|
+
* unless an adequate TTL is explicit; multiple concrete models partition
|
|
30
|
+
* into one primer group per model/request-prefix.
|
|
31
|
+
*
|
|
32
|
+
* This module is pure and offline: no GitHub, no harness, no clock. All runtime
|
|
33
|
+
* execution adapters consume it; it never executes a reviewer itself.
|
|
34
|
+
*/
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
|
|
37
|
+
/* ------------------------------------------------------------------ *
|
|
38
|
+
* 1. Harness capability model
|
|
39
|
+
* ------------------------------------------------------------------ */
|
|
40
|
+
|
|
41
|
+
/** Allowed values for each capability dimension (Section D). */
|
|
42
|
+
export const BREAKPOINT_CONTROL_VALUES = Object.freeze(["explicit", "automatic", "opaque"]);
|
|
43
|
+
export const BARRIER_SIGNAL_VALUES = Object.freeze(["first_output", "completion_only"]);
|
|
44
|
+
export const CACHE_TTL_CONTROL_VALUES = Object.freeze(["5m_1h", "fixed", "opaque"]);
|
|
45
|
+
export const USAGE_TELEMETRY_VALUES = Object.freeze(["available", "unavailable"]);
|
|
46
|
+
|
|
47
|
+
/** Allowed breakpoint/TTL intents a consumer may declare for a request group. */
|
|
48
|
+
export const TTL_INTENT_VALUES = Object.freeze(["5m", "1h", "harness_managed"]);
|
|
49
|
+
|
|
50
|
+
/** Cache boundary markers; the only current boundary is after the shared prefix. */
|
|
51
|
+
export const CACHE_BOUNDARY_AFTER_SHARED_PREFIX = "after_shared_prefix";
|
|
52
|
+
export const CACHE_BOUNDARY_VALUES = Object.freeze([CACHE_BOUNDARY_AFTER_SHARED_PREFIX]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Default capability posture per harness name. These are conservative defaults:
|
|
56
|
+
* any harness whose provider-cache controls we cannot assert explicitly is
|
|
57
|
+
* represented with `opaque` / `unavailable` capabilities, which fail closed
|
|
58
|
+
* rather than over-claim. Consumers may pass explicit capabilities to
|
|
59
|
+
* {normalizeHarnessCapabilities}.
|
|
60
|
+
*/
|
|
61
|
+
export const HARNESS_DEFAULT_CAPABILITIES = Object.freeze({
|
|
62
|
+
claude: Object.freeze({
|
|
63
|
+
breakpointControl: "automatic",
|
|
64
|
+
barrierSignal: "completion_only",
|
|
65
|
+
cacheTtlControl: "fixed",
|
|
66
|
+
usageTelemetry: "available",
|
|
67
|
+
}),
|
|
68
|
+
pi: Object.freeze({
|
|
69
|
+
breakpointControl: "opaque",
|
|
70
|
+
barrierSignal: "completion_only",
|
|
71
|
+
cacheTtlControl: "opaque",
|
|
72
|
+
usageTelemetry: "unavailable",
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const CAPABILITY_DIMENSIONS = [
|
|
77
|
+
["breakpointControl", BREAKPOINT_CONTROL_VALUES],
|
|
78
|
+
["barrierSignal", BARRIER_SIGNAL_VALUES],
|
|
79
|
+
["cacheTtlControl", CACHE_TTL_CONTROL_VALUES],
|
|
80
|
+
["usageTelemetry", USAGE_TELEMETRY_VALUES],
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize + validate a harness capability object. Fails closed on unknown
|
|
85
|
+
* dimension names, unknown values, or a non-object. Returns a frozen canonical
|
|
86
|
+
* object.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} input
|
|
89
|
+
* @param {string} [input.harness] - harness name; used only for the default merge
|
|
90
|
+
* (matches HARNESS_DEFAULT_CAPABILITIES keys, else fails closed on unknown).
|
|
91
|
+
* @param {object} [input.capabilities] - explicit per-dimension overrides.
|
|
92
|
+
* @returns {Readonly<Record<string,string>>} frozen capability object.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeHarnessCapabilities({ harness, capabilities } = {}) {
|
|
95
|
+
let base = {};
|
|
96
|
+
if (harness != null) {
|
|
97
|
+
const key = String(harness).trim().toLowerCase();
|
|
98
|
+
if (!Object.prototype.hasOwnProperty.call(HARNESS_DEFAULT_CAPABILITIES, key)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Unknown harness ${JSON.stringify(harness)} — must be one of ${Object.keys(HARNESS_DEFAULT_CAPABILITIES).join(", ")} or supply explicit capabilities`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
base = HARNESS_DEFAULT_CAPABILITIES[key];
|
|
104
|
+
}
|
|
105
|
+
const merged = { ...base };
|
|
106
|
+
if (capabilities != null) {
|
|
107
|
+
if (typeof capabilities !== "object" || Array.isArray(capabilities)) {
|
|
108
|
+
throw new Error("capabilities must be an object of dimension -> value pairs");
|
|
109
|
+
}
|
|
110
|
+
for (const [dim, value] of Object.entries(capabilities)) {
|
|
111
|
+
const known = CAPABILITY_DIMENSIONS.find(([name]) => name === dim);
|
|
112
|
+
if (!known) {
|
|
113
|
+
throw new Error(`Unknown capability dimension ${JSON.stringify(dim)}`);
|
|
114
|
+
}
|
|
115
|
+
const [, allowed] = known;
|
|
116
|
+
if (!allowed.includes(value)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Invalid ${dim} ${JSON.stringify(value)} — must be one of ${allowed.join(", ")}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
merged[dim] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Fail closed if any dimension was never resolved to a real value.
|
|
125
|
+
for (const [dim] of CAPABILITY_DIMENSIONS) {
|
|
126
|
+
if (merged[dim] == null) {
|
|
127
|
+
throw new Error(`Capability dimension ${dim} was not resolved (must be explicit or from a known harness)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
breakpointControl: merged.breakpointControl,
|
|
132
|
+
barrierSignal: merged.barrierSignal,
|
|
133
|
+
cacheTtlControl: merged.cacheTtlControl,
|
|
134
|
+
usageTelemetry: merged.usageTelemetry,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Is this capability set honest about provider cache reuse? A harness whose
|
|
140
|
+
* usage telemetry is `unavailable` cannot claim verified `1 write + N reads`.
|
|
141
|
+
* This is the fail-closed honesty gate for telemetry evidence (Section D).
|
|
142
|
+
*
|
|
143
|
+
* @param {Readonly<Record<string,string>>} caps - normalized capabilities.
|
|
144
|
+
* @returns {{ verified: boolean, reason: string|null }}
|
|
145
|
+
*/
|
|
146
|
+
export function cacheReuseVeracity(caps) {
|
|
147
|
+
if (!caps) return { verified: false, reason: "missing capability record" };
|
|
148
|
+
if (caps.usageTelemetry !== "available") {
|
|
149
|
+
return {
|
|
150
|
+
verified: false,
|
|
151
|
+
reason: `usageTelemetry=${caps.usageTelemetry} — provider reuse cannot be verified, only ordering + fingerprint invariants may be claimed`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { verified: true, reason: null };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ------------------------------------------------------------------ *
|
|
158
|
+
* 2. Request-prefix fingerprinting
|
|
159
|
+
* ------------------------------------------------------------------ */
|
|
160
|
+
|
|
161
|
+
const HEX = /^[0-9a-f]{64}$/;
|
|
162
|
+
|
|
163
|
+
/** @param {string} value @returns {boolean} */
|
|
164
|
+
function isSha256Hex(value) {
|
|
165
|
+
return typeof value === "string" && HEX.test(value.trim().toLowerCase());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deterministic sha256 hex of a canonical-JSON serialization. Content may be a
|
|
170
|
+
* string, Buffer, or the live object; buffers are canonicalized by hex so a
|
|
171
|
+
* push/pull through memory vs disk yields identical bytes.
|
|
172
|
+
*
|
|
173
|
+
* @param {unknown} content
|
|
174
|
+
* @returns {string} `sha256:<64-hex>`
|
|
175
|
+
*/
|
|
176
|
+
export function sha256Hex(content) {
|
|
177
|
+
const h = createHash("sha256");
|
|
178
|
+
if (Buffer.isBuffer(content)) {
|
|
179
|
+
h.update(content);
|
|
180
|
+
} else if (typeof content === "string") {
|
|
181
|
+
h.update(content);
|
|
182
|
+
} else {
|
|
183
|
+
// Deterministic canonical key ordering via stable stringify.
|
|
184
|
+
const canonical = stableStringify(content);
|
|
185
|
+
h.update(JSON.stringify(canonical));
|
|
186
|
+
}
|
|
187
|
+
return `sha256:${h.digest("hex")}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const isPlainObject = (v) =>
|
|
191
|
+
v != null && typeof v === "object" && !Array.isArray(v) && !Buffer.isBuffer(v);
|
|
192
|
+
|
|
193
|
+
/** Recursively sort object keys for a byte-deterministic serialization. */
|
|
194
|
+
function stableStringify(value) {
|
|
195
|
+
if (Array.isArray(value)) return value.map(stableStringify);
|
|
196
|
+
// Canonicalize nested Buffers to hex (the `__buffer:` prefix keeps a Buffer
|
|
197
|
+
// distinct from a string that happens to equal its hex, so bytes and text can
|
|
198
|
+
// never collide). Mirrors sha256Hex's top-level Buffer handling so a push/pull
|
|
199
|
+
// through memory vs disk yields identical bytes even for nested buffers.
|
|
200
|
+
if (Buffer.isBuffer(value)) return `__buffer:${value.toString("hex")}`;
|
|
201
|
+
if (isPlainObject(value)) {
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const key of Object.keys(value).sort()) out[key] = stableStringify(value[key]);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fingerprint the complete observable request prefix (Section A). Every
|
|
211
|
+
* cache-relevant value the dev-loops layer observes or controls is folded into
|
|
212
|
+
* the hash: concrete model, tool definitions/order, system/project/agent
|
|
213
|
+
* instructions, thinking/tool-choice settings, content-block boundaries, shared
|
|
214
|
+
* artifact bytes, and breakpoint/TTL intent. Values owned opaquely by a harness
|
|
215
|
+
* should be passed as `null`-free opaque markers (see `opaqueMarker`).
|
|
216
|
+
*
|
|
217
|
+
* @param {object} input
|
|
218
|
+
* @param {string} input.model - concrete model id for this request group.
|
|
219
|
+
* @param {string[]|object[]} [input.tools] - tool definitions/order.
|
|
220
|
+
* @param {string|string[]} [input.systemInstructions] - system/project/agent instructions.
|
|
221
|
+
* @param {object|string} [input.settings] - thinking/tool-choice settings.
|
|
222
|
+
* @param {object[]} [input.contentBlocks] - content-block boundaries + shared bytes.
|
|
223
|
+
* @param {string} [input.sharedArtifact] - shared artifact reference (path) or bytes.
|
|
224
|
+
* @param {string} [input.cacheBoundary] - e.g. `after_shared_prefix`.
|
|
225
|
+
* @param {string} [input.ttlIntent] - one of TTL_INTENT_VALUES.
|
|
226
|
+
* @param {string[]} [input.angleSuffix] - angle-specific suffix (excluded from the
|
|
227
|
+
* STABLE prefix fingerprint; included here only when invasive).
|
|
228
|
+
* @returns {{ fingerprint: string, canonical: object }}
|
|
229
|
+
*/
|
|
230
|
+
export function fingerprintRequestPrefix(input) {
|
|
231
|
+
if (!input || typeof input !== "object") {
|
|
232
|
+
throw new Error("fingerprintRequestPrefix requires an object input");
|
|
233
|
+
}
|
|
234
|
+
if (typeof input.model !== "string" || input.model.trim().length === 0) {
|
|
235
|
+
throw new Error("fingerprintRequestPrefix requires a non-empty concrete model");
|
|
236
|
+
}
|
|
237
|
+
// Fail closed on invalid shapes (AC-2: the fingerprint covers the COMPLETE
|
|
238
|
+
// observable prefix). A silently-coerced non-array tools/contentBlocks would
|
|
239
|
+
// collapse two genuinely different request prefixes into one fingerprint, so
|
|
240
|
+
// reject rather than quietly omit.
|
|
241
|
+
if (input.tools != null && !Array.isArray(input.tools)) {
|
|
242
|
+
throw new Error("fingerprintRequestPrefix tools must be an array of tool definitions");
|
|
243
|
+
}
|
|
244
|
+
if (input.contentBlocks != null && !Array.isArray(input.contentBlocks)) {
|
|
245
|
+
throw new Error("fingerprintRequestPrefix contentBlocks must be an array");
|
|
246
|
+
}
|
|
247
|
+
// Fail closed on out-of-enum cacheBoundary/ttlIntent: a caller typo would
|
|
248
|
+
// fold an undefinable value into the fingerprint that no other caller could
|
|
249
|
+
// ever reproduce/validate, silently undermining the mechanically-checkable
|
|
250
|
+
// contract (AC-2 parity with validateRequestGroups).
|
|
251
|
+
const boundary = input.cacheBoundary ?? CACHE_BOUNDARY_AFTER_SHARED_PREFIX;
|
|
252
|
+
const ttl = input.ttlIntent ?? "harness_managed";
|
|
253
|
+
if (!CACHE_BOUNDARY_VALUES.includes(boundary)) {
|
|
254
|
+
throw new Error(`fingerprintRequestPrefix invalid cacheBoundary ${JSON.stringify(boundary)}`);
|
|
255
|
+
}
|
|
256
|
+
if (!TTL_INTENT_VALUES.includes(ttl)) {
|
|
257
|
+
throw new Error(`fingerprintRequestPrefix invalid ttlIntent ${JSON.stringify(ttl)}`);
|
|
258
|
+
}
|
|
259
|
+
const canonical = {
|
|
260
|
+
model: input.model.trim(),
|
|
261
|
+
tools: input.tools ?? [],
|
|
262
|
+
systemInstructions: input.systemInstructions ?? null,
|
|
263
|
+
settings: input.settings ?? null,
|
|
264
|
+
contentBlocks: input.contentBlocks ?? null,
|
|
265
|
+
sharedArtifact: input.sharedArtifact ?? null,
|
|
266
|
+
cacheBoundary: boundary,
|
|
267
|
+
ttlIntent: ttl,
|
|
268
|
+
// angleSuffix is excluded from the STABLE prefix fingerprint but is always
|
|
269
|
+
// folded into this full request-prefix fingerprint when present, so the
|
|
270
|
+
// claimed volatile difference stays assertable.
|
|
271
|
+
...(input.angleSuffix != null ? { angleSuffix: input.angleSuffix } : {}),
|
|
272
|
+
};
|
|
273
|
+
return { fingerprint: sha256Hex(canonical), canonical };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Opaque placeholder for a value owned opaquely by the harness. Using this in a
|
|
278
|
+
* fingerprint input records that the value existed but was not byte-observable,
|
|
279
|
+
* so two runs that differ only in an unobservable harness value STILL collapse
|
|
280
|
+
* to the same fingerprint (they cannot be proven different) — and, symmetrically,
|
|
281
|
+
* a claimed difference under an opaque value is not assertable.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} label
|
|
284
|
+
* @returns {string}
|
|
285
|
+
*/
|
|
286
|
+
export function opaqueMarker(label) {
|
|
287
|
+
return `__opaque:${String(label)}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* ------------------------------------------------------------------ *
|
|
291
|
+
* 3. Stable/volatile request separation
|
|
292
|
+
* ------------------------------------------------------------------ */
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The stable-prefix fingerprint is computed ONLY over the stable prefix +
|
|
296
|
+
* materialized briefing block — never the volatile tail or angle suffix. This
|
|
297
|
+
* is the mechanical proof for AC-1: changing only `gateState` (or the angle
|
|
298
|
+
* suffix) MUST NOT change the shared request prefix block.
|
|
299
|
+
*
|
|
300
|
+
* @param {object} input
|
|
301
|
+
* @param {string|Buffer} input.stablePrefix - stable review-agent/system/tool prefix.
|
|
302
|
+
* @param {string|Buffer} input.briefingBlock - materialized shared briefing block bytes.
|
|
303
|
+
* @param {string} [input.cacheBoundary]
|
|
304
|
+
* @param {string} [input.ttlIntent]
|
|
305
|
+
* @returns {{ stableFingerprint: string, briefedBytes: string }}
|
|
306
|
+
*/
|
|
307
|
+
export function fingerprintStablePrefix({ stablePrefix, briefingBlock, cacheBoundary, ttlIntent } = {}) {
|
|
308
|
+
const parts = [stablePrefix ?? "", briefingBlock ?? ""];
|
|
309
|
+
const stableFingerprint = sha256Hex({ stablePrefix: parts[0], briefingBlock: parts[1] });
|
|
310
|
+
return {
|
|
311
|
+
stableFingerprint,
|
|
312
|
+
// Canonicalize through stableStringify so a Buffer stablePrefix/briefingBlock
|
|
313
|
+
// becomes `__buffer:<hex>` here too — raw JSON.stringify would expand Buffers
|
|
314
|
+
// into large {type:"Buffer",data:[…]} decimal arrays (byte-unstable + costly),
|
|
315
|
+
// reintroducing exactly what sha256Hex avoids.
|
|
316
|
+
briefedBytes: JSON.stringify(stableStringify({
|
|
317
|
+
cacheBoundary: cacheBoundary ?? CACHE_BOUNDARY_AFTER_SHARED_PREFIX,
|
|
318
|
+
ttlIntent: ttlIntent ?? "harness_managed",
|
|
319
|
+
stableBytes: parts,
|
|
320
|
+
})),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Compose a cache-aware request as ordered segments with a declared cache
|
|
326
|
+
* boundary after the stable briefing block (Section B):
|
|
327
|
+
*
|
|
328
|
+
* [stable review-agent/system/tool prefix]
|
|
329
|
+
* [materialized shared briefing block]
|
|
330
|
+
* <cache boundary>
|
|
331
|
+
* [late volatile gate state, when needed]
|
|
332
|
+
* [angle-specific suffix]
|
|
333
|
+
*
|
|
334
|
+
* Returns the ordered segment list plus the boundary index and the stable
|
|
335
|
+
* fingerprints, so a consumer can render the request and a verifier can assert
|
|
336
|
+
* stable-prefix equality byte-for-byte regardless of volatile/angle changes.
|
|
337
|
+
*
|
|
338
|
+
* @param {object} input
|
|
339
|
+
* @param {string|Buffer} input.stablePrefix
|
|
340
|
+
* @param {string|Buffer} input.briefingBlock
|
|
341
|
+
* @param {object} [input.volatileState] - late volatile gate state (serialized AFTER the boundary).
|
|
342
|
+
* @param {string|Buffer} [input.angleSuffix]
|
|
343
|
+
* @param {string} [input.cacheBoundary]
|
|
344
|
+
* @param {string} [input.ttlIntent]
|
|
345
|
+
* @returns {object} ordered segments + boundary + fingerprints.
|
|
346
|
+
*/
|
|
347
|
+
export function composeCacheAwareRequest({ stablePrefix, briefingBlock, volatileState, angleSuffix, cacheBoundary, ttlIntent } = {}) {
|
|
348
|
+
const boundary = cacheBoundary ?? CACHE_BOUNDARY_AFTER_SHARED_PREFIX;
|
|
349
|
+
const ttl = ttlIntent ?? "harness_managed";
|
|
350
|
+
// Fail closed on out-of-enum cacheBoundary/ttlIntent (parity with
|
|
351
|
+
// fingerprintRequestPrefix / validateRequestGroups) so a caller typo cannot
|
|
352
|
+
// silently flow into the returned structure and break later parity checks.
|
|
353
|
+
if (!CACHE_BOUNDARY_VALUES.includes(boundary)) {
|
|
354
|
+
throw new Error(`composeCacheAwareRequest invalid cacheBoundary ${JSON.stringify(boundary)}`);
|
|
355
|
+
}
|
|
356
|
+
if (!TTL_INTENT_VALUES.includes(ttl)) {
|
|
357
|
+
throw new Error(`composeCacheAwareRequest invalid ttlIntent ${JSON.stringify(ttl)}`);
|
|
358
|
+
}
|
|
359
|
+
const { stableFingerprint, briefedBytes } = fingerprintStablePrefix({
|
|
360
|
+
stablePrefix,
|
|
361
|
+
briefingBlock,
|
|
362
|
+
cacheBoundary: boundary,
|
|
363
|
+
ttlIntent: ttl,
|
|
364
|
+
});
|
|
365
|
+
const late = (typeof volatileState === "object" && volatileState !== null && !Buffer.isBuffer(volatileState))
|
|
366
|
+
? JSON.stringify(volatileState)
|
|
367
|
+
: (volatileState ?? "");
|
|
368
|
+
const segments = [
|
|
369
|
+
{ slot: "stablePrefix", bytes: stablePrefix ?? "" },
|
|
370
|
+
{ slot: "briefingBlock", bytes: briefingBlock ?? "" },
|
|
371
|
+
];
|
|
372
|
+
// The cache boundary sits AFTER the stable prefix + briefing block. The marker
|
|
373
|
+
// segment is a structural pointer, NOT request bytes: it is byte-empty so a
|
|
374
|
+
// consumer concatenating segment bytes never injects the boundary label into
|
|
375
|
+
// the provider-visible prompt (the label lives in the separate cacheBoundary
|
|
376
|
+
// field).
|
|
377
|
+
const boundaryIndex = segments.length;
|
|
378
|
+
segments.push({ slot: "<cache boundary>", bytes: "" });
|
|
379
|
+
if (late.length > 0) segments.push({ slot: "volatileState", bytes: late });
|
|
380
|
+
if (angleSuffix != null && String(angleSuffix).length > 0) {
|
|
381
|
+
segments.push({ slot: "angleSuffix", bytes: String(angleSuffix) });
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
cacheBoundary: boundary,
|
|
385
|
+
boundaryIndex,
|
|
386
|
+
stableFingerprint,
|
|
387
|
+
briefedBytes,
|
|
388
|
+
segments,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ------------------------------------------------------------------ *
|
|
393
|
+
* 4. Dispatch-plan builder (Section A)
|
|
394
|
+
* ------------------------------------------------------------------ */
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Build a deterministic per-gate-round dispatch plan. The plan records the
|
|
398
|
+
* complete cache-relevant request shape WITHOUT duplicating briefing content
|
|
399
|
+
* (it stores the shared-prefix path + hash, not the bytes).
|
|
400
|
+
*
|
|
401
|
+
* @param {object} input
|
|
402
|
+
* @param {string} input.gate - draft_gate | pre_approval_gate | ...
|
|
403
|
+
* @param {string} input.headSha - full reviewed head SHA.
|
|
404
|
+
* @param {string} [input.sharedPrefixPath] - path to the materialized briefing-prefix file.
|
|
405
|
+
* @param {string} [input.sharedPrefixHash] - `sha256:<hex>` of those bytes.
|
|
406
|
+
* @param {Array<object>} [input.requestGroups] - each { model, requestPrefixFingerprint, cacheBoundary, ttlIntent, angles[] }.
|
|
407
|
+
* @param {object} [input.capabilities] - normalized harness capabilities.
|
|
408
|
+
* @param {object} [input.extra] - opaque consumer fields folded into the canonical hash but ignored by validation.
|
|
409
|
+
* @returns {object} validated dispatch-plan object.
|
|
410
|
+
*/
|
|
411
|
+
export function buildReviewDispatchPlan({ gate, headSha, sharedPrefixPath, sharedPrefixHash, requestGroups = [], capabilities, extra } = {}) {
|
|
412
|
+
if (typeof gate !== "string" || gate.length === 0) {
|
|
413
|
+
throw new Error("buildReviewDispatchPlan requires a non-empty gate");
|
|
414
|
+
}
|
|
415
|
+
if (typeof headSha !== "string" || !/^[0-9a-f]{7,64}$/i.test(headSha.trim())) {
|
|
416
|
+
throw new Error("buildReviewDispatchPlan requires a hex headSha");
|
|
417
|
+
}
|
|
418
|
+
if (sharedPrefixHash != null && !isSha256Hex(String(sharedPrefixHash).replace(/^sha256:/, ""))) {
|
|
419
|
+
throw new Error(`sharedPrefixHash must be sha256:<64 hex> or absent, got ${JSON.stringify(sharedPrefixHash)}`);
|
|
420
|
+
}
|
|
421
|
+
// Normalize the stored artifact to a single canonical `sha256:<hex>` form so
|
|
422
|
+
// the plan (and its planHash) never depends on whether the caller passed a
|
|
423
|
+
// raw 64-hex string or an already-prefixed one for identical bytes (mixed-
|
|
424
|
+
// format artifacts are byte-non-deterministic across callers).
|
|
425
|
+
const normalizedSharedPrefixHash = sharedPrefixHash != null
|
|
426
|
+
? `sha256:${String(sharedPrefixHash).replace(/^sha256:/, "").trim().toLowerCase()}`
|
|
427
|
+
: null;
|
|
428
|
+
const groups = validateRequestGroups(requestGroups);
|
|
429
|
+
let caps = capabilities;
|
|
430
|
+
if (caps != null) {
|
|
431
|
+
// A capability spec may carry a `harness` key plus dimension overrides.
|
|
432
|
+
const hasHarness = typeof caps === "object" && !Array.isArray(caps) && typeof caps.harness === "string";
|
|
433
|
+
if (hasHarness) {
|
|
434
|
+
const { harness: harnessName, ...dims } = caps;
|
|
435
|
+
caps = normalizeHarnessCapabilities({ harness: harnessName, capabilities: dims });
|
|
436
|
+
} else {
|
|
437
|
+
caps = normalizeHarnessCapabilities({ capabilities: caps });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const plan = {
|
|
441
|
+
gate,
|
|
442
|
+
headSha: headSha.trim().toLowerCase(),
|
|
443
|
+
...(sharedPrefixPath != null ? { sharedPrefixPath } : {}),
|
|
444
|
+
...(normalizedSharedPrefixHash != null ? { sharedPrefixHash: normalizedSharedPrefixHash } : {}),
|
|
445
|
+
requestGroups: groups,
|
|
446
|
+
...(caps != null ? { capabilities: caps } : {}),
|
|
447
|
+
};
|
|
448
|
+
// Deterministic plan hash over the canonical plan, with opaque `extra` folded
|
|
449
|
+
// under its own namespace so an `extra` key can never shadow a real plan
|
|
450
|
+
// field (the fingerprint must always pin the plan's actual values).
|
|
451
|
+
const planHash = sha256Hex({ ...plan, extra: extra ?? {} }).replace(/^sha256:/, "");
|
|
452
|
+
return Object.freeze({ ...plan, planHash: `sha256:${planHash}` });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function validateRequestGroups(requestGroups) {
|
|
456
|
+
if (!Array.isArray(requestGroups)) {
|
|
457
|
+
throw new Error("requestGroups must be an array");
|
|
458
|
+
}
|
|
459
|
+
return requestGroups.map((g, i) => {
|
|
460
|
+
if (typeof g.model !== "string" || g.model.trim().length === 0) {
|
|
461
|
+
throw new Error(`requestGroups[${i}].model must be a non-empty concrete model`);
|
|
462
|
+
}
|
|
463
|
+
const fpRaw = typeof g.requestPrefixFingerprint === "string" ? g.requestPrefixFingerprint.trim() : null;
|
|
464
|
+
if (fpRaw != null && !isSha256Hex(fpRaw.replace(/^sha256:/, ""))) {
|
|
465
|
+
throw new Error(`requestGroups[${i}].requestPrefixFingerprint must be sha256:<hex> or absent`);
|
|
466
|
+
}
|
|
467
|
+
// Normalize to a single canonical `sha256:<hex>` form (same rationale as
|
|
468
|
+
// sharedPrefixHash) so the grouping/partition logic and the plan artifact
|
|
469
|
+
// see byte-identical fingerprints regardless of caller format.
|
|
470
|
+
const fp = fpRaw != null
|
|
471
|
+
? `sha256:${fpRaw.replace(/^sha256:/, "").toLowerCase()}`
|
|
472
|
+
: null;
|
|
473
|
+
const cacheBoundary = g.cacheBoundary ?? CACHE_BOUNDARY_AFTER_SHARED_PREFIX;
|
|
474
|
+
if (!CACHE_BOUNDARY_VALUES.includes(cacheBoundary)) {
|
|
475
|
+
throw new Error(`requestGroups[${i}] invalid cacheBoundary ${JSON.stringify(cacheBoundary)}`);
|
|
476
|
+
}
|
|
477
|
+
const ttlIntent = g.ttlIntent ?? "harness_managed";
|
|
478
|
+
if (!TTL_INTENT_VALUES.includes(ttlIntent)) {
|
|
479
|
+
throw new Error(`requestGroups[${i}] invalid ttlIntent ${JSON.stringify(ttlIntent)}`);
|
|
480
|
+
}
|
|
481
|
+
if (!Array.isArray(g.angles) || g.angles.length === 0) {
|
|
482
|
+
throw new Error(`requestGroups[${i}].angles must be a non-empty array of angle names`);
|
|
483
|
+
}
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
model: g.model.trim(),
|
|
486
|
+
...(fpRaw != null ? { requestPrefixFingerprint: fp } : {}),
|
|
487
|
+
cacheBoundary,
|
|
488
|
+
ttlIntent,
|
|
489
|
+
angles: [...new Set(g.angles.map(String))],
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ------------------------------------------------------------------ *
|
|
495
|
+
* 5. Primer-form default (Section C/D)
|
|
496
|
+
* ------------------------------------------------------------------ */
|
|
497
|
+
|
|
498
|
+
export const PRIMER_FORM_LEAD_REVIEWER = "lead_reviewer";
|
|
499
|
+
export const PRIMER_FORM_DEDICATED = "dedicated_primer";
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Resolve the primer form for a request group by harness capability (Section
|
|
503
|
+
* C/D default-by-harness-capability):
|
|
504
|
+
* - first_output-observable harness + an adequate TTL (5m/1h) → may prime with
|
|
505
|
+
* a lead reviewer (`lead_reviewer`).
|
|
506
|
+
* - completion_only harness without an adequate explicit TTL → short dedicated
|
|
507
|
+
* primer (`dedicated_primer`) so a full review wait cannot silently outrun
|
|
508
|
+
* the cache TTL.
|
|
509
|
+
* - usageTelemetry unavailable + opaque controls → conservative dedicated
|
|
510
|
+
* primer (cannot observe barrier evidence), failing toward the safe option.
|
|
511
|
+
*
|
|
512
|
+
* @param {object} input
|
|
513
|
+
* @param {Readonly<Record<string,string>>} input.capabilities - normalized capabilities.
|
|
514
|
+
* @param {string} [input.ttlIntent] - declared TTL intent for the group.
|
|
515
|
+
* @returns {{ primerForm: "lead_reviewer"|"dedicated_primer", reason: string }}
|
|
516
|
+
*/
|
|
517
|
+
export function resolvePrimerForm({ capabilities, ttlIntent } = {}) {
|
|
518
|
+
const caps = capabilities ?? {};
|
|
519
|
+
const adequateTtl = ttlIntent === "5m" || ttlIntent === "1h";
|
|
520
|
+
if (caps.barrierSignal === "first_output" && (adequateTtl || caps.cacheTtlControl === "5m_1h")) {
|
|
521
|
+
const reason = adequateTtl
|
|
522
|
+
? "barrierSignal=first_output and an adequate TTL is available — a real lead reviewer may prime the group"
|
|
523
|
+
: "barrierSignal=first_output with cacheTtlControl=5m_1h capability — a real lead reviewer fits the cache control window";
|
|
524
|
+
return {
|
|
525
|
+
primerForm: PRIMER_FORM_LEAD_REVIEWER,
|
|
526
|
+
reason,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (caps.barrierSignal === "completion_only" && caps.cacheTtlControl === "5m_1h" && ttlIntent === "1h") {
|
|
530
|
+
return {
|
|
531
|
+
primerForm: PRIMER_FORM_LEAD_REVIEWER,
|
|
532
|
+
reason: "completion_only harness with an explicit one-hour TTL — a long lead review fits the cache window",
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
primerForm: PRIMER_FORM_DEDICATED,
|
|
537
|
+
reason: "completion_only or opaque controls without an adequate explicit TTL — a short dedicated primer avoids a full review outrunning the cache TTL",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Partition request groups into primer groups — one per distinct concrete
|
|
543
|
+
* model/request-prefix (Section C: heterogeneous per-angle model routing cannot
|
|
544
|
+
* silently reuse one model's primer to warm another).
|
|
545
|
+
*
|
|
546
|
+
* @param {object[]} requestGroups - validated request groups.
|
|
547
|
+
* @param {Readonly<Record<string,string>>} [capabilities]
|
|
548
|
+
* @returns {Array<{ model: string, requestPrefixFingerprint: string|null, groups: object[] }>}
|
|
549
|
+
*/
|
|
550
|
+
export function partitionPrimerGroups(requestGroups, capabilities = {}) {
|
|
551
|
+
if (!Array.isArray(requestGroups)) throw new Error("partitionPrimerGroups requires an array");
|
|
552
|
+
// One primer group per distinct (model, request-prefix) — a primer warms ONE
|
|
553
|
+
// concrete model's provider cache under a specific request prefix. Groups that
|
|
554
|
+
// share a model AND a real (`sha256:<hex>`) fingerprint collapse into one
|
|
555
|
+
// bucket; groups without a proven fingerprint never collapse (see below). Use
|
|
556
|
+
// a nested Map keyed by model then by fingerprint (never a single delimiter-joined
|
|
557
|
+
// string), so a model id that itself contains "::" stays intact and two distinct
|
|
558
|
+
// (model, fp) pairs can never collide onto one bucket (dedup/identity safety).
|
|
559
|
+
const byModel = new Map();
|
|
560
|
+
for (let i = 0; i < requestGroups.length; i++) {
|
|
561
|
+
const g = requestGroups[i];
|
|
562
|
+
if (!byModel.has(g.model)) byModel.set(g.model, new Map());
|
|
563
|
+
const fpMap = byModel.get(g.model);
|
|
564
|
+
// Fail closed: only groups carrying a REAL (`sha256:<hex>`) fingerprint may
|
|
565
|
+
// share a primer bucket (that proves a common cache-relevant prefix). A
|
|
566
|
+
// fingerprint-less group is keyed by its own index so it NEVER collapses
|
|
567
|
+
// with another — without a fingerprint you cannot prove two groups share the
|
|
568
|
+
// same prefix, so merging them would let one primer silently cover multiple
|
|
569
|
+
// unknown prefixes (at best wasted priming, at worst misleading evidence).
|
|
570
|
+
const hasRealFp = typeof g.requestPrefixFingerprint === "string"
|
|
571
|
+
&& g.requestPrefixFingerprint.startsWith("sha256:");
|
|
572
|
+
const key = hasRealFp ? g.requestPrefixFingerprint : `__unkeyed:${i}`;
|
|
573
|
+
if (!fpMap.has(key)) fpMap.set(key, []);
|
|
574
|
+
fpMap.get(key).push(g);
|
|
575
|
+
}
|
|
576
|
+
const out = [];
|
|
577
|
+
for (const [model, fpMap] of byModel.entries()) {
|
|
578
|
+
for (const [fp, groups] of fpMap.entries()) {
|
|
579
|
+
// Resolve the primer form CONSERVATIVELY across every collapsed group's TTL
|
|
580
|
+
// intent: a partition primes with a lead reviewer only when ALL its groups
|
|
581
|
+
// would; any group that needs a dedicated primer forces the whole partition
|
|
582
|
+
// down, so a mixed-TTL collapse is never decided by the first group alone.
|
|
583
|
+
const allLead = groups.every(
|
|
584
|
+
(g) => resolvePrimerForm({ capabilities, ttlIntent: g.ttlIntent }).primerForm === PRIMER_FORM_LEAD_REVIEWER,
|
|
585
|
+
);
|
|
586
|
+
out.push({
|
|
587
|
+
model,
|
|
588
|
+
requestPrefixFingerprint: fp.startsWith("sha256:") ? fp : null,
|
|
589
|
+
primerForm: allLead ? PRIMER_FORM_LEAD_REVIEWER : PRIMER_FORM_DEDICATED,
|
|
590
|
+
groups: groups.map((g) => ({ model: g.model, angles: g.angles, ttlIntent: g.ttlIntent })),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|