@crewhaus/spec-patch 0.5.7 → 0.6.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/dist/index.d.ts +150 -4
- package/dist/index.js +681 -46
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -117,10 +117,12 @@ export type SpecEdit = {
|
|
|
117
117
|
};
|
|
118
118
|
export type ApplySpecEditsOptions = {
|
|
119
119
|
/**
|
|
120
|
-
* The optimizer surface: every edit path must
|
|
121
|
-
* `OPTIMIZABLE_PATHS` whitelist
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* The optimizer surface: every edit path must be admitted by the spec
|
|
121
|
+
* target's `OPTIMIZABLE_PATHS` whitelist through {@link isOptimizable} —
|
|
122
|
+
* exact match (wildcards = one segment each), the structural
|
|
123
|
+
* `model_pool` / `judge` / `sub_agents` / `temperature` rule, then prefix;
|
|
124
|
+
* the same rule as `validatePatch`. Author surfaces leave this off and may
|
|
125
|
+
* edit any field — the atomic `parseSpec` re-validation is their safety floor.
|
|
124
126
|
*/
|
|
125
127
|
readonly restrictToOptimizable?: boolean;
|
|
126
128
|
};
|
|
@@ -144,6 +146,29 @@ export type ApplySpecEditsResult = {
|
|
|
144
146
|
* byte-identical (no CST round-trip reformatting).
|
|
145
147
|
*/
|
|
146
148
|
export declare function applySpecEdits(yamlText: string, edits: ReadonlyArray<SpecEdit>, opts?: ApplySpecEditsOptions): ApplySpecEditsResult;
|
|
149
|
+
/**
|
|
150
|
+
* 0.6.0 (plan §10.3, mechanism 1) — the WILDCARD segment. Inside an
|
|
151
|
+
* `OPTIMIZABLE_PATHS` entry, `"*"` matches EXACTLY ONE key segment of the
|
|
152
|
+
* patched path (a `models:` profile name, a `steps` index, a `nodes` / `roles`
|
|
153
|
+
* name, a `rules` index) — never zero segments, never more than one, never a
|
|
154
|
+
* literal `"*"` key. Two placement rules make the wildcard safe, pinned by
|
|
155
|
+
* {@link wildcardPlacementIssues} (a CI guard, not a load-time throw):
|
|
156
|
+
*
|
|
157
|
+
* 1. a wildcard is never the LAST segment — `["models","*"]` would admit a
|
|
158
|
+
* whole profile (its `model`, `tools`, `permissions`, …) by prefix;
|
|
159
|
+
* 2. a wildcard never sits ABOVE a human-owned key — the concrete tail after
|
|
160
|
+
* the last `"*"` must end in a dial, never in one of
|
|
161
|
+
* {@link WILDCARD_FORBIDDEN_LEAVES} (the identity / security / prompt keys
|
|
162
|
+
* the §10.3 table excludes) and never in a structural segment.
|
|
163
|
+
*/
|
|
164
|
+
export declare const WILDCARD_SEGMENT = "*";
|
|
165
|
+
/**
|
|
166
|
+
* Keys a wildcard-bearing entry may never terminate in: the §10.3 exclusions
|
|
167
|
+
* that live UNDER a dynamic key (`models.<name>`, `candidates[i]`,
|
|
168
|
+
* `rules[i]`, `sub_agents.<name>`). A dial entry that ended in one of these
|
|
169
|
+
* would whitelist identity or security surface across every profile at once.
|
|
170
|
+
*/
|
|
171
|
+
export declare const WILDCARD_FORBIDDEN_LEAVES: ReadonlyArray<string>;
|
|
147
172
|
/**
|
|
148
173
|
* Per-target whitelist of mutation paths the active optimizer is
|
|
149
174
|
* allowed to touch. Adding a new field here is the explicit signal that
|
|
@@ -169,6 +194,127 @@ export declare function applySpecEdits(yamlText: string, edits: ReadonlyArray<Sp
|
|
|
169
194
|
* reason: there is no whitelisted path for it to patch.
|
|
170
195
|
*/
|
|
171
196
|
export declare const OPTIMIZABLE_PATHS: Readonly<Record<Spec["target"], ReadonlyArray<ReadonlyArray<string>>>>;
|
|
197
|
+
/**
|
|
198
|
+
* 0.6.0 (plan §10.3, mechanism 2) — the STRUCTURAL rule: a path that passes
|
|
199
|
+
* through any of these segments is optimizable only by EXACT match against a
|
|
200
|
+
* listed entry, never by prefix. The whole-block entries `["steps"]`,
|
|
201
|
+
* `["nodes"]` and `["roles"]` exist because the step index / node name / role
|
|
202
|
+
* name is positional, and whole-block replacement was meant to span
|
|
203
|
+
* `model`/`instructions` — not the human-owned structure that now hangs off
|
|
204
|
+
* every routed block:
|
|
205
|
+
*
|
|
206
|
+
* - `model_pool` — the candidate ROSTER (mirroring the standing
|
|
207
|
+
* `["agent","model"]` exclusion), and from 0.6.0 the hybrid container:
|
|
208
|
+
* `rules.*.use` (rule targets), `strategy.*` role slots and
|
|
209
|
+
* `model_directed` (registers the `Escalate`/`Consult` tool surface),
|
|
210
|
+
* `reward.*` (the floor), `directives` (admits user-directed input),
|
|
211
|
+
* `classifier.{model,labels}` (judge identity), `scope`. The plan's
|
|
212
|
+
* verdict table classifies every one of these as excluded.
|
|
213
|
+
* - `judge` — a `kind: judge` gate's `escalate_to` (a routing target),
|
|
214
|
+
* `judges` (judge identity), `temperature`/`target`.
|
|
215
|
+
* - `sub_agents` — a sub-agent definition is a tool-surface / identity block
|
|
216
|
+
* (`tools`, `permissions`, `model`, `inherit_bypass`, and from 0.6.0
|
|
217
|
+
* `allowed_profiles`, `budget_share`, `inherit_routing`, its own routing).
|
|
218
|
+
* - `temperature` — a quality dial the plan lists as optimizable, but ONLY
|
|
219
|
+
* via the exact entries (`["agent","temperature"]`, the per-step / node /
|
|
220
|
+
* role wildcard entries, `models.*.temperature`): every OTHER
|
|
221
|
+
* `temperature` leaf — a candidate's, a sub-agent's, a judge's pinned
|
|
222
|
+
* sampling temperature — is human-owned, and a prefix-reached patch
|
|
223
|
+
* would land on one of those.
|
|
224
|
+
* - `model_tiers`, `model_fallbacks`, `circuit_breaker` — the three
|
|
225
|
+
* pre-pool routing blocks (§11.1 adds them to graph nodes and sub-agents
|
|
226
|
+
* in 0.6.0; steps and roles carried them before). `model_tiers` is a
|
|
227
|
+
* two-slot roster (`fast`/`default` are model identity, mirroring the
|
|
228
|
+
* standing `agent.model` exclusion), `model_fallbacks` is the failover
|
|
229
|
+
* chain and `circuit_breaker` decides when an arm is withdrawn from
|
|
230
|
+
* service — the same verdicts the §10.3 table gives their `models.*`
|
|
231
|
+
* twins. No exact entry names any of them, so the rule closes the
|
|
232
|
+
* whole-block prefix route (`["nodes"]` → `nodes.a.circuit_breaker`)
|
|
233
|
+
* without removing anything the table promises.
|
|
234
|
+
*
|
|
235
|
+
* One rule, no negative-list semantics: the exact entries
|
|
236
|
+
* `["agent","model_pool",{policy,routing,learning}]` keep working, and a
|
|
237
|
+
* whole-block `["steps"]` replacement (whose PATH carries none of these
|
|
238
|
+
* segments) is unchanged. The wildcard segment ({@link WILDCARD_SEGMENT}) and
|
|
239
|
+
* the exact entries the §10.3 table promises (`poolDials`, `judgeGateDials`,
|
|
240
|
+
* `MODEL_PROFILE_DIALS`) ride on top of this rule — it is what makes that
|
|
241
|
+
* table true for the positional shapes. Note the reconciliation the exact
|
|
242
|
+
* match performs: `["agent","temperature"]` IS listed, so the structural
|
|
243
|
+
* `temperature` segment blocks only the prefix route, never the entry.
|
|
244
|
+
*/
|
|
245
|
+
export declare const STRUCTURAL_SEGMENTS: ReadonlyArray<string>;
|
|
246
|
+
/**
|
|
247
|
+
* 0.6.0 §10.3 — the EXCLUDED half of the verdict table, stated in code rather
|
|
248
|
+
* than left to the whitelist's default-deny (the 0.3.0 discipline). Each row
|
|
249
|
+
* is a segment pattern matched as a CONTIGUOUS sub-sequence ANYWHERE in a leaf
|
|
250
|
+
* path (`"*"` = any one segment), so one row covers the same key on every
|
|
251
|
+
* host: `["model_pool","rules","*","use"]` excludes `agent.model_pool.rules[i].use`
|
|
252
|
+
* on cli and `steps[i].model_pool.rules[j].use` on workflow alike. A row that
|
|
253
|
+
* names a block (`["model_pool","candidates"]`) excludes every leaf under it.
|
|
254
|
+
*
|
|
255
|
+
* This table is NOT consulted by {@link isOptimizable} — the whitelist is the
|
|
256
|
+
* only admission rule, and anything it does not list is refused. The rows
|
|
257
|
+
* exist so the table's closing invariant can be TESTED: every leaf the
|
|
258
|
+
* 0.6.0 spec delta introduced (enumerated from the zod schema) must be
|
|
259
|
+
* classified exactly once — matched by the whitelist XOR by a row here — and
|
|
260
|
+
* the test fails on a leaf that is unclassified or claimed twice, so this
|
|
261
|
+
* table, the whitelist and the schema cannot drift apart. The Hangar spec
|
|
262
|
+
* editor and `crewhaus advise` read the reasons.
|
|
263
|
+
*/
|
|
264
|
+
export declare const HUMAN_OWNED_PATHS: ReadonlyArray<{
|
|
265
|
+
readonly pattern: ReadonlyArray<string>;
|
|
266
|
+
readonly reason: string;
|
|
267
|
+
}>;
|
|
268
|
+
/** A leaf path is human-owned when some {@link HUMAN_OWNED_PATHS} row matches it. */
|
|
269
|
+
export declare function humanOwnedReason(path: ReadonlyArray<SpecEditPathSegment>): string | undefined;
|
|
270
|
+
/**
|
|
271
|
+
* The wildcard placement guard (plan §10.3, mechanism 1): every issue is a
|
|
272
|
+
* whitelist entry that would let `"*"` sit above a human-owned key. Returns
|
|
273
|
+
* `[]` for the shipped table — pinned by the spec-patch tests, so a future
|
|
274
|
+
* entry that breaks the rule fails CI rather than widening the surface.
|
|
275
|
+
*/
|
|
276
|
+
export declare function wildcardPlacementIssues(table?: Readonly<Record<string, ReadonlyArray<ReadonlyArray<string>>>>): string[];
|
|
277
|
+
/**
|
|
278
|
+
* The single admission rule behind `validatePatch`, `applySpecEdits({
|
|
279
|
+
* restrictToOptimizable })`, the Hangar spec editor's trust badge, the
|
|
280
|
+
* feedback / watch-me advice tiers and the orchestrator's stage guard:
|
|
281
|
+
*
|
|
282
|
+
* 1. an EXACT match against a listed entry admits the path (wildcards
|
|
283
|
+
* match one segment each);
|
|
284
|
+
* 2. a path through a STRUCTURAL segment ({@link STRUCTURAL_SEGMENTS}) is
|
|
285
|
+
* admitted by exact match ONLY — never by prefix;
|
|
286
|
+
* 3. otherwise a listed entry that is a PREFIX of the path admits it
|
|
287
|
+
* (`["nodes","0","instructions"]` under `["nodes"]`), so the optimizer
|
|
288
|
+
* can make fine-grained updates without listing every sub-path.
|
|
289
|
+
*
|
|
290
|
+
* Exported so every consumer shares one matcher — a hand-rolled prefix copy
|
|
291
|
+
* would neither honour the wildcard nor close the structural leak.
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* 0.6.0 §6.1 (PR 12) — the leaves the whitelist admits BY PREFIX but the
|
|
295
|
+
* optimizer must never move, matched as a contiguous sub-sequence anywhere in
|
|
296
|
+
* a path (the {@link HUMAN_OWNED_PATHS} matcher) and ENFORCED by
|
|
297
|
+
* {@link isOptimizable}.
|
|
298
|
+
*
|
|
299
|
+
* There is exactly one row, and it exists because `model_pool.learning` is
|
|
300
|
+
* deliberately whitelisted WHOLESALE: `crewhaus advise` mines the reward
|
|
301
|
+
* scoreboard into that block and patches it AS a block, so a narrower
|
|
302
|
+
* enumeration would break the advisor. PR 19 shipped the verdict table with
|
|
303
|
+
* the `learning.seed` exclusion documented as an accepted gap "until the eval
|
|
304
|
+
* runner (PR 12) needs it"; it needs it now — a routed eval PINS
|
|
305
|
+
* `learning.seed`, so an optimizer free to patch the seed could produce a
|
|
306
|
+
* measured delta of guaranteed zero, the one mutation that always "wins"
|
|
307
|
+
* because it changes nothing the measurement can see.
|
|
308
|
+
*
|
|
309
|
+
* This is an enforcement narrower than the whitelist, NOT a second
|
|
310
|
+
* classification: the `learning` BLOCK stays legitimately optimizable (which
|
|
311
|
+
* is what §10.3's verdict table and its closing-invariant test record), and
|
|
312
|
+
* `validatePatch` additionally refuses a block patch that would CHANGE the
|
|
313
|
+
* seed — a block patch that preserves it, as the advisor's spread does,
|
|
314
|
+
* passes.
|
|
315
|
+
*/
|
|
316
|
+
export declare const OPTIMIZER_REFUSED_LEAVES: ReadonlyArray<ReadonlyArray<string>>;
|
|
317
|
+
export declare function isOptimizable(target: Spec["target"], path: ReadonlyArray<SpecEditPathSegment>): boolean;
|
|
172
318
|
/**
|
|
173
319
|
* Format a YAML header comment to prepend to a written-back file. The
|
|
174
320
|
* orchestrator's `--write-back` writes this above the original spec so
|
package/dist/index.js
CHANGED
|
@@ -148,6 +148,52 @@ export function validatePatch(spec, patch) {
|
|
|
148
148
|
if (!isOptimizable(spec.target, patch.path)) {
|
|
149
149
|
throw new SpecPatchError(`path ${formatPath(patch.path)} is not listed in OPTIMIZABLE_PATHS for target "${spec.target}"; add it to packages/spec-patch/src/index.ts if it's intended to be tunable`);
|
|
150
150
|
}
|
|
151
|
+
// 0.6.0 §6.1 — the block-level way around `OPTIMIZER_REFUSED_LEAVES`:
|
|
152
|
+
// `model_pool.learning` is whitelisted WHOLESALE (the advisor patches the
|
|
153
|
+
// block), so a patch at the block carrying a `seed` key would move a leaf
|
|
154
|
+
// the path check just refused. A routed eval pins that seed; a patch to it
|
|
155
|
+
// would produce a measured delta of guaranteed zero.
|
|
156
|
+
const moved = refusedBlockValueKey(spec, patch.path, patch.value);
|
|
157
|
+
if (moved !== undefined) {
|
|
158
|
+
throw new SpecPatchError(`patch at ${formatPath(patch.path)} would change "${moved}", which is not optimizable: a routed eval PINS model_pool.learning.seed, so a patch to it measures a guaranteed-zero delta. Re-issue the block patch carrying the spec's existing seed (a block patch that PRESERVES it is fine).`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The refused LEAF a block-level patch's value would CHANGE, when the patch
|
|
163
|
+
* lands on the parent block of an {@link OPTIMIZER_REFUSED_LEAVES} row.
|
|
164
|
+
*
|
|
165
|
+
* Deliberately "change", not "carry": `crewhaus advise`'s pool rules propose a
|
|
166
|
+
* whole-`learning`-block replace built by SPREADING the spec's current block,
|
|
167
|
+
* so a preserved seed must pass. Only adding, removing or altering the value
|
|
168
|
+
* is refused — the residual the path-level check cannot see.
|
|
169
|
+
*/
|
|
170
|
+
function refusedBlockValueKey(spec, path, value) {
|
|
171
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
172
|
+
return undefined;
|
|
173
|
+
const segs = path.map((s) => String(s));
|
|
174
|
+
for (const pattern of OPTIMIZER_REFUSED_LEAVES) {
|
|
175
|
+
const parent = pattern.slice(0, -1);
|
|
176
|
+
const leaf = pattern[pattern.length - 1];
|
|
177
|
+
const endsWithParent = segs.length >= parent.length &&
|
|
178
|
+
parent.every((p, i) => segs[segs.length - parent.length + i] === p);
|
|
179
|
+
if (!endsWithParent)
|
|
180
|
+
continue;
|
|
181
|
+
const proposed = value[leaf];
|
|
182
|
+
const current = readSpecPath(spec, [...path, leaf]);
|
|
183
|
+
if (proposed !== current)
|
|
184
|
+
return `${segs.join(".")}.${leaf}`;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
/** Read a plain path out of a parsed spec. `undefined` for any missing segment. */
|
|
189
|
+
function readSpecPath(spec, path) {
|
|
190
|
+
let cur = spec;
|
|
191
|
+
for (const seg of path) {
|
|
192
|
+
if (cur === null || typeof cur !== "object")
|
|
193
|
+
return undefined;
|
|
194
|
+
cur = cur[String(seg)];
|
|
195
|
+
}
|
|
196
|
+
return cur;
|
|
151
197
|
}
|
|
152
198
|
function formatPath(path) {
|
|
153
199
|
return path.join(".");
|
|
@@ -281,6 +327,135 @@ function formatEditPath(path) {
|
|
|
281
327
|
}
|
|
282
328
|
return out;
|
|
283
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* 0.6.0 (plan §10.3, mechanism 1) — the WILDCARD segment. Inside an
|
|
332
|
+
* `OPTIMIZABLE_PATHS` entry, `"*"` matches EXACTLY ONE key segment of the
|
|
333
|
+
* patched path (a `models:` profile name, a `steps` index, a `nodes` / `roles`
|
|
334
|
+
* name, a `rules` index) — never zero segments, never more than one, never a
|
|
335
|
+
* literal `"*"` key. Two placement rules make the wildcard safe, pinned by
|
|
336
|
+
* {@link wildcardPlacementIssues} (a CI guard, not a load-time throw):
|
|
337
|
+
*
|
|
338
|
+
* 1. a wildcard is never the LAST segment — `["models","*"]` would admit a
|
|
339
|
+
* whole profile (its `model`, `tools`, `permissions`, …) by prefix;
|
|
340
|
+
* 2. a wildcard never sits ABOVE a human-owned key — the concrete tail after
|
|
341
|
+
* the last `"*"` must end in a dial, never in one of
|
|
342
|
+
* {@link WILDCARD_FORBIDDEN_LEAVES} (the identity / security / prompt keys
|
|
343
|
+
* the §10.3 table excludes) and never in a structural segment.
|
|
344
|
+
*/
|
|
345
|
+
export const WILDCARD_SEGMENT = "*";
|
|
346
|
+
/**
|
|
347
|
+
* Keys a wildcard-bearing entry may never terminate in: the §10.3 exclusions
|
|
348
|
+
* that live UNDER a dynamic key (`models.<name>`, `candidates[i]`,
|
|
349
|
+
* `rules[i]`, `sub_agents.<name>`). A dial entry that ended in one of these
|
|
350
|
+
* would whitelist identity or security surface across every profile at once.
|
|
351
|
+
*/
|
|
352
|
+
export const WILDCARD_FORBIDDEN_LEAVES = Object.freeze([
|
|
353
|
+
"model",
|
|
354
|
+
"models",
|
|
355
|
+
"tags",
|
|
356
|
+
"tools",
|
|
357
|
+
"tool_config",
|
|
358
|
+
"permissions",
|
|
359
|
+
"rate_limits",
|
|
360
|
+
"cost",
|
|
361
|
+
"requires",
|
|
362
|
+
"capabilities",
|
|
363
|
+
"fallbacks",
|
|
364
|
+
"circuit_breaker",
|
|
365
|
+
"instructions",
|
|
366
|
+
"caching",
|
|
367
|
+
"candidates",
|
|
368
|
+
"when",
|
|
369
|
+
"use",
|
|
370
|
+
"labels",
|
|
371
|
+
"reward",
|
|
372
|
+
"directives",
|
|
373
|
+
"model_directed",
|
|
374
|
+
"allowed_profiles",
|
|
375
|
+
"inherit_routing",
|
|
376
|
+
"budget_share",
|
|
377
|
+
"tool_flags",
|
|
378
|
+
"escalate_to",
|
|
379
|
+
"judges",
|
|
380
|
+
"criteria",
|
|
381
|
+
"on_fail",
|
|
382
|
+
"seed",
|
|
383
|
+
]);
|
|
384
|
+
/**
|
|
385
|
+
* 0.6.0 §10.3 — the per-profile DIALS: quality/cost knobs with no identity,
|
|
386
|
+
* security or routing meaning, tunable on every `models:` profile of every
|
|
387
|
+
* shape (`models:` is attached to all 14 schemas). `thinking.budget_tokens`
|
|
388
|
+
* follows the standing rule for the agent block — the budget is a dial, the
|
|
389
|
+
* thinking FORM (`effort`) stays human-owned. Every other profile field
|
|
390
|
+
* (`model`, `tags`, `tools`, `tool_config`, `permissions`, `rate_limits`,
|
|
391
|
+
* `cost`, `requires`, `capabilities`, `fallbacks`, `circuit_breaker`,
|
|
392
|
+
* `instructions`, `caching`) is EXCLUDED — see {@link HUMAN_OWNED_PATHS}.
|
|
393
|
+
*/
|
|
394
|
+
const MODEL_PROFILE_DIALS = Object.freeze([
|
|
395
|
+
Object.freeze(["models", WILDCARD_SEGMENT, "max_tokens"]),
|
|
396
|
+
Object.freeze(["models", WILDCARD_SEGMENT, "thinking", "budget_tokens"]),
|
|
397
|
+
Object.freeze(["models", WILDCARD_SEGMENT, "temperature"]),
|
|
398
|
+
Object.freeze(["models", WILDCARD_SEGMENT, "limits", "model_call_timeout_ms"]),
|
|
399
|
+
]);
|
|
400
|
+
/**
|
|
401
|
+
* 0.6.0 §10.3 — the `model_pool` entries for one routed block (`agent`, a
|
|
402
|
+
* `steps[*]` step, a `nodes.*` node, a `roles.*` role). Because the
|
|
403
|
+
* structural rule ({@link STRUCTURAL_SEGMENTS}) admits a `model_pool` path
|
|
404
|
+
* ONLY by exact match, every tunable pool key needs its own entry — nothing
|
|
405
|
+
* under `model_pool` is reached by prefix any more, on any shape:
|
|
406
|
+
*
|
|
407
|
+
* - `policy` / `routing` / `learning` — the pre-0.6.0 policy knobs
|
|
408
|
+
* (`routing` and `learning` deliberately WHOLESALE: `advise` mines the
|
|
409
|
+
* scoreboard into them). On the positional shapes these were reached by
|
|
410
|
+
* the whole-block `["steps"]` / `["nodes"]` / `["roles"]` prefix until the
|
|
411
|
+
* structural rule closed prefix reach into `model_pool`; the exact
|
|
412
|
+
* wildcard entries restore exactly the G37 intent and nothing more.
|
|
413
|
+
* - `rules[*].enabled` — a switch, not a target (`rules[*].when` / `.use`
|
|
414
|
+
* / `.id` stay human-owned).
|
|
415
|
+
* - `strategy.shadow.sample_rate`, `strategy.guide.max_tokens`,
|
|
416
|
+
* `strategy.max_escalations`, `classifier.max_tokens` — pure cost dials.
|
|
417
|
+
* - `strategy.cascade.clean_prompt` — a prompt-shape dial (draft kept or
|
|
418
|
+
* dropped before the escalation re-run).
|
|
419
|
+
*
|
|
420
|
+
* `candidates` (incl. `enabled`), `rules[*].{when,use,id}`,
|
|
421
|
+
* `classifier.{model,labels}`, every `strategy.*` role / model slot,
|
|
422
|
+
* `strategy.model_directed`, `reward.*`, `directives`, `scope` and
|
|
423
|
+
* `objective` are EXCLUDED — see {@link HUMAN_OWNED_PATHS}.
|
|
424
|
+
*/
|
|
425
|
+
function poolDials(block) {
|
|
426
|
+
const pool = [...block, "model_pool"];
|
|
427
|
+
return Object.freeze([
|
|
428
|
+
Object.freeze([...pool, "policy"]),
|
|
429
|
+
Object.freeze([...pool, "routing"]),
|
|
430
|
+
Object.freeze([...pool, "learning"]),
|
|
431
|
+
Object.freeze([...pool, "rules", WILDCARD_SEGMENT, "enabled"]),
|
|
432
|
+
Object.freeze([...pool, "strategy", "shadow", "sample_rate"]),
|
|
433
|
+
Object.freeze([...pool, "strategy", "guide", "max_tokens"]),
|
|
434
|
+
Object.freeze([...pool, "strategy", "max_escalations"]),
|
|
435
|
+
Object.freeze([...pool, "strategy", "cascade", "clean_prompt"]),
|
|
436
|
+
Object.freeze([...pool, "classifier", "max_tokens"]),
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 0.6.0 §10.3 — a `kind: judge` gate's dials on a positional block (`steps[*]`
|
|
441
|
+
* / `nodes.*`): `threshold` and `max_retries` mirror the agent-shape
|
|
442
|
+
* `evaluation.threshold` / `evaluation.max_retries` entries (the structural
|
|
443
|
+
* `judge` segment closed their prefix reach; these restore it exactly), and
|
|
444
|
+
* `repeats` mirrors `evaluation.grader.repeats`. `criteria`, `model`,
|
|
445
|
+
* `judges`, `temperature`, `target`, `on_fail` and `escalate_to` are EXCLUDED.
|
|
446
|
+
*/
|
|
447
|
+
function judgeGateDials(block) {
|
|
448
|
+
const judge = [...block, "judge"];
|
|
449
|
+
return Object.freeze([
|
|
450
|
+
Object.freeze([...judge, "threshold"]),
|
|
451
|
+
Object.freeze([...judge, "max_retries"]),
|
|
452
|
+
Object.freeze([...judge, "repeats"]),
|
|
453
|
+
]);
|
|
454
|
+
}
|
|
455
|
+
const STEP = Object.freeze(["steps", WILDCARD_SEGMENT]);
|
|
456
|
+
const NODE = Object.freeze(["nodes", WILDCARD_SEGMENT]);
|
|
457
|
+
const ROLE = Object.freeze(["roles", WILDCARD_SEGMENT]);
|
|
458
|
+
const AGENT = Object.freeze(["agent"]);
|
|
284
459
|
/**
|
|
285
460
|
* Per-target whitelist of mutation paths the active optimizer is
|
|
286
461
|
* allowed to touch. Adding a new field here is the explicit signal that
|
|
@@ -351,9 +526,21 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
351
526
|
// ["agent","model_pool","candidates"]: the candidate ROSTER is
|
|
352
527
|
// human-owned, mirroring the standing ["agent","model"] exclusion —
|
|
353
528
|
// learning tunes selection within the declared set, never the set.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
529
|
+
// 0.6.0 §10.3 — the exact pool entries (policy/routing/learning plus the
|
|
530
|
+
// hybrid dials); see `poolDials`. `["agent","model_pool","routing"]` and
|
|
531
|
+
// `["agent","model_pool","learning"]` stay WHOLESALE — a narrower
|
|
532
|
+
// enumeration would break `advise`'s scoreboard mining, which patches the
|
|
533
|
+
// `learning` BLOCK. §6.1's `learning.seed` exclusion is therefore enforced
|
|
534
|
+
// beside this table rather than inside it: see OPTIMIZER_REFUSED_LEAVES
|
|
535
|
+
// (PR 12 closed the gap PR 19 documented).
|
|
536
|
+
...poolDials(AGENT),
|
|
537
|
+
// 0.6.0 §10.3 — `agent.temperature`: a threshold-shaped leaf like the
|
|
538
|
+
// existing params (exclusive with `thinking` on one block — the spec
|
|
539
|
+
// refine rejects a patch that lands both, so the optimizer can move one
|
|
540
|
+
// dial only when the other is absent).
|
|
541
|
+
Object.freeze(["agent", "temperature"]),
|
|
542
|
+
// 0.6.0 §10.3 — per-profile dials on every `models:` profile.
|
|
543
|
+
...MODEL_PROFILE_DIALS,
|
|
357
544
|
// 0.3.0 memory release (design §7.5, PR 20) — the memory/continuity
|
|
358
545
|
// QUALITY knobs, registered on the five emit-wired memory shapes (cli,
|
|
359
546
|
// channel, managed, research, crew — mirrored below with a pointer to
|
|
@@ -396,6 +583,10 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
396
583
|
// the optimizer tunes how strict the gate is, never what it judges.
|
|
397
584
|
Object.freeze(["evaluation", "threshold"]),
|
|
398
585
|
Object.freeze(["evaluation", "max_retries"]),
|
|
586
|
+
// 0.6.0 §10.3 — `evaluation.grader.repeats`: a measurement dial (verdicts
|
|
587
|
+
// per judge, folded by median). `judges`, `temperature`, `target`,
|
|
588
|
+
// `criteria`, `model` and `on_fail` / `allow_self_judge` stay human-owned.
|
|
589
|
+
Object.freeze(["evaluation", "grader", "repeats"]),
|
|
399
590
|
// Loop contract 0.4 (Batch E) — the agent-shape RAG (`knowledge:`) dials
|
|
400
591
|
// + the per-turn recall cadence. All scalars that survive lower() 1:1
|
|
401
592
|
// with their bounds owned by the spec schema, so an out-of-bounds patch
|
|
@@ -411,12 +602,21 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
411
602
|
Object.freeze(["memory", "refreshEvery"]),
|
|
412
603
|
]),
|
|
413
604
|
workflow: Object.freeze([
|
|
414
|
-
// Whole-step replacement —
|
|
415
|
-
// model routing (
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
605
|
+
// Whole-step replacement — a whole-step VALUE may carry item 9's (G37)
|
|
606
|
+
// per-step model routing (a pool-bearing step round-trips, see the G37
|
|
607
|
+
// tests) because the path `["steps"]` crosses no structural segment.
|
|
608
|
+
// The sub-path route is narrower: the structural rule stops prefix reach
|
|
609
|
+
// at `model_pool` / `judge` / `temperature` / `model_tiers` /
|
|
610
|
+
// `model_fallbacks` / `circuit_breaker`, so `steps[i].model_tiers.fast`
|
|
611
|
+
// is refused and only the exact dials below are admitted under a step.
|
|
419
612
|
Object.freeze(["steps"]),
|
|
613
|
+
// 0.6.0 §10.3 — the structural rule closed prefix reach into a step's
|
|
614
|
+
// `model_pool` / `judge` / `temperature`; these exact wildcard entries
|
|
615
|
+
// (`*` = one step index) are the dials the table promises per step.
|
|
616
|
+
...poolDials(STEP),
|
|
617
|
+
...judgeGateDials(STEP),
|
|
618
|
+
Object.freeze([...STEP, "temperature"]),
|
|
619
|
+
...MODEL_PROFILE_DIALS,
|
|
420
620
|
Object.freeze(["failure_taxonomy"]),
|
|
421
621
|
Object.freeze(["chains"]),
|
|
422
622
|
Object.freeze(["transaction_policy"]),
|
|
@@ -431,10 +631,10 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
431
631
|
Object.freeze(["failure_taxonomy"]),
|
|
432
632
|
Object.freeze(["chains"]),
|
|
433
633
|
Object.freeze(["transaction_policy"]),
|
|
434
|
-
// Adaptive model routing
|
|
435
|
-
|
|
436
|
-
Object.freeze(["agent", "
|
|
437
|
-
|
|
634
|
+
// Adaptive model routing + 0.6.0 hybrid dials — see the cli entry.
|
|
635
|
+
...poolDials(AGENT),
|
|
636
|
+
Object.freeze(["agent", "temperature"]),
|
|
637
|
+
...MODEL_PROFILE_DIALS,
|
|
438
638
|
// 0.3.0 memory/continuity quality knobs — types/bounds at the cli entry.
|
|
439
639
|
Object.freeze(["memory", "recallK"]),
|
|
440
640
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
@@ -445,6 +645,7 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
445
645
|
// Loop contract 0.4 (Batch B, G40) — evaluation dials; see the cli entry.
|
|
446
646
|
Object.freeze(["evaluation", "threshold"]),
|
|
447
647
|
Object.freeze(["evaluation", "max_retries"]),
|
|
648
|
+
Object.freeze(["evaluation", "grader", "repeats"]),
|
|
448
649
|
// Loop contract 0.4 (Batch E) — knowledge/recall dials; see the cli entry.
|
|
449
650
|
Object.freeze(["knowledge", "default_k"]),
|
|
450
651
|
Object.freeze(["knowledge", "chunk", "size"]),
|
|
@@ -453,6 +654,11 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
453
654
|
]),
|
|
454
655
|
graph: Object.freeze([
|
|
455
656
|
Object.freeze(["nodes"]),
|
|
657
|
+
// 0.6.0 §10.3 — per-node dials (`*` = one node name); see the workflow entry.
|
|
658
|
+
...poolDials(NODE),
|
|
659
|
+
...judgeGateDials(NODE),
|
|
660
|
+
Object.freeze([...NODE, "temperature"]),
|
|
661
|
+
...MODEL_PROFILE_DIALS,
|
|
456
662
|
Object.freeze(["failure_taxonomy"]),
|
|
457
663
|
Object.freeze(["chains"]),
|
|
458
664
|
Object.freeze(["transaction_policy"]),
|
|
@@ -465,10 +671,10 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
465
671
|
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
466
672
|
Object.freeze(["limits", "max_tool_iterations"]),
|
|
467
673
|
Object.freeze(["failure_taxonomy"]),
|
|
468
|
-
// Adaptive model routing
|
|
469
|
-
|
|
470
|
-
Object.freeze(["agent", "
|
|
471
|
-
|
|
674
|
+
// Adaptive model routing + 0.6.0 hybrid dials — see the cli entry.
|
|
675
|
+
...poolDials(AGENT),
|
|
676
|
+
Object.freeze(["agent", "temperature"]),
|
|
677
|
+
...MODEL_PROFILE_DIALS,
|
|
472
678
|
// 0.3.0 memory/continuity quality knobs — types/bounds at the cli entry.
|
|
473
679
|
Object.freeze(["memory", "recallK"]),
|
|
474
680
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
@@ -479,6 +685,7 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
479
685
|
// Loop contract 0.4 (Batch B, G40) — evaluation dials; see the cli entry.
|
|
480
686
|
Object.freeze(["evaluation", "threshold"]),
|
|
481
687
|
Object.freeze(["evaluation", "max_retries"]),
|
|
688
|
+
Object.freeze(["evaluation", "grader", "repeats"]),
|
|
482
689
|
// Loop contract 0.4 (Batch E) — knowledge/recall dials; see the cli entry.
|
|
483
690
|
Object.freeze(["knowledge", "default_k"]),
|
|
484
691
|
Object.freeze(["knowledge", "chunk", "size"]),
|
|
@@ -487,18 +694,29 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
487
694
|
]),
|
|
488
695
|
pipeline: Object.freeze([
|
|
489
696
|
Object.freeze(["agent", "instructions"]),
|
|
697
|
+
// 0.6.0 §10.3 — the pipeline agent carries a `model_pool` and a
|
|
698
|
+
// `temperature` (params/overlay only per §11.3): the same policy knobs
|
|
699
|
+
// and dials as the cli agent.
|
|
700
|
+
...poolDials(AGENT),
|
|
701
|
+
Object.freeze(["agent", "temperature"]),
|
|
702
|
+
...MODEL_PROFILE_DIALS,
|
|
490
703
|
Object.freeze(["failure_taxonomy"]),
|
|
491
704
|
Object.freeze(["indexing", "chunkSize"]),
|
|
492
705
|
Object.freeze(["indexing", "chunkOverlap"]),
|
|
493
706
|
Object.freeze(["retrieve", "defaultK"]),
|
|
494
707
|
]),
|
|
495
708
|
crew: Object.freeze([
|
|
496
|
-
// Whole-role replacement —
|
|
497
|
-
// model routing (
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
709
|
+
// Whole-role replacement — a whole-role VALUE may carry item 9's (G37)
|
|
710
|
+
// per-role model routing (the role name is a dynamic map key; see the
|
|
711
|
+
// G37 tests). The sub-path route is narrower, as at the workflow entry:
|
|
712
|
+
// `roles.<r>.model_tiers` / `model_fallbacks` / `circuit_breaker` /
|
|
713
|
+
// `model_pool.*` are refused by the structural rule; only the exact
|
|
714
|
+
// dials below are admitted under a role.
|
|
501
715
|
Object.freeze(["roles"]),
|
|
716
|
+
// 0.6.0 §10.3 — per-role dials (`*` = one role name); see the workflow entry.
|
|
717
|
+
...poolDials(ROLE),
|
|
718
|
+
Object.freeze([...ROLE, "temperature"]),
|
|
719
|
+
...MODEL_PROFILE_DIALS,
|
|
502
720
|
Object.freeze(["failure_taxonomy"]),
|
|
503
721
|
Object.freeze(["chains"]),
|
|
504
722
|
Object.freeze(["transaction_policy"]),
|
|
@@ -516,6 +734,11 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
516
734
|
]) /* whole-role replacement */,
|
|
517
735
|
research: Object.freeze([
|
|
518
736
|
Object.freeze(["agent", "instructions"]),
|
|
737
|
+
// 0.6.0 §10.3 — the pooled single-agent shapes (research, batch, browser)
|
|
738
|
+
// carry `agent.model_pool` + `agent.temperature`: same dials as cli.
|
|
739
|
+
...poolDials(AGENT),
|
|
740
|
+
Object.freeze(["agent", "temperature"]),
|
|
741
|
+
...MODEL_PROFILE_DIALS,
|
|
519
742
|
Object.freeze(["failure_taxonomy"]),
|
|
520
743
|
// NOTE ["retrieve","maxDepth"] was REMOVED here (Batch A): the research
|
|
521
744
|
// retrieve block never grew a maxDepth field (strict schema rejects
|
|
@@ -537,6 +760,10 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
537
760
|
]),
|
|
538
761
|
batch: Object.freeze([
|
|
539
762
|
Object.freeze(["agent", "instructions"]),
|
|
763
|
+
// 0.6.0 §10.3 — see the research entry.
|
|
764
|
+
...poolDials(AGENT),
|
|
765
|
+
Object.freeze(["agent", "temperature"]),
|
|
766
|
+
...MODEL_PROFILE_DIALS,
|
|
540
767
|
Object.freeze(["failure_taxonomy"]),
|
|
541
768
|
Object.freeze(["chains"]),
|
|
542
769
|
Object.freeze(["transaction_policy"]),
|
|
@@ -545,21 +772,32 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
545
772
|
]),
|
|
546
773
|
voice: Object.freeze([
|
|
547
774
|
Object.freeze(["agent", "instructions"]),
|
|
775
|
+
// 0.6.0 §10.3 — `models:` is attached to every shape; the profile dials
|
|
776
|
+
// are the only 0.6.0 entries here (voice has no pool and no temperature).
|
|
777
|
+
...MODEL_PROFILE_DIALS,
|
|
548
778
|
Object.freeze(["failure_taxonomy"]),
|
|
549
779
|
]),
|
|
550
780
|
browser: Object.freeze([
|
|
551
781
|
Object.freeze(["agent", "instructions"]),
|
|
782
|
+
// 0.6.0 §10.3 — see the research entry.
|
|
783
|
+
...poolDials(AGENT),
|
|
784
|
+
Object.freeze(["agent", "temperature"]),
|
|
785
|
+
...MODEL_PROFILE_DIALS,
|
|
552
786
|
Object.freeze(["failure_taxonomy"]),
|
|
553
787
|
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
554
788
|
Object.freeze(["limits", "max_tool_iterations"]),
|
|
555
789
|
]),
|
|
556
790
|
eval: Object.freeze([
|
|
557
791
|
Object.freeze(["agent", "instructions"]),
|
|
792
|
+
// 0.6.0 §10.3 — profile dials only (see the voice entry).
|
|
793
|
+
...MODEL_PROFILE_DIALS,
|
|
558
794
|
Object.freeze(["failure_taxonomy"]),
|
|
559
795
|
]),
|
|
560
796
|
// §47 onchain daemon: full cross-cutting blocks are optimizable.
|
|
561
797
|
onchain: Object.freeze([
|
|
562
798
|
Object.freeze(["agent", "instructions"]),
|
|
799
|
+
// 0.6.0 §10.3 — profile dials only (see the voice entry).
|
|
800
|
+
...MODEL_PROFILE_DIALS,
|
|
563
801
|
Object.freeze(["failure_taxonomy"]),
|
|
564
802
|
Object.freeze(["chains"]),
|
|
565
803
|
Object.freeze(["triggers"]),
|
|
@@ -570,41 +808,438 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
570
808
|
// the productive knobs; move-timeout-ms is the realtime quality knob.
|
|
571
809
|
"onchain-game": Object.freeze([
|
|
572
810
|
Object.freeze(["agent", "instructions"]),
|
|
811
|
+
// 0.6.0 §10.3 — profile dials only (see the voice entry).
|
|
812
|
+
...MODEL_PROFILE_DIALS,
|
|
573
813
|
Object.freeze(["failure_taxonomy"]),
|
|
574
814
|
Object.freeze(["game"]),
|
|
575
815
|
Object.freeze(["transaction_policy"]),
|
|
576
816
|
]),
|
|
577
817
|
});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
818
|
+
/**
|
|
819
|
+
* 0.6.0 (plan §10.3, mechanism 2) — the STRUCTURAL rule: a path that passes
|
|
820
|
+
* through any of these segments is optimizable only by EXACT match against a
|
|
821
|
+
* listed entry, never by prefix. The whole-block entries `["steps"]`,
|
|
822
|
+
* `["nodes"]` and `["roles"]` exist because the step index / node name / role
|
|
823
|
+
* name is positional, and whole-block replacement was meant to span
|
|
824
|
+
* `model`/`instructions` — not the human-owned structure that now hangs off
|
|
825
|
+
* every routed block:
|
|
826
|
+
*
|
|
827
|
+
* - `model_pool` — the candidate ROSTER (mirroring the standing
|
|
828
|
+
* `["agent","model"]` exclusion), and from 0.6.0 the hybrid container:
|
|
829
|
+
* `rules.*.use` (rule targets), `strategy.*` role slots and
|
|
830
|
+
* `model_directed` (registers the `Escalate`/`Consult` tool surface),
|
|
831
|
+
* `reward.*` (the floor), `directives` (admits user-directed input),
|
|
832
|
+
* `classifier.{model,labels}` (judge identity), `scope`. The plan's
|
|
833
|
+
* verdict table classifies every one of these as excluded.
|
|
834
|
+
* - `judge` — a `kind: judge` gate's `escalate_to` (a routing target),
|
|
835
|
+
* `judges` (judge identity), `temperature`/`target`.
|
|
836
|
+
* - `sub_agents` — a sub-agent definition is a tool-surface / identity block
|
|
837
|
+
* (`tools`, `permissions`, `model`, `inherit_bypass`, and from 0.6.0
|
|
838
|
+
* `allowed_profiles`, `budget_share`, `inherit_routing`, its own routing).
|
|
839
|
+
* - `temperature` — a quality dial the plan lists as optimizable, but ONLY
|
|
840
|
+
* via the exact entries (`["agent","temperature"]`, the per-step / node /
|
|
841
|
+
* role wildcard entries, `models.*.temperature`): every OTHER
|
|
842
|
+
* `temperature` leaf — a candidate's, a sub-agent's, a judge's pinned
|
|
843
|
+
* sampling temperature — is human-owned, and a prefix-reached patch
|
|
844
|
+
* would land on one of those.
|
|
845
|
+
* - `model_tiers`, `model_fallbacks`, `circuit_breaker` — the three
|
|
846
|
+
* pre-pool routing blocks (§11.1 adds them to graph nodes and sub-agents
|
|
847
|
+
* in 0.6.0; steps and roles carried them before). `model_tiers` is a
|
|
848
|
+
* two-slot roster (`fast`/`default` are model identity, mirroring the
|
|
849
|
+
* standing `agent.model` exclusion), `model_fallbacks` is the failover
|
|
850
|
+
* chain and `circuit_breaker` decides when an arm is withdrawn from
|
|
851
|
+
* service — the same verdicts the §10.3 table gives their `models.*`
|
|
852
|
+
* twins. No exact entry names any of them, so the rule closes the
|
|
853
|
+
* whole-block prefix route (`["nodes"]` → `nodes.a.circuit_breaker`)
|
|
854
|
+
* without removing anything the table promises.
|
|
855
|
+
*
|
|
856
|
+
* One rule, no negative-list semantics: the exact entries
|
|
857
|
+
* `["agent","model_pool",{policy,routing,learning}]` keep working, and a
|
|
858
|
+
* whole-block `["steps"]` replacement (whose PATH carries none of these
|
|
859
|
+
* segments) is unchanged. The wildcard segment ({@link WILDCARD_SEGMENT}) and
|
|
860
|
+
* the exact entries the §10.3 table promises (`poolDials`, `judgeGateDials`,
|
|
861
|
+
* `MODEL_PROFILE_DIALS`) ride on top of this rule — it is what makes that
|
|
862
|
+
* table true for the positional shapes. Note the reconciliation the exact
|
|
863
|
+
* match performs: `["agent","temperature"]` IS listed, so the structural
|
|
864
|
+
* `temperature` segment blocks only the prefix route, never the entry.
|
|
865
|
+
*/
|
|
866
|
+
export const STRUCTURAL_SEGMENTS = Object.freeze([
|
|
867
|
+
"model_pool",
|
|
868
|
+
"judge",
|
|
869
|
+
"sub_agents",
|
|
870
|
+
"temperature",
|
|
871
|
+
"model_tiers",
|
|
872
|
+
"model_fallbacks",
|
|
873
|
+
"circuit_breaker",
|
|
874
|
+
]);
|
|
875
|
+
/**
|
|
876
|
+
* 0.6.0 §10.3 — the EXCLUDED half of the verdict table, stated in code rather
|
|
877
|
+
* than left to the whitelist's default-deny (the 0.3.0 discipline). Each row
|
|
878
|
+
* is a segment pattern matched as a CONTIGUOUS sub-sequence ANYWHERE in a leaf
|
|
879
|
+
* path (`"*"` = any one segment), so one row covers the same key on every
|
|
880
|
+
* host: `["model_pool","rules","*","use"]` excludes `agent.model_pool.rules[i].use`
|
|
881
|
+
* on cli and `steps[i].model_pool.rules[j].use` on workflow alike. A row that
|
|
882
|
+
* names a block (`["model_pool","candidates"]`) excludes every leaf under it.
|
|
883
|
+
*
|
|
884
|
+
* This table is NOT consulted by {@link isOptimizable} — the whitelist is the
|
|
885
|
+
* only admission rule, and anything it does not list is refused. The rows
|
|
886
|
+
* exist so the table's closing invariant can be TESTED: every leaf the
|
|
887
|
+
* 0.6.0 spec delta introduced (enumerated from the zod schema) must be
|
|
888
|
+
* classified exactly once — matched by the whitelist XOR by a row here — and
|
|
889
|
+
* the test fails on a leaf that is unclassified or claimed twice, so this
|
|
890
|
+
* table, the whitelist and the schema cannot drift apart. The Hangar spec
|
|
891
|
+
* editor and `crewhaus advise` read the reasons.
|
|
892
|
+
*/
|
|
893
|
+
export const HUMAN_OWNED_PATHS = Object.freeze([
|
|
894
|
+
// -- the roster (already excluded today; the 0.6.0 per-candidate fields
|
|
895
|
+
// ride inside it, including `enabled`) --
|
|
896
|
+
{
|
|
897
|
+
pattern: ["agent", "model"],
|
|
898
|
+
reason: "the model roster is human-owned (the standing agent.model exclusion)",
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
pattern: ["model_pool", "candidates"],
|
|
902
|
+
reason: "the candidate roster — every inline profile field and `enabled` included — is human-owned; learning tunes selection within the declared set, never the set",
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
pattern: ["model_pool", "objective"],
|
|
906
|
+
reason: "the reward weights decide the quality/cost/latency trade-off the learner optimises — a spend policy, not a dial (sibling of reward)",
|
|
907
|
+
},
|
|
908
|
+
// -- the pre-pool routing blocks on every routed host (agent, steps[i],
|
|
909
|
+
// nodes.<n>, roles.<r>, sub_agents.<n>): roster, failover, withdrawal --
|
|
910
|
+
{
|
|
911
|
+
pattern: ["model_tiers"],
|
|
912
|
+
reason: "the two-tier router is a two-slot roster (fast/default are model identity) plus its thresholds — human-owned like agent.model; the pool's exact routing entry is the tunable generalisation",
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
pattern: ["model_fallbacks"],
|
|
916
|
+
reason: "the failover chain decides which model answers when the primary is down",
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
pattern: ["circuit_breaker"],
|
|
920
|
+
reason: "the breaker decides when an arm is withdrawn from service",
|
|
921
|
+
},
|
|
922
|
+
// -- models.<name>: identity, security surface, routing semantics, prompt --
|
|
923
|
+
{
|
|
924
|
+
pattern: ["models", "*", "model"],
|
|
925
|
+
reason: "a profile's model is identity — the roster is never auto-patched",
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
pattern: ["models", "*", "tags"],
|
|
929
|
+
reason: "tags decide what `strong` means: the default floor arm and the target of `strongest`",
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
pattern: ["models", "*", "tools"],
|
|
933
|
+
reason: "a profile's toolset is tool surface — security, not quality",
|
|
934
|
+
},
|
|
935
|
+
{ pattern: ["models", "*", "tool_config"], reason: "tool config decides what tools may reach" },
|
|
936
|
+
{
|
|
937
|
+
pattern: ["models", "*", "permissions"],
|
|
938
|
+
reason: "permissions are the security floor; a profile may only narrow them, and only a human decides how",
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
pattern: ["models", "*", "rate_limits"],
|
|
942
|
+
reason: "rate limits are a spend / abuse control, never a quality dial",
|
|
943
|
+
},
|
|
944
|
+
{ pattern: ["models", "*", "cost"], reason: "a per-profile spend cap is human-owned policy" },
|
|
945
|
+
{
|
|
946
|
+
pattern: ["models", "*", "requires"],
|
|
947
|
+
reason: "capability requirements are routing semantics (N1 eligibility)",
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
pattern: ["models", "*", "capabilities"],
|
|
951
|
+
reason: "a declared capability override is a fact about a model, not a dial",
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
pattern: ["models", "*", "fallbacks"],
|
|
955
|
+
reason: "the failover chain decides which provider answers when the first is down",
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
pattern: ["models", "*", "circuit_breaker"],
|
|
959
|
+
reason: "the breaker decides when an arm is withdrawn from service",
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
pattern: ["models", "*", "instructions"],
|
|
963
|
+
reason: "prompt text stays human-owned like evaluation.grader — an accepted inconsistency with agent.instructions, documented rather than widened",
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
pattern: ["models", "*", "caching"],
|
|
967
|
+
reason: "cache posture is a cost/latency contract with the provider, classified explicitly",
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
pattern: ["models", "*", "thinking", "effort"],
|
|
971
|
+
reason: "the thinking FORM stays human-owned (only the explicit budget is a dial — the standing agent.thinking rule)",
|
|
972
|
+
},
|
|
973
|
+
// -- model_pool: rule targets, judge identity, the floor, the two switches
|
|
974
|
+
// that admit model- or user-directed input --
|
|
975
|
+
{ pattern: ["model_pool", "rules", "*", "id"], reason: "a rule id is persisted identity" },
|
|
976
|
+
{
|
|
977
|
+
pattern: ["model_pool", "rules", "*", "when"],
|
|
978
|
+
reason: "a rule's condition decides which turns leave the cheap lane — a routing target, human-owned",
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
pattern: ["model_pool", "rules", "*", "use"],
|
|
982
|
+
reason: "a rule's target names which arm serves — the roster decision, human-owned",
|
|
983
|
+
},
|
|
984
|
+
{ pattern: ["model_pool", "classifier", "model"], reason: "judge identity" },
|
|
985
|
+
{
|
|
986
|
+
pattern: ["model_pool", "classifier", "labels"],
|
|
987
|
+
reason: "the label text is the classifier's prompt and its verdict vocabulary",
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
pattern: ["model_pool", "strategy", "cascade", "draft"],
|
|
991
|
+
reason: "a strategy role slot names a roster member",
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
pattern: ["model_pool", "strategy", "cascade", "escalate_to"],
|
|
995
|
+
reason: "a strategy role slot names a roster member",
|
|
996
|
+
},
|
|
997
|
+
{ pattern: ["model_pool", "strategy", "guide", "model"], reason: "a strategy model slot" },
|
|
998
|
+
{
|
|
999
|
+
pattern: ["model_pool", "strategy", "guide", "every"],
|
|
1000
|
+
reason: "guide cadence decides a transcript-cache rewrite per turn — classified explicitly",
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
pattern: ["model_pool", "strategy", "guide", "budget_usd"],
|
|
1004
|
+
reason: "a spend cap is human-owned policy",
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
pattern: ["model_pool", "strategy", "shadow", "candidate"],
|
|
1008
|
+
reason: "the audition candidate is a roster decision",
|
|
1009
|
+
},
|
|
1010
|
+
{ pattern: ["model_pool", "strategy", "shadow", "grade_with"], reason: "judge identity" },
|
|
1011
|
+
{
|
|
1012
|
+
pattern: ["model_pool", "strategy", "committee"],
|
|
1013
|
+
reason: "committee membership, its judge and its tie-breaker are roster and judge identity",
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
pattern: ["model_pool", "strategy", "model_directed"],
|
|
1017
|
+
reason: "registers the Escalate / Consult tools — a tool-surface decision that admits model-directed input",
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
pattern: ["model_pool", "reward"],
|
|
1021
|
+
reason: "the reward block (quality source, priors, the floor, reset) bounds what the learner may exploit — never tuned by the loop it bounds",
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
pattern: ["model_pool", "directives"],
|
|
1025
|
+
reason: "admits user-directed per-message steering — a trust-boundary switch",
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
pattern: ["model_pool", "scope"],
|
|
1029
|
+
reason: "the scoped routeKey prefix is arm identity (stamped by the compiler)",
|
|
1030
|
+
},
|
|
1031
|
+
// -- evaluation / judge gates: what is judged and by whom --
|
|
1032
|
+
{
|
|
1033
|
+
pattern: ["evaluation", "grader", "type"],
|
|
1034
|
+
reason: "the grader kind is human-owned semantics (the optimizer tunes how strict the gate is, never what it judges)",
|
|
1035
|
+
},
|
|
1036
|
+
{ pattern: ["evaluation", "grader", "criteria"], reason: "prompt text — what the judge judges" },
|
|
1037
|
+
{
|
|
1038
|
+
pattern: ["evaluation", "grader", "value"],
|
|
1039
|
+
reason: "a deterministic grader's match rule is what it judges",
|
|
1040
|
+
},
|
|
1041
|
+
{ pattern: ["evaluation", "grader", "model"], reason: "judge identity" },
|
|
1042
|
+
{ pattern: ["evaluation", "grader", "judges"], reason: "judge identity (the panel)" },
|
|
1043
|
+
{
|
|
1044
|
+
pattern: ["evaluation", "grader", "temperature"],
|
|
1045
|
+
reason: "the judge's pinned sampling temperature is measurement integrity, not a quality dial",
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
pattern: ["evaluation", "grader", "target"],
|
|
1049
|
+
reason: "what the judge grades (output vs trajectory) is measurement semantics",
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
pattern: ["evaluation", "on_fail"],
|
|
1053
|
+
reason: "the below-threshold behaviour (incl. escalate) is human-owned semantics",
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
pattern: ["evaluation", "allow_self_judge"],
|
|
1057
|
+
reason: "a measurement-integrity waiver, never a knob",
|
|
1058
|
+
},
|
|
1059
|
+
{ pattern: ["judge", "criteria"], reason: "prompt text — what the gate judges" },
|
|
1060
|
+
{ pattern: ["judge", "model"], reason: "judge identity" },
|
|
1061
|
+
{ pattern: ["judge", "judges"], reason: "judge identity (the panel)" },
|
|
1062
|
+
{
|
|
1063
|
+
pattern: ["judge", "temperature"],
|
|
1064
|
+
reason: "the judge's pinned sampling temperature is measurement integrity",
|
|
1065
|
+
},
|
|
1066
|
+
{ pattern: ["judge", "target"], reason: "what the gate grades is measurement semantics" },
|
|
1067
|
+
{
|
|
1068
|
+
pattern: ["judge", "on_fail"],
|
|
1069
|
+
reason: "the below-threshold behaviour is human-owned semantics",
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
pattern: ["judge", "escalate_to"],
|
|
1073
|
+
reason: "the re-run's forced arm is a roster decision",
|
|
1074
|
+
},
|
|
1075
|
+
// -- budget: spend policy, whole block --
|
|
1076
|
+
{ pattern: ["budget", "usd"], reason: "the run cap is human-owned spend policy" },
|
|
1077
|
+
{ pattern: ["budget", "judge_share"], reason: "a spend split is human-owned policy" },
|
|
1078
|
+
{
|
|
1079
|
+
pattern: ["budget", "scope"],
|
|
1080
|
+
reason: "run vs session bounds a cap's blast radius — an operator's call",
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
pattern: ["budget", "on_exceed"],
|
|
1084
|
+
reason: "what happens at the cap (stop / degrade to a model) is roster and spend policy",
|
|
1085
|
+
},
|
|
1086
|
+
// -- sub-agents: a tool-surface / identity block, wholesale --
|
|
1087
|
+
{
|
|
1088
|
+
pattern: ["sub_agents"],
|
|
1089
|
+
reason: "a sub-agent definition is tool surface, identity and (0.6.0) its own routing, budget share, inheritance and profile allowlist — human-owned wholesale",
|
|
1090
|
+
},
|
|
1091
|
+
// -- MCP trust flags, crew router model --
|
|
1092
|
+
{
|
|
1093
|
+
pattern: ["tool_flags"],
|
|
1094
|
+
reason: "MCP trust flags narrow a tool's trust posture — security surface",
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
pattern: ["routing", "model"],
|
|
1098
|
+
reason: "the crew llm router's model is a roster decision",
|
|
1099
|
+
},
|
|
1100
|
+
// -- observability.slo: production alerting and mitigation thresholds --
|
|
1101
|
+
{
|
|
1102
|
+
pattern: ["observability", "slo"],
|
|
1103
|
+
reason: "an SLO threshold decides when production is alerted, paused or rolled back — an operator's call, never an eval loop's (the routing rates added in 0.6.0 follow the existing keys)",
|
|
1104
|
+
},
|
|
1105
|
+
]);
|
|
1106
|
+
/** A leaf path is human-owned when some {@link HUMAN_OWNED_PATHS} row matches it. */
|
|
1107
|
+
export function humanOwnedReason(path) {
|
|
1108
|
+
for (const row of HUMAN_OWNED_PATHS) {
|
|
1109
|
+
if (containsPattern(path, row.pattern))
|
|
1110
|
+
return row.reason;
|
|
1111
|
+
}
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
/** One segment of a pattern against one segment of a path (`"*"` matches any one segment). */
|
|
1115
|
+
function segmentMatches(pattern, actual) {
|
|
1116
|
+
return pattern === WILDCARD_SEGMENT || pattern === String(actual);
|
|
1117
|
+
}
|
|
1118
|
+
/** `pattern` appears as a contiguous sub-sequence of `path` (wildcards honoured). */
|
|
1119
|
+
function containsPattern(path, pattern) {
|
|
1120
|
+
if (pattern.length === 0 || pattern.length > path.length)
|
|
1121
|
+
return false;
|
|
1122
|
+
outer: for (let start = 0; start + pattern.length <= path.length; start++) {
|
|
1123
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1124
|
+
if (!segmentMatches(pattern[i], path[start + i])) {
|
|
1125
|
+
continue outer;
|
|
588
1126
|
}
|
|
589
1127
|
}
|
|
590
|
-
|
|
591
|
-
return true;
|
|
1128
|
+
return true;
|
|
592
1129
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
/** `entry` matches the first `entry.length` segments of `path` (wildcards honoured). */
|
|
1133
|
+
function entryIsPrefixOf(entry, path) {
|
|
1134
|
+
if (path.length < entry.length)
|
|
1135
|
+
return false;
|
|
1136
|
+
for (let i = 0; i < entry.length; i++) {
|
|
1137
|
+
if (!segmentMatches(entry[i], path[i]))
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* The wildcard placement guard (plan §10.3, mechanism 1): every issue is a
|
|
1144
|
+
* whitelist entry that would let `"*"` sit above a human-owned key. Returns
|
|
1145
|
+
* `[]` for the shipped table — pinned by the spec-patch tests, so a future
|
|
1146
|
+
* entry that breaks the rule fails CI rather than widening the surface.
|
|
1147
|
+
*/
|
|
1148
|
+
export function wildcardPlacementIssues(table = OPTIMIZABLE_PATHS) {
|
|
1149
|
+
const issues = [];
|
|
1150
|
+
for (const [target, entries] of Object.entries(table)) {
|
|
1151
|
+
for (const entry of entries) {
|
|
1152
|
+
const stars = entry.filter((seg) => seg === WILDCARD_SEGMENT).length;
|
|
1153
|
+
if (stars === 0)
|
|
1154
|
+
continue;
|
|
1155
|
+
const label = `${target}: [${entry.join(", ")}]`;
|
|
1156
|
+
const last = entry[entry.length - 1];
|
|
1157
|
+
if (last === WILDCARD_SEGMENT) {
|
|
1158
|
+
issues.push(`${label} ends in a wildcard — it would admit a whole dynamic subtree by prefix`);
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (WILDCARD_FORBIDDEN_LEAVES.includes(last)) {
|
|
1162
|
+
issues.push(`${label} terminates in the human-owned key "${last}" under a wildcard — the §10.3 table excludes it`);
|
|
1163
|
+
}
|
|
1164
|
+
// `temperature` is the one structural segment that is a scalar LEAF (it
|
|
1165
|
+
// is structural to block the prefix route, not because it is a block),
|
|
1166
|
+
// so a wildcard entry may terminate in it; the block segments may not.
|
|
1167
|
+
if (STRUCTURAL_SEGMENTS.includes(last) && last !== "temperature") {
|
|
1168
|
+
issues.push(`${label} terminates in the structural block "${last}" under a wildcard — a structural block is never whitelisted wholesale`);
|
|
1169
|
+
}
|
|
1170
|
+
if (entry.length < 3) {
|
|
1171
|
+
issues.push(`${label} is too short to name a dial — a wildcard entry needs a concrete parent AND a concrete leaf`);
|
|
605
1172
|
}
|
|
606
1173
|
}
|
|
607
|
-
|
|
1174
|
+
}
|
|
1175
|
+
return issues;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* The single admission rule behind `validatePatch`, `applySpecEdits({
|
|
1179
|
+
* restrictToOptimizable })`, the Hangar spec editor's trust badge, the
|
|
1180
|
+
* feedback / watch-me advice tiers and the orchestrator's stage guard:
|
|
1181
|
+
*
|
|
1182
|
+
* 1. an EXACT match against a listed entry admits the path (wildcards
|
|
1183
|
+
* match one segment each);
|
|
1184
|
+
* 2. a path through a STRUCTURAL segment ({@link STRUCTURAL_SEGMENTS}) is
|
|
1185
|
+
* admitted by exact match ONLY — never by prefix;
|
|
1186
|
+
* 3. otherwise a listed entry that is a PREFIX of the path admits it
|
|
1187
|
+
* (`["nodes","0","instructions"]` under `["nodes"]`), so the optimizer
|
|
1188
|
+
* can make fine-grained updates without listing every sub-path.
|
|
1189
|
+
*
|
|
1190
|
+
* Exported so every consumer shares one matcher — a hand-rolled prefix copy
|
|
1191
|
+
* would neither honour the wildcard nor close the structural leak.
|
|
1192
|
+
*/
|
|
1193
|
+
/**
|
|
1194
|
+
* 0.6.0 §6.1 (PR 12) — the leaves the whitelist admits BY PREFIX but the
|
|
1195
|
+
* optimizer must never move, matched as a contiguous sub-sequence anywhere in
|
|
1196
|
+
* a path (the {@link HUMAN_OWNED_PATHS} matcher) and ENFORCED by
|
|
1197
|
+
* {@link isOptimizable}.
|
|
1198
|
+
*
|
|
1199
|
+
* There is exactly one row, and it exists because `model_pool.learning` is
|
|
1200
|
+
* deliberately whitelisted WHOLESALE: `crewhaus advise` mines the reward
|
|
1201
|
+
* scoreboard into that block and patches it AS a block, so a narrower
|
|
1202
|
+
* enumeration would break the advisor. PR 19 shipped the verdict table with
|
|
1203
|
+
* the `learning.seed` exclusion documented as an accepted gap "until the eval
|
|
1204
|
+
* runner (PR 12) needs it"; it needs it now — a routed eval PINS
|
|
1205
|
+
* `learning.seed`, so an optimizer free to patch the seed could produce a
|
|
1206
|
+
* measured delta of guaranteed zero, the one mutation that always "wins"
|
|
1207
|
+
* because it changes nothing the measurement can see.
|
|
1208
|
+
*
|
|
1209
|
+
* This is an enforcement narrower than the whitelist, NOT a second
|
|
1210
|
+
* classification: the `learning` BLOCK stays legitimately optimizable (which
|
|
1211
|
+
* is what §10.3's verdict table and its closing-invariant test record), and
|
|
1212
|
+
* `validatePatch` additionally refuses a block patch that would CHANGE the
|
|
1213
|
+
* seed — a block patch that preserves it, as the advisor's spread does,
|
|
1214
|
+
* passes.
|
|
1215
|
+
*/
|
|
1216
|
+
export const OPTIMIZER_REFUSED_LEAVES = Object.freeze([
|
|
1217
|
+
Object.freeze(["model_pool", "learning", "seed"]),
|
|
1218
|
+
]);
|
|
1219
|
+
/** True when `path` contains one of {@link OPTIMIZER_REFUSED_LEAVES} as a contiguous run. */
|
|
1220
|
+
function isRefusedLeaf(path) {
|
|
1221
|
+
const segs = path.map((s) => String(s));
|
|
1222
|
+
return OPTIMIZER_REFUSED_LEAVES.some((pattern) => segs.some((_s, i) => pattern.every((p, j) => segs[i + j] === p)));
|
|
1223
|
+
}
|
|
1224
|
+
export function isOptimizable(target, path) {
|
|
1225
|
+
// 0.6.0 §6.1 — the enforced refusals win over every admission rule below.
|
|
1226
|
+
if (isRefusedLeaf(path))
|
|
1227
|
+
return false;
|
|
1228
|
+
const allowed = OPTIMIZABLE_PATHS[target];
|
|
1229
|
+
if (allowed === undefined || path.length === 0)
|
|
1230
|
+
return false;
|
|
1231
|
+
for (const ok of allowed) {
|
|
1232
|
+
if (ok.length === path.length && entryIsPrefixOf(ok, path))
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
// The structural rule (see STRUCTURAL_SEGMENTS): past this point only a
|
|
1236
|
+
// PREFIX match can admit the path, and a path through a human-owned
|
|
1237
|
+
// structural block is never admitted by prefix.
|
|
1238
|
+
if (path.some((seg) => typeof seg === "string" && STRUCTURAL_SEGMENTS.includes(seg))) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
for (const ok of allowed) {
|
|
1242
|
+
if (entryIsPrefixOf(ok, path))
|
|
608
1243
|
return true;
|
|
609
1244
|
}
|
|
610
1245
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/spec-patch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pillar-2 patch infrastructure — apply a SpecPatch to a YAML source preserving comments and key order via the yaml CST. Drives the active eval optimizer's spec-level mutation loop.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/spec": "0.
|
|
18
|
+
"@crewhaus/errors": "0.6.0",
|
|
19
|
+
"@crewhaus/spec": "0.6.0",
|
|
20
20
|
"yaml": "^2.6.0",
|
|
21
21
|
"zod": "^3.23.8"
|
|
22
22
|
},
|