@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/src/chapter.ts ADDED
@@ -0,0 +1,624 @@
1
+ import { readFileSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
4
+ import { GoogleGenAI } from "@google/genai"
5
+ import { setChapterMeta } from "./progress.ts"
6
+ import { readProgress } from "./progress.ts"
7
+ import {
8
+ implementPrompt,
9
+ planPrompt,
10
+ prereqPrompt,
11
+ theoryPrompt as theoryStepPrompt,
12
+ writeTestsPrompt,
13
+ } from "./steps.ts"
14
+ import type {
15
+ ChapterKind,
16
+ ChapterState,
17
+ ChapterStep,
18
+ RecentProject,
19
+ Roadmap,
20
+ UserProfile,
21
+ } from "./types.ts"
22
+ import { exists, expandHome, readChapterState, writeChapterState } from "./utils.ts"
23
+
24
+ // ── Helpers ───────────────────────────────────────────────────────────────────
25
+
26
+ const chaptersDir = (projectDir: string): string =>
27
+ join(expandHome(projectDir), ".poiesis", "chapters")
28
+
29
+ const chapterFile = (projectDir: string, n: number): string =>
30
+ join(chaptersDir(projectDir), `chapter-${n}.md`)
31
+
32
+ const roadmapJsonFile = (projectDir: string): string =>
33
+ join(chaptersDir(projectDir), "roadmap.json")
34
+ const techstackFile = (projectDir: string): string => join(chaptersDir(projectDir), "techstack.md")
35
+
36
+ const readFile = (path: string): string => readFileSync(path, "utf8")
37
+ const writeFile = (path: string, content: string): void => writeFileSync(path, content, "utf8")
38
+
39
+ // ── classifyChapter ───────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * One Gemini call — classifies a chapter as "code" | "theory".
43
+ * ponytail: mixed removed — any chapter with runnable output is "code".
44
+ */
45
+ export const classifyChapter = async (
46
+ chapterMd: string,
47
+ primaryStack: string[]
48
+ ): Promise<ChapterKind> => {
49
+ const apiKey = process.env.GEMINI_API_KEY
50
+ if (!apiKey) throw new Error("GEMINI_API_KEY is not set")
51
+
52
+ const ai = new GoogleGenAI({ apiKey })
53
+ const result = await ai.models.generateContent({
54
+ model: "gemini-2.0-flash",
55
+ contents: [
56
+ {
57
+ role: "user",
58
+ parts: [
59
+ {
60
+ text: `Classify this tutorial chapter. Primary stack: ${primaryStack.join(", ")}.
61
+
62
+ Reply with exactly one word: "code" or "theory".
63
+
64
+ - "code": chapter involves writing runnable code — even if it also explains concepts
65
+ - "theory": chapter is PURELY conceptual — no runnable output whatsoever
66
+
67
+ Chapter content:
68
+ ${chapterMd}`,
69
+ },
70
+ ],
71
+ },
72
+ ],
73
+ })
74
+
75
+ const raw = (result.text ?? "").trim().toLowerCase()
76
+ if (raw === "theory") return "theory"
77
+ return "code" // ponytail: default — any ambiguity leans code; mixed → code
78
+ }
79
+
80
+ // ── TDD file operations ───────────────────────────────────────────────────────
81
+
82
+ /**
83
+ * Append a ## TDD section to chapter-N.md after classification + test writing.
84
+ * Called once, before the RED phase begins.
85
+ */
86
+ export const writeTddSection = (
87
+ projectDir: string,
88
+ chapter: number,
89
+ testsFile: string,
90
+ testNames: string[]
91
+ ): void => {
92
+ const path = chapterFile(projectDir, chapter)
93
+ const existing = readFile(path)
94
+ const section = `\n## TDD\n- Test file: \`${testsFile}\`\n- Status: 🟡 written, not passing\n- Tests: ${testNames.join(", ")}\n`
95
+ writeFile(path, `${existing}${section}`)
96
+ }
97
+
98
+ /**
99
+ * Update the Status line inside the ## TDD section.
100
+ * ponytail: regex replace — avoids re-parsing the whole file.
101
+ */
102
+ export const appendTddStatus = (projectDir: string, chapter: number, status: string): void => {
103
+ const path = chapterFile(projectDir, chapter)
104
+ const content = readFile(path)
105
+ const updated = content.replace(/- Status: .+/, `- Status: ${status}`)
106
+ writeFile(path, updated)
107
+ }
108
+
109
+ /**
110
+ * Append a ## Reflection section to chapter-N.md on chapter_done.
111
+ */
112
+ export const appendReflection = (projectDir: string, chapter: number, reflection: string): void => {
113
+ const path = chapterFile(projectDir, chapter)
114
+ const existing = readFile(path)
115
+ writeFile(path, `${existing}\n## Reflection\n\n${reflection}\n`)
116
+ }
117
+
118
+ /**
119
+ * Tick the chapter off in roadmap.json.
120
+ */
121
+ export const checkOffChapter = (projectDir: string, chapter: number, kind: ChapterKind): void => {
122
+ const path = roadmapJsonFile(projectDir)
123
+ if (!exists(path)) return
124
+ const roadmap = JSON.parse(readFile(path)) as Roadmap
125
+ const entry = roadmap.chapters.find((c) => c.n === chapter)
126
+ if (entry) {
127
+ entry.done = true
128
+ entry.kind = kind
129
+ }
130
+ writeFile(path, JSON.stringify(roadmap, null, 2))
131
+ }
132
+
133
+ /**
134
+ * Build a compact past-chapters summary for the system prompt.
135
+ * Reads only the ## Reflection block — never loads full chapter markdown.
136
+ * ~1–3 lines per past chapter regardless of chapter length.
137
+ * ponytail: grep reflection only — avoids loading full chapter markdown
138
+ */
139
+ export const buildPastChaptersSummary = (projectDir: string, upTo: number): string => {
140
+ const roadmapPath = roadmapJsonFile(projectDir)
141
+ if (!exists(roadmapPath)) return ""
142
+ const roadmap = JSON.parse(readFile(roadmapPath)) as Roadmap
143
+ const progress = readProgress(projectDir)
144
+ return roadmap.chapters
145
+ .filter((c) => c.done && c.n < upTo)
146
+ .map((c) => {
147
+ const chFile = chapterFile(projectDir, c.n)
148
+ const reflection = exists(chFile)
149
+ ? (readFile(chFile)
150
+ .match(/## Reflection\n\n([\s\S]+?)(?=\n##|$)/)?.[1]
151
+ ?.trim() ?? "")
152
+ : ""
153
+ const status =
154
+ c.kind === "theory"
155
+ ? "\u2705 done"
156
+ : progress.chapters[c.n]?.testsPass
157
+ ? "\u2705 tests pass"
158
+ : "\u26a0\ufe0f incomplete"
159
+ return `${c.n} \u2014 ${c.title} (${c.kind ?? "code"}, ${status})\n Reflection: ${reflection || "(none recorded)"}`.trim()
160
+ })
161
+ .join("\n")
162
+ }
163
+
164
+ /**
165
+ * Build the full chapter context block injected into the system prompt via before_agent_start.
166
+ * Past chapters: compact (reflection only, ~300 tokens for 10 chapters).
167
+ * Current chapter markdown trimmed to ~8 000 chars.
168
+ */
169
+ export const buildChapterContext = (
170
+ projectDir: string,
171
+ chapterMd: string,
172
+ profile: UserProfile,
173
+ n: number
174
+ ): string => {
175
+ const roadmapPath = roadmapJsonFile(projectDir)
176
+ const roadmap = exists(roadmapPath) ? (JSON.parse(readFile(roadmapPath)) as Roadmap) : null
177
+ const title = roadmap?.chapters.find((c) => c.n === n)?.title ?? `Chapter ${n}`
178
+ const stack = profile.primaryStack.join(", ") || "unknown"
179
+ const projects = profile.recentProjects
180
+ .map((p) => ` - ${p.name}: ${p.summary} [${p.stack.join(", ")}]`)
181
+ .join("\n")
182
+ const past = buildPastChaptersSummary(projectDir, n)
183
+ const trimmed =
184
+ chapterMd.length > 8000 ? `${chapterMd.slice(0, 8000)}\n\u2026(truncated)` : chapterMd
185
+
186
+ return [
187
+ `## Active Chapter: ${n} \u2014 ${title}`,
188
+ `Student stack: ${stack}`,
189
+ `Recent projects:\n${projects}`,
190
+ past ? `\n## Completed chapters\n${past}` : "",
191
+ `\n## Current chapter content\n${trimmed}`,
192
+ ]
193
+ .filter(Boolean)
194
+ .join("\n")
195
+ }
196
+
197
+ /**
198
+ * Build the LLM injection for a chapter session.
199
+ *
200
+ * Code chapter flow (6 steps):
201
+ * 1. Introduce what will be built
202
+ * 2. Theory foundations + level-calibrated quiz (what & why before any code)
203
+ * 3. Propose test plan
204
+ * 4. Write test file
205
+ * 5. Implement with "what & why" narration + wrong-answer TDD path
206
+ * 6. Run tests → chapter_done
207
+ *
208
+ * Theory chapter: concepts → quiz → chapter_done (no code, no tests)
209
+ */
210
+ export const chapterPrompt = (
211
+ chapterMd: string,
212
+ kind: ChapterKind,
213
+ chapterNum: number,
214
+ techstackMd = "",
215
+ primaryStack: string[] = [],
216
+ recentProjects: RecentProject[] = []
217
+ ): string => {
218
+ const profileContext = [
219
+ `Stack: ${primaryStack.join(", ") || "unknown"}`,
220
+ "Projects:",
221
+ ...recentProjects.map((p) => ` - ${p.name}: ${p.summary} [${p.stack.join(", ")}]`),
222
+ ].join("\n")
223
+ const tddSection =
224
+ kind === "code"
225
+ ? `
226
+ ## Chapter session — follow these steps IN ORDER
227
+
228
+ You are a patient tutor, not a code generator.
229
+
230
+ **Step 0 — Prerequisite gate (run FIRST, before anything else)**
231
+
232
+ The student's profile:
233
+ ${profileContext}
234
+
235
+ Look at this chapter's primary tech. Does it appear in their stack or project summaries?
236
+
237
+ FAMILIAR (tech is in their stack or they've built something with it):
238
+ → Acknowledge in one sentence ("Looks like you've worked with X before — let's move fast.")
239
+ → Skip the primer. Go directly to Step 1 with concise, tradeoff-focused explanations.
240
+
241
+ UNFAMILIAR (tech NOT in their known stack or projects):
242
+ → Ask 2–3 prerequisite questions via ask_user_question about this chapter's foundational concept.
243
+ → Scoring:
244
+ 2+ correct → proceed to Step 1 with normal depth
245
+ 0–1 correct → give a focused primer (key concept + one analogy, ~5 min read),
246
+ re-quiz once, then proceed regardless — never block progress
247
+
248
+ The gate outcome calibrates ALL remaining steps:
249
+ - Familiar / 2+ correct → concise, assume competence, focus on tradeoffs and edge cases
250
+ - Needed primer → slower pace, more analogies, explain before doing
251
+
252
+ **Step 1 — Introduce what will be built**
253
+ In 2–3 sentences: what concrete thing will the student have working by the end?
254
+ Frame it as an outcome, not a topic list. Calibrate depth based on Step 0 outcome.
255
+
256
+ **Step 2 — Theory foundations + quiz (before any code or tests)**
257
+ Explain the core "what and why" behind this chapter's approach — no code yet.
258
+ Calibrate depth based on Step 0 outcome.
259
+
260
+ Then quiz with 1–2 questions via ask_user_question:
261
+
262
+ ⚠️ WRONG-ANSWER TDD PATH — if the student answers incorrectly:
263
+ 1. Say "Let's see what happens" — don't reveal the answer
264
+ 2. Implement their wrong answer via write/bash tools
265
+ 3. Call \`poiesis_run_tests\` — the test will fail
266
+ 4. Show the failure output in 1 sentence
267
+ 5. Explain WHY it failed (the correct concept) in 2–3 sentences
268
+ 6. Revert to the correct implementation
269
+ This makes the wrong path a teaching moment, not a dead end.
270
+
271
+ ⚠️ LIVE RESEARCH RULE — you are a tutor, not a search engine from training data:
272
+ - Before explaining any concept: look up the official docs or spec with agent-browser
273
+ so the explanation is current and accurate, not a recollection.
274
+ - Student challenges a claim: open the authoritative source live and settle it from
275
+ evidence. Never argue from memory.
276
+ - You catch yourself saying "I think" or "I believe": stop, look it up, confirm first.
277
+
278
+ **Step 3 — Propose a test plan (DO NOT write code or call ask_user_question)**
279
+ Think of tests as learning checkpoints — each one proves the student built something real.
280
+
281
+ Call \`poiesis_confirm_test_plan\` with:
282
+ - chapterNum: ${chapterNum}
283
+ - intro: 1–2 sentences describing what the student will have built by the end
284
+ - tests: array of { name, why } — name in plain words, why in one sentence
285
+
286
+ Do NOT write the list in chat first. Do NOT call ask_user_question. The tool renders
287
+ its own full-screen dialog. Just call the tool.
288
+
289
+ If the tool returns "add": ask the student what to add, then re-call with the updated list.
290
+ If "skip": ask which one, then re-call without it.
291
+
292
+ **Step 4 — Write the test file**
293
+ Based on the chapter's tech stack, choose the right test framework and an appropriate file path
294
+ (e.g. \`tests/chapter-N.test.ts\` + vitest for TS, \`tests/test_chapter_N.py\` + pytest, etc.).
295
+ Write the test file yourself using the write tool.
296
+ Name tests after behaviour: \`server_returns_200_on_root\` not \`test_server\`.
297
+ No mocking unless I/O is the point.
298
+ After writing: "Done — <your test file> is our contract. Let's make these pass."
299
+
300
+ **Step 5 — YOU implement. Student is HITL for decisions + wrong-answer TDD.**
301
+
302
+ ⚠️ CRITICAL RULES — violating these is a bug:
303
+ 1. YOU run every shell command via the bash tool. The student NEVER runs commands manually.
304
+ BAD: "Run this in your terminal: npm install"
305
+ GOOD: call bash with "cd <project-dir> && npm install"
306
+
307
+ 2. For interactive CLI scaffolders (npm create, create-vite, etc.) use non-interactive flags.
308
+ GOOD: \`npm create hono@latest . -- --template cloudflare-workers\`
309
+ NEVER ask the student to pick answers in their terminal.
310
+
311
+ 3. Student HITL = architecture/design DECISIONS only.
312
+ Ask via ask_user_question for: which template, library choice, project structure.
313
+ Do NOT ask for: running commands, installing packages, creating files.
314
+
315
+ 4. "What & why" narration: before each significant code block, one sentence on WHAT
316
+ you're adding and WHY — not a lecture, just intent.
317
+
318
+ 5. LIVE WEB RESEARCH — use agent-browser instead of guessing in any of these cases:
319
+ a) TEACHING: before explaining a concept, look up the official docs or spec so the
320
+ explanation is accurate, not a recollection. MDN, framework docs, stdlib refs.
321
+ b) IN DEBATE: student challenges a claim → open the authoritative source live,
322
+ quote it, settle it from evidence. Never argue from memory.
323
+ c) SELF-CONFLICT: you catch yourself saying "I think..." or holding two contradicting
324
+ ideas → stop and look it up before continuing.
325
+ d) PACKAGES / APIs: before writing code that imports anything unfamiliar, check the
326
+ actual API signature. pi.dev/packages, npmjs.com, or the GitHub README.
327
+ e) ERRORS: unfamiliar error message → look it up. Don't guess the cause.
328
+ Rule: if it's version-sensitive, spec-sensitive, or you'd say "I believe" — look it up.
329
+
330
+ 6. Wrong-answer TDD on design choices: if the student picks a wrong design,
331
+ implement it → call poiesis_run_tests → show failure → explain → correct.
332
+
333
+ 7. If a command fails 3 times: THEN explain and ask the student. Otherwise handle silently.
334
+
335
+ Your code MUST make the test file you wrote pass. Don't modify the test file.
336
+ If a test seems wrong: raise it via ask_user_question before touching it.
337
+
338
+ **Step 6 — Run & complete**
339
+ Call \`poiesis_run_tests\` with chapter=${chapterNum} and cmd="<the runner + test file you chose>".
340
+ Fail → YOU diagnose + fix + re-run. No student involvement unless stuck after 3 attempts.
341
+ Pass → call \`poiesis_chapter_done\`.
342
+ `
343
+ : ""
344
+
345
+ const theoryGate =
346
+ kind === "theory"
347
+ ? `
348
+ ## Theory chapter — no code, no tests
349
+
350
+ **Step 0 — Prerequisite gate (run FIRST)**
351
+
352
+ The student's profile:
353
+ ${profileContext}
354
+
355
+ FAMILIAR: acknowledge in one sentence, proceed with concise tradeoff-focused questions.
356
+ UNFAMILIAR: 2–3 foundation questions → if <2 correct, give a primer, re-quiz once.
357
+ Calibrate all remaining steps based on the gate outcome.
358
+
359
+ Then:
360
+ 1. Introduce the chapter concepts in 2–3 sentences — what and why, not a feature list.
361
+ 2. Quiz the student on each key idea via ask_user_question.
362
+ Calibrate question style based on Step 0 outcome:
363
+ - Familiar / passed: "What are the tradeoffs?" / "What edge case does this miss?"
364
+ - Needed primer: "What is X?" / "Why Y instead of Z?"
365
+ 3. Wrong answer → gently correct with a brief explanation, then re-ask.
366
+ 4. Once they demonstrate understanding of all key concepts, call \`poiesis_chapter_done\`.
367
+
368
+ ⚠️ LIVE RESEARCH RULE — you are a tutor, not a static knowledge base:
369
+ - Before explaining any concept: look up the official docs or spec with agent-browser
370
+ so the explanation is current and accurate, not a recollection.
371
+ - Student challenges a claim: open the authoritative source live and quote it.
372
+ Never argue from memory.
373
+ - You catch yourself saying "I think" or "I believe": stop, look it up first.
374
+ `
375
+ : ""
376
+
377
+ const techstackSection = techstackMd ? `## Project Tech Stack\n\n${techstackMd}\n\n---\n\n` : ""
378
+
379
+ return `# Chapter ${chapterNum} — Session
380
+
381
+ You are a patient, encouraging tutor. The student may be a beginner or an expert — calibrate your
382
+ explanations to their responses.
383
+
384
+ ${techstackSection}Here is the chapter content for reference:
385
+
386
+ ---
387
+ ${chapterMd}
388
+ ---
389
+ ${tddSection}${theoryGate}`
390
+ }
391
+
392
+ // ── runChapter ────────────────────────────────────────────────────────────────
393
+
394
+ /**
395
+ * Orchestrate one chapter: read state → resume from correct step.
396
+ *
397
+ * setSession is injected by index.ts to avoid a circular import.
398
+ * It sets the module-level _projectDir / _chapterNum used by gate tools + event handlers.
399
+ */
400
+ export const runChapter = async (
401
+ pi: ExtensionAPI,
402
+ ctx: ExtensionCommandContext,
403
+ profile: UserProfile,
404
+ projectDir: string,
405
+ setSession: (dir: string, n: number) => void
406
+ ): Promise<void> => {
407
+ const p = readProgress(projectDir)
408
+ const n = p.current
409
+ const path = chapterFile(projectDir, n)
410
+
411
+ if (!exists(path)) {
412
+ ctx.ui.notify(`Chapter ${n} file not found at ${path}`, "error")
413
+ return
414
+ }
415
+
416
+ // Register session state FIRST so gate tools and event handlers see it immediately
417
+ setSession(projectDir, n)
418
+
419
+ const profileContext = [
420
+ `Stack: ${profile.primaryStack.join(", ") || "unknown"}`,
421
+ "Projects:",
422
+ ...profile.recentProjects.map((pr) => ` - ${pr.name}: ${pr.summary} [${pr.stack.join(", ")}]`),
423
+ ].join("\n")
424
+
425
+ const existingState = readChapterState(projectDir, n)
426
+
427
+ // Fresh start: classify then write initial state
428
+ if (!existingState) {
429
+ ctx.ui.setStatus("poiesis", ` Classifying chapter ${n}\u2026`)
430
+ let kind: ChapterKind = "code"
431
+ try {
432
+ kind = await classifyChapter(readFile(path), profile.primaryStack)
433
+ } catch {
434
+ // ponytail: fall back to 'code' if Gemini is unavailable
435
+ }
436
+ setChapterMeta(projectDir, n, kind, null)
437
+ ctx.ui.setStatus("poiesis", undefined)
438
+
439
+ const initialStep: ChapterStep = kind === "theory" ? "theory" : "prereq"
440
+ writeChapterState(projectDir, n, {
441
+ step: initialStep,
442
+ prereqResult: null,
443
+ testsFile: null,
444
+ testsPlan: [],
445
+ testsPass: false,
446
+ startedAt: new Date().toISOString(),
447
+ })
448
+
449
+ if (kind === "theory") {
450
+ pi.sendUserMessage(theoryStepPrompt("familiar", n))
451
+ } else {
452
+ pi.sendUserMessage(prereqPrompt(profileContext))
453
+ }
454
+ return
455
+ }
456
+
457
+ // Resume: dispatch to the correct step
458
+ switch (existingState.step) {
459
+ case "prereq":
460
+ pi.sendUserMessage(prereqPrompt(profileContext))
461
+ break
462
+ case "theory":
463
+ pi.sendUserMessage(theoryStepPrompt(existingState.prereqResult ?? "familiar", n))
464
+ break
465
+ case "plan":
466
+ pi.sendUserMessage(planPrompt(n))
467
+ break
468
+ case "write-tests":
469
+ pi.sendUserMessage(writeTestsPrompt(existingState.testsPlan))
470
+ break
471
+ case "implement":
472
+ pi.sendUserMessage(implementPrompt(existingState.testsFile ?? "(unknown)", n))
473
+ break
474
+ }
475
+ }
476
+
477
+ // ── self-check ────────────────────────────────────────────────────────────────
478
+
479
+ if (process.argv[1]?.endsWith("chapter.ts")) {
480
+ ;(async () => {
481
+ const { tmpdir } = await import("node:os")
482
+ const { mkdirSync, rmSync } = await import("node:fs")
483
+ const { initProgress } = await import("./progress.ts")
484
+
485
+ const dir = join(tmpdir(), `poiesis-chapter-${Date.now()}`)
486
+ const chDir = join(dir, ".poiesis", "chapters")
487
+ mkdirSync(chDir, { recursive: true })
488
+
489
+ // seed files
490
+ writeFile(join(chDir, "chapter-1.md"), "# Chapter 1 — Intro\n\nSome content.\n")
491
+ writeFile(
492
+ join(chDir, "techstack.md"),
493
+ "**Runtime**: Bun\n**Package manager**: bun\n**Framework**: Hono\n"
494
+ )
495
+ writeFile(
496
+ join(chDir, "roadmap.json"),
497
+ JSON.stringify(
498
+ {
499
+ name: "Project",
500
+ chapters: [
501
+ { n: 1, title: "Intro", duration: "0:00-5:00", done: false, kind: null },
502
+ { n: 2, title: "Middleware", duration: "5:00-10:00", done: false, kind: null },
503
+ ],
504
+ },
505
+ null,
506
+ 2
507
+ )
508
+ )
509
+ initProgress(dir, 2, {
510
+ "1": { type: "code", testsFile: null, testsPass: false },
511
+ "2": { type: "theory", testsFile: null, testsPass: null },
512
+ })
513
+
514
+ // writeTddSection
515
+ writeTddSection(dir, 1, "tests/chapter-1.test.ts", ["server returns 200", "unknown route 404"])
516
+ const after = readFile(join(chDir, "chapter-1.md"))
517
+ console.assert(after.includes("## TDD"), "TDD section missing")
518
+ console.assert(after.includes("🟡"), "initial status missing")
519
+
520
+ // appendTddStatus
521
+ appendTddStatus(dir, 1, "🟢 passing")
522
+ const updated = readFile(join(chDir, "chapter-1.md"))
523
+ console.assert(updated.includes("🟢 passing"), "status not updated")
524
+ console.assert(!updated.includes("🟡"), "old status still present")
525
+
526
+ // checkOffChapter
527
+ checkOffChapter(dir, 1, "code")
528
+ const roadmap = JSON.parse(readFile(join(chDir, "roadmap.json")))
529
+ console.assert(roadmap.chapters[0].done === true, "chapter not ticked off")
530
+ console.assert(roadmap.chapters[0].kind === "code", "kind not set")
531
+
532
+ // appendReflection
533
+ appendReflection(dir, 1, "Learned about routing.")
534
+ const withReflection = readFile(join(chDir, "chapter-1.md"))
535
+ console.assert(withReflection.includes("## Reflection"), "reflection section missing")
536
+
537
+ // chapterPrompt — code chapter with profile context
538
+ const testProjects = [
539
+ { name: "hono-api", summary: "REST API with Hono", stack: ["TypeScript", "Hono"] },
540
+ ]
541
+ const prompt = chapterPrompt("# Ch1\nContent.", "code", 1, "", ["TypeScript"], testProjects)
542
+ // Step 0 — prerequisite gate
543
+ console.assert(prompt.includes("Prerequisite gate"), "Step 0 gate missing")
544
+ console.assert(prompt.includes("FAMILIAR"), "FAMILIAR path missing")
545
+ console.assert(prompt.includes("UNFAMILIAR"), "UNFAMILIAR path missing")
546
+ console.assert(prompt.includes("hono-api"), "profile context (project name) missing")
547
+ console.assert(prompt.includes("TypeScript"), "profile context (stack) missing")
548
+ // Steps 1–6
549
+ console.assert(prompt.includes("Introduce what will be built"), "intro step missing")
550
+ console.assert(prompt.includes("Theory foundations"), "theory+quiz step missing")
551
+ console.assert(prompt.includes("WRONG-ANSWER TDD PATH"), "wrong-answer TDD path missing")
552
+ console.assert(prompt.includes("what and why"), "what-and-why narration missing")
553
+ console.assert(prompt.includes("poiesis_confirm_test_plan"), "confirm tool call missing")
554
+ console.assert(prompt.includes("poiesis_run_tests"), "run tests step missing")
555
+ console.assert(
556
+ prompt.includes("Do NOT call ask_user_question"),
557
+ "step 3 must prohibit ask_user_question"
558
+ )
559
+ console.assert(prompt.includes("ask_user_question"), "hitl ask_user_question still needed")
560
+ console.assert(prompt.includes("NEVER runs commands manually"), "agent must own all shell")
561
+ console.assert(prompt.includes("non-interactive flags"), "must guide on non-interactive CLIs")
562
+ console.assert(prompt.includes("fails 3 times"), "must define retry-then-escalate rule")
563
+ console.assert(
564
+ prompt.includes("LIVE WEB RESEARCH"),
565
+ "code chapter must include live web research rule"
566
+ )
567
+ console.assert(
568
+ prompt.includes("LIVE RESEARCH RULE"),
569
+ "code chapter theory+quiz step must include live research rule"
570
+ )
571
+ // experienceLevel must NOT be statically baked in
572
+ console.assert(!prompt.includes("experienceLevel"), "experienceLevel must not appear in prompt")
573
+ // theory chapter
574
+ const theoryPrompt = chapterPrompt("# Ch2\nConcepts.", "theory", 2, "", ["Go"], [])
575
+ console.assert(
576
+ !theoryPrompt.includes("test file"),
577
+ "test file must not appear in theory prompt"
578
+ )
579
+ console.assert(theoryPrompt.includes("Theory chapter"), "theory gate missing")
580
+ console.assert(theoryPrompt.includes("Prerequisite gate"), "theory also needs Step 0 gate")
581
+ console.assert(theoryPrompt.includes("what and why"), "theory also needs what-and-why")
582
+ console.assert(
583
+ theoryPrompt.includes("LIVE RESEARCH RULE"),
584
+ "theory chapter must include live research rule"
585
+ )
586
+
587
+ // buildPastChaptersSummary — run AFTER checkOffChapter + appendReflection so chapter 1 is done
588
+ const past = buildPastChaptersSummary(dir, 2) // upTo=2: chapter 1 (n=1 < 2) included
589
+ console.assert(past.includes("1 — Intro"), `past summary missing chapter 1: ${past}`)
590
+ console.assert(!past.includes("2 —"), `past summary must not include chapter 2: ${past}`)
591
+ console.assert(past.includes("Reflection:"), `past summary missing reflection label: ${past}`)
592
+ console.assert(
593
+ past.includes("Learned about routing."),
594
+ `past summary missing reflection text: ${past}`
595
+ )
596
+ // no past chapters when upTo=1
597
+ const noPast = buildPastChaptersSummary(dir, 1)
598
+ console.assert(noPast === "", `upTo=1 should return empty string: '${noPast}'`)
599
+
600
+ // buildChapterContext
601
+ const testProf: UserProfile = {
602
+ primaryStack: ["TypeScript"],
603
+ recentProjects: [{ name: "hono-api", summary: "REST API", stack: ["TypeScript"] }],
604
+ recentActivity: "building APIs",
605
+ scannedAt: new Date().toISOString(),
606
+ }
607
+ const ctxOut = buildChapterContext(dir, "# Ch2\nContent here.", testProf, 2)
608
+ console.assert(
609
+ ctxOut.includes("## Active Chapter: 2 — Middleware"),
610
+ `context missing chapter header: ${ctxOut.slice(0, 300)}`
611
+ )
612
+ console.assert(
613
+ ctxOut.includes("## Completed chapters"),
614
+ `context missing completed section: ${ctxOut.slice(0, 300)}`
615
+ )
616
+ console.assert(ctxOut.includes("## Current chapter content"), "context missing content section")
617
+ console.assert(ctxOut.includes("TypeScript"), "context missing stack")
618
+ console.assert(ctxOut.includes("hono-api"), "context missing project")
619
+ console.assert(ctxOut.includes("# Ch2"), "context missing chapter markdown")
620
+
621
+ rmSync(dir, { recursive: true })
622
+ console.log("chapter.ts: ok")
623
+ })()
624
+ }
package/src/config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Config } from "./types.ts"
2
+ import { exists, readJson, writeJson } from "./utils.ts"
3
+
4
+ export const CONFIG_PATH = "~/.poiesis/config.json"
5
+
6
+ export const loadConfig = (): Config | null => {
7
+ if (!exists(CONFIG_PATH)) return null
8
+ return readJson<Config>(CONFIG_PATH)
9
+ }
10
+
11
+ export const saveConfig = (cfg: Config): void => writeJson(CONFIG_PATH, cfg)
12
+
13
+ export const defaultConfig = (): Config => ({
14
+ state_dir: "~/.poiesis",
15
+ })