@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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * First-run onboarding.
3
+ *
4
+ * "Sure!" → inject scan prompt → return immediately (false)
5
+ * LLM scans + calls poiesis_save_profile
6
+ * poiesis_save_profile queues '/poiesis' as followUp
7
+ * '/poiesis' fires in fresh context → needsOnboarding=false → runProject
8
+ *
9
+ * "No" → inject QnA prompt → return immediately (false)
10
+ * same chain: LLM chats → poiesis_save_profile → followUp → runProject
11
+ */
12
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
13
+ import { exists } from "./utils.ts"
14
+
15
+ export const PROFILE_PATH = "~/.poiesis/user-profile.json"
16
+
17
+ export const needsOnboarding = (): boolean => !exists(PROFILE_PATH)
18
+
19
+ export const runOnboarding = async (
20
+ pi: ExtensionAPI,
21
+ ctx: ExtensionCommandContext
22
+ ): Promise<void> => {
23
+ const pick = await ctx.ui.select(
24
+ "Mind if I sneak a peek at your GitHub repos and local folders? I won't read any code.",
25
+ ["Sure!", "No, ask me instead"]
26
+ )
27
+
28
+ if (pick === "Sure!") {
29
+ await pi.sendUserMessage(
30
+ `Run \`gh repo list --limit 50 --json name,primaryLanguage,description,updatedAt\` and list directory names (not contents) in ~/Desktop, ~/projects, ~/dev, ~/code.
31
+
32
+ From what you find, call \`poiesis_save_profile\` with:
33
+ - primaryStack: top languages/frameworks across all repos
34
+ - recentProjects: up to 8 projects, each with:
35
+ name: repo or dir name
36
+ summary: one sentence — what was built and what tech was used (use description field or infer from name/language)
37
+ stack: languages/frameworks in that project
38
+ - recentActivity: one-line summary of what they’ve been building lately
39
+
40
+ Call the tool immediately once you have enough data. No summary, no greeting. After calling the tool, tell the user to run /poiesis.`
41
+ )
42
+ return
43
+ }
44
+
45
+ // Multi-turn QnA path — same tail: LLM calls poiesis_save_profile → followUp → runProject
46
+ await pi.sendUserMessage(
47
+ `Ask the user about themselves — one question at a time, casual tone. Find out:
48
+ - primaryStack: languages/frameworks they build with most
49
+ - recentProjects: up to 5 things they've built recently, for each:
50
+ name: project or repo name
51
+ summary: one sentence — what they built and what tech they used
52
+ stack: the main languages/frameworks in that project
53
+ - recentActivity: one-line summary of what they've been working on
54
+
55
+ Once you have enough, call \`poiesis_save_profile\`. No sign-off after saving.`
56
+ )
57
+ }
@@ -0,0 +1,94 @@
1
+ import { join } from "node:path"
2
+ import type { ChapterKind, ChapterMeta, Progress } from "./types.ts"
3
+ import { expandHome, readJson, writeJson } from "./utils.ts"
4
+
5
+ // ponytail: progress lives in .poiesis/chapters/.progress.json
6
+ const progressPath = (projectDir: string): string =>
7
+ join(expandHome(projectDir), ".poiesis", "chapters", ".progress.json")
8
+
9
+ export const readProgress = (projectDir: string): Progress =>
10
+ readJson<Progress>(progressPath(projectDir))
11
+
12
+ export const writeProgress = (projectDir: string, p: Progress): void =>
13
+ writeJson(progressPath(projectDir), p)
14
+
15
+ /** Create initial .progress.json at scaffold time. */
16
+ export const initProgress = (
17
+ projectDir: string,
18
+ total: number,
19
+ chapters: Record<string, ChapterMeta>
20
+ ): void => {
21
+ const now = new Date().toISOString()
22
+ writeProgress(projectDir, {
23
+ current: 1,
24
+ total,
25
+ completed: [],
26
+ startedAt: now,
27
+ lastActiveAt: now,
28
+ chapters,
29
+ })
30
+ }
31
+
32
+ /** Called by poiesis_run_tests on success. */
33
+ export const markTestsPass = (projectDir: string, chapter: number): void => {
34
+ const p = readProgress(projectDir)
35
+ const ch = p.chapters[chapter]
36
+ if (ch) ch.testsPass = true
37
+ p.lastActiveAt = new Date().toISOString()
38
+ writeProgress(projectDir, p)
39
+ }
40
+
41
+ /** Set chapter type + testsFile after classification. */
42
+ export const setChapterMeta = (
43
+ projectDir: string,
44
+ chapter: number,
45
+ kind: ChapterKind,
46
+ testsFile: string | null
47
+ ): void => {
48
+ const p = readProgress(projectDir)
49
+ p.chapters[chapter] = { type: kind, testsFile, testsPass: kind === "theory" ? null : false }
50
+ p.lastActiveAt = new Date().toISOString()
51
+ writeProgress(projectDir, p)
52
+ }
53
+
54
+ /** Move current → next after chapter_done gate passes. */
55
+ export const advanceChapter = (projectDir: string): void => {
56
+ const p = readProgress(projectDir)
57
+ if (!p.completed.includes(p.current)) p.completed.push(p.current)
58
+ p.current = Math.min(p.current + 1, p.total)
59
+ p.lastActiveAt = new Date().toISOString()
60
+ writeProgress(projectDir, p)
61
+ }
62
+
63
+ // ponytail: self-check
64
+ if (process.argv[1]?.endsWith("progress.ts")) {
65
+ import("node:os").then(({ tmpdir }) => {
66
+ import("node:fs").then(({ mkdirSync, rmSync }) => {
67
+ const dir = join(tmpdir(), `poiesis-test-${Date.now()}`)
68
+ mkdirSync(join(dir, ".poiesis", "chapters"), { recursive: true })
69
+
70
+ initProgress(dir, 3, {
71
+ "1": { type: "code", testsFile: "tests/chapter-1.test.ts", testsPass: false },
72
+ "2": { type: "theory", testsFile: null, testsPass: null },
73
+ "3": { type: "code", testsFile: "tests/chapter-3.test.ts", testsPass: false },
74
+ })
75
+
76
+ let p = readProgress(dir)
77
+ console.assert(p.current === 1, "current should be 1")
78
+ console.assert(p.total === 3, "total should be 3")
79
+ console.assert(p.completed.length === 0, "none completed")
80
+
81
+ markTestsPass(dir, 1)
82
+ p = readProgress(dir)
83
+ console.assert(p.chapters["1"]?.testsPass === true, "chapter 1 should pass")
84
+
85
+ advanceChapter(dir)
86
+ p = readProgress(dir)
87
+ console.assert(p.current === 2, "current should advance to 2")
88
+ console.assert(p.completed.includes(1), "chapter 1 should be completed")
89
+
90
+ rmSync(dir, { recursive: true })
91
+ console.log("progress.ts: ok")
92
+ })
93
+ })
94
+ }
package/src/project.ts ADDED
@@ -0,0 +1,311 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
4
+ import { initProgress } from "./progress.ts"
5
+ import type { ChapterMeta } from "./types.ts"
6
+
7
+ /**
8
+ * Post-onboarding project flow.
9
+ *
10
+ * 1. ctx.ui.input — YouTube URL
11
+ * 2. ctx.ui.input — project name (defaults to YT video title)
12
+ * 3. Scaffold {cwd}/{name}/chapters/
13
+ * 4. Gemini (JSON mode) → chapter-index.md + summary.md + chapter-N.md per chapter
14
+ */
15
+ import { Type as GType, GoogleGenAI } from "@google/genai"
16
+
17
+ // ── Types ─────────────────────────────────────────────────────────────────────
18
+
19
+ interface Chapter {
20
+ title: string
21
+ concepts: string[]
22
+ duration: string
23
+ notes: string
24
+ keyTakeaway: string
25
+ }
26
+
27
+ interface VideoAnalysis {
28
+ summary: string
29
+ techstack: string
30
+ chapters: Chapter[]
31
+ }
32
+
33
+ // ── Helpers ───────────────────────────────────────────────────────────────────
34
+
35
+ const toFolder = (s: string): string =>
36
+ s
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, "-")
39
+ .replace(/^-+|-+$/g, "")
40
+ .slice(0, 50)
41
+
42
+ const YT_RE = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/
43
+
44
+ const ytTitle = async (url: string): Promise<string> => {
45
+ try {
46
+ const res = await fetch(
47
+ `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`
48
+ )
49
+ const data = (await res.json()) as { title?: string }
50
+ return data.title ?? "project"
51
+ } catch {
52
+ return "project"
53
+ }
54
+ }
55
+
56
+ // ── Gemini analysis ───────────────────────────────────────────────────────────
57
+
58
+ const analyzeVideo = async (url: string, name: string): Promise<VideoAnalysis> => {
59
+ const apiKey = process.env.GEMINI_API_KEY
60
+ if (!apiKey) throw new Error("GEMINI_API_KEY is not set")
61
+
62
+ const ai = new GoogleGenAI({ apiKey })
63
+
64
+ const result = await ai.models.generateContent({
65
+ model: "gemini-3.5-flash",
66
+ contents: [
67
+ {
68
+ role: "user",
69
+ parts: [
70
+ {
71
+ text: `Analyze this YouTube tutorial and return structured JSON.
72
+ URL: ${url}
73
+ Project name: "${name}"
74
+
75
+ Return a JSON object matching the schema exactly. Be specific to the actual video content — no filler.
76
+
77
+ Fields:
78
+ - summary: 2–3 sentence description of what this project builds and what problem it solves — written as a project README description, not a tutorial summary. Do not mention "this tutorial" or "this video".
79
+ - techstack: a concise Markdown description of the project's tech stack (runtime, package manager, language, framework, key libraries, test runner, any important conventions). This will be injected into every chapter session so the AI tutor never needs to ask. Example: "**Runtime**: Bun\n**Package manager**: bun\n**Language**: TypeScript\n**Framework**: Hono\n**Testing**: Vitest (via bunx)\n\nAll commands use \'bun\'. Strict TypeScript throughout."
80
+ - chapters: array of chapters, each with:
81
+ - title: short chapter title (no number prefix)
82
+ - concepts: array of 2–5 key topics covered
83
+ - duration: estimated time range (e.g. "0:00–8:30")
84
+ - notes: one sentence on what the viewer builds or learns
85
+ - keyTakeaway: single most important insight from this chapter`,
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ config: {
91
+ responseMimeType: "application/json",
92
+ responseSchema: {
93
+ type: GType.OBJECT,
94
+ properties: {
95
+ summary: { type: GType.STRING },
96
+ techstack: { type: GType.STRING },
97
+ chapters: {
98
+ type: GType.ARRAY,
99
+ items: {
100
+ type: GType.OBJECT,
101
+ properties: {
102
+ title: { type: GType.STRING },
103
+ concepts: { type: GType.ARRAY, items: { type: GType.STRING } },
104
+ duration: { type: GType.STRING },
105
+ notes: { type: GType.STRING },
106
+ keyTakeaway: { type: GType.STRING },
107
+ },
108
+ required: ["title", "concepts", "duration", "notes", "keyTakeaway"],
109
+ },
110
+ },
111
+ },
112
+ required: ["summary", "techstack", "chapters"],
113
+ },
114
+ },
115
+ })
116
+
117
+ return JSON.parse(result.text ?? "{}") as VideoAnalysis
118
+ }
119
+
120
+ // ── File builders ─────────────────────────────────────────────────────────────
121
+
122
+ const buildTechstack = (techstack: string): string => techstack.trim()
123
+
124
+ const buildRoadmapJson = (name: string, analysis: VideoAnalysis): string =>
125
+ JSON.stringify(
126
+ {
127
+ name,
128
+ chapters: analysis.chapters.map((ch, i) => ({
129
+ n: i + 1,
130
+ title: ch.title,
131
+ duration: ch.duration,
132
+ done: false,
133
+ kind: null,
134
+ })),
135
+ },
136
+ null,
137
+ 2
138
+ )
139
+
140
+ const buildSummary = (
141
+ name: string,
142
+ url: string,
143
+ analysis: VideoAnalysis
144
+ ): string => `# ${name} — Summary
145
+
146
+ **Source**: ${url}
147
+
148
+ ${analysis.summary}
149
+
150
+ ## What You'll Learn
151
+
152
+ ${analysis.chapters.map((ch, i) => `- **Chapter ${i + 1}** — ${ch.title}: ${ch.keyTakeaway}`).join("\n")}
153
+
154
+ ---
155
+
156
+ ← [[chapter-index]]
157
+ `
158
+
159
+ const buildIndex = (name: string, url: string, analysis: VideoAnalysis): string => {
160
+ const rows = analysis.chapters
161
+ .map(
162
+ (ch, i) =>
163
+ `| [[chapter-${i + 1}\\|Chapter ${i + 1}]] | ${ch.title} | ${ch.duration} | ${ch.concepts.join(", ")} |`
164
+ )
165
+ .join("\n")
166
+
167
+ return `# ${name} — Chapter Index
168
+
169
+ **Source**: ${url}
170
+
171
+ > ${analysis.summary}
172
+
173
+ [[summary|→ Full Summary]]
174
+
175
+ ## Chapters
176
+
177
+ | # | Title | Duration | Key Concepts |
178
+ |---|-------|----------|--------------|
179
+ ${rows}
180
+
181
+ ---
182
+
183
+ *${analysis.chapters.length} chapters · generated by poiesis*
184
+ `
185
+ }
186
+
187
+ const buildChapter = (ch: Chapter, n: number, total: number): string => {
188
+ const prev = n > 1 ? `[[chapter-${n - 1}|← Chapter ${n - 1}]]` : "[[chapter-index|← Index]]"
189
+ const next = n < total ? `[[chapter-${n + 1}|Chapter ${n + 1} →]]` : "[[chapter-index|→ Index]]"
190
+
191
+ return `# Chapter ${n} — ${ch.title}
192
+
193
+ **Concepts**: ${ch.concepts.join(", ")}
194
+ **Duration**: ${ch.duration}
195
+
196
+ ## What You Learn
197
+
198
+ ${ch.notes}
199
+
200
+ ## Key Takeaway
201
+
202
+ > ${ch.keyTakeaway}
203
+
204
+ ---
205
+
206
+ ${prev} · ${next}
207
+ `
208
+ }
209
+
210
+ // ── Scaffold ──────────────────────────────────────────────────────────────────
211
+
212
+ const GITIGNORE = `node_modules/
213
+ dist/
214
+ .env
215
+ .env.local
216
+ *.log
217
+ .pi
218
+ .poiesis
219
+ `
220
+
221
+ const buildReadme = (name: string, url: string, analysis: VideoAnalysis): string =>
222
+ `# ${name}\n\n${analysis.summary}\n\n---\n\n<sub>Made with help of <a href="https://shanvit7.github.io/poiesis/">Poiesis</a> watching <a href="${url}">${url}</a></sub>\n`
223
+
224
+ const scaffoldChapters = (
225
+ chaptersDir: string,
226
+ projectDir: string,
227
+ name: string,
228
+ url: string,
229
+ analysis: VideoAnalysis
230
+ ): void => {
231
+ const write = (filename: string, content: string): void =>
232
+ writeFileSync(join(chaptersDir, filename), content, "utf8")
233
+
234
+ write("summary.md", buildSummary(name, url, analysis))
235
+ write("chapter-index.md", buildIndex(name, url, analysis))
236
+ write("techstack.md", buildTechstack(analysis.techstack))
237
+ write("roadmap.json", buildRoadmapJson(name, analysis))
238
+
239
+ for (let i = 0; i < analysis.chapters.length; i++) {
240
+ write(
241
+ `chapter-${i + 1}.md`,
242
+ buildChapter(analysis.chapters[i], i + 1, analysis.chapters.length)
243
+ )
244
+ }
245
+
246
+ writeFileSync(join(projectDir, "README.md"), buildReadme(name, url, analysis), "utf8")
247
+
248
+ // .gitignore at project root (not inside .poiesis)
249
+ writeFileSync(join(projectDir, ".gitignore"), GITIGNORE, "utf8")
250
+
251
+ // initialise progress — classifyChapter refines type at runtime
252
+ const chapters: Record<string, ChapterMeta> = {}
253
+ for (let i = 0; i < analysis.chapters.length; i++) {
254
+ chapters[i + 1] = { type: "code", testsFile: null, testsPass: false }
255
+ }
256
+ initProgress(projectDir, analysis.chapters.length, chapters)
257
+ }
258
+
259
+ // ── Entry point ───────────────────────────────────────────────────────────────
260
+
261
+ export const runProject = async (pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> => {
262
+ // 1. YT URL
263
+ const url = (await ctx.ui.input("Paste a YouTube URL")) ?? ""
264
+ if (!YT_RE.test(url)) {
265
+ ctx.ui.notify("Not a valid YouTube URL — try again.", "error")
266
+ return
267
+ }
268
+
269
+ // 2. Project name
270
+ const defaultTitle = await ytTitle(url)
271
+ const input =
272
+ (await ctx.ui.input(`Project name — press Enter to use "${defaultTitle}"`, defaultTitle)) ||
273
+ defaultTitle
274
+ const name = toFolder(input) || "project"
275
+
276
+ // 3. Scaffold directory (.poiesis/chapters lives inside the project)
277
+ const projectDir = join(ctx.cwd, name)
278
+ const poiesisChaptersDir = join(projectDir, ".poiesis", "chapters")
279
+ mkdirSync(poiesisChaptersDir, { recursive: true })
280
+
281
+ // 4. Analyze + write files
282
+ ctx.ui.setStatus("poiesis", " Analyzing video with Gemini…")
283
+ try {
284
+ const analysis = await analyzeVideo(url, name)
285
+ scaffoldChapters(poiesisChaptersDir, projectDir, name, url, analysis)
286
+ ctx.ui.setStatus("poiesis", undefined)
287
+
288
+ // 5. Handoff — trigger the LLM to brief the user on what was created
289
+ await pi.sendUserMessage(
290
+ `The poiesis project "${name}" has been scaffolded at: ${projectDir}
291
+
292
+ What was created:
293
+ - ${analysis.chapters.length} chapter guide files in .poiesis/chapters/
294
+ - techstack.md (tech context injected into every chapter session)
295
+ - roadmap.json, chapter-index.md, summary.md
296
+ - README.md
297
+ - .gitignore
298
+ - .poiesis/chapters/.progress.json (tracks chapter progress)
299
+
300
+ Tell the user all of the following:
301
+ 1. The project is ready at \`${projectDir}\`
302
+ 2. How to start: \`cd ${projectDir}\` then run \`/poiesis\`
303
+ 3. One-line teaser of chapter 1: "${analysis.chapters[0]?.title ?? "Chapter 1"}"
304
+
305
+ Be concise — 3–4 sentences max.`
306
+ )
307
+ } catch (err) {
308
+ ctx.ui.setStatus("poiesis", undefined)
309
+ ctx.ui.notify(`Gemini error: ${String(err)}`, "error")
310
+ }
311
+ }
package/src/steps.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Step prompt functions — each loads a .md template from prompts/ and renders {{vars}}.
3
+ * The .md files are the source of truth; edit them directly to tune prompts.
4
+ * Consumed by runChapter (on resume) and by gate tools (on advance).
5
+ */
6
+
7
+ import { readFileSync } from "fs"
8
+ import { dirname, join } from "path"
9
+ import { fileURLToPath } from "url"
10
+
11
+ const PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "prompts")
12
+
13
+ // ponytail: flat {{var}} replace — no handlebars needed for 5 simple templates
14
+ const render = (tmpl: string, vars: Record<string, string>): string =>
15
+ tmpl.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? `{{${k}}}`)
16
+
17
+ const load = (name: string, vars: Record<string, string> = {}): string =>
18
+ render(readFileSync(join(PROMPTS_DIR, `${name}.md`), "utf8").trim(), vars)
19
+
20
+ export const prereqPrompt = (profileContext: string): string => load("prereq", { profileContext })
21
+
22
+ export const theoryPrompt = (prereqResult: "familiar" | "primed", chapterNum: number): string =>
23
+ load("theory", { prereqResult, chapterNum: String(chapterNum) })
24
+
25
+ export const planPrompt = (chapterNum: number): string =>
26
+ load("plan", { chapterNum: String(chapterNum) })
27
+
28
+ export const writeTestsPrompt = (testsPlan: Array<{ name: string; why: string }>): string =>
29
+ load("write-tests", {
30
+ testsPlan: testsPlan.map((t, i) => ` ${i + 1}. ${t.name} — ${t.why}`).join("\n"),
31
+ })
32
+
33
+ export const implementPrompt = (testsFile: string, chapterNum: number): string =>
34
+ load("implement", { testsFile, chapterNum: String(chapterNum) })
35
+
36
+ // ponytail: self-check — no LLM, just verify templates load and vars substitute
37
+ if (import.meta.url === `file://${process.argv[1]}`) {
38
+ const p = prereqPrompt("Stack: TypeScript\n - hono-api: REST API")
39
+ console.assert(p.includes("FAMILIAR"), "prereqPrompt missing FAMILIAR path")
40
+ console.assert(p.includes("UNFAMILIAR"), "prereqPrompt missing UNFAMILIAR path")
41
+ console.assert(!p.includes("{{"), "prereqPrompt has unresolved vars")
42
+
43
+ const t = theoryPrompt("primed", 3)
44
+ console.assert(t.includes("Prereq result: primed"), "theoryPrompt missing prereqResult")
45
+ console.assert(t.includes("LIVE RESEARCH RULE"), "theoryPrompt missing live research rule")
46
+ console.assert(t.includes("poiesis_theory_done"), "theoryPrompt missing gate call")
47
+ console.assert(!t.includes("{{"), "theoryPrompt has unresolved vars")
48
+
49
+ const pl = planPrompt(3)
50
+ console.assert(pl.includes("chapterNum: 3"), "planPrompt missing chapterNum")
51
+ console.assert(pl.includes("poiesis_confirm_test_plan"), "planPrompt missing tool call")
52
+ console.assert(!pl.includes("{{"), "planPrompt has unresolved vars")
53
+
54
+ const wt = writeTestsPrompt([{ name: "server 200", why: "proves server starts" }])
55
+ console.assert(wt.includes("server 200"), "writeTestsPrompt missing test name")
56
+ console.assert(wt.includes("poiesis_tests_written"), "writeTestsPrompt missing gate call")
57
+ console.assert(!wt.includes("{{"), "writeTestsPrompt has unresolved vars")
58
+
59
+ const imp = implementPrompt("tests/chapter-3.test.ts", 3)
60
+ console.assert(imp.includes("tests/chapter-3.test.ts"), "implementPrompt missing testsFile")
61
+ console.assert(imp.includes("chapter=3"), "implementPrompt missing chapterNum in run call")
62
+ console.assert(imp.includes("CRITICAL RULES"), "implementPrompt missing critical rules")
63
+ console.assert(!imp.includes("{{"), "implementPrompt has unresolved vars")
64
+
65
+ console.log("steps.ts: ok")
66
+ }
package/src/types.ts ADDED
@@ -0,0 +1,67 @@
1
+ // ── Chapter / Progress types ──────────────────────────────────────────────
2
+
3
+ export type ChapterKind = "code" | "theory"
4
+
5
+ export interface ChapterMeta {
6
+ type: ChapterKind
7
+ testsFile: string | null // null = theory chapter
8
+ testsPass: boolean | null // null = theory; false = failing; true = passing
9
+ }
10
+
11
+ export interface Progress {
12
+ current: number
13
+ total: number
14
+ completed: number[]
15
+ startedAt: string
16
+ lastActiveAt: string
17
+ chapters: Record<string, ChapterMeta>
18
+ }
19
+
20
+ // ── User / Config types ─────────────────────────────────────────────────────
21
+
22
+ export interface RecentProject {
23
+ name: string // repo or dir name
24
+ summary: string // one sentence: what was built, key tech used
25
+ stack: string[] // languages/frameworks in this project
26
+ }
27
+
28
+ export interface UserProfile {
29
+ primaryStack: string[] // languages/frameworks used most across all projects
30
+ recentProjects: RecentProject[] // up to 8 projects with context
31
+ recentActivity: string // one-line human summary of recent work
32
+ scannedAt: string // ISO timestamp — stale after 7 days
33
+ }
34
+
35
+ export interface Config {
36
+ state_dir: string // ~/.poiesis by default
37
+ scan_user?: boolean // undefined = not asked yet; true = opted in; false = opted out
38
+ // ponytail: llm_model / editor_cmd / gemini_api_key removed with tutor/build stripdown
39
+ }
40
+
41
+ // ── Chapter step state ────────────────────────────────────────────────────────
42
+
43
+ export type ChapterStep = "prereq" | "theory" | "plan" | "write-tests" | "implement"
44
+
45
+ export interface ChapterState {
46
+ step: ChapterStep
47
+ prereqResult: "familiar" | "primed" | null
48
+ testsFile: string | null
49
+ testsPlan: Array<{ name: string; why: string }>
50
+ testsPass: boolean
51
+ startedAt: string
52
+ }
53
+
54
+ // ── Roadmap types ─────────────────────────────────────────────────────────────
55
+
56
+ export interface RoadmapChapter {
57
+ n: number
58
+ title: string
59
+ duration: string
60
+ done: boolean
61
+ kind: ChapterKind | null
62
+ }
63
+
64
+ export interface Roadmap {
65
+ name: string
66
+ chapters: RoadmapChapter[]
67
+ }