pi-gauntlet 4.2.3 → 4.3.1
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 +51 -0
- package/README.md +2 -0
- package/agents/spec-summarizer.md +2 -1
- package/extensions/lib/gauntlet-settings-loader.ts +23 -0
- package/extensions/lib/gauntlet-settings.test.ts +118 -0
- package/extensions/lib/gauntlet-settings.ts +124 -0
- package/extensions/phase-tracker.ts +71 -26
- package/extensions/verify-before-ship.ts +24 -17
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +32 -11
- package/skills/roasting-the-spec/SKILL.md +3 -3
- package/skills/subagent-driven-development/SKILL.md +11 -6
- package/skills/verification-before-completion/reference/conformance-check.md +12 -11
- package/skills/verification-before-completion/reference/settings-precedence.md +43 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v4.3.1 - 2026-07-06
|
|
4
|
+
|
|
5
|
+
Fix spec-summary pruning at the brainstorming user-review gate. The gate's
|
|
6
|
+
`spec-summarizer` dispatch now writes to a temp file via `outputMode: "file-only"`
|
|
7
|
+
and the main loop reads it back verbatim, so the ~9KB summary survives pi-condense
|
|
8
|
+
instead of being pruned to a paraphrase before render.
|
|
9
|
+
|
|
10
|
+
- **`brainstorming` gate transport:** dispatch `spec-summarizer` with an absolute
|
|
11
|
+
temp-dir `output:` path + `outputMode: "file-only"`, `Read` it back as the last
|
|
12
|
+
content-producing tool call before the gate, and degrade to a one-line note on a
|
|
13
|
+
stub / truncated / missing file. The temp path is outside the worktree, so it is
|
|
14
|
+
never committed.
|
|
15
|
+
- **`spec-summarizer` persona:** stays `tools: read`; a directive tells it not to
|
|
16
|
+
attempt the injected "write your findings" instruction (the harness persists its
|
|
17
|
+
final text for it), dodging the documented stub/stall failure on weaker models.
|
|
18
|
+
Adds a length-proportionality directive so summaries scale with spec size.
|
|
19
|
+
- **`AGENTS.md`:** `outputMode` added to the call-site-overridable knobs list.
|
|
20
|
+
|
|
21
|
+
Restore the per-task spec gate in `subagent-driven-development` Parallel-Wave mode.
|
|
22
|
+
Wave-mode step 3 reviewed spec compliance inline instead of dispatching the
|
|
23
|
+
`spec-reviewer` agent (sequential mode already dispatched it), silently collapsing
|
|
24
|
+
the gate.
|
|
25
|
+
|
|
26
|
+
- **`subagent-driven-development` wave step 3:** fan out one `spec-reviewer` per
|
|
27
|
+
accepted patch (fresh context, `cwd` worktree, diff-based); pin the two-stage
|
|
28
|
+
line to "dispatched spec-reviewer - not inline"; add a Red Flag.
|
|
29
|
+
|
|
30
|
+
## v4.3.0 - 2026-07-06
|
|
31
|
+
|
|
32
|
+
DRY gauntlet settings resolution. All `piGauntlet.*` settings reads now route
|
|
33
|
+
through one shared resolver (`extensions/lib/gauntlet-settings.ts`), consumed by
|
|
34
|
+
a new `gauntlet_setting` tool (for skills) and by the extensions directly.
|
|
35
|
+
|
|
36
|
+
- **New `gauntlet_setting` tool** (registered by `phase-tracker`, gauntlet-internal):
|
|
37
|
+
returns the resolved `specCouncil` or `closureReview` value from pi's merged
|
|
38
|
+
settings. Five skill call sites (`brainstorming`, `roasting-the-spec`,
|
|
39
|
+
`subagent-driven-development`, `conformance-check`, `settings-precedence`)
|
|
40
|
+
replace their hand-rolled two-file bash/prose merge with this tool. This fixes
|
|
41
|
+
an observed failure where the repo-only read fell back to the fresh `worker`
|
|
42
|
+
critique and silently skipped a configured spec council.
|
|
43
|
+
- **Behavior change (latent-bug fix):** extensions previously read `pi.settings`,
|
|
44
|
+
which does not exist on the extension API - so every `closureReview`,
|
|
45
|
+
`flowGuards`, and `verifyBeforeShip` config read was dead (always `{}`) and
|
|
46
|
+
consumer overrides were silently ignored. These settings now take effect for
|
|
47
|
+
the first time. Most visible: `closureReview.model` now actually pins the
|
|
48
|
+
conformance-gate model (and the phase-tracker guard can block a mismatched
|
|
49
|
+
dispatch). If you set any of these keys expecting the prior no-op, review them.
|
|
50
|
+
- `scripts/ci.mjs` now type-checks `extensions/lib/`, runs the resolver unit
|
|
51
|
+
tests, asserts the lib files are packed, and enforces a no-`pi.settings`
|
|
52
|
+
invariant.
|
|
53
|
+
|
|
3
54
|
## v4.2.3 - 2026-07-05
|
|
4
55
|
|
|
5
56
|
Branding, funding, and gallery preview. No behavior change.
|
package/README.md
CHANGED
|
@@ -232,6 +232,8 @@ A tool, not a hook. Skills call `phase_tracker({ action: "start" | "complete" |
|
|
|
232
232
|
|
|
233
233
|
Distinct from `plan-tracker`: `phase-tracker` answers "what stage of the workflow am I in?"; `plan-tracker` answers "which task within the current stage am I on?"
|
|
234
234
|
|
|
235
|
+
**`gauntlet_setting` tool.** `phase-tracker` also registers `gauntlet_setting({ key: "specCouncil" | "closureReview" })`, a gauntlet-internal tool through which skills resolve merged `piGauntlet.*` settings (repo `.pi/settings.json` over the agent preset, via pi's own `SettingsManager`). It returns the resolved value as a JSON block in the tool result — `specCouncil` yields the council-vs-worker verdict, `closureReview` yields the conformance-gate `model`/`enforce`/`maxFixRounds`. It introduces no new settings key. Every `piGauntlet.*` read — the skills via this tool, both extensions directly — routes through one shared helper (`extensions/lib/gauntlet-settings*.ts`); no code reads `pi.settings` by hand.
|
|
236
|
+
|
|
235
237
|
**Closure-review gate.** `complete verify` is rejected unless a successful
|
|
236
238
|
`conformance-reviewer` dispatch (a `subagent` result whose `results[]` contains
|
|
237
239
|
`agent: "conformance-reviewer"` with `exitCode: 0`) has been observed since the
|
|
@@ -19,6 +19,7 @@ Your output is judged on whether a busy supervisor can decide from it alone. A t
|
|
|
19
19
|
|
|
20
20
|
Optimize for "what does a supervisor need to approve **this** spec." The list below is a recommended checklist, not a rigid template:
|
|
21
21
|
|
|
22
|
+
- **Scale length to the spec.** The summary is proportional to the spec's size and complexity - a short or simple spec gets a short summary. Do not expand every recommended section to full depth to look thorough; the summary should be a fraction of the spec, not a near-copy of it. This is proportionality, **not** aggressive compression - never drop a decision-relevant point, rejected-alternative, risk, or gap-footer entry to hit a length target. When in doubt, keep the point and cut the words around it.
|
|
22
23
|
- **Omit any section that is empty.** A bugfix has no new endpoint; a refactor has no algorithm. Write nothing for an empty section - never "N/A" or filler.
|
|
23
24
|
- **Order decision-layer-first** (problem -> decisions -> scope -> risk), then the descriptive layer, so the reader can stop early once confident.
|
|
24
25
|
- **Add a section the spec demands** if it carries decision-relevant content none of the below captures.
|
|
@@ -39,4 +40,4 @@ Recommended sections:
|
|
|
39
40
|
|
|
40
41
|
Tight, human-readable, no obvious statements. If the topic is complex, an example with explanation beats prose.
|
|
41
42
|
|
|
42
|
-
Output the summary as your final text response.
|
|
43
|
+
Output the summary as your final text response. You have no write tool. If your task instructs you to write your findings to a file path, do **not** attempt to write, create, or edit any file and do **not** treat the inability to write as a failure - just emit the full summary as your final text response. The harness persists that response to the requested path for you.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Isolates the pi runtime import so the pure resolver module stays importable by
|
|
2
|
+
// ci.mjs unit tests. Only pi-loaded extensions import this file.
|
|
3
|
+
import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { mergeGauntlet, type PiGauntlet } from "./gauntlet-settings.ts";
|
|
5
|
+
|
|
6
|
+
export interface LoadedGauntlet {
|
|
7
|
+
gauntlet: PiGauntlet;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Reads the preset (agentDir/settings.json) and repo (cwd/.pi/settings.json)
|
|
12
|
+
// layers via pi's own SettingsManager and returns the whole-object second-level
|
|
13
|
+
// merge (repo over preset). SettingsManager never throws on a bad file - it
|
|
14
|
+
// substitutes {} for that layer and records the error, surfaced here via errors[]
|
|
15
|
+
// so callers can report a degraded read instead of failing silent.
|
|
16
|
+
export function loadGauntletSettings(cwd: string, agentDir: string = getAgentDir()): LoadedGauntlet {
|
|
17
|
+
const sm = SettingsManager.create(cwd, agentDir);
|
|
18
|
+
const preset = sm.getGlobalSettings() as { piGauntlet?: Record<string, unknown> };
|
|
19
|
+
const repo = sm.getProjectSettings() as { piGauntlet?: Record<string, unknown> };
|
|
20
|
+
const gauntlet = mergeGauntlet(preset?.piGauntlet, repo?.piGauntlet);
|
|
21
|
+
const errors = sm.drainErrors().map((e) => `${e.scope}: ${e.error.message}`);
|
|
22
|
+
return { gauntlet, errors };
|
|
23
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
mergeGauntlet,
|
|
5
|
+
resolveSpecCouncil,
|
|
6
|
+
resolveClosureReview,
|
|
7
|
+
resolveFlowGuards,
|
|
8
|
+
resolveVerifyBeforeShip,
|
|
9
|
+
settingsErrorWarning,
|
|
10
|
+
type PiGauntlet,
|
|
11
|
+
} from "./gauntlet-settings.ts";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TEST_COMMANDS = ["make ci", "pytest"];
|
|
14
|
+
|
|
15
|
+
test("mergeGauntlet: repo key replaces preset key whole-object", () => {
|
|
16
|
+
const preset = { specCouncil: { members: ["a"], chair: "c" }, closureReview: { model: "m" } };
|
|
17
|
+
const repo = { specCouncil: { members: ["b"] } };
|
|
18
|
+
const merged = mergeGauntlet(preset, repo);
|
|
19
|
+
assert.deepEqual(merged.specCouncil, { members: ["b"] });
|
|
20
|
+
assert.deepEqual(merged.closureReview, { model: "m" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("mergeGauntlet: undefined layers -> {}", () => {
|
|
24
|
+
assert.deepEqual(mergeGauntlet(undefined, undefined), {});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("specCouncil: non-empty string array -> council", () => {
|
|
28
|
+
const r = resolveSpecCouncil({ specCouncil: { members: ["p/m1", " p/m2 "], chair: "p/c" } });
|
|
29
|
+
assert.equal(r.verdict, "council");
|
|
30
|
+
assert.deepEqual(r.members, ["p/m1", "p/m2"]);
|
|
31
|
+
assert.equal(r.chair, "p/c");
|
|
32
|
+
assert.equal(r.malformed, false);
|
|
33
|
+
assert.equal(r.warning, undefined);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("specCouncil: absent -> worker, not malformed", () => {
|
|
37
|
+
const r = resolveSpecCouncil({});
|
|
38
|
+
assert.equal(r.verdict, "worker");
|
|
39
|
+
assert.deepEqual(r.members, []);
|
|
40
|
+
assert.equal(r.malformed, false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("specCouncil: empty array -> worker, not malformed", () => {
|
|
44
|
+
const r = resolveSpecCouncil({ specCouncil: { members: [] } });
|
|
45
|
+
assert.equal(r.verdict, "worker");
|
|
46
|
+
assert.equal(r.malformed, false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("specCouncil: non-array members -> worker + malformed + warning", () => {
|
|
50
|
+
const r = resolveSpecCouncil({ specCouncil: { members: "p/m" as unknown as string[] } });
|
|
51
|
+
assert.equal(r.verdict, "worker");
|
|
52
|
+
assert.equal(r.malformed, true);
|
|
53
|
+
assert.ok(r.warning);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("specCouncil: entry not a non-empty string -> worker + malformed", () => {
|
|
57
|
+
const r = resolveSpecCouncil({ specCouncil: { members: ["p/m", ""] } });
|
|
58
|
+
assert.equal(r.verdict, "worker");
|
|
59
|
+
assert.equal(r.malformed, true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("specCouncil: chair echoed in worker path", () => {
|
|
63
|
+
const r = resolveSpecCouncil({ specCouncil: { members: [], chair: "p/c" } });
|
|
64
|
+
assert.equal(r.verdict, "worker");
|
|
65
|
+
assert.equal(r.chair, "p/c");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("specCouncil: non-string chair does NOT downgrade a valid members verdict", () => {
|
|
69
|
+
const r = resolveSpecCouncil({ specCouncil: { members: ["p/m"], chair: 5 as unknown as string } });
|
|
70
|
+
assert.equal(r.verdict, "council");
|
|
71
|
+
assert.equal(r.chair, undefined);
|
|
72
|
+
assert.equal(r.malformed, true);
|
|
73
|
+
assert.ok(r.warning);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("closureReview: model present/non-string/absent", () => {
|
|
77
|
+
assert.equal(resolveClosureReview({ closureReview: { model: " m " } }).model, "m");
|
|
78
|
+
assert.equal(resolveClosureReview({ closureReview: { model: 5 as unknown as string } }).model, undefined);
|
|
79
|
+
assert.equal(resolveClosureReview({}).model, undefined);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("closureReview: enforce default true; false only when explicitly false", () => {
|
|
83
|
+
assert.equal(resolveClosureReview({}).enforce, true);
|
|
84
|
+
assert.equal(resolveClosureReview({ closureReview: { enforce: false } }).enforce, false);
|
|
85
|
+
assert.equal(resolveClosureReview({ closureReview: { enforce: true } }).enforce, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("closureReview: maxFixRounds default 2, <0 -> 0, non-int -> 2", () => {
|
|
89
|
+
assert.equal(resolveClosureReview({}).maxFixRounds, 2);
|
|
90
|
+
assert.equal(resolveClosureReview({ closureReview: { maxFixRounds: 5 } }).maxFixRounds, 5);
|
|
91
|
+
assert.equal(resolveClosureReview({ closureReview: { maxFixRounds: -3 } }).maxFixRounds, 0);
|
|
92
|
+
assert.equal(resolveClosureReview({ closureReview: { maxFixRounds: 1.5 } }).maxFixRounds, 2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("flowGuards: defaults + overrides", () => {
|
|
96
|
+
assert.deepEqual(resolveFlowGuards({}), { enforce: true, specDirs: ["doc/specs"] });
|
|
97
|
+
assert.equal(resolveFlowGuards({ flowGuards: { enforce: false } }).enforce, false);
|
|
98
|
+
assert.deepEqual(resolveFlowGuards({ flowGuards: { specDirs: ["a/b"] } }).specDirs, ["a/b"]);
|
|
99
|
+
assert.deepEqual(resolveFlowGuards({ flowGuards: { specDirs: [] } }).specDirs, ["doc/specs"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("settingsErrorWarning: includes prefix and joined errors", () => {
|
|
103
|
+
const w = settingsErrorWarning(["bad json", "missing field"]);
|
|
104
|
+
assert.match(w, /gauntlet settings load error \(using defaults\)/);
|
|
105
|
+
assert.ok(w.includes("bad json; missing field"));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("verifyBeforeShip: default vs override", () => {
|
|
109
|
+
const d = resolveVerifyBeforeShip({}, DEFAULT_TEST_COMMANDS);
|
|
110
|
+
assert.deepEqual(d.testCommands, DEFAULT_TEST_COMMANDS);
|
|
111
|
+
assert.equal(d.warningReference, undefined);
|
|
112
|
+
const o = resolveVerifyBeforeShip(
|
|
113
|
+
{ verifyBeforeShip: { testCommands: ["x"], warningReference: "doc/t.md" } },
|
|
114
|
+
DEFAULT_TEST_COMMANDS,
|
|
115
|
+
);
|
|
116
|
+
assert.deepEqual(o.testCommands, ["x"]);
|
|
117
|
+
assert.equal(o.warningReference, "doc/t.md");
|
|
118
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Pure gauntlet-settings resolvers. NO pi runtime import: this module is imported
|
|
2
|
+
// by ci.mjs unit tests (node --test) which run outside pi, where
|
|
3
|
+
// @earendil-works/pi-coding-agent is unresolvable. The loader
|
|
4
|
+
// (gauntlet-settings-loader.ts) owns the pi import.
|
|
5
|
+
|
|
6
|
+
export interface PiGauntlet {
|
|
7
|
+
specCouncil?: { members?: unknown; chair?: unknown };
|
|
8
|
+
closureReview?: { enforce?: unknown; model?: unknown; maxFixRounds?: unknown };
|
|
9
|
+
flowGuards?: { enforce?: unknown; specDirs?: unknown };
|
|
10
|
+
verifyBeforeShip?: { testCommands?: unknown; warningReference?: unknown };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Whole-object second-level merge: each piGauntlet key present in the repo layer
|
|
14
|
+
// replaces the preset's wholesale; keys absent from repo fall through to preset.
|
|
15
|
+
// Mirrors pi's own deepMergeSettings second-level spread (not exported).
|
|
16
|
+
export function mergeGauntlet(
|
|
17
|
+
preset: Record<string, unknown> | undefined,
|
|
18
|
+
repo: Record<string, unknown> | undefined,
|
|
19
|
+
): PiGauntlet {
|
|
20
|
+
return { ...(preset ?? {}), ...(repo ?? {}) } as PiGauntlet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nonEmptyString = (v: unknown): v is string => typeof v === "string" && v.trim().length > 0;
|
|
24
|
+
const joinWarn = (ws: string[]): string | undefined => (ws.length ? ws.join("; ") : undefined);
|
|
25
|
+
|
|
26
|
+
export interface SpecCouncilResolved {
|
|
27
|
+
verdict: "council" | "worker";
|
|
28
|
+
members: string[];
|
|
29
|
+
chair: string | undefined;
|
|
30
|
+
malformed: boolean;
|
|
31
|
+
warning: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveSpecCouncil(g: PiGauntlet): SpecCouncilResolved {
|
|
35
|
+
const sc = g.specCouncil;
|
|
36
|
+
const warnings: string[] = [];
|
|
37
|
+
let malformed = false;
|
|
38
|
+
|
|
39
|
+
let chair: string | undefined;
|
|
40
|
+
const rawChair = sc?.chair;
|
|
41
|
+
if (rawChair === undefined) {
|
|
42
|
+
chair = undefined;
|
|
43
|
+
} else if (nonEmptyString(rawChair)) {
|
|
44
|
+
chair = rawChair.trim();
|
|
45
|
+
} else {
|
|
46
|
+
chair = undefined;
|
|
47
|
+
malformed = true;
|
|
48
|
+
warnings.push("specCouncil.chair is not a non-empty string; ignoring it");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rawMembers = sc?.members;
|
|
52
|
+
const worker = (extra?: string): SpecCouncilResolved => {
|
|
53
|
+
if (extra) {
|
|
54
|
+
malformed = true;
|
|
55
|
+
warnings.push(extra);
|
|
56
|
+
}
|
|
57
|
+
return { verdict: "worker", members: [], chair, malformed, warning: joinWarn(warnings) };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (rawMembers === undefined) return worker();
|
|
61
|
+
if (!Array.isArray(rawMembers)) return worker("specCouncil.members is not an array; using the worker critique");
|
|
62
|
+
if (rawMembers.length === 0) return worker();
|
|
63
|
+
if (!rawMembers.every(nonEmptyString))
|
|
64
|
+
return worker("specCouncil.members has a non-string or empty entry; using the worker critique");
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
verdict: "council",
|
|
68
|
+
members: rawMembers.map((m) => (m as string).trim()),
|
|
69
|
+
chair,
|
|
70
|
+
malformed,
|
|
71
|
+
warning: joinWarn(warnings),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ClosureReviewResolved {
|
|
76
|
+
model: string | undefined;
|
|
77
|
+
enforce: boolean;
|
|
78
|
+
maxFixRounds: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveClosureReview(g: PiGauntlet): ClosureReviewResolved {
|
|
82
|
+
const cr = g.closureReview;
|
|
83
|
+
const model = nonEmptyString(cr?.model) ? cr!.model.trim() : undefined;
|
|
84
|
+
const enforce = cr?.enforce !== false;
|
|
85
|
+
const raw = cr?.maxFixRounds;
|
|
86
|
+
const maxFixRounds = typeof raw === "number" && Number.isInteger(raw) ? (raw < 0 ? 0 : raw) : 2;
|
|
87
|
+
return { model, enforce, maxFixRounds };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface FlowGuardsResolved {
|
|
91
|
+
enforce: boolean;
|
|
92
|
+
specDirs: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveFlowGuards(g: PiGauntlet): FlowGuardsResolved {
|
|
96
|
+
const fg = g.flowGuards;
|
|
97
|
+
const enforce = fg?.enforce !== false;
|
|
98
|
+
const rawDirs = fg?.specDirs;
|
|
99
|
+
const specDirs =
|
|
100
|
+
Array.isArray(rawDirs) && rawDirs.length > 0 && rawDirs.every(nonEmptyString)
|
|
101
|
+
? (rawDirs as string[]).map((d) => d.trim())
|
|
102
|
+
: ["doc/specs"];
|
|
103
|
+
return { enforce, specDirs };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface VerifyBeforeShipResolved {
|
|
107
|
+
testCommands: string[];
|
|
108
|
+
warningReference: string | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveVerifyBeforeShip(g: PiGauntlet, defaultTestCommands: string[]): VerifyBeforeShipResolved {
|
|
112
|
+
const vbs = g.verifyBeforeShip;
|
|
113
|
+
const rawCmds = vbs?.testCommands;
|
|
114
|
+
const testCommands =
|
|
115
|
+
Array.isArray(rawCmds) && rawCmds.length > 0 && rawCmds.every(nonEmptyString)
|
|
116
|
+
? (rawCmds as string[])
|
|
117
|
+
: defaultTestCommands;
|
|
118
|
+
const warningReference = nonEmptyString(vbs?.warningReference) ? vbs!.warningReference.trim() : undefined;
|
|
119
|
+
return { testCommands, warningReference };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function settingsErrorWarning(errors: string[]): string {
|
|
123
|
+
return `\u26a0\ufe0f gauntlet settings load error (using defaults): ${errors.join("; ")}`;
|
|
124
|
+
}
|
|
@@ -14,6 +14,13 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
14
14
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
16
16
|
import { type Static, Type } from "@sinclair/typebox";
|
|
17
|
+
import {
|
|
18
|
+
resolveClosureReview,
|
|
19
|
+
resolveFlowGuards,
|
|
20
|
+
resolveSpecCouncil,
|
|
21
|
+
settingsErrorWarning,
|
|
22
|
+
} from "./lib/gauntlet-settings.ts";
|
|
23
|
+
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
17
24
|
|
|
18
25
|
const PHASES = ["brainstorm", "plan", "implement", "verify", "ship"] as const;
|
|
19
26
|
type Phase = (typeof PHASES)[number];
|
|
@@ -32,19 +39,6 @@ interface PhaseTrackerDetails {
|
|
|
32
39
|
error?: string;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
type Settings = {
|
|
36
|
-
piGauntlet?: {
|
|
37
|
-
closureReview?: {
|
|
38
|
-
enforce?: boolean;
|
|
39
|
-
model?: string;
|
|
40
|
-
};
|
|
41
|
-
flowGuards?: {
|
|
42
|
-
enforce?: boolean;
|
|
43
|
-
specDirs?: string[];
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
|
|
48
42
|
// Qualification per spec "Qualifying dispatch": a successful conformance-reviewer
|
|
49
43
|
// child run in the result details (pi-cohort SingleResult: { agent, exitCode }).
|
|
50
44
|
// Management mode and async dispatches return results: [] and fail this check.
|
|
@@ -227,14 +221,6 @@ function formatStatus(phases: PhaseMap): string {
|
|
|
227
221
|
export default function (pi: ExtensionAPI) {
|
|
228
222
|
let phases: PhaseMap = emptyPhases();
|
|
229
223
|
let conformanceDispatched = false;
|
|
230
|
-
const closureEnforced = () =>
|
|
231
|
-
((pi.settings ?? {}) as Settings).piGauntlet?.closureReview?.enforce !== false;
|
|
232
|
-
const closureReviewModel = () =>
|
|
233
|
-
((pi.settings ?? {}) as Settings).piGauntlet?.closureReview?.model;
|
|
234
|
-
|
|
235
|
-
const flowGuardsCfg = () => ((pi.settings ?? {}) as Settings).piGauntlet?.flowGuards ?? {};
|
|
236
|
-
const flowGuardsEnforced = () => flowGuardsCfg().enforce !== false;
|
|
237
|
-
const specDirs = () => flowGuardsCfg().specDirs ?? ["doc/specs"];
|
|
238
224
|
|
|
239
225
|
// Warn-once-per-phase ledger; cleared on every phase transition and on reconstruct.
|
|
240
226
|
const firedGuards = new Map<string, boolean>();
|
|
@@ -321,7 +307,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
321
307
|
});
|
|
322
308
|
}
|
|
323
309
|
|
|
324
|
-
pi.on("tool_call", async (event) => {
|
|
310
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
311
|
+
// D3.b: one fresh settings read per event, lazily, only if a guard needs it.
|
|
312
|
+
const addGuardWarning = (id: string, text: string) => {
|
|
313
|
+
const prior = pendingGuardWarnings.get(id);
|
|
314
|
+
pendingGuardWarnings.set(id, prior ? prior + "\n\n" + text : text);
|
|
315
|
+
};
|
|
316
|
+
let gauntletCache: ReturnType<typeof loadGauntletSettings> | undefined;
|
|
317
|
+
const g = () => {
|
|
318
|
+
if (!gauntletCache) {
|
|
319
|
+
gauntletCache = loadGauntletSettings(ctx.cwd);
|
|
320
|
+
if (gauntletCache.errors.length > 0) addGuardWarning(event.toolCallId, settingsErrorWarning(gauntletCache.errors));
|
|
321
|
+
}
|
|
322
|
+
return gauntletCache.gauntlet;
|
|
323
|
+
};
|
|
324
|
+
const closureEnforced = () => resolveClosureReview(g()).enforce;
|
|
325
|
+
const closureReviewModel = () => resolveClosureReview(g()).model;
|
|
326
|
+
const flowGuardsEnforced = () => resolveFlowGuards(g()).enforce;
|
|
327
|
+
const specDirs = () => resolveFlowGuards(g()).specDirs;
|
|
328
|
+
|
|
325
329
|
// Closure-review model guard - independent of flowGuards, gated by closureReview.enforce.
|
|
326
330
|
if (event.toolName === "subagent" && closureEnforced()) {
|
|
327
331
|
const model = closureReviewModel();
|
|
@@ -338,12 +342,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
338
342
|
}
|
|
339
343
|
const mismatched = [...new Set((models as string[]).filter((m) => m !== configured))];
|
|
340
344
|
if (mismatched.length > 0) {
|
|
341
|
-
|
|
345
|
+
addGuardWarning(event.toolCallId, closureModelMismatchWarning(configured, mismatched));
|
|
342
346
|
}
|
|
343
347
|
}
|
|
344
348
|
}
|
|
345
349
|
}
|
|
346
350
|
|
|
351
|
+
// Flow guards apply only to write/edit/bash while a guard phase is active.
|
|
352
|
+
// Cheap in-memory gate BEFORE any settings load: phase state is in-memory, so an
|
|
353
|
+
// event no guard inspects (any read-only tool, or any tool in a dormant session)
|
|
354
|
+
// returns here without touching disk. Only genuinely guardable events pay for g().
|
|
355
|
+
const brainstormActive = phases.brainstorm.status === "in_progress";
|
|
356
|
+
const guardableWrite = (event.toolName === "write" || event.toolName === "edit") && brainstormActive;
|
|
357
|
+
const guardableBash = event.toolName === "bash" && activeGuardPhase() !== undefined;
|
|
358
|
+
if (!guardableWrite && !guardableBash) return undefined;
|
|
359
|
+
|
|
347
360
|
if (!flowGuardsEnforced()) return undefined;
|
|
348
361
|
|
|
349
362
|
// Guard 3 — write/edit outside the spec dir during brainstorm.
|
|
@@ -352,7 +365,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
352
365
|
const p = (event.input as { path?: unknown } | undefined)?.path;
|
|
353
366
|
if (typeof p !== "string" || pathInSpecDirs(p, specDirs())) return undefined;
|
|
354
367
|
firedGuards.set("brainstorm-write", true);
|
|
355
|
-
|
|
368
|
+
addGuardWarning(event.toolCallId, brainstormWriteWarning(specDirs()));
|
|
356
369
|
return undefined;
|
|
357
370
|
}
|
|
358
371
|
|
|
@@ -392,7 +405,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
405
|
}
|
|
393
406
|
}
|
|
394
407
|
|
|
395
|
-
if (warnings.length > 0)
|
|
408
|
+
if (warnings.length > 0) addGuardWarning(event.toolCallId, warnings.join("\n\n"));
|
|
396
409
|
return undefined;
|
|
397
410
|
});
|
|
398
411
|
|
|
@@ -427,6 +440,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
427
440
|
updateWidget(ctx);
|
|
428
441
|
});
|
|
429
442
|
|
|
443
|
+
const GauntletSettingParams = Type.Object({
|
|
444
|
+
key: StringEnum(["specCouncil", "closureReview"] as const, {
|
|
445
|
+
description: "Which gauntlet setting to resolve (merged repo-over-preset).",
|
|
446
|
+
}),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
pi.registerTool({
|
|
450
|
+
name: "gauntlet_setting",
|
|
451
|
+
label: "Gauntlet Setting",
|
|
452
|
+
description:
|
|
453
|
+
"Gauntlet-internal, invoked by skills: resolve a merged piGauntlet.* setting " +
|
|
454
|
+
"(repo .pi/settings.json over the agent preset). Returns the resolved value as a " +
|
|
455
|
+
"JSON block in the result content - specCouncil yields the council-vs-worker verdict, " +
|
|
456
|
+
"closureReview yields the conformance-gate model/enforce/maxFixRounds. Not for ad-hoc use.",
|
|
457
|
+
parameters: GauntletSettingParams,
|
|
458
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
459
|
+
const { gauntlet, errors } = loadGauntletSettings(ctx.cwd);
|
|
460
|
+
const payload =
|
|
461
|
+
params.key === "specCouncil"
|
|
462
|
+
? { key: "specCouncil" as const, ...resolveSpecCouncil(gauntlet), errors }
|
|
463
|
+
: { key: "closureReview" as const, ...resolveClosureReview(gauntlet), errors };
|
|
464
|
+
return {
|
|
465
|
+
content: [{ type: "text", text: "```json\n" + JSON.stringify(payload, null, 2) + "\n```" }],
|
|
466
|
+
details: payload,
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
430
471
|
pi.registerTool({
|
|
431
472
|
name: "phase_tracker",
|
|
432
473
|
label: "Phase Tracker",
|
|
@@ -504,7 +545,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
504
545
|
} as PhaseTrackerDetails,
|
|
505
546
|
};
|
|
506
547
|
}
|
|
507
|
-
if (
|
|
548
|
+
if (
|
|
549
|
+
params.phase === "verify" &&
|
|
550
|
+
resolveClosureReview(loadGauntletSettings(ctx.cwd).gauntlet).enforce &&
|
|
551
|
+
!conformanceDispatched
|
|
552
|
+
) {
|
|
508
553
|
return {
|
|
509
554
|
content: [{ type: "text", text: CLOSURE_GATE_ERROR }],
|
|
510
555
|
details: {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { resolveVerifyBeforeShip, settingsErrorWarning } from "./lib/gauntlet-settings.ts";
|
|
28
|
+
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
27
29
|
|
|
28
30
|
// Default verification entrypoints. Override via settings.
|
|
29
31
|
const DEFAULT_TEST_COMMANDS = [
|
|
@@ -38,18 +40,10 @@ const DEFAULT_TEST_COMMANDS = [
|
|
|
38
40
|
];
|
|
39
41
|
|
|
40
42
|
const SHIP_CMD = /\b(git\s+commit|git\s+push|gh\s+pr\s+create)\b/;
|
|
43
|
+
|
|
41
44
|
const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|rb|go|rs|java|swift|kt)$/;
|
|
42
45
|
const TEST_PATH = /(^|\/)(tests?|__tests__)\/|\.(test|spec)\.|_test\.(py|go|rb)$/;
|
|
43
46
|
|
|
44
|
-
type Settings = {
|
|
45
|
-
piGauntlet?: {
|
|
46
|
-
verifyBeforeShip?: {
|
|
47
|
-
testCommands?: string[];
|
|
48
|
-
warningReference?: string;
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
47
|
const isSourceWrite = (filePath: string | undefined): boolean => {
|
|
54
48
|
if (!filePath) return false;
|
|
55
49
|
return SOURCE_EXT.test(filePath) && !TEST_PATH.test(filePath);
|
|
@@ -72,12 +66,6 @@ const formatWarning = (command: string, testCommands: string[], reference: strin
|
|
|
72
66
|
};
|
|
73
67
|
|
|
74
68
|
export default function (pi: ExtensionAPI) {
|
|
75
|
-
const settings = (pi.settings ?? {}) as Settings;
|
|
76
|
-
const cfg = settings.piGauntlet?.verifyBeforeShip ?? {};
|
|
77
|
-
const testCommands = cfg.testCommands ?? DEFAULT_TEST_COMMANDS;
|
|
78
|
-
const testRegex = buildTestCmdRegex(testCommands);
|
|
79
|
-
const reference = cfg.warningReference;
|
|
80
|
-
|
|
81
69
|
let verified = false;
|
|
82
70
|
let pendingTestCallId: string | null = null;
|
|
83
71
|
const pendingShipWarnings = new Map<string, string>();
|
|
@@ -92,7 +80,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
92
80
|
return typeof input?.path === "string" ? input.path : undefined;
|
|
93
81
|
};
|
|
94
82
|
|
|
95
|
-
pi.on("tool_call", async (event) => {
|
|
83
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
96
84
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
97
85
|
if (isSourceWrite(getPath(event))) verified = false;
|
|
98
86
|
return undefined;
|
|
@@ -102,13 +90,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
90
|
const command = getCommand(event);
|
|
103
91
|
if (!command) return undefined;
|
|
104
92
|
|
|
93
|
+
const addShipWarning = (id: string, text: string) => {
|
|
94
|
+
const prior = pendingShipWarnings.get(id);
|
|
95
|
+
pendingShipWarnings.set(id, prior ? prior + "\n\n" + text : text);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const { gauntlet, errors } = loadGauntletSettings(ctx.cwd);
|
|
99
|
+
const { testCommands, warningReference } = resolveVerifyBeforeShip(gauntlet, DEFAULT_TEST_COMMANDS);
|
|
100
|
+
const settingsWarned = errors.length > 0 ? settingsErrorWarning(errors) : undefined;
|
|
101
|
+
let settingsErrShown = false;
|
|
102
|
+
const testRegex = buildTestCmdRegex(testCommands);
|
|
103
|
+
|
|
105
104
|
if (testRegex.test(command)) {
|
|
106
105
|
pendingTestCallId = event.toolCallId;
|
|
106
|
+
if (settingsWarned && !settingsErrShown) {
|
|
107
|
+
addShipWarning(event.toolCallId, settingsWarned);
|
|
108
|
+
settingsErrShown = true;
|
|
109
|
+
}
|
|
107
110
|
return undefined;
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
if (SHIP_CMD.test(command) && !verified) {
|
|
111
|
-
|
|
114
|
+
if (settingsWarned && !settingsErrShown) {
|
|
115
|
+
addShipWarning(event.toolCallId, settingsWarned);
|
|
116
|
+
settingsErrShown = true;
|
|
117
|
+
}
|
|
118
|
+
addShipWarning(event.toolCallId, formatWarning(command, testCommands, warningReference));
|
|
112
119
|
}
|
|
113
120
|
return undefined;
|
|
114
121
|
});
|
package/package.json
CHANGED
|
@@ -18,11 +18,13 @@ Identify the target project → set up an isolated worktree → understand curre
|
|
|
18
18
|
While this skill is active, do **not** take any implementation action until you have presented a design and the user has approved it. This applies to **every** change regardless of perceived simplicity. An implementation-heavy request ("test it end-to-end", "push a request to rabbit", "run the service") does not lift this gate — it just tells you what the spec must cover.
|
|
19
19
|
|
|
20
20
|
You **may**:
|
|
21
|
+
|
|
21
22
|
- Read code and docs
|
|
22
23
|
- Run the existing system to observe its **current** behaviour — this is research and feeds the spec (boot a local service, replay a sample request, capture a baseline classification, etc.)
|
|
23
24
|
- Write to the project's `doc/specs/` directory
|
|
24
25
|
|
|
25
26
|
You may **not**:
|
|
27
|
+
|
|
26
28
|
- Write or edit code outside `doc/specs/`
|
|
27
29
|
- Scaffold a project, or take any action that builds, deploys, or validates the **proposed change** — running new/edited code, exercising behaviour that doesn't exist yet, or "testing the fix" before there is one
|
|
28
30
|
- Run implementation skills (`/skill:test-driven-development`, `/skill:subagent-driven-development`, etc.)
|
|
@@ -38,11 +40,13 @@ This skill ends with a **written, user-reviewed spec inside a worktree**. Nothin
|
|
|
38
40
|
Work through the items below **in order**. This is your own checklist to follow, not a `plan_tracker` plan — brainstorming is open-ended exploration, and `plan_tracker` is execution-only (the implement phase). The terminal state is the user review gate; after approval the **only** next skill is `/skill:writing-plans`. Do not jump to implementation, and do not silently drop the critique pass.
|
|
39
41
|
|
|
40
42
|
1. **Start brainstorm tracking (fresh epoch)** — as the **first action on entry**, before reading code, setting up the worktree, or answering the user, reset **both** trackers and then start the phase. A new brainstorm is a new flow, so it owns a clean slate — clear any stale phases *and* tasks left in the session by earlier work:
|
|
43
|
+
|
|
41
44
|
```
|
|
42
45
|
phase_tracker({ action: "reset" }) // clears all phases
|
|
43
46
|
plan_tracker({ action: "clear" }) // clears all tasks
|
|
44
47
|
phase_tracker({ action: "start", phase: "brainstorm" })
|
|
45
48
|
```
|
|
49
|
+
|
|
46
50
|
Re-entering while a brainstorm is already in progress is safe: mid-brainstorm there are no tasks or later phases to lose, so the reset just re-establishes the same clean slate.
|
|
47
51
|
2. **Set up the worktree** — see [Worktree First](#worktree-first)
|
|
48
52
|
3. **Explore project context** — files, docs, recent commits, current behaviour
|
|
@@ -51,9 +55,9 @@ Work through the items below **in order**. This is your own checklist to follow,
|
|
|
51
55
|
6. **Present the design** — in sections, get approval after each
|
|
52
56
|
7. **Write the spec** — to `doc/specs/` (see [Filename Convention](#filename-convention))
|
|
53
57
|
8. **Spec self-review (lint)** — placeholder scan + internal consistency + documentation named, run inline
|
|
54
|
-
9. **Critique pass (auto-dispatched)** — scope + ambiguity; the spec council via `/skill:roasting-the-spec` when `
|
|
58
|
+
9. **Critique pass (auto-dispatched)** — scope + ambiguity; the spec council via `/skill:roasting-the-spec` when `gauntlet_setting` returns verdict `council`, else a fresh `worker` (see [Spec Council](#spec-council-optional))
|
|
55
59
|
10. **Re-run placeholder scan** — after the critique pass returns, first inline any `external-ref:` flags it raised (see [Spec Self-Review](#spec-self-review-before-user-review-gate)), then re-scan for placeholders its edits may have introduced; surface any ambiguity the worker could not safely resolve at the user gate
|
|
56
|
-
11. **Generate spec summary** — dispatch a fresh, spec-only `spec-summarizer` and render its
|
|
60
|
+
11. **Generate spec summary** — dispatch a fresh, spec-only `spec-summarizer` writing to an absolute temp-dir path via `outputMode: "file-only"`, then `Read` that file back as the **last content-producing** tool call before composing the gate and render its contents **verbatim** at the top of the gate message — do not paraphrase, condense, re-section, or rewrite it (see [User Review Gate](#user-review-gate)); this is part of the existing gate, not a new one
|
|
57
61
|
12. **User review gate** — user reviews the committed spec
|
|
58
62
|
13. **Transition** — only after approval, invoke `/skill:writing-plans`
|
|
59
63
|
|
|
@@ -64,6 +68,7 @@ Before brainstorming, identify the target project. The match drives the spec dir
|
|
|
64
68
|
Read the repo's top-level `AGENTS.md` (already in pi's context) and any service-level `AGENTS.md` for the area the user mentioned. If the project documents a routing table mapping subprojects to spec directories and verification commands, follow it. Otherwise rely on the conventions you find.
|
|
65
69
|
|
|
66
70
|
Detection order:
|
|
71
|
+
|
|
67
72
|
1. Issue tracker reference (Linear, Jira, GitHub Issues) → infer from labels, description, or mentioned file paths
|
|
68
73
|
2. cwd inside a service / package directory → use that area
|
|
69
74
|
3. Unclear → ask the developer
|
|
@@ -135,6 +140,7 @@ When sketching the design, prefer:
|
|
|
135
140
|
Sections of 200-300 words. Ask after each whether it looks right.
|
|
136
141
|
|
|
137
142
|
Cover at minimum:
|
|
143
|
+
|
|
138
144
|
- Architecture overview
|
|
139
145
|
- Components / responsibilities
|
|
140
146
|
- Data flow (or request flow)
|
|
@@ -203,7 +209,7 @@ After writing the spec to `<project>/doc/specs/<filename>.md` (per [Filename Con
|
|
|
203
209
|
|
|
204
210
|
The first three checks — **placeholder scan**, **internal consistency**, and **documentation named** — are the inline **lint**: run them here at the main loop and fix what they surface. The last two — **scope** and **ambiguity** — are **not** run inline; they are the **critique pass**, auto-dispatched (per [Spec Council](#spec-council-optional)):
|
|
205
211
|
|
|
206
|
-
- **Council
|
|
212
|
+
- **Council** (`gauntlet_setting({ key: "specCouncil" })` returns verdict `council`) → invoke `/skill:roasting-the-spec`; it runs the critique and proposes dispositions.
|
|
207
213
|
- **Otherwise** → dispatch one fresh `worker` that applies the scope + ambiguity checks and fixes them in place:
|
|
208
214
|
|
|
209
215
|
```
|
|
@@ -224,25 +230,39 @@ After the critique pass returns, scan it for load-bearing external references be
|
|
|
224
230
|
|
|
225
231
|
## Spec Council (Optional)
|
|
226
232
|
|
|
227
|
-
After the inline lint and before the user review gate, **brainstorming owns the critique-pass gate
|
|
233
|
+
After the inline lint and before the user review gate, **brainstorming owns the critique-pass gate**. Resolve the council with `gauntlet_setting({ key: "specCouncil" })` - the tool returns the merged (repo-over-preset) value as `{ verdict, members, chair, malformed, warning, errors }`. **Do not** hand-roll a settings read. When `verdict` is `"council"`, the council *is* the critique pass - invoke `/skill:roasting-the-spec` automatically (no offer, no prompt), passing `members`/`chair`. When `verdict` is `"worker"`, run the fresh-`worker` critique instead (see [Spec Self-Review](#spec-self-review-before-user-review-gate)). If `malformed` is true or `errors` is non-empty, emit the `warning`/error as one line, then branch strictly on `verdict` - `malformed` can accompany *either* verdict (e.g. a bad `chair` with valid `members` still returns `council`), so never infer the worker path from `malformed` alone. If `gauntlet_setting` is unavailable, stop and report - never fall back to a manual bash/JSON settings merge. Approved council edits (or the worker's in-place fixes) ride in the same worktree commit. The conceptual precedence rule lives in `verification-before-completion/reference/settings-precedence.md`.
|
|
228
234
|
|
|
229
235
|
## User Review Gate
|
|
230
236
|
|
|
231
237
|
After self-review (and council review, if configured) and after inlining any external-ref flags, dispatch the spec-only summarizer, then commit the spec on the worktree branch and stop. This is the **same** single human gate - the summary is folded into it, not a new gate.
|
|
232
238
|
|
|
233
|
-
|
|
239
|
+
Mint an absolute temp path outside the worktree (so it is never committed), then dispatch the summarizer on a fresh context, reading only the spec, writing to that path via file-only output (no `model:` - it inherits the main loop unless a preset sets `subagents.agentOverrides.spec-summarizer.model`):
|
|
234
240
|
|
|
241
|
+
```bash
|
|
242
|
+
SUMMARY_PATH=$(mktemp "${TMPDIR:-/tmp}/gauntlet-spec-summary.XXXXXX") # absolute, portable across GNU/BSD mktemp
|
|
235
243
|
```
|
|
236
|
-
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
subagent({ agent: "spec-summarizer", context: "fresh", cwd: "<abs worktree path, from git rev-parse --show-toplevel>",
|
|
247
|
+
output: "<SUMMARY_PATH>", outputMode: "file-only", task:
|
|
237
248
|
"Summarize the spec at <abs path to doc/specs/...> for the user review gate. Read ONLY that file." })
|
|
238
249
|
```
|
|
239
250
|
|
|
240
|
-
|
|
251
|
+
`<SUMMARY_PATH>` above is a placeholder in the dispatch object; it means substitute the value of the shell variable `$SUMMARY_PATH` set above. The steps below use `$SUMMARY_PATH` (the shell form) once the value is in hand.
|
|
252
|
+
|
|
253
|
+
Then commit the spec — this commit is **unconditional**: the summary is only a gate aid, so a degraded or missing summary never blocks it. Evaluate the summary in two stages (the **Degrade path** referenced in each is defined just below):
|
|
254
|
+
|
|
255
|
+
1. **From the dispatch tool result, before the `Read`.** If the result is **not** an `"Output saved to: <path> (<N> KB, <M> lines)"` reference (e.g. an exit-0 save error returns the full inline output plus an "Output file error" line — the prunable shape, no file to read), or the reference reports under ~500 bytes, or a size grossly disproportionate to the spec (under ~2% of its byte size), or over ~45 KB (the `Read` truncates at 50KB / 2000 lines, so a larger file cannot render whole) — skip the `Read` and take the degrade path. Use the reference's reported figures; do not re-derive them.
|
|
256
|
+
2. **The `Read` itself, as the last content-producing tool call before composing the gate.** `Read` `$SUMMARY_PATH` and paste its contents verbatim at the top of the gate. If the `Read` fails, returns 0 bytes, or reports truncation — take the degrade path. The `Read` must be last: pi-condense does not protect a `/tmp` read, so any turn boundary between the `Read` and the render lets the ~9KB read result be pruned, reproducing the bug.
|
|
257
|
+
|
|
258
|
+
**Degrade path** — reach the gate with a one-line "summary generation failed" note; never paraphrase from the file-only reference, never render a stub as the canonical summary.
|
|
259
|
+
|
|
260
|
+
Either way — summary rendered or degraded — then `rm "$SUMMARY_PATH"` (unconditional cleanup; harmless if the file was never created, since it lives outside the worktree under the OS temp dir).
|
|
241
261
|
|
|
242
|
-
Render the
|
|
262
|
+
Render the temp file's contents **verbatim** first — paste it as-is, do **not** paraphrase, condense, re-section, drop sections, or merge it with council output. "Fold into the gate" means *place it inside the gate message*, not *rewrite it*. After the verbatim block, append the commit confirmation, then — as their **own** adjacent lines, not edits to the summary — any council outcome, critique-pass-unresolved ambiguities, and every entry from the summarizer's gap/external-context footer (surface **all** of them, not just the top risk):
|
|
243
263
|
|
|
244
264
|
```
|
|
245
|
-
<spec-only summary from
|
|
265
|
+
<spec-only summary read back from the temp file — pasted verbatim, unedited>
|
|
246
266
|
|
|
247
267
|
Spec written and committed to <project>/doc/specs/<filename>.md (worktree: <path>).
|
|
248
268
|
|
|
@@ -253,7 +273,7 @@ Please review. Approve to proceed, or tell me what to change in the spec.
|
|
|
253
273
|
|
|
254
274
|
If you believe the summary needs correcting, do **not** silently rewrite it — re-dispatch the summarizer or note the discrepancy as an adjacent line beneath the verbatim block.
|
|
255
275
|
|
|
256
|
-
Wait for the user. On a change request, revise and re-present. On approval, proceed immediately to `/skill:writing-plans` with no further prompt — the plan and execution mode are mechanical derivatives, so the only human gate here is spec approval itself. Don't land the spec on `main`; it stays in the worktree and ships in the same squash commit as the implementation.
|
|
276
|
+
Wait for the user. On a change request, revise the spec and re-present — mint a **fresh** temp path for the re-dispatched summarizer (never reuse a prior round's path, so stale content can never be mistaken for the new summary). On approval, proceed immediately to `/skill:writing-plans` with no further prompt — the plan and execution mode are mechanical derivatives, so the only human gate here is spec approval itself. Don't land the spec on `main`; it stays in the worktree and ships in the same squash commit as the implementation.
|
|
257
277
|
|
|
258
278
|
After approval, mark the brainstorm phase complete:
|
|
259
279
|
|
|
@@ -278,7 +298,8 @@ phase_tracker({ action: "complete", phase: "brainstorm" })
|
|
|
278
298
|
- About to skip the critique pass (council if configured, else fresh worker)
|
|
279
299
|
- Critique dispatch (council or worker) failed to complete and you proceeded to the gate anyway
|
|
280
300
|
- About to reach the user gate without re-running the placeholder scan after the critique returned
|
|
281
|
-
- About to reach the user gate without rendering the spec-only summary (dispatch `spec-summarizer` first; a failed
|
|
301
|
+
- About to reach the user gate without rendering the spec-only summary (dispatch `spec-summarizer` to a temp file first; a failed/stub/truncated read degrades to a one-line note, it is not silently skipped)
|
|
302
|
+
- About to compose the gate message when the summary `Read` was not the last content-producing tool call before it (a following `rm` of the temp file is fine) — a turn boundary between the `Read` and the render lets pi-condense prune the ~9KB read result, reproducing the original bug
|
|
282
303
|
- About to present a paraphrased, condensed, or re-sectioned version of the summarizer's output instead of pasting its returned text verbatim — rewriting the summary counts as not rendering it
|
|
283
304
|
- About to run the scope or ambiguity checks inline yourself instead of dispatching them (those two are the critique pass, not the inline lint)
|
|
284
305
|
- About to skip the self-review pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: roasting-the-spec
|
|
3
|
-
description: Use after writing a spec, when a spec council is configured (piGauntlet.specCouncil
|
|
3
|
+
description: Use after writing a spec, when a spec council is configured (the resolved piGauntlet.specCouncil council, via the gauntlet_setting tool, repo settings over the preset). Auto-dispatched by /skill:brainstorming as the critique pass when members is non-empty (no longer offered). N members on different models critique in parallel, a neutral chair consolidates and adjudicates, the parent proposes dispositions, the user approves.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Roasting the Spec (Spec Council)
|
|
@@ -24,7 +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
|
-
|
|
27
|
+
The caller (`/skill:brainstorming`) already resolved the council via `gauntlet_setting({ key: "specCouncil" })` and dispatched this skill only when `verdict` was `"council"`, passing the resolved `members` and `chair`. This skill therefore receives `members`/`chair` from that resolved value and does **not** read settings files itself. The resolved config looks like:
|
|
28
28
|
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
@@ -40,7 +40,7 @@ Resolve `piGauntlet.specCouncil` **repo-local first** — the repo's `.pi/settin
|
|
|
40
40
|
- `members` — array of `provider/model` strings. Council size = array length.
|
|
41
41
|
- `chair` — optional model for the synthesizer; if omitted, the synthesizer inherits the parent's model.
|
|
42
42
|
|
|
43
|
-
brainstorming owns the gate: it
|
|
43
|
+
brainstorming owns the gate: it resolves this config via `gauntlet_setting`, emits any malformed-config warning, and decides whether to invoke this skill. This skill is dispatched **only after** brainstorming confirms `verdict` is `council`, so it runs the council unconditionally on entry — no offer, no numbered choice. Absent/empty/malformed config never reaches here (brainstorming runs the fresh-`worker` critique instead). Ownership of "should the council run" lives in brainstorming, not here.
|
|
44
44
|
|
|
45
45
|
## The council run
|
|
46
46
|
|
|
@@ -86,7 +86,7 @@ Pi-subagents accepts a per-task `model` override. Use it.
|
|
|
86
86
|
| Hard / novel / large surface | Most capable | New subsystem, complex algorithm, cross-service contract change |
|
|
87
87
|
| Spec review | Default | Reads diff + spec, mechanical comparison |
|
|
88
88
|
| Code-quality review | Most capable | Judgment call on naming, design, complexity |
|
|
89
|
-
| Conformance / closure | Most capable | Whole-deliverable-vs-origin intent gate (`conformance-reviewer`; model from `
|
|
89
|
+
| Conformance / closure | Most capable | Whole-deliverable-vs-origin intent gate (`conformance-reviewer`; model from `gauntlet_setting({ key: "closureReview" }).model`, injected call-site) |
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
subagent({
|
|
@@ -111,8 +111,9 @@ subagent({ agent: "spec-reviewer", task: "<diff range + spec excerpt + ask: does
|
|
|
111
111
|
subagent({ agent: "code-reviewer", task: "<diff range + ask: production-ready?>" })
|
|
112
112
|
|
|
113
113
|
// closing-loop conformance (origin vs deliverable) — its OWN dispatch, never fused with code quality
|
|
114
|
-
// model
|
|
115
|
-
|
|
114
|
+
// model: call gauntlet_setting({ key: "closureReview" }) first; use the returned model (omit model: if undefined to inherit) and maxFixRounds
|
|
115
|
+
// If gauntlet_setting is unavailable, stop and report - never fall back to a manual bash/JSON settings merge.
|
|
116
|
+
subagent({ agent: "conformance-reviewer", model: /* gauntlet_setting({ key: "closureReview" }).model, else omit to inherit */, task: "<spec path + verbatim original prompt + full diff vs main; per conformance-check.md>" })
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
Prompt templates live alongside this SKILL.md:
|
|
@@ -140,13 +141,13 @@ Auto-selected at handoff by `writing-plans` (any wave with ≥2 tasks) when the
|
|
|
140
141
|
|
|
141
142
|
1. **Independence check.** Parse the wave's tasks' `Files:` blocks; assert pairwise-disjoint (mechanical). Runtime-resource disjointness (DB/schema, port, fixture, external service, shared temp path) is not machine-checkable here — trust the plan's wave grouping, which `writing-plans`' D5 contract guarantees. Either kind of overlap → the wave is mis-grouped; run those tasks as sequential single-task waves and note it.
|
|
142
143
|
2. **Fan out.** One parallel dispatch (shape below): `implementer` per task, `context: "fresh"`, `worktree: true`. Each returns a status + a patch.
|
|
143
|
-
3. **Status + spec review per task.** Parse each `DONE`/`BLOCKED`/etc. (see [Implementer Status](#implementer-status))
|
|
144
|
+
3. **Status + spec review per task.** Parse each `DONE`/`BLOCKED`/etc. (see [Implementer Status](#implementer-status)) **first**. Then **dispatch a `spec-reviewer` per accepted patch** (`DONE`, or a `DONE_WITH_CONCERNS` you proceeded with) in one parallel fan-out — `context: "fresh"`, `cwd: <this worktree>`, **no `worktree` flag** (read-only) — each passed its task text, the returned **patch diff**, and the absolute spec path. Review is **diff-based**: the diff's hunks carry `file:line`, and test execution is not the reviewer's job here (the wave test gate in step 5 runs the suite). Inline verdicts are fine at normal wave sizes; large waves use `output:` + `outputMode: "file-only"` to keep verdicts out of your context. **Re-dispatch by cause:** `BLOCKED`/`NEEDS_CONTEXT` per the [Implementer Status](#implementer-status) matrix; a **spec gap** re-dispatches the implementer (fresh, `worktree: true`) carrying the prior patch + the reviewer's findings, the new patch superseding the old at step 4. Loop until accepted + spec ✅.
|
|
144
145
|
4. **Integrate.** `git apply` each task's patch sequentially onto HEAD. Apply fails = textual conflict → drop that task, finish the rest, re-run the dropped task sequentially on the updated HEAD.
|
|
145
146
|
5. **Test gate.** Run the suite on the integrated tree. Failure = semantic conflict or bug → re-run the offending task sequentially, else fix per [When a Subagent Fails](#when-a-subagent-fails).
|
|
146
147
|
6. **Quality review.** Code-quality review on the integrated wave diff; loop fixes to ✅.
|
|
147
148
|
7. **Commit the wave.** Leaves a clean tree; the next wave's children branch from this commit and so see the integrated work.
|
|
148
149
|
|
|
149
|
-
**Two-stage review is preserved:** spec review per task (pre-integration), quality review per wave (post-integration). Both gates required before the wave commits.
|
|
150
|
+
**Two-stage review is preserved:** spec review per task (pre-integration, dispatched `spec-reviewer` — not inline), quality review per wave (post-integration). Both gates required before the wave commits.
|
|
150
151
|
|
|
151
152
|
**Dependent context across waves:** wave N+1 tasks branch from a HEAD containing wave N, so they see the code; still forward wave N's task summaries into wave N+1 prompts.
|
|
152
153
|
|
|
@@ -179,6 +180,7 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
179
180
|
3. **After 2 failed attempts:** STOP. Report failure to the user. The task probably needs redesign.
|
|
180
181
|
|
|
181
182
|
**NEVER:**
|
|
183
|
+
|
|
182
184
|
- Write code yourself to "help" or "finish up"
|
|
183
185
|
- Fix the subagent's work inline — that pollutes your context and defeats fresh-subagent isolation
|
|
184
186
|
- Silently skip the failed task
|
|
@@ -188,7 +190,7 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
188
190
|
|
|
189
191
|
0. Call `phase_tracker({ action: "start", phase: "verify" })`. (The `implement` phase was started at execution start and auto-completes from `plan_tracker` once all tasks are done; this flow runs its own verify gate instead of `/skill:verification-before-completion`, so it must mark verify itself.)
|
|
190
192
|
1. **Run the whole-diff code review.** Dispatch `/skill:requesting-code-review` against the worktree's full diff vs `main` (already covered in [The Process](#the-process) step "After all tasks"). Address Critical and Moderate findings before handoff. (Consumers wanting an in-flow project-specific audit re-add it as an explicit step in `.pi/gauntlet-overrides.md`, or run `/self-audit` manually.)
|
|
191
|
-
2. **Close the loop — conformance check.** The review in step 1 is plan-vs-code (single-step); it inherits any requirement the plan already dropped. Before marking verify complete, dispatch a fresh-context **`conformance-reviewer`** — its **own** dispatch, never fused into the step-1 review — to confront the deliverable (code **and** docs) against the *origin* — the spec **and** the original prompt — per `verification-before-completion/reference/conformance-check.md`. Pass the spec path, the verbatim original prompt, and the full diff. On `GAPS`, do not auto-fix or auto-proceed: run the remediation loop in `verification-before-completion/reference/conformance-check.md` "When the check finds gaps" (disposition menu → isolated fix waves → bounded delta re-audit, capped by `
|
|
193
|
+
2. **Close the loop — conformance check.** The review in step 1 is plan-vs-code (single-step); it inherits any requirement the plan already dropped. Before marking verify complete, dispatch a fresh-context **`conformance-reviewer`** — its **own** dispatch, never fused into the step-1 review — to confront the deliverable (code **and** docs) against the *origin* — the spec **and** the original prompt — per `verification-before-completion/reference/conformance-check.md`. Pass the spec path, the verbatim original prompt, and the full diff. On `GAPS`, do not auto-fix or auto-proceed: run the remediation loop in `verification-before-completion/reference/conformance-check.md` "When the check finds gaps" (disposition menu → isolated fix waves → bounded delta re-audit, capped by `gauntlet_setting({ key: "closureReview" }).maxFixRounds`). Only when the verdict is `CONFORMS` (or every gap is dispositioned) call `phase_tracker({ action: "complete", phase: "verify" })`.
|
|
192
194
|
3. Summarize what was implemented (tasks completed, files changed, test counts, code-review verdict). Give the closing loop its **own section** — `Closure / conformance: CONFORMS` (or `GAPS` with each gap and its disposition) — so the user sees intent-fidelity as a first-class line before any finishing decision, not buried in the review verdict.
|
|
193
195
|
4. **Proceed to finishing — no confirmation prompt.** When the verdict is `CONFORMS` (or every gap is dispositioned), invoke `/skill:finishing-a-development-branch` immediately. Its Step 4 menu (squash / PR / keep / discard) is the human gate; a separate "ready to finish?" prompt only stacks a second stop in front of it. Open gaps are already owned by step 2, so nothing is left to decide here. Manual testing is a follow-up after the finishing choice (on `<base-branch>` after a squash-merge, or on the PR branch), never a reason to hold this gate.
|
|
194
196
|
|
|
@@ -201,6 +203,7 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
201
203
|
- Starting code-quality review before spec compliance is ✅
|
|
202
204
|
- Moving to next task with either review still showing issues
|
|
203
205
|
- Letting implementer self-review replace external review (both needed)
|
|
206
|
+
- Spec-reviewing wave patches inline instead of dispatching `spec-reviewer` per patch — sequential mode's step 3 dispatches it; wave mode must too
|
|
204
207
|
- Pausing to "check in" between tasks (continuous execution rule)
|
|
205
208
|
- Skipping the `Implementer Status` parse — treating every response as DONE
|
|
206
209
|
- Starting on main without explicit user consent
|
|
@@ -208,12 +211,14 @@ For the fan-out + worktree + patch-integration + conflict mechanics, see `dispat
|
|
|
208
211
|
## Integration
|
|
209
212
|
|
|
210
213
|
**Required workflow skills:**
|
|
214
|
+
|
|
211
215
|
- `/skill:using-git-worktrees` — set up isolation first (small changes can branch in place with user approval)
|
|
212
216
|
- `/skill:writing-plans` — creates the plan this skill executes
|
|
213
217
|
- `/skill:requesting-code-review` — review template for reviewer subagents
|
|
214
218
|
- `/skill:finishing-a-development-branch` — invoked automatically once the conformance verdict is `CONFORMS` (or all gaps dispositioned)
|
|
215
219
|
|
|
216
220
|
**Subagents follow by default:**
|
|
221
|
+
|
|
217
222
|
- TDD — runtime warnings on source-before-test. Implementer agents receive the three-scenario TDD instructions (new feature / modifying tested code / trivial) via agent profile and prompt template.
|
|
218
223
|
|
|
219
224
|
## Project overrides
|
|
@@ -41,14 +41,14 @@ whole-PR code-quality review. Fusing the two subordinates intent-coverage to a
|
|
|
41
41
|
code-quality system prompt and compresses the conformance result to an
|
|
42
42
|
afterthought. Code quality is one dispatch; conformance is another.
|
|
43
43
|
|
|
44
|
-
The persona ships model-free.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`
|
|
44
|
+
The persona ships model-free. Get the model from `gauntlet_setting({ key: "closureReview" }).model`.
|
|
45
|
+
If `gauntlet_setting` is unavailable, stop and report - never fall back to a manual bash/JSON
|
|
46
|
+
settings merge. Inject the model **call-site** on the dispatch (omit `model:` when it is `undefined` to inherit
|
|
47
|
+
the parent's model) — the same mechanism the spec-council chair uses. If the configured model
|
|
48
|
+
is unreachable, retry once with the inherited model. Point it at the strongest reasoning model
|
|
49
|
+
the resolved config can reach — this is the last correctness gate. `thinking` stays
|
|
50
|
+
frontmatter-pinned at `xhigh` and is not call-site overridable, so the config supplies only
|
|
51
|
+
`model`.
|
|
52
52
|
|
|
53
53
|
Self-checking in the main session is the fallback when delegation isn't possible.
|
|
54
54
|
|
|
@@ -205,9 +205,10 @@ still-open with its prior verdict, or introducing `Gn+1`.
|
|
|
205
205
|
blocks a `conformance-reviewer` dispatch that omits `model:`, and warns — non-blocking —
|
|
206
206
|
on one whose model differs from the configured value).
|
|
207
207
|
- New or still-open gaps within the cap re-enter the menu above.
|
|
208
|
-
- **Cap:
|
|
209
|
-
|
|
210
|
-
unresolved gap escalates instead of
|
|
208
|
+
- **Cap: `gauntlet_setting({ key: "closureReview" }).maxFixRounds`** (the tool applies the
|
|
209
|
+
default `2`, floors negatives at `0`, and coerces non-integers to `2`). `0` = audit-only:
|
|
210
|
+
`GAPS` renders an accept/rescope-only menu and any unresolved gap escalates instead of
|
|
211
|
+
dispatching a fix.
|
|
211
212
|
- **On non-convergence** (cap reached with open gaps): **escalate to human** with the
|
|
212
213
|
per-gap round-by-round verdict trail. No silent re-loop, no auto-ship.
|
|
213
214
|
|
|
@@ -1,44 +1,55 @@
|
|
|
1
1
|
# Settings precedence: repo-local first
|
|
2
2
|
|
|
3
|
-
How
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
How a `piGauntlet.<key>` value is resolved when both a repo and an agent preset
|
|
4
|
+
define it. Both the spec-critique gate (`specCouncil`) and the conformance gate
|
|
5
|
+
(`closureReview`) use this rule; it is stated once here.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## The rule
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Repo settings override the preset, **whole-object at the second level**:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
2. `$PI_CODING_AGENT_DIR/settings.json` - the agent preset.
|
|
11
|
+
- Preset: `$PI_CODING_AGENT_DIR/settings.json`.
|
|
12
|
+
- Repo: `<repo-root>/.pi/settings.json`.
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
14
|
+
If the repo file defines a `piGauntlet.<key>` at all, that definition **replaces**
|
|
15
|
+
the preset's for that key entirely - the two are never merged leaf-by-leaf. If the
|
|
16
|
+
repo file does not define the key, the preset's value is used unchanged. This is
|
|
17
|
+
exactly pi's own `deepMergeSettings` behaviour: it spreads the second-level keys
|
|
18
|
+
(`specCouncil`, `closureReview`, `flowGuards`, `verifyBeforeShip`) wholesale, and
|
|
19
|
+
does not recurse into their leaves.
|
|
37
20
|
|
|
38
21
|
**Caveat - partial definitions drop siblings.** Because the replace is
|
|
39
22
|
whole-object, a repo file that sets only *one* leaf of a key silently drops the
|
|
40
23
|
preset's other leaves for that key. A repo `closureReview: { "model": "..." }` with
|
|
41
24
|
no `enforce`/`maxFixRounds` makes those fall back to their code defaults
|
|
42
25
|
(`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
|
|
44
|
-
|
|
26
|
+
leaf you care about together in the file that owns the key. (Sibling keys are
|
|
27
|
+
unaffected - only the key the repo redefines is replaced.)
|
|
28
|
+
|
|
29
|
+
**Settings files are strict JSON.** pi's `SettingsManager` parses them with
|
|
30
|
+
`JSON.parse`; comments make the file invalid to pi itself, so never add them.
|
|
31
|
+
|
|
32
|
+
## The single operational path
|
|
33
|
+
|
|
34
|
+
Do **not** read or merge these files by hand. Resolution is centralized:
|
|
35
|
+
|
|
36
|
+
- **Skills** resolve `piGauntlet.*` via the `gauntlet_setting` tool (registered by
|
|
37
|
+
the `phase-tracker` extension): `gauntlet_setting({ key: "specCouncil" })` or
|
|
38
|
+
`gauntlet_setting({ key: "closureReview" })`. The tool returns the merged value
|
|
39
|
+
(repo over preset) in its result content. If the tool is unavailable, stop and
|
|
40
|
+
report - never fall back to a manual bash/JSON merge (that fallback is exactly
|
|
41
|
+
the failure mode this centralization removes).
|
|
42
|
+
- **Extensions** call `loadGauntletSettings(ctx.cwd)` from
|
|
43
|
+
`extensions/lib/gauntlet-settings-loader.ts`, then the matching resolver in
|
|
44
|
+
`extensions/lib/gauntlet-settings.ts`.
|
|
45
|
+
|
|
46
|
+
Both paths go through pi's own `SettingsManager`, so they resolve the same merged
|
|
47
|
+
value. The `phase-tracker.ts` closure guard reads via `loadGauntletSettings` +
|
|
48
|
+
`resolveClosureReview`, resolving the same value the tool returns.
|
|
49
|
+
|
|
50
|
+
## Scope: launch directory
|
|
51
|
+
|
|
52
|
+
The tool and loader read relative to pi's session/launch directory (`ctx.cwd`, the
|
|
53
|
+
primary checkout), not a mid-session worktree. A per-branch `.pi/settings.json`
|
|
54
|
+
that diverges from the launch checkout is not resolved and is an unsupported
|
|
55
|
+
workflow - keep `piGauntlet` settings at the repo root or in the preset.
|