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.
- package/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- 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
|
+
}
|