@crewhaus/spec-patch 0.3.2 → 0.4.2
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 +69 -0
- package/dist/index.js +246 -9
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -91,6 +91,59 @@ export declare function applySpecPatch(yamlText: string, patch: SpecPatch): Appl
|
|
|
91
91
|
* via `parseSpec`.
|
|
92
92
|
*/
|
|
93
93
|
export declare function validatePatch(spec: Spec, patch: SpecPatch): void;
|
|
94
|
+
/**
|
|
95
|
+
* Loop contract 0.4 (Batch B, G40) — one edit in an `applySpecEdits` batch.
|
|
96
|
+
*
|
|
97
|
+
* Where `SpecPatch` is the optimizer's IMPERATIVE op (`add` vs `replace` vs
|
|
98
|
+
* `remove`, each with a strict presence pre-check), `SpecEdit` is the
|
|
99
|
+
* DECLARATIVE surface shared by spec authors (the studio's form fields, the
|
|
100
|
+
* compiler-worker) and the optimizer: "make `path` carry `value`" (upsert —
|
|
101
|
+
* creates missing intermediate collections) or, when `value` is `undefined`,
|
|
102
|
+
* "make `path` absent" (idempotent — deleting an already-absent path is a
|
|
103
|
+
* no-op, so a cleared form field never needs to track prior presence).
|
|
104
|
+
*
|
|
105
|
+
* Paths may address sequence items by non-negative integer index (numeric
|
|
106
|
+
* strings work too — the CST coerces them for sequences): `["steps", 0,
|
|
107
|
+
* "instructions"]`. Index `length` appends; anything past that is an error
|
|
108
|
+
* (never null-padding), and a brand-new sequence can only start at index 0.
|
|
109
|
+
*/
|
|
110
|
+
export type SpecEditPathSegment = string | number;
|
|
111
|
+
export type SpecEdit = {
|
|
112
|
+
readonly path: ReadonlyArray<SpecEditPathSegment>;
|
|
113
|
+
/** New value at `path` (upsert). `undefined` — or omitting the key — DELETES the path. */
|
|
114
|
+
readonly value?: unknown;
|
|
115
|
+
/** Optional rationale string for audit / write-back commit messages. */
|
|
116
|
+
readonly rationale?: string;
|
|
117
|
+
};
|
|
118
|
+
export type ApplySpecEditsOptions = {
|
|
119
|
+
/**
|
|
120
|
+
* The optimizer surface: every edit path must fall under the spec target's
|
|
121
|
+
* `OPTIMIZABLE_PATHS` whitelist (exact or prefix match, same rule as
|
|
122
|
+
* `validatePatch`). Author surfaces leave this off and may edit any field —
|
|
123
|
+
* the atomic `parseSpec` re-validation is their safety floor.
|
|
124
|
+
*/
|
|
125
|
+
readonly restrictToOptimizable?: boolean;
|
|
126
|
+
};
|
|
127
|
+
export type ApplySpecEditsResult = {
|
|
128
|
+
/** The mutated YAML text, with comments and key order preserved. */
|
|
129
|
+
readonly yaml: string;
|
|
130
|
+
/** The re-parsed Spec after the whole batch. */
|
|
131
|
+
readonly spec: Spec;
|
|
132
|
+
/** Edits that changed the document — idempotent deletes of absent paths don't count. */
|
|
133
|
+
readonly applied: number;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Apply a batch of `SpecEdit`s to a YAML spec source ATOMICALLY: all edits
|
|
137
|
+
* mutate one CST in order, the result is re-validated via `parseSpec` once,
|
|
138
|
+
* and any failure — a malformed edit, an out-of-bounds sequence index, or a
|
|
139
|
+
* batch that produces an invalid spec — throws `SpecPatchError` without
|
|
140
|
+
* yielding a partially-edited text. Comments and key order survive exactly
|
|
141
|
+
* as in `applySpecPatch` (same `yaml`-package CST underneath).
|
|
142
|
+
*
|
|
143
|
+
* A zero-edit batch is a validated no-op: the input text is returned
|
|
144
|
+
* byte-identical (no CST round-trip reformatting).
|
|
145
|
+
*/
|
|
146
|
+
export declare function applySpecEdits(yamlText: string, edits: ReadonlyArray<SpecEdit>, opts?: ApplySpecEditsOptions): ApplySpecEditsResult;
|
|
94
147
|
/**
|
|
95
148
|
* Per-target whitelist of mutation paths the active optimizer is
|
|
96
149
|
* allowed to touch. Adding a new field here is the explicit signal that
|
|
@@ -98,6 +151,22 @@ export declare function validatePatch(spec: Spec, patch: SpecPatch): void;
|
|
|
98
151
|
* only mutate prompts (the default), preserving the "spec safety floor"
|
|
99
152
|
* that an optimizer can't accidentally rewrite security-critical fields
|
|
100
153
|
* like `permissions.mode` or `model_router` rules.
|
|
154
|
+
*
|
|
155
|
+
* "Watch me" (`watchme:` on cli/channel/managed, design/watch-me.md §4.3) —
|
|
156
|
+
* EVERY `watchme.*` path is deliberately EXCLUDED; none may be whitelisted
|
|
157
|
+
* piecemeal later without revisiting that design section:
|
|
158
|
+
* - `watchme.enabled` / `watchme.capture` — the observer must not be tuned
|
|
159
|
+
* by the loop it observes (self-referential optimization), and capture
|
|
160
|
+
* fidelity is a consent/data-plane posture, not a quality dial.
|
|
161
|
+
* - `watchme.judge.model` — the model roster is never auto-patched (the
|
|
162
|
+
* standing `agent.model` / model-roster exclusion).
|
|
163
|
+
* - `watchme.judge.sample_rate` / `watchme.judge.budget_usd` — spend-class
|
|
164
|
+
* knobs, human-only (sample_rate MULTIPLIES judge calls, so it is a
|
|
165
|
+
* spend dial wearing a quality dial's clothes).
|
|
166
|
+
* - `watchme.scope` / `watchme.share` — privacy/trust-boundary switches
|
|
167
|
+
* (`share` crosses the harness boundary to Thredz).
|
|
168
|
+
* The paired `crewhaus advise` watchme rule is text-only for exactly this
|
|
169
|
+
* reason: there is no whitelisted path for it to patch.
|
|
101
170
|
*/
|
|
102
171
|
export declare const OPTIMIZABLE_PATHS: Readonly<Record<Spec["target"], ReadonlyArray<ReadonlyArray<string>>>>;
|
|
103
172
|
/**
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { CrewhausError } from "@crewhaus/errors";
|
|
34
34
|
import { parseSpec } from "@crewhaus/spec";
|
|
35
|
-
import { parse, parseDocument } from "yaml";
|
|
35
|
+
import { isSeq, parse, parseDocument } from "yaml";
|
|
36
36
|
import { z } from "zod";
|
|
37
37
|
import { REDACTED_VALUE, isCredentialKey, maskCredentialTokens } from "./redact";
|
|
38
38
|
// Re-exported so downstream renderers (the CLI changelog, future reporters)
|
|
@@ -152,6 +152,135 @@ export function validatePatch(spec, patch) {
|
|
|
152
152
|
function formatPath(path) {
|
|
153
153
|
return path.join(".");
|
|
154
154
|
}
|
|
155
|
+
const specEditSchema = z.object({
|
|
156
|
+
path: z.array(z.union([z.string().min(1), z.number().int().nonnegative()])).min(1),
|
|
157
|
+
value: z.unknown().optional(),
|
|
158
|
+
rationale: z.string().optional(),
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* Apply a batch of `SpecEdit`s to a YAML spec source ATOMICALLY: all edits
|
|
162
|
+
* mutate one CST in order, the result is re-validated via `parseSpec` once,
|
|
163
|
+
* and any failure — a malformed edit, an out-of-bounds sequence index, or a
|
|
164
|
+
* batch that produces an invalid spec — throws `SpecPatchError` without
|
|
165
|
+
* yielding a partially-edited text. Comments and key order survive exactly
|
|
166
|
+
* as in `applySpecPatch` (same `yaml`-package CST underneath).
|
|
167
|
+
*
|
|
168
|
+
* A zero-edit batch is a validated no-op: the input text is returned
|
|
169
|
+
* byte-identical (no CST round-trip reformatting).
|
|
170
|
+
*/
|
|
171
|
+
export function applySpecEdits(yamlText, edits, opts = {}) {
|
|
172
|
+
edits.forEach((edit, i) => {
|
|
173
|
+
const parsed = specEditSchema.safeParse(edit);
|
|
174
|
+
if (!parsed.success) {
|
|
175
|
+
throw new SpecPatchError(`edit #${i} shape is invalid: ${parsed.error.message}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
if (edits.length === 0) {
|
|
179
|
+
let spec;
|
|
180
|
+
try {
|
|
181
|
+
spec = parseSpec(yamlText);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
throw new SpecPatchError(`input YAML failed spec validation: ${err.message}`, err);
|
|
185
|
+
}
|
|
186
|
+
return { yaml: yamlText, spec, applied: 0 };
|
|
187
|
+
}
|
|
188
|
+
let doc;
|
|
189
|
+
try {
|
|
190
|
+
doc = parseDocument(yamlText);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
throw new SpecPatchError("input YAML is not parseable", err);
|
|
194
|
+
}
|
|
195
|
+
// `parseDocument` collects syntax errors on `doc.errors` instead of
|
|
196
|
+
// throwing — without this check they'd only surface as the CST's opaque
|
|
197
|
+
// "Document with errors cannot be stringified" at toString() time.
|
|
198
|
+
if (doc.errors.length > 0) {
|
|
199
|
+
throw new SpecPatchError(`input YAML is not parseable: ${doc.errors[0]?.message ?? "unknown error"}`);
|
|
200
|
+
}
|
|
201
|
+
if (opts.restrictToOptimizable === true) {
|
|
202
|
+
const docTarget = doc.getIn(["target"]);
|
|
203
|
+
if (typeof docTarget !== "string" || !(docTarget in OPTIMIZABLE_PATHS)) {
|
|
204
|
+
throw new SpecPatchError(`cannot restrict to optimizable paths: spec target ${JSON.stringify(docTarget)} is not a known target`);
|
|
205
|
+
}
|
|
206
|
+
edits.forEach((edit, i) => {
|
|
207
|
+
if (!isOptimizable(docTarget, edit.path)) {
|
|
208
|
+
throw new SpecPatchError(`edit #${i}: path ${formatEditPath(edit.path)} is not listed in OPTIMIZABLE_PATHS for target "${docTarget}"; add it to packages/spec-patch/src/index.ts if it's intended to be tunable`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
let applied = 0;
|
|
213
|
+
edits.forEach((edit, i) => {
|
|
214
|
+
const path = [...edit.path];
|
|
215
|
+
const label = `edit #${i} (${formatEditPath(path)})`;
|
|
216
|
+
if (edit.value === undefined) {
|
|
217
|
+
// Delete-on-undefined. hasIn is false both for an absent leaf and for a
|
|
218
|
+
// scalar intermediate, so the deleteIn below can never hit the CST's
|
|
219
|
+
// "Expected YAML collection" throw — absent paths are a clean no-op.
|
|
220
|
+
if (doc.hasIn(path)) {
|
|
221
|
+
doc.deleteIn(path);
|
|
222
|
+
applied += 1;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
guardSequenceIndices(doc, path, label);
|
|
227
|
+
try {
|
|
228
|
+
doc.setIn(path, edit.value);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
throw new SpecPatchError(`${label} failed: ${err.message}`, err);
|
|
232
|
+
}
|
|
233
|
+
applied += 1;
|
|
234
|
+
});
|
|
235
|
+
const newYaml = doc.toString();
|
|
236
|
+
let spec;
|
|
237
|
+
try {
|
|
238
|
+
spec = parseSpec(newYaml);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
throw new SpecPatchError(`edited YAML failed spec validation: ${err.message}`, err);
|
|
242
|
+
}
|
|
243
|
+
return { yaml: newYaml, spec, applied };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Pre-flight for `setIn` along a path that may address sequences: the CST
|
|
247
|
+
* happily null-pads a sequence up to any index (`steps[5]` on a 2-step list
|
|
248
|
+
* yields three `null` steps), which is never what an author or optimizer
|
|
249
|
+
* meant. Reject indices past `length` (index === `length` appends) and
|
|
250
|
+
* refuse to CREATE a sequence anywhere but at index 0. Map keys and the
|
|
251
|
+
* CST's own errors (negative / non-integer index, scalar intermediates)
|
|
252
|
+
* pass through untouched — the caller wraps those with the edit label.
|
|
253
|
+
*/
|
|
254
|
+
function guardSequenceIndices(doc, path, label) {
|
|
255
|
+
for (let i = 0; i < path.length; i++) {
|
|
256
|
+
const seg = path[i];
|
|
257
|
+
const parent = i === 0 ? doc.contents : doc.getIn(path.slice(0, i), true);
|
|
258
|
+
const asIndex = typeof seg === "number" ? seg : /^\d+$/.test(seg) ? Number.parseInt(seg, 10) : undefined;
|
|
259
|
+
if (isSeq(parent)) {
|
|
260
|
+
if (asIndex !== undefined && asIndex > parent.items.length) {
|
|
261
|
+
throw new SpecPatchError(`${label}: index ${asIndex} is out of bounds for the sequence at ${i === 0 ? "(root)" : formatEditPath(path.slice(0, i))} (length ${parent.items.length}; index ${parent.items.length} appends)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (parent === undefined && typeof seg === "number" && seg !== 0) {
|
|
265
|
+
throw new SpecPatchError(`${label}: cannot create a new sequence at ${i === 0 ? "(root)" : formatEditPath(path.slice(0, i))} starting at index ${seg} — a new sequence starts at index 0`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Render an edit path for messages: string keys dot-joined, integer
|
|
270
|
+
* sequence indices as `[i]` — `["steps", 0, "instructions"]` →
|
|
271
|
+
* `steps[0].instructions`. */
|
|
272
|
+
function formatEditPath(path) {
|
|
273
|
+
let out = "";
|
|
274
|
+
for (const seg of path) {
|
|
275
|
+
if (typeof seg === "number") {
|
|
276
|
+
out += `[${seg}]`;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
out += out === "" ? seg : `.${seg}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
155
284
|
/**
|
|
156
285
|
* Per-target whitelist of mutation paths the active optimizer is
|
|
157
286
|
* allowed to touch. Adding a new field here is the explicit signal that
|
|
@@ -159,6 +288,22 @@ function formatPath(path) {
|
|
|
159
288
|
* only mutate prompts (the default), preserving the "spec safety floor"
|
|
160
289
|
* that an optimizer can't accidentally rewrite security-critical fields
|
|
161
290
|
* like `permissions.mode` or `model_router` rules.
|
|
291
|
+
*
|
|
292
|
+
* "Watch me" (`watchme:` on cli/channel/managed, design/watch-me.md §4.3) —
|
|
293
|
+
* EVERY `watchme.*` path is deliberately EXCLUDED; none may be whitelisted
|
|
294
|
+
* piecemeal later without revisiting that design section:
|
|
295
|
+
* - `watchme.enabled` / `watchme.capture` — the observer must not be tuned
|
|
296
|
+
* by the loop it observes (self-referential optimization), and capture
|
|
297
|
+
* fidelity is a consent/data-plane posture, not a quality dial.
|
|
298
|
+
* - `watchme.judge.model` — the model roster is never auto-patched (the
|
|
299
|
+
* standing `agent.model` / model-roster exclusion).
|
|
300
|
+
* - `watchme.judge.sample_rate` / `watchme.judge.budget_usd` — spend-class
|
|
301
|
+
* knobs, human-only (sample_rate MULTIPLIES judge calls, so it is a
|
|
302
|
+
* spend dial wearing a quality dial's clothes).
|
|
303
|
+
* - `watchme.scope` / `watchme.share` — privacy/trust-boundary switches
|
|
304
|
+
* (`share` crosses the harness boundary to Thredz).
|
|
305
|
+
* The paired `crewhaus advise` watchme rule is text-only for exactly this
|
|
306
|
+
* reason: there is no whitelisted path for it to patch.
|
|
162
307
|
*/
|
|
163
308
|
export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
164
309
|
cli: Object.freeze([
|
|
@@ -167,7 +312,15 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
167
312
|
// per-turn output-token cap when max_tokens truncations recur; safe to
|
|
168
313
|
// autotune (a bigger cap can only trade cost for completeness).
|
|
169
314
|
Object.freeze(["agent", "max_tokens"]),
|
|
315
|
+
// Loop contract 0.4 (Batch A) — the explicit thinking-token budget is a
|
|
316
|
+
// pure quality/cost dial (>= 1024 enforced by the spec); the optimizer
|
|
317
|
+
// may raise/lower it but never flips the thinking FORM (effort presets
|
|
318
|
+
// stay human-owned via the exactly-one-form superRefine).
|
|
319
|
+
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
170
320
|
Object.freeze(["failure_taxonomy"]),
|
|
321
|
+
// Loop contract 0.4 (Batch A) — real as of the `compaction.threshold`
|
|
322
|
+
// spec field (0.5–0.99): when to trigger autocompaction is a classic
|
|
323
|
+
// token-cost vs context-completeness dial.
|
|
171
324
|
Object.freeze(["compaction", "threshold"]),
|
|
172
325
|
// Pillar 2 active context curation — eval-optimizer can flip the
|
|
173
326
|
// semantic-dedupe + relevance-reorder pass on/off and tune its
|
|
@@ -175,10 +328,16 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
175
328
|
Object.freeze(["compaction", "curate"]),
|
|
176
329
|
Object.freeze(["compaction", "dedupeThreshold"]),
|
|
177
330
|
Object.freeze(["compaction", "relevanceTopK"]),
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
331
|
+
// Loop contract 0.4 (Batch A) — tool-iteration ceiling: quality (task
|
|
332
|
+
// completion) vs runaway-cost dial. NOTE ["security","egressPolicy"]
|
|
333
|
+
// was REMOVED here: the spec never grew that key (strict schemas
|
|
334
|
+
// reject it), and the OPTIMIZABLE_PATHS guard test requires every
|
|
335
|
+
// listed path to round-trip through parseSpec. Re-add it WITH the spec
|
|
336
|
+
// field when the egress-fabric FRs ship it.
|
|
337
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
338
|
+
// Pillar 3 intent gate — tunable so the eval-optimizer can find the
|
|
339
|
+
// sweet spot between false-positive denials and false-negative exfil
|
|
340
|
+
// bypasses.
|
|
182
341
|
Object.freeze(["security", "justification"]),
|
|
183
342
|
// §47 blockchain subsystem (slice 0). Whole-block replacement so the
|
|
184
343
|
// optimizer can tune `chains[*].finality.count`, `chains[*].rpcPolicy`,
|
|
@@ -212,26 +371,63 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
212
371
|
// the volatile tail block (token cost vs
|
|
213
372
|
// plan/ledger completeness)
|
|
214
373
|
// Deliberately EXCLUDED — the optimizer tunes quality, never semantics:
|
|
215
|
-
// thredz.* (credentials
|
|
216
|
-
//
|
|
374
|
+
// thredz.* (credentials; thredz.messaging = a destructive send-tool
|
|
375
|
+
// switch), memory.backend (store flip), memory.dream.every + .mode
|
|
376
|
+
// (side-effecting schedule / model-spend switch), continuity
|
|
217
377
|
// .proof/.scope/.enabled (behavioral switches), compaction
|
|
218
378
|
// .preserve_user_messages (safety), learning.sources (allowlist =
|
|
219
|
-
// security).
|
|
379
|
+
// security). Batch G adds more of the same category: expose.* (G30 — the
|
|
380
|
+
// deployment/exposure surface, not a quality knob), plugins (G32 — a
|
|
381
|
+
// capability allowlist = supply-chain security, human-owned like
|
|
382
|
+
// learning.sources / the model roster), and sub_agents.*.federation (G31
|
|
383
|
+
// — cross-deployment wiring / trust boundary). None are optimizer-reachable.
|
|
220
384
|
Object.freeze(["memory", "recallK"]),
|
|
221
385
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
222
386
|
Object.freeze(["memory", "ttl"]),
|
|
223
387
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
224
388
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
225
389
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
390
|
+
// Loop contract 0.4 (Batch B, G40) — the in-loop evaluation dials. The
|
|
391
|
+
// pass bar (llm_judge graders only — the spec rejects threshold on
|
|
392
|
+
// deterministic graders, so a mis-aimed patch fails the re-parse) and
|
|
393
|
+
// the retry cap are classic quality-vs-cost knobs. Deliberately NOT
|
|
394
|
+
// ["evaluation"] wholesale and NOT ["evaluation","grader"]: the grader
|
|
395
|
+
// (criteria/type) and on_fail behaviour are human-owned semantics —
|
|
396
|
+
// the optimizer tunes how strict the gate is, never what it judges.
|
|
397
|
+
Object.freeze(["evaluation", "threshold"]),
|
|
398
|
+
Object.freeze(["evaluation", "max_retries"]),
|
|
399
|
+
// Loop contract 0.4 (Batch E) — the agent-shape RAG (`knowledge:`) dials
|
|
400
|
+
// + the per-turn recall cadence. All scalars that survive lower() 1:1
|
|
401
|
+
// with their bounds owned by the spec schema, so an out-of-bounds patch
|
|
402
|
+
// fails applySpecPatch's re-parse. `default_k` (hits/turn) and the chunker
|
|
403
|
+
// window are classic recall-quality vs token-cost dials; `refreshEvery`
|
|
404
|
+
// trades recall freshness against per-turn recall cost. Deliberately NOT
|
|
405
|
+
// ["knowledge"] wholesale and NOT ["knowledge","sources"]: the corpus
|
|
406
|
+
// (an allowlist of what the agent may read) is human-owned, mirroring the
|
|
407
|
+
// learning.sources / model-roster exclusions.
|
|
408
|
+
Object.freeze(["knowledge", "default_k"]),
|
|
409
|
+
Object.freeze(["knowledge", "chunk", "size"]),
|
|
410
|
+
Object.freeze(["knowledge", "chunk", "overlap"]),
|
|
411
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
226
412
|
]),
|
|
227
413
|
workflow: Object.freeze([
|
|
414
|
+
// Whole-step replacement — this ALSO reaches item 9's (G37) per-step
|
|
415
|
+
// model routing (model_pool policy/routing/learning, tiers, fallbacks):
|
|
416
|
+
// no narrower entry is possible (the roster/step index is positional) and
|
|
417
|
+
// whole-step replacement already spans model/instructions, so the policy
|
|
418
|
+
// knobs ride here rather than as standalone paths.
|
|
228
419
|
Object.freeze(["steps"]),
|
|
229
420
|
Object.freeze(["failure_taxonomy"]),
|
|
230
421
|
Object.freeze(["chains"]),
|
|
231
422
|
Object.freeze(["transaction_policy"]),
|
|
423
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
424
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
232
425
|
]) /* whole-step replacement allowed */,
|
|
233
426
|
channel: Object.freeze([
|
|
234
427
|
Object.freeze(["agent", "instructions"]),
|
|
428
|
+
// Loop contract 0.4 (Batch A) — see the cli entries.
|
|
429
|
+
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
430
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
235
431
|
Object.freeze(["failure_taxonomy"]),
|
|
236
432
|
Object.freeze(["chains"]),
|
|
237
433
|
Object.freeze(["transaction_policy"]),
|
|
@@ -246,15 +442,28 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
246
442
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
247
443
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
248
444
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
445
|
+
// Loop contract 0.4 (Batch B, G40) — evaluation dials; see the cli entry.
|
|
446
|
+
Object.freeze(["evaluation", "threshold"]),
|
|
447
|
+
Object.freeze(["evaluation", "max_retries"]),
|
|
448
|
+
// Loop contract 0.4 (Batch E) — knowledge/recall dials; see the cli entry.
|
|
449
|
+
Object.freeze(["knowledge", "default_k"]),
|
|
450
|
+
Object.freeze(["knowledge", "chunk", "size"]),
|
|
451
|
+
Object.freeze(["knowledge", "chunk", "overlap"]),
|
|
452
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
249
453
|
]),
|
|
250
454
|
graph: Object.freeze([
|
|
251
455
|
Object.freeze(["nodes"]),
|
|
252
456
|
Object.freeze(["failure_taxonomy"]),
|
|
253
457
|
Object.freeze(["chains"]),
|
|
254
458
|
Object.freeze(["transaction_policy"]),
|
|
459
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
460
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
255
461
|
]),
|
|
256
462
|
managed: Object.freeze([
|
|
257
463
|
Object.freeze(["agent", "instructions"]),
|
|
464
|
+
// Loop contract 0.4 (Batch A) — see the cli entries.
|
|
465
|
+
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
466
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
258
467
|
Object.freeze(["failure_taxonomy"]),
|
|
259
468
|
// Adaptive model routing — pool policy knobs only (see the cli entry).
|
|
260
469
|
Object.freeze(["agent", "model_pool", "policy"]),
|
|
@@ -267,6 +476,14 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
267
476
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
268
477
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
269
478
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
479
|
+
// Loop contract 0.4 (Batch B, G40) — evaluation dials; see the cli entry.
|
|
480
|
+
Object.freeze(["evaluation", "threshold"]),
|
|
481
|
+
Object.freeze(["evaluation", "max_retries"]),
|
|
482
|
+
// Loop contract 0.4 (Batch E) — knowledge/recall dials; see the cli entry.
|
|
483
|
+
Object.freeze(["knowledge", "default_k"]),
|
|
484
|
+
Object.freeze(["knowledge", "chunk", "size"]),
|
|
485
|
+
Object.freeze(["knowledge", "chunk", "overlap"]),
|
|
486
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
270
487
|
]),
|
|
271
488
|
pipeline: Object.freeze([
|
|
272
489
|
Object.freeze(["agent", "instructions"]),
|
|
@@ -276,10 +493,17 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
276
493
|
Object.freeze(["retrieve", "defaultK"]),
|
|
277
494
|
]),
|
|
278
495
|
crew: Object.freeze([
|
|
496
|
+
// Whole-role replacement — this ALSO reaches item 9's (G37) per-role
|
|
497
|
+
// model routing (model_pool policy/routing/learning, tiers, fallbacks):
|
|
498
|
+
// the role name is a dynamic map key, so no static narrower path exists,
|
|
499
|
+
// and whole-role replacement already spans model/instructions, so the
|
|
500
|
+
// policy knobs ride here rather than as standalone paths.
|
|
279
501
|
Object.freeze(["roles"]),
|
|
280
502
|
Object.freeze(["failure_taxonomy"]),
|
|
281
503
|
Object.freeze(["chains"]),
|
|
282
504
|
Object.freeze(["transaction_policy"]),
|
|
505
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
506
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
283
507
|
// 0.3.0 memory/continuity quality knobs — types/bounds at the cli entry.
|
|
284
508
|
Object.freeze(["memory", "recallK"]),
|
|
285
509
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
@@ -287,13 +511,20 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
287
511
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
288
512
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
289
513
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
514
|
+
// Loop contract 0.4 (Batch E) — per-turn recall cadence; see the cli entry.
|
|
515
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
290
516
|
]) /* whole-role replacement */,
|
|
291
517
|
research: Object.freeze([
|
|
292
518
|
Object.freeze(["agent", "instructions"]),
|
|
293
519
|
Object.freeze(["failure_taxonomy"]),
|
|
294
|
-
|
|
520
|
+
// NOTE ["retrieve","maxDepth"] was REMOVED here (Batch A): the research
|
|
521
|
+
// retrieve block never grew a maxDepth field (strict schema rejects
|
|
522
|
+
// it), and the OPTIMIZABLE_PATHS guard test requires every listed path
|
|
523
|
+
// to round-trip through parseSpec. Re-add it WITH the spec field.
|
|
295
524
|
Object.freeze(["chains"]),
|
|
296
525
|
Object.freeze(["transaction_policy"]),
|
|
526
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
527
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
297
528
|
// 0.3.0 memory/continuity quality knobs — types/bounds at the cli entry.
|
|
298
529
|
Object.freeze(["memory", "recallK"]),
|
|
299
530
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
@@ -301,12 +532,16 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
301
532
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
302
533
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
303
534
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
535
|
+
// Loop contract 0.4 (Batch E) — per-turn recall cadence; see the cli entry.
|
|
536
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
304
537
|
]),
|
|
305
538
|
batch: Object.freeze([
|
|
306
539
|
Object.freeze(["agent", "instructions"]),
|
|
307
540
|
Object.freeze(["failure_taxonomy"]),
|
|
308
541
|
Object.freeze(["chains"]),
|
|
309
542
|
Object.freeze(["transaction_policy"]),
|
|
543
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
544
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
310
545
|
]),
|
|
311
546
|
voice: Object.freeze([
|
|
312
547
|
Object.freeze(["agent", "instructions"]),
|
|
@@ -315,6 +550,8 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
315
550
|
browser: Object.freeze([
|
|
316
551
|
Object.freeze(["agent", "instructions"]),
|
|
317
552
|
Object.freeze(["failure_taxonomy"]),
|
|
553
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
554
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
318
555
|
]),
|
|
319
556
|
eval: Object.freeze([
|
|
320
557
|
Object.freeze(["agent", "instructions"]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/spec-patch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
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.4.2",
|
|
19
|
+
"@crewhaus/spec": "0.4.2",
|
|
20
20
|
"yaml": "^2.6.0",
|
|
21
21
|
"zod": "^3.23.8"
|
|
22
22
|
},
|