@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/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
|
+
})
|