fix(fleet): additive hook-event merge and gated-composition acceptance reads
Integration adjudication (fred, W-F1): the general arrays-replace merge rule conflicts with the gap-7 base/overlay split — base and lease overlay share the PreToolUse and Stop events, so replace semantics would silently drop the base QA hooks from every gated seat. Ruling: hook event arrays directly under the top-level hooks key concatenate (base first); all other arrays keep replace semantics; null tombstones still delete an event. - mutator-gate acceptance now asserts lease wiring against the COMPOSED gated settings (base + lease-overlay via the launcher's own merge), matching the post-split contract. - fleet subcommand canary gains the intended new 'agent' surface from T3. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
This commit is contained in:
co-authored by
Claude Fable 5
parent
0fdcfa0ff4
commit
92e790ae9d
@@ -253,9 +253,12 @@ function cloneValue(value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
type MergeContext = 'root' | 'hooks' | 'nested';
|
||||
|
||||
function mergeObject(
|
||||
lower: Record<string, unknown>,
|
||||
higher: Record<string, unknown>,
|
||||
context: MergeContext = 'nested',
|
||||
): Record<string, unknown> {
|
||||
const result = cloneValue(lower) as Record<string, unknown>;
|
||||
for (const [key, highValue] of Object.entries(higher)) {
|
||||
@@ -264,20 +267,36 @@ function mergeObject(
|
||||
continue;
|
||||
}
|
||||
const lowValue = result[key];
|
||||
if (context === 'hooks' && Array.isArray(lowValue) && Array.isArray(highValue)) {
|
||||
result[key] = [...(lowValue as unknown[]), ...(cloneValue(highValue) as unknown[])];
|
||||
continue;
|
||||
}
|
||||
result[key] =
|
||||
isPlainObject(lowValue) && isPlainObject(highValue)
|
||||
? mergeObject(lowValue, highValue)
|
||||
? mergeObject(
|
||||
lowValue,
|
||||
highValue,
|
||||
context === 'root' && key === 'hooks' ? 'hooks' : 'nested',
|
||||
)
|
||||
: cloneValue(highValue);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Deep object merge. Scalars and arrays replace; null in a higher layer deletes. */
|
||||
/**
|
||||
* Deep object merge. Scalars and arrays replace; null in a higher layer
|
||||
* deletes. Exception: hook event arrays directly under the top-level `hooks`
|
||||
* key concatenate (base entries first), so an overlay ADDS gating without
|
||||
* erasing the base QA hooks that share an event — replacing them would make
|
||||
* the gap-7 base/overlay split unimplementable without duplicating base
|
||||
* hooks inside the lease overlay. Removing an event entirely still works via
|
||||
* the null tombstone.
|
||||
*/
|
||||
export function deepMergeSettings(
|
||||
...layers: ReadonlyArray<Record<string, unknown> | undefined>
|
||||
): Record<string, unknown> {
|
||||
return layers.reduce<Record<string, unknown>>(
|
||||
(merged, layer) => (layer === undefined ? merged : mergeObject(merged, layer)),
|
||||
(merged, layer) => (layer === undefined ? merged : mergeObject(merged, layer, 'root')),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user