@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/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.
@@ -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.