wizz-method 2.1.0 → 2.2.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/package.json +1 -1
- package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +2 -2
- package/src/bmm-skills/4-implementation/wizz-code-review/customize.toml +102 -8
- package/src/bmm-skills/4-implementation/wizz-code-review/references/review-economy.md +75 -0
- package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-01-gather-context.md +29 -15
- package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-02-review.md +13 -10
- package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-03-triage.md +11 -5
- package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-04-present.md +4 -0
- package/src/bmm-skills/4-implementation/wizz-quick-dev/customize.toml +103 -7
- package/src/bmm-skills/4-implementation/wizz-quick-dev/references/review-economy.md +75 -0
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-04-review.md +43 -18
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-05-present.md +4 -0
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-oneshot.md +2 -2
- package/src/modules/wizz/_shared/model-ladder.md +1 -0
- package/src/modules/wizz/_shared/token-economy.md +1 -1
- package/src/modules/wizz/subagents/codex/wizz-exec-review.toml +2 -1
- package/src/modules/wizz/subagents/gemini/wizz-exec-review.md +2 -1
- package/src/modules/wizz/subagents/opencode/wizz-exec-review.md +2 -1
- package/src/modules/wizz/subagents/spec.yaml +2 -1
- package/src/modules/wizz/subagents/wizz-exec-review.md +2 -1
- package/tools/fetch-assets.mjs +8 -1
- package/tools/installer/commands/fetch-assets.js +7 -8
- package/tools/installer/core/resolved-customizations.js +58 -10
- package/tools/installer/core/resolved-schema.js +116 -18
|
@@ -78,11 +78,35 @@ function detectKeyedMergeField(items) {
|
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Warn when a team/user override REPLACES a base review layer (matched by
|
|
83
|
+
* `id`/`code`) but omits `tiers` or `max_calls` the base layer had.
|
|
84
|
+
* `mergeByKey` replaces the whole matched item — it never merges an
|
|
85
|
+
* override item field-by-field against its base counterpart (Python
|
|
86
|
+
* parity) — so dropping those fields silently loses the base layer's
|
|
87
|
+
* scaling: the layer now runs on every tier and/or with no call cap.
|
|
88
|
+
* No-op when `opts.warnings`/`opts.layerLabel` are not supplied, so callers
|
|
89
|
+
* that don't care about this (e.g. direct `deepMerge` calls in tests) pay
|
|
90
|
+
* nothing for it.
|
|
91
|
+
*/
|
|
92
|
+
function warnOverrideDropsLayerScaling({ warnings, layerLabel, key, matchedBase, item }) {
|
|
93
|
+
if (!warnings || !layerLabel) return;
|
|
94
|
+
const missing = ['tiers', 'max_calls'].filter((field) => hasOwn(matchedBase, field) && !hasOwn(item, field));
|
|
95
|
+
if (missing.length === 0) return;
|
|
96
|
+
const consequence = missing.map((field) => (field === 'tiers' ? 'every tier' : 'uncapped')).join(' and ');
|
|
97
|
+
warnings.push(
|
|
98
|
+
`${layerLabel} override: "${key}" replaces the whole base layer and omits ${missing.join(' and ')} — it now runs on ${consequence}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
/**
|
|
82
103
|
* Keyed merge: matching identifiers replace in place, new ones append.
|
|
83
104
|
* Non-table items in the override are appended (Python parity).
|
|
105
|
+
* @param {object} [opts] - `{ warnings, layerLabel }` to collect the
|
|
106
|
+
* dropped-scaling warning above; omit for warning-free merges (tests,
|
|
107
|
+
* internal recomputation).
|
|
84
108
|
*/
|
|
85
|
-
function mergeByKey(base, override, keyName) {
|
|
109
|
+
function mergeByKey(base, override, keyName, opts) {
|
|
86
110
|
const result = [];
|
|
87
111
|
const indexByKey = new Map();
|
|
88
112
|
for (const item of base) {
|
|
@@ -99,7 +123,9 @@ function mergeByKey(base, override, keyName) {
|
|
|
99
123
|
}
|
|
100
124
|
const key = item[keyName];
|
|
101
125
|
if (key !== undefined && key !== null && indexByKey.has(key)) {
|
|
102
|
-
|
|
126
|
+
const matchedIndex = indexByKey.get(key);
|
|
127
|
+
if (opts) warnOverrideDropsLayerScaling({ ...opts, key, matchedBase: result[matchedIndex], item });
|
|
128
|
+
result[matchedIndex] = { ...item };
|
|
103
129
|
} else {
|
|
104
130
|
if (key !== undefined && key !== null) indexByKey.set(key, result.length);
|
|
105
131
|
result.push({ ...item });
|
|
@@ -112,30 +138,33 @@ function mergeByKey(base, override, keyName) {
|
|
|
112
138
|
* Shape-aware array merge: keyed merge when every item (base + override)
|
|
113
139
|
* shares `code` or `id`, else append.
|
|
114
140
|
*/
|
|
115
|
-
function mergeArrays(base, override) {
|
|
141
|
+
function mergeArrays(base, override, opts) {
|
|
116
142
|
const baseArr = Array.isArray(base) ? base : [];
|
|
117
143
|
const overrideArr = Array.isArray(override) ? override : [];
|
|
118
144
|
const keyedField = detectKeyedMergeField([...baseArr, ...overrideArr]);
|
|
119
|
-
if (keyedField) return mergeByKey(baseArr, overrideArr, keyedField);
|
|
145
|
+
if (keyedField) return mergeByKey(baseArr, overrideArr, keyedField, opts);
|
|
120
146
|
return [...baseArr, ...overrideArr];
|
|
121
147
|
}
|
|
122
148
|
|
|
123
149
|
/**
|
|
124
150
|
* Structural deep merge. Tables deep-merge, arrays shape-merge,
|
|
125
151
|
* everything else: override wins. Python `deep_merge` parity.
|
|
152
|
+
* @param {object} [opts] - `{ warnings, layerLabel }`, threaded down to
|
|
153
|
+
* every `mergeByKey` call reached while walking `base`/`override`; see
|
|
154
|
+
* `warnOverrideDropsLayerScaling`.
|
|
126
155
|
*/
|
|
127
|
-
function deepMerge(base, override) {
|
|
156
|
+
function deepMerge(base, override, opts) {
|
|
128
157
|
if (isPlainObject(base) && isPlainObject(override)) {
|
|
129
158
|
const result = { ...base };
|
|
130
159
|
for (const [key, overVal] of Object.entries(override)) {
|
|
131
160
|
// hasOwn, not `in`: `'constructor' in result` is true via the
|
|
132
161
|
// prototype chain even when the base layer never declared it, which
|
|
133
162
|
// would deep-merge the override into Object's constructor function.
|
|
134
|
-
safeSet(result, key, hasOwn(result, key) ? deepMerge(result[key], overVal) : overVal);
|
|
163
|
+
safeSet(result, key, hasOwn(result, key) ? deepMerge(result[key], overVal, opts) : overVal);
|
|
135
164
|
}
|
|
136
165
|
return result;
|
|
137
166
|
}
|
|
138
|
-
if (Array.isArray(base) && Array.isArray(override)) return mergeArrays(base, override);
|
|
167
|
+
if (Array.isArray(base) && Array.isArray(override)) return mergeArrays(base, override, opts);
|
|
139
168
|
return override;
|
|
140
169
|
}
|
|
141
170
|
|
|
@@ -455,16 +484,35 @@ async function resolveSkillSnapshot({ customizePath, customDir, resolvedDir, pro
|
|
|
455
484
|
// layer is exempt (it owns the LLM-only fields and new experimental
|
|
456
485
|
// keys); a typo here is exactly the silent-merge the audit flagged.
|
|
457
486
|
// Warnings, never errors — the snapshot is still written (fail-open).
|
|
487
|
+
// findUnknownKeys() returns structured { path, kind, detail? } entries:
|
|
488
|
+
// `kind` says "unknown key" vs "invalid value" directly, so this loop
|
|
489
|
+
// never has to sniff the rendered path text (a quoted TOML key can itself
|
|
490
|
+
// contain a space, which broke the old `includes(' ')` heuristic).
|
|
458
491
|
for (const [layerName, layer] of [
|
|
459
492
|
['team', teamRaw],
|
|
460
493
|
['user', userRaw],
|
|
461
494
|
]) {
|
|
462
|
-
for (const
|
|
463
|
-
warnings.push(
|
|
495
|
+
for (const { path: keyPath, kind, detail } of findUnknownKeys(layer)) {
|
|
496
|
+
warnings.push(
|
|
497
|
+
kind === 'invalid'
|
|
498
|
+
? `${layerName} override: invalid ${keyPath}${detail ? ` (${detail})` : ''} — check customize.toml docs`
|
|
499
|
+
: `${layerName} override: unknown key "${keyPath}" (not a recognized workflow field — typo? check customize.toml docs)`,
|
|
500
|
+
);
|
|
464
501
|
}
|
|
465
502
|
}
|
|
466
503
|
|
|
467
|
-
|
|
504
|
+
// Audit item 24 follow-up: a team/user override that REPLACES a base
|
|
505
|
+
// review layer (matched by id/code) but drops the base's `tiers`/
|
|
506
|
+
// `max_calls` silently loses that scaling — see
|
|
507
|
+
// warnOverrideDropsLayerScaling. Two separate deepMerge calls (rather
|
|
508
|
+
// than the previous deepMerge(deepMerge(defaults, team), user)) so each
|
|
509
|
+
// one can be labeled with the layer that is doing the overriding; a user
|
|
510
|
+
// override is checked against the team-adjusted result, which is the
|
|
511
|
+
// layer it actually replaces.
|
|
512
|
+
const overrideWarnings = [];
|
|
513
|
+
const afterTeam = deepMerge(defaults, team, { warnings: overrideWarnings, layerLabel: 'team' });
|
|
514
|
+
const merged = deepMerge(afterTeam, user, { warnings: overrideWarnings, layerLabel: 'user' });
|
|
515
|
+
warnings.push(...overrideWarnings);
|
|
468
516
|
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
469
517
|
// Atomic write via fs-native (tmp + rename), consistent with every other
|
|
470
518
|
// config file the installer writes.
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from `resolved-customizations.js` (2026-09-11) to keep that file
|
|
5
5
|
* under the 800-line cap. Validation only: nothing here writes, parses or
|
|
6
|
-
* merges — it takes ONE parsed layer and reports the keys no code
|
|
6
|
+
* merges — it takes ONE parsed layer and reports the keys/values no code
|
|
7
|
+
* consumes or that are structurally invalid.
|
|
7
8
|
*
|
|
8
9
|
* "additionalProperties: false" in spirit. The BASE layer (shipped,
|
|
9
10
|
* code-reviewed) is exempt: it may legitimately carry LLM-only fields
|
|
@@ -14,6 +15,12 @@
|
|
|
14
15
|
* is a warning (not an error: fail-open, the snapshot is still written).
|
|
15
16
|
* `include` is the documented extension point and is validated by
|
|
16
17
|
* `resolveIncludes`.
|
|
18
|
+
*
|
|
19
|
+
* Findings are structured `{ path, kind: 'unknown' | 'invalid', detail? }`
|
|
20
|
+
* entries — never plain strings. `kind` is how a caller tells "this key
|
|
21
|
+
* doesn't exist" from "this key exists but its value is wrong"; sniffing the
|
|
22
|
+
* rendered path text (e.g. for a space) breaks the moment a dotted path or a
|
|
23
|
+
* quoted TOML key legitimately contains one.
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
const { isPlainObject } = require('./resolved-includes');
|
|
@@ -72,31 +79,56 @@ const WORKFLOW_KEYS = new Set([
|
|
|
72
79
|
// item keys are validated against REVIEW_LAYER_KEYS below.
|
|
73
80
|
'review_layers',
|
|
74
81
|
'oneshot_review_layers',
|
|
82
|
+
// review economy (review-economy.md): diff cleaning excludes/critical paths.
|
|
83
|
+
'review_exclude',
|
|
84
|
+
'review_critical_paths',
|
|
75
85
|
]);
|
|
76
86
|
const PARTY_MEMBER_KEYS = new Set(['code', 'name', 'icon', 'title', 'persona', 'capabilities', 'model']);
|
|
77
87
|
const PARTY_GROUP_KEYS = new Set(['id', 'name', 'scene', 'members', 'memory']);
|
|
78
|
-
// Exactly the keys the
|
|
79
|
-
// (src/bmm-skills/4-implementation/wizz-code-review + wizz-quick-dev)
|
|
80
|
-
|
|
88
|
+
// Exactly the keys the shipped `[[workflow.*review_layers]]` entries use
|
|
89
|
+
// (src/bmm-skills/4-implementation/wizz-code-review + wizz-quick-dev), plus
|
|
90
|
+
// `tiers`/`max_calls` for the review-economy tier scaling (review-economy.md).
|
|
91
|
+
const REVIEW_LAYER_KEYS = new Set(['id', 'name', 'instruction', 'when', 'tiers', 'max_calls']);
|
|
92
|
+
// The only valid values for a review_layers/oneshot_review_layers `tiers`
|
|
93
|
+
// entry (review-economy.md tier scaling). A typo (`"crtical"`) used to load
|
|
94
|
+
// silently and drop the layer on every tier — nothing ever matched it.
|
|
95
|
+
const VALID_REVIEW_TIERS = new Set(['small', 'normal', 'critical']);
|
|
81
96
|
// The only top-level keys any of the 47 shipped customize.toml carry. A
|
|
82
97
|
// top-level typo (`[workflw]`) or a stray scalar merged into silence before.
|
|
83
98
|
const TOP_LEVEL_KEYS = new Set(['workflow', 'agent', 'include']);
|
|
84
99
|
|
|
100
|
+
/** `value` is an array whose every element is a string. */
|
|
101
|
+
function isArrayOfStrings(value) {
|
|
102
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
103
|
+
}
|
|
104
|
+
|
|
85
105
|
/**
|
|
86
|
-
* Collect unknown
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
106
|
+
* Collect unknown/invalid entries under `[workflow]`: unknown fields by
|
|
107
|
+
* name, invalid values for the two review-economy string-array fields, and
|
|
108
|
+
* — for the arrays-of-tables we own (party members/groups, review layers) —
|
|
109
|
+
* the unknown keys and invalid values of each item.
|
|
110
|
+
*
|
|
111
|
+
* Every array-of-tables item is validated on its own, even when the array
|
|
112
|
+
* also holds non-object elements: a non-object item is flagged (invalid)
|
|
113
|
+
* instead of skipping validation for the whole array, which used to let a
|
|
114
|
+
* stray non-table entry hide typos in every OTHER entry.
|
|
115
|
+
* @returns {Array<{path: string, kind: 'unknown'|'invalid', detail?: string}>}
|
|
90
116
|
*/
|
|
91
117
|
function findUnknownWorkflowKeys(workflow) {
|
|
92
118
|
const unknown = [];
|
|
93
119
|
for (const [k2, v2] of Object.entries(workflow)) {
|
|
94
120
|
const wfPath = `workflow.${k2}`;
|
|
95
121
|
if (!WORKFLOW_KEYS.has(k2)) {
|
|
96
|
-
unknown.push(wfPath);
|
|
122
|
+
unknown.push({ path: wfPath, kind: 'unknown' });
|
|
97
123
|
continue;
|
|
98
124
|
}
|
|
99
|
-
if (
|
|
125
|
+
if (k2 === 'review_exclude' || k2 === 'review_critical_paths') {
|
|
126
|
+
if (!isArrayOfStrings(v2)) {
|
|
127
|
+
unknown.push({ path: wfPath, kind: 'invalid', detail: `expected an array of strings, got ${JSON.stringify(v2)}` });
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(v2) || v2.length === 0) continue;
|
|
100
132
|
const allowed =
|
|
101
133
|
k2 === 'party_members'
|
|
102
134
|
? PARTY_MEMBER_KEYS
|
|
@@ -106,30 +138,93 @@ function findUnknownWorkflowKeys(workflow) {
|
|
|
106
138
|
? REVIEW_LAYER_KEYS
|
|
107
139
|
: null;
|
|
108
140
|
if (!allowed) continue;
|
|
141
|
+
const isOneshot = k2 === 'oneshot_review_layers';
|
|
109
142
|
for (const item of v2) {
|
|
143
|
+
if (!isPlainObject(item)) {
|
|
144
|
+
unknown.push({ path: `${wfPath}[]`, kind: 'invalid', detail: `expected a table, got ${JSON.stringify(item)}` });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
110
147
|
for (const k3 of Object.keys(item)) {
|
|
111
|
-
if (!allowed.has(k3)) unknown.push(`${wfPath}[].${k3}
|
|
148
|
+
if (!allowed.has(k3)) unknown.push({ path: `${wfPath}[].${k3}`, kind: 'unknown' });
|
|
112
149
|
}
|
|
150
|
+
if (allowed === REVIEW_LAYER_KEYS) unknown.push(...findInvalidReviewLayerValues(item, wfPath, isOneshot));
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
return unknown;
|
|
116
154
|
}
|
|
117
155
|
|
|
118
156
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* `
|
|
157
|
+
* Validate the VALUES of a review_layers/oneshot_review_layers item (item
|
|
158
|
+
* keys themselves are already checked by the caller against
|
|
159
|
+
* REVIEW_LAYER_KEYS). A typo in `tiers` (`"crtical"`), an empty `tiers`
|
|
160
|
+
* array, a non-positive `max_calls`, or a `tiers` entry on an
|
|
161
|
+
* `oneshot_review_layers` item (that step ignores tiers — it always runs)
|
|
162
|
+
* all load silently today: deepMerge applies them, nothing catches the
|
|
163
|
+
* mistake, the layer misbehaves. Warnings, same shape as unknown keys —
|
|
164
|
+
* fail-open, the snapshot is still written.
|
|
165
|
+
* @param {object} item
|
|
166
|
+
* @param {string} wfPath
|
|
167
|
+
* @param {boolean} isOneshot - true for an `oneshot_review_layers` item
|
|
168
|
+
* @returns {Array<{path: string, kind: 'invalid', detail: string}>}
|
|
169
|
+
*/
|
|
170
|
+
function findInvalidReviewLayerValues(item, wfPath, isOneshot) {
|
|
171
|
+
const invalid = [];
|
|
172
|
+
if ('tiers' in item) {
|
|
173
|
+
if (isOneshot) {
|
|
174
|
+
invalid.push({
|
|
175
|
+
path: `${wfPath}[].tiers`,
|
|
176
|
+
kind: 'invalid',
|
|
177
|
+
detail: 'oneshot_review_layers always runs regardless of tier — tiers has no effect here',
|
|
178
|
+
});
|
|
179
|
+
} else if (!Array.isArray(item.tiers)) {
|
|
180
|
+
invalid.push({
|
|
181
|
+
path: `${wfPath}[].tiers`,
|
|
182
|
+
kind: 'invalid',
|
|
183
|
+
detail: `expected an array of "small"/"normal"/"critical", got ${JSON.stringify(item.tiers)}`,
|
|
184
|
+
});
|
|
185
|
+
} else if (item.tiers.length === 0) {
|
|
186
|
+
invalid.push({
|
|
187
|
+
path: `${wfPath}[].tiers`,
|
|
188
|
+
kind: 'invalid',
|
|
189
|
+
detail: 'empty — the layer would never run on any tier',
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
for (const tier of item.tiers) {
|
|
193
|
+
if (!VALID_REVIEW_TIERS.has(tier)) {
|
|
194
|
+
invalid.push({
|
|
195
|
+
path: `${wfPath}[].tiers[]`,
|
|
196
|
+
kind: 'invalid',
|
|
197
|
+
detail: `invalid tier "${tier}" — expected one of "small", "normal", "critical"`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if ('max_calls' in item && !(Number.isInteger(item.max_calls) && item.max_calls > 0)) {
|
|
204
|
+
invalid.push({
|
|
205
|
+
path: `${wfPath}[].max_calls`,
|
|
206
|
+
kind: 'invalid',
|
|
207
|
+
detail: `expected a positive integer, got ${JSON.stringify(item.max_calls)}`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return invalid;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Walk a loaded TOML layer and collect unknown/invalid entries: unknown
|
|
215
|
+
* TOP-level tables/scalars (anything outside `workflow`, `agent`, `include`)
|
|
216
|
+
* plus the `[workflow]` fields and array-of-table item keys/values.
|
|
217
|
+
* `[agent]` is prose the LLM reads, not a code-consumed schema, so its keys
|
|
218
|
+
* are open by design; `include` is validated by resolveIncludes.
|
|
124
219
|
* @param {object} data - a single layer, as parsed (never a merged tree)
|
|
125
|
-
* @returns {string
|
|
220
|
+
* @returns {Array<{path: string, kind: 'unknown'|'invalid', detail?: string}>} caller prefixes with layer info
|
|
126
221
|
*/
|
|
127
222
|
function findUnknownKeys(data) {
|
|
128
223
|
const unknown = [];
|
|
129
224
|
if (!isPlainObject(data)) return unknown;
|
|
130
225
|
for (const [key, value] of Object.entries(data)) {
|
|
131
226
|
if (!TOP_LEVEL_KEYS.has(key)) {
|
|
132
|
-
unknown.push(key);
|
|
227
|
+
unknown.push({ path: key, kind: 'unknown' });
|
|
133
228
|
continue;
|
|
134
229
|
}
|
|
135
230
|
if (key === 'workflow' && isPlainObject(value)) unknown.push(...findUnknownWorkflowKeys(value));
|
|
@@ -140,9 +235,12 @@ function findUnknownKeys(data) {
|
|
|
140
235
|
module.exports = {
|
|
141
236
|
findUnknownKeys,
|
|
142
237
|
findUnknownWorkflowKeys,
|
|
238
|
+
findInvalidReviewLayerValues,
|
|
239
|
+
isArrayOfStrings,
|
|
143
240
|
WORKFLOW_KEYS,
|
|
144
241
|
PARTY_MEMBER_KEYS,
|
|
145
242
|
PARTY_GROUP_KEYS,
|
|
146
243
|
REVIEW_LAYER_KEYS,
|
|
244
|
+
VALID_REVIEW_TIERS,
|
|
147
245
|
TOP_LEVEL_KEYS,
|
|
148
246
|
};
|