@shanvit7/poiesis 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 +303 -0
- package/index.ts +534 -0
- package/package.json +59 -0
- package/prompts/implement.md +21 -0
- package/prompts/plan.md +10 -0
- package/prompts/prereq.md +15 -0
- package/prompts/theory.md +32 -0
- package/prompts/write-tests.md +8 -0
- package/skills/agent-browser/SKILL.md +143 -0
- package/src/chapter.ts +624 -0
- package/src/config.ts +15 -0
- package/src/onboarding.ts +57 -0
- package/src/progress.ts +94 -0
- package/src/project.ts +311 -0
- package/src/steps.ts +66 -0
- package/src/types.ts +67 -0
- package/src/utils.ts +164 -0
package/index.ts
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { DynamicBorder, getSelectListTheme } from "@earendil-works/pi-coding-agent"
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
5
|
+
import { Container, type SelectItem, SelectList, Spacer, Text } from "@earendil-works/pi-tui"
|
|
6
|
+
import { Type } from "typebox"
|
|
7
|
+
import {
|
|
8
|
+
appendReflection,
|
|
9
|
+
appendTddStatus,
|
|
10
|
+
buildChapterContext,
|
|
11
|
+
checkOffChapter,
|
|
12
|
+
runChapter,
|
|
13
|
+
writeTddSection,
|
|
14
|
+
} from "./src/chapter.ts"
|
|
15
|
+
import { PROFILE_PATH, needsOnboarding, runOnboarding } from "./src/onboarding.ts"
|
|
16
|
+
import { advanceChapter, markTestsPass, readProgress, setChapterMeta } from "./src/progress.ts"
|
|
17
|
+
import { runProject } from "./src/project.ts"
|
|
18
|
+
import {
|
|
19
|
+
implementPrompt,
|
|
20
|
+
planPrompt,
|
|
21
|
+
theoryPrompt as theoryStepPrompt,
|
|
22
|
+
writeTestsPrompt,
|
|
23
|
+
} from "./src/steps.ts"
|
|
24
|
+
import type { ChapterStep, UserProfile } from "./src/types.ts"
|
|
25
|
+
import {
|
|
26
|
+
expandHome,
|
|
27
|
+
findActiveProject,
|
|
28
|
+
readChapterState,
|
|
29
|
+
readJson,
|
|
30
|
+
run,
|
|
31
|
+
writeChapterState,
|
|
32
|
+
writeJson,
|
|
33
|
+
} from "./src/utils.ts"
|
|
34
|
+
|
|
35
|
+
// ── Session state — set by runChapter, used by gate tools + event handlers ────────
|
|
36
|
+
let _projectDir: string | null = null
|
|
37
|
+
let _chapterNum = 0
|
|
38
|
+
|
|
39
|
+
const setSessionState = (dir: string, n: number): void => {
|
|
40
|
+
_projectDir = dir
|
|
41
|
+
_chapterNum = n
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const extension = (pi: ExtensionAPI): void => {
|
|
45
|
+
// ── Tool: poiesis_save_profile ────────────────────────────────────────────
|
|
46
|
+
pi.registerTool({
|
|
47
|
+
name: "poiesis_save_profile",
|
|
48
|
+
label: "Poiesis: Save Profile",
|
|
49
|
+
description: "Save the user profile once all fields are known from the conversation.",
|
|
50
|
+
parameters: Type.Object({
|
|
51
|
+
primaryStack: Type.Array(Type.String(), { description: "Languages and frameworks they use" }),
|
|
52
|
+
recentProjects: Type.Array(
|
|
53
|
+
Type.Object({
|
|
54
|
+
name: Type.String({ description: "Repo or directory name" }),
|
|
55
|
+
summary: Type.String({ description: "One sentence: what was built and key tech used" }),
|
|
56
|
+
stack: Type.Array(Type.String(), { description: "Languages/frameworks in this project" }),
|
|
57
|
+
})
|
|
58
|
+
),
|
|
59
|
+
recentActivity: Type.String({ description: "One-line summary" }),
|
|
60
|
+
}),
|
|
61
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
62
|
+
const profile: UserProfile = { ...params, scannedAt: new Date().toISOString() }
|
|
63
|
+
writeJson(PROFILE_PATH, profile)
|
|
64
|
+
ctx.ui.notify("✅ Profile saved — run /poiesis to start your project.", "info")
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{ type: "text" as const, text: "Profile saved. Tell the user to run /poiesis now." },
|
|
68
|
+
],
|
|
69
|
+
details: {},
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// ── Tool: poiesis_confirm_test_plan ───────────────────────────────────
|
|
75
|
+
// Shows the full test plan + choice in a full-screen TUI (no overlay) — owns keyboard focus.
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: "poiesis_confirm_test_plan",
|
|
78
|
+
label: "Poiesis: Review Test Plan",
|
|
79
|
+
description:
|
|
80
|
+
"Show the proposed test plan to the student in a scrollable TUI dialog. " +
|
|
81
|
+
"Use this INSTEAD of ask_user_question for all test-plan confirmation steps.",
|
|
82
|
+
parameters: Type.Object({
|
|
83
|
+
chapterNum: Type.Number({ description: "Chapter number" }),
|
|
84
|
+
intro: Type.String({
|
|
85
|
+
description: "1\u20132 sentences: what the student will build by the end",
|
|
86
|
+
}),
|
|
87
|
+
tests: Type.Array(
|
|
88
|
+
Type.Object({
|
|
89
|
+
name: Type.String({ description: "Short test name in plain words" }),
|
|
90
|
+
why: Type.String({ description: "One sentence: what it checks and why it matters" }),
|
|
91
|
+
}),
|
|
92
|
+
{ minItems: 1, maxItems: 8, description: "3\u20135 test checkpoints" }
|
|
93
|
+
),
|
|
94
|
+
}),
|
|
95
|
+
async execute(_id, { chapterNum, intro, tests }, _signal, _onUpdate, ctx) {
|
|
96
|
+
type Choice = "proceed" | "add" | "skip"
|
|
97
|
+
|
|
98
|
+
// No overlay: true — plain custom() replaces the full TUI and owns keyboard focus.
|
|
99
|
+
// overlay: true renders on top but never steals focus, so SelectList is un-navigable.
|
|
100
|
+
const result = await ctx.ui.custom<Choice | null>((tui, theme, _kb, done) => {
|
|
101
|
+
const root = new Container()
|
|
102
|
+
const slTheme = getSelectListTheme()
|
|
103
|
+
|
|
104
|
+
// Header
|
|
105
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)))
|
|
106
|
+
root.addChild(
|
|
107
|
+
new Text(theme.fg("accent", theme.bold(`Chapter ${chapterNum} \u2014 Test Plan`)), 1, 0)
|
|
108
|
+
)
|
|
109
|
+
root.addChild(new Spacer(1))
|
|
110
|
+
root.addChild(new Text(theme.fg("text", intro), 1, 0))
|
|
111
|
+
root.addChild(new Spacer(1))
|
|
112
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)))
|
|
113
|
+
|
|
114
|
+
// Test list
|
|
115
|
+
root.addChild(new Spacer(1))
|
|
116
|
+
root.addChild(new Text(theme.fg("accent", "Learning checkpoints:"), 1, 0))
|
|
117
|
+
root.addChild(new Spacer(1))
|
|
118
|
+
tests.forEach((t, i) => {
|
|
119
|
+
root.addChild(new Text(theme.bold(` ${i + 1}. ${t.name}`), 1, 0))
|
|
120
|
+
root.addChild(new Text(theme.fg("muted", ` ${t.why}`), 1, 0))
|
|
121
|
+
if (i < tests.length - 1) root.addChild(new Spacer(1))
|
|
122
|
+
})
|
|
123
|
+
root.addChild(new Spacer(1))
|
|
124
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)))
|
|
125
|
+
|
|
126
|
+
// Choices
|
|
127
|
+
const items: SelectItem[] = [
|
|
128
|
+
{
|
|
129
|
+
value: "proceed",
|
|
130
|
+
label: "Looks good \u2014 write the tests",
|
|
131
|
+
description: "Lock in this plan and start building",
|
|
132
|
+
},
|
|
133
|
+
{ value: "add", label: "Add a checkpoint", description: "Tell me what else to verify" },
|
|
134
|
+
{ value: "skip", label: "Remove one", description: "Tell me which one to drop" },
|
|
135
|
+
]
|
|
136
|
+
const list = new SelectList(items, items.length, slTheme)
|
|
137
|
+
list.onSelect = (item) => done(item.value as Choice)
|
|
138
|
+
list.onCancel = () => done(null)
|
|
139
|
+
root.addChild(list)
|
|
140
|
+
|
|
141
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)))
|
|
142
|
+
root.addChild(
|
|
143
|
+
new Text(
|
|
144
|
+
theme.fg("dim", "\u2191\u2193 navigate \u00b7 Enter select \u00b7 Esc cancel"),
|
|
145
|
+
1,
|
|
146
|
+
0
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
render: (w: number) => root.render(w),
|
|
152
|
+
invalidate: () => root.invalidate(),
|
|
153
|
+
handleInput: (data: string) => {
|
|
154
|
+
list.handleInput(data)
|
|
155
|
+
tui.requestRender()
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (result === null) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text" as const,
|
|
165
|
+
text: "Student cancelled. Ask them how they want to proceed.",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
details: { action: "cancelled" },
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (result === "proceed") {
|
|
173
|
+
if (_projectDir) {
|
|
174
|
+
writeChapterState(_projectDir, _chapterNum, { step: "write-tests", testsPlan: tests })
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{ type: "text" as const, text: `Test plan confirmed.\n\n${writeTestsPrompt(tests)}` },
|
|
179
|
+
],
|
|
180
|
+
details: { action: "proceed", testCount: tests.length },
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const msgs: Record<"add" | "skip", string> = {
|
|
185
|
+
add:
|
|
186
|
+
"Student wants to add a checkpoint. Ask them: \u2018What behaviour would you like to also verify?\u2019 " +
|
|
187
|
+
"Then call poiesis_confirm_test_plan again with the new test appended.",
|
|
188
|
+
skip:
|
|
189
|
+
"Student wants to remove a test. Ask them which one, then call poiesis_confirm_test_plan " +
|
|
190
|
+
"again without that test.",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text" as const, text: msgs[result as "add" | "skip"] }],
|
|
195
|
+
details: { action: result, testCount: tests.length },
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// ── Tool: poiesis_run_tests ───────────────────────────────────────────────
|
|
201
|
+
pi.registerTool({
|
|
202
|
+
name: "poiesis_run_tests",
|
|
203
|
+
label: "Poiesis: Run Tests",
|
|
204
|
+
description: "Run the chapter's test suite. Returns pass/fail + output.",
|
|
205
|
+
parameters: Type.Object({
|
|
206
|
+
chapter: Type.Number({ description: "Chapter number" }),
|
|
207
|
+
cmd: Type.String({ description: "e.g. 'npx vitest run tests/chapter-1.test.ts'" }),
|
|
208
|
+
}),
|
|
209
|
+
async execute(_id, { chapter, cmd }, _signal, _onUpdate, ctx) {
|
|
210
|
+
const projectDir = findActiveProject(ctx.cwd)
|
|
211
|
+
if (!projectDir) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text" as const, text: "No active project found in cwd." }],
|
|
214
|
+
details: { pass: false },
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const output = run(cmd, projectDir)
|
|
219
|
+
markTestsPass(projectDir, chapter)
|
|
220
|
+
appendTddStatus(projectDir, chapter, "🟢 passing")
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text" as const, text: `PASS\n${output}` }],
|
|
223
|
+
details: { pass: true },
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const out = String(e)
|
|
227
|
+
appendTddStatus(projectDir, chapter, "🔴 failing")
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: "text" as const, text: `FAIL\n${out}` }],
|
|
230
|
+
details: { pass: false },
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// ── Tool: poiesis_chapter_done (gated) ───────────────────────────────────
|
|
237
|
+
pi.registerTool({
|
|
238
|
+
name: "poiesis_chapter_done",
|
|
239
|
+
label: "Poiesis: Chapter Done",
|
|
240
|
+
description:
|
|
241
|
+
"Mark the current chapter complete. GATED — rejected if tests are not passing (unless theory chapter). Always call poiesis_run_tests first for code chapters.",
|
|
242
|
+
parameters: Type.Object({
|
|
243
|
+
reflection: Type.String({
|
|
244
|
+
description: "2–4 sentences on what the user learned and any struggles.",
|
|
245
|
+
}),
|
|
246
|
+
}),
|
|
247
|
+
async execute(_id, { reflection }, _signal, _onUpdate, ctx) {
|
|
248
|
+
const projectDir = findActiveProject(ctx.cwd)
|
|
249
|
+
if (!projectDir) {
|
|
250
|
+
return {
|
|
251
|
+
content: [{ type: "text" as const, text: "No active project found in cwd." }],
|
|
252
|
+
details: { error: "no_project" },
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const p = readProgress(projectDir)
|
|
257
|
+
const chData = p.chapters[p.current]
|
|
258
|
+
|
|
259
|
+
// ── GATE: tests must be green for code/mixed chapters ──────────────
|
|
260
|
+
if (chData?.type !== "theory" && !chData?.testsPass) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: "text" as const,
|
|
265
|
+
text: "Cannot complete chapter — tests are not passing. Run poiesis_run_tests first.",
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
details: { error: "tests_not_passing" },
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const completedChapter = p.current
|
|
273
|
+
appendReflection(projectDir, completedChapter, reflection)
|
|
274
|
+
checkOffChapter(projectDir, completedChapter, chData?.type ?? "code")
|
|
275
|
+
advanceChapter(projectDir)
|
|
276
|
+
|
|
277
|
+
const isLast = completedChapter >= p.total
|
|
278
|
+
const msg = isLast
|
|
279
|
+
? `🎉 All ${p.total} chapters complete! Project finished.`
|
|
280
|
+
: `✓ Chapter ${completedChapter} done — chapter ${completedChapter + 1} is next. Run /poiesis to continue.`
|
|
281
|
+
|
|
282
|
+
ctx.ui.notify(msg, "info")
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text" as const, text: msg }],
|
|
285
|
+
details: { completedChapter, nextChapter: isLast ? null : completedChapter + 1 },
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// ── Tool: poiesis_prereq_done ──────────────────────────────────────────────
|
|
291
|
+
pi.registerTool({
|
|
292
|
+
name: "poiesis_prereq_done",
|
|
293
|
+
label: "Poiesis: Prereq Gate Done",
|
|
294
|
+
description:
|
|
295
|
+
"Call after the prerequisite calibration step completes. " +
|
|
296
|
+
"Stores the result and fires the theory step prompt.",
|
|
297
|
+
parameters: Type.Object({
|
|
298
|
+
result: Type.Union([Type.Literal("familiar"), Type.Literal("primed")], {
|
|
299
|
+
description: '"familiar" = tech is in student stack; "primed" = needed primer first',
|
|
300
|
+
}),
|
|
301
|
+
}),
|
|
302
|
+
async execute(_id, { result }, _signal, _onUpdate, _ctx) {
|
|
303
|
+
if (!_projectDir)
|
|
304
|
+
return { content: [{ type: "text" as const, text: "No active chapter." }], details: {} }
|
|
305
|
+
writeChapterState(_projectDir, _chapterNum, { step: "theory", prereqResult: result })
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text" as const,
|
|
310
|
+
text: `Prereq: ${result}.\n\n${theoryStepPrompt(result, _chapterNum)}`,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
details: { result },
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
// ── Tool: poiesis_theory_done ───────────────────────────────────────────────
|
|
319
|
+
pi.registerTool({
|
|
320
|
+
name: "poiesis_theory_done",
|
|
321
|
+
label: "Poiesis: Theory Done",
|
|
322
|
+
description:
|
|
323
|
+
"Call when the student has demonstrated understanding of the theory concepts. " +
|
|
324
|
+
"Fires the test-plan step.",
|
|
325
|
+
parameters: Type.Object({}),
|
|
326
|
+
async execute(_id, _params, _signal, _onUpdate, _ctx) {
|
|
327
|
+
if (!_projectDir)
|
|
328
|
+
return { content: [{ type: "text" as const, text: "No active chapter." }], details: {} }
|
|
329
|
+
writeChapterState(_projectDir, _chapterNum, { step: "plan" })
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text" as const, text: `Theory done.\n\n${planPrompt(_chapterNum)}` }],
|
|
332
|
+
details: {},
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// ── Tool: poiesis_tests_written ─────────────────────────────────────────────
|
|
338
|
+
pi.registerTool({
|
|
339
|
+
name: "poiesis_tests_written",
|
|
340
|
+
label: "Poiesis: Tests Written",
|
|
341
|
+
description: "Call after writing the test file. Stores the path and fires the implement step.",
|
|
342
|
+
parameters: Type.Object({
|
|
343
|
+
testsFile: Type.String({
|
|
344
|
+
description: "Path to the test file, e.g. tests/chapter-3.test.ts",
|
|
345
|
+
}),
|
|
346
|
+
testNames: Type.Array(Type.String(), { description: "List of test names as written" }),
|
|
347
|
+
}),
|
|
348
|
+
async execute(_id, { testsFile, testNames }, _signal, _onUpdate, _ctx) {
|
|
349
|
+
if (!_projectDir)
|
|
350
|
+
return { content: [{ type: "text" as const, text: "No active chapter." }], details: {} }
|
|
351
|
+
writeChapterState(_projectDir, _chapterNum, { step: "implement", testsFile })
|
|
352
|
+
writeTddSection(_projectDir, _chapterNum, testsFile, testNames)
|
|
353
|
+
setChapterMeta(_projectDir, _chapterNum, "code", testsFile)
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: "text" as const,
|
|
358
|
+
text: `Tests written at ${testsFile}.\n\n${implementPrompt(testsFile, _chapterNum)}`,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
details: { testsFile },
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// ── Bash pre-run review ─────────────────────────────────────────────
|
|
367
|
+
// ponytail: safe-list = language-agnostic OS read-only commands only
|
|
368
|
+
const SAFE_CMD =
|
|
369
|
+
/^(cat |ls(?:$| )|grep |find |head |tail |wc |pwd$|echo [^>|]+$|diff |type |which |env$|printenv)/
|
|
370
|
+
// agent-browser is trusted — runs without review
|
|
371
|
+
const AGENT_TOOL = /^agent-browser /
|
|
372
|
+
|
|
373
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
374
|
+
if (!_projectDir) return // only active during a Poiesis chapter session
|
|
375
|
+
if (event.toolName !== "bash") return
|
|
376
|
+
const cmd = ((event.input as Record<string, unknown>).command as string | undefined) ?? ""
|
|
377
|
+
if (!cmd || SAFE_CMD.test(cmd.trimStart()) || AGENT_TOOL.test(cmd.trimStart())) return // auto-proceed for safe/trusted commands
|
|
378
|
+
|
|
379
|
+
type BashChoice = "run" | "skip" | "explain" | "steer"
|
|
380
|
+
|
|
381
|
+
const result = await ctx.ui.custom<BashChoice | null>((tui, theme, _kb, done) => {
|
|
382
|
+
const root = new Container()
|
|
383
|
+
|
|
384
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)))
|
|
385
|
+
root.addChild(new Text(theme.fg("accent", theme.bold("⚡ Command Review")), 1, 0))
|
|
386
|
+
root.addChild(new Spacer(1))
|
|
387
|
+
|
|
388
|
+
// Show up to 3 wrapped lines of the command
|
|
389
|
+
const chunks = cmd
|
|
390
|
+
.split("\n")
|
|
391
|
+
.flatMap((l) => {
|
|
392
|
+
const parts: string[] = []
|
|
393
|
+
for (let i = 0; i < l.length; i += 100) parts.push(l.slice(i, i + 100))
|
|
394
|
+
return parts
|
|
395
|
+
})
|
|
396
|
+
.slice(0, 4)
|
|
397
|
+
for (const line of chunks) root.addChild(new Text(theme.fg("text", ` ${line}`), 1, 0))
|
|
398
|
+
if (cmd.length > 400) root.addChild(new Text(theme.fg("muted", " … (truncated)"), 1, 0))
|
|
399
|
+
|
|
400
|
+
root.addChild(new Spacer(1))
|
|
401
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)))
|
|
402
|
+
|
|
403
|
+
const items: SelectItem[] = [
|
|
404
|
+
{ value: "run", label: "✅ Run it", description: "Execute this command" },
|
|
405
|
+
{
|
|
406
|
+
value: "steer",
|
|
407
|
+
label: "🔀 Steer",
|
|
408
|
+
description: "Give the agent a correction (e.g. use pnpm, not npm)",
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
value: "skip",
|
|
412
|
+
label: "❌ Skip — don't run this",
|
|
413
|
+
description: "Block without explanation",
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
value: "explain",
|
|
417
|
+
label: "❓ Explain first",
|
|
418
|
+
description: "Agent explains what this does, then re-proposes",
|
|
419
|
+
},
|
|
420
|
+
]
|
|
421
|
+
const list = new SelectList(items, items.length, getSelectListTheme())
|
|
422
|
+
list.onSelect = (item) => done(item.value as BashChoice)
|
|
423
|
+
list.onCancel = () => done("skip") // Esc = skip (safe default)
|
|
424
|
+
root.addChild(list)
|
|
425
|
+
|
|
426
|
+
root.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)))
|
|
427
|
+
root.addChild(new Text(theme.fg("dim", "↑↓ navigate · Enter select · Esc = skip"), 1, 0))
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
render: (w: number) => root.render(w),
|
|
431
|
+
invalidate: () => root.invalidate(),
|
|
432
|
+
handleInput: (data: string) => {
|
|
433
|
+
list.handleInput(data)
|
|
434
|
+
tui.requestRender()
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
if (result === "run") return // allow through
|
|
440
|
+
if (result === "steer") {
|
|
441
|
+
const instruction = await ctx.ui.input("Steer the agent", "e.g. use pnpm instead of npm")
|
|
442
|
+
const msg = instruction?.trim()
|
|
443
|
+
return {
|
|
444
|
+
block: true,
|
|
445
|
+
reason: msg
|
|
446
|
+
? `⚠️ USER CORRECTION: "${msg}"\n\nDo NOT run the blocked command. Follow the user's instruction instead: "${msg}". Adjust your approach and continue.`
|
|
447
|
+
: "User chose not to run this command.",
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (result === "explain") {
|
|
451
|
+
const preview = cmd.length > 200 ? `${cmd.slice(0, 200)}…` : cmd
|
|
452
|
+
return {
|
|
453
|
+
block: true,
|
|
454
|
+
reason:
|
|
455
|
+
`⚠️ EXPLANATION REQUIRED — do not proceed until you have explained this.\n\nCommand blocked:\n\`${preview}\`\n\n` +
|
|
456
|
+
`1. Explain in 2–3 sentences: what it does, why it\'s needed now, and whether it\'s reversible.\n` +
|
|
457
|
+
`2. After explaining, propose running it again.\n` +
|
|
458
|
+
`Do NOT skip to another task.`,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// skip or null → block
|
|
462
|
+
return { block: true, reason: "User chose not to run this command." }
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// ── before_agent_start: inject chapter context into system prompt ──────────
|
|
466
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
467
|
+
if (!_projectDir) return
|
|
468
|
+
const profile = readJson<UserProfile>(PROFILE_PATH)
|
|
469
|
+
const chPath = join(
|
|
470
|
+
expandHome(_projectDir),
|
|
471
|
+
".poiesis",
|
|
472
|
+
"chapters",
|
|
473
|
+
`chapter-${_chapterNum}.md`
|
|
474
|
+
)
|
|
475
|
+
const chapterMd = existsSync(chPath) ? readFileSync(chPath, "utf8") : ""
|
|
476
|
+
const context = buildChapterContext(_projectDir, chapterMd, profile, _chapterNum)
|
|
477
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${context}` }
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// ── session_before_compact: chapter-aware summary ──────────────────────────
|
|
481
|
+
const STEP_NEXT: Record<ChapterStep, string> = {
|
|
482
|
+
prereq: "call poiesis_prereq_done",
|
|
483
|
+
theory: "finish quiz \u2192 call poiesis_theory_done",
|
|
484
|
+
plan: "call poiesis_confirm_test_plan",
|
|
485
|
+
"write-tests": "write test file \u2192 call poiesis_tests_written",
|
|
486
|
+
implement: "make tests pass \u2192 call poiesis_run_tests",
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
pi.on("session_before_compact", async (event, _ctx) => {
|
|
490
|
+
if (!_projectDir) return
|
|
491
|
+
const state = readChapterState(_projectDir, _chapterNum)
|
|
492
|
+
if (!state) return
|
|
493
|
+
const planNames = state.testsPlan.map((t) => t.name).join(", ") || "(not confirmed yet)"
|
|
494
|
+
const summary = [
|
|
495
|
+
`## Poiesis \u2014 Chapter ${_chapterNum} (active session)`,
|
|
496
|
+
`Step: ${state.step} | Prereq: ${state.prereqResult ?? "n/a"} | Tests file: ${state.testsFile ?? "(not written yet)"}`,
|
|
497
|
+
`Tests passing: ${state.testsPass}`,
|
|
498
|
+
`Test plan: ${planNames}`,
|
|
499
|
+
`Next action: ${STEP_NEXT[state.step]}`,
|
|
500
|
+
"Chapter content is re-injected by before_agent_start on every turn.",
|
|
501
|
+
].join("\n")
|
|
502
|
+
return {
|
|
503
|
+
compaction: {
|
|
504
|
+
summary,
|
|
505
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
506
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// ── /poiesis ──────────────────────────────────────────────────────────────
|
|
512
|
+
pi.registerCommand("poiesis", {
|
|
513
|
+
description: "Poiesis — onboard, start a project, or continue the active chapter",
|
|
514
|
+
handler: async (_args, ctx) => {
|
|
515
|
+
if (needsOnboarding()) {
|
|
516
|
+
await runOnboarding(pi, ctx)
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Resume active project if one exists in cwd
|
|
521
|
+
const projectDir = findActiveProject(ctx.cwd)
|
|
522
|
+
if (projectDir) {
|
|
523
|
+
const profile = readJson<UserProfile>(PROFILE_PATH)
|
|
524
|
+
await runChapter(pi, ctx, profile, projectDir, setSessionState)
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// No active project — scaffold a new one
|
|
529
|
+
await runProject(pi, ctx)
|
|
530
|
+
},
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export default extension
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shanvit7/poiesis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension — YouTube tutorial tutor. Ingest any coding video, grill the user, scaffold the project, and generate chapter-by-chapter lab guides.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"pi-skill",
|
|
9
|
+
"poiesis",
|
|
10
|
+
"tutor",
|
|
11
|
+
"youtube",
|
|
12
|
+
"coding-tutorial"
|
|
13
|
+
],
|
|
14
|
+
"author": "shanvit7",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./index.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"index.ts",
|
|
20
|
+
"src",
|
|
21
|
+
"prompts",
|
|
22
|
+
"skills",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts",
|
|
32
|
+
"node_modules/@juicesharp/rpiv-ask-user-question/index.ts"
|
|
33
|
+
],
|
|
34
|
+
"skills": [
|
|
35
|
+
"./skills"
|
|
36
|
+
],
|
|
37
|
+
"prompts": [
|
|
38
|
+
"./prompts"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@google/genai": "^1.0.0",
|
|
43
|
+
"@juicesharp/rpiv-ask-user-question": "^1.20.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
47
|
+
"typebox": "*"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^26.1.1",
|
|
51
|
+
"promptfoo": "^0.121.19",
|
|
52
|
+
"typescript": "^7.0.2"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"eval": "promptfoo eval",
|
|
56
|
+
"eval:view": "promptfoo view",
|
|
57
|
+
"eval:ci": "promptfoo eval --ci --no-cache"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Test file: {{testsFile}}
|
|
2
|
+
|
|
3
|
+
Before each code block: one sentence on WHAT you're adding and WHY.
|
|
4
|
+
|
|
5
|
+
⚠️ CRITICAL RULES:
|
|
6
|
+
1. YOU run every shell command via bash. Student NEVER runs commands manually.
|
|
7
|
+
2. Interactive CLIs (npm create, create-vite): use non-interactive flags.
|
|
8
|
+
3. Student HITL = design decisions only (ask_user_question). Not commands.
|
|
9
|
+
4. LIVE WEB RESEARCH: use agent-browser instead of guessing.
|
|
10
|
+
- Teaching → look up official docs/spec first (MDN, framework docs, stdlib)
|
|
11
|
+
- Student challenges a claim → open the authoritative source, quote it
|
|
12
|
+
- "I think..." → stop and verify first
|
|
13
|
+
- Unfamiliar package/API → pi.dev/packages, npmjs.com, or GitHub README
|
|
14
|
+
- Unfamiliar error → look it up, don't guess the cause
|
|
15
|
+
5. Wrong design by student → implement it → call poiesis_run_tests → show failure → correct.
|
|
16
|
+
6. During implement: respond in minimal prose. One action per message. No preamble.
|
|
17
|
+
Format: [what you did] → [result]. Code first.
|
|
18
|
+
7. Command fails 3 times → explain and ask student. Otherwise handle silently.
|
|
19
|
+
|
|
20
|
+
Make {{testsFile}} pass. Do not modify the test file.
|
|
21
|
+
Call poiesis_run_tests with chapter={{chapterNum}} and cmd="<runner> <file>" when ready.
|
package/prompts/plan.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Propose 3–5 tests as learning checkpoints. Call poiesis_confirm_test_plan with:
|
|
2
|
+
- chapterNum: {{chapterNum}}
|
|
3
|
+
- intro: 1–2 sentences on what the student will have built by the end
|
|
4
|
+
- tests: array of { name, why } — name in plain words, why explains what it proves
|
|
5
|
+
|
|
6
|
+
Do NOT write the list in chat first. Do NOT call ask_user_question.
|
|
7
|
+
The tool renders its own full-screen dialog. Just call the tool.
|
|
8
|
+
|
|
9
|
+
If the tool returns "add": ask the student what to add, then re-call with it appended.
|
|
10
|
+
If "skip": ask which one, then re-call without it.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
The student's profile:
|
|
2
|
+
{{profileContext}}
|
|
3
|
+
|
|
4
|
+
Look at this chapter's primary tech. Does it appear in their known stack or project summaries?
|
|
5
|
+
|
|
6
|
+
FAMILIAR (tech is in their stack or a recent project uses it):
|
|
7
|
+
→ Call poiesis_prereq_done with result="familiar"
|
|
8
|
+
→ (No questions needed — move straight to theory with tradeoff-focused depth.)
|
|
9
|
+
|
|
10
|
+
UNFAMILIAR (tech NOT in their known stack or projects):
|
|
11
|
+
→ Ask 2–3 prerequisite questions via ask_user_question first.
|
|
12
|
+
→ Then call poiesis_prereq_done with result="primed" regardless of score.
|
|
13
|
+
→ (Score only calibrates depth — never blocks progress.)
|
|
14
|
+
|
|
15
|
+
Never block progress. The goal is calibration only.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Prereq result: {{prereqResult}}
|
|
2
|
+
|
|
3
|
+
Introduce what will be built by the end of this chapter (2–3 sentences).
|
|
4
|
+
Then explain the core "what and why" — prose first, no code during initial explanation.
|
|
5
|
+
- familiar: tradeoffs and edge cases only
|
|
6
|
+
- primed: full explanation with an analogy
|
|
7
|
+
|
|
8
|
+
⚠️ LIVE RESEARCH RULE — you are a tutor, not a static knowledge base:
|
|
9
|
+
- Before explaining any concept: look up the official docs or spec with agent-browser so
|
|
10
|
+
the explanation is accurate, not a recollection.
|
|
11
|
+
- Student challenges a claim: open the authoritative source live and quote it.
|
|
12
|
+
Never argue from memory.
|
|
13
|
+
- Catch yourself saying "I think": stop and verify first.
|
|
14
|
+
|
|
15
|
+
Quiz with 1–2 questions via ask_user_question.
|
|
16
|
+
|
|
17
|
+
WRONG-ANSWER TDD PATH — if the student answers a code question incorrectly:
|
|
18
|
+
1. Say "Let's see what happens" — don't reveal the answer
|
|
19
|
+
2. Implement their wrong answer via write/bash tools
|
|
20
|
+
3. Call poiesis_run_tests — the test will fail
|
|
21
|
+
4. Show failure in one sentence, explain why in 2–3 sentences
|
|
22
|
+
5. Revert to the correct implementation
|
|
23
|
+
|
|
24
|
+
If the student asks follow-up questions, answer them — but once they demonstrate
|
|
25
|
+
understanding (correct answer OR sustained engagement showing comprehension):
|
|
26
|
+
|
|
27
|
+
1. **If the concept is code-applicable** — write a minimal working code snippet
|
|
28
|
+
directly into the project (the actual source file it belongs in).
|
|
29
|
+
Run it via bash so the student sees real output. This is the first real code of the chapter
|
|
30
|
+
— it should be a foundation the implementation step will build on top of.
|
|
31
|
+
Skip this step only if the concept is purely conceptual (architecture, tradeoffs, mental models).
|
|
32
|
+
2. Call poiesis_theory_done()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Tests confirmed:
|
|
2
|
+
{{testsPlan}}
|
|
3
|
+
|
|
4
|
+
Choose the right test framework for this chapter's tech. Write the test file.
|
|
5
|
+
Name tests after behaviour: server_returns_200_on_root not test_server.
|
|
6
|
+
No mocking unless I/O is the point.
|
|
7
|
+
|
|
8
|
+
When written: call poiesis_tests_written with the file path and test names.
|