@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
|
@@ -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
|
+
}
|
package/src/progress.ts
ADDED
|
@@ -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
|
+
}
|