@firstpick/pi-extension-grill-me 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/index.ts +179 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @firstpick/pi-extension-grill-me
|
|
2
|
+
|
|
3
|
+
Deterministic `/grill-me` design interview workflow for Pi.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Adds `/grill-me [plan]` to start a rigorous design interview.
|
|
8
|
+
- Forces progress through structured tools instead of relying only on prompt text.
|
|
9
|
+
- Records each question, recommended answer, user answer, status, and notes.
|
|
10
|
+
- Persists session state in the active project at `.pi/grill-me/state.json`.
|
|
11
|
+
- Saves final results to Markdown inside the project directory.
|
|
12
|
+
- Refuses to write result files outside the current project directory.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:@firstpick/pi-extension-grill-me
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Development symlink
|
|
21
|
+
|
|
22
|
+
For local development, symlink Pi's global extension entry to this package:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
ln -s /home/firstpick/npm-packages/pi-extension-grill-me/index.ts ~/.pi/agent/extensions/grill-me.ts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then run `/reload` in Pi.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
No required configuration.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
- `/grill-me [plan]` — initialize a grill session for the current project and start the interview.
|
|
37
|
+
- If no plan is supplied, the agent asks you to paste or describe the plan first.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
- `grill_record_turn`
|
|
42
|
+
- Records exactly one interview question at a time.
|
|
43
|
+
- Captures the assistant recommendation, user answer, decision status, and notes.
|
|
44
|
+
- `grill_save_results`
|
|
45
|
+
- Writes the active interview state to Markdown.
|
|
46
|
+
- Defaults to `GRILL-ME.md` in the project root.
|
|
47
|
+
- Accepts a project-relative `path` override.
|
|
48
|
+
|
|
49
|
+
## Output files
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
.pi/grill-me/state.json # structured session state
|
|
53
|
+
GRILL-ME.md # default rendered result file
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
/grill-me Build a new plugin system for the app
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pi will ask one question at a time, provide a recommended answer, explore the codebase when possible, and save results when the interview is complete or when asked to stop/save.
|
package/index.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
interface GrillTurn {
|
|
7
|
+
question: string;
|
|
8
|
+
recommendedAnswer: string;
|
|
9
|
+
userAnswer?: string;
|
|
10
|
+
decisionStatus: "resolved" | "open" | "needs-codebase-check";
|
|
11
|
+
notes?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GrillState {
|
|
15
|
+
createdAt: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
projectDir: string;
|
|
18
|
+
plan: string;
|
|
19
|
+
turns: GrillTurn[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RecordTurnParams = Type.Object({
|
|
23
|
+
question: Type.String({ description: "The exact question asked, one question only." }),
|
|
24
|
+
recommendedAnswer: Type.String({ description: "The assistant's recommended answer to the question." }),
|
|
25
|
+
userAnswer: Type.Optional(Type.String({ description: "The user's answer, if already provided." })),
|
|
26
|
+
decisionStatus: Type.Union([
|
|
27
|
+
Type.Literal("resolved"),
|
|
28
|
+
Type.Literal("open"),
|
|
29
|
+
Type.Literal("needs-codebase-check"),
|
|
30
|
+
]),
|
|
31
|
+
notes: Type.Optional(Type.String({ description: "Short rationale, dependency, or follow-up notes." })),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const SaveResultsParams = Type.Object({
|
|
35
|
+
path: Type.Optional(Type.String({ description: "Relative output path. Defaults to GRILL-ME.md." })),
|
|
36
|
+
summary: Type.Optional(Type.String({ description: "Optional current shared-understanding summary." })),
|
|
37
|
+
agreedDecisions: Type.Optional(Type.Array(Type.String())),
|
|
38
|
+
openRisks: Type.Optional(Type.Array(Type.String())),
|
|
39
|
+
nextDecisionNeeded: Type.Optional(Type.String()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function stateDir(cwd: string): string {
|
|
43
|
+
return join(cwd, ".pi", "grill-me");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function statePath(cwd: string): string {
|
|
47
|
+
return join(stateDir(cwd), "state.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readState(cwd: string): Promise<GrillState | undefined> {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(await readFile(statePath(cwd), "utf8")) as GrillState;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeState(cwd: string, state: GrillState): Promise<void> {
|
|
59
|
+
await mkdir(stateDir(cwd), { recursive: true });
|
|
60
|
+
await writeFile(statePath(cwd), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderMarkdown(
|
|
64
|
+
state: GrillState,
|
|
65
|
+
extra: { summary?: string; agreedDecisions?: string[]; openRisks?: string[]; nextDecisionNeeded?: string },
|
|
66
|
+
): string {
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
lines.push("# Grill Me Results", "");
|
|
69
|
+
lines.push(`Generated: ${new Date().toISOString()}`, "");
|
|
70
|
+
lines.push("## Plan", "", state.plan.trim() || "_(none recorded)_", "");
|
|
71
|
+
if (extra.summary?.trim()) {
|
|
72
|
+
lines.push("## Shared Understanding", "", extra.summary.trim(), "");
|
|
73
|
+
}
|
|
74
|
+
lines.push("## Questions and Answers", "");
|
|
75
|
+
if (state.turns.length === 0) {
|
|
76
|
+
lines.push("_(No turns recorded.)", "");
|
|
77
|
+
} else {
|
|
78
|
+
state.turns.forEach((turn, index) => {
|
|
79
|
+
lines.push(`### ${index + 1}. ${turn.question}`, "");
|
|
80
|
+
lines.push(`**Recommended answer:** ${turn.recommendedAnswer || "_(none)_"}`, "");
|
|
81
|
+
lines.push(`**User answer:** ${turn.userAnswer || "_(not recorded)_"}`, "");
|
|
82
|
+
lines.push(`**Status:** ${turn.decisionStatus}`, "");
|
|
83
|
+
if (turn.notes?.trim()) lines.push(`**Notes:** ${turn.notes.trim()}`, "");
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (extra.agreedDecisions?.length) {
|
|
87
|
+
lines.push("## Agreed Decisions", "", ...extra.agreedDecisions.map((d) => `- ${d}`), "");
|
|
88
|
+
}
|
|
89
|
+
if (extra.openRisks?.length) {
|
|
90
|
+
lines.push("## Open Risks", "", ...extra.openRisks.map((r) => `- ${r}`), "");
|
|
91
|
+
}
|
|
92
|
+
if (extra.nextDecisionNeeded?.trim()) {
|
|
93
|
+
lines.push("## Next Decision Needed", "", extra.nextDecisionNeeded.trim(), "");
|
|
94
|
+
}
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safeOutputPath(cwd: string, input?: string): string {
|
|
99
|
+
const requested = input?.trim() || "GRILL-ME.md";
|
|
100
|
+
const absolute = resolve(cwd, requested);
|
|
101
|
+
const root = resolve(cwd);
|
|
102
|
+
if (absolute !== root && !absolute.startsWith(root + "/")) {
|
|
103
|
+
throw new Error(`Refusing to write outside project directory: ${requested}`);
|
|
104
|
+
}
|
|
105
|
+
return absolute;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function grillMeExtension(pi: ExtensionAPI) {
|
|
109
|
+
pi.registerCommand("grill-me", {
|
|
110
|
+
description: "Start a deterministic design interview and save results to Markdown",
|
|
111
|
+
handler: async (args, ctx) => {
|
|
112
|
+
const plan = args.trim() || "(No plan supplied yet. Ask the user to paste or describe the plan first.)";
|
|
113
|
+
const state: GrillState = {
|
|
114
|
+
createdAt: new Date().toISOString(),
|
|
115
|
+
updatedAt: new Date().toISOString(),
|
|
116
|
+
projectDir: ctx.cwd,
|
|
117
|
+
plan,
|
|
118
|
+
turns: [],
|
|
119
|
+
};
|
|
120
|
+
await writeState(ctx.cwd, state);
|
|
121
|
+
ctx.ui.notify(`Grill session initialized: ${statePath(ctx.cwd)}`, "info");
|
|
122
|
+
|
|
123
|
+
pi.sendUserMessage(`Start /grill-me for this plan:\n\n${plan}\n\nRules:\n- Interview me relentlessly about every aspect of this plan until we reach shared understanding.\n- Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.\n- Ask exactly one question at a time.\n- For each question, provide your recommended answer.\n- If a question can be answered by exploring the codebase, explore the codebase instead.\n- Use grill_record_turn after each question/answer decision is captured.\n- Use grill_save_results to save the results into a Markdown file in the project directory when enough understanding has been reached or when I ask to stop/save.`);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
pi.registerTool({
|
|
128
|
+
name: "grill_record_turn",
|
|
129
|
+
label: "Grill Record Turn",
|
|
130
|
+
description: "Record one /grill-me question, recommended answer, user answer, and decision status in project state.",
|
|
131
|
+
promptSnippet: "Record structured progress for an active /grill-me design interview",
|
|
132
|
+
promptGuidelines: [
|
|
133
|
+
"Use grill_record_turn after each /grill-me interview question is answered or resolved from codebase exploration.",
|
|
134
|
+
"Do not use grill_record_turn for more than one question at a time.",
|
|
135
|
+
],
|
|
136
|
+
parameters: RecordTurnParams,
|
|
137
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
138
|
+
const state = (await readState(ctx.cwd)) ?? {
|
|
139
|
+
createdAt: new Date().toISOString(),
|
|
140
|
+
updatedAt: new Date().toISOString(),
|
|
141
|
+
projectDir: ctx.cwd,
|
|
142
|
+
plan: "(state was created by grill_record_turn; no plan recorded)",
|
|
143
|
+
turns: [],
|
|
144
|
+
};
|
|
145
|
+
state.turns.push(params as GrillTurn);
|
|
146
|
+
state.updatedAt = new Date().toISOString();
|
|
147
|
+
await writeState(ctx.cwd, state);
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: `Recorded grill turn #${state.turns.length}` }],
|
|
150
|
+
details: { path: statePath(ctx.cwd), count: state.turns.length },
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
pi.registerTool({
|
|
156
|
+
name: "grill_save_results",
|
|
157
|
+
label: "Grill Save Results",
|
|
158
|
+
description: "Save the active /grill-me interview state as a Markdown file inside the project directory.",
|
|
159
|
+
promptSnippet: "Save /grill-me interview decisions and risks to Markdown in the project directory",
|
|
160
|
+
promptGuidelines: ["Use grill_save_results when the /grill-me interview is complete or the user asks to save/stop."],
|
|
161
|
+
parameters: SaveResultsParams,
|
|
162
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
163
|
+
const state = await readState(ctx.cwd);
|
|
164
|
+
if (!state) {
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: "No active /grill-me state found. Run /grill-me first." }],
|
|
167
|
+
isError: true,
|
|
168
|
+
details: { path: statePath(ctx.cwd) },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const outputPath = safeOutputPath(ctx.cwd, params.path);
|
|
172
|
+
await writeFile(outputPath, renderMarkdown(state, params), "utf8");
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: `Saved grill results to ${outputPath}` }],
|
|
175
|
+
details: { path: outputPath, turns: state.turns.length },
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firstpick/pi-extension-grill-me",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic design interview command and tools for Pi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"extension",
|
|
11
|
+
"design",
|
|
12
|
+
"interview"
|
|
13
|
+
],
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./index.ts"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
21
|
+
"typebox": "*"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.ts",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
]
|
|
28
|
+
}
|