pi-gauntlet 4.4.2 → 4.4.3
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 +20 -0
- package/README.md +3 -3
- package/extensions/lib/phase-tracker-helpers.test.ts +49 -0
- package/extensions/lib/phase-tracker-helpers.ts +40 -0
- package/extensions/phase-tracker.ts +22 -4
- package/package.json +1 -1
- package/skills/verification-before-completion/reference/conformance-check.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v4.4.3 - 2026-07-19
|
|
4
|
+
|
|
5
|
+
Brainstorming is the sole gauntlet entry point (#2): gate all three phase-tracker
|
|
6
|
+
enforcement surfaces (closure completion gate, closure-model guard, flow guards) on a
|
|
7
|
+
durable `gauntletEntered` marker so the gauntlet is dormant unless brainstorming started
|
|
8
|
+
the flow. A reflexive `phase_tracker start verify` on an ad-hoc one-liner no longer trips
|
|
9
|
+
the closure gate or forces a spurious conformance dispatch.
|
|
10
|
+
|
|
11
|
+
The marker is derived in `reconstructState` from the session branch (mirroring
|
|
12
|
+
`conformanceDispatched`: reset on reconstruct, threaded through replay, updated in the live
|
|
13
|
+
start/reset handlers via one `nextGauntletEntered` transition), so it survives pi-condense
|
|
14
|
+
pruning and `--session` resume. All three surfaces gate marker-first, so a dormant
|
|
15
|
+
out-of-flow session short-circuits before any settings load; in-flow behavior is unchanged
|
|
16
|
+
(`brainstormActive` implies `gauntletEntered`). New pure helpers `nextGauntletEntered` /
|
|
17
|
+
`closureGateBlocks` / `closureModelGuardApplies` / `flowGuardApplies` are unit-tested; the
|
|
18
|
+
two settings-dependent surfaces inline-match their predicate (the `markerGuardApplies`
|
|
19
|
+
convention) to preserve marker-first laziness. Docs (README, configuration.md, AGENTS.md,
|
|
20
|
+
conformance-check.md, the `phase_tracker` tool description) reworded: enforcement is opt-in
|
|
21
|
+
by brainstorming entry, not ambient.
|
|
22
|
+
|
|
3
23
|
## v4.4.2 - 2026-07-15
|
|
4
24
|
|
|
5
25
|
Condense the finish-time conformance disposition gate to a dense, human-readable
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Concretely, one change through the gauntlet:
|
|
|
38
38
|
1. You describe the change. **`brainstorming`** sets up an isolated worktree, explores the codebase, and turns your description into a written spec. A multi-model critique runs on it automatically. **You read and approve the spec - human gate 1.** No implementation code exists yet.
|
|
39
39
|
2. **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files.
|
|
40
40
|
3. **`subagent-driven-development`** executes the plan one task at a time, each in a fresh subagent, behind spec-compliance review then code-quality review. TDD-locked: red, green, refactor.
|
|
41
|
-
4. **verify**: a whole-diff code review, then the **conformance gate** - a subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized.
|
|
41
|
+
4. **verify**: a whole-diff code review, then the **conformance gate** - a subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a dense list - one line per decision, plain-language, with its recommended choice inline. Reply `1` to take every recommendation, or `2:` with per-item overrides; a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
|
|
42
42
|
5. **`finishing-a-development-branch`**: squash, PR, keep, or discard. **Human gate 2** - the only other decision you make.
|
|
43
43
|
|
|
44
44
|
```mermaid
|
|
@@ -66,13 +66,13 @@ pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
|
|
|
66
66
|
- **7 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `code-reviewer`, `spec-reviewer`, `conformance-reviewer`, `spec-summarizer`, `spec-council-member`, `spec-council-synthesizer`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
|
|
67
67
|
- **3 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you commit/push without a passing test run since your last edit. See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
|
|
68
68
|
|
|
69
|
-
pi-gauntlet is **opinionated**: every non-trivial change
|
|
69
|
+
pi-gauntlet is **opinionated**: every non-trivial change is *meant* to ride this one pipeline, entered through `brainstorming`. Enforcement is opt-in by entry, not ambient: once brainstorming starts a flow, the phase-tracker extension mechanically blocks a phase from closing before its gate runs. A change made *without* entering the flow (a typo, a formatting run, a dependency bump - see "When to use / when NOT to use") is not gated; the discipline of routing real work through the pipeline is a convention the tooling supports, not a trap it springs on every edit.
|
|
70
70
|
|
|
71
71
|
## Key concepts
|
|
72
72
|
|
|
73
73
|
| Term | Meaning |
|
|
74
74
|
| --- | --- |
|
|
75
|
-
| Gate | A machine-enforced checkpoint between phases (e.g. `complete verify` is blocked until conformance review has run). Not a suggestion. |
|
|
75
|
+
| Gate | A machine-enforced checkpoint between phases (e.g. within a brainstorming-entered flow, `complete verify` is blocked until conformance review has run). Not a suggestion. |
|
|
76
76
|
| Spec council | Multi-model critique of the spec before you see it (`roasting-the-spec`); falls back to a single-model critique if no council is configured. |
|
|
77
77
|
| Conformance gate | The closing check: does the delivered code + docs match your *original prompt*, not the derived plan? Compatible executable recommendations auto-fix first; anything still open renders as a dense one-line-per-decision list with each recommended choice inline. Reply `1` to accept all recommendations or `2:` with per-item overrides; a current `CONFORMS` / no-concerns handoff goes straight to branch options with no extra sign-off. |
|
|
78
78
|
| Wave | A batch of plan tasks that don't touch the same files, dispatched to implementers in parallel. |
|
|
@@ -3,6 +3,10 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import {
|
|
4
4
|
CONTEXT_DRAFT_MARKER,
|
|
5
5
|
checkSubstep,
|
|
6
|
+
closureGateBlocks,
|
|
7
|
+
closureModelGuardApplies,
|
|
8
|
+
flowGuardApplies,
|
|
9
|
+
nextGauntletEntered,
|
|
6
10
|
phaseLabel,
|
|
7
11
|
parseGitCommit,
|
|
8
12
|
resolveRepoDir,
|
|
@@ -118,3 +122,48 @@ test("markerGuardApplies: gated by flowGuards.enforce and brainstorm in_progress
|
|
|
118
122
|
assert.equal(markerGuardApplies(true, "in_progress"), true);
|
|
119
123
|
assert.equal(markerGuardApplies(true, "pending"), false);
|
|
120
124
|
});
|
|
125
|
+
|
|
126
|
+
test("nextGauntletEntered: arms only when a start makes brainstorm in_progress", () => {
|
|
127
|
+
assert.equal(nextGauntletEntered(false, "start", "in_progress"), true);
|
|
128
|
+
assert.equal(nextGauntletEntered(true, "start", "in_progress"), true); // re-arm idempotent
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("nextGauntletEntered: reset always disarms", () => {
|
|
132
|
+
assert.equal(nextGauntletEntered(true, "reset", "pending"), false);
|
|
133
|
+
assert.equal(nextGauntletEntered(true, "reset", "in_progress"), false);
|
|
134
|
+
assert.equal(nextGauntletEntered(false, "reset", "pending"), false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("nextGauntletEntered: marker survives downstream actions", () => {
|
|
138
|
+
assert.equal(nextGauntletEntered(true, "start", "complete"), true); // start plan/implement (brainstorm already complete)
|
|
139
|
+
assert.equal(nextGauntletEntered(true, "complete", "complete"), true);
|
|
140
|
+
assert.equal(nextGauntletEntered(true, "substep", "in_progress"), true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("nextGauntletEntered: a non-brainstorm start never arms a dormant flow", () => {
|
|
144
|
+
assert.equal(nextGauntletEntered(false, "start", "complete"), false); // cold start verify/implement
|
|
145
|
+
assert.equal(nextGauntletEntered(false, "start", "skipped"), false);
|
|
146
|
+
assert.equal(nextGauntletEntered(false, "complete", "pending"), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("closureGateBlocks: blocks only for verify + entered + enforce + not-yet-dispatched", () => {
|
|
150
|
+
assert.equal(closureGateBlocks("verify", true, true, false), true);
|
|
151
|
+
assert.equal(closureGateBlocks("verify", false, true, false), false); // incident: not entered -> no block
|
|
152
|
+
assert.equal(closureGateBlocks("verify", true, true, true), false); // already dispatched
|
|
153
|
+
assert.equal(closureGateBlocks("verify", true, false, false), false); // enforce off
|
|
154
|
+
assert.equal(closureGateBlocks("plan", true, true, false), false); // wrong phase
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("flowGuardApplies: requires both an active guard phase and an entered flow", () => {
|
|
158
|
+
assert.equal(flowGuardApplies(true, true), true);
|
|
159
|
+
assert.equal(flowGuardApplies(true, false), false); // not entered
|
|
160
|
+
assert.equal(flowGuardApplies(false, true), false); // phase not active
|
|
161
|
+
assert.equal(flowGuardApplies(false, false), false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("closureModelGuardApplies: requires both an entered flow and closure enforcement", () => {
|
|
165
|
+
assert.equal(closureModelGuardApplies(true, true), true);
|
|
166
|
+
assert.equal(closureModelGuardApplies(false, true), false); // not entered -> dormant
|
|
167
|
+
assert.equal(closureModelGuardApplies(true, false), false); // enforce off
|
|
168
|
+
assert.equal(closureModelGuardApplies(false, false), false);
|
|
169
|
+
});
|
|
@@ -90,6 +90,46 @@ export function markerGuardApplies(flowGuardsEnforced: boolean, brainstormStatus
|
|
|
90
90
|
return flowGuardsEnforced && brainstormStatus === "in_progress";
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Flow-entry marker (spec 2026-07-19-sole-gauntlet-entry-point). The gauntlet is
|
|
94
|
+
// opt-in: enforcement is dormant unless brainstorming started this flow. A `start`
|
|
95
|
+
// that leaves brainstorm in_progress is the unique arming signal (exactly one phase
|
|
96
|
+
// is in_progress at a time; the start target is not stored but is inferable). `reset`
|
|
97
|
+
// disarms; every other action preserves the running marker so it survives brainstorm
|
|
98
|
+
// -> plan -> implement -> verify. brainstormStatus is the phase status AFTER the action.
|
|
99
|
+
export function nextGauntletEntered(prev: boolean, action: string, brainstormStatus: string): boolean {
|
|
100
|
+
if (action === "reset") return false;
|
|
101
|
+
if (action === "start" && brainstormStatus === "in_progress") return true;
|
|
102
|
+
return prev;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Closure completion-gate decision. Marker-first: a non-entered flow never blocks
|
|
106
|
+
// `complete verify`. Executable contract for the extension's inlined check (same
|
|
107
|
+
// define-and-test-but-inline pattern as markerGuardApplies).
|
|
108
|
+
export function closureGateBlocks(
|
|
109
|
+
phase: string,
|
|
110
|
+
gauntletEntered: boolean,
|
|
111
|
+
closureEnforced: boolean,
|
|
112
|
+
conformanceDispatched: boolean,
|
|
113
|
+
): boolean {
|
|
114
|
+
return phase === "verify" && gauntletEntered && closureEnforced && !conformanceDispatched;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Closure-model-guard applicability. Marker-first: the extension inlines this with the
|
|
118
|
+
// settings read (closureEnforced()) as a lazy call AFTER gauntletEntered, so a dormant
|
|
119
|
+
// (out-of-flow) session performs no settings I/O. This predicate is the tested contract
|
|
120
|
+
// for that inlined check (same define-test-and-inline pattern as markerGuardApplies /
|
|
121
|
+
// closureGateBlocks); it takes the already-resolved booleans, never triggering the read.
|
|
122
|
+
export function closureModelGuardApplies(gauntletEntered: boolean, closureEnforced: boolean): boolean {
|
|
123
|
+
return gauntletEntered && closureEnforced;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Flow-guard applicability: a guard fires only when its phase is active AND the flow
|
|
127
|
+
// was entered via brainstorming. Both inputs are in-memory, so the extension calls
|
|
128
|
+
// this directly with no settings-load cost.
|
|
129
|
+
export function flowGuardApplies(phaseActive: boolean, gauntletEntered: boolean): boolean {
|
|
130
|
+
return phaseActive && gauntletEntered;
|
|
131
|
+
}
|
|
132
|
+
|
|
93
133
|
export const markerBlockReason = (file: string): string =>
|
|
94
134
|
`Blocked: ${file} still begins with the context-draft marker - the spec-writing ` +
|
|
95
135
|
`overwrite has not happened. Overwrite the draft with the real spec (write tool, ` +
|
|
@@ -26,7 +26,9 @@ import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
|
26
26
|
import {
|
|
27
27
|
checkSubstep,
|
|
28
28
|
findMarkerFile,
|
|
29
|
+
flowGuardApplies,
|
|
29
30
|
markerBlockReason,
|
|
31
|
+
nextGauntletEntered,
|
|
30
32
|
parseGitCommit,
|
|
31
33
|
phaseLabel,
|
|
32
34
|
resolveRepoDir,
|
|
@@ -239,6 +241,7 @@ function formatStatus(phases: PhaseMap): string {
|
|
|
239
241
|
export default function (pi: ExtensionAPI) {
|
|
240
242
|
let phases: PhaseMap = emptyPhases();
|
|
241
243
|
let conformanceDispatched = false;
|
|
244
|
+
let gauntletEntered = false;
|
|
242
245
|
|
|
243
246
|
// Warn-once-per-phase ledger; cleared on every phase transition and on reconstruct.
|
|
244
247
|
const firedGuards = new Map<string, boolean>();
|
|
@@ -286,6 +289,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
286
289
|
const reconstructState = (ctx: ExtensionContext) => {
|
|
287
290
|
phases = emptyPhases();
|
|
288
291
|
conformanceDispatched = false;
|
|
292
|
+
gauntletEntered = false;
|
|
289
293
|
firedGuards.clear();
|
|
290
294
|
pendingGuardWarnings.clear();
|
|
291
295
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
@@ -296,6 +300,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
296
300
|
const details = msg.details as PhaseTrackerDetails | undefined;
|
|
297
301
|
if (details && !details.error) {
|
|
298
302
|
phases = details.phases;
|
|
303
|
+
gauntletEntered = nextGauntletEntered(gauntletEntered, details.action, details.phases.brainstorm.status);
|
|
299
304
|
if (details.action === "reset") conformanceDispatched = false;
|
|
300
305
|
}
|
|
301
306
|
} else if (msg.toolName === "subagent") {
|
|
@@ -345,7 +350,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
345
350
|
const specDirs = () => resolveFlowGuards(g()).specDirs;
|
|
346
351
|
|
|
347
352
|
// Closure-review model guard - independent of flowGuards, gated by closureReview.enforce.
|
|
348
|
-
|
|
353
|
+
// gating contract: closureModelGuardApplies (see helpers). Marker-first: gauntletEntered
|
|
354
|
+
// short-circuits before closureEnforced()'s settings load, so an ad-hoc (out-of-flow)
|
|
355
|
+
// subagent dispatch never loads settings or leaks a settingsErrorWarning onto its result.
|
|
356
|
+
// Inline-matched (not called) so closureEnforced() stays a lazy second conjunct.
|
|
357
|
+
if (event.toolName === "subagent" && gauntletEntered && closureEnforced()) {
|
|
349
358
|
const model = closureReviewModel();
|
|
350
359
|
// Only execution-mode dispatches carry a model; management/control modes
|
|
351
360
|
// (action: list/get/create/update/delete/status/...) execute nothing, so skip them.
|
|
@@ -371,8 +380,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
371
380
|
// event no guard inspects (any read-only tool, or any tool in a dormant session)
|
|
372
381
|
// returns here without touching disk. Only genuinely guardable events pay for g().
|
|
373
382
|
const brainstormActive = phases.brainstorm.status === "in_progress";
|
|
374
|
-
const guardableWrite =
|
|
375
|
-
|
|
383
|
+
const guardableWrite =
|
|
384
|
+
(event.toolName === "write" || event.toolName === "edit") && flowGuardApplies(brainstormActive, gauntletEntered);
|
|
385
|
+
const guardableBash =
|
|
386
|
+
event.toolName === "bash" && flowGuardApplies(activeGuardPhase() !== undefined, gauntletEntered);
|
|
376
387
|
if (!guardableWrite && !guardableBash) return undefined;
|
|
377
388
|
|
|
378
389
|
if (!flowGuardsEnforced()) return undefined;
|
|
@@ -527,7 +538,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
527
538
|
description:
|
|
528
539
|
"Track workflow phase progress (brainstorm → plan → implement → verify → ship). " +
|
|
529
540
|
"Actions: start (mark phase in_progress), complete (mark phase complete), " +
|
|
530
|
-
"skip (mark phase skipped with reason), status (show all phases), reset (clear all phases), substep (set/clear a substep label on an in_progress phase)."
|
|
541
|
+
"skip (mark phase skipped with reason), status (show all phases), reset (clear all phases), substep (set/clear a substep label on an in_progress phase). " +
|
|
542
|
+
"Drives gauntlet-flow enforcement entered via brainstorming; the closure gate, closure-model guard, and flow guards arm only when brainstorming started the flow. Ad-hoc start verify/start implement calls do not arm the gates. Not for ad-hoc use.",
|
|
531
543
|
parameters: PhaseTrackerParams,
|
|
532
544
|
|
|
533
545
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -572,6 +584,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
572
584
|
};
|
|
573
585
|
}
|
|
574
586
|
phases = { ...phases, [params.phase]: transitionPhaseState("in_progress") as PhaseState };
|
|
587
|
+
gauntletEntered = nextGauntletEntered(gauntletEntered, "start", phases.brainstorm.status);
|
|
575
588
|
firedGuards.clear();
|
|
576
589
|
updateWidget(ctx);
|
|
577
590
|
return {
|
|
@@ -598,8 +611,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
598
611
|
} as PhaseTrackerDetails,
|
|
599
612
|
};
|
|
600
613
|
}
|
|
614
|
+
// gating contract: closureGateBlocks (see helpers). gauntletEntered is the leading
|
|
615
|
+
// conjunct so a cold-session `complete verify` (the #2 incident) neither blocks nor
|
|
616
|
+
// loads settings; the remaining conjuncts match closureGateBlocks exactly.
|
|
601
617
|
if (
|
|
602
618
|
params.phase === "verify" &&
|
|
619
|
+
gauntletEntered &&
|
|
603
620
|
resolveClosureReview(loadGauntletSettings(ctx.cwd).gauntlet).enforce &&
|
|
604
621
|
!conformanceDispatched
|
|
605
622
|
) {
|
|
@@ -694,6 +711,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
694
711
|
PHASES.map((p) => [p, transitionPhaseState("pending")]),
|
|
695
712
|
) as PhaseMap;
|
|
696
713
|
conformanceDispatched = false;
|
|
714
|
+
gauntletEntered = nextGauntletEntered(gauntletEntered, "reset", phases.brainstorm.status);
|
|
697
715
|
firedGuards.clear();
|
|
698
716
|
updateWidget(ctx);
|
|
699
717
|
return {
|
package/package.json
CHANGED
|
@@ -168,10 +168,10 @@ Per round:
|
|
|
168
168
|
including DELIVERED rows and their `evidence` `file:line`) and the round's
|
|
169
169
|
fix diff. Inject `model:` call-site per `gauntlet_setting({ key:
|
|
170
170
|
"closureReview" }).model` — same mechanism as the initial audit; omit
|
|
171
|
-
`model:` when it is `undefined` to inherit the parent's model.
|
|
172
|
-
phase-tracker closure guard blocks a dispatch
|
|
173
|
-
`closureReview.model` is set, and warns (non-blocking)
|
|
174
|
-
differs.
|
|
171
|
+
`model:` when it is `undefined` to inherit the parent's model. Inside a
|
|
172
|
+
brainstorming-entered flow, the phase-tracker closure guard blocks a dispatch
|
|
173
|
+
that omits `model:` when `closureReview.model` is set, and warns (non-blocking)
|
|
174
|
+
on one whose model differs.
|
|
175
175
|
7. **Converge or continue**: verdict `CONFORMS` → record it, done. Open gaps
|
|
176
176
|
within the cap → re-partition (per the rule above) and start the next
|
|
177
177
|
round. Cap (`gauntlet_setting({ key: "closureReview" }).maxFixRounds`,
|