pi-gauntlet 4.9.0 → 4.11.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +85 -3
  3. package/agents/conformance-reviewer.md +1 -1
  4. package/agents/spec-council-member.md +1 -1
  5. package/agents/spec-council-synthesizer.md +1 -1
  6. package/extensions/plan-tracker.test.ts +130 -5
  7. package/extensions/plan-tracker.ts +21 -9
  8. package/package.json +1 -1
  9. package/skills/brainstorming/SKILL.md +3 -3
  10. package/skills/brainstorming/gatherer.md +1 -1
  11. package/skills/brainstorming/reference/documentation-impact.md +3 -2
  12. package/skills/dispatching-parallel-agents/SKILL.md +1 -1
  13. package/skills/finishing-a-development-branch/SKILL.md +1 -1
  14. package/skills/finishing-a-development-branch/reference/disposition-protocol.md +1 -1
  15. package/skills/gatekeep-pr/SKILL.md +299 -0
  16. package/skills/gatekeep-pr/review-baseline.md +43 -0
  17. package/skills/gatekeep-pr/verification-brief.md +187 -0
  18. package/skills/receiving-code-review/SKILL.md +1 -1
  19. package/skills/requesting-code-review/SKILL.md +1 -1
  20. package/skills/roasting-the-spec/SKILL.md +1 -1
  21. package/skills/shape-ticket/SKILL.md +267 -0
  22. package/skills/subagent-driven-development/SKILL.md +2 -2
  23. package/skills/systematic-debugging/SKILL.md +1 -1
  24. package/skills/test-driven-development/SKILL.md +1 -1
  25. package/skills/using-git-worktrees/SKILL.md +2 -2
  26. package/skills/verification-before-completion/SKILL.md +1 -1
  27. package/skills/verification-before-completion/reference/conformance-check.md +1 -1
  28. package/skills/writing-plans/SKILL.md +2 -2
  29. package/skills/writing-skills/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v4.11.0 - 2026-08-18
4
+
5
+ - New skill: `gatekeep-pr` - consent-gated pre-merge PR verification: read-only three-role brief, `REVIEW.md` rubric convention with a shipped baseline, deterministic authorship-aware consent menu, thin-wrapper contract.
6
+ - `plan_tracker`: additive `failed` task status (terminal-negative `✗`), excluded from the complete count.
7
+
8
+ ## v4.10.0 - 2026-08-18
9
+
10
+ New skill: `shape-ticket` - the tracker-facing entry stage ahead of brainstorming.
11
+
12
+ - `shape-ticket` (user-invoked only, `disable-model-invocation: true`): creates or repairs exactly one tracker issue per run - Context/Problem/Idea/AC template, AC integrity gate (wishful/tautological hard stops, unspecified/external-input parking, deploy-window relocation, discovery-ticket conversion), vertical-only split rule, metadata audit, evidence gate, cheap council roast (`:low` model-suffix over the xhigh pin; worker fallback; failure never blocks), and a single human-gated batched write with post-approval re-fetch. Zero-config on `gh`/`linearis`; other trackers via the overrides `## Issue tracker` section.
13
+ - Overrides discovery ladder: the gauntlet overrides file is now found at `.pi/gauntlet-overrides.md` -> `<repo root>/gauntlet-overrides.md` -> `<repo root>/doc/gauntlet-overrides.md` (first found wins) - migrated across all skills' "Project overrides" blocks and inline references (Claude Code prep for #11).
14
+ - Shared AGENTS core v2: the "Ticket convention" section now points at `/skill:shape-ticket` (roast applied to the ticket body pre-write; no roast comments); propagated to pi-quiver, pi-cohort, pi-condense.
15
+ - `spec-council-member`/`spec-council-synthesizer` gain shape-ticket as a second sanctioned dispatcher (docs).
16
+
3
17
  ## v4.9.0 - 2026-08-14
4
18
 
5
19
  Review fix rounds parallelize when the reviewer certifies disjoint findings.
package/README.md CHANGED
@@ -35,17 +35,20 @@ pi-gauntlet's only hard dependency is pi-cohort - every gate that dispatches a r
35
35
 
36
36
  Concretely, one change through the gauntlet:
37
37
 
38
+ 0. *(Optional)* Before there's even a spec, `/skill:shape-ticket` can create or repair a single tracker issue - shaping a raw ask into a Context/Problem/Idea/Acceptance Criteria ticket, gated by an AC integrity check, a cheap council roast, and one human-confirmed write. It's a tool, not a phase: no worktree, no plan/phase tracker, runs from any repo state. It never activates on its own (`disable-model-invocation: true`) - invoke it explicitly.
38
39
  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. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; event-driven only — gauntlet never sweeps historical specs). **You read and approve the spec - human gate 1.** No implementation code exists yet.
39
40
  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
41
  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
42
  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
- 5. **`finishing-a-development-branch`**: squash, PR, keep, or discard. **Human gate 2** - the only other decision you make.
43
+ 5. **`finishing-a-development-branch`**: squash, PR, keep, or discard. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it against its issue before merging. **Human gate 2** - the only other decision you make.
43
44
 
44
45
  Only the machine-owned `plan -> implement` and `verify -> ship` handoffs receive a branch-local one-shot nudge after an unexpected settled stop; it is fire-and-forget, does not bypass either human gate, and older Pi hosts without `agent_settled` retain existing behavior.
45
46
 
46
47
  ```mermaid
47
48
  flowchart LR
49
+ T["shape-ticket<br/>(optional, explicit)"]
48
50
  R([request]) --> B[brainstorm<br/>+ spec]
51
+ T -.-> R
49
52
  B --> G1{{human gate 1:<br/>approve spec}}
50
53
  G1 --> P[plan]
51
54
  P --> I[implement<br/>waves + reviews]
@@ -64,7 +67,7 @@ Everything between gate 1 and gate 2 - task breakdown, implementation, both revi
64
67
 
65
68
  pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
66
69
 
67
- - **13 skills** - the workflow logic. They activate automatically when pi sees the matching kind of task, and each one gates the next: `brainstorming`, `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `dispatching-parallel-agents`, `verification-before-completion`, `systematic-debugging`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `writing-skills`.
70
+ - **15 skills** - the workflow logic. Thirteen activate automatically when pi sees the matching kind of task, and each one gates the next: `brainstorming`, `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `dispatching-parallel-agents`, `verification-before-completion`, `systematic-debugging`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `writing-skills`. The fourteenth, `shape-ticket`, is explicit-invocation-only (`disable-model-invocation: true`): create or repair one tracker issue per run against a Context/Problem/Idea/Acceptance-Criteria template, gated by an AC integrity check, a cheap council roast, and a single human-confirmed write. Run it with `/skill:shape-ticket`. The fifteenth, `gatekeep-pr`, is also explicit-invocation-only: consent-gated pre-merge verification of a PR against its issue - read-only gathering, running the project's verification command, a rubric-based review, then a deterministic authorship-aware menu; nothing mutates (fixes, pushes, reviews, merges) until you pick a row. Run it with `/skill:gatekeep-pr <pr>`.
68
71
  - **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.
69
72
  - **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 push or open a PR without a passing test run since your last edit; a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
70
73
 
@@ -89,7 +92,7 @@ pi-gauntlet is **opinionated**: every non-trivial change is *meant* to ride this
89
92
  ## Requirements
90
93
 
91
94
  - [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.1.0
92
- - [pi-cohort](https://github.com/jjuraszek/pi-cohort) ≥ 1.4.5 - required peer package. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `writing-skills`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call.
95
+ - [pi-cohort](https://github.com/jjuraszek/pi-cohort) ≥ 1.4.5 - required peer package. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `writing-skills`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call.
93
96
 
94
97
  Both packages must be listed in your `.pi/settings.json#packages` array (pi adds them automatically when you `pi install`). pi-gauntlet and pi-cohort are versioned independently but release together whenever dispatch semantics change - pin compatible versions of both.
95
98
 
@@ -137,6 +140,85 @@ database and copies `.env.local`. Never call `git worktree add` directly.
137
140
 
138
141
  Section headers should match skill names (`## verification-before-completion`) or skill topics (`## worktrees`, `## routing`). The override file is read by the skill instructions at runtime, not by the pi runtime itself, so adding a section only matters once the matching skill is active.
139
142
 
143
+ **Discovery ladder:** skills check three locations, in order, and use the first one found - never merged: `.pi/gauntlet-overrides.md`, then `<repo root>/gauntlet-overrides.md`, then `<repo root>/doc/gauntlet-overrides.md` (`<repo root>` = `git rev-parse --show-toplevel`, or the current directory outside a repo). Pick one location per repo.
144
+
145
+ **`## Issue tracker` section:** `shape-ticket` resolves tracker access through a capability ladder, and this is its first rung - it overrides the zero-config `gh` (GitHub) / `linearis` (Linear) defaults for any other tracker. Name the CLI's read, search, create, and update commands explicitly. For a Jira CLI, for example:
146
+
147
+ ```markdown
148
+ ## Issue tracker
149
+
150
+ Use the `jira` CLI (authenticated via `jira login`), not `gh` or `linearis`.
151
+
152
+ - read (full, incl. comments): `jira issue view ABC-123 --comments`
153
+ - search (dup/reversal check): `jira issue search --jql "project = ABC AND text ~ '<query>'"`
154
+ - create: `jira issue create --project ABC --type Task --summary "<title>" --description "<body>"`
155
+ - update: `jira issue edit ABC-123 --summary "<title>" --description "<body>"`
156
+ ```
157
+
158
+ ## REVIEW.md convention
159
+
160
+ `/skill:gatekeep-pr` (the pre-merge gate) reads an optional root-level `REVIEW.md` -
161
+ discovered at the repo root only, read from the PR's base (never the PR's own head,
162
+ so a PR can't weaken the rubric that gates it). It's a plain data file, not agent
163
+ instructions: a rubric other tooling can read too. The skill is fully functional with
164
+ no `REVIEW.md` present - it falls back to the shipped baseline rubric
165
+ (`skills/gatekeep-pr/review-baseline.md`).
166
+
167
+ **Overlay precedence**, first match wins on any conflict:
168
+
169
+ 1. Repo root `REVIEW.md` - always wins over everything below it.
170
+ 2. Shipped `skills/gatekeep-pr/review-baseline.md` - the generic default rubric.
171
+ 3. Reviewer-persona defaults.
172
+
173
+ A `REVIEW.md` entry that names a baseline concern (e.g. a severity mapping) replaces
174
+ it; everything it doesn't name stays baseline. Severities it introduces but doesn't
175
+ map to blocking/non-blocking are treated as **blocking** (fail-safe), noted in the
176
+ gate's output.
177
+
178
+ `REVIEW.md` is a diff over the baseline, not a full rewrite. Starter template:
179
+
180
+ ```markdown
181
+ # REVIEW.md
182
+
183
+ Severity mapping: Critical and Moderate findings block merge; Minor is a
184
+ non-blocking follow-up. Migration-safety findings also block merge.
185
+
186
+ Project checks (in addition to the baseline):
187
+ - Schema migrations are additive and reversible - no destructive column drops
188
+ without a documented backfill/rollback plan.
189
+ - New background jobs declare an explicit retry/backoff policy - unbounded
190
+ retries block merge.
191
+
192
+ Everything else follows the shipped baseline rubric.
193
+ ```
194
+
195
+ ## Thin-wrapper contract
196
+
197
+ A consumer repo that wants its own trigger phrases for the pre-merge gate (e.g. "gate
198
+ this PR", "ready to merge?") adds a wrapper skill that carries **zero data** - only a
199
+ name, its trigger phrases, and an instruction to follow `/skill:gatekeep-pr`. All
200
+ customization lives in two places, never in the wrapper itself:
201
+
202
+ - **`REVIEW.md`** - the review rubric (see above).
203
+ - **The gauntlet overrides file, `## PR gate` section** - everything operational:
204
+
205
+ ```markdown
206
+ ## PR gate
207
+ - verification command: <command> # required unless documented elsewhere
208
+ - timeout minutes: 15 # optional; default 15
209
+ - requires credentials: false # optional; true => skill reports "not run" as missing evidence
210
+ - worktree wrapper: <command> # optional
211
+ - issue fetch: <command with <ref> placeholder> # optional, replaces gh issue view
212
+ - merge policy: squash | merge-commit # optional
213
+ ```
214
+
215
+ An existing `## verification-before-completion` overrides section is an accepted
216
+ equivalent source for the verification command only; all other PR-gate keys
217
+ still live under `## PR gate`.
218
+
219
+ Anything a wrapper skill contains beyond trigger phrases is misplaced - move it to
220
+ `REVIEW.md` or the overrides file instead.
221
+
140
222
  ## Configuring the gates
141
223
 
142
224
  The conformance gate's model, the spec council's roster, and the phase-tracker's flow guards are all configured per pi preset (or per repo, via `.pi/settings.json`). See [doc/configuration.md](./doc/configuration.md) for every setting, its default, and how repo-local config overrides a preset.
@@ -26,7 +26,7 @@ Work flows `origin (prompt + spec) → plan → code/doc`. Every hop is lossy: a
26
26
  |---|---|---|
27
27
  | 1 | The written spec (`doc/specs/…`) | Canonical. Brainstorm already fetched the ticket, reconciled its ACs, and recorded deviations here. |
28
28
  | 2 | Original prompt (verbatim) | Catches inline requirements never folded into the spec. |
29
- | 3 | Re-fetch the ticket | **Fallback only**, when no spec exists. Skip when a spec exists — the live ticket may have drifted. The project's issue-tracker skill (for this fallback) is named in `.pi/gauntlet-overrides.md`. |
29
+ | 3 | Re-fetch the ticket | **Fallback only**, when no spec exists. Skip when a spec exists — the live ticket may have drifted. The project's issue-tracker skill (for this fallback) is named in the gauntlet overrides file (see Project overrides in the dispatching skill). |
30
30
 
31
31
  ## Process
32
32
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spec-council-member
3
- description: Adversarial single-model spec critic dispatched by the roasting-the-spec skill; assesses whether a spec is sound, complete, and actionable. Not for direct dispatch.
3
+ description: Adversarial single-model spec critic dispatched by the roasting-the-spec or shape-ticket skills; assesses whether a spec is sound, complete, and actionable. Not for direct dispatch.
4
4
  tools: read, grep, find, ls, bash
5
5
  thinking: xhigh
6
6
  defaultContext: fresh
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spec-council-synthesizer
3
- description: Neutral chair that consolidates and adjudicates spec-council member critiques into a single ranked, conflict-resolved report. Dispatched by the roasting-the-spec skill; not for direct dispatch.
3
+ description: Neutral chair that consolidates and adjudicates spec-council member critiques into a single ranked, conflict-resolved report. Dispatched by the roasting-the-spec or shape-ticket skills; not for direct dispatch.
4
4
  tools: read, grep, find, ls, bash
5
5
  thinking: xhigh
6
6
  defaultContext: fresh
@@ -8,18 +8,38 @@ type ToolResult = {
8
8
  };
9
9
 
10
10
  function harness(branch: unknown[] = []) {
11
- const tools: { name: string; execute: (...args: any[]) => unknown }[] = [];
11
+ const tools: {
12
+ name: string;
13
+ execute: (...args: any[]) => unknown;
14
+ renderResult: (result: unknown, options: unknown, theme: unknown) => { text?: string };
15
+ }[] = [];
16
+ const handlers: { event: string; handler: (event: unknown, ctx: unknown) => Promise<void> }[] = [];
12
17
  const pi = {
13
- on(_event: string, _handler: unknown) {},
14
- registerTool(tool: { name: string; execute: (...args: any[]) => unknown }) {
18
+ on(event: string, handler: (event: unknown, ctx: unknown) => Promise<void>) {
19
+ handlers.push({ event, handler });
20
+ },
21
+ registerTool(tool: any) {
15
22
  tools.push(tool);
16
23
  },
17
24
  };
18
25
  registerPlanTracker(pi as any);
19
- const ctx = { hasUI: false, sessionManager: { getBranch: () => branch } };
26
+ let widgetText: string | undefined;
27
+ const theme = { fg: (_c: string, s: string) => s, bold: (s: string) => s };
28
+ const ctx = {
29
+ hasUI: true,
30
+ ui: {
31
+ setWidget(_id: string, cb?: (tui: unknown, theme: unknown) => { text: string }) {
32
+ widgetText = cb ? cb(undefined, theme).text : undefined;
33
+ },
34
+ },
35
+ sessionManager: { getBranch: () => branch },
36
+ };
20
37
  const call = async (params: Record<string, unknown>): Promise<ToolResult> =>
21
38
  (await tools[0].execute("id", params, undefined, undefined, ctx)) as ToolResult;
22
- return { call };
39
+ const fire = async (event: string) => {
40
+ for (const h of handlers) if (h.event === event) await h.handler({}, ctx);
41
+ };
42
+ return { call, fire, tool: () => tools[0], theme, widget: () => widgetText };
23
43
  }
24
44
 
25
45
  test("add appends pending tasks and preserves existing statuses", async () => {
@@ -64,3 +84,108 @@ test("add with empty/missing tasks errors and preserves state", async () => {
64
84
  const res2 = await call({ action: "add" });
65
85
  assert.equal(res2.details.error, "tasks required");
66
86
  });
87
+
88
+ test("update to failed round-trips and is excluded from complete count", async () => {
89
+ const { call } = harness();
90
+ await call({ action: "init", tasks: ["a", "b", "c"] });
91
+ await call({ action: "update", index: 0, status: "complete" });
92
+ const res = await call({ action: "update", index: 1, status: "failed" });
93
+ assert.equal(res.details.error, undefined);
94
+ assert.deepEqual(
95
+ res.details.tasks.map((t) => [t.name, t.status]),
96
+ [["a", "complete"], ["b", "failed"], ["c", "pending"]],
97
+ );
98
+ assert.match(res.content[0].text, /1\/3 complete/);
99
+ assert.match(res.content[0].text, /1 failed/);
100
+ });
101
+
102
+ test("reconstruction preserves failed status from serialized details", async () => {
103
+ const branch = [
104
+ {
105
+ type: "message",
106
+ message: {
107
+ role: "toolResult",
108
+ toolName: "plan_tracker",
109
+ details: {
110
+ action: "update",
111
+ tasks: [
112
+ { name: "a", status: "complete" },
113
+ { name: "b", status: "failed" },
114
+ ],
115
+ },
116
+ },
117
+ },
118
+ ];
119
+ const { call, fire } = harness(branch);
120
+ await fire("session_start");
121
+ const res = await call({ action: "status" });
122
+ assert.deepEqual(
123
+ res.details.tasks.map((t) => [t.name, t.status]),
124
+ [["a", "complete"], ["b", "failed"]],
125
+ );
126
+ assert.match(res.content[0].text, /\u2717 \[1\] b/);
127
+ });
128
+
129
+ test("widget renders failed as \u2717, keeps failed out of complete count and current", async () => {
130
+ const { call, widget } = harness();
131
+ await call({ action: "init", tasks: ["a", "b"] });
132
+ await call({ action: "update", index: 0, status: "failed" });
133
+ const w = widget();
134
+ assert.ok(w);
135
+ assert.match(w!, /\u2717/);
136
+ assert.match(w!, /\(0\/2\)/);
137
+ assert.match(w!, /b$/); // current = first pending, never the failed task
138
+ });
139
+
140
+ test("renderResult status path shows \u2717 for failed and excludes it from complete", async () => {
141
+ const { call, tool, theme } = harness();
142
+ await call({ action: "init", tasks: ["a", "b"] });
143
+ await call({ action: "update", index: 1, status: "failed" });
144
+ const res = await call({ action: "status" });
145
+ const rendered = tool().renderResult(res as any, {}, theme as any);
146
+ const text = (rendered as any).text as string;
147
+ assert.match(text, /0\/2 complete/);
148
+ assert.match(text, /\u2717/);
149
+ });
150
+
151
+ test("renderResult status header appends failed count when a task has failed", async () => {
152
+ const { call, tool, theme } = harness();
153
+ await call({ action: "init", tasks: ["a", "b", "c"] });
154
+ await call({ action: "update", index: 0, status: "complete" });
155
+ await call({ action: "update", index: 1, status: "failed" });
156
+ const res = await call({ action: "status" });
157
+ const rendered = tool().renderResult(res as any, {}, theme as any);
158
+ const text = (rendered as any).text as string;
159
+ assert.match(text, /1\/3 complete, 1 failed/);
160
+ });
161
+
162
+ test("renderResult status header omits failed count when no task has failed", async () => {
163
+ const { call, tool, theme } = harness();
164
+ await call({ action: "init", tasks: ["a", "b"] });
165
+ await call({ action: "update", index: 0, status: "complete" });
166
+ const res = await call({ action: "status" });
167
+ const rendered = tool().renderResult(res as any, {}, theme as any);
168
+ const text = (rendered as any).text as string;
169
+ assert.match(text, /^1\/2 complete\n/);
170
+ assert.doesNotMatch(text, /failed/);
171
+ });
172
+
173
+ test("renderResult update case appends failed count when a task has failed", async () => {
174
+ const { call, tool, theme } = harness();
175
+ await call({ action: "init", tasks: ["a", "b", "c"] });
176
+ await call({ action: "update", index: 0, status: "complete" });
177
+ const res = await call({ action: "update", index: 1, status: "failed" });
178
+ const rendered = tool().renderResult(res as any, {}, theme as any);
179
+ const text = (rendered as any).text as string;
180
+ assert.match(text, /^\u2713 Updated \(1\/3 complete, 1 failed\)$/);
181
+ });
182
+
183
+ test("renderResult update case omits failed count when no task has failed", async () => {
184
+ const { call, tool, theme } = harness();
185
+ await call({ action: "init", tasks: ["a", "b"] });
186
+ const res = await call({ action: "update", index: 0, status: "complete" });
187
+ const rendered = tool().renderResult(res as any, {}, theme as any);
188
+ const text = (rendered as any).text as string;
189
+ assert.match(text, /^\u2713 Updated \(1\/2 complete\)$/);
190
+ assert.doesNotMatch(text, /failed/);
191
+ });
@@ -11,7 +11,7 @@ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-c
11
11
  import { Text } from "@earendil-works/pi-tui";
12
12
  import { type Static, Type } from "@sinclair/typebox";
13
13
 
14
- type TaskStatus = "pending" | "in_progress" | "complete";
14
+ type TaskStatus = "pending" | "in_progress" | "complete" | "failed";
15
15
 
16
16
  interface Task {
17
17
  name: string;
@@ -40,8 +40,8 @@ const PlanTrackerParams = Type.Object({
40
40
  }),
41
41
  ),
42
42
  status: Type.Optional(
43
- StringEnum(["pending", "in_progress", "complete"] as const, {
44
- description: "New status (for update)",
43
+ StringEnum(["pending", "in_progress", "complete", "failed"] as const, {
44
+ description: "New status (for update); failed is terminal-negative (ran and did not pass)",
45
45
  }),
46
46
  ),
47
47
  });
@@ -59,6 +59,8 @@ function formatWidget(tasks: Task[], theme: Theme): string {
59
59
  return theme.fg("success", "✓");
60
60
  case "in_progress":
61
61
  return theme.fg("warning", "→");
62
+ case "failed":
63
+ return theme.fg("error", "✗");
62
64
  default:
63
65
  return theme.fg("dim", "○");
64
66
  }
@@ -78,13 +80,16 @@ function formatStatus(tasks: Task[]): string {
78
80
  const complete = tasks.filter((t) => t.status === "complete").length;
79
81
  const inProgress = tasks.filter((t) => t.status === "in_progress").length;
80
82
  const pending = tasks.filter((t) => t.status === "pending").length;
83
+ const failed = tasks.filter((t) => t.status === "failed").length;
81
84
 
82
85
  const lines: string[] = [];
83
- lines.push(`Plan: ${complete}/${tasks.length} complete (${inProgress} in progress, ${pending} pending)`);
86
+ lines.push(
87
+ `Plan: ${complete}/${tasks.length} complete (${inProgress} in progress, ${pending} pending, ${failed} failed)`,
88
+ );
84
89
  lines.push("");
85
90
  for (let i = 0; i < tasks.length; i++) {
86
91
  const t = tasks[i];
87
- const icon = t.status === "complete" ? "✓" : t.status === "in_progress" ? "→" : "○";
92
+ const icon = t.status === "complete" ? "✓" : t.status === "in_progress" ? "→" : t.status === "failed" ? "✗" : "○";
88
93
  lines.push(` ${icon} [${i}] ${t.name}`);
89
94
  }
90
95
  return lines.join("\n");
@@ -129,7 +134,7 @@ export default function (pi: ExtensionAPI) {
129
134
  name: "plan_tracker",
130
135
  label: "Plan Tracker",
131
136
  description:
132
- "Track progress while EXECUTING an implementation plan (the implement phase) or a verify-phase conformance fix wave. Actions: init (set task list), add (append tasks as pending; existing statuses preserved), update (change task status), status (show current state), clear (remove plan). Do NOT use for brainstorming, research, or planning checklists: those phases are open-ended and a bounded task list misrepresents them as a fixed N-step process.",
137
+ "Track progress while EXECUTING an implementation plan (the implement phase), a verify-phase conformance fix wave, or another bounded gate checklist (e.g. pre-merge PR verification). Statuses: pending, in_progress, complete, failed (terminal-negative: the task ran and did not pass; never counted complete). Actions: init (set task list), add (append tasks as pending; existing statuses preserved), update (change task status), status (show current state), clear (remove plan). Do NOT use for brainstorming, research, or planning checklists: those phases are open-ended and a bounded task list misrepresents them as a fixed N-step process.",
133
138
  parameters: PlanTrackerParams,
134
139
 
135
140
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -305,8 +310,11 @@ export default function (pi: ExtensionAPI) {
305
310
  );
306
311
  case "update": {
307
312
  const complete = taskList.filter((t) => t.status === "complete").length;
313
+ const failed = taskList.filter((t) => t.status === "failed").length;
314
+ const suffix = failed > 0 ? `, ${failed} failed` : "";
308
315
  return new Text(
309
- theme.fg("success", "✓ ") + theme.fg("muted", `Updated (${complete}/${taskList.length} complete)`),
316
+ theme.fg("success", "✓ ") +
317
+ theme.fg("muted", `Updated (${complete}/${taskList.length} complete${suffix})`),
310
318
  0,
311
319
  0,
312
320
  );
@@ -316,14 +324,18 @@ export default function (pi: ExtensionAPI) {
316
324
  return new Text(theme.fg("dim", "No plan active"), 0, 0);
317
325
  }
318
326
  const complete = taskList.filter((t) => t.status === "complete").length;
319
- let text = theme.fg("muted", `${complete}/${taskList.length} complete`);
327
+ const failed = taskList.filter((t) => t.status === "failed").length;
328
+ const suffix = failed > 0 ? `, ${failed} failed` : "";
329
+ let text = theme.fg("muted", `${complete}/${taskList.length} complete${suffix}`);
320
330
  for (const t of taskList) {
321
331
  const icon =
322
332
  t.status === "complete"
323
333
  ? theme.fg("success", "✓")
324
334
  : t.status === "in_progress"
325
335
  ? theme.fg("warning", "→")
326
- : theme.fg("dim", "");
336
+ : t.status === "failed"
337
+ ? theme.fg("error", "✗")
338
+ : theme.fg("dim", "○");
327
339
  text += `\n${icon} ${theme.fg("muted", t.name)}`;
328
340
  }
329
341
  return new Text(text, 0, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "4.9.0",
3
+ "version": "4.11.0",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -176,7 +176,7 @@ Cover at minimum:
176
176
  - Derived / memory docs invalidated: <routers / AGENTS.md sections / topic guides / indexes, or "none">
177
177
  ```
178
178
 
179
- Each entry answers with a doc name, "none", or "deferred: <trigger>". A new standalone `.md` appears only where no existing doc already owns the topic. Project-specific doc taxonomy goes in a `## documentation` block in `.pi/gauntlet-overrides.md` (no new settings key; guidance only). Doc updates ship in the same commit and are verified against the spec by the conformance gate.
179
+ Each entry answers with a doc name, "none", or "deferred: <trigger>". A new standalone `.md` appears only where no existing doc already owns the topic. Project-specific doc taxonomy goes in a `## documentation` block in the gauntlet overrides file (see Project overrides) (no new settings key; guidance only). Doc updates ship in the same commit and are verified against the spec by the conformance gate.
180
180
 
181
181
  Be ready to go back and clarify when something doesn't make sense.
182
182
 
@@ -240,7 +240,7 @@ When the new spec replaces a prior spec — fully or in part — and you **alrea
240
240
  - **Mark, never delete.** Delete/archive policy is consumer territory via overrides.
241
241
  - **Coverage limits**: unmarked does NOT mean current (code drift, abandoned designs, and partial ships produce no successor spec); marked does NOT mean dead (partial supersession leaves live sections).
242
242
  - Predecessor in a **different service's spec directory**: out of scope — record it in the new spec's Open Questions instead of editing outside the write grant.
243
- - **Override contract**: `.pi/gauntlet-overrides.md` may replace the banner *syntax*; placement, append-only, no-transitive-rewrite, and mark-never-delete stay fixed. A syntax override entry must itself state the scout-citation guidance for its format (the shipped `gatherer.md` guidance names only the default banner).
243
+ - **Override contract**: the gauntlet overrides file (see Project overrides) may replace the banner *syntax*; placement, append-only, no-transitive-rewrite, and mark-never-delete stay fixed. A syntax override entry must itself state the scout-citation guidance for its format (the shipped `gatherer.md` guidance names only the default banner).
244
244
 
245
245
  ## Spec Self-Review (Before User Review Gate)
246
246
 
@@ -384,4 +384,4 @@ phase_tracker({ action: "complete", phase: "brainstorm" })
384
384
 
385
385
  ## Project overrides
386
386
 
387
- If `.pi/gauntlet-overrides.md` exists, read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
387
+ If a gauntlet overrides file exists - checked in order: `.pi/gauntlet-overrides.md`, `<repo root>/gauntlet-overrides.md`, `<repo root>/doc/gauntlet-overrides.md`; first found wins - read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
@@ -68,7 +68,7 @@ contains any of:
68
68
 
69
69
  - an `http(s)://` URL;
70
70
  - a tracker-style ID matching `[A-Z][A-Z0-9]+-\d+` (Linear/Jira form) **when a fetch
71
- path exists** (a tracker tool/MCP, or a URL pattern in `.pi/gauntlet-overrides.md`);
71
+ path exists** (a tracker tool/MCP, or a URL pattern in the gauntlet overrides file, see Project overrides);
72
72
  - a GitHub-style ref `owner/repo#N`, or a bare `#N` when the repo's tracker is
73
73
  GitHub Issues.
74
74
 
@@ -130,5 +130,6 @@ Keep this list in sync with the skills that cite this doc:
130
130
  ## Project-specific taxonomy
131
131
 
132
132
  This doc is generic. Project-specific doc taxonomy (which docs a given
133
- project treats as canonical for which topic) lives in a project's
134
- `.pi/gauntlet-overrides.md`, in a `## documentation` section - not here.
133
+ project treats as canonical for which topic) lives in the gauntlet
134
+ overrides file (see Project overrides in `brainstorming/SKILL.md`), in a
135
+ `## documentation` section - not here.
@@ -203,4 +203,4 @@ After agents return:
203
203
 
204
204
  ## Project overrides
205
205
 
206
- If `.pi/gauntlet-overrides.md` exists, read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
206
+ If a gauntlet overrides file exists - checked in order: `.pi/gauntlet-overrides.md`, `<repo root>/gauntlet-overrides.md`, `<repo root>/doc/gauntlet-overrides.md`; first found wins - read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
@@ -358,4 +358,4 @@ phase_tracker({ action: "complete", phase: "ship" })
358
358
 
359
359
  ## Project overrides
360
360
 
361
- If `.pi/gauntlet-overrides.md` exists, read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
361
+ If a gauntlet overrides file exists - checked in order: `.pi/gauntlet-overrides.md`, `<repo root>/gauntlet-overrides.md`, `<repo root>/doc/gauntlet-overrides.md`; first found wins - read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
@@ -38,7 +38,7 @@ Take **no** disposition action before the reply. Then, once, in order:
38
38
  3. **Re-audit if step 2 changed the spec**; regenerate the inventory and re-render if it changed. Project `fix-now` only from the refreshed inventory.
39
39
  4. **fix-now + code-changing custom:** project the selected concerns per gap into the reference's concern-scoped fix contract (excluding accepted/rescoped/followed-up siblings); run the reference "Fix loop" (unchanged - do not re-describe it). A code-changing `custom` runs the project's tests + `code-reviewer` on its delta before proceeding. Re-run Step 1's canonical tests.
40
40
  5. **Re-audit after all state-changing work;** obtain fresh decisions **only if** the refreshed inventory differs from the approved one, else proceed.
41
- 6. **follow-up** from the current inventory: create the item via the project's issue-tracker convention (`.pi/gauntlet-overrides.md`), record the ticket ID/URL; on failure keep the concern open.
41
+ 6. **follow-up** from the current inventory: create the item via the project's issue-tracker convention (the gauntlet overrides file, see Project overrides in `finishing-a-development-branch/SKILL.md`), record the ticket ID/URL; on failure keep the concern open.
42
42
  7. **Non-state-changing custom:** execute and record the result.
43
43
  8. **revert** (`revert conformance fix Gn`): light-revert the indexed commit, re-run Step 1's canonical tests; on failure stop; on pass re-audit and regenerate.
44
44
  9. Re-enter Step 3.5 with the re-audited block if any concern remains open.