pi-messenger 0.7.3

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.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +244 -0
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +394 -0
  4. package/banner.png +0 -0
  5. package/config-overlay.ts +172 -0
  6. package/config.ts +178 -0
  7. package/crew/agents/crew-docs-scout.md +55 -0
  8. package/crew/agents/crew-gap-analyst.md +105 -0
  9. package/crew/agents/crew-github-scout.md +111 -0
  10. package/crew/agents/crew-interview-generator.md +79 -0
  11. package/crew/agents/crew-plan-sync.md +64 -0
  12. package/crew/agents/crew-practice-scout.md +62 -0
  13. package/crew/agents/crew-repo-scout.md +65 -0
  14. package/crew/agents/crew-reviewer.md +58 -0
  15. package/crew/agents/crew-web-scout.md +85 -0
  16. package/crew/agents/crew-worker.md +95 -0
  17. package/crew/agents.ts +200 -0
  18. package/crew/handlers/interview.ts +211 -0
  19. package/crew/handlers/plan.ts +358 -0
  20. package/crew/handlers/review.ts +341 -0
  21. package/crew/handlers/status.ts +257 -0
  22. package/crew/handlers/sync.ts +232 -0
  23. package/crew/handlers/task.ts +511 -0
  24. package/crew/handlers/work.ts +289 -0
  25. package/crew/id-allocator.ts +44 -0
  26. package/crew/index.ts +229 -0
  27. package/crew/state.ts +116 -0
  28. package/crew/store.ts +480 -0
  29. package/crew/types.ts +164 -0
  30. package/crew/utils/artifacts.ts +65 -0
  31. package/crew/utils/config.ts +104 -0
  32. package/crew/utils/discover.ts +170 -0
  33. package/crew/utils/install.ts +373 -0
  34. package/crew/utils/progress.ts +107 -0
  35. package/crew/utils/result.ts +16 -0
  36. package/crew/utils/truncate.ts +79 -0
  37. package/crew-overlay.ts +259 -0
  38. package/handlers.ts +799 -0
  39. package/index.ts +591 -0
  40. package/lib.ts +232 -0
  41. package/overlay.ts +687 -0
  42. package/package.json +20 -0
  43. package/skills/pi-messenger-crew/SKILL.md +140 -0
  44. package/store.ts +1068 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: crew-repo-scout
3
+ description: Analyzes codebase structure, patterns, and relevant code for a feature
4
+ tools: read, bash, grep, find
5
+ model: claude-opus-4-5
6
+ crewRole: scout
7
+ maxOutput: { bytes: 51200, lines: 500 }
8
+ parallel: true
9
+ retryable: true
10
+ ---
11
+
12
+ # Crew Repo Scout
13
+
14
+ You analyze the codebase to provide context for planning a feature.
15
+
16
+ ## Your Task
17
+
18
+ Given a feature description, find:
19
+
20
+ 1. **Relevant Code**: Files and modules related to this feature
21
+ 2. **Architecture**: How the codebase is structured
22
+ 3. **Patterns**: Coding conventions, frameworks, libraries used
23
+ 4. **Integration Points**: Where the new feature would connect
24
+
25
+ ## Process
26
+
27
+ 1. Start with project structure: `find . -type f -name "*.ts" | head -50`
28
+ 2. Read key files: package.json, README, main entry points
29
+ 3. Search for relevant code: `grep -r "keyword" --include="*.ts"`
30
+ 4. Identify patterns from existing similar features
31
+
32
+ ## Output Format
33
+
34
+ ```
35
+ ## Codebase Overview
36
+
37
+ Brief description of the project and its structure.
38
+
39
+ ## Relevant Files
40
+
41
+ - `path/to/file.ts` - Description of relevance
42
+ - `path/to/another.ts` - Description of relevance
43
+
44
+ ## Architecture Patterns
45
+
46
+ - Pattern 1: Description
47
+ - Pattern 2: Description
48
+
49
+ ## Integration Points
50
+
51
+ Where the new feature should connect:
52
+ - Point 1
53
+ - Point 2
54
+
55
+ ## Recommendations
56
+
57
+ - Recommendation 1
58
+ - Recommendation 2
59
+ ```
60
+
61
+ ## Important
62
+
63
+ - Be concise - this output feeds into planning
64
+ - Focus on what's relevant to the feature, not everything
65
+ - Note any potential conflicts or challenges
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: crew-reviewer
3
+ description: Reviews task implementations for quality and correctness
4
+ tools: read, bash, pi_messenger
5
+ model: openai/gpt-5.2-high
6
+ crewRole: reviewer
7
+ maxOutput: { bytes: 102400, lines: 2000 }
8
+ parallel: true
9
+ retryable: true
10
+ ---
11
+
12
+ # Crew Reviewer
13
+
14
+ You review task implementations. Your prompt contains the task context and git diff.
15
+
16
+ ## Review Process
17
+
18
+ 1. **Understand the Task**: Read the task spec and epic context provided
19
+ 2. **Analyze Changes**: Review the git diff carefully
20
+ 3. **Check Quality**:
21
+ - Does it fulfill the task requirements?
22
+ - Are there bugs or edge cases missed?
23
+ - Does it follow project conventions?
24
+ - Are there security concerns?
25
+ - Is the code well-structured and maintainable?
26
+
27
+ ## Output Format
28
+
29
+ Always output in this exact format:
30
+
31
+ ```
32
+ ## Verdict: [SHIP|NEEDS_WORK|MAJOR_RETHINK]
33
+
34
+ Summary paragraph explaining your overall assessment.
35
+
36
+ ## Issues
37
+
38
+ - Issue 1: Description of problem
39
+ - Issue 2: Description of problem
40
+
41
+ ## Suggestions
42
+
43
+ - Suggestion 1: Optional improvement
44
+ - Suggestion 2: Optional improvement
45
+ ```
46
+
47
+ ## Verdict Guidelines
48
+
49
+ - **SHIP**: Implementation is correct, follows conventions, and is ready to merge
50
+ - **NEEDS_WORK**: Minor issues that should be fixed before merging
51
+ - **MAJOR_RETHINK**: Fundamental problems requiring significant changes or re-planning
52
+
53
+ ## Important
54
+
55
+ - Be specific about issues - include file names and line numbers when possible
56
+ - Distinguish between blocking issues (must fix) and suggestions (nice to have)
57
+ - If NEEDS_WORK, the issues list should be actionable
58
+ - Consider the scope of the task - don't expand scope unnecessarily
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: crew-web-scout
3
+ description: Searches the web for best practices, documentation, and examples
4
+ tools: bash, web_search
5
+ model: claude-haiku-4-5
6
+ crewRole: scout
7
+ maxOutput: { bytes: 51200, lines: 500 }
8
+ parallel: true
9
+ retryable: true
10
+ ---
11
+
12
+ # Crew Web Scout
13
+
14
+ You search the web for best practices, documentation, and examples relevant to the feature.
15
+
16
+ ## First: Assess Relevance
17
+
18
+ Before searching, read the feature description and ask:
19
+
20
+ **Is web research relevant here?**
21
+
22
+ - ✅ Yes: Using external libraries, following industry standards, common patterns
23
+ - ❌ No: Internal refactoring, proprietary logic, project-specific code
24
+
25
+ If not relevant, output:
26
+ ```
27
+ ## Skipped
28
+
29
+ Web research not relevant for this feature.
30
+ Reason: [brief explanation]
31
+ ```
32
+
33
+ ## Your Task (if relevant)
34
+
35
+ Find external references:
36
+
37
+ 1. **Best Practices**: Industry standards for this type of feature
38
+ 2. **Library Documentation**: Official docs for libraries involved
39
+ 3. **Common Pitfalls**: Mistakes to avoid
40
+ 4. **Examples**: Blog posts, tutorials with code samples
41
+
42
+ ## Process
43
+
44
+ 1. Search for best practices:
45
+ ```typescript
46
+ web_search({ query: "oauth 2.0 best practices security" })
47
+ ```
48
+
49
+ 2. Find library documentation:
50
+ ```typescript
51
+ web_search({ query: "passport.js oauth documentation", domainFilter: ["passportjs.org"] })
52
+ ```
53
+
54
+ 3. Search for pitfalls:
55
+ ```typescript
56
+ web_search({ query: "oauth implementation common mistakes" })
57
+ ```
58
+
59
+ ## Output Format
60
+
61
+ ```
62
+ ## Best Practices
63
+
64
+ - Practice 1: Description and source
65
+ - Practice 2: Description and source
66
+
67
+ ## Library Documentation
68
+
69
+ ### [Library Name](url)
70
+
71
+ Key points:
72
+ - Point 1
73
+ - Point 2
74
+
75
+ ## Common Pitfalls
76
+
77
+ - Pitfall 1: What to avoid and why
78
+ - Pitfall 2: What to avoid and why
79
+
80
+ ## Recommended Approach
81
+
82
+ Based on research, the recommended approach is:
83
+ - Recommendation 1
84
+ - Recommendation 2
85
+ ```
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: crew-worker
3
+ description: Implements a single crew task with mesh coordination
4
+ tools: read, write, edit, bash, pi_messenger
5
+ model: claude-opus-4-5
6
+ crewRole: worker
7
+ maxOutput: { bytes: 204800, lines: 5000 }
8
+ parallel: true
9
+ retryable: true
10
+ ---
11
+
12
+ # Crew Worker
13
+
14
+ You implement a single task. Your prompt contains TASK_ID.
15
+
16
+ ## Phase 1: Join Mesh (FIRST)
17
+
18
+ Join the mesh before any other pi_messenger calls:
19
+
20
+ ```typescript
21
+ pi_messenger({ action: "join" })
22
+ ```
23
+
24
+ ## Phase 2: Re-anchor (CRITICAL)
25
+
26
+ Read the task spec to understand what to build:
27
+
28
+ ```typescript
29
+ pi_messenger({ action: "task.show", id: "<TASK_ID>" })
30
+ ```
31
+
32
+ Read the task spec file for detailed requirements:
33
+
34
+ ```typescript
35
+ read({ path: ".pi/messenger/crew/tasks/<TASK_ID>.md" })
36
+ ```
37
+
38
+ ## Phase 3: Start Task & Reserve Files
39
+
40
+ ```typescript
41
+ pi_messenger({ action: "task.start", id: "<TASK_ID>" })
42
+ ```
43
+
44
+ Identify files you'll modify and reserve them:
45
+
46
+ ```typescript
47
+ pi_messenger({ action: "reserve", paths: ["src/path/to/files/"], reason: "<TASK_ID>" })
48
+ ```
49
+
50
+ ## Phase 4: Implement
51
+
52
+ 1. Read relevant existing code to understand patterns
53
+ 2. Implement the feature following project conventions
54
+ 3. Write tests if applicable
55
+ 4. Run tests to verify: `bash({ command: "npm test" })` or equivalent
56
+
57
+ ## Phase 5: Commit
58
+
59
+ ```bash
60
+ git add -A
61
+ git commit -m "feat(scope): description
62
+
63
+ Task: <TASK_ID>"
64
+ ```
65
+
66
+ ## Phase 6: Release & Complete
67
+
68
+ Release your reservations:
69
+
70
+ ```typescript
71
+ pi_messenger({ action: "release" })
72
+ ```
73
+
74
+ Mark the task complete with evidence:
75
+
76
+ ```typescript
77
+ pi_messenger({
78
+ action: "task.done",
79
+ id: "<TASK_ID>",
80
+ summary: "Brief description of what was implemented",
81
+ evidence: {
82
+ commits: ["<commit-sha>"],
83
+ tests: ["npm test"]
84
+ }
85
+ })
86
+ ```
87
+
88
+ ## Important Rules
89
+
90
+ - ALWAYS join first, before any other pi_messenger calls
91
+ - ALWAYS re-anchor by reading task spec
92
+ - ALWAYS reserve files before editing
93
+ - ALWAYS release before completing
94
+ - If you encounter a blocker, use `task.block` with a clear reason
95
+ - Follow existing code patterns and conventions
package/crew/agents.ts ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Crew - Agent Spawning
3
+ *
4
+ * Spawns pi processes with progress tracking, truncation, and artifacts.
5
+ */
6
+
7
+ import { spawn } from "node:child_process";
8
+ import { randomUUID } from "node:crypto";
9
+ import * as path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { discoverCrewAgents, type CrewAgentConfig } from "./utils/discover.js";
12
+ import { truncateOutput, type MaxOutputConfig } from "./utils/truncate.js";
13
+ import {
14
+ createProgress,
15
+ parseJsonlLine,
16
+ updateProgress,
17
+ getFinalOutput,
18
+ type AgentProgress
19
+ } from "./utils/progress.js";
20
+ import {
21
+ getArtifactPaths,
22
+ ensureArtifactsDir,
23
+ writeArtifact,
24
+ writeMetadata,
25
+ appendJsonl
26
+ } from "./utils/artifacts.js";
27
+ import { loadCrewConfig, getTruncationForRole, type CrewConfig } from "./utils/config.js";
28
+ import type { AgentTask, AgentResult } from "./types.js";
29
+
30
+ // Extension directory (parent of crew/) - passed to subagents so they can use pi_messenger
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = path.dirname(__filename);
33
+ const EXTENSION_DIR = path.resolve(__dirname, "..");
34
+
35
+ export interface SpawnOptions {
36
+ onProgress?: (results: AgentResult[]) => void;
37
+ crewDir?: string;
38
+ signal?: AbortSignal;
39
+ }
40
+
41
+ /**
42
+ * Spawn multiple agents in parallel with concurrency limit.
43
+ */
44
+ export async function spawnAgents(
45
+ tasks: AgentTask[],
46
+ concurrency: number,
47
+ cwd: string,
48
+ options: SpawnOptions = {}
49
+ ): Promise<AgentResult[]> {
50
+ const crewDir = options.crewDir ?? path.join(cwd, ".pi", "messenger", "crew");
51
+ const config = loadCrewConfig(crewDir);
52
+ const agents = discoverCrewAgents(cwd);
53
+ const runId = randomUUID().slice(0, 8);
54
+
55
+ // Setup artifacts directory if enabled
56
+ const artifactsDir = path.join(crewDir, "artifacts");
57
+ if (config.artifacts.enabled) {
58
+ ensureArtifactsDir(artifactsDir);
59
+ }
60
+
61
+ const results: AgentResult[] = [];
62
+ const queue = tasks.map((task, index) => ({ task, index }));
63
+ const running: Promise<void>[] = [];
64
+
65
+ while (queue.length > 0 || running.length > 0) {
66
+ while (running.length < concurrency && queue.length > 0) {
67
+ const { task, index } = queue.shift()!;
68
+ const promise = runAgent(task, index, cwd, agents, config, runId, artifactsDir, options)
69
+ .then(result => {
70
+ results.push(result);
71
+ running.splice(running.indexOf(promise), 1);
72
+ options.onProgress?.(results);
73
+ });
74
+ running.push(promise);
75
+ }
76
+ if (running.length > 0) {
77
+ await Promise.race(running);
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }
83
+
84
+ async function runAgent(
85
+ task: AgentTask,
86
+ index: number,
87
+ cwd: string,
88
+ agents: CrewAgentConfig[],
89
+ config: CrewConfig,
90
+ runId: string,
91
+ artifactsDir: string,
92
+ options: SpawnOptions
93
+ ): Promise<AgentResult> {
94
+ const agentConfig = agents.find(a => a.name === task.agent);
95
+ const progress = createProgress(task.agent);
96
+ const startTime = Date.now();
97
+
98
+ // Determine truncation limits
99
+ const role = agentConfig?.crewRole ?? "worker";
100
+ const maxOutput = task.maxOutput
101
+ ?? agentConfig?.maxOutput
102
+ ?? getTruncationForRole(config, role);
103
+
104
+ // Setup artifact paths
105
+ const artifactPaths = config.artifacts.enabled
106
+ ? getArtifactPaths(artifactsDir, runId, task.agent, index)
107
+ : undefined;
108
+
109
+ // Write input artifact
110
+ if (artifactPaths) {
111
+ writeArtifact(artifactPaths.inputPath, `# Task for ${task.agent}\n\n${task.task}`);
112
+ }
113
+
114
+ return new Promise((resolve) => {
115
+ // Build args for pi command
116
+ const args = ["--mode", "json", "--agent", task.agent, "-p", task.task];
117
+ if (agentConfig?.model) args.push("--model", agentConfig.model);
118
+
119
+ // Pass extension so workers can use pi_messenger
120
+ args.push("--extension", EXTENSION_DIR);
121
+
122
+ const proc = spawn("pi", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
123
+
124
+ let jsonlBuffer = "";
125
+ const events: unknown[] = [];
126
+
127
+ proc.stdout?.on("data", (data) => {
128
+ jsonlBuffer += data.toString();
129
+ const lines = jsonlBuffer.split("\n");
130
+ jsonlBuffer = lines.pop() ?? "";
131
+
132
+ for (const line of lines) {
133
+ const event = parseJsonlLine(line);
134
+ if (event) {
135
+ events.push(event);
136
+ updateProgress(progress, event, startTime);
137
+ if (artifactPaths) appendJsonl(artifactPaths.jsonlPath, line);
138
+ }
139
+ }
140
+ });
141
+
142
+ let stderr = "";
143
+ proc.stderr?.on("data", (data) => { stderr += data.toString(); });
144
+
145
+ proc.on("close", (code) => {
146
+ progress.status = code === 0 ? "completed" : "failed";
147
+ progress.durationMs = Date.now() - startTime;
148
+ if (stderr && code !== 0) progress.error = stderr;
149
+
150
+ // Get final output from events
151
+ const fullOutput = getFinalOutput(events as any[]);
152
+ const truncation = truncateOutput(fullOutput, maxOutput, artifactPaths?.outputPath);
153
+
154
+ // Write output artifact (untruncated)
155
+ if (artifactPaths) {
156
+ writeArtifact(artifactPaths.outputPath, fullOutput);
157
+ writeMetadata(artifactPaths.metadataPath, {
158
+ runId,
159
+ agent: task.agent,
160
+ index,
161
+ exitCode: code ?? 1,
162
+ durationMs: progress.durationMs,
163
+ tokens: progress.tokens,
164
+ truncated: truncation.truncated,
165
+ error: progress.error,
166
+ });
167
+ }
168
+
169
+ resolve({
170
+ agent: task.agent,
171
+ exitCode: code ?? 1,
172
+ output: truncation.text,
173
+ truncated: truncation.truncated,
174
+ progress,
175
+ config: agentConfig,
176
+ error: progress.error,
177
+ artifactPaths: artifactPaths ? {
178
+ input: artifactPaths.inputPath,
179
+ output: artifactPaths.outputPath,
180
+ jsonl: artifactPaths.jsonlPath,
181
+ metadata: artifactPaths.metadataPath,
182
+ } : undefined,
183
+ });
184
+ });
185
+
186
+ // Handle abort signal
187
+ if (options.signal) {
188
+ const kill = () => {
189
+ proc.kill("SIGTERM");
190
+ setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
191
+ };
192
+ if (options.signal.aborted) kill();
193
+ else options.signal.addEventListener("abort", kill, { once: true });
194
+ }
195
+ });
196
+ }
197
+
198
+ // Alias for semantic clarity
199
+ export const spawnWorkers = spawnAgents;
200
+ export type WorkerResult = AgentResult;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Crew - Interview Handler
3
+ *
4
+ * Generates interview questions for requirement clarification.
5
+ * Works with current plan's PRD.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
11
+ import type { MessengerState, Dirs } from "../../lib.js";
12
+ import type { CrewParams } from "../types.js";
13
+ import { result } from "../utils/result.js";
14
+ import { spawnAgents } from "../agents.js";
15
+ import { discoverCrewAgents } from "../utils/discover.js";
16
+ import * as store from "../store.js";
17
+ import { getCrewDir } from "../store.js";
18
+
19
+ export async function execute(
20
+ params: CrewParams,
21
+ _state: MessengerState,
22
+ _dirs: Dirs,
23
+ ctx: ExtensionContext
24
+ ) {
25
+ const cwd = ctx.cwd ?? process.cwd();
26
+ const { target } = params;
27
+
28
+ // Check for interview-generator agent
29
+ const availableAgents = discoverCrewAgents(cwd);
30
+ const hasGenerator = availableAgents.some(a => a.name === "crew-interview-generator");
31
+ if (!hasGenerator) {
32
+ return result("Error: crew-interview-generator agent not found.", {
33
+ mode: "interview",
34
+ error: "no_generator"
35
+ });
36
+ }
37
+
38
+ // Determine feature description from plan or target
39
+ let featureDescription: string;
40
+ const plan = store.getPlan(cwd);
41
+
42
+ if (target) {
43
+ // Use target as feature description
44
+ featureDescription = target;
45
+ } else if (plan) {
46
+ // Use plan's PRD
47
+ const prdPath = path.isAbsolute(plan.prd) ? plan.prd : path.join(cwd, plan.prd);
48
+ if (fs.existsSync(prdPath)) {
49
+ featureDescription = fs.readFileSync(prdPath, "utf-8");
50
+ } else {
51
+ const planSpec = store.getPlanSpec(cwd);
52
+ featureDescription = planSpec ?? `Plan: ${plan.prd}`;
53
+ }
54
+ } else {
55
+ return result("Error: No plan found. Create one first with pi_messenger({ action: \"plan\" }) or provide a target.", {
56
+ mode: "interview",
57
+ error: "no_plan"
58
+ });
59
+ }
60
+
61
+ // Spawn interview generator
62
+ const [genResult] = await spawnAgents([{
63
+ agent: "crew-interview-generator",
64
+ task: `Generate interview questions to clarify requirements for this feature:
65
+
66
+ ${featureDescription}
67
+
68
+ Follow your output format exactly for question parsing.`
69
+ }], 1, cwd);
70
+
71
+ if (genResult.exitCode !== 0) {
72
+ return result(`Error: Interview generator failed: ${genResult.error ?? "Unknown error"}`, {
73
+ mode: "interview",
74
+ error: "generator_failed"
75
+ });
76
+ }
77
+
78
+ // Parse questions from output
79
+ const questions = parseInterviewQuestions(genResult.output);
80
+
81
+ if (questions.length === 0) {
82
+ return result("No interview questions could be parsed from generator output.", {
83
+ mode: "interview",
84
+ error: "no_questions",
85
+ rawOutput: genResult.output.slice(0, 500)
86
+ });
87
+ }
88
+
89
+ // Write questions to JSON file for pi's interview tool
90
+ const crewDir = getCrewDir(cwd);
91
+ const questionsPath = path.join(crewDir, "interview-questions.json");
92
+
93
+ const questionsJson = {
94
+ title: `Interview: ${plan?.prd ?? "Feature Clarification"}`,
95
+ questions: questions.map((q, i) => ({
96
+ id: `q${i + 1}`,
97
+ type: q.type,
98
+ question: q.question,
99
+ options: q.options,
100
+ })),
101
+ };
102
+
103
+ fs.mkdirSync(crewDir, { recursive: true });
104
+ fs.writeFileSync(questionsPath, JSON.stringify(questionsJson, null, 2));
105
+
106
+ // Build question preview
107
+ const preview = questions.slice(0, 5).map((q, i) => {
108
+ const typeIcon = q.type === "single" ? "○" : q.type === "multi" ? "☐" : "✎";
109
+ const optionsText = q.options ? ` (${q.options.length} options)` : "";
110
+ return `${i + 1}. ${typeIcon} ${q.question.slice(0, 60)}${q.question.length > 60 ? "..." : ""}${optionsText}`;
111
+ }).join("\n");
112
+
113
+ const moreText = questions.length > 5
114
+ ? `\n... and ${questions.length - 5} more questions`
115
+ : "";
116
+
117
+ const text = `# Interview Generated
118
+
119
+ **Questions:** ${questions.length}
120
+ **File:** ${questionsPath}
121
+ ${plan ? `**PRD:** ${plan.prd}` : ""}
122
+
123
+ ## Preview
124
+
125
+ ${preview}${moreText}
126
+
127
+ ## Next Steps
128
+
129
+ Run the interview using pi's interview tool:
130
+ \`\`\`typescript
131
+ interview({ questions: "${questionsPath}" })
132
+ \`\`\`
133
+
134
+ After completing the interview, use the responses to refine task specs or update the plan.`;
135
+
136
+ return result(text, {
137
+ mode: "interview",
138
+ prd: plan?.prd,
139
+ questionCount: questions.length,
140
+ questionsPath,
141
+ questionTypes: {
142
+ single: questions.filter(q => q.type === "single").length,
143
+ multi: questions.filter(q => q.type === "multi").length,
144
+ text: questions.filter(q => q.type === "text").length,
145
+ }
146
+ });
147
+ }
148
+
149
+ // =============================================================================
150
+ // Question Parsing
151
+ // =============================================================================
152
+
153
+ interface InterviewQuestion {
154
+ type: "single" | "multi" | "text";
155
+ question: string;
156
+ options?: string[];
157
+ }
158
+
159
+ /**
160
+ * Parses interview questions from the generator output.
161
+ *
162
+ * Expected format:
163
+ * ### Q1 (single)
164
+ * Question text?
165
+ * - Option 1
166
+ * - Option 2
167
+ *
168
+ * ### Q2 (text)
169
+ * Question text?
170
+ */
171
+ function parseInterviewQuestions(output: string): InterviewQuestion[] {
172
+ const questions: InterviewQuestion[] = [];
173
+
174
+ // Match question blocks
175
+ const questionRegex = /###\s*Q\d+\s*\((\w+)\)\s*\n([\s\S]*?)(?=###\s*Q\d+|$)/gi;
176
+ let match;
177
+
178
+ while ((match = questionRegex.exec(output)) !== null) {
179
+ const typeRaw = match[1].toLowerCase();
180
+ const body = match[2].trim();
181
+
182
+ // Normalize type
183
+ let type: "single" | "multi" | "text";
184
+ if (typeRaw === "single" || typeRaw === "radio") {
185
+ type = "single";
186
+ } else if (typeRaw === "multi" || typeRaw === "multiple" || typeRaw === "checkbox") {
187
+ type = "multi";
188
+ } else {
189
+ type = "text";
190
+ }
191
+
192
+ // First line is the question
193
+ const lines = body.split("\n").map(l => l.trim()).filter(Boolean);
194
+ const question = lines[0];
195
+
196
+ // Remaining lines starting with - are options
197
+ const options = lines
198
+ .slice(1)
199
+ .filter(l => l.startsWith("-") || l.startsWith("*"))
200
+ .map(l => l.replace(/^[-*]\s*/, "").trim())
201
+ .filter(Boolean);
202
+
203
+ questions.push({
204
+ type,
205
+ question,
206
+ options: options.length > 0 ? options : undefined,
207
+ });
208
+ }
209
+
210
+ return questions;
211
+ }