Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .pi/agents/developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: developer
description: Implements assigned tasks.
model: openrouter/stepfun/step-3.5-flash:free
tools: read,edit,write,bash,grep,find,ls
tools: read,edit,write,bash,grep,find,ls,report_task_result
---

You are a developer. Implement the task assigned to you, and only it.
Expand All @@ -12,7 +12,8 @@ When done, call the `report_task_result` tool with:
- status: "done"
- summary: Brief summary of what was implemented
- filesChanged: Array of file paths that were created or modified
- notes: Any additional notes (optional)
- evidence: Array of concrete command, test, inspection, or manual evidence
- issues: Array of `{severity, description, reproduction?}` objects

Example:

Expand All @@ -21,6 +22,7 @@ report_task_result({
status: "done",
summary: "Created config module with validation",
filesChanged: ["app/config.py"],
notes: "Uses pydantic for validation"
evidence: [{kind: "test", description: "The config tests pass", outcome: "pass"}],
issues: []
})
```
10 changes: 7 additions & 3 deletions .pi/agents/pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
name: pm
description: Project manager who plans and delegates tasks in waves.
model: openrouter/stepfun/step-3.5-flash:free
tools: read,grep,find,ls
tools: read,grep,find,ls,generate_wave
---

You are a technical PM. Your goal is to implement user's project.
Split the project into waves, each wave consisting of atomic technical tasks.
All tasks within one wave will be completed at the same time in parallel.
Only generate one wave at a time. Once the wave is finished, you will have an opportunity to create the next one.
Each task should be testable and should have clear verification requirements.
Each task should be testable and should have clear, nonempty verification requirements.
Consider integration and tests too.

When ready to spawn a wave, call the `generate_wave` tool with:

- wave: { goal: "...", tasks: [{id, title, description, requirements, assignee}, ...] }
- wave: { goal: "...", tasks: [{id, title, description, requirements, assignee: "developer"}, ...] }
- done: true (if project complete) or false

"tasks" should include:
Expand All @@ -25,6 +25,10 @@ When ready to spawn a wave, call the `generate_wave` tool with:
- requirements: Verification requirements for the verifier (developer won't see this)
- assignee: "developer" (only assign to developers, verifiers auto-assign)

Task IDs must be unique safe identifiers (letters, numbers, `_`, or `-`, up to 64 characters).
Call `generate_wave` at most once per turn. Do not report completion with a wave, and do not
report a wave without calling the tool.

Example for new wave:

```
Expand Down
18 changes: 13 additions & 5 deletions .pi/agents/verifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@
name: verifier
description: Reviews developer work and validates requirements.
model: openrouter/stepfun/step-3.5-flash:free
tools: read,grep,find,ls,bash
tools: read,grep,find,ls,bash,report_task_result
---

You are a verifier. Your job is only to review and QA the task assigned to you.
You are a verifier. Your job is only to review and QA the task assigned to you. You are
structurally read-only: do not edit or write files. Verify every requirement and use adversarial
checks where appropriate.

When done, call the `report_task_result` tool with:

- status: "pass" or "fail"
- issues: Array of issue descriptions (empty array if pass)
- status: "pass", "fail", or "partial"
- summary: Concise verification conclusion
- evidence: Concrete evidence; `pass` requires at least one item with outcome `pass`
- issues: Array of `{severity, description, reproduction?}` objects

Example for passing:

```
report_task_result({
status: "pass",
summary: "All requirements are satisfied.",
evidence: [{kind: "test", description: "The verification suite passes", outcome: "pass"}],
issues: []
})
```
Expand All @@ -26,6 +32,8 @@ Example for failing:
```
report_task_result({
status: "fail",
issues: ["File missing: app/config.py", "Validation not implemented"]
summary: "The implementation is incomplete.",
evidence: [{kind: "inspection", description: "The required file is missing", outcome: "fail"}],
issues: [{severity: "blocking", description: "File missing: app/config.py"}]
})
```
24 changes: 23 additions & 1 deletion .pi/extensions/workflow-orchestrator/agents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";

function getAgentDir(): string {
return process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
}

function parseFrontmatter<T extends Record<string, unknown>>(
content: string,
): { frontmatter: T; body: string } {
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
if (!normalized.startsWith("---")) return { frontmatter: {} as T, body: normalized };
const end = normalized.indexOf("\n---", 3);
if (end < 0) return { frontmatter: {} as T, body: normalized };
const frontmatter: Record<string, unknown> = {};
for (const line of normalized.slice(4, end).split("\n")) {
const separator = line.indexOf(":");
if (separator < 0) continue;
const key = line.slice(0, separator).trim();
const value = line.slice(separator + 1).trim();
if (key) frontmatter[key] = value;
}
return { frontmatter: frontmatter as T, body: normalized.slice(end + 4).trim() };
}

export type AgentSource = "user" | "project";

Expand Down
127 changes: 127 additions & 0 deletions .pi/extensions/workflow-orchestrator/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { DEFAULT_WORKFLOW_NAME } from "./setup.js";
import { normalizeGoal } from "./utils.js";

export const WORKFLOW_COMMANDS = new Set([
"start",
"resume",
"status",
"stop",
"stop-task",
"message",
"expand",
"collapse",
"help",
]);

export interface ParsedWorkflowStart {
workflowName: string;
goal?: string;
model?: string;
}

/** Tokenize command arguments while preserving quoted phrases as one token. */
export function tokenizeWorkflowArgs(args: string): string[] {
const tokens: string[] = [];
let token = "";
let quote: '"' | "'" | undefined;
let escaped = false;

const pushToken = () => {
if (token) tokens.push(token);
token = "";
};

for (const char of args) {
if (escaped) {
token += char;
escaped = false;
continue;
}

if (char === "\\" && quote !== "'") {
escaped = true;
continue;
}

if (quote) {
if (char === quote) quote = undefined;
else token += char;
continue;
}

if (char === '"' || (char === "'" && token.length === 0)) {
quote = char;
continue;
}

if (/\s/.test(char)) pushToken();
else token += char;
}

if (escaped) token += "\\";
pushToken();
return tokens;
}

export function isValidWorkflowName(name: string): boolean {
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 50;
}

export function extractModelFlag(tokens: string[]): { tokens: string[]; model?: string } {
const stripped: string[] = [];
let model: string | undefined;

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token === "--model" && i + 1 < tokens.length) {
model = normalizeGoal(tokens[++i]);
continue;
}
if (token === "--model") continue;
stripped.push(token);
}

return { tokens: stripped, model };
}

export function parseWorkflowStartArgs(tokens: string[]): ParsedWorkflowStart | null {
const { tokens: rest, model } = extractModelFlag(tokens.slice(1));
if (rest.length === 0) return null;

if (rest.length === 1) {
const token = rest[0];
if (isValidWorkflowName(token)) {
return { workflowName: token, goal: undefined, model };
}
return {
workflowName: DEFAULT_WORKFLOW_NAME,
goal: normalizeGoal(token),
model,
};
}

if (!isValidWorkflowName(rest[0])) {
return {
workflowName: DEFAULT_WORKFLOW_NAME,
goal: normalizeGoal(rest.join(" ")),
model,
};
}

return {
workflowName: rest[0],
goal: normalizeGoal(rest.slice(1).join(" ")),
model,
};
}

export function parseWorkflowShorthandGoal(tokens: string[]): {
goal?: string;
model?: string;
} {
const { tokens: stripped, model } = extractModelFlag(tokens);
return {
goal: normalizeGoal(stripped.join(" ")),
model,
};
}
Loading
Loading