mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add provider/model selection for schedules and workspace fetch debug logging
- Unify resolveProviderAndModel across CLI and server, supporting provider/model syntax (e.g. codex/gpt-5.4) for agent runs and schedules - Add --provider flag to schedule create CLI and both MCP schedule tools - Add structured debug logging to workspace fetch hydrate, sidebar refresh, and real-time update paths - Update orchestrate skill references
This commit is contained in:
@@ -53,6 +53,7 @@ import { buildDraftStoreKey } from "@/stores/draft-keys";
|
||||
import type { AttachmentMetadata } from "@/attachments/types";
|
||||
import { reconcilePreviousAgentStatuses } from "@/contexts/session-status-tracking";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { summarizeWorkspaceCollection } from "@/utils/workspace-fetch-debug";
|
||||
|
||||
// Re-export types from session-store and draft-store for backward compatibility
|
||||
export type { DraftInput } from "@/stores/draft-store";
|
||||
@@ -333,11 +334,24 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
let includeSubscribe = options?.subscribe ?? false;
|
||||
|
||||
while (true) {
|
||||
console.log("[WorkspaceFetch][hydrate] request", {
|
||||
serverId,
|
||||
cursor,
|
||||
includeSubscribe,
|
||||
existingWorkspaces: summarizeWorkspaceCollection(existingWorkspaces?.values()),
|
||||
});
|
||||
const payload = await client.fetchWorkspaces({
|
||||
sort: [{ key: "activity_at", direction: "desc" }],
|
||||
...(includeSubscribe ? { subscribe: {} } : {}),
|
||||
page: cursor ? { limit: 200, cursor } : { limit: 200 },
|
||||
});
|
||||
console.log("[WorkspaceFetch][hydrate] response", {
|
||||
serverId,
|
||||
cursor,
|
||||
includeSubscribe,
|
||||
pageInfo: payload.pageInfo,
|
||||
payload: summarizeWorkspaceCollection(payload.entries),
|
||||
});
|
||||
if (options?.isCancelled?.()) {
|
||||
return;
|
||||
}
|
||||
@@ -366,6 +380,11 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
|
||||
setWorkspaces(serverId, workspaces);
|
||||
setHasHydratedWorkspaces(serverId, true);
|
||||
console.log("[WorkspaceFetch][hydrate] applied", {
|
||||
serverId,
|
||||
subscribed: options?.subscribe ?? false,
|
||||
nextWorkspaces: summarizeWorkspaceCollection(workspaces.values()),
|
||||
});
|
||||
},
|
||||
[client, isConnected, serverId, setHasHydratedWorkspaces, setWorkspaces],
|
||||
);
|
||||
@@ -1153,10 +1172,23 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
const unsubWorkspaceUpdate = client.on("workspace_update", (message) => {
|
||||
if (message.type !== "workspace_update") return;
|
||||
if (message.payload.kind === "remove") {
|
||||
console.log("[WorkspaceFetch][update] remove", {
|
||||
serverId,
|
||||
workspaceId: message.payload.id,
|
||||
});
|
||||
removeWorkspace(serverId, message.payload.id);
|
||||
return;
|
||||
}
|
||||
mergeWorkspaces(serverId, [normalizeWorkspaceDescriptor(message.payload.workspace)]);
|
||||
const workspace = normalizeWorkspaceDescriptor(message.payload.workspace);
|
||||
const existingWorkspace = useSessionStore
|
||||
.getState()
|
||||
.sessions[serverId]?.workspaces.get(workspace.id);
|
||||
console.log("[WorkspaceFetch][update] upsert", {
|
||||
serverId,
|
||||
existedInStore: Boolean(existingWorkspace),
|
||||
workspace: summarizeWorkspaceCollection([workspace]),
|
||||
});
|
||||
mergeWorkspaces(serverId, [workspace]);
|
||||
});
|
||||
|
||||
const unsubStatus = client.on("status", (message) => {
|
||||
|
||||
@@ -9,6 +9,10 @@ import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import type { WorkspaceDescriptorPayload } from "@server/shared/messages";
|
||||
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
||||
import {
|
||||
summarizeSidebarProjects,
|
||||
summarizeWorkspaceCollection,
|
||||
} from "@/utils/workspace-fetch-debug";
|
||||
|
||||
const EMPTY_ORDER: string[] = [];
|
||||
const EMPTY_PROJECTS: SidebarProjectEntry[] = [];
|
||||
@@ -284,6 +288,20 @@ export function useSidebarWorkspacesList(options?: {
|
||||
});
|
||||
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[WorkspaceFetch][sidebar] model", {
|
||||
serverId,
|
||||
connectionStatus,
|
||||
hasHydratedWorkspaces,
|
||||
sessionWorkspaces: summarizeWorkspaceCollection(sessionWorkspaces?.values()),
|
||||
projects: summarizeSidebarProjects(projects),
|
||||
});
|
||||
}, [connectionStatus, hasHydratedWorkspaces, projects, serverId, sessionWorkspaces]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId || projects.length === 0) {
|
||||
return;
|
||||
@@ -327,10 +345,21 @@ export function useSidebarWorkspacesList(options?: {
|
||||
let cursor: string | null = null;
|
||||
try {
|
||||
while (true) {
|
||||
console.log("[WorkspaceFetch][sidebar-refresh] request", {
|
||||
serverId,
|
||||
cursor,
|
||||
existingWorkspaces: summarizeWorkspaceCollection(existingWorkspaces?.values()),
|
||||
});
|
||||
const payload = await client.fetchWorkspaces({
|
||||
sort: [{ key: "activity_at", direction: "desc" }],
|
||||
page: cursor ? { limit: 200, cursor } : { limit: 200 },
|
||||
});
|
||||
console.log("[WorkspaceFetch][sidebar-refresh] response", {
|
||||
serverId,
|
||||
cursor,
|
||||
pageInfo: payload.pageInfo,
|
||||
payload: summarizeWorkspaceCollection(payload.entries),
|
||||
});
|
||||
for (const entry of payload.entries) {
|
||||
const workspace = toWorkspaceDescriptor(entry);
|
||||
next.set(
|
||||
@@ -349,7 +378,16 @@ export function useSidebarWorkspacesList(options?: {
|
||||
const store = useSessionStore.getState();
|
||||
store.setWorkspaces(serverId, next);
|
||||
store.setHasHydratedWorkspaces(serverId, true);
|
||||
} catch {
|
||||
console.log("[WorkspaceFetch][sidebar-refresh] applied", {
|
||||
serverId,
|
||||
nextWorkspaces: summarizeWorkspaceCollection(next.values()),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[WorkspaceFetch][sidebar-refresh] failed", {
|
||||
serverId,
|
||||
cursor,
|
||||
error,
|
||||
});
|
||||
// ignore explicit refresh failures; hook keeps existing data
|
||||
}
|
||||
})();
|
||||
|
||||
104
packages/app/src/utils/workspace-fetch-debug.ts
Normal file
104
packages/app/src/utils/workspace-fetch-debug.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
type WorkspaceLike = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectDisplayName?: string;
|
||||
name: string;
|
||||
status: string;
|
||||
workspaceKind: string;
|
||||
};
|
||||
|
||||
type SidebarWorkspaceLike = {
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
statusBucket: string;
|
||||
workspaceKind: string;
|
||||
};
|
||||
|
||||
type SidebarProjectLike = {
|
||||
projectKey: string;
|
||||
projectName: string;
|
||||
statusBucket: string;
|
||||
activeCount: number;
|
||||
totalWorkspaces: number;
|
||||
workspaces: SidebarWorkspaceLike[];
|
||||
};
|
||||
|
||||
function countByValue(values: Iterable<string>): Record<string, number> {
|
||||
const counts = new Map<string, number>();
|
||||
for (const value of values) {
|
||||
counts.set(value, (counts.get(value) ?? 0) + 1);
|
||||
}
|
||||
return Object.fromEntries(counts);
|
||||
}
|
||||
|
||||
export function summarizeWorkspaceCollection(
|
||||
workspaces: Iterable<WorkspaceLike> | null | undefined,
|
||||
): {
|
||||
count: number;
|
||||
projectIds: string[];
|
||||
statusCounts: Record<string, number>;
|
||||
workspaces: Array<{
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string | null;
|
||||
name: string;
|
||||
status: string;
|
||||
workspaceKind: string;
|
||||
}>;
|
||||
} {
|
||||
const entries = Array.from(workspaces ?? [], (workspace) => ({
|
||||
id: workspace.id,
|
||||
projectId: workspace.projectId,
|
||||
projectDisplayName: workspace.projectDisplayName?.trim() || null,
|
||||
name: workspace.name,
|
||||
status: workspace.status,
|
||||
workspaceKind: workspace.workspaceKind,
|
||||
}));
|
||||
|
||||
return {
|
||||
count: entries.length,
|
||||
projectIds: [...new Set(entries.map((workspace) => workspace.projectId))],
|
||||
statusCounts: countByValue(entries.map((workspace) => workspace.status)),
|
||||
workspaces: entries,
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeSidebarProjects(
|
||||
projects: Iterable<SidebarProjectLike> | null | undefined,
|
||||
): {
|
||||
count: number;
|
||||
projectKeys: string[];
|
||||
projects: Array<{
|
||||
projectKey: string;
|
||||
projectName: string;
|
||||
statusBucket: string;
|
||||
activeCount: number;
|
||||
totalWorkspaces: number;
|
||||
workspaces: Array<{
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
statusBucket: string;
|
||||
workspaceKind: string;
|
||||
}>;
|
||||
}>;
|
||||
} {
|
||||
const entries = Array.from(projects ?? [], (project) => ({
|
||||
projectKey: project.projectKey,
|
||||
projectName: project.projectName,
|
||||
statusBucket: project.statusBucket,
|
||||
activeCount: project.activeCount,
|
||||
totalWorkspaces: project.totalWorkspaces,
|
||||
workspaces: project.workspaces.map((workspace) => ({
|
||||
workspaceId: workspace.workspaceId,
|
||||
name: workspace.name,
|
||||
statusBucket: workspace.statusBucket,
|
||||
workspaceKind: workspace.workspaceKind,
|
||||
})),
|
||||
}));
|
||||
|
||||
return {
|
||||
count: entries.length,
|
||||
projectKeys: entries.map((project) => project.projectKey),
|
||||
projects: entries,
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { resolve } from "node:path";
|
||||
import { lookup } from "mime-types";
|
||||
import { parseDuration } from "../../utils/duration.js";
|
||||
import { collectMultiple } from "../../utils/command-options.js";
|
||||
import { resolveProviderAndModel } from "../../utils/provider-model.js";
|
||||
|
||||
export function addRunOptions(cmd: Command): Command {
|
||||
return cmd
|
||||
@@ -98,11 +99,6 @@ export interface AgentRunOptions extends CommandOptions {
|
||||
outputSchema?: string;
|
||||
}
|
||||
|
||||
interface ResolvedProviderModel {
|
||||
provider: string;
|
||||
model: string | undefined;
|
||||
}
|
||||
|
||||
function toRunResult(
|
||||
agent: AgentSnapshotPayload,
|
||||
statusOverride?: AgentRunResult["status"],
|
||||
@@ -222,54 +218,6 @@ function structuredRunSchema(output: Record<string, unknown>): OutputSchema<Agen
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveProviderAndModel(
|
||||
options: Pick<AgentRunOptions, "provider" | "model">,
|
||||
): ResolvedProviderModel {
|
||||
const providerInput = options.provider?.trim() || "claude";
|
||||
const modelInput = options.model?.trim();
|
||||
|
||||
if (options.model !== undefined && !modelInput) {
|
||||
const error: CommandError = {
|
||||
code: "INVALID_MODEL",
|
||||
message: "--model cannot be empty",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
const slashIndex = providerInput.indexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return {
|
||||
provider: providerInput,
|
||||
model: modelInput,
|
||||
};
|
||||
}
|
||||
|
||||
const provider = providerInput.slice(0, slashIndex).trim();
|
||||
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
|
||||
if (!provider || !modelFromProvider) {
|
||||
const error: CommandError = {
|
||||
code: "INVALID_PROVIDER",
|
||||
message: "Invalid --provider value",
|
||||
details: "Use --provider <provider> or --provider <provider>/<model>",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (modelInput && modelInput !== modelFromProvider) {
|
||||
const error: CommandError = {
|
||||
code: "CONFLICTING_MODEL_OPTIONS",
|
||||
message: "Conflicting model values provided",
|
||||
details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
model: modelInput ?? modelFromProvider,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runRunCommand(
|
||||
prompt: string,
|
||||
options: AgentRunOptions,
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface ScheduleCreateOptions extends ScheduleCommandOptions {
|
||||
cron?: string;
|
||||
name?: string;
|
||||
target?: string;
|
||||
provider?: string;
|
||||
maxRuns?: string;
|
||||
expiresIn?: string;
|
||||
}
|
||||
@@ -30,6 +31,7 @@ export async function runCreateCommand(
|
||||
cron: options.cron,
|
||||
name: options.name,
|
||||
target: options.target,
|
||||
provider: options.provider,
|
||||
maxRuns: options.maxRuns,
|
||||
expiresIn: options.expiresIn,
|
||||
});
|
||||
|
||||
@@ -21,6 +21,10 @@ export function createScheduleCommand(): Command {
|
||||
.option("--cron <expr>", "Cron cadence expression")
|
||||
.option("--name <name>", "Optional schedule name")
|
||||
.option("--target <self|new-agent|agent-id>", "Run target")
|
||||
.option(
|
||||
"--provider <provider>",
|
||||
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
|
||||
)
|
||||
.option("--max-runs <n>", "Maximum number of runs")
|
||||
.option("--expires-in <duration>", "Time to live for the schedule"),
|
||||
).action(withOutput(runCreateCommand));
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ScheduleTarget,
|
||||
} from "./types.js";
|
||||
import { parseDuration } from "../../utils/duration.js";
|
||||
import { resolveProviderAndModel } from "../../utils/provider-model.js";
|
||||
|
||||
export interface ScheduleCommandOptions extends CommandOptions {
|
||||
host?: string;
|
||||
@@ -58,7 +59,8 @@ export function formatTarget(target: ScheduleTarget | ScheduleListItem["target"]
|
||||
if (target.type === "agent") {
|
||||
return `agent:${target.agentId.slice(0, 7)}`;
|
||||
}
|
||||
return `new-agent:${target.config.provider}`;
|
||||
const modelSuffix = target.config.model ? `/${target.config.model}` : "";
|
||||
return `new-agent:${target.config.provider}${modelSuffix}`;
|
||||
}
|
||||
|
||||
export function formatDurationMs(durationMs: number): string {
|
||||
@@ -87,6 +89,7 @@ export function parseScheduleCreateInput(options: {
|
||||
cron?: string;
|
||||
name?: string;
|
||||
target?: string;
|
||||
provider?: string;
|
||||
maxRuns?: string;
|
||||
expiresIn?: string;
|
||||
}): CreateScheduleInput {
|
||||
@@ -111,29 +114,53 @@ export function parseScheduleCreateInput(options: {
|
||||
: { type: "cron", expression: options.cron!.trim() };
|
||||
|
||||
const targetValue = options.target?.trim();
|
||||
const hasExplicitProviderSelection = options.provider !== undefined;
|
||||
const resolvedProviderModel = resolveProviderAndModel({
|
||||
provider: options.provider,
|
||||
defaultProvider: "claude",
|
||||
});
|
||||
const newAgentTarget: ScheduleTarget = {
|
||||
type: "new-agent",
|
||||
config: {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: process.cwd(),
|
||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||
},
|
||||
};
|
||||
let target: ScheduleTarget;
|
||||
if (!targetValue || targetValue === "self") {
|
||||
if (!targetValue) {
|
||||
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
|
||||
if (currentAgentId) {
|
||||
if (currentAgentId && !hasExplicitProviderSelection) {
|
||||
target = { type: "self", agentId: currentAgentId };
|
||||
} else {
|
||||
target = {
|
||||
type: "new-agent",
|
||||
config: {
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
};
|
||||
target = newAgentTarget;
|
||||
}
|
||||
} else if (targetValue === "self") {
|
||||
if (hasExplicitProviderSelection) {
|
||||
throw {
|
||||
code: "INVALID_TARGET",
|
||||
message: "--provider can only be used with a new-agent target",
|
||||
details: "Use --target new-agent or omit --target to create a new agent schedule",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
|
||||
if (!currentAgentId) {
|
||||
throw {
|
||||
code: "INVALID_TARGET",
|
||||
message: "--target self requires running inside a Paseo agent",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
target = { type: "self", agentId: currentAgentId };
|
||||
} else if (targetValue === "new-agent") {
|
||||
target = {
|
||||
type: "new-agent",
|
||||
config: {
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
};
|
||||
target = newAgentTarget;
|
||||
} else {
|
||||
if (hasExplicitProviderSelection) {
|
||||
throw {
|
||||
code: "INVALID_TARGET",
|
||||
message: "--provider can only be used with a new-agent target",
|
||||
details: "Use --target new-agent or omit --target to create a new agent schedule",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
target = {
|
||||
type: "agent",
|
||||
agentId: targetValue,
|
||||
|
||||
60
packages/cli/src/utils/provider-model.ts
Normal file
60
packages/cli/src/utils/provider-model.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { CommandError } from "../output/index.js";
|
||||
|
||||
export interface ResolveProviderAndModelOptions {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
defaultProvider?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedProviderModel {
|
||||
provider: string;
|
||||
model: string | undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderAndModel(
|
||||
options: ResolveProviderAndModelOptions,
|
||||
): ResolvedProviderModel {
|
||||
const providerInput = options.provider?.trim() || options.defaultProvider || "claude";
|
||||
const modelInput = options.model?.trim();
|
||||
|
||||
if (options.model !== undefined && !modelInput) {
|
||||
const error: CommandError = {
|
||||
code: "INVALID_MODEL",
|
||||
message: "--model cannot be empty",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
const slashIndex = providerInput.indexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return {
|
||||
provider: providerInput,
|
||||
model: modelInput,
|
||||
};
|
||||
}
|
||||
|
||||
const provider = providerInput.slice(0, slashIndex).trim();
|
||||
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
|
||||
if (!provider || !modelFromProvider) {
|
||||
const error: CommandError = {
|
||||
code: "INVALID_PROVIDER",
|
||||
message: "Invalid --provider value",
|
||||
details: "Use --provider <provider> or --provider <provider>/<model>",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (modelInput && modelInput !== modelFromProvider) {
|
||||
const error: CommandError = {
|
||||
code: "CONFLICTING_MODEL_OPTIONS",
|
||||
message: "Conflicting model values provided",
|
||||
details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
model: modelInput ?? modelFromProvider,
|
||||
};
|
||||
}
|
||||
@@ -23,6 +23,11 @@ try {
|
||||
const createdJson = JSON.parse(created.stdout);
|
||||
assert.strictEqual(createdJson.name, "review-prs");
|
||||
assert.strictEqual(createdJson.cadence, "every:5m");
|
||||
assert(
|
||||
typeof createdJson.target === "string" &&
|
||||
(createdJson.target.startsWith("agent:") || createdJson.target === "new-agent:claude"),
|
||||
created.stdout,
|
||||
);
|
||||
|
||||
const listed = await ctx.paseo(["schedule", "ls", "--json"]);
|
||||
assert.strictEqual(listed.exitCode, 0, listed.stderr);
|
||||
@@ -53,6 +58,61 @@ try {
|
||||
console.log("schedule commands work\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 1b: schedule create accepts provider/model syntax for new-agent runs");
|
||||
const created = await ctx.paseo(
|
||||
[
|
||||
"schedule",
|
||||
"create",
|
||||
"Refactor the API layer",
|
||||
"--every",
|
||||
"10m",
|
||||
"--provider",
|
||||
"codex/gpt-5.4",
|
||||
"--json",
|
||||
],
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
assert.strictEqual(created.exitCode, 0, created.stderr);
|
||||
const createdJson = JSON.parse(created.stdout);
|
||||
assert.strictEqual(createdJson.target, "new-agent:codex/gpt-5.4");
|
||||
|
||||
const inspected = await ctx.paseo(["schedule", "inspect", createdJson.id, "--json"]);
|
||||
assert.strictEqual(inspected.exitCode, 0, inspected.stderr);
|
||||
const inspectedJson = JSON.parse(inspected.stdout);
|
||||
assert.strictEqual(inspectedJson.target.config.provider, "codex");
|
||||
assert.strictEqual(inspectedJson.target.config.model, "gpt-5.4");
|
||||
|
||||
const deleted = await ctx.paseo(["schedule", "delete", createdJson.id, "--json"]);
|
||||
assert.strictEqual(deleted.exitCode, 0, deleted.stderr);
|
||||
console.log("schedule provider/model syntax works\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 1c: schedule create rejects provider with self target");
|
||||
const result = await ctx.paseo(
|
||||
[
|
||||
"schedule",
|
||||
"create",
|
||||
"Conflicting schedule",
|
||||
"--every",
|
||||
"5m",
|
||||
"--target",
|
||||
"self",
|
||||
"--provider",
|
||||
"codex/gpt-5.4",
|
||||
],
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
assert.notStrictEqual(result.exitCode, 0, "should fail for self target with provider");
|
||||
const output = result.stdout + result.stderr;
|
||||
assert(
|
||||
output.includes("--provider can only be used with a new-agent target"),
|
||||
"should explain provider target mismatch",
|
||||
);
|
||||
console.log("schedule rejects provider with self target\n");
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Test 2: loop run/ls/inspect/logs/stop work");
|
||||
const run = await ctx.paseo(
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
AgentStatusEnum,
|
||||
ProviderSummarySchema,
|
||||
parseDurationString,
|
||||
resolveProviderAndModel,
|
||||
sanitizePermissionRequest,
|
||||
serializeSnapshotWithMetadata,
|
||||
startAgentRun,
|
||||
@@ -81,13 +82,20 @@ export async function createAgentManagementMcpServer(
|
||||
component: "agent-management-mcp",
|
||||
});
|
||||
const waitTracker = new WaitForAgentTracker(logger);
|
||||
const resolveNewAgentScheduleTarget = (params?: { provider?: AgentProvider; cwd?: string }) => ({
|
||||
type: "new-agent" as const,
|
||||
config: {
|
||||
provider: params?.provider ?? ("claude" as AgentProvider),
|
||||
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
|
||||
},
|
||||
});
|
||||
const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => {
|
||||
const resolvedProviderModel = resolveProviderAndModel({
|
||||
provider: params?.provider,
|
||||
defaultProvider: "claude",
|
||||
});
|
||||
return {
|
||||
type: "new-agent" as const,
|
||||
config: {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
|
||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const server = new McpServer({
|
||||
name: "paseo-agent-management",
|
||||
@@ -656,7 +664,9 @@ export async function createAgentManagementMcpServer(
|
||||
cron: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
target: z.enum(["self", "new-agent"]).optional(),
|
||||
provider: AgentProviderEnum.optional(),
|
||||
provider: AgentProviderEnum.optional().describe(
|
||||
"Provider, or provider/model (for example: codex or codex/gpt-5.4).",
|
||||
),
|
||||
cwd: z.string().optional(),
|
||||
maxRuns: z.number().int().positive().optional(),
|
||||
expiresIn: z.string().optional(),
|
||||
|
||||
@@ -478,6 +478,28 @@ describe("MCP parity end-to-end", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("create_schedule accepts provider/model syntax", async () => {
|
||||
let scheduleId: string | null = null;
|
||||
try {
|
||||
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
||||
prompt: "say hello",
|
||||
every: "5m",
|
||||
name: "Parity provider schedule",
|
||||
provider: "codex/gpt-5.4",
|
||||
});
|
||||
scheduleId = created.id as string;
|
||||
expect(created.target).toMatchObject({
|
||||
type: "new-agent",
|
||||
config: {
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await deleteScheduleIfPresent(scheduleId);
|
||||
}
|
||||
});
|
||||
|
||||
test("inspect_schedule returns details", async () => {
|
||||
let scheduleId: string | null = null;
|
||||
try {
|
||||
@@ -568,6 +590,27 @@ describe("MCP parity end-to-end", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("create_schedule on agent MCP accepts provider/model override for new-agent", async () => {
|
||||
let scheduleId: string | null = null;
|
||||
try {
|
||||
const created = await callToolStructured(agentScopedClient, "create_schedule", {
|
||||
prompt: "say hello",
|
||||
every: "5m",
|
||||
provider: "codex/gpt-5.4",
|
||||
});
|
||||
scheduleId = created.id as string;
|
||||
expect(created.target).toMatchObject({
|
||||
type: "new-agent",
|
||||
config: {
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await deleteScheduleIfPresent(scheduleId);
|
||||
}
|
||||
});
|
||||
|
||||
test("create_schedule target self without callerAgentId throws", async () => {
|
||||
await expectToolError(
|
||||
topLevelClient,
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
AgentStatusEnum,
|
||||
ProviderSummarySchema,
|
||||
parseDurationString,
|
||||
resolveProviderAndModel,
|
||||
sanitizePermissionRequest,
|
||||
setupFinishNotification,
|
||||
serializeSnapshotWithMetadata,
|
||||
@@ -230,16 +231,36 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
return expandUserPath(trimmedCwd);
|
||||
};
|
||||
|
||||
const resolveNewAgentScheduleTarget = () => {
|
||||
const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => {
|
||||
const callerAgent = resolveCallerAgent();
|
||||
if (callerAgent) {
|
||||
const hasProviderOverride = params?.provider !== undefined;
|
||||
const resolvedProviderModel = hasProviderOverride
|
||||
? resolveProviderAndModel({
|
||||
provider: params?.provider,
|
||||
defaultProvider: callerAgent.provider,
|
||||
})
|
||||
: null;
|
||||
const resolvedProvider = resolvedProviderModel?.provider ?? callerAgent.provider;
|
||||
return {
|
||||
type: "new-agent" as const,
|
||||
config: {
|
||||
provider: callerAgent.provider,
|
||||
cwd: callerAgent.cwd,
|
||||
...(callerAgent.currentModeId ? { modeId: callerAgent.currentModeId } : {}),
|
||||
...(callerAgent.config.model ? { model: callerAgent.config.model } : {}),
|
||||
provider: resolvedProvider,
|
||||
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : callerAgent.cwd,
|
||||
...(callerAgent.currentModeId
|
||||
? {
|
||||
modeId: mapModeAcrossProviders(
|
||||
callerAgent.currentModeId,
|
||||
callerAgent.provider,
|
||||
resolvedProvider,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(resolvedProviderModel?.model
|
||||
? { model: resolvedProviderModel.model }
|
||||
: !hasProviderOverride && callerAgent.config.model
|
||||
? { model: callerAgent.config.model }
|
||||
: {}),
|
||||
...(callerAgent.config.thinkingOptionId
|
||||
? { thinkingOptionId: callerAgent.config.thinkingOptionId }
|
||||
: {}),
|
||||
@@ -267,10 +288,17 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
|
||||
return {
|
||||
type: "new-agent" as const,
|
||||
config: {
|
||||
provider: "claude" as AgentProvider,
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
config: (() => {
|
||||
const resolvedProviderModel = resolveProviderAndModel({
|
||||
provider: params?.provider,
|
||||
defaultProvider: "claude",
|
||||
});
|
||||
return {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
|
||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||
};
|
||||
})(),
|
||||
};
|
||||
};
|
||||
const agentToAgentInputSchema = {
|
||||
@@ -1181,12 +1209,16 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
cron: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
target: z.enum(["self", "new-agent"]).optional(),
|
||||
provider: AgentProviderEnum.optional().describe(
|
||||
"Provider, or provider/model (for example: codex or codex/gpt-5.4).",
|
||||
),
|
||||
cwd: z.string().optional(),
|
||||
maxRuns: z.number().int().positive().optional(),
|
||||
expiresIn: z.string().optional(),
|
||||
},
|
||||
outputSchema: ScheduleSummarySchema.shape,
|
||||
},
|
||||
async ({ prompt, every, cron, name, target, maxRuns, expiresIn }) => {
|
||||
async ({ prompt, every, cron, name, target, provider, cwd, maxRuns, expiresIn }) => {
|
||||
if (!scheduleService) {
|
||||
throw new Error("Schedule service is not configured");
|
||||
}
|
||||
@@ -1202,9 +1234,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
if (!callerAgentId) {
|
||||
throw new Error("target=self requires a caller agent");
|
||||
}
|
||||
if (provider !== undefined || cwd !== undefined) {
|
||||
throw new Error("provider and cwd can only be used with target=new-agent");
|
||||
}
|
||||
return { type: "agent" as const, agentId: callerAgentId };
|
||||
})()
|
||||
: resolveNewAgentScheduleTarget();
|
||||
: resolveNewAgentScheduleTarget({ provider, cwd });
|
||||
|
||||
const schedule = await scheduleService.create({
|
||||
prompt: prompt.trim(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { curateAgentActivity } from "./activity-curator.js";
|
||||
import type { AgentStorage } from "./agent-storage.js";
|
||||
import { serializeAgentSnapshot } from "../messages.js";
|
||||
import { StoredScheduleSchema } from "../schedule/types.js";
|
||||
import type { AgentProvider } from "./agent-sdk-types.js";
|
||||
|
||||
export const AgentProviderEnum = z.string();
|
||||
|
||||
@@ -48,6 +49,49 @@ export const AgentModelSchema = z.object({
|
||||
// 30 seconds - surface friendly message before SDK tool timeout (~60s)
|
||||
export const AGENT_WAIT_TIMEOUT_MS = 30000;
|
||||
|
||||
export interface ResolvedProviderModel {
|
||||
provider: AgentProvider;
|
||||
model: string | undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderAndModel(params: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
defaultProvider: AgentProvider;
|
||||
}): ResolvedProviderModel {
|
||||
const providerInput = params.provider?.trim() || params.defaultProvider;
|
||||
const modelInput = params.model?.trim();
|
||||
|
||||
if (params.model !== undefined && !modelInput) {
|
||||
throw new Error("model cannot be empty");
|
||||
}
|
||||
|
||||
const slashIndex = providerInput.indexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return {
|
||||
provider: providerInput as AgentProvider,
|
||||
model: modelInput,
|
||||
};
|
||||
}
|
||||
|
||||
const provider = providerInput.slice(0, slashIndex).trim();
|
||||
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
|
||||
if (!provider || !modelFromProvider) {
|
||||
throw new Error("provider must be <provider> or <provider>/<model>");
|
||||
}
|
||||
|
||||
if (modelInput && modelInput !== modelFromProvider) {
|
||||
throw new Error(
|
||||
`Conflicting model values provided: provider specifies ${modelFromProvider}, but model specifies ${modelInput}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
provider: provider as AgentProvider,
|
||||
model: modelInput ?? modelFromProvider,
|
||||
};
|
||||
}
|
||||
|
||||
export type StartAgentRunOptions = {
|
||||
replaceRunning?: boolean;
|
||||
};
|
||||
|
||||
@@ -335,6 +335,42 @@ type FetchWorkspacesCursor = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
function summarizeFetchWorkspacesEntries(entries: Iterable<FetchWorkspacesResponseEntry>): {
|
||||
count: number;
|
||||
projectIds: string[];
|
||||
statusCounts: Record<string, number>;
|
||||
workspaces: Array<{
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
name: string;
|
||||
status: FetchWorkspacesResponseEntry["status"];
|
||||
workspaceKind: FetchWorkspacesResponseEntry["workspaceKind"];
|
||||
activityAt: string | null;
|
||||
}>;
|
||||
} {
|
||||
const workspaces = Array.from(entries, (entry) => ({
|
||||
id: entry.id,
|
||||
projectId: entry.projectId,
|
||||
projectDisplayName: entry.projectDisplayName,
|
||||
name: entry.name,
|
||||
status: entry.status,
|
||||
workspaceKind: entry.workspaceKind,
|
||||
activityAt: entry.activityAt,
|
||||
}));
|
||||
const statusCounts = new Map<string, number>();
|
||||
for (const workspace of workspaces) {
|
||||
statusCounts.set(workspace.status, (statusCounts.get(workspace.status) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
count: workspaces.length,
|
||||
projectIds: [...new Set(workspaces.map((workspace) => workspace.projectId))],
|
||||
statusCounts: Object.fromEntries(statusCounts),
|
||||
workspaces,
|
||||
};
|
||||
}
|
||||
|
||||
class SessionRequestError extends Error {
|
||||
constructor(
|
||||
readonly code: string,
|
||||
@@ -5730,7 +5766,9 @@ export class Session {
|
||||
const filter = request.filter;
|
||||
const sort = this.normalizeFetchWorkspacesSort(request.sort);
|
||||
let entries = await this.listWorkspaceDescriptors();
|
||||
const listedCount = entries.length;
|
||||
entries = entries.filter((workspace) => this.matchesWorkspaceFilter({ workspace, filter }));
|
||||
const filteredCount = entries.length;
|
||||
entries.sort((left, right) => this.compareFetchWorkspacesEntries(left, right, sort));
|
||||
|
||||
const cursorToken = request.page?.cursor;
|
||||
@@ -5749,6 +5787,21 @@ export class Session {
|
||||
? this.encodeFetchWorkspacesCursor(pagedEntries[pagedEntries.length - 1], sort)
|
||||
: null;
|
||||
|
||||
this.sessionLogger.debug(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
filter: request.filter ?? null,
|
||||
sort,
|
||||
page: request.page ?? null,
|
||||
listedCount,
|
||||
filteredCount,
|
||||
returnedCount: pagedEntries.length,
|
||||
hasMore,
|
||||
nextCursor,
|
||||
},
|
||||
"fetch_workspaces_entries_listed",
|
||||
);
|
||||
|
||||
return {
|
||||
entries: pagedEntries,
|
||||
pageInfo: {
|
||||
@@ -6031,6 +6084,16 @@ export class Session {
|
||||
: null;
|
||||
|
||||
try {
|
||||
this.sessionLogger.debug(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
subscribeRequested: Boolean(request.subscribe),
|
||||
filter: request.filter ?? null,
|
||||
sort: request.sort ?? null,
|
||||
page: request.page ?? null,
|
||||
},
|
||||
"fetch_workspaces_request_received",
|
||||
);
|
||||
if (subscriptionId) {
|
||||
this.workspaceUpdatesSubscription = {
|
||||
subscriptionId,
|
||||
@@ -6042,6 +6105,15 @@ export class Session {
|
||||
}
|
||||
|
||||
const payload = await this.listFetchWorkspacesEntries(request);
|
||||
this.sessionLogger.debug(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
subscriptionId,
|
||||
pageInfo: payload.pageInfo,
|
||||
payload: summarizeFetchWorkspacesEntries(payload.entries),
|
||||
},
|
||||
"fetch_workspaces_response_ready",
|
||||
);
|
||||
const snapshotLatestActivityByWorkspaceId = new Map<string, number>();
|
||||
for (const entry of payload.entries) {
|
||||
const parsedLatestActivity = entry.activityAt
|
||||
|
||||
@@ -41,14 +41,20 @@ Read user preferences:
|
||||
cat ~/.paseo/orchestrate.json 2>/dev/null || echo '{}'
|
||||
```
|
||||
|
||||
See [preferences.md](references/preferences.md) for schema, defaults, and mode resolution. Merge with defaults for any missing fields.
|
||||
Merge with defaults for any missing fields. The file maps role categories to `<agent-type>/<model>` strings:
|
||||
|
||||
If the user asks to store a preference at any point, update the file per the preferences reference.
|
||||
- The part before `/` is the `agentType` (e.g., `codex`, `claude`, `opencode`)
|
||||
- The part after `/` is the `model` (e.g., `gpt-5.4`, `opus`)
|
||||
|
||||
Example models:
|
||||
| Category | Roles covered | Default |
|
||||
|----------|--------------|---------|
|
||||
| `impl` | impl, tester, refactorer | `codex/gpt-5.4` |
|
||||
| `ui` | impl agents doing UI/styling work | `claude/opus` |
|
||||
| `research` | researcher | `codex/gpt-5.4` |
|
||||
| `planning` | planner, plan-reviewer | `codex/gpt-5.4` |
|
||||
| `audit` | auditor, qa | `codex/gpt-5.4` |
|
||||
|
||||
- claude/opus
|
||||
- codex/gpt-5.4
|
||||
The file also has a `preferences` array of freeform natural language strings. Read these at startup and weave them into your behavior contextually. When the user says "store my preference: X", update the file.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
@@ -62,6 +68,8 @@ Example models:
|
||||
- **Never trust implementation agents at face value.** Always verify with separate auditor agents.
|
||||
- **Never classify failures as "pre-existing."** If a test is failing, fix it or delete it.
|
||||
- **The plan file on disk is the source of truth.** Re-read `~/.paseo/plans/<task-slug>.md` before every verification and QA phase. It survives compaction.
|
||||
- **Never micromanage agents.** Describe the **problem** (what's broken, how it fails, the error output), not the **solution** (which line to change, what to change it to). Agents are smart — give them context and let them figure out the fix. If you find yourself writing specific line numbers or code snippets in an agent prompt, you're doing it wrong. Say "this test fails with this error" not "change line 47 to use X instead of Y."
|
||||
- **Any task that touches tests MUST run those tests.** This is non-negotiable. If an agent modifies, fixes, or writes a test file, the prompt MUST explicitly say "run the test(s) and confirm they pass." Typecheck alone is never sufficient for test changes. An agent that changes a test without running it has not completed its task.
|
||||
|
||||
## Launching Agents
|
||||
|
||||
@@ -79,6 +87,24 @@ All agents are launched via the Paseo **create agent** tool. The standard patter
|
||||
To send follow-up instructions: Paseo **send agent prompt**.
|
||||
To archive: Paseo **archive agent**.
|
||||
|
||||
### How to Write Agent Prompts
|
||||
|
||||
**Describe the problem, not the solution.** Your prompt should tell the agent:
|
||||
- What's wrong or what needs to be built (the goal)
|
||||
- How it currently fails (error output, test output, user-visible behavior)
|
||||
- The acceptance criteria (what "done" looks like)
|
||||
|
||||
**Do NOT tell the agent:**
|
||||
- Which specific lines to change
|
||||
- What code to write
|
||||
- Which functions to call or which patterns to use
|
||||
|
||||
The agent reads the plan and the code. It will figure out the implementation. If you're writing specific line numbers or code snippets in the prompt, you're micromanaging and it will backfire — the agent takes you literally and skips its own judgment.
|
||||
|
||||
Bad: "In `new-workspace.spec.ts` at line 164, change the tab assertion from `getByText('New Agent')` to `getByTestId(/workspace-tab-agent_/)`"
|
||||
|
||||
Good: "The new-workspace E2E test is failing. The test creates a workspace via empty submit, but then the tab assertion fails because it looks for text 'New Agent' which doesn't match the actual tab label. Here's the error output: [paste error]. Fix the test and run it to confirm it passes."
|
||||
|
||||
---
|
||||
|
||||
## Worktree Mode
|
||||
@@ -97,51 +123,223 @@ If `--worktree` is NOT set, skip this — work in the current directory as norma
|
||||
default mode only default mode only
|
||||
```
|
||||
|
||||
### Phase 1: Triage
|
||||
---
|
||||
|
||||
See [triage.md](references/triage.md).
|
||||
## Phase 1: Triage
|
||||
|
||||
Assess complexity order (1-4) yourself. This is fast — grep relevant files, read the task, determine how many packages/modules are involved.
|
||||
Triage is fast and cheap. You do it yourself — no agents. The goal is to assess complexity order, which determines how many agents to deploy at each phase.
|
||||
|
||||
State the order and why: "Order 3 — touches server session management and the app's git status display."
|
||||
1. Read the task description
|
||||
2. Grep the codebase for relevant files, types, and functions
|
||||
3. Identify how many packages/modules are touched
|
||||
4. Identify whether it's a new feature, refactor, bug fix, or architectural change
|
||||
5. Assign a complexity order
|
||||
|
||||
The order determines how many agents to deploy at each subsequent phase.
|
||||
State the order and briefly why: "Order 3 — touches server session management and the app's git status display across two packages."
|
||||
|
||||
### Phase 2: Grill (default mode only)
|
||||
### Complexity Orders
|
||||
|
||||
See [grill.md](references/grill.md).
|
||||
**Order 1 — Single file, single concern.** A contained change: fix a bug in one function, add a field to one type, update one component.
|
||||
|
||||
| Phase | Agents |
|
||||
|-------|--------|
|
||||
| Research | 1 researcher |
|
||||
| Planning | 0 — orchestrator plans inline |
|
||||
| Implement | 1 impl |
|
||||
| Verify | 1-2 auditors |
|
||||
| Cleanup | 0-1 refactorer |
|
||||
|
||||
**Order 2 — Single module, few files.** A feature or fix within one package that touches 3-8 files.
|
||||
|
||||
| Phase | Agents |
|
||||
|-------|--------|
|
||||
| Research | 2 researchers |
|
||||
| Planning | 1 planner |
|
||||
| Implement | 1 impl per phase |
|
||||
| Verify | 2-3 auditors |
|
||||
| Cleanup | 1 refactorer |
|
||||
|
||||
**Order 3 — Cross-module, multiple packages.** A feature that spans packages.
|
||||
|
||||
| Phase | Agents |
|
||||
|-------|--------|
|
||||
| Research | 3-4 researchers |
|
||||
| Planning | 2 planners + 1 plan-reviewer |
|
||||
| Implement | 1-2 impl agents per phase |
|
||||
| Verify | 3-4 auditors |
|
||||
| Cleanup | 1-2 refactorers |
|
||||
|
||||
**Order 4 — Architectural, system-wide.** A new subsystem, major refactor, or system-wide change.
|
||||
|
||||
| Phase | Agents |
|
||||
|-------|--------|
|
||||
| Research | 5+ researchers |
|
||||
| Planning | 2+ planners + 2 plan-reviewers |
|
||||
| Implement | 2+ impl agents per phase |
|
||||
| Verify | Full auditor suite per phase |
|
||||
| Cleanup | 2+ refactorers |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Grill (default mode only)
|
||||
|
||||
Skipped in `--auto` mode.
|
||||
|
||||
Research the codebase first to avoid asking questions the code can answer. Then question the user depth-first through the decision tree until all branches are resolved.
|
||||
### Protocol: Research First, Grill Second
|
||||
|
||||
Conclude with a summary of resolved decisions. This feeds the research and planning phases.
|
||||
Before asking the user anything:
|
||||
|
||||
### Phase 3: Research
|
||||
1. Read the task description
|
||||
2. Grep relevant files, types, functions
|
||||
3. Read key files to understand the current state
|
||||
4. Form your own understanding of the problem space
|
||||
|
||||
See [research-phase.md](references/research-phase.md).
|
||||
Then ask the user ONLY about things the code cannot answer: intent, scope boundaries, UX preferences, tradeoffs, priorities, acceptance criteria. Never ask a question the codebase could answer.
|
||||
|
||||
Deploy researchers in parallel based on complexity order. Each gets a narrow mandate — one area of the codebase, one external doc source, one reference project.
|
||||
### Questioning Approach
|
||||
|
||||
Wait for all researchers to complete (you'll be notified). Check their activity with Paseo **get agent activity** to read findings. If findings raise new questions (default mode), go back and ask the user.
|
||||
Treat the task as a decision tree. Each design choice branches into sub-decisions, constraints, and consequences.
|
||||
|
||||
- Ask one question at a time
|
||||
- Wait for the answer before moving on
|
||||
- Drill depth-first into each branch until it's resolved or explicitly deferred
|
||||
- For each question, state your recommended answer based on what you've learned from the code — the user can confirm or override
|
||||
- Cycle through question types: feasibility, dependency, edge case, alternative, scope, ordering, failure mode
|
||||
|
||||
Every 3-4 questions, summarize: resolved decisions, open branches, current focus.
|
||||
|
||||
Stop grilling when all branches are resolved, the user signals they're done, or no meaningful questions remain. Conclude with a final summary of all resolved decisions.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Research
|
||||
|
||||
Deploy researchers to gather information before planning. Each researcher gets a narrow mandate — one area of the codebase, one external doc source, one reference project.
|
||||
|
||||
### Launching Researchers
|
||||
|
||||
```
|
||||
title: "researcher-<scope>"
|
||||
agentType: <resolved from providers.research>
|
||||
model: <resolved from providers.research>
|
||||
background: true
|
||||
notifyOnFinish: true
|
||||
initialPrompt: "You are a researcher.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for the objective.
|
||||
|
||||
<specific research mandate>
|
||||
|
||||
Include in your findings: relevant files, types, interfaces, patterns, gotchas, and anything surprising. Do NOT suggest solutions or edit files."
|
||||
```
|
||||
|
||||
Wait for all researchers to complete (you'll be notified). Use Paseo **get agent activity** to read their findings. Synthesize into a research summary that feeds the planning phase.
|
||||
|
||||
If findings raise new questions (default mode), go back and ask the user.
|
||||
|
||||
Archive all researchers when done.
|
||||
|
||||
### Phase 4: Plan
|
||||
---
|
||||
|
||||
See [planning-phase.md](references/planning-phase.md).
|
||||
## Phase 4: Plan
|
||||
|
||||
Deploy planners informed by research findings. For Order 3+, deploy multiple planners and plan-reviewers. Iterate until the plan is solid.
|
||||
Deploy planners to create an implementation plan informed by research findings.
|
||||
|
||||
Persist the final plan to `~/.paseo/plans/<task-slug>.md`.
|
||||
### Refactor-First Thinking
|
||||
|
||||
### Phase 5: Approve (default mode only)
|
||||
Every planner prompt must emphasize this: the default agent instinct is to bolt new code on top of existing code. Resist this.
|
||||
|
||||
The right approach:
|
||||
1. Study the existing code — understand why it's shaped the way it is
|
||||
2. Design the target shape — what would the code look like if this feature had always existed?
|
||||
3. Identify the refactoring gap — what needs to change so the new feature slots in cleanly?
|
||||
4. Plan refactor phases before feature phases
|
||||
|
||||
If the plan has a phase called "wire up" or "connect" or "integrate," a refactor phase could probably eliminate the need for it.
|
||||
|
||||
### Launching Planners
|
||||
|
||||
```
|
||||
title: "planner-<scope>"
|
||||
agentType: <resolved from providers.planning>
|
||||
model: <resolved from providers.planning>
|
||||
background: true
|
||||
notifyOnFinish: true
|
||||
initialPrompt: "You are a planner.
|
||||
|
||||
Read the research findings provided below and the objective.
|
||||
|
||||
<paste synthesized research findings and objective>
|
||||
|
||||
Draft a phased implementation plan. Think refactor-first: before planning the feature, identify what existing code needs to be reshaped so the feature slots in naturally.
|
||||
|
||||
For each phase, specify:
|
||||
- What changes and why
|
||||
- Files involved
|
||||
- Types and interfaces affected
|
||||
- Tests to write (failing test first — TDD)
|
||||
- Acceptance criteria for the phase
|
||||
|
||||
Write the plan to ~/.paseo/plans/<task-slug>.md"
|
||||
```
|
||||
|
||||
### Launching Plan-Reviewers
|
||||
|
||||
```
|
||||
title: "plan-reviewer-<scope>"
|
||||
agentType: <resolved from providers.planning>
|
||||
model: <resolved from providers.planning>
|
||||
background: true
|
||||
notifyOnFinish: true
|
||||
initialPrompt: "You are a plan-reviewer.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md.
|
||||
|
||||
Challenge the plan:
|
||||
- Is it bolting new code on top, or reshaping existing code first?
|
||||
- Are there coordination/glue/bridge layers that a better refactor would eliminate?
|
||||
- What edge cases are missing? What will break?
|
||||
- What's over-engineered? What's under-specified?
|
||||
- Is the phase ordering correct? Are there hidden dependencies?"
|
||||
```
|
||||
|
||||
For Order 3+, deploy multiple planners (one per area) + plan-reviewers. Iterate until the plan-reviewer's only feedback is minor.
|
||||
|
||||
### Plan Structure
|
||||
|
||||
The final plan must follow:
|
||||
|
||||
```
|
||||
# <Task Title>
|
||||
|
||||
## Objective
|
||||
<one-paragraph summary>
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] <criterion 1>
|
||||
- [ ] <criterion 2>
|
||||
|
||||
## Plan
|
||||
### Phase 1: <name>
|
||||
<description, files, types, tests, acceptance criteria>
|
||||
|
||||
### Phase 2: <name>
|
||||
...
|
||||
```
|
||||
|
||||
Persist to `~/.paseo/plans/<task-slug>.md`. Archive all planners and plan-reviewers.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Approve (default mode only)
|
||||
|
||||
Skipped in `--auto` mode.
|
||||
|
||||
Present the plan to the user. Wait for explicit confirmation before proceeding.
|
||||
|
||||
### Phase 6: Set Up
|
||||
---
|
||||
|
||||
## Phase 6: Set Up
|
||||
|
||||
Persist the plan to disk and set up the heartbeat:
|
||||
|
||||
@@ -152,7 +350,7 @@ Use the Paseo **create schedule** tool with:
|
||||
- `expiresIn`: `"4h"`
|
||||
- `prompt`: (see heartbeat prompt below)
|
||||
|
||||
#### Heartbeat prompt
|
||||
### Heartbeat prompt
|
||||
|
||||
```
|
||||
HEARTBEAT — periodic self-check.
|
||||
@@ -191,9 +389,9 @@ Do the following steps in order:
|
||||
- Do NOT delete this schedule yet — if the user requests a PR, the heartbeat transitions to CI monitoring mode. Only delete it once CI is fully green (or if the user declines a PR).
|
||||
```
|
||||
|
||||
### Phase 7: Implement
|
||||
---
|
||||
|
||||
See [impl-phase.md](references/impl-phase.md).
|
||||
## Phase 7: Implement
|
||||
|
||||
Execute phases from the plan sequentially. For each phase:
|
||||
1. Launch impl agent(s) with `background: true, notifyOnFinish: true`
|
||||
@@ -205,52 +403,401 @@ Execute phases from the plan sequentially. For each phase:
|
||||
|
||||
UI passes use `providers.ui` from preferences. All other impl work uses `providers.impl`.
|
||||
|
||||
### Phase 8: Verify
|
||||
### TDD — Not Optional
|
||||
|
||||
See [verification.md](references/verification.md).
|
||||
Every impl agent works TDD:
|
||||
1. Write a failing test that defines the expected behavior
|
||||
2. Make it pass
|
||||
3. Refactor if needed
|
||||
4. All tests green — not just new ones, the full relevant suite
|
||||
|
||||
After each implementation phase, deploy auditors in parallel. Match auditors to the type of work (refactor, feature, UI). Each auditor checks exactly one thing.
|
||||
If an impl agent finds a broken test, it fixes it. No "pre-existing failures." No exceptions.
|
||||
|
||||
If auditors find issues, direct the impl agent to fix or launch a new one. Re-verify after fixes.
|
||||
### Impl Agent Prompt Template
|
||||
|
||||
Archive all auditors when done.
|
||||
```
|
||||
title: "impl-<scope>-<phase>"
|
||||
agentType: <resolved from providers.impl>
|
||||
model: <resolved from providers.impl>
|
||||
cwd: <worktree-path if worktree mode, omit otherwise>
|
||||
background: true
|
||||
notifyOnFinish: true
|
||||
initialPrompt: "You are an implementation engineer. [Load the e2e-playwright skill if frontend/E2E work.]
|
||||
|
||||
### Phase 9: Cleanup
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md to understand the objective and your specific phase.
|
||||
|
||||
See [cleanup.md](references/cleanup.md).
|
||||
Do not bolt new code on top of existing code. If the existing code isn't shaped to accommodate your work, reshape it first. The goal is code that looks like this feature always existed.
|
||||
|
||||
After all phases are implemented and verified, deploy refactorers for a final sweep: DRY, dead code, naming. Run a regression auditor after cleanup to confirm nothing broke.
|
||||
Work TDD: write a failing test first, then make it pass. All tests must be green when done — not just your new ones, the full relevant suite. If you find a broken test, fix it.
|
||||
|
||||
Archive all refactorers when done.
|
||||
<describe the problem and acceptance criteria — NOT the solution>
|
||||
|
||||
### Phase 10: Final QA
|
||||
When done: run typecheck AND run any tests you modified or that cover your changes. Both must pass. Do NOT commit."
|
||||
```
|
||||
|
||||
See [final-qa.md](references/final-qa.md).
|
||||
### UI Agent Prompt Template
|
||||
|
||||
Re-read the plan from disk. Run typecheck and tests yourself. Deploy final review and quality auditors. Fix any issues found. Do not deliver until everything passes.
|
||||
```
|
||||
title: "impl-<scope>-ui"
|
||||
agentType: <resolved from providers.ui>
|
||||
model: <resolved from providers.ui>
|
||||
cwd: <worktree-path if worktree mode, omit otherwise>
|
||||
background: true
|
||||
notifyOnFinish: true
|
||||
initialPrompt: "You are a UI engineer. [Load the e2e-playwright skill.]
|
||||
|
||||
Archive all QA agents when done.
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
### Phase 11: Deliver
|
||||
The functionality is implemented. Your job is the styling pass:
|
||||
- Study existing components and styles in nearby screens
|
||||
- Follow existing conventions exactly — no new patterns
|
||||
- Keep design minimal and consistent with the rest of the app
|
||||
- Think carefully about spacing, alignment, and visual hierarchy
|
||||
|
||||
<describe the specific UI work>
|
||||
|
||||
Run typecheck when done. Do NOT commit."
|
||||
```
|
||||
|
||||
### Handling Blockers
|
||||
|
||||
If an impl agent reports a blocker:
|
||||
- Do NOT ask the user (in either mode)
|
||||
- Spin up a researcher to investigate
|
||||
- Spin up an impl agent to fix it
|
||||
- The scope of work is unlimited — touching other files, packages, or systems is fine
|
||||
|
||||
Archive every impl agent as soon as its phase is verified.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Verify
|
||||
|
||||
After every implementation phase, deploy auditors to verify the work. Auditors are read-only — they check, they don't fix. Each auditor has a single specialization.
|
||||
|
||||
### Which Auditors to Deploy
|
||||
|
||||
| Phase type | Auditors |
|
||||
|-----------|----------|
|
||||
| Refactor | `parity`, `regression`, `types` |
|
||||
| Feature (backend) | `overeng`, `tests`, `regression`, `types` |
|
||||
| Feature (frontend) | `overeng`, `tests`, `types`, `browser` (if applicable) |
|
||||
| UI pass | `overeng`, `browser` (if applicable) |
|
||||
| Test-only | `regression` |
|
||||
|
||||
Deploy all relevant auditors in parallel — they're read-only so they don't conflict.
|
||||
|
||||
### Auditor Prompts
|
||||
|
||||
All auditors are launched via the Paseo **create agent** tool with `background: true` and `notifyOnFinish: true`.
|
||||
|
||||
#### overeng (anti-over-engineering)
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-overeng"
|
||||
initialPrompt: "You are an anti-over-engineering auditor.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Check the recent changes (use git diff) for:
|
||||
- Unnecessary abstractions, helpers, or utility functions
|
||||
- Defensive code for scenarios that can't happen
|
||||
- Event emitters, observers, or pub/sub where a direct call would do
|
||||
- Coordination/glue/bridge layers between old and new code
|
||||
- Flag parameters or special-case branches
|
||||
- Weird or overly literal naming
|
||||
|
||||
For each issue: file, line, what's wrong, what it should be instead.
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
#### dry (DRY violations)
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-dry"
|
||||
initialPrompt: "You are a DRY auditor.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Check the recent changes (use git diff) for:
|
||||
- Duplicated logic across files
|
||||
- Copy-pasted code with minor variations
|
||||
- Types that repeat fields from other types instead of deriving
|
||||
- Constants or strings repeated instead of extracted
|
||||
|
||||
For each issue: the duplicated code locations and a brief note on how to consolidate.
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
#### tests (test coverage)
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-tests"
|
||||
initialPrompt: "You are a test coverage auditor. [Load the e2e-playwright skill if E2E tests are in scope.]
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Check:
|
||||
- Does every new behavior have a test?
|
||||
- Do tests verify behavior, not implementation details?
|
||||
- Are tests asserting real outcomes or just mocks?
|
||||
- Are there edge cases without test coverage?
|
||||
- Do E2E tests follow DSL-style helpers and ARIA role selectors (if applicable)?
|
||||
|
||||
Run the full relevant test suite and report output.
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
#### regression
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-regression"
|
||||
initialPrompt: "You are a regression auditor.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Run the full test suite. Report:
|
||||
- Total tests, passed, failed, skipped
|
||||
- Any failures with full error output
|
||||
- Whether failures are in new tests or existing tests
|
||||
|
||||
If ANY test fails, this phase is not done.
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
#### types
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-types"
|
||||
initialPrompt: "You are a type auditor.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Run typecheck (npm run typecheck). Report:
|
||||
- Pass/fail
|
||||
- All type errors with file, line, and error message
|
||||
- Any use of 'any', type assertions, or @ts-ignore in the changes
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
#### browser
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-browser"
|
||||
initialPrompt: "You are a browser QA auditor. Load the e2e-playwright skill.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Test the affected user flows in a browser:
|
||||
- Navigate to the relevant screens
|
||||
- Exercise the new/changed functionality
|
||||
- Check for visual regressions, broken layouts, missing states
|
||||
- Take screenshots of results
|
||||
|
||||
Report what works and what doesn't with evidence. Do NOT edit files."
|
||||
```
|
||||
|
||||
#### parity (for refactors)
|
||||
|
||||
```
|
||||
title: "auditor-<scope>-parity"
|
||||
initialPrompt: "You are a parity auditor.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
This was a refactoring phase — behavior must be identical before and after. Check:
|
||||
- All existing tests still pass (run them)
|
||||
- No behavioral changes were introduced
|
||||
- Public APIs and interfaces are unchanged
|
||||
- No removed functionality unless explicitly planned
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
### Interpreting Findings
|
||||
|
||||
If any auditor reports issues:
|
||||
1. Check the auditor's activity with Paseo **get agent activity** for details
|
||||
2. Direct the impl agent to fix them via Paseo **send agent prompt**, or launch a new impl agent if the old one is stale
|
||||
3. Re-deploy the same auditor after fixes
|
||||
4. Do not proceed to the next phase until all auditors pass
|
||||
|
||||
Archive every auditor as soon as its report is reviewed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Cleanup
|
||||
|
||||
After all implementation phases are verified, deploy refactorer agents for targeted cleanup. Each refactorer has a single specialization.
|
||||
|
||||
### Refactorer Prompts
|
||||
|
||||
All refactorers launched via the Paseo **create agent** tool with `background: true` and `notifyOnFinish: true`.
|
||||
|
||||
#### dry (consolidate duplication)
|
||||
|
||||
```
|
||||
title: "refactorer-<scope>-dry"
|
||||
initialPrompt: "You are a cleanup engineer specializing in DRY.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Look at the full diff of changes in this task (use git diff). Consolidate:
|
||||
- Duplicated logic — extract shared functions or reuse existing ones
|
||||
- Repeated types — derive with Pick, Omit, or extend instead of redefining
|
||||
- Repeated constants or strings — extract to a single source
|
||||
|
||||
Only fix genuine duplication. Three similar lines is fine — don't create premature abstractions. Run typecheck and any tests you touch when done.
|
||||
|
||||
Do NOT commit."
|
||||
```
|
||||
|
||||
#### dead-code (remove unused code)
|
||||
|
||||
```
|
||||
title: "refactorer-<scope>-dead-code"
|
||||
initialPrompt: "You are a cleanup engineer specializing in dead code.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Look at the full diff of changes (use git diff). Remove:
|
||||
- Unused imports
|
||||
- Unused variables, functions, or types introduced by this task
|
||||
- Commented-out code
|
||||
- Backwards-compatibility shims or renamed _vars that serve no purpose
|
||||
|
||||
Do NOT remove code that predates this task unless it was made dead by this task's changes. Run typecheck and any tests you touch when done.
|
||||
|
||||
Do NOT commit."
|
||||
```
|
||||
|
||||
#### naming (fix unclear names)
|
||||
|
||||
```
|
||||
title: "refactorer-<scope>-naming"
|
||||
initialPrompt: "You are a cleanup engineer specializing in naming.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Look at all new names introduced by this task (functions, variables, types, files). Fix:
|
||||
- Overly literal or verbose names
|
||||
- Inconsistent naming relative to surrounding code conventions
|
||||
- Unclear abbreviations
|
||||
- Names that describe implementation instead of intent
|
||||
|
||||
Only rename things introduced or modified by this task. Run typecheck and any tests you touch when done.
|
||||
|
||||
Do NOT commit."
|
||||
```
|
||||
|
||||
Deploy refactorers in parallel. After cleanup, run a regression auditor to confirm nothing broke.
|
||||
|
||||
Archive every refactorer as soon as verified.
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Final QA
|
||||
|
||||
After all phases are implemented, verified, and cleaned up, run one final pass.
|
||||
|
||||
### 1. Re-read the plan
|
||||
|
||||
```bash
|
||||
cat ~/.paseo/plans/<task-slug>.md
|
||||
```
|
||||
|
||||
### 2. Run typecheck yourself
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
Must pass. No exceptions.
|
||||
|
||||
### 3. Run the full test suite yourself
|
||||
|
||||
Run all relevant tests. Must be 100% green. No skipped tests, no "known failures."
|
||||
|
||||
### 4. Final review agent
|
||||
|
||||
```
|
||||
title: "qa-<scope>-review"
|
||||
initialPrompt: "You are a final reviewer.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for the objective and acceptance criteria.
|
||||
|
||||
Review the entire git diff for this task. For each acceptance criterion, report:
|
||||
- YES — met, with evidence (file, line, test that proves it)
|
||||
- NO — not met, with explanation of what's missing
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
### 5. Final anti-over-engineering agent
|
||||
|
||||
```
|
||||
title: "qa-<scope>-overeng"
|
||||
initialPrompt: "You are a final quality auditor.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Audit the entire git diff for this task:
|
||||
- Unnecessary abstractions or helpers
|
||||
- Code that's clever instead of clear
|
||||
- Missing error handling at system boundaries
|
||||
- Excessive error handling for internal code
|
||||
- Any code that doesn't serve the acceptance criteria
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
### 6. Browser QA (if applicable)
|
||||
|
||||
If the task involves UI changes:
|
||||
|
||||
```
|
||||
title: "qa-<scope>-browser"
|
||||
initialPrompt: "You are a QA engineer. Load the e2e-playwright skill.
|
||||
|
||||
Read the plan at ~/.paseo/plans/<task-slug>.md for context.
|
||||
|
||||
Test all affected user flows end-to-end in the browser. For each flow:
|
||||
- What you tested
|
||||
- What you expected
|
||||
- What actually happened
|
||||
- Screenshot evidence
|
||||
|
||||
Do NOT edit files."
|
||||
```
|
||||
|
||||
If any final QA agent reports issues, launch an impl or refactorer to fix, then re-run the specific check. Do not deliver with any failing checks.
|
||||
|
||||
Archive all QA agents once reports are reviewed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Deliver
|
||||
|
||||
1. Archive any remaining implementation/QA agents
|
||||
2. **If in worktree mode:**
|
||||
- Report the worktree path and branch name
|
||||
- Ask: "The work is in worktree `<worktree-path>` on branch `orchestrate/<task-slug>`. Should I merge it into your current branch, create a PR, or leave the worktree for you to review?"
|
||||
- Do NOT remove the worktree automatically — the user decides what to do with it
|
||||
- Do NOT remove the worktree automatically
|
||||
3. **If NOT in worktree mode:**
|
||||
- Report to the user:
|
||||
- What was done (high-level)
|
||||
- What files changed
|
||||
- Verification results (typecheck, tests, auditor verdicts)
|
||||
- Ask: "Should I commit this? Create a PR? Or leave it uncommitted for you to review?"
|
||||
- Report: what was done (high-level), what files changed, verification results
|
||||
- Ask: "Should I commit this? Create a PR? Or leave it uncommitted for you to review?"
|
||||
|
||||
Wait for the user's instruction.
|
||||
|
||||
**When the user asks for a PR, the job is NOT done when the PR is created.** The objective is: PR created AND all CI checks passing. After creating the PR:
|
||||
|
||||
1. Keep the heartbeat schedule running — do NOT delete it yet.
|
||||
2. Update the heartbeat prompt to include CI monitoring (see below).
|
||||
2. Update the heartbeat prompt to CI monitoring mode (below).
|
||||
3. Monitor CI status via `gh pr checks <pr-number> --watch` or `gh pr checks <pr-number>`.
|
||||
4. If any check fails:
|
||||
- Read the failure logs (`gh run view <run-id> --log-failed`).
|
||||
@@ -259,11 +806,9 @@ Wait for the user's instruction.
|
||||
- Continue monitoring.
|
||||
5. Only when ALL checks are green:
|
||||
- Delete the heartbeat schedule.
|
||||
- Report to the user with the full PR URL (e.g. `https://github.com/owner/repo/pull/N`), not just a number.
|
||||
- Report to the user with the full PR URL.
|
||||
|
||||
#### Post-PR heartbeat prompt
|
||||
|
||||
After creating the PR, update the heartbeat schedule prompt to:
|
||||
### Post-PR heartbeat prompt
|
||||
|
||||
```
|
||||
HEARTBEAT — CI monitoring for PR #<pr-number>.
|
||||
@@ -290,22 +835,25 @@ Do the following steps in order:
|
||||
|
||||
---
|
||||
|
||||
## Role Reference
|
||||
## Roles Reference
|
||||
|
||||
See [roles.md](references/roles.md) for the complete role definitions, naming convention, and what each role can and cannot do.
|
||||
Every agent has exactly one role. The role determines what the agent does, whether it can edit files, and how it's named.
|
||||
|
||||
| Role | Job | Edits? |
|
||||
|------|-----|--------|
|
||||
| `researcher` | Gathers info: codebase, docs, web, scripts | No |
|
||||
| `planner` | Creates implementation plan from research | No |
|
||||
| `plan-reviewer` | Adversarially challenges a plan | No |
|
||||
| `impl` | Writes code, TDD | Yes |
|
||||
| `tester` | Writes/runs tests | Yes |
|
||||
| `auditor` | Read-only verification (sub-specializations) | No |
|
||||
| `refactorer` | Targeted cleanup (sub-specializations) | Yes |
|
||||
| `qa` | End-to-end QA, browser testing | No |
|
||||
**Naming:** `<role>-<scope>[-<specialization>]` in kebab-case.
|
||||
|
||||
Naming: `<role>-<scope>[-<specialization>]`
|
||||
| Role | Job | Edits? | Prompt emphasis |
|
||||
|------|-----|--------|----------------|
|
||||
| `researcher` | Gathers info: codebase, docs, web, scripts | No | "Report what you find. Do not suggest solutions. Do not edit files." |
|
||||
| `planner` | Creates implementation plan from research | No | "Think refactor-first. Design the target shape, not the steps." |
|
||||
| `plan-reviewer` | Adversarially challenges a plan | No | "Challenge the plan. Find what's wrong, missing, or over-engineered." |
|
||||
| `impl` | Writes code, works TDD | Yes | "Work TDD. Reshape existing code. Run typecheck AND run any tests you modified. Both must pass. Do NOT commit." |
|
||||
| `tester` | Writes/fixes tests | Yes | "Verify behavior, not implementation. Run every test you modified and confirm it passes. A test change without running the test is not done." |
|
||||
| `auditor` | Read-only verification | No | "Check [specialization]. Report YES/NO with evidence. Do NOT edit files." |
|
||||
| `refactorer` | Targeted cleanup | Yes | "Fix [specialization] only. Run typecheck and any tests you touch. Do NOT commit." |
|
||||
| `qa` | End-to-end QA, browser testing | No | "Test the actual user experience. Report with evidence." |
|
||||
|
||||
Auditor specializations: `overeng`, `dry`, `tests`, `regression`, `types`, `browser`, `parity`
|
||||
Refactorer specializations: `dry`, `dead-code`, `naming`
|
||||
|
||||
---
|
||||
|
||||
@@ -319,3 +867,4 @@ Naming: `<role>-<scope>[-<specialization>]`
|
||||
- **The plan file is the shared context.** Every agent reads the plan from disk.
|
||||
- **Archive aggressively.** Done agents clutter the UI.
|
||||
- **Trust but verify.** Always verify with separate agents. Never take an impl agent's word for it.
|
||||
- **Describe problems, not solutions.** Tell agents what's wrong, not what to type.
|
||||
|
||||
@@ -26,6 +26,26 @@ After each phase:
|
||||
|
||||
Use the Paseo **create agent** tool with `background: true` and `notifyOnFinish: true`. **If in worktree mode, always set `cwd` to the worktree path.**
|
||||
|
||||
### How to describe the work
|
||||
|
||||
**Describe the problem, not the solution.** Your prompt should tell the agent:
|
||||
- What's wrong or what needs to be built (the goal)
|
||||
- How it currently fails (error output, test output, user-visible behavior)
|
||||
- The acceptance criteria (what "done" looks like)
|
||||
|
||||
**Do NOT tell the agent:**
|
||||
- Which specific lines to change
|
||||
- What code to write
|
||||
- Which functions to call or which patterns to use
|
||||
|
||||
The agent reads the plan and the code. It will figure out the implementation. If you're writing specific line numbers or code snippets in the prompt, you're micromanaging and it will backfire — the agent takes you literally and skips its own judgment.
|
||||
|
||||
Bad: "In `new-workspace.spec.ts` at line 164, change the tab assertion from `getByText('New Agent')` to `getByTestId(/workspace-tab-agent_/)`"
|
||||
|
||||
Good: "The new-workspace E2E test is failing. The test creates a workspace via empty submit, but then the tab assertion fails because it looks for text 'New Agent' which doesn't match the actual tab label. Here's the error output: [paste error]. Fix the test and run it to confirm it passes."
|
||||
|
||||
### Prompt template
|
||||
|
||||
```
|
||||
title: "impl-<scope>-<phase>"
|
||||
agentType: <resolved from providers.impl>
|
||||
@@ -41,9 +61,9 @@ Do not bolt new code on top of existing code. If the existing code isn't shaped
|
||||
|
||||
Work TDD: write a failing test first, then make it pass. All tests must be green when done — not just your new ones, the full relevant suite. If you find a broken test, fix it.
|
||||
|
||||
<describe the specific phase work and acceptance criteria>
|
||||
<describe the problem and acceptance criteria — NOT the solution>
|
||||
|
||||
Run typecheck and tests when done. Do NOT commit."
|
||||
When done: run typecheck AND run any tests you modified or that cover your changes. Both must pass. Do NOT commit."
|
||||
```
|
||||
|
||||
## UI Passes
|
||||
|
||||
@@ -37,17 +37,17 @@ Adversarially challenges a plan. Looks for: bolted-on code vs natural fit, missi
|
||||
|
||||
### impl
|
||||
|
||||
Writes code. Works TDD: failing test first, then make it pass. Runs typecheck and tests when done.
|
||||
Writes code. Works TDD: failing test first, then make it pass. Runs typecheck and all modified/related tests when done.
|
||||
|
||||
- **Edits files:** Yes
|
||||
- **Prompt emphasis:** "Work TDD. Do not bolt new code on top — reshape existing code so the feature slots in naturally. Run typecheck and tests when done. Do NOT commit."
|
||||
- **Prompt emphasis:** "Work TDD. Do not bolt new code on top — reshape existing code so the feature slots in naturally. Run typecheck AND run any tests you modified or that cover your changes when done. Both must pass. Do NOT commit."
|
||||
|
||||
### tester
|
||||
|
||||
Writes or fixes tests specifically. Used when test work is substantial enough to warrant a dedicated agent separate from impl.
|
||||
|
||||
- **Edits files:** Yes
|
||||
- **Prompt emphasis:** "Write tests that verify behavior, not implementation details. Run the full relevant suite when done."
|
||||
- **Prompt emphasis:** "Write tests that verify behavior, not implementation details. Run every test you modified and confirm it passes. A test change without running the test is not done."
|
||||
|
||||
### auditor
|
||||
|
||||
|
||||
Reference in New Issue
Block a user