@aprimediet/codewalker 1.0.0 → 1.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/README.md +44 -50
- package/index.ts +6 -42
- package/package.json +20 -39
- package/prompts/codewalker.md +7 -0
- package/skills/codewalker/SKILL.md +43 -0
- package/src/cards.test.ts +88 -0
- package/src/cards.ts +87 -0
- package/src/db.test.ts +197 -0
- package/src/db.ts +196 -0
- package/src/extract/ctags-parse.test.ts +108 -0
- package/src/extract/ctags-parse.ts +112 -0
- package/src/extract/ctags.ts +51 -0
- package/src/extract/docs.test.ts +81 -0
- package/src/extract/docs.ts +169 -0
- package/src/extract/regex.test.ts +202 -0
- package/src/extract/regex.ts +192 -0
- package/src/format.test.ts +71 -0
- package/src/format.ts +63 -0
- package/src/git.test.ts +75 -0
- package/src/git.ts +62 -0
- package/src/index.contract.test.ts +100 -0
- package/src/index.ts +124 -0
- package/src/indexer.test.ts +138 -0
- package/src/indexer.ts +352 -0
- package/src/project.test.ts +115 -0
- package/src/project.ts +204 -0
- package/src/query.test.ts +98 -0
- package/src/query.ts +73 -0
- package/src/sync.test.ts +116 -0
- package/src/types.ts +89 -0
- package/vitest.config.ts +28 -0
- package/LICENSE +0 -21
- package/agents.ts +0 -126
- package/compat.ts +0 -217
- package/detect.ts +0 -188
- package/docs/PRD.md +0 -78
- package/prd.ts +0 -106
- package/skills/learn-this/SKILL.md +0 -325
package/compat.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compatibility probe: detect if @aprimediet/minion and @aprimediet/memory are active
|
|
3
|
-
* for this project by reading the shared .pi/<project-id>.md marker and probing
|
|
4
|
-
* global directories.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
|
|
11
|
-
export interface CompatResult {
|
|
12
|
-
projectId: string | null;
|
|
13
|
-
minionActive: boolean;
|
|
14
|
-
memoryActive: boolean;
|
|
15
|
-
memoryEntries: number;
|
|
16
|
-
openTaskCount: number;
|
|
17
|
-
openTaskSummary: string;
|
|
18
|
-
memorySummary: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Walk upward from cwd until we find a dir containing .pi/ or .git/.
|
|
23
|
-
* Return cwd if root is reached without finding either.
|
|
24
|
-
*/
|
|
25
|
-
function findProjectRoot(cwd: string): string {
|
|
26
|
-
let dir = cwd;
|
|
27
|
-
for (;;) {
|
|
28
|
-
const piPath = path.join(dir, CONFIG_DIR_NAME);
|
|
29
|
-
const gitPath = path.join(dir, ".git");
|
|
30
|
-
if (fs.existsSync(piPath) || fs.existsSync(gitPath)) {
|
|
31
|
-
return dir;
|
|
32
|
-
}
|
|
33
|
-
const parent = path.dirname(dir);
|
|
34
|
-
if (parent === dir) {
|
|
35
|
-
// reached filesystem root
|
|
36
|
-
return cwd;
|
|
37
|
-
}
|
|
38
|
-
dir = parent;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Scan <configDir>/*.md files for one with frontmatter containing pi-project: true.
|
|
44
|
-
* Return that file's id: value, or null if none found.
|
|
45
|
-
*/
|
|
46
|
-
function readMarkerId(configDir: string): string | null {
|
|
47
|
-
if (!fs.existsSync(configDir)) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let names: string[];
|
|
52
|
-
try {
|
|
53
|
-
names = fs.readdirSync(configDir).filter((n) => n.endsWith(".md"));
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const name of names) {
|
|
59
|
-
const file = path.join(configDir, name);
|
|
60
|
-
try {
|
|
61
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
62
|
-
const { frontmatter } = parseFrontmatter<Record<string, string>>(content);
|
|
63
|
-
if (frontmatter && String(frontmatter["pi-project"]) === "true" && frontmatter.id) {
|
|
64
|
-
return frontmatter.id;
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
// not a valid marker, skip
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Extract status and title from task frontmatter.
|
|
76
|
-
* Return { status, title } or null if cannot parse.
|
|
77
|
-
* title may be undefined if missing from frontmatter; callers should supply a fallback.
|
|
78
|
-
*/
|
|
79
|
-
function parseTaskMetadata(content: string): { status: string; title: string | undefined } | null {
|
|
80
|
-
try {
|
|
81
|
-
const { frontmatter } = parseFrontmatter<Record<string, string>>(content);
|
|
82
|
-
if (!frontmatter) return null;
|
|
83
|
-
const status = frontmatter.status || "";
|
|
84
|
-
const title = frontmatter.title || undefined;
|
|
85
|
-
if (!status) return null;
|
|
86
|
-
return { status, title };
|
|
87
|
-
} catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Probe minion integration: check if <globalDir>/tasks/ exists and has open tasks.
|
|
94
|
-
* Return { openTasks, openTaskSummary } or null if minion is not active.
|
|
95
|
-
*/
|
|
96
|
-
function probeMinionTasks(globalDir: string): { openTasks: number; openTaskSummary: string } | null {
|
|
97
|
-
const tasksDir = path.join(globalDir, "tasks");
|
|
98
|
-
|
|
99
|
-
// If tasks dir doesn't exist, minion is not active for this project
|
|
100
|
-
if (!fs.existsSync(tasksDir)) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const OPEN_STATUSES = new Set(["backlog", "todo", "in_progress", "blocked", "review"]);
|
|
105
|
-
const openTasks: string[] = [];
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const names = fs.readdirSync(tasksDir);
|
|
109
|
-
for (const name of names) {
|
|
110
|
-
if (!name.endsWith(".md")) continue;
|
|
111
|
-
const file = path.join(tasksDir, name);
|
|
112
|
-
try {
|
|
113
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
114
|
-
const meta = parseTaskMetadata(content);
|
|
115
|
-
if (meta && OPEN_STATUSES.has(meta.status)) {
|
|
116
|
-
const taskTitle = meta.title ?? name.replace(/\.md$/, "");
|
|
117
|
-
openTasks.push(`${meta.status}: ${taskTitle}`);
|
|
118
|
-
if (openTasks.length >= 10) break;
|
|
119
|
-
}
|
|
120
|
-
} catch {
|
|
121
|
-
// skip unparseable files
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
} catch {
|
|
125
|
-
// tasks dir exists but is unreadable — still report as active
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const summary = openTasks.length > 0 ? openTasks.join("\n") : "(no open tasks)";
|
|
129
|
-
return { openTasks: openTasks.length, openTaskSummary: summary };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Probe memory integration: check if <globalDir>/memory/ exists and count entries.
|
|
134
|
-
* Also read MEMORY.md (up to 3000 chars).
|
|
135
|
-
* Return { entries, memorySummary } or null if memory is not active.
|
|
136
|
-
*/
|
|
137
|
-
function probeMemoryIntegration(globalDir: string): { entries: number; memorySummary: string } | null {
|
|
138
|
-
const memoryDir = path.join(globalDir, "memory");
|
|
139
|
-
|
|
140
|
-
// If memory dir doesn't exist, memory is not active for this project
|
|
141
|
-
if (!fs.existsSync(memoryDir)) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Count entries in memory/entries/
|
|
146
|
-
let entriesCount = 0;
|
|
147
|
-
const entriesDir = path.join(memoryDir, "entries");
|
|
148
|
-
try {
|
|
149
|
-
if (fs.existsSync(entriesDir)) {
|
|
150
|
-
const files = fs.readdirSync(entriesDir);
|
|
151
|
-
entriesCount = files.filter((f) => f.endsWith(".md")).length;
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
// entries dir unreadable, but memory is still active
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Read MEMORY.md (truncated at 3000 chars)
|
|
158
|
-
let memorySummary = "";
|
|
159
|
-
const memoryFile = path.join(memoryDir, "MEMORY.md");
|
|
160
|
-
try {
|
|
161
|
-
if (fs.existsSync(memoryFile)) {
|
|
162
|
-
const content = fs.readFileSync(memoryFile, "utf-8");
|
|
163
|
-
memorySummary = content.length > 3000 ? content.slice(0, 3000) : content;
|
|
164
|
-
}
|
|
165
|
-
} catch {
|
|
166
|
-
// MEMORY.md unreadable
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return { entries: entriesCount, memorySummary };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Probe compatibility: detect minion and memory integration for the project at cwd.
|
|
174
|
-
* Return detailed status for both extensions in a flat structure.
|
|
175
|
-
*/
|
|
176
|
-
export function probeCompat(cwd: string): CompatResult {
|
|
177
|
-
const root = findProjectRoot(cwd);
|
|
178
|
-
const configDir = path.join(root, CONFIG_DIR_NAME);
|
|
179
|
-
const projectId = readMarkerId(configDir);
|
|
180
|
-
|
|
181
|
-
// Initialize with defaults
|
|
182
|
-
const base: CompatResult = {
|
|
183
|
-
projectId,
|
|
184
|
-
minionActive: false,
|
|
185
|
-
memoryActive: false,
|
|
186
|
-
memoryEntries: 0,
|
|
187
|
-
openTaskCount: 0,
|
|
188
|
-
openTaskSummary: "",
|
|
189
|
-
memorySummary: "",
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// If no project marker, return defaults
|
|
193
|
-
if (!projectId) {
|
|
194
|
-
return base;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const piHome = path.dirname(getAgentDir());
|
|
198
|
-
const globalDir = path.join(piHome, "projects", projectId);
|
|
199
|
-
|
|
200
|
-
// Probe minion
|
|
201
|
-
const minionProbe = probeMinionTasks(globalDir);
|
|
202
|
-
if (minionProbe) {
|
|
203
|
-
base.minionActive = true;
|
|
204
|
-
base.openTaskCount = minionProbe.openTasks;
|
|
205
|
-
base.openTaskSummary = minionProbe.openTaskSummary;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Probe memory
|
|
209
|
-
const memoryProbe = probeMemoryIntegration(globalDir);
|
|
210
|
-
if (memoryProbe) {
|
|
211
|
-
base.memoryActive = true;
|
|
212
|
-
base.memoryEntries = memoryProbe.entries;
|
|
213
|
-
base.memorySummary = memoryProbe.memorySummary;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return base;
|
|
217
|
-
}
|
package/detect.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
export interface TechStack {
|
|
6
|
-
primary: string[];
|
|
7
|
-
frameworks: string[];
|
|
8
|
-
infrastructure: string[];
|
|
9
|
-
packageManagers: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface TechIssue {
|
|
13
|
-
severity: "error" | "warning" | "info";
|
|
14
|
-
location: string;
|
|
15
|
-
message: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ProjectStatus {
|
|
19
|
-
recentCommits: string;
|
|
20
|
-
hasChangelog: boolean;
|
|
21
|
-
todoCount: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const LANG_FILES: Array<{ file: string; lang: string }> = [
|
|
25
|
-
{ file: "package.json", lang: "Node.js" },
|
|
26
|
-
{ file: "tsconfig.json", lang: "TypeScript" },
|
|
27
|
-
{ file: "requirements.txt", lang: "Python" },
|
|
28
|
-
{ file: "pyproject.toml", lang: "Python" },
|
|
29
|
-
{ file: "go.mod", lang: "Go" },
|
|
30
|
-
{ file: "Cargo.toml", lang: "Rust" },
|
|
31
|
-
{ file: "pom.xml", lang: "Java" },
|
|
32
|
-
{ file: "build.gradle", lang: "Java/Kotlin" },
|
|
33
|
-
{ file: "build.gradle.kts", lang: "Kotlin" },
|
|
34
|
-
{ file: "Gemfile", lang: "Ruby" },
|
|
35
|
-
{ file: "composer.json", lang: "PHP" },
|
|
36
|
-
{ file: "mix.exs", lang: "Elixir" },
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const FRAMEWORK_FILES: Array<{ file: string; name: string; infra?: boolean }> = [
|
|
40
|
-
{ file: "next.config.js", name: "Next.js" },
|
|
41
|
-
{ file: "next.config.ts", name: "Next.js" },
|
|
42
|
-
{ file: "next.config.mjs", name: "Next.js" },
|
|
43
|
-
{ file: "vite.config.ts", name: "Vite" },
|
|
44
|
-
{ file: "vite.config.js", name: "Vite" },
|
|
45
|
-
{ file: "nuxt.config.ts", name: "Nuxt.js" },
|
|
46
|
-
{ file: "svelte.config.js", name: "SvelteKit" },
|
|
47
|
-
{ file: "astro.config.mjs", name: "Astro" },
|
|
48
|
-
{ file: "remix.config.js", name: "Remix" },
|
|
49
|
-
{ file: "angular.json", name: "Angular" },
|
|
50
|
-
{ file: "tailwind.config.ts", name: "Tailwind CSS" },
|
|
51
|
-
{ file: "tailwind.config.js", name: "Tailwind CSS" },
|
|
52
|
-
{ file: "biome.json", name: "Biome" },
|
|
53
|
-
{ file: "Dockerfile", name: "Docker", infra: true },
|
|
54
|
-
{ file: "docker-compose.yml", name: "Docker Compose", infra: true },
|
|
55
|
-
{ file: ".github/workflows", name: "GitHub Actions", infra: true },
|
|
56
|
-
{ file: ".gitlab-ci.yml", name: "GitLab CI", infra: true },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
const NPM_FRAMEWORKS: Record<string, string> = {
|
|
60
|
-
react: "React",
|
|
61
|
-
vue: "Vue",
|
|
62
|
-
express: "Express",
|
|
63
|
-
fastify: "Fastify",
|
|
64
|
-
hono: "Hono",
|
|
65
|
-
"@nestjs/core": "NestJS",
|
|
66
|
-
prisma: "Prisma",
|
|
67
|
-
"@prisma/client": "Prisma",
|
|
68
|
-
"drizzle-orm": "Drizzle ORM",
|
|
69
|
-
"@trpc/server": "tRPC",
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export function detectTechStack(root: string): TechStack {
|
|
73
|
-
const primary = new Set<string>();
|
|
74
|
-
const frameworks = new Set<string>();
|
|
75
|
-
const infrastructure = new Set<string>();
|
|
76
|
-
const packageManagers = new Set<string>();
|
|
77
|
-
|
|
78
|
-
for (const d of LANG_FILES) {
|
|
79
|
-
if (fs.existsSync(path.join(root, d.file))) primary.add(d.lang);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const d of FRAMEWORK_FILES) {
|
|
83
|
-
if (fs.existsSync(path.join(root, d.file))) {
|
|
84
|
-
if (d.infra) infrastructure.add(d.name);
|
|
85
|
-
else frameworks.add(d.name);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (fs.existsSync(path.join(root, "package-lock.json"))) packageManagers.add("npm");
|
|
90
|
-
if (fs.existsSync(path.join(root, "yarn.lock"))) packageManagers.add("yarn");
|
|
91
|
-
if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) packageManagers.add("pnpm");
|
|
92
|
-
if (fs.existsSync(path.join(root, "bun.lock"))) packageManagers.add("bun");
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
|
|
96
|
-
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
97
|
-
for (const [dep, label] of Object.entries(NPM_FRAMEWORKS)) {
|
|
98
|
-
if (dep in deps) frameworks.add(label);
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
/***/
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
primary: [...primary],
|
|
106
|
-
frameworks: [...frameworks],
|
|
107
|
-
infrastructure: [...infrastructure],
|
|
108
|
-
packageManagers: [...packageManagers],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function detectProjectStatus(root: string): ProjectStatus {
|
|
113
|
-
let recentCommits = "";
|
|
114
|
-
try {
|
|
115
|
-
recentCommits = execSync("git log --oneline -20", {
|
|
116
|
-
cwd: root,
|
|
117
|
-
encoding: "utf-8",
|
|
118
|
-
timeout: 5000,
|
|
119
|
-
}).trim();
|
|
120
|
-
} catch {
|
|
121
|
-
recentCommits = "(git not available or no commits)";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const hasChangelog =
|
|
125
|
-
fs.existsSync(path.join(root, "CHANGELOG.md")) || fs.existsSync(path.join(root, "CHANGELOG"));
|
|
126
|
-
|
|
127
|
-
let todoCount = 0;
|
|
128
|
-
try {
|
|
129
|
-
const result = execSync(
|
|
130
|
-
`grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" --include="*.rs" -c "TODO:\\|FIXME:\\|HACK:\\|BUG:" . 2>/dev/null || true`,
|
|
131
|
-
{ cwd: root, encoding: "utf-8", timeout: 10000 }
|
|
132
|
-
);
|
|
133
|
-
for (const line of result.split("\n")) {
|
|
134
|
-
const m = line.match(/:(\d+)$/);
|
|
135
|
-
if (m) todoCount += parseInt(m[1], 10);
|
|
136
|
-
}
|
|
137
|
-
} catch {
|
|
138
|
-
/***/
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { recentCommits, hasChangelog, todoCount };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function detectTechnicalIssues(root: string): TechIssue[] {
|
|
145
|
-
const issues: TechIssue[] = [];
|
|
146
|
-
|
|
147
|
-
// Invalid tsconfig.json
|
|
148
|
-
const tsconfigPath = path.join(root, "tsconfig.json");
|
|
149
|
-
if (fs.existsSync(tsconfigPath)) {
|
|
150
|
-
try {
|
|
151
|
-
JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
|
|
152
|
-
} catch (e) {
|
|
153
|
-
issues.push({
|
|
154
|
-
severity: "error",
|
|
155
|
-
location: "tsconfig.json",
|
|
156
|
-
message: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// No test directory or config
|
|
162
|
-
const testDirs = ["test", "tests", "__tests__", "spec"];
|
|
163
|
-
const testConfigs = ["vitest.config.ts", "vitest.config.js", "jest.config.ts", "jest.config.js"];
|
|
164
|
-
const hasTests =
|
|
165
|
-
testDirs.some((d) => fs.existsSync(path.join(root, d))) ||
|
|
166
|
-
testConfigs.some((f) => fs.existsSync(path.join(root, f)));
|
|
167
|
-
if (!hasTests) {
|
|
168
|
-
issues.push({
|
|
169
|
-
severity: "warning",
|
|
170
|
-
location: "root",
|
|
171
|
-
message: "No test directory or test config detected",
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// .env.example without .env
|
|
176
|
-
if (
|
|
177
|
-
fs.existsSync(path.join(root, ".env.example")) &&
|
|
178
|
-
!fs.existsSync(path.join(root, ".env"))
|
|
179
|
-
) {
|
|
180
|
-
issues.push({
|
|
181
|
-
severity: "info",
|
|
182
|
-
location: ".env",
|
|
183
|
-
message: ".env.example exists but .env is missing — check environment setup",
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return issues;
|
|
188
|
-
}
|
package/docs/PRD.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Product Requirements Document: @aprimediet/codewalker
|
|
2
|
-
|
|
3
|
-
**Version:** 1.0
|
|
4
|
-
**Date:** 2026-06-25
|
|
5
|
-
**Status:** Draft
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
Project intelligence extension for the pi coding agent that systematically analyzes any project — tech stack, goals, boundaries, status, technical issues — and produces structured documentation: a **PRD** for humans and **AGENTS.md** / **CLAUDE.md** for coding agents.
|
|
9
|
-
|
|
10
|
-
## Problem Statement
|
|
11
|
-
Developers using the pi coding agent need a fast, reliable way to understand a project they're working on without manually reading every file. The coding agent also needs structured project instructions to work effectively. Currently there's no automated way to generate this intelligence snapshot and split the content by audience (human vs. agent).
|
|
12
|
-
|
|
13
|
-
## Goals
|
|
14
|
-
- Identify tech stack (language, frameworks, infrastructure, package manager)
|
|
15
|
-
- Identify project goals and non-goals (interactively when missing)
|
|
16
|
-
- Identify boundaries, scope, and constraints of a project
|
|
17
|
-
- Generate a human-readable PRD.md with product context
|
|
18
|
-
- Generate an engineering-focused AGENTS.md for coding agents
|
|
19
|
-
- Detect companion extensions (minion, memory) and report their state
|
|
20
|
-
- Store the intelligence snapshot to persistent memory for session continuity
|
|
21
|
-
|
|
22
|
-
## Non-Goals
|
|
23
|
-
- Give suggestions or recommendations on how to improve the project
|
|
24
|
-
- Try to solve or fix technical issues found during analysis
|
|
25
|
-
- Research how to solve technical issues — diagnose only, no solutions
|
|
26
|
-
- Modify project files beyond the documentation it generates
|
|
27
|
-
|
|
28
|
-
## Target Users
|
|
29
|
-
Developers who use the pi coding agent and need to quickly get up to speed on a project or generate structured documentation for human and agent consumption.
|
|
30
|
-
|
|
31
|
-
## Key Features
|
|
32
|
-
|
|
33
|
-
### Tech Stack Detection
|
|
34
|
-
Automatically scan manifest files (package.json, tsconfig.json, requirements.txt, go.mod, Cargo.toml, etc.) and framework configs to identify primary language, frameworks, infrastructure, and package manager.
|
|
35
|
-
|
|
36
|
-
### Goals & Non-Goals Gathering
|
|
37
|
-
Search README and docs for existing goals. If not found, interactively gather them from the user one question at a time — problem solved, primary users, key features, out-of-scope items, success metrics.
|
|
38
|
-
|
|
39
|
-
### Boundary Documentation
|
|
40
|
-
Identify key entry points, external services, exposed interfaces (APIs, CLI flags), and technical constraints (runtime version, env vars, OS requirements).
|
|
41
|
-
|
|
42
|
-
### Project Status Scanning
|
|
43
|
-
Read recent git commits, check for changelogs/roadmaps, scan for TODO/FIXME/HACK/BUG markers in source code.
|
|
44
|
-
|
|
45
|
-
### Technical Issue Detection
|
|
46
|
-
Detect missing tests, broken env files, invalid configs, and TypeScript strictness issues — report without trying to fix.
|
|
47
|
-
|
|
48
|
-
### Audience-Split Documentation
|
|
49
|
-
Generate three documents from the same intelligence, each targeted at its audience:
|
|
50
|
-
- **docs/PRD.md** (humans, product) — overview, problem, goals, users, features, success metrics
|
|
51
|
-
- **AGENTS.md** (coding agents, engineering) — tech stack, commands, conventions, technical boundaries, gotchas
|
|
52
|
-
- **CLAUDE.md** (Claude Code) — thin pointer that imports AGENTS.md
|
|
53
|
-
|
|
54
|
-
### Integration Detection
|
|
55
|
-
Detect active `@aprimediet/minion` and `@aprimediet/memory` extensions via shared `.pi/<id>.md` project marker. Report open tasks and memory entries without modifying any files.
|
|
56
|
-
|
|
57
|
-
### Memory Storage
|
|
58
|
-
Store the full project intelligence snapshot to `@aprimediet/memory` (scope: project) for persistent recall across sessions.
|
|
59
|
-
|
|
60
|
-
## Success Metrics
|
|
61
|
-
- User can read a summary of their current project in PRD.md without reading every file manually
|
|
62
|
-
- Coding agent can read specific project instructions in AGENTS.md to work effectively
|
|
63
|
-
- Pi coding agent can learn and store the latest project snapshot in memory for session continuity
|
|
64
|
-
- Documentation is correctly split by audience with no overlap
|
|
65
|
-
|
|
66
|
-
## Scope & Boundaries
|
|
67
|
-
- **Runtime:** Node.js with ESM modules (`"type": "module"`)
|
|
68
|
-
- **Peer dependency:** `@earendil-works/pi-coding-agent`
|
|
69
|
-
- **No third-party runtime dependencies** — Node built-ins only (`fs`, `path`, `child_process`)
|
|
70
|
-
- **Exposed interface:** single `/learn-this` command registered with the pi extension API
|
|
71
|
-
- **File system operations:** all detection probes are read-only; writes are confined to documentation files (PRD.md, AGENTS.md, CLAUDE.md) and memory storage
|
|
72
|
-
- **Not a CI tool:** runs on-demand via `/learn-this`, not as a background service
|
|
73
|
-
|
|
74
|
-
## Open Questions
|
|
75
|
-
- Should AGENTS.md be auto-generated or user-edited after generation?
|
|
76
|
-
- How often should memory snapshots be refreshed — on every `/learn-this` run or on demand?
|
|
77
|
-
- Should the extension support non-Node.js projects (Python, Go, Rust) for full PRD/AGENTS.md generation?
|
|
78
|
-
- Should there be a flag to skip interactive questions when running in non-interactive mode?
|
package/prd.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
const PRD_CANDIDATES = [
|
|
5
|
-
"docs/PRD.md",
|
|
6
|
-
"docs/prd.md",
|
|
7
|
-
"PRD.md",
|
|
8
|
-
".pi/prd.md",
|
|
9
|
-
"PRODUCT.md",
|
|
10
|
-
"SPEC.md",
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
export function findExistingPRD(root: string): string | null {
|
|
14
|
-
for (const p of PRD_CANDIDATES) {
|
|
15
|
-
const filePath = path.join(root, p);
|
|
16
|
-
if (fs.existsSync(filePath)) {
|
|
17
|
-
return filePath;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// README with a goals/requirements heading
|
|
22
|
-
const readme = path.join(root, "README.md");
|
|
23
|
-
if (fs.existsSync(readme)) {
|
|
24
|
-
try {
|
|
25
|
-
const text = fs.readFileSync(readme, "utf-8");
|
|
26
|
-
if (/^##\s+(Goals|Non-Goals|Requirements|Product Requirements)/im.test(text)) {
|
|
27
|
-
return readme;
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
// Ignore read errors
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function readPRD(filePath: string): string {
|
|
38
|
-
try {
|
|
39
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
40
|
-
} catch {
|
|
41
|
-
return "";
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function createPRD(root: string, content: string): string {
|
|
46
|
-
const outPath = path.join(root, "docs", "PRD.md");
|
|
47
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
48
|
-
fs.writeFileSync(outPath, content, "utf-8");
|
|
49
|
-
return outPath;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function prdTemplate(p: {
|
|
53
|
-
projectName: string;
|
|
54
|
-
overview: string;
|
|
55
|
-
problem: string;
|
|
56
|
-
goals: string[];
|
|
57
|
-
nonGoals: string[];
|
|
58
|
-
targetUsers: string;
|
|
59
|
-
keyFeatures: Array<{ name: string; description: string }>;
|
|
60
|
-
successMetrics: string[];
|
|
61
|
-
boundaries: string;
|
|
62
|
-
openQuestions: string[];
|
|
63
|
-
date: string;
|
|
64
|
-
}): string {
|
|
65
|
-
const goalsSection = p.goals.map((g) => `- ${g}`).join("\n");
|
|
66
|
-
const nonGoalsSection = p.nonGoals.map((g) => `- ${g}`).join("\n");
|
|
67
|
-
const featuresSection = p.keyFeatures
|
|
68
|
-
.map((f) => `### ${f.name}\n${f.description}`)
|
|
69
|
-
.join("\n\n");
|
|
70
|
-
const metricsSection = p.successMetrics.map((m) => `- ${m}`).join("\n");
|
|
71
|
-
const questionsSection = p.openQuestions.map((q) => `- ${q}`).join("\n");
|
|
72
|
-
|
|
73
|
-
return `# Product Requirements Document: ${p.projectName}
|
|
74
|
-
|
|
75
|
-
**Version:** 1.0
|
|
76
|
-
**Date:** ${p.date}
|
|
77
|
-
**Status:** Draft
|
|
78
|
-
|
|
79
|
-
## Overview
|
|
80
|
-
${p.overview}
|
|
81
|
-
|
|
82
|
-
## Problem Statement
|
|
83
|
-
${p.problem}
|
|
84
|
-
|
|
85
|
-
## Goals
|
|
86
|
-
${goalsSection}
|
|
87
|
-
|
|
88
|
-
## Non-Goals
|
|
89
|
-
${nonGoalsSection}
|
|
90
|
-
|
|
91
|
-
## Target Users
|
|
92
|
-
${p.targetUsers}
|
|
93
|
-
|
|
94
|
-
## Key Features
|
|
95
|
-
${featuresSection}
|
|
96
|
-
|
|
97
|
-
## Success Metrics
|
|
98
|
-
${metricsSection}
|
|
99
|
-
|
|
100
|
-
## Boundaries & Constraints
|
|
101
|
-
${p.boundaries}
|
|
102
|
-
|
|
103
|
-
## Open Questions
|
|
104
|
-
${questionsSection}
|
|
105
|
-
`;
|
|
106
|
-
}
|