pi-gauntlet 4.0.0 → 4.1.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/CHANGELOG.md +23 -0
- package/README.md +3 -3
- package/extensions/phase-tracker.ts +46 -19
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +1 -8
- package/skills/roasting-the-spec/SKILL.md +1 -6
- package/skills/subagent-driven-development/SKILL.md +3 -3
- package/skills/verification-before-completion/reference/conformance-check.md +10 -9
- package/skills/verification-before-completion/reference/settings-precedence.md +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v4.1.0 - 2026-07-02
|
|
4
|
+
|
|
5
|
+
- **`closureReview` now resolves repo-local first.** The verify-step skills
|
|
6
|
+
(`subagent-driven-development`, `verification-before-completion/reference/conformance-check.md`)
|
|
7
|
+
resolve `piGauntlet.closureReview.model`/`maxFixRounds` from a repo's
|
|
8
|
+
`.pi/settings.json` over the agent preset, **whole-object** - matching how the
|
|
9
|
+
`phase-tracker.ts` guard already reads `pi.settings` (pi replaces the whole
|
|
10
|
+
`closureReview` block when a repo defines it). Previously they read the preset
|
|
11
|
+
only, so a repo-local `closureReview.model` was silently ignored and the
|
|
12
|
+
conformance gate ran on the parent's model.
|
|
13
|
+
- **New shared reference `skills/verification-before-completion/reference/settings-precedence.md`.**
|
|
14
|
+
States the two-file source order and the whole-object merge rule once; `specCouncil`
|
|
15
|
+
and `closureReview` both resolve the same way and both point at it. `roasting-the-spec`
|
|
16
|
+
and `brainstorming` replace their duplicated lookup prose with a pointer.
|
|
17
|
+
- **Closure guard upgraded from presence-check to match-check.** A
|
|
18
|
+
`conformance-reviewer` dispatch that omits `model:` is still blocked; one whose
|
|
19
|
+
`model:` differs from the configured `closureReview.model` now gets a non-blocking
|
|
20
|
+
warning appended to the result. Gated by `closureReview.enforce` as before.
|
|
21
|
+
|
|
22
|
+
## v4.0.1 - 2026-07-02
|
|
23
|
+
|
|
24
|
+
No content change. Smoke-tests the tag-triggered CI publish path (OIDC trusted publishing to npm) added in v4.0.0's release machinery commit.
|
|
25
|
+
|
|
3
26
|
## v4.0.0 - 2026-07-02
|
|
4
27
|
|
|
5
28
|
**Breaking: public identity.** First public npm release. The package is renamed and re-homed for its own identity as a diverged reinterpretation of obra/superpowers (via coctostan/pi-superpowers-plus); see the README `Lineage` section. No skill, agent, or extension *behavior* changes in this release - it is a rename + credits + publish-readiness pass. Consumers must update their `.pi/settings.json` and any override file.
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ Unset → provider default thinking for that model. `conformance-reviewer` and t
|
|
|
162
162
|
|
|
163
163
|
### Conformance gate model
|
|
164
164
|
|
|
165
|
-
`conformance-reviewer` ships without a `model:` in its frontmatter — like the spec-council personas, its model is supplied per preset so each profile points the last correctness gate at the strongest reasoning model its providers can reach. The verify-step skills
|
|
165
|
+
`conformance-reviewer` ships without a `model:` in its frontmatter — like the spec-council personas, its model is supplied per preset so each profile points the last correctness gate at the strongest reasoning model its providers can reach. The verify-step skills resolve `piGauntlet.closureReview.model` **repo-local first** (a repo's `.pi/settings.json` overrides the preset whole-object - defining `closureReview` there replaces the preset's entire block, so set every leaf you need together) and inject it **call-site** on the conformance dispatch (the same mechanism the spec-council chair uses). Add it to each preset's `settings.json` (or a repo's `.pi/settings.json` to override per repo):
|
|
166
166
|
|
|
167
167
|
```json
|
|
168
168
|
{
|
|
@@ -174,7 +174,7 @@ Unset → provider default thinking for that model. `conformance-reviewer` and t
|
|
|
174
174
|
|
|
175
175
|
Frontmatter pins `thinking: xhigh` and `defaultContext: fresh` (the gate always runs cold, with max reasoning) and `thinking` is not call-site overridable, so the config supplies only `model`. If `closureReview.model` is unset the dispatch omits `model:` and the gate inherits the parent's model; if the configured model is unreachable it retries once inherited.
|
|
176
176
|
|
|
177
|
-
When `closureReview.model` **is** set, the phase-tracker
|
|
177
|
+
When `closureReview.model` **is** set, the phase-tracker match-checks call-site injection: a `subagent` dispatch of `conformance-reviewer` that omits `model:` is **blocked at tool-call time** (before it runs) so the gate can never silently degrade to the parent's builder model, and a dispatch whose `model:` **differs** from the configured value gets a non-blocking **warning** appended to the result (drift is surfaced, not blocked). The documented one-retry fallback still works - pass an explicit model and it runs (with a warning if it differs). Disabling `closureReview.enforce` disables this guard too.
|
|
178
178
|
|
|
179
179
|
`closureReview.enforce` (default `true`) controls the phase-tracker gate that
|
|
180
180
|
blocks `complete verify` until the conformance-reviewer has run; set `false` to
|
|
@@ -202,7 +202,7 @@ If you want to know what's in each persona before using it, see [`agents/`](./ag
|
|
|
202
202
|
- `members` (required) — roster of `provider/model` strings; council size = array length, one critique per model. Empty or absent → the council never runs; brainstorming falls back to a single fresh-`worker` critique (scope + ambiguity, auto-applied).
|
|
203
203
|
- `chair` (optional) — model for the consolidating synthesizer; defaults to the inherited model when omitted.
|
|
204
204
|
|
|
205
|
-
Rosters
|
|
205
|
+
Rosters resolve **repo-local first**: a repo's `.pi/settings.json` overrides the preset (whole-object — the first file that defines `specCouncil` wins), otherwise each pi profile (`agent`, `agent.anthropic`, `agent.bedrock`, …) reads its own `settings.json`. List only models the resolving config's providers can reach. The two personas it dispatches — `spec-council-member` and `spec-council-synthesizer` — are model-free; their model is injected per task from this config.
|
|
206
206
|
|
|
207
207
|
## Extensions
|
|
208
208
|
|
|
@@ -98,19 +98,21 @@ const branchBlockReason = (phase: Phase): string =>
|
|
|
98
98
|
"To override, set piGauntlet.flowGuards.enforce: false.";
|
|
99
99
|
|
|
100
100
|
// Closure-review model guard: when piGauntlet.closureReview.model is configured,
|
|
101
|
-
// a conformance-reviewer dispatch
|
|
102
|
-
// model-free, so a bare omission silently inherits the parent session's
|
|
103
|
-
// defeating the point of an independent closing gate
|
|
104
|
-
// single / tasks / chain / parallel dispatch shapes and
|
|
105
|
-
// entry
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
// a conformance-reviewer dispatch should inject that model call-site. The persona
|
|
102
|
+
// ships model-free, so a bare omission silently inherits the parent session's
|
|
103
|
+
// builder model - defeating the point of an independent closing gate. Walk the
|
|
104
|
+
// single / tasks / chain / parallel dispatch shapes and collect the model of every
|
|
105
|
+
// conformance-reviewer entry (undefined = absent/empty/non-string). A bare omission
|
|
106
|
+
// is BLOCKED; an explicit model that differs from the configured one is WARNED
|
|
107
|
+
// (non-blocking), preserving the documented retry-with-fallback hatch while
|
|
108
|
+
// surfacing drift.
|
|
109
|
+
const conformanceModels = (input: unknown): (string | undefined)[] => {
|
|
110
|
+
const models: (string | undefined)[] = [];
|
|
111
|
+
const norm = (m: unknown) => (typeof m === "string" && m.trim() ? m.trim() : undefined);
|
|
110
112
|
const collect = (node: unknown): void => {
|
|
111
113
|
if (!node || typeof node !== "object") return;
|
|
112
114
|
const o = node as Record<string, unknown>;
|
|
113
|
-
if (
|
|
115
|
+
if (o.agent === "conformance-reviewer") models.push(norm(o.model));
|
|
114
116
|
for (const key of ["tasks", "chain", "parallel"] as const) {
|
|
115
117
|
const v = o[key];
|
|
116
118
|
if (Array.isArray(v)) for (const item of v) collect(item);
|
|
@@ -118,19 +120,28 @@ const conformanceEntriesMissingModel = (input: unknown): boolean => {
|
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
122
|
collect(input);
|
|
121
|
-
return
|
|
123
|
+
return models;
|
|
122
124
|
};
|
|
123
125
|
|
|
124
|
-
const closureModelBlockReason = (model: string): string =>
|
|
125
|
-
`Blocked: conformance-reviewer
|
|
126
|
-
`piGauntlet.closureReview.model is set to "${model}".\n` +
|
|
126
|
+
const closureModelBlockReason = (model: string, missing: number, total: number): string =>
|
|
127
|
+
`Blocked: ${missing} of ${total} conformance-reviewer ${total === 1 ? "dispatch" : "entries"} ` +
|
|
128
|
+
`omitted a model while piGauntlet.closureReview.model is set to "${model}".\n` +
|
|
127
129
|
`The persona ships model-free, so a bare omission silently inherits this session's ` +
|
|
128
130
|
`builder model - the closing gate would then run on the same model that built the work, ` +
|
|
129
|
-
`not the independent one the preset
|
|
130
|
-
`
|
|
131
|
-
`
|
|
131
|
+
`not the independent one configured (repo .pi/settings.json, else the preset). Re-dispatch ` +
|
|
132
|
+
`with model: "${model}" injected call-site on every conformance-reviewer entry. If that ` +
|
|
133
|
+
`model is unreachable, pass an explicit fallback model (the documented one-retry escape ` +
|
|
134
|
+
`hatch) - only a bare omission is blocked.\n` +
|
|
132
135
|
`To disable this gate, set piGauntlet.closureReview.enforce: false.`;
|
|
133
136
|
|
|
137
|
+
const closureModelMismatchWarning = (configured: string, mismatched: string[]): string =>
|
|
138
|
+
`⚠️ conformance-reviewer dispatched with ${mismatched.length === 1 ? "model" : "models"} ` +
|
|
139
|
+
`${mismatched.map((m) => `"${m}"`).join(", ")} while ` +
|
|
140
|
+
`piGauntlet.closureReview.model is set to "${configured}".\n` +
|
|
141
|
+
`If this is the documented one-retry fallback (the configured model was ` +
|
|
142
|
+
`unreachable), ignore this. Otherwise the closing gate is running on an ` +
|
|
143
|
+
`unintended model - re-dispatch with model: "${configured}".`;
|
|
144
|
+
|
|
134
145
|
const brainstormWriteWarning = (specDirs: string[]): string =>
|
|
135
146
|
"⚠️ Writing outside the spec directory during the brainstorm phase.\n" +
|
|
136
147
|
`Brainstorming may only edit the spec under ${specDirs.join(", ")}. Implementation\n` +
|
|
@@ -317,8 +328,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
317
328
|
// Only execution-mode dispatches carry a model; management/control modes
|
|
318
329
|
// (action: list/get/create/update/delete/status/...) execute nothing, so skip them.
|
|
319
330
|
const hasAction = !!(event.input as { action?: unknown })?.action;
|
|
320
|
-
if (model && !hasAction
|
|
321
|
-
|
|
331
|
+
if (model && !hasAction) {
|
|
332
|
+
const configured = model.trim();
|
|
333
|
+
const models = conformanceModels(event.input);
|
|
334
|
+
if (models.length > 0) {
|
|
335
|
+
const missing = models.filter((m) => m === undefined).length;
|
|
336
|
+
if (missing > 0) {
|
|
337
|
+
return { block: true, reason: closureModelBlockReason(configured, missing, models.length) };
|
|
338
|
+
}
|
|
339
|
+
const mismatched = [...new Set((models as string[]).filter((m) => m !== configured))];
|
|
340
|
+
if (mismatched.length > 0) {
|
|
341
|
+
pendingGuardWarnings.set(event.toolCallId, closureModelMismatchWarning(configured, mismatched));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
322
344
|
}
|
|
323
345
|
}
|
|
324
346
|
|
|
@@ -377,6 +399,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
377
399
|
pi.on("tool_result", async (event) => {
|
|
378
400
|
if (event.toolName === "subagent") {
|
|
379
401
|
if (qualifiesAsClosureDispatch(event.details)) conformanceDispatched = true;
|
|
402
|
+
const warning = pendingGuardWarnings.get(event.toolCallId);
|
|
403
|
+
if (warning) {
|
|
404
|
+
pendingGuardWarnings.delete(event.toolCallId);
|
|
405
|
+
return { content: [{ type: "text" as const, text: warning }, ...event.content] };
|
|
406
|
+
}
|
|
380
407
|
return undefined;
|
|
381
408
|
}
|
|
382
409
|
|
package/package.json
CHANGED
|
@@ -215,14 +215,7 @@ After the critique pass returns, scan it for load-bearing external references be
|
|
|
215
215
|
|
|
216
216
|
## Spec Council (Optional)
|
|
217
217
|
|
|
218
|
-
After the inline lint and before the user review gate, **brainstorming owns the critique-pass gate**: resolve `piGauntlet.specCouncil`
|
|
219
|
-
|
|
220
|
-
Lookup order (first file that **defines** `specCouncil` wins — do not merge across files):
|
|
221
|
-
|
|
222
|
-
1. `<repo-root>/.pi/settings.json` — repo root from `git rev-parse --show-toplevel` (the worktree root inside a worktree). A repo that defines `specCouncil` overrides the preset, even with empty `members` (explicit "no council here").
|
|
223
|
-
2. `$PI_CODING_AGENT_DIR/settings.json` — agent preset; consulted only when the repo file does not define `specCouncil`.
|
|
224
|
-
|
|
225
|
-
Both files may contain comments — read them, don't strict-parse. **Expand `$PI_CODING_AGENT_DIR`; never substitute a hardcoded project path for it** (reading the repo `.pi/settings.json` as if it were the preset file is the classic miss — repo-local and preset are different files). **When `members` is non-empty, the council *is* the critique pass — invoke `/skill:roasting-the-spec` automatically (no offer, no prompt).** When it is absent or empty, run the fresh-`worker` critique instead (see [Spec Self-Review](#spec-self-review-before-user-review-gate)); if `specCouncil` is present but malformed, emit one warning line and fall back to the worker. Approved council edits (or the worker's in-place fixes) are applied to the spec and ride in the same worktree commit as the rest of this skill's output.
|
|
218
|
+
After the inline lint and before the user review gate, **brainstorming owns the critique-pass gate**: resolve `piGauntlet.specCouncil` **repo-local first** — the repo's `.pi/settings.json` overrides the agent preset, **whole-object** (the first file that defines `specCouncil` wins; an empty `members` there is an explicit "no council here"). Full source order and mechanics: `verification-before-completion/reference/settings-precedence.md`. **Expand `$PI_CODING_AGENT_DIR`; never substitute a hardcoded project path for it.** **When `members` is non-empty, the council *is* the critique pass — invoke `/skill:roasting-the-spec` automatically (no offer, no prompt).** When it is absent or empty, run the fresh-`worker` critique instead (see [Spec Self-Review](#spec-self-review-before-user-review-gate)); if `specCouncil` is present but malformed, emit one warning line and fall back to the worker. Approved council edits (or the worker's in-place fixes) are applied to the spec and ride in the same worktree commit as the rest of this skill's output.
|
|
226
219
|
|
|
227
220
|
## User Review Gate
|
|
228
221
|
|
|
@@ -24,12 +24,7 @@ This skill may read anything and edit **only** the spec under `doc/specs/`. It d
|
|
|
24
24
|
|
|
25
25
|
## Configuration and gating
|
|
26
26
|
|
|
27
|
-
Resolve `piGauntlet.specCouncil`
|
|
28
|
-
|
|
29
|
-
1. `<repo-root>/.pi/settings.json` (repo root = `git rev-parse --show-toplevel`, which inside a worktree is the worktree root). **If this file defines `specCouncil`, it wins** — even an empty `members` here is an explicit "no council for this repo".
|
|
30
|
-
2. `$PI_CODING_AGENT_DIR/settings.json` (agent preset) — consulted **only** when the repo file does not define `specCouncil` at all.
|
|
31
|
-
|
|
32
|
-
The first file that defines the key wins; do not merge member lists across files. Do not hardcode a project path in place of `$PI_CODING_AGENT_DIR` — expand the env var. The resolved config looks like:
|
|
27
|
+
Resolve `piGauntlet.specCouncil` **repo-local first** — the repo's `.pi/settings.json` overrides the agent preset, **whole-object** (the first file that defines `specCouncil` wins outright; an empty `members` there is an explicit "no council for this repo"). Full source order, mechanics, and the per-key granularity rule live in `verification-before-completion/reference/settings-precedence.md`. Do not hardcode a path in place of `$PI_CODING_AGENT_DIR` — expand the env var. The resolved config looks like:
|
|
33
28
|
|
|
34
29
|
```json
|
|
35
30
|
{
|
|
@@ -87,7 +87,7 @@ Pi-subagents accepts a per-task `model` override. Use it.
|
|
|
87
87
|
| Spec review | Default | Reads diff + spec, mechanical comparison |
|
|
88
88
|
| Code-quality review | Most capable | Judgment call on naming, design, complexity |
|
|
89
89
|
| Final reviewer | Most capable | Whole-PR-scope review |
|
|
90
|
-
| Conformance / closure | Most capable | Whole-deliverable-vs-origin intent gate (`conformance-reviewer`; model from `piGauntlet.closureReview.model`, injected call-site) |
|
|
90
|
+
| Conformance / closure | Most capable | Whole-deliverable-vs-origin intent gate (`conformance-reviewer`; model from `piGauntlet.closureReview.model`, resolved repo-first whole-object, injected call-site) |
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
93
|
subagent({
|
|
@@ -112,8 +112,8 @@ subagent({ agent: "spec-reviewer", task: "<diff range + spec excerpt + ask: does
|
|
|
112
112
|
subagent({ agent: "code-reviewer", task: "<diff range + ask: production-ready?>" })
|
|
113
113
|
|
|
114
114
|
// closing-loop conformance (origin vs deliverable) — its OWN dispatch, never fused with code quality
|
|
115
|
-
// model from piGauntlet.closureReview.model (
|
|
116
|
-
subagent({ agent: "conformance-reviewer", model: /* piGauntlet.closureReview.model
|
|
115
|
+
// model from piGauntlet.closureReview.model — resolve repo-local .pi/settings.json over the preset, whole-object (see verification-before-completion/reference/settings-precedence.md); omit to inherit
|
|
116
|
+
subagent({ agent: "conformance-reviewer", model: /* piGauntlet.closureReview.model, repo-first per settings-precedence.md, else omit to inherit */, task: "<spec path + verbatim original prompt + full diff vs main; per conformance-check.md>" })
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
Prompt templates live alongside this SKILL.md:
|
|
@@ -36,14 +36,14 @@ whole-PR code-quality review. Fusing the two subordinates intent-coverage to a
|
|
|
36
36
|
code-quality system prompt and compresses the conformance result to an
|
|
37
37
|
afterthought. Code quality is one dispatch; conformance is another.
|
|
38
38
|
|
|
39
|
-
The persona ships model-free.
|
|
40
|
-
|
|
41
|
-
inject it **call-site** on the dispatch
|
|
42
|
-
parent's model) — the same mechanism the
|
|
43
|
-
model is unreachable, retry once with the
|
|
44
|
-
|
|
45
|
-
`thinking` stays frontmatter-pinned at
|
|
46
|
-
the config supplies only `model`.
|
|
39
|
+
The persona ships model-free. Resolve `piGauntlet.closureReview.model` **repo-local
|
|
40
|
+
first** — the repo's `.pi/settings.json` overrides the agent preset, whole-object (see
|
|
41
|
+
sibling `settings-precedence.md`) — and inject it **call-site** on the dispatch
|
|
42
|
+
(`model:` if set, else omit to inherit the parent's model) — the same mechanism the
|
|
43
|
+
spec-council chair uses. If the configured model is unreachable, retry once with the
|
|
44
|
+
inherited model. Point it at the strongest reasoning model the resolved config can
|
|
45
|
+
reach — this is the last correctness gate. `thinking` stays frontmatter-pinned at
|
|
46
|
+
`xhigh` and is not call-site overridable, so the config supplies only `model`.
|
|
47
47
|
|
|
48
48
|
Self-checking in the main session is the fallback when delegation isn't possible.
|
|
49
49
|
|
|
@@ -197,7 +197,8 @@ still-open with its prior verdict, or introducing `Gn+1`.
|
|
|
197
197
|
|
|
198
198
|
- The re-audit dispatch carries the **same call-site `model:` injection** as the initial
|
|
199
199
|
audit (when `piGauntlet.closureReview.model` is set, the phase-tracker closure guard
|
|
200
|
-
|
|
200
|
+
blocks a `conformance-reviewer` dispatch that omits `model:`, and warns — non-blocking —
|
|
201
|
+
on one whose model differs from the configured value).
|
|
201
202
|
- New or still-open gaps within the cap re-enter the menu above.
|
|
202
203
|
- **Cap: read `piGauntlet.closureReview.maxFixRounds`** (default `2`; missing/non-integer
|
|
203
204
|
→ `2`; `< 0` → `0`). `0` = audit-only: `GAPS` renders an accept/rescope-only menu and any
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Settings precedence: repo-local first
|
|
2
|
+
|
|
3
|
+
How to resolve a `piGauntlet.<key>` settings value that both a repo and an agent
|
|
4
|
+
preset may define. Both the spec-critique gate (`specCouncil`) and the conformance
|
|
5
|
+
gate (`closureReview`) use this rule; it is stated once here.
|
|
6
|
+
|
|
7
|
+
## Source order (both keys)
|
|
8
|
+
|
|
9
|
+
Read two files, **repo-local first**:
|
|
10
|
+
|
|
11
|
+
1. `<repo-root>/.pi/settings.json` - repo root from `git rev-parse --show-toplevel`
|
|
12
|
+
(inside a worktree this is the worktree root).
|
|
13
|
+
2. `$PI_CODING_AGENT_DIR/settings.json` - the agent preset.
|
|
14
|
+
|
|
15
|
+
Mechanics for both files: they may contain comments - read them, do not pipe
|
|
16
|
+
through a strict JSON parser. Expand `$PI_CODING_AGENT_DIR`; never substitute a
|
|
17
|
+
hardcoded path for it (reading the repo `.pi/settings.json` as if it were the
|
|
18
|
+
preset is the classic miss - they are different files). On a malformed repo-local
|
|
19
|
+
file, the reading agent emits one warning line in its response and falls back to
|
|
20
|
+
the preset.
|
|
21
|
+
|
|
22
|
+
## Merge granularity (same for both keys): whole-object
|
|
23
|
+
|
|
24
|
+
Both `specCouncil` and `closureReview` resolve **whole-object**: if the repo file
|
|
25
|
+
defines the key at all, that definition **replaces** the preset's entirely; the two
|
|
26
|
+
are never merged leaf-by-leaf. If the repo file does not define the key, the
|
|
27
|
+
preset's value is used unchanged.
|
|
28
|
+
|
|
29
|
+
This is not a pi-gauntlet convention - it is exactly what pi's own settings merge
|
|
30
|
+
does. `deepMergeSettings` (pi's `SettingsManager`) merges only the **top-level**
|
|
31
|
+
Settings keys (of which `piGauntlet` is one) by a one-level spread
|
|
32
|
+
`{ ...preset.piGauntlet, ...repo.piGauntlet }`. That spread replaces each
|
|
33
|
+
**second-level** key (`closureReview`, `specCouncil`, `flowGuards`) wholesale when
|
|
34
|
+
the repo defines it; it does **not** recurse into that key's leaves. So the
|
|
35
|
+
`phase-tracker.ts` closure guard (which reads `pi.settings.piGauntlet.closureReview.model`)
|
|
36
|
+
and the skills (which read repo-first, whole-object) resolve the same value.
|
|
37
|
+
|
|
38
|
+
**Caveat - partial definitions drop siblings.** Because the replace is
|
|
39
|
+
whole-object, a repo file that sets only *one* leaf of a key silently drops the
|
|
40
|
+
preset's other leaves for that key. A repo `closureReview: { "model": "..." }` with
|
|
41
|
+
no `enforce`/`maxFixRounds` makes those fall back to their code defaults
|
|
42
|
+
(`enforce` true, `maxFixRounds` 2), **not** to the preset's values. Define every
|
|
43
|
+
leaf you care about together in the file that owns the key. (Sibling keys like
|
|
44
|
+
`specCouncil` are unaffected - only the key the repo redefines is replaced.)
|