pi-gauntlet 4.0.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 (46) hide show
  1. package/CHANGELOG.md +300 -0
  2. package/LICENSE +24 -0
  3. package/README.md +278 -0
  4. package/agents/code-reviewer.md +48 -0
  5. package/agents/conformance-reviewer.md +139 -0
  6. package/agents/implementer.md +40 -0
  7. package/agents/spec-council-member.md +47 -0
  8. package/agents/spec-council-synthesizer.md +39 -0
  9. package/agents/spec-reviewer.md +47 -0
  10. package/agents/spec-summarizer.md +42 -0
  11. package/bin/install-agents.mjs +141 -0
  12. package/extensions/phase-tracker.ts +622 -0
  13. package/extensions/plan-tracker.ts +308 -0
  14. package/extensions/verify-before-ship.ts +132 -0
  15. package/package.json +43 -0
  16. package/skills/brainstorming/SKILL.md +290 -0
  17. package/skills/dispatching-parallel-agents/SKILL.md +192 -0
  18. package/skills/finishing-a-development-branch/SKILL.md +311 -0
  19. package/skills/receiving-code-review/SKILL.md +200 -0
  20. package/skills/requesting-code-review/SKILL.md +115 -0
  21. package/skills/requesting-code-review/code-reviewer.md +166 -0
  22. package/skills/roasting-the-spec/SKILL.md +139 -0
  23. package/skills/subagent-driven-development/SKILL.md +223 -0
  24. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +25 -0
  25. package/skills/subagent-driven-development/implementer-prompt.md +113 -0
  26. package/skills/subagent-driven-development/spec-reviewer-prompt.md +68 -0
  27. package/skills/systematic-debugging/SKILL.md +151 -0
  28. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  29. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  30. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  31. package/skills/systematic-debugging/find-polluter.sh +63 -0
  32. package/skills/systematic-debugging/reference/rationalizations.md +61 -0
  33. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  34. package/skills/test-driven-development/SKILL.md +230 -0
  35. package/skills/test-driven-development/reference/examples.md +99 -0
  36. package/skills/test-driven-development/reference/rationalizations.md +65 -0
  37. package/skills/test-driven-development/reference/when-stuck.md +31 -0
  38. package/skills/test-driven-development/testing-anti-patterns.md +299 -0
  39. package/skills/using-git-worktrees/SKILL.md +193 -0
  40. package/skills/verification-before-completion/SKILL.md +169 -0
  41. package/skills/verification-before-completion/reference/conformance-check.md +220 -0
  42. package/skills/writing-plans/SKILL.md +244 -0
  43. package/skills/writing-skills/SKILL.md +429 -0
  44. package/skills/writing-skills/reference/anthropic-best-practices.md +1130 -0
  45. package/skills/writing-skills/reference/persuasion.md +187 -0
  46. package/skills/writing-skills/reference/testing-skills-with-subagents.md +384 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Plan Tracker Extension
3
+ *
4
+ * A native pi tool for tracking plan progress.
5
+ * State is stored in tool result details for proper branching support.
6
+ * Shows a persistent TUI widget above the editor.
7
+ */
8
+
9
+ import { StringEnum } from "@earendil-works/pi-ai";
10
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
11
+ import { Text } from "@earendil-works/pi-tui";
12
+ import { type Static, Type } from "@sinclair/typebox";
13
+
14
+ type TaskStatus = "pending" | "in_progress" | "complete";
15
+
16
+ interface Task {
17
+ name: string;
18
+ status: TaskStatus;
19
+ }
20
+
21
+ interface PlanTrackerDetails {
22
+ action: "init" | "update" | "status" | "clear";
23
+ tasks: Task[];
24
+ error?: string;
25
+ }
26
+
27
+ const PlanTrackerParams = Type.Object({
28
+ action: StringEnum(["init", "update", "status", "clear"] as const, {
29
+ description: "Action to perform",
30
+ }),
31
+ tasks: Type.Optional(
32
+ Type.Array(Type.String(), {
33
+ description: "Task names (for init)",
34
+ }),
35
+ ),
36
+ index: Type.Optional(
37
+ Type.Integer({
38
+ minimum: 0,
39
+ description: "Task index, 0-based (for update)",
40
+ }),
41
+ ),
42
+ status: Type.Optional(
43
+ StringEnum(["pending", "in_progress", "complete"] as const, {
44
+ description: "New status (for update)",
45
+ }),
46
+ ),
47
+ });
48
+
49
+ export type PlanTrackerInput = Static<typeof PlanTrackerParams>;
50
+
51
+ function formatWidget(tasks: Task[], theme: Theme): string {
52
+ if (tasks.length === 0) return "";
53
+
54
+ const complete = tasks.filter((t) => t.status === "complete").length;
55
+ const icons = tasks
56
+ .map((t) => {
57
+ switch (t.status) {
58
+ case "complete":
59
+ return theme.fg("success", "✓");
60
+ case "in_progress":
61
+ return theme.fg("warning", "→");
62
+ default:
63
+ return theme.fg("dim", "○");
64
+ }
65
+ })
66
+ .join("");
67
+
68
+ // Find current task (first in_progress, or first pending)
69
+ const current = tasks.find((t) => t.status === "in_progress") ?? tasks.find((t) => t.status === "pending");
70
+ const currentName = current ? ` ${current.name}` : "";
71
+
72
+ return `${theme.fg("muted", "Tasks:")} ${icons} ${theme.fg("muted", `(${complete}/${tasks.length})`)}${currentName}`;
73
+ }
74
+
75
+ function formatStatus(tasks: Task[]): string {
76
+ if (tasks.length === 0) return "No plan active.";
77
+
78
+ const complete = tasks.filter((t) => t.status === "complete").length;
79
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
80
+ const pending = tasks.filter((t) => t.status === "pending").length;
81
+
82
+ const lines: string[] = [];
83
+ lines.push(`Plan: ${complete}/${tasks.length} complete (${inProgress} in progress, ${pending} pending)`);
84
+ lines.push("");
85
+ for (let i = 0; i < tasks.length; i++) {
86
+ const t = tasks[i];
87
+ const icon = t.status === "complete" ? "✓" : t.status === "in_progress" ? "→" : "○";
88
+ lines.push(` ${icon} [${i}] ${t.name}`);
89
+ }
90
+ return lines.join("\n");
91
+ }
92
+
93
+ export default function (pi: ExtensionAPI) {
94
+ let tasks: Task[] = [];
95
+
96
+ const reconstructState = (ctx: ExtensionContext) => {
97
+ tasks = [];
98
+ for (const entry of ctx.sessionManager.getBranch()) {
99
+ if (entry.type !== "message") continue;
100
+ const msg = entry.message;
101
+ if (msg.role !== "toolResult" || msg.toolName !== "plan_tracker") continue;
102
+ const details = msg.details as PlanTrackerDetails | undefined;
103
+ if (details && !details.error) {
104
+ tasks = details.tasks.map((t) => ({ ...t }));
105
+ }
106
+ }
107
+ };
108
+
109
+ const updateWidget = (ctx: ExtensionContext) => {
110
+ if (!ctx.hasUI) return;
111
+ if (tasks.length === 0) {
112
+ ctx.ui.setWidget("plan_tracker", undefined);
113
+ } else {
114
+ ctx.ui.setWidget("plan_tracker", (_tui, theme) => {
115
+ return new Text(formatWidget(tasks, theme), 0, 0);
116
+ });
117
+ }
118
+ };
119
+
120
+ // Reconstruct state + widget on session events
121
+ for (const event of ["session_start", "session_switch", "session_fork", "session_tree"] as const) {
122
+ pi.on(event, async (_event, ctx) => {
123
+ reconstructState(ctx);
124
+ updateWidget(ctx);
125
+ });
126
+ }
127
+
128
+ pi.registerTool({
129
+ name: "plan_tracker",
130
+ label: "Plan Tracker",
131
+ description:
132
+ "Track progress while EXECUTING an implementation plan — the implement phase only. Actions: init (set task list), 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
+ parameters: PlanTrackerParams,
134
+
135
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
136
+ switch (params.action) {
137
+ case "init": {
138
+ if (!params.tasks || params.tasks.length === 0) {
139
+ return {
140
+ content: [{ type: "text", text: "Error: tasks array required for init" }],
141
+ details: {
142
+ action: "init",
143
+ tasks: tasks.map((t) => ({ ...t })),
144
+ error: "tasks required",
145
+ } as PlanTrackerDetails,
146
+ };
147
+ }
148
+ tasks = params.tasks.map((name) => ({ name, status: "pending" as TaskStatus }));
149
+ updateWidget(ctx);
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: `Plan initialized with ${tasks.length} tasks.\n${formatStatus(tasks)}`,
155
+ },
156
+ ],
157
+ details: { action: "init", tasks: tasks.map((t) => ({ ...t })) } as PlanTrackerDetails,
158
+ };
159
+ }
160
+
161
+ case "update": {
162
+ if (params.index === undefined || !params.status) {
163
+ return {
164
+ content: [{ type: "text", text: "Error: index and status required for update" }],
165
+ details: {
166
+ action: "update",
167
+ tasks: tasks.map((t) => ({ ...t })),
168
+ error: "index and status required",
169
+ } as PlanTrackerDetails,
170
+ };
171
+ }
172
+ if (tasks.length === 0) {
173
+ return {
174
+ content: [{ type: "text", text: "Error: no plan active. Use init first." }],
175
+ details: {
176
+ action: "update",
177
+ tasks: [],
178
+ error: "no plan active",
179
+ } as PlanTrackerDetails,
180
+ };
181
+ }
182
+ if (params.index < 0 || params.index >= tasks.length) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: `Error: index ${params.index} out of range (0-${tasks.length - 1})`,
188
+ },
189
+ ],
190
+ details: {
191
+ action: "update",
192
+ tasks: tasks.map((t) => ({ ...t })),
193
+ error: `index ${params.index} out of range`,
194
+ } as PlanTrackerDetails,
195
+ };
196
+ }
197
+ tasks[params.index].status = params.status;
198
+ updateWidget(ctx);
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: `Task ${params.index} "${tasks[params.index].name}" → ${params.status}\n${formatStatus(tasks)}`,
204
+ },
205
+ ],
206
+ details: { action: "update", tasks: tasks.map((t) => ({ ...t })) } as PlanTrackerDetails,
207
+ };
208
+ }
209
+
210
+ case "status": {
211
+ return {
212
+ content: [{ type: "text", text: formatStatus(tasks) }],
213
+ details: { action: "status", tasks: tasks.map((t) => ({ ...t })) } as PlanTrackerDetails,
214
+ };
215
+ }
216
+
217
+ case "clear": {
218
+ const count = tasks.length;
219
+ tasks = [];
220
+ updateWidget(ctx);
221
+ return {
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: count > 0 ? `Plan cleared (${count} tasks removed).` : "No plan was active.",
226
+ },
227
+ ],
228
+ details: { action: "clear", tasks: [] } as PlanTrackerDetails,
229
+ };
230
+ }
231
+
232
+ default:
233
+ return {
234
+ content: [{ type: "text", text: `Unknown action: ${params.action}` }],
235
+ details: {
236
+ action: "status",
237
+ tasks: tasks.map((t) => ({ ...t })),
238
+ error: `unknown action`,
239
+ } as PlanTrackerDetails,
240
+ };
241
+ }
242
+ },
243
+
244
+ renderCall(args, theme) {
245
+ let text = theme.fg("toolTitle", theme.bold("plan_tracker "));
246
+ text += theme.fg("muted", args.action);
247
+ if (args.action === "update" && args.index !== undefined) {
248
+ text += ` ${theme.fg("accent", `[${args.index}]`)}`;
249
+ if (args.status) text += ` → ${theme.fg("dim", args.status)}`;
250
+ }
251
+ if (args.action === "init" && args.tasks) {
252
+ text += ` ${theme.fg("dim", `(${args.tasks.length} tasks)`)}`;
253
+ }
254
+ return new Text(text, 0, 0);
255
+ },
256
+
257
+ renderResult(result, _options, theme) {
258
+ const details = result.details as PlanTrackerDetails | undefined;
259
+ if (!details) {
260
+ const text = result.content[0];
261
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
262
+ }
263
+
264
+ if (details.error) {
265
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
266
+ }
267
+
268
+ const taskList = details.tasks;
269
+ switch (details.action) {
270
+ case "init":
271
+ return new Text(
272
+ theme.fg("success", "✓ ") + theme.fg("muted", `Plan initialized with ${taskList.length} tasks`),
273
+ 0,
274
+ 0,
275
+ );
276
+ case "update": {
277
+ const complete = taskList.filter((t) => t.status === "complete").length;
278
+ return new Text(
279
+ theme.fg("success", "✓ ") + theme.fg("muted", `Updated (${complete}/${taskList.length} complete)`),
280
+ 0,
281
+ 0,
282
+ );
283
+ }
284
+ case "status": {
285
+ if (taskList.length === 0) {
286
+ return new Text(theme.fg("dim", "No plan active"), 0, 0);
287
+ }
288
+ const complete = taskList.filter((t) => t.status === "complete").length;
289
+ let text = theme.fg("muted", `${complete}/${taskList.length} complete`);
290
+ for (const t of taskList) {
291
+ const icon =
292
+ t.status === "complete"
293
+ ? theme.fg("success", "✓")
294
+ : t.status === "in_progress"
295
+ ? theme.fg("warning", "→")
296
+ : theme.fg("dim", "○");
297
+ text += `\n${icon} ${theme.fg("muted", t.name)}`;
298
+ }
299
+ return new Text(text, 0, 0);
300
+ }
301
+ case "clear":
302
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Plan cleared"), 0, 0);
303
+ default:
304
+ return new Text(theme.fg("dim", "Done"), 0, 0);
305
+ }
306
+ },
307
+ });
308
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Verify-before-ship extension
3
+ *
4
+ * Single-session verification gate for shipping commands (git commit / git push /
5
+ * gh pr create). Tracks whether a recognised verification command has succeeded
6
+ * since the last source-file write; injects an advisory warning into the tool
7
+ * result of any ship command when verification is stale.
8
+ *
9
+ * In-memory only. No persisted state.
10
+ *
11
+ * Configurable via settings.json:
12
+ *
13
+ * {
14
+ * "piGauntlet": {
15
+ * "verifyBeforeShip": {
16
+ * "testCommands": ["make ci", "make test", "pytest", "rspec"],
17
+ * "warningReference": "doc/testing.md"
18
+ * }
19
+ * }
20
+ * }
21
+ *
22
+ * Defaults match common multi-language verification entrypoints. Override
23
+ * `testCommands` to match your project's conventions.
24
+ */
25
+
26
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
+
28
+ // Default verification entrypoints. Override via settings.
29
+ const DEFAULT_TEST_COMMANDS = [
30
+ "make\\s+(?:ci|test)(?![-\\w])", // rejects make test-smoke, test-corpus, etc.
31
+ "npm\\s+(?:test|run\\s+test)",
32
+ "pnpm\\s+test",
33
+ "yarn\\s+test",
34
+ "pytest",
35
+ "rspec",
36
+ "cargo\\s+test",
37
+ "go\\s+test",
38
+ ];
39
+
40
+ const SHIP_CMD = /\b(git\s+commit|git\s+push|gh\s+pr\s+create)\b/;
41
+ const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|rb|go|rs|java|swift|kt)$/;
42
+ const TEST_PATH = /(^|\/)(tests?|__tests__)\/|\.(test|spec)\.|_test\.(py|go|rb)$/;
43
+
44
+ type Settings = {
45
+ piGauntlet?: {
46
+ verifyBeforeShip?: {
47
+ testCommands?: string[];
48
+ warningReference?: string;
49
+ };
50
+ };
51
+ };
52
+
53
+ const isSourceWrite = (filePath: string | undefined): boolean => {
54
+ if (!filePath) return false;
55
+ return SOURCE_EXT.test(filePath) && !TEST_PATH.test(filePath);
56
+ };
57
+
58
+ const buildTestCmdRegex = (commands: string[]): RegExp =>
59
+ new RegExp(`\\b(${commands.join("|")})\\b`);
60
+
61
+ const formatWarning = (command: string, testCommands: string[], reference: string | undefined): string => {
62
+ const examples = testCommands
63
+ .slice(0, 3)
64
+ .map((c) => "`" + c.replace(/\\s\+/g, " ").replace(/\\b|\(\?!.*?\)/g, "") + "`")
65
+ .join(" / ");
66
+ const refLine = reference ? `\nReference: ${reference}` : "";
67
+ return (
68
+ `⚠️ Ship command \`${command.trim()}\` ran without verification.\n\n` +
69
+ `No successful ${examples} run since the last source edit in this session.\n` +
70
+ `Run your project's verification target before continuing, or confirm with the user that you are deliberately skipping it.${refLine}`
71
+ );
72
+ };
73
+
74
+ 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
+ let verified = false;
82
+ let pendingTestCallId: string | null = null;
83
+ const pendingShipWarnings = new Map<string, string>();
84
+
85
+ const getCommand = (event: { input: unknown }): string => {
86
+ const input = event.input as { command?: unknown } | undefined;
87
+ return typeof input?.command === "string" ? input.command : "";
88
+ };
89
+
90
+ const getPath = (event: { input: unknown }): string | undefined => {
91
+ const input = event.input as { path?: unknown } | undefined;
92
+ return typeof input?.path === "string" ? input.path : undefined;
93
+ };
94
+
95
+ pi.on("tool_call", async (event) => {
96
+ if (event.toolName === "write" || event.toolName === "edit") {
97
+ if (isSourceWrite(getPath(event))) verified = false;
98
+ return undefined;
99
+ }
100
+
101
+ if (event.toolName !== "bash") return undefined;
102
+ const command = getCommand(event);
103
+ if (!command) return undefined;
104
+
105
+ if (testRegex.test(command)) {
106
+ pendingTestCallId = event.toolCallId;
107
+ return undefined;
108
+ }
109
+
110
+ if (SHIP_CMD.test(command) && !verified) {
111
+ pendingShipWarnings.set(event.toolCallId, formatWarning(command, testCommands, reference));
112
+ }
113
+ return undefined;
114
+ });
115
+
116
+ pi.on("tool_result", async (event) => {
117
+ if (event.toolName !== "bash") return undefined;
118
+
119
+ if (pendingTestCallId === event.toolCallId) {
120
+ pendingTestCallId = null;
121
+ if (!event.isError) verified = true;
122
+ }
123
+
124
+ const warning = pendingShipWarnings.get(event.toolCallId);
125
+ if (!warning) return undefined;
126
+ pendingShipWarnings.delete(event.toolCallId);
127
+
128
+ return {
129
+ content: [{ type: "text" as const, text: warning }, ...event.content],
130
+ };
131
+ });
132
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "pi-gauntlet",
3
+ "version": "4.0.0",
4
+ "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/jjuraszek/pi-gauntlet.git"
10
+ },
11
+ "homepage": "https://github.com/jjuraszek/pi-gauntlet#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/jjuraszek/pi-gauntlet/issues"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi",
18
+ "pi-coding-agent",
19
+ "skills",
20
+ "agents",
21
+ "workflow"
22
+ ],
23
+ "files": [
24
+ "skills",
25
+ "extensions",
26
+ "agents",
27
+ "bin",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "pi": {
31
+ "skills": [
32
+ "./skills"
33
+ ],
34
+ "extensions": [
35
+ "./extensions"
36
+ ]
37
+ },
38
+ "scripts": {
39
+ "postinstall": "node ./bin/install-agents.mjs",
40
+ "link-agents": "node ./bin/install-agents.mjs",
41
+ "test": "node scripts/ci.mjs"
42
+ }
43
+ }