Compare commits

..
Author SHA1 Message Date
Mohamed Boudra c28d917a4b Simplify prompt shortcut handling 2026-03-24 11:14:13 +07:00
Mohamed Boudra 2f6b8cea01 Add number key shortcuts for agent prompts 2026-03-23 21:56:39 +07:00
31 changed files with 931 additions and 1168 deletions
-3
View File
@@ -26,9 +26,6 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build highlight dependency
run: npm run build --workspace=@getpaseo/highlight
- name: Typecheck
run: npm run typecheck --workspace=@getpaseo/app
-3
View File
@@ -36,9 +36,6 @@ jobs:
- name: Install server dependencies
run: npm install --workspace=@getpaseo/server --include-workspace-root
- name: Build highlight dependency
run: npm run build --workspace=@getpaseo/highlight
- name: Build relay dependency
run: npm run build --workspace=@getpaseo/relay
-8
View File
@@ -1,13 +1,5 @@
# Changelog
## 0.1.33 - 2026-03-23
### Fixed
- Fixed the desktop app failing to reopen after closing on macOS — the daemon and agent processes were registering with Launch Services as instances of the main app, blocking subsequent launches.
- Fixed dictation not working in the packaged desktop app — the microphone entitlement was missing from the hardened runtime configuration.
- Fixed leaked Claude Code child processes when agents were closed — the SDK query stream was not being properly shut down.
- The notification test button now surfaces errors instead of failing silently.
## 0.1.32 - 2026-03-23
### Added
+19 -19
View File
@@ -1,12 +1,12 @@
{
"name": "paseo",
"version": "0.1.33",
"version": "0.1.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paseo",
"version": "0.1.33",
"version": "0.1.32",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"workspaces": [
@@ -34842,16 +34842,16 @@
},
"packages/app": {
"name": "@getpaseo/app",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.33",
"@getpaseo/highlight": "0.1.33",
"@getpaseo/server": "0.1.33",
"@getpaseo/expo-two-way-audio": "0.1.32",
"@getpaseo/highlight": "*",
"@getpaseo/server": "0.1.32",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -34963,11 +34963,11 @@
},
"packages/cli": {
"name": "@getpaseo/cli",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.33",
"@getpaseo/server": "0.1.33",
"@getpaseo/relay": "0.1.32",
"@getpaseo/server": "0.1.32",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -35004,10 +35004,10 @@
},
"packages/desktop": {
"name": "@getpaseo/desktop",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"@getpaseo/cli": "0.1.33",
"@getpaseo/server": "0.1.33",
"@getpaseo/cli": "0.1.32",
"@getpaseo/server": "0.1.32",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
},
@@ -35040,7 +35040,7 @@
},
"packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.33",
"version": "0.1.32",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.9.4",
@@ -35241,7 +35241,7 @@
},
"packages/highlight": {
"name": "@getpaseo/highlight",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/cpp": "^1.1.5",
@@ -35267,7 +35267,7 @@
},
"packages/relay": {
"name": "@getpaseo/relay",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"base64-js": "^1.5.1",
"tweetnacl": "^1.0.3",
@@ -35283,13 +35283,13 @@
},
"packages/server": {
"name": "@getpaseo/server",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.33",
"@getpaseo/relay": "0.1.33",
"@getpaseo/highlight": "*",
"@getpaseo/relay": "0.1.32",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -35637,7 +35637,7 @@
},
"packages/website": {
"name": "@getpaseo/website",
"version": "0.1.33",
"version": "0.1.32",
"dependencies": {
"@cloudflare/vite-plugin": "^1.20.3",
"@cloudflare/workers-types": "^4.20260114.0",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "paseo",
"version": "0.1.33",
"version": "0.1.32",
"private": true,
"workspaces": [
"packages/expo-two-way-audio",
@@ -44,9 +44,9 @@
"version:all:patch": "npm version patch --include-workspace-root --message \"chore(release): cut %s\"",
"version:all:minor": "npm version minor --include-workspace-root --message \"chore(release): cut %s\"",
"version:all:major": "npm version major --include-workspace-root --message \"chore(release): cut %s\"",
"release:check": "npm run release:prepare && npm run typecheck --workspace=@getpaseo/highlight && npm run typecheck --workspace=@getpaseo/relay && npm run typecheck --workspace=@getpaseo/server && npm run typecheck --workspace=@getpaseo/cli && npm run build --workspace=@getpaseo/highlight && npm run build --workspace=@getpaseo/relay && npm run build --workspace=@getpaseo/server && npm run build --workspace=@getpaseo/cli && npm pack --dry-run --workspace=@getpaseo/highlight && npm pack --dry-run --workspace=@getpaseo/relay && npm pack --dry-run --workspace=@getpaseo/server && npm pack --dry-run --workspace=@getpaseo/cli",
"release:publish:dry-run": "npm publish --dry-run --workspace=@getpaseo/highlight --access public && npm publish --dry-run --workspace=@getpaseo/relay --access public && npm publish --dry-run --workspace=@getpaseo/server --access public && npm publish --dry-run --workspace=@getpaseo/cli --access public",
"release:publish": "npm publish --workspace=@getpaseo/highlight --access public && npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public",
"release:check": "npm run release:prepare && npm run typecheck --workspace=@getpaseo/relay && npm run typecheck --workspace=@getpaseo/server && npm run typecheck --workspace=@getpaseo/cli && npm run build --workspace=@getpaseo/relay && npm run build --workspace=@getpaseo/server && npm run build --workspace=@getpaseo/cli && npm pack --dry-run --workspace=@getpaseo/relay && npm pack --dry-run --workspace=@getpaseo/server && npm pack --dry-run --workspace=@getpaseo/cli",
"release:publish:dry-run": "npm publish --dry-run --workspace=@getpaseo/relay --access public && npm publish --dry-run --workspace=@getpaseo/server --access public && npm publish --dry-run --workspace=@getpaseo/cli --access public",
"release:publish": "npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public",
"release:push": "node scripts/push-current-release-tag.mjs",
"draft-release:push": "node scripts/push-current-release-tag.mjs --draft-release",
"draft-release:patch": "npm run version:all:patch && npm run release:check && npm run draft-release:push",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@getpaseo/app",
"main": "index.ts",
"version": "0.1.33",
"version": "0.1.32",
"private": true,
"scripts": {
"start": "expo start",
@@ -31,9 +31,9 @@
"@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.33",
"@getpaseo/highlight": "0.1.33",
"@getpaseo/server": "0.1.33",
"@getpaseo/expo-two-way-audio": "0.1.32",
"@getpaseo/highlight": "*",
"@getpaseo/server": "0.1.32",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -5,8 +5,8 @@ import {
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
useRef,
} from "react";
import { View, Text, Pressable, Platform, ActivityIndicator } from "react-native";
import Markdown from "react-native-markdown-display";
@@ -42,6 +42,7 @@ import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-typ
import type { Agent } from "@/contexts/session-context";
import { useSessionStore } from "@/stores/session-store";
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { DaemonClient } from "@server/client/daemon-client";
import { ToolCallDetailsContent } from "./tool-call-details";
import { QuestionFormCard } from "./question-form-card";
@@ -85,6 +86,7 @@ export interface AgentStreamViewProps {
agent: Agent;
streamItems: StreamItem[];
pendingPermissions: Map<string, PendingPermission>;
isPaneFocused?: boolean;
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
isAuthoritativeHistoryReady?: boolean;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
@@ -98,6 +100,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
agent,
streamItems,
pendingPermissions,
isPaneFocused = false,
routeBottomAnchorRequest = null,
isAuthoritativeHistoryReady = true,
onOpenWorkspaceFile,
@@ -208,6 +211,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
workspaceId,
],
);
const pendingPermissionItems = useMemo(
() => Array.from(pendingPermissions.values()).filter((perm) => perm.agentId === agentId),
[pendingPermissions, agentId],
);
const baseRenderModel = useMemo(() => {
return buildAgentStreamRenderModel({
@@ -490,18 +497,18 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
[getGapBetween, renderStreamItemContent, agent.status, streamRenderStrategy],
);
const pendingPermissionItems = useMemo(
() => Array.from(pendingPermissions.values()).filter((perm) => perm.agentId === agentId),
[pendingPermissions, agentId],
);
const showWorkingIndicator = agent.status === "running";
const renderModel = useMemo<AgentStreamRenderModel>(() => {
const pendingPermissionsNode =
pendingPermissionItems.length > 0 ? (
<View style={stylesheet.permissionsContainer}>
{pendingPermissionItems.map((permission) => (
<PermissionRequestCard key={permission.key} permission={permission} client={client} />
{pendingPermissionItems.map((permission, index) => (
<PermissionRequestCard
key={permission.key}
permission={permission}
client={client}
shortcutActive={isPaneFocused && index === 0}
/>
))}
</View>
) : null;
@@ -525,7 +532,14 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
workingIndicator: workingIndicatorNode,
},
};
}, [baseRenderModel, client, getGapBetween, pendingPermissionItems, showWorkingIndicator]);
}, [
baseRenderModel,
client,
getGapBetween,
isPaneFocused,
pendingPermissionItems,
showWorkingIndicator,
]);
const listEmptyComponent = useMemo(() => {
if (
@@ -727,9 +741,11 @@ function WorkingIndicator() {
function PermissionRequestCard({
permission,
client,
shortcutActive,
}: {
permission: PendingPermission;
client: DaemonClient | null;
shortcutActive: boolean;
}) {
const { theme } = useUnistyles();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
@@ -740,9 +756,6 @@ function PermissionRequestCard({
const description = request.description ?? "";
const planMarkdown = useMemo(() => {
if (!request) {
return undefined;
}
const planFromMetadata =
typeof request.metadata?.planText === "string" ? request.metadata.planText : undefined;
if (planFromMetadata) {
@@ -868,6 +881,7 @@ function PermissionRequestCard({
resetPermissionMutation();
setRespondingAction(null);
}, [permission.request.id, resetPermissionMutation]);
const handleResponse = useCallback(
(response: AgentPermissionResponse) => {
respondToPermission({
@@ -881,12 +895,52 @@ function PermissionRequestCard({
[permission.agentId, permission.request.id, respondToPermission],
);
const handleDeny = useCallback(() => {
setRespondingAction("deny");
handleResponse({
behavior: "deny",
message: "Denied by user",
});
}, [handleResponse]);
const handleAccept = useCallback(() => {
setRespondingAction("accept");
handleResponse({ behavior: "allow" });
}, [handleResponse]);
const handlePromptSelection = useCallback(
(action: { id: string; index?: number }): boolean => {
if (action.id !== "agent.prompt.select" || request.kind === "question" || isResponding) {
return false;
}
if (action.index === 1) {
handleDeny();
return true;
}
if (action.index === 2) {
handleAccept();
return true;
}
return false;
},
[handleAccept, handleDeny, isResponding, request.kind],
);
useKeyboardActionHandler({
handlerId: `agent-prompt-permission:${permission.key}`,
actions: ["agent.prompt.select"],
enabled: shortcutActive && request.kind !== "question" && !isResponding,
priority: 150,
handle: handlePromptSelection,
});
if (request.kind === "question") {
return (
<QuestionFormCard
permission={permission}
onRespond={handleResponse}
isResponding={isResponding}
shortcutActive={shortcutActive}
/>
);
}
@@ -958,13 +1012,7 @@ function PermissionRequestCard({
},
pressed ? permissionStyles.optionButtonPressed : null,
]}
onPress={() => {
setRespondingAction("deny");
handleResponse({
behavior: "deny",
message: "Denied by user",
});
}}
onPress={handleDeny}
disabled={isResponding}
>
{respondingAction === "deny" ? (
@@ -989,10 +1037,7 @@ function PermissionRequestCard({
},
pressed ? permissionStyles.optionButtonPressed : null,
]}
onPress={() => {
setRespondingAction("accept");
handleResponse({ behavior: "allow" });
}}
onPress={handleAccept}
disabled={isResponding}
>
{respondingAction === "accept" ? (
@@ -1,8 +1,9 @@
import { useState, useCallback } from "react";
import { useCallback, useState } from "react";
import { View, Text, TextInput, Pressable, ActivityIndicator, Platform } from "react-native";
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
import { Check, CircleHelp, X } from "lucide-react-native";
import type { PendingPermission } from "@/types/shared";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
interface QuestionOption {
@@ -17,6 +18,9 @@ interface Question {
multiSelect: boolean;
}
type QuestionSelections = Record<number, Set<number>>;
type QuestionOtherTexts = Record<number, string>;
function parseQuestions(input: unknown): Question[] | null {
if (
typeof input !== "object" ||
@@ -26,128 +30,334 @@ function parseQuestions(input: unknown): Question[] | null {
) {
return null;
}
const raw = (input as Record<string, unknown>).questions as unknown[];
const questions: Question[] = [];
for (const item of raw) {
if (typeof item !== "object" || item === null) return null;
const q = item as Record<string, unknown>;
if (typeof q.question !== "string" || typeof q.header !== "string") return null;
if (!Array.isArray(q.options)) return null;
if (typeof item !== "object" || item === null) {
return null;
}
const question = item as Record<string, unknown>;
if (typeof question.question !== "string" || typeof question.header !== "string") {
return null;
}
if (!Array.isArray(question.options)) {
return null;
}
const options: QuestionOption[] = [];
for (const opt of q.options as unknown[]) {
if (typeof opt !== "object" || opt === null) return null;
const o = opt as Record<string, unknown>;
if (typeof o.label !== "string") return null;
for (const option of question.options as unknown[]) {
if (typeof option !== "object" || option === null) {
return null;
}
const candidate = option as Record<string, unknown>;
if (typeof candidate.label !== "string") {
return null;
}
options.push({
label: o.label,
description: typeof o.description === "string" ? o.description : undefined,
label: candidate.label,
description: typeof candidate.description === "string" ? candidate.description : undefined,
});
}
questions.push({
question: q.question,
header: q.header,
question: question.question,
header: question.header,
options,
multiSelect: q.multiSelect === true,
multiSelect: question.multiSelect === true,
});
}
return questions.length > 0 ? questions : null;
}
function cloneSelections(input: QuestionSelections): QuestionSelections {
const next: QuestionSelections = {};
for (const [key, value] of Object.entries(input)) {
next[Number(key)] = new Set(value);
}
return next;
}
function isQuestionAnswered(input: {
selections: QuestionSelections;
otherTexts: QuestionOtherTexts;
questionIndex: number;
}): boolean {
const selected = input.selections[input.questionIndex];
const otherText = input.otherTexts[input.questionIndex]?.trim();
return (selected && selected.size > 0) || Boolean(otherText && otherText.length > 0);
}
function findFirstUnansweredQuestionIndex(input: {
questions: readonly Question[];
selections: QuestionSelections;
otherTexts: QuestionOtherTexts;
}): number | null {
for (let questionIndex = 0; questionIndex < input.questions.length; questionIndex += 1) {
if (
!isQuestionAnswered({
selections: input.selections,
otherTexts: input.otherTexts,
questionIndex,
})
) {
return questionIndex;
}
}
return null;
}
function areAllQuestionsAnswered(input: {
questions: readonly Question[];
selections: QuestionSelections;
otherTexts: QuestionOtherTexts;
}): boolean {
return input.questions.every((_, questionIndex) =>
isQuestionAnswered({
selections: input.selections,
otherTexts: input.otherTexts,
questionIndex,
}),
);
}
function buildQuestionAnswers(input: {
questions: readonly Question[];
selections: QuestionSelections;
otherTexts: QuestionOtherTexts;
}): Record<string, string> {
const answers: Record<string, string> = {};
for (let questionIndex = 0; questionIndex < input.questions.length; questionIndex += 1) {
const question = input.questions[questionIndex];
if (!question) {
continue;
}
const selected = input.selections[questionIndex];
const otherText = input.otherTexts[questionIndex]?.trim();
if (otherText && otherText.length > 0) {
answers[question.header] = otherText;
continue;
}
if (!selected || selected.size === 0) {
continue;
}
const labels = Array.from(selected)
.map((optionIndex) => question.options[optionIndex]?.label)
.filter((label): label is string => typeof label === "string");
if (labels.length > 0) {
answers[question.header] = labels.join(", ");
}
}
return answers;
}
function selectQuestionOption(input: {
questions: readonly Question[];
selections: QuestionSelections;
otherTexts: QuestionOtherTexts;
questionIndex: number;
optionIndex: number;
}): {
selections: QuestionSelections;
otherTexts: QuestionOtherTexts;
} {
const question = input.questions[input.questionIndex];
if (!question || !question.options[input.optionIndex]) {
return {
selections: input.selections,
otherTexts: input.otherTexts,
};
}
const selections = cloneSelections(input.selections);
const otherTexts = { ...input.otherTexts };
const current = selections[input.questionIndex] ?? new Set<number>();
const next = new Set(current);
if (question.multiSelect) {
if (next.has(input.optionIndex)) {
next.delete(input.optionIndex);
} else {
next.add(input.optionIndex);
}
} else if (next.has(input.optionIndex)) {
next.clear();
} else {
next.clear();
next.add(input.optionIndex);
}
selections[input.questionIndex] = next;
delete otherTexts[input.questionIndex];
return { selections, otherTexts };
}
interface QuestionFormCardProps {
permission: PendingPermission;
onRespond: (response: AgentPermissionResponse) => void;
isResponding: boolean;
shortcutActive: boolean;
}
const IS_WEB = Platform.OS === "web";
export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) {
export function QuestionFormCard({
permission,
onRespond,
isResponding,
shortcutActive,
}: QuestionFormCardProps) {
const { theme } = useUnistyles();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const questions = parseQuestions(permission.request.input);
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
const [otherTexts, setOtherTexts] = useState<Record<number, string>>({});
const [selections, setSelections] = useState<QuestionSelections>({});
const [otherTexts, setOtherTexts] = useState<QuestionOtherTexts>({});
const [respondingAction, setRespondingAction] = useState<"submit" | "dismiss" | null>(null);
const toggleOption = useCallback((qIndex: number, optIndex: number, multiSelect: boolean) => {
setSelections((prev) => {
const current = prev[qIndex] ?? new Set<number>();
const next = new Set(current);
if (multiSelect) {
if (next.has(optIndex)) {
next.delete(optIndex);
} else {
next.add(optIndex);
}
} else {
if (next.has(optIndex)) {
next.clear();
} else {
next.clear();
next.add(optIndex);
}
const submitResponses = useCallback(
(input: { selections: QuestionSelections; otherTexts: QuestionOtherTexts }) => {
if (!questions) {
return;
}
return { ...prev, [qIndex]: next };
});
setOtherTexts((prev) => {
if (!prev[qIndex]) return prev;
const next = { ...prev };
delete next[qIndex];
return next;
});
}, []);
const setOtherText = useCallback((qIndex: number, text: string) => {
setOtherTexts((prev) => ({ ...prev, [qIndex]: text }));
if (text.length > 0) {
setSelections((prev) => {
if (!prev[qIndex] || prev[qIndex].size === 0) return prev;
return { ...prev, [qIndex]: new Set<number>() };
setRespondingAction("submit");
onRespond({
behavior: "allow",
updatedInput: {
...permission.request.input,
answers: buildQuestionAnswers({
questions,
selections: input.selections,
otherTexts: input.otherTexts,
}),
},
});
},
[onRespond, permission.request.input, questions],
);
const toggleOption = useCallback(
(input: { questionIndex: number; optionIndex: number }) => {
if (!questions) {
return;
}
const next = selectQuestionOption({
questions,
selections,
otherTexts,
questionIndex: input.questionIndex,
optionIndex: input.optionIndex,
});
setSelections(next.selections);
setOtherTexts(next.otherTexts);
},
[otherTexts, questions, selections],
);
const setOtherText = useCallback((input: { questionIndex: number; text: string }) => {
setOtherTexts((previous) => ({ ...previous, [input.questionIndex]: input.text }));
if (input.text.length > 0) {
setSelections((previous) => {
if (!previous[input.questionIndex] || previous[input.questionIndex]?.size === 0) {
return previous;
}
return { ...previous, [input.questionIndex]: new Set<number>() };
});
}
}, []);
if (!questions) {
return null;
}
const handleSubmit = useCallback(() => {
submitResponses({ selections, otherTexts });
}, [otherTexts, selections, submitResponses]);
const allAnswered = questions.every((_, qIndex) => {
const selected = selections[qIndex];
const otherText = otherTexts[qIndex]?.trim();
return (selected && selected.size > 0) || (otherText && otherText.length > 0);
});
function handleSubmit() {
setRespondingAction("submit");
const answers: Record<string, string> = {};
for (let i = 0; i < questions!.length; i++) {
const q = questions![i];
const selected = selections[i];
const otherText = otherTexts[i]?.trim();
if (otherText && otherText.length > 0) {
answers[q.header] = otherText;
} else if (selected && selected.size > 0) {
const labels = Array.from(selected).map((idx) => q.options[idx].label);
answers[q.header] = labels.join(", ");
}
}
onRespond({
behavior: "allow",
updatedInput: { ...permission.request.input, answers },
});
}
function handleDeny() {
const handleDeny = useCallback(() => {
setRespondingAction("dismiss");
onRespond({
behavior: "deny",
message: "Dismissed by user",
});
}, [onRespond]);
const handlePromptSelection = useCallback(
(action: { id: string; index?: number }): boolean => {
if (action.id !== "agent.prompt.select" || isResponding || !questions) {
return false;
}
const questionIndex = findFirstUnansweredQuestionIndex({
questions,
selections,
otherTexts,
});
if (questionIndex === null) {
return false;
}
const optionIndex = (action.index ?? 0) - 1;
const question = questions[questionIndex];
if (!question?.options[optionIndex]) {
return false;
}
const next = selectQuestionOption({
questions,
selections,
otherTexts,
questionIndex,
optionIndex,
});
setSelections(next.selections);
setOtherTexts(next.otherTexts);
const shouldAutoSubmit =
areAllQuestionsAnswered({
questions,
selections: next.selections,
otherTexts: next.otherTexts,
}) && questions.every((candidate) => candidate.multiSelect !== true);
if (shouldAutoSubmit) {
submitResponses(next);
}
return true;
},
[isResponding, otherTexts, questions, selections, submitResponses],
);
useKeyboardActionHandler({
handlerId: `agent-prompt-question:${permission.key}`,
actions: ["agent.prompt.select"],
enabled: shortcutActive && !isResponding && questions !== null,
priority: 150,
handle: handlePromptSelection,
});
if (!questions) {
return null;
}
const allAnswered = areAllQuestionsAnswered({
questions,
selections,
otherTexts,
});
return (
<View
style={[
@@ -158,24 +368,24 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
},
]}
>
{questions.map((q, qIndex) => {
const selected = selections[qIndex] ?? new Set<number>();
const otherText = otherTexts[qIndex] ?? "";
{questions.map((question, questionIndex) => {
const selected = selections[questionIndex] ?? new Set<number>();
const otherText = otherTexts[questionIndex] ?? "";
return (
<View key={qIndex} style={styles.questionBlock}>
<View key={questionIndex} style={styles.questionBlock}>
<View style={styles.questionHeader}>
<Text style={[styles.questionText, { color: theme.colors.foreground }]}>
{q.question}
{question.question}
</Text>
<CircleHelp size={14} color={theme.colors.foregroundMuted} />
</View>
<View style={styles.optionsWrap}>
{q.options.map((opt, optIndex) => {
const isSelected = selected.has(optIndex);
{question.options.map((option, optionIndex) => {
const isSelected = selected.has(optionIndex);
return (
<Pressable
key={optIndex}
key={optionIndex}
style={({ pressed, hovered = false }) => [
styles.optionItem,
(hovered || isSelected) && {
@@ -183,22 +393,27 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
},
pressed && styles.optionItemPressed,
]}
onPress={() => toggleOption(qIndex, optIndex, q.multiSelect)}
onPress={() =>
toggleOption({
questionIndex,
optionIndex,
})
}
disabled={isResponding}
>
<View style={styles.optionItemContent}>
<View style={styles.optionTextBlock}>
<Text style={[styles.optionLabel, { color: theme.colors.foreground }]}>
{opt.label}
{option.label}
</Text>
{opt.description ? (
{option.description ? (
<Text
style={[
styles.optionDescription,
{ color: theme.colors.foregroundMuted },
]}
>
{opt.description}
{option.description}
</Text>
) : null}
</View>
@@ -222,12 +437,21 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
backgroundColor: theme.colors.surface2,
},
// @ts-expect-error - outlineStyle is web-only
IS_WEB && { outlineStyle: "none", outlineWidth: 0, outlineColor: "transparent" },
IS_WEB && {
outlineStyle: "none",
outlineWidth: 0,
outlineColor: "transparent",
},
]}
placeholder="Other..."
placeholderTextColor={theme.colors.foregroundMuted}
value={otherText}
onChangeText={(text) => setOtherText(qIndex, text)}
onChangeText={(text) =>
setOtherText({
questionIndex,
text,
})
}
editable={!isResponding}
/>
</View>
@@ -14,7 +14,6 @@ export function DesktopPermissionsSection() {
isRefreshing,
requestingPermission,
isSendingTestNotification,
testNotificationError,
refreshPermissions,
requestPermission,
sendTestNotification,
@@ -59,11 +58,6 @@ export function DesktopPermissionsSection() {
void sendTestNotification();
}}
/>
{testNotificationError ? (
<Text style={[styles.errorText, { color: theme.colors.destructive }]}>
{testNotificationError}
</Text>
) : null}
<DesktopPermissionRow
title="Microphone"
showBorder
@@ -86,9 +80,4 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2],
marginBottom: theme.spacing[3],
},
errorText: {
fontSize: theme.fontSize.xs,
paddingHorizontal: theme.spacing[4],
paddingBottom: theme.spacing[2],
},
}));
@@ -14,7 +14,6 @@ export interface UseDesktopPermissionsReturn {
isRefreshing: boolean;
requestingPermission: DesktopPermissionKind | null;
isSendingTestNotification: boolean;
testNotificationError: string | null;
refreshPermissions: () => Promise<void>;
requestPermission: (kind: DesktopPermissionKind) => Promise<void>;
sendTestNotification: () => Promise<void>;
@@ -113,25 +112,22 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
[isDesktop, refreshPermissions],
);
const [testNotificationError, setTestNotificationError] = useState<string | null>(null);
const sendTestNotification = useCallback(async () => {
if (!isDesktop) {
return;
}
setIsSendingTestNotification(true);
setTestNotificationError(null);
try {
const sent = await sendOsNotification({
title: "Paseo notification test",
body: "If you can see this, desktop notifications work.",
});
if (!sent) {
setTestNotificationError("Notification was not delivered. Check System Settings > Notifications.");
console.warn("[Settings] Desktop test notification was not delivered");
}
} catch (error) {
setTestNotificationError("Failed to send notification.");
console.error("[Settings] Failed to send desktop test notification", error);
} finally {
if (isMountedRef.current) {
setIsSendingTestNotification(false);
@@ -153,7 +149,6 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
isRefreshing,
requestingPermission,
isSendingTestNotification,
testNotificationError,
refreshPermissions,
requestPermission,
sendTestNotification,
@@ -161,6 +161,15 @@ export function useKeyboardShortcuts({
switch (input.action) {
case "agent.new":
return openProjectPicker();
case "agent.prompt.select":
if (!input.payload || typeof input.payload !== "object" || !("index" in input.payload)) {
return false;
}
return keyboardActionDispatcher.dispatch({
id: "agent.prompt.select",
scope: "workspace",
index: input.payload.index,
});
case "workspace.tab.new":
return keyboardActionDispatcher.dispatch({
id: "workspace.tab.new",
+1
View File
@@ -16,6 +16,7 @@ export type MessageInputKeyboardActionKind =
export type KeyboardActionId =
| "agent.new"
| "agent.prompt.select"
| "workspace.tab.new"
| "workspace.tab.close.current"
| "workspace.tab.navigate.index"
@@ -1,6 +1,7 @@
export type KeyboardActionScope = "global" | "message-input" | "sidebar" | "workspace";
export type KeyboardActionId =
| "agent.prompt.select"
| "message-input.focus"
| "message-input.dictation-toggle"
| "message-input.dictation-cancel"
@@ -26,6 +27,7 @@ export type KeyboardActionId =
| "worktree.archive";
export type KeyboardActionDefinition =
| { id: "agent.prompt.select"; scope: KeyboardActionScope; index: number }
| { id: "message-input.focus"; scope: KeyboardActionScope }
| { id: "message-input.dictation-toggle"; scope: KeyboardActionScope }
| { id: "message-input.dictation-cancel"; scope: KeyboardActionScope }
@@ -261,9 +261,15 @@ describe("keyboard-shortcuts", () => {
action: "sidebar.toggle.left",
},
{
name: "keeps Mod+. as sidebar toggle fallback",
name: "matches Mod+. to toggle both sidebars on non-mac",
event: { key: ".", code: "Period", ctrlKey: true },
context: { isMac: false },
action: "sidebar.toggle.both",
},
{
name: "matches Ctrl+B sidebar toggle on non-mac",
event: { key: "b", code: "KeyB", ctrlKey: true },
context: { isMac: false },
action: "sidebar.toggle.left",
},
{
@@ -289,6 +295,13 @@ describe("keyboard-shortcuts", () => {
preventDefault: false,
stopPropagation: false,
},
{
name: "routes number keys to prompt selection outside editable scopes",
event: { key: "2", code: "Digit2" },
context: { focusScope: "other" },
action: "agent.prompt.select",
payload: { index: 2 },
},
];
it.each(matchingCases)("$name", ({
@@ -344,11 +357,6 @@ describe("keyboard-shortcuts", () => {
event: { key: "k", code: "KeyK", ctrlKey: true },
context: { isMac: false, focusScope: "terminal" },
},
{
name: "does not bind Ctrl+B on non-mac",
event: { key: "b", code: "KeyB", ctrlKey: true },
context: { isMac: false },
},
{
name: "does not route message-input actions when terminal is focused",
event: { key: "d", code: "KeyD", metaKey: true },
@@ -364,6 +372,11 @@ describe("keyboard-shortcuts", () => {
event: { key: " ", code: "Space" },
context: { focusScope: "message-input" },
},
{
name: "does not route prompt selection while typing in the message input",
event: { key: "1", code: "Digit1" },
context: { focusScope: "message-input" },
},
];
it.each(nonMatchingCases)("$name", ({ event, context }) => {
@@ -467,6 +480,9 @@ describe("keyboard-shortcut help sections", () => {
"workspace-tab-close-current": ["meta", "W"],
"workspace-pane-split-right": ["mod", "\\"],
"workspace-pane-close": ["mod", "shift", "W"],
"agent-prompt-select-1": ["1"],
"agent-prompt-select-2": ["2"],
"agent-prompt-select-3": ["3"],
},
},
{
@@ -477,10 +493,11 @@ describe("keyboard-shortcut help sections", () => {
},
},
{
name: "uses mod+period as non-mac left sidebar shortcut",
name: "uses mod+b and mod+period for non-mac sidebar shortcuts",
context: { isMac: false, isDesktop: false },
expectedKeys: {
"toggle-left-sidebar": ["mod", "."],
"toggle-left-sidebar": ["mod", "B"],
"toggle-both-sidebars": ["mod", "."],
},
},
];
@@ -814,6 +814,48 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
keys: ["Esc"],
},
},
{
id: "agent-prompt-select-1",
action: "agent.prompt.select",
combo: "1",
repeat: false,
when: { focusScope: "other", commandCenter: false },
payload: { type: "index" },
help: {
id: "agent-prompt-select-1",
section: "agent-input",
label: "Select prompt option 1",
keys: ["1"],
},
},
{
id: "agent-prompt-select-2",
action: "agent.prompt.select",
combo: "2",
repeat: false,
when: { focusScope: "other", commandCenter: false },
payload: { type: "index" },
help: {
id: "agent-prompt-select-2",
section: "agent-input",
label: "Select prompt option 2",
keys: ["2"],
},
},
{
id: "agent-prompt-select-3",
action: "agent.prompt.select",
combo: "3",
repeat: false,
when: { focusScope: "other", commandCenter: false },
payload: { type: "index" },
help: {
id: "agent-prompt-select-3",
section: "agent-input",
label: "Select prompt option 3",
keys: ["3"],
},
},
{
id: "message-input-send-enter",
action: "message-input.action",
+2
View File
@@ -322,6 +322,7 @@ function AgentPanelBody({
attentionReason: agent?.attentionReason,
isScreenFocused: isPaneFocused,
});
useEffect(() => {
clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur;
}, [attentionController.clearOnAgentBlur]);
@@ -796,6 +797,7 @@ function AgentPanelBody({
agent={effectiveAgent}
streamItems={shouldUseOptimisticStream ? mergedStreamItems : streamItems}
pendingPermissions={pendingPermissions}
isPaneFocused={isPaneFocused}
routeBottomAnchorRequest={routeBottomAnchorRequest}
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
onOpenWorkspaceFile={onOpenWorkspaceFile}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.1.33",
"version": "0.1.32",
"description": "Paseo CLI - control your AI coding agents from the command line",
"type": "module",
"files": [
@@ -24,8 +24,8 @@
},
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.33",
"@getpaseo/server": "0.1.33",
"@getpaseo/relay": "0.1.32",
"@getpaseo/server": "0.1.32",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -6,7 +6,5 @@
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
@@ -6,7 +6,5 @@
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/desktop",
"version": "0.1.33",
"version": "0.1.32",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"main": "dist/main.js",
@@ -12,8 +12,8 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@getpaseo/cli": "0.1.33",
"@getpaseo/server": "0.1.33",
"@getpaseo/cli": "0.1.32",
"@getpaseo/server": "0.1.32",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
},
+1 -25
View File
@@ -168,30 +168,6 @@ export function resolveCliEntrypoint(): NodeEntrypointSpec {
};
}
function resolveNodeExecPath(): string {
if (app.isPackaged && process.platform === "darwin") {
const marker = ".app/Contents/MacOS/";
const markerIndex = process.execPath.indexOf(marker);
if (markerIndex !== -1) {
const bundleRoot = process.execPath.substring(0, markerIndex + ".app".length);
const name = path.basename(process.execPath);
const helperPath = path.join(
bundleRoot,
"Contents",
"Frameworks",
`${name} Helper.app`,
"Contents",
"MacOS",
`${name} Helper`,
);
if (existsSync(helperPath)) {
return helperPath;
}
}
}
return process.execPath;
}
export function createNodeEntrypointInvocation(input: {
entrypoint: NodeEntrypointSpec;
argvMode: NodeEntrypointArgvMode;
@@ -199,7 +175,7 @@ export function createNodeEntrypointInvocation(input: {
baseEnv: NodeJS.ProcessEnv;
}): NodeEntrypointInvocation {
return createSharedNodeEntrypointInvocation({
execPath: resolveNodeExecPath(),
execPath: process.execPath,
isPackaged: app.isPackaged,
packagedRunnerPath: app.isPackaged
? assertPathExists({
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.33",
"version": "0.1.32",
"description": "Native module for two way audio streaming",
"main": "build/index.js",
"types": "build/index.d.ts",
+3 -8
View File
@@ -1,15 +1,10 @@
{
"name": "@getpaseo/highlight",
"version": "0.1.33",
"version": "0.1.32",
"private": true,
"type": "module",
"publishConfig": {
"access": "public"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"types": "./src/index.ts",
"scripts": {
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
"prepack": "npm run build",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/relay",
"version": "0.1.33",
"version": "0.1.32",
"description": "Paseo relay for bridging daemon and client connections",
"type": "module",
"publishConfig": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/server",
"version": "0.1.33",
"version": "0.1.32",
"description": "Paseo backend server",
"type": "module",
"publishConfig": {
@@ -64,8 +64,8 @@
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.33",
"@getpaseo/relay": "0.1.33",
"@getpaseo/highlight": "*",
"@getpaseo/relay": "0.1.32",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -1611,7 +1611,6 @@ class ClaudeAgentSession implements AgentSession {
this.activeTurnPromise = null;
this.sidechainTracker.clear();
this.input?.end();
this.query?.close();
await this.awaitWithTimeout(this.query?.interrupt?.(), "close query interrupt");
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
this.query = null;
@@ -1878,7 +1877,6 @@ class ClaudeAgentSession implements AgentSession {
if (this.queryRestartNeeded && this.query) {
this.input?.end();
this.query.close();
try {
await this.query.return?.();
} catch {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/website",
"version": "0.1.33",
"version": "0.1.32",
"private": true,
"type": "module",
"scripts": {
+180 -270
View File
@@ -1,18 +1,12 @@
---
name: paseo-loop
description: Run an agent loop until an exit condition is met. Use when the user says "loop", "babysit", "keep trying until", "check every X", "watch", or wants iterative autonomous execution.
description: Run a task in a loop until an exit condition is met. Use when the user says "loop", "loop this", "keep trying until", "babysit", "poll", or wants iterative autonomous execution.
user-invocable: true
---
# Paseo Loop Skill
# Loop Skill
You are setting up `/paseo-loop` as a flexible loop primitive.
Think of it like a `while` loop:
- each iteration sends work to a target
- an optional verifier judges completion
- optional sleep schedules the next iteration
- hard caps stop the loop if it runs too long
You are setting up an autonomous loop. An agent runs repeatedly until an exit condition is met.
**User's arguments:** $ARGUMENTS
@@ -20,319 +14,235 @@ Think of it like a `while` loop:
## Prerequisites
Load the **Paseo skill** first. It contains the CLI reference for `paseo run`, `paseo send`, `paseo wait`, and related commands.
Load the **Paseo skill** first — it contains the CLI reference for all agent commands.
## Core Model
## What Is a Loop
Every loop has these parts:
A loop runs an agent repeatedly until it's done. There are two modes:
1. **Target**: who acts each iteration
2. **Prompt**: what the target does each iteration
3. **Verifier**: optional independent judge
4. **Sleep**: optional pause between iterations
5. **Stop conditions**: max iterations and/or max total runtime
### Self-terminating (no verifier)
### Target
A single worker agent runs each iteration. It returns `{ done: boolean, reason: string }` via structured output. The loop exits when `done` is true.
There are two target modes:
### Worker + Verifier
#### `self`
A worker agent runs each iteration (detached, no structured output). After the worker finishes, a separate verifier agent evaluates the verification prompt and returns `{ done: boolean, reason: string }`. The loop exits when `done` is true.
The loop sends the prompt back to the current agent each iteration. The current agent is identified by `$PASEO_AGENT_ID`.
### Feedback between iterations
Use `self` when:
- the current agent should keep ownership of the task
- the user says "babysit", "watch", "check every X", "poll", or "monitor"
- waking the same agent is cheaper and more natural than starting fresh
#### `new-agent`
The loop launches a fresh worker agent each iteration.
Use `new-agent` when:
- the user says "create a loop", "spin up a loop", or "launch a codex agent"
- the task benefits from fresh context per iteration
- you want isolated retries, often in a worktree
### Verifier
The verifier is orthogonal to the target:
- no verifier: the target decides whether the loop is done
- verifier present: the verifier decides whether the loop is done
If a verifier exists, it is the source of truth for loop completion.
## Defaults by User Intent
Infer defaults from the user's phrasing:
### Babysit / watch / check every X
Default to:
- `target=self`
- `sleep=<explicit value or sensible default>`
- no verifier unless the user asks for independent verification
- `max-time=1h` if the user gives no bound and the task could run indefinitely
### Ensure X is done
Default to:
- `target=self`
- verifier enabled
- no sleep unless the task is waiting on an external system
Reason: by default, do not trust the same agent to judge its own completion when the user is asking for assurance.
### Create a loop / launch a loop / loop a codex agent
Default to:
- `target=new-agent`
- verifier enabled when success criteria need independent judgment
- worktree enabled when code changes are involved
## Stop Conditions
Support both:
- `--max-iterations N`
- `--max-time DURATION`
Use at least one bound for open-ended or polling loops when the user does not specify one.
## Feedback Between Iterations
Each iteration should receive the previous result as `<previous-iteration-result>`.
This applies in all cases:
- if there is a verifier, feed back the verifier's `reason`
- otherwise feed back the target's `reason`
In both modes, the `reason` from the previous iteration is fed back to the next worker as `<previous-iteration-result>`. This gives the worker context about what happened last time.
## Live Steering
Each loop persists state in:
Each loop run persists state in:
```text
~/.paseo/loops/<loop-id>/
target-prompt.md # prompt sent to self or worker (live-editable)
verifier-prompt.md # verifier prompt (live-editable, optional)
last_reason.md # latest reason used for feedback
history.log # per-iteration records
worker-prompt.md # worker prompt (live-editable)
verifier-prompt.md # verifier prompt (live-editable, only when verifier is used)
last_reason.md # latest iteration result
history.log # per-iteration records
```
Edits to prompt files are picked up on the next iteration without restarting the loop.
## Script Interface
## Parsing Arguments
The loop is implemented at:
Parse `$ARGUMENTS` to determine:
```bash
skills/paseo-loop/bin/loop.sh
1. **Worker prompt** — what the worker does each iteration
2. **Verifier prompt** (optional) — what the verifier checks after each worker iteration
3. **Worker** — which agent does the work (default: Codex)
4. **Verifier** — which agent verifies (default: Claude sonnet)
5. **Sleep** (optional) — delay between iterations
6. **Name** — a short name for tracking
7. **Max iterations** — safety cap (default: unlimited)
8. **Archive** — whether to archive agents after each iteration
9. **Worktree** — whether to run in an isolated git worktree
### Examples
```
/loop babysit PR #42 until CI is green, check every 5 minutes
→ worker-prompt: "Check the CI status of PR #42 using `gh pr checks 42`. Report done when ALL checks
have passed (no pending, no failures). If any checks are still running or have failed, report not
done and list which checks are pending or failing."
No verifier (self-terminating: the worker inspects CI and reports done when green)
sleep: 5m
archive: yes
name: babysit-pr-42
/loop implement the auth refactor from the plan in /tmp/plan.md
→ worker-prompt-file: /tmp/plan.md
verifier-prompt: "Verify every step of the plan was implemented. Check file changes, types, and
run `npm run typecheck` and `npm test`. Report each criterion individually with evidence."
name: auth-refactor
worktree: auth-refactor
/loop run the test suite until it passes
→ worker-prompt: "Run `npm test`. If any tests fail, read the failure output, investigate the root
cause in the source code, and fix it. Report done when `npm test` exits with code 0 and all
tests pass. Report not done if any test still fails, and explain which tests failed and why."
No verifier (self-terminating: worker reports done when tests pass)
name: fix-tests
/loop watch error rates after deploy, check every 10 minutes
→ worker-prompt: "Check the error rate for the canary deployment by running `./scripts/check-canary.sh`.
Report done when the error rate has been below 0.1% for at least 2 consecutive checks. Report not
done with the current error rate and trend."
No verifier (self-terminating: worker reports done when error rate is stable)
sleep: 10m
max-iterations: 30
archive: yes
name: canary-watch
```
### Flags
## Using the Script
The loop is implemented as a bash script at `~/.claude/skills/paseo-loop/bin/loop.sh`.
```bash
# Self-terminating: worker checks PR CI and reports done when all checks pass
~/.claude/skills/paseo-loop/bin/loop.sh \
--worker-prompt "Check CI status of PR #42 using gh pr checks 42. Report done when ALL checks pass. Report not done with a list of pending/failing checks." \
--name "babysit-pr" \
--sleep 5m \
--archive
# Worker + verifier: worker implements, verifier independently checks
~/.claude/skills/paseo-loop/bin/loop.sh \
--worker-prompt "Implement the auth refactor: ..." \
--verifier-prompt "Verify the auth refactor is complete. Run npm run typecheck and npm test. Check that all file changes match the plan. Report each criterion with evidence." \
--name "auth-refactor" \
--worktree "auth-refactor"
```
### Arguments
| Flag | Required | Default | Description |
|---|---|---|---|
| `--target self|new-agent` | No | inferred / `new-agent` in raw script | Who acts each iteration |
| `--target-prompt` | Yes* | — | Prompt given to the target each iteration |
| `--target-prompt-file` | Yes* | — | Read the target prompt from a file |
| `--worker` | No | `codex` | Worker agent for `new-agent` loops |
| `--agent-id` | No | `$PASEO_AGENT_ID` | Existing agent id for `self` loops |
| `--verifier-prompt` | No* | — | Prompt for an independent verifier |
| `--verifier-prompt-file` | No* | — | Read verifier prompt from a file |
| `--verifier` | No | `claude/sonnet` | Verifier agent |
| `--name` | Yes | — | Name prefix for loop tracking |
| `--sleep` | No | — | Delay between iterations |
| `--max-iterations` | No | unlimited | Hard cap on iteration count |
| `--max-time` | No | unlimited | Hard cap on total wall-clock runtime |
| `--archive` | No | off | Archive newly created agents after iteration |
| `--worktree` | No | — | Worktree name for `new-agent` loops |
| `--worker-prompt` | Yes* | — | Prompt given to the worker each iteration |
| `--worker-prompt-file` | Yes* | — | Read the worker prompt from a file |
| `--worker` | No | `codex` | Worker agent (`provider/model`, e.g. `codex/gpt-5.4`, `claude/sonnet`) |
| `--verifier-prompt` | No* | — | Verification prompt for a separate verifier agent |
| `--verifier-prompt-file` | No* | — | Read the verifier prompt from a file |
| `--verifier` | No | `claude/sonnet` | Verifier agent (`provider/model`) |
| `--name` | Yes | — | Name prefix for agents |
| `--sleep` | No | — | Delay between iterations (e.g. `30s`, `5m`, `1h`) |
| `--max-iterations` | No | unlimited | Safety cap on iterations |
| `--archive` | No | off | Archive agents after each iteration |
| `--worktree` | No | — | Worktree name. Created on first use, reused after. |
| `--thinking` | No | `medium` | Thinking level for worker |
\* Provide exactly one of `--target-prompt` or `--target-prompt-file`. Provide at most one of `--verifier-prompt` or `--verifier-prompt-file`.
\* Provide exactly one of `--worker-prompt` or `--worker-prompt-file`. Provide at most one of `--verifier-prompt` or `--verifier-prompt-file`.
## Behavior Rules
### Behavior by parameters
### `target=self`, no verifier
| Parameters | Mode | Use case |
|---|---|---|
| `--worker-prompt` only | Self-terminating worker | Worker does work and decides when done |
| `--worker-prompt` + `--verifier-prompt` | Worker + verifier | Worker implements, verifier independently checks |
| `--worker-prompt` + `--sleep` | Polling with self-termination | Periodic check until a condition is met |
| `--worker-prompt` + `--verifier-prompt` + `--sleep` | Periodic work + verifier | Periodic work with independent verification |
The current agent must return structured JSON:
### Agent Naming
```json
{ "done": true, "reason": "..." }
Without verifier: agents are named `{name}-{N}`:
```
babysit-1 # First iteration
babysit-2 # Second iteration
```
Use this for:
- babysitting a PR
- scheduled status checks
- monitoring a deployment
- repeating an objective task the current agent can judge itself
### `target=self`, verifier enabled
The current agent does the work. A separate verifier decides whether the loop is done.
Use this for:
- "ensure X is done"
- "work until the tests are passing"
- cases where the user wants independent judgment but keeping the same agent is still the right execution model
### `target=new-agent`, no verifier
A fresh worker launches each iteration and must return structured JSON itself.
Use this when:
- the task is naturally self-judging
- you want fresh context each retry
### `target=new-agent`, verifier enabled
A fresh worker launches each iteration. After it finishes, a separate verifier judges completion.
Use this for:
- implementation loops
- fix-and-verify cycles
- loops the user explicitly asks you to create
## Examples
### Babysit the PR
Interpretation:
- `target=self`
- `sleep=2m`
- no verifier unless requested
- reasonable `max-time` if none given
Example:
```bash
skills/paseo-loop/bin/loop.sh \
--target self \
--target-prompt "Check PR #42. Review CI, review comments, and branch status. Fix issues as they arise. Return JSON with done=true only when the PR is fully green and ready." \
--sleep 2m \
--max-time 1h \
--name babysit-pr-42
With verifier: workers are `{name}-{N}`, verifiers are `{name}-verify-{N}`:
```
feat-1 # First worker
feat-verify-1 # First verifier
feat-2 # Second worker (with previous result context)
feat-verify-2 # Second verifier
```
### Launch a Codex agent to babysit the PR
### Worktree Support
Interpretation:
- `target=new-agent`
- `worker=codex`
- `sleep=2m`
- usually archive
When `--worktree` is passed, all agents run in the same git worktree. The worktree is created on first launch and reused for all subsequent agents.
```bash
skills/paseo-loop/bin/loop.sh \
--target new-agent \
--worker codex \
--target-prompt "Check PR #42. Review CI, review comments, and branch status. Fix issues as they arise. Return JSON with done=true only when the PR is fully green and ready." \
--sleep 2m \
--max-time 1h \
--archive \
--name babysit-pr-42
```
### Work until the tests are passing
Interpretation:
- `target=self`
- verifier enabled
- no sleep
```bash
skills/paseo-loop/bin/loop.sh \
--target self \
--target-prompt "Run the test suite, investigate failures, and fix the code. Stop after you have made a coherent attempt for this iteration." \
--verifier-prompt "Run the relevant test suite. Return done=true only if all tests pass. Reason must cite the exact command and outcome." \
--max-iterations 10 \
--name fix-tests
```
### Create a loop to fix the tests
Interpretation:
- `target=new-agent`
- verifier enabled
- often use a worktree
```bash
skills/paseo-loop/bin/loop.sh \
--target new-agent \
--worker codex \
--target-prompt "Run the test suite, investigate failures, fix the code, and leave the repo in a clean verifiable state for this iteration." \
--verifier-prompt "Run the relevant test suite. Return done=true only if all tests pass. Reason must cite the exact command and outcome." \
--worktree fix-tests \
--max-iterations 10 \
--name fix-tests
```
### Loop a Codex agent to complete issue 456 in a worktree
Interpretation:
- `target=new-agent`
- worker codex
- verifier enabled
- worktree enabled
```bash
skills/paseo-loop/bin/loop.sh \
--target new-agent \
--worker codex \
--target-prompt "Implement issue #456 in this repo. Use the issue description and surrounding code as context. Make incremental progress each iteration and leave clear evidence of what changed." \
--verifier-prompt "Verify issue #456 is complete. Check changed files, run typecheck and relevant tests, and return done=true only if the implementation meets the issue requirements with evidence." \
--worktree issue-456 \
--max-iterations 8 \
--max-time 2h \
--name issue-456
~/.claude/skills/paseo-loop/bin/loop.sh \
--worker-prompt "Implement the feature..." \
--verifier-prompt "Verify the feature works and typecheck passes" \
--name "feature-x" \
--worktree "feature-x"
```
## Your Job
1. Understand the user's intent from the conversation and `$ARGUMENTS`
2. Decide `target=self` or `target=new-agent`
3. Decide whether a verifier is needed
4. Write the target prompt so it is self-contained for the selected target
5. Write the verifier prompt, if used, so it is factual and evidence-based
6. Choose sleep only when the task is naturally scheduled or polling
7. Add sensible stop conditions
8. Choose worker / verifier agents
9. Choose a short name
10. Run `skills/paseo-loop/bin/loop.sh` with the final arguments
1. **Understand the task** from the conversation context and `$ARGUMENTS`
2. **Decide the mode** — does this need a separate verifier, or can the worker self-terminate?
3. **Write the worker prompt** — what the worker does each iteration. Must be self-contained (the agent has zero prior context).
4. **Write the verifier prompt** (if needed) — what the verifier checks. Should be factual and verifiable.
5. **Choose sleep** — if the task is polling/monitoring, add a sleep duration
6. **Choose archive** — use `--archive` for long-running or polling loops to keep the agent list clean
7. **Choose agents** — default: `codex` worker + `claude/sonnet` verifier
8. **Choose a name** — short, descriptive
9. **Run the script** — call `loop.sh` with all the arguments
## Prompt Writing Rules
### When to use a verifier vs self-terminating
### Target prompt
Use a verifier (`--verifier-prompt`) when:
- The worker's job is to implement something and you want independent verification
- You want the verification done by a different agent than the worker
- The worker should focus on doing work, not on judging its own work
The target prompt must be:
- self-contained
- concrete about commands, files, branches, tests, PRs, or systems to inspect
- explicit about what counts as progress this iteration
Use self-terminating (no verifier) when:
- The worker is checking/polling an external condition (CI status, deployment health)
- The worker can objectively determine when it's done (tests pass, file exists)
- You want a single agent doing both the work and the evaluation
If there is no verifier, the target prompt must instruct the target to end with strict JSON matching:
### Writing a Good Worker Prompt
```json
{ "done": boolean, "reason": "string" }
```
The worker prompt is what the agent receives each iteration. It must be:
### Verifier prompt
1. **Self-contained** — The agent starts with zero context. Everything needed is in the prompt.
2. **Specific** — Name files, functions, types, URLs. Be concrete.
3. **Action-oriented** — Tell the agent what to do, not what to think about.
The verifier prompt should:
- check facts, not offer fixes
- cite commands, outputs, or file evidence
- return strict JSON matching:
### Writing a Good Verifier Prompt
```json
{ "done": boolean, "reason": "string" }
```
The verifier prompt defines what the verifier checks after each worker iteration. It can range from strict factual checks to qualitative code review:
## Skill Stacking
**Factual / objective checks:**
- "Run `npm test` and `npm run typecheck`. Report done only if both pass with zero failures."
- "Check that PR #42 has all CI checks green and no unresolved review comments."
- "Verify the API responds to `GET /health` with status 200 within 500ms."
The target prompt can instruct the worker to use other skills:
**Code quality / style checks:**
- "Review the changes for DRY violations. Report done when there is no duplicated logic across files."
- "Check that the implementation does not over-engineer. No unnecessary abstractions, no premature generalization, no feature flags for single-use code."
- "Verify the code follows the project's conventions: functional style, no classes, explicit types, no `any`."
**Performance / resource checks:**
- "Run the benchmark suite. Report done when p99 latency is under 100ms."
- "Check bundle size. Report done when the production build is under 500KB gzipped."
**Comprehensive verification:**
- "Verify every step of the plan was implemented. Check file changes, types, test output. Run `npm run typecheck` and `npm test`. Report each criterion individually with evidence."
The verifier should report facts with evidence, not suggest fixes.
### Skill Stacking
You can instruct the worker to use other skills:
```bash
skills/paseo-loop/bin/loop.sh \
--target new-agent \
--target-prompt "Use /committee first if you are stuck, then fix the provider list bug." \
--verifier-prompt "Verify the provider list renders correctly and typecheck passes." \
--name provider-fix
~/.claude/skills/paseo-loop/bin/loop.sh \
--worker-prompt "Use /committee to plan, then fix the provider list bug. The bug is..." \
--verifier-prompt "The provider list renders correctly and npm run typecheck passes" \
--name "provider-fix"
```
### Composing with Handoff
A handoff can launch a loop:
```
/handoff a loop in a worktree to babysit PR #42
```
+142 -336
View File
@@ -3,34 +3,29 @@ set -euo pipefail
usage() {
cat <<'EOF'
Usage: loop.sh --name NAME --target-prompt TEXT [options]
Usage: loop.sh --worker-prompt TEXT --name NAME [options]
Target (required):
--target self|new-agent Who acts each iteration. Default: new-agent
--target-prompt TEXT Prompt given to the target each iteration
--target-prompt-file PATH Read the target prompt from a file
Target options:
--agent-id ID Existing agent id for --target self
Default: $PASEO_AGENT_ID
--worker PROVIDER/MODEL Worker agent for --target new-agent
Default: codex
Worker (required):
--worker-prompt TEXT Prompt given to the worker agent each iteration
--worker-prompt-file PATH Read the worker prompt from a file
--worker PROVIDER/MODEL Worker agent (e.g. codex, codex/gpt-5.4, claude/sonnet). Default: codex
Verifier (optional):
--verifier-prompt TEXT Verification prompt evaluated after each iteration
--verifier-prompt-file PATH Read the verification prompt from a file
--verifier PROVIDER/MODEL Verifier agent. Default: claude/sonnet
--verifier-prompt TEXT Verification prompt evaluated by a separate agent after each iteration
--verifier-prompt-file PATH Read the verification prompt from a file
--verifier PROVIDER/MODEL Verifier agent (e.g. claude/sonnet, codex/gpt-5.4). Default: claude/sonnet
Limits:
--sleep DURATION Sleep between iterations (e.g. 30s, 5m, 1h)
--max-iterations N Maximum loop iterations (default: unlimited)
--max-time DURATION Maximum total runtime (e.g. 30m, 2h)
When a verifier prompt is provided, a separate verifier agent evaluates the
condition after each worker iteration. When omitted, the worker itself returns
{ done: boolean, reason: string } and decides when the loop is done.
Other options:
--name NAME Name prefix for loop tracking (required)
--archive Archive newly created agents after each iteration
--worktree NAME Run new agents in this worktree
--thinking LEVEL Thinking level for new-agent worker (default: medium)
Options:
--name NAME Name prefix for agents (required)
--sleep DURATION Sleep between iterations (e.g. 30s, 5m, 1h). Default: no delay.
--max-iterations N Maximum loop iterations (default: unlimited)
--archive Archive agents after each iteration
--worktree NAME Run all agents in this worktree (created on first use, reused after)
--thinking LEVEL Thinking level for worker (default: medium)
EOF
exit 1
}
@@ -52,37 +47,39 @@ parse_agent_spec() {
fi
}
parse_duration_to_seconds() {
local raw="$1"
if [[ -z "$raw" ]]; then
echo "Error: duration cannot be empty" >&2
exit 1
fi
# Defaults
max_iterations=0
archive=false
worker_spec=""
verifier_spec=""
worker_prompt_input=""
worker_prompt_file_input=""
verifier_prompt_input=""
verifier_prompt_file_input=""
name=""
thinking="medium"
worktree=""
sleep_duration=""
state_root="${HOME}/.paseo/loops"
if [[ "$raw" =~ ^[0-9]+$ ]]; then
echo "$raw"
return
fi
if [[ "$raw" =~ ^([0-9]+)(s|m|h|d)$ ]]; then
local value="${BASH_REMATCH[1]}"
local unit="${BASH_REMATCH[2]}"
case "$unit" in
s) echo "$value" ;;
m) echo $((value * 60)) ;;
h) echo $((value * 3600)) ;;
d) echo $((value * 86400)) ;;
*)
echo "Error: unsupported duration unit: $unit" >&2
exit 1
;;
esac
return
fi
echo "Error: invalid duration: $raw (use Ns, Nm, Nh, Nd, or seconds)" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--worker-prompt) worker_prompt_input="$2"; shift 2 ;;
--worker-prompt-file) worker_prompt_file_input="$2"; shift 2 ;;
--worker) worker_spec="$2"; shift 2 ;;
--verifier-prompt) verifier_prompt_input="$2"; shift 2 ;;
--verifier-prompt-file) verifier_prompt_file_input="$2"; shift 2 ;;
--verifier) verifier_spec="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--max-iterations) max_iterations="$2"; shift 2 ;;
--archive) archive=true; shift ;;
--sleep) sleep_duration="$2"; shift 2 ;;
--worktree) worktree="$2"; shift 2 ;;
--thinking) thinking="$2"; shift 2 ;;
--help|-h) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
load_prompt() {
local inline_value="$1"
@@ -117,171 +114,10 @@ load_prompt() {
return 1
}
build_target_prompt() {
local prompt="$1"
local prior_reason="$2"
local require_structured_output="$3"
local full_prompt="$prompt"
if [[ -n "$prior_reason" ]]; then
full_prompt="$full_prompt
<previous-iteration-result>
The previous iteration reported the following:
$prior_reason
</previous-iteration-result>"
fi
if [[ "$require_structured_output" == "true" ]]; then
full_prompt="$full_prompt
End your response with strict JSON matching:
{ \"done\": true/false, \"reason\": \"...\" }
Rules:
- Return only valid JSON
- done=true only if the loop's goal is complete
- done=false if more work is needed
- reason must briefly explain the current state"
fi
printf '%s' "$full_prompt"
}
extract_last_assistant_message() {
local agent_id="$1"
PASEO_LOOP_REPO_ROOT="$repo_root" TARGET_AGENT_ID="$agent_id" npx tsx --eval '
import { pathToFileURL } from "node:url";
const repoRoot = process.env.PASEO_LOOP_REPO_ROOT;
const agentId = process.env.TARGET_AGENT_ID;
if (!repoRoot || !agentId) {
throw new Error("Missing repo root or agent id");
}
const { connectToDaemon } = await import(
pathToFileURL(`${repoRoot}/packages/cli/src/utils/client.ts`).href
);
const { resolveStructuredResponseMessage } = await import(
pathToFileURL(`${repoRoot}/packages/cli/src/commands/agent/run.ts`).href
);
const client = await connectToDaemon({});
try {
const message = await resolveStructuredResponseMessage({
client,
agentId,
lastMessage: null,
});
if (message) {
process.stdout.write(message);
}
} finally {
await client.close();
}
'
}
parse_done_reason() {
local raw="$1"
local context_label="$2"
if ! echo "$raw" | jq -e '.done | type == "boolean"' >/dev/null 2>&1; then
echo "Error: ${context_label} response did not include boolean .done" >&2
echo "$raw" >&2
exit 1
fi
if ! echo "$raw" | jq -e '.reason | type == "string"' >/dev/null 2>&1; then
echo "Error: ${context_label} response did not include string .reason" >&2
echo "$raw" >&2
exit 1
fi
}
ensure_time_remaining() {
if [[ "$max_time_seconds" -le 0 ]]; then
return
fi
local now
now="$(date +%s)"
local elapsed=$((now - start_epoch))
if [[ "$elapsed" -ge "$max_time_seconds" ]]; then
echo "=== Loop exhausted: max time reached (${max_time_raw}) ==="
exit 1
fi
}
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../../.." && pwd)"
target="new-agent"
max_iterations=0
max_time_raw=""
max_time_seconds=0
archive=false
worker_spec=""
verifier_spec=""
target_prompt_input=""
target_prompt_file_input=""
verifier_prompt_input=""
verifier_prompt_file_input=""
name=""
thinking="medium"
worktree=""
sleep_raw=""
sleep_seconds=0
agent_id="${PASEO_AGENT_ID:-}"
state_root="${HOME}/.paseo/loops"
while [[ $# -gt 0 ]]; do
case "$1" in
--target) target="$2"; shift 2 ;;
--target-prompt) target_prompt_input="$2"; shift 2 ;;
--target-prompt-file) target_prompt_file_input="$2"; shift 2 ;;
--agent-id) agent_id="$2"; shift 2 ;;
--worker) worker_spec="$2"; shift 2 ;;
--verifier-prompt) verifier_prompt_input="$2"; shift 2 ;;
--verifier-prompt-file) verifier_prompt_file_input="$2"; shift 2 ;;
--verifier) verifier_spec="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--max-iterations) max_iterations="$2"; shift 2 ;;
--max-time) max_time_raw="$2"; shift 2 ;;
--archive) archive=true; shift ;;
--sleep) sleep_raw="$2"; shift 2 ;;
--worktree) worktree="$2"; shift 2 ;;
--thinking) thinking="$2"; shift 2 ;;
--help|-h) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
case "$target" in
self|new-agent) ;;
*)
echo "Error: --target must be 'self' or 'new-agent'"
usage
;;
esac
target_prompt="$(load_prompt "$target_prompt_input" "$target_prompt_file_input" "target-prompt" "true")"
worker_prompt="$(load_prompt "$worker_prompt_input" "$worker_prompt_file_input" "worker-prompt" "true")"
[[ -z "$name" ]] && { echo "Error: --name is required"; usage; }
if [[ "$target" == "self" && -z "$agent_id" ]]; then
echo "Error: --target self requires --agent-id or \$PASEO_AGENT_ID"
exit 1
fi
if [[ -n "$sleep_raw" ]]; then
sleep_seconds="$(parse_duration_to_seconds "$sleep_raw")"
fi
if [[ -n "$max_time_raw" ]]; then
max_time_seconds="$(parse_duration_to_seconds "$max_time_raw")"
fi
# Load verifier prompt if provided (optional)
has_verifier=false
verifier_prompt_text=""
if [[ -n "$verifier_prompt_input" || -n "$verifier_prompt_file_input" ]]; then
@@ -289,6 +125,7 @@ if [[ -n "$verifier_prompt_input" || -n "$verifier_prompt_file_input" ]]; then
has_verifier=true
fi
# Parse agent specs
read -r worker_provider worker_model <<< "$(parse_agent_spec "$worker_spec" "codex" "")"
read -r verifier_provider verifier_model <<< "$(parse_agent_spec "$verifier_spec" "claude" "sonnet")"
@@ -304,13 +141,14 @@ while [[ -e "$state_dir" ]]; do
loop_id="$(generate_loop_id)"
state_dir="${state_root}/${loop_id}"
done
mkdir -p "$state_dir"
target_prompt_file="${state_dir}/target-prompt.md"
prompt_file="${state_dir}/worker-prompt.md"
last_reason_file="${state_dir}/last_reason.md"
history_log="${state_dir}/history.log"
printf '%s\n' "$target_prompt" > "$target_prompt_file"
printf '%s\n' "$worker_prompt" > "$prompt_file"
printf '' > "$last_reason_file"
printf '' > "$history_log"
@@ -319,181 +157,135 @@ if [[ "$has_verifier" == true ]]; then
printf '%s\n' "$verifier_prompt_text" > "$verifier_prompt_file"
fi
# Build worker flags
worker_flags=()
if [[ "$worker_provider" == "codex" ]]; then
worker_flags+=(--mode full-access --provider codex)
elif [[ "$worker_provider" == "claude" ]]; then
worker_flags+=(--mode bypassPermissions --provider claude)
worker_flags+=(--mode bypassPermissions)
fi
[[ -n "$worker_model" ]] && worker_flags+=(--model "$worker_model")
[[ -n "$thinking" ]] && worker_flags+=(--thinking "$thinking")
# Build verifier flags
verifier_flags=()
if [[ "$verifier_provider" == "codex" ]]; then
verifier_flags+=(--mode full-access --provider codex)
elif [[ "$verifier_provider" == "claude" ]]; then
verifier_flags+=(--mode bypassPermissions --provider claude)
verifier_flags+=(--mode bypassPermissions)
fi
[[ -n "$verifier_model" ]] && verifier_flags+=(--model "$verifier_model")
# Worktree flags — passed to every paseo run call
worktree_flags=()
if [[ -n "$worktree" ]]; then
base_branch="$(git branch --show-current 2>/dev/null || echo "main")"
worktree_flags+=(--worktree "$worktree" --base "$base_branch")
fi
# Structured output schema
done_schema='{"type":"object","properties":{"done":{"type":"boolean"},"reason":{"type":"string"}},"required":["done","reason"],"additionalProperties":false}'
start_epoch="$(date +%s)"
iteration=0
echo "=== Loop started: $name ==="
echo " Loop ID: $loop_id"
echo " State dir: $state_dir"
echo " Target: $target"
if [[ "$target" == "self" ]]; then
echo " Agent ID: $agent_id"
else
echo " Worker: $worker_provider/${worker_model:-(default)}"
fi
echo " Target prompt: $target_prompt_file (live-editable)"
echo " Worker prompt: $prompt_file (live-editable)"
if [[ "$has_verifier" == true ]]; then
echo " Verifier prompt: $verifier_prompt_file (live-editable)"
echo " Verifier: $verifier_provider/${verifier_model:-(default)}"
echo " Mode: worker + verifier"
else
echo " Verifier: none"
echo " Mode: self-terminating worker"
fi
echo " Last reason file: $last_reason_file"
echo " History log: $history_log"
if [[ -n "$sleep_raw" ]]; then
echo " Sleep: $sleep_raw between iterations"
echo " Worker: $worker_provider/${worker_model:-(default)}"
if [[ "$has_verifier" == true ]]; then
echo " Verifier: $verifier_provider/${verifier_model:-(default)}"
fi
if [[ -n "$max_time_raw" ]]; then
echo " Max time: $max_time_raw"
else
echo " Max time: unlimited"
if [[ -n "$sleep_duration" ]]; then
echo " Sleep: $sleep_duration between iterations"
fi
if [[ "$archive" == true ]]; then
echo " Archive: newly created agents archived after each iteration"
echo " Archive: agents archived after each iteration"
fi
if [[ -n "$worktree" ]]; then
echo " Worktree: $worktree (base: $base_branch)"
fi
if [[ "$max_iterations" -gt 0 ]]; then
if [[ $max_iterations -gt 0 ]]; then
echo " Max iterations: $max_iterations"
else
echo " Max iterations: unlimited"
fi
echo ""
while [[ "$max_iterations" -eq 0 || "$iteration" -lt "$max_iterations" ]]; do
ensure_time_remaining
while [[ $max_iterations -eq 0 || $iteration -lt $max_iterations ]]; do
iteration=$((iteration + 1))
if [[ "$max_iterations" -gt 0 ]]; then
if [[ $max_iterations -gt 0 ]]; then
echo "--- Iteration $iteration/$max_iterations ---"
else
echo "--- Iteration $iteration ---"
fi
if [[ ! -s "$target_prompt_file" ]]; then
echo "Error: target prompt file is missing or empty: $target_prompt_file"
if [[ ! -s "$prompt_file" ]]; then
echo "Error: worker prompt file is missing or empty: $prompt_file"
exit 1
fi
current_target_prompt="$(cat "$target_prompt_file")"
current_prompt="$(cat "$prompt_file")"
last_reason="$(cat "$last_reason_file" 2>/dev/null || true)"
target_needs_structured_output=false
if [[ "$has_verifier" == false ]]; then
target_needs_structured_output=true
# Build the full worker prompt
full_worker_prompt="$current_prompt"
if [[ -n "$last_reason" ]]; then
full_worker_prompt="$full_worker_prompt
<previous-iteration-result>
The previous iteration reported the following:
$last_reason
</previous-iteration-result>"
fi
full_target_prompt="$(build_target_prompt "$current_target_prompt" "$last_reason" "$target_needs_structured_output")"
if [[ "$target" == "new-agent" ]]; then
worker_name="${name}-${iteration}"
if [[ "$has_verifier" == true ]]; then
echo "Launching worker: $worker_name"
worker_id=$(paseo run -d "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" "$full_target_prompt" -q)
echo "Worker [$worker_name] launched. ID: $worker_id"
echo " Stream logs: paseo logs $worker_id -f"
echo " Inspect: paseo inspect $worker_id"
echo ""
echo "Waiting for worker to complete..."
paseo wait "$worker_id"
echo "Worker done."
if [[ "$archive" == true ]]; then
paseo agent archive "$worker_name" 2>/dev/null || true
fi
else
echo "Launching worker: $worker_name"
verdict=$(paseo run "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" --output-schema "$done_schema" "$full_target_prompt")
echo "Result: $verdict"
parse_done_reason "$verdict" "worker"
done_value=$(echo "$verdict" | jq -r '.done')
reason=$(echo "$verdict" | jq -r '.reason')
if [[ "$archive" == true ]]; then
paseo agent archive "$worker_name" 2>/dev/null || true
fi
printf '[%s] iteration=%s target=%s target_agent=%s done=%s reason=%s\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"$iteration" \
"$target" \
"$worker_name" \
"$done_value" \
"$(echo "$reason" | tr '\n' ' ')" >> "$history_log"
fi
else
iteration_label="self:${agent_id}"
iteration_prompt_file="$(mktemp)"
trap 'rm -f "$iteration_prompt_file"' EXIT
printf '%s\n' "$full_target_prompt" > "$iteration_prompt_file"
echo "Sending iteration prompt to existing agent: $agent_id"
paseo send "$agent_id" --prompt-file "$iteration_prompt_file" >/dev/null
rm -f "$iteration_prompt_file"
trap - EXIT
if [[ "$has_verifier" == false ]]; then
verdict="$(extract_last_assistant_message "$agent_id")"
echo "Result: $verdict"
parse_done_reason "$verdict" "self target"
done_value=$(echo "$verdict" | jq -r '.done')
reason=$(echo "$verdict" | jq -r '.reason')
printf '[%s] iteration=%s target=%s target_agent=%s done=%s reason=%s\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"$iteration" \
"$target" \
"$iteration_label" \
"$done_value" \
"$(echo "$reason" | tr '\n' ' ')" >> "$history_log"
fi
fi
worker_name="${name}-${iteration}"
if [[ "$has_verifier" == true ]]; then
# --- Mode: worker + separate verifier ---
# Worker runs detached (no structured output)
echo "Launching worker: $worker_name"
worker_id=$(paseo run -d "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" "$full_worker_prompt" -q)
echo "Worker [$worker_name] launched. ID: $worker_id"
echo " Stream logs: paseo logs $worker_id -f"
echo " Inspect: paseo inspect $worker_id"
echo ""
echo "Waiting for worker to complete..."
paseo wait "$worker_id"
echo "Worker done."
if [[ "$archive" == true ]]; then
paseo agent archive "$worker_name" 2>/dev/null || true
fi
# Re-read verifier prompt for live steering
current_verifier_prompt="$(cat "$verifier_prompt_file")"
# Verifier runs synchronously with structured output
verifier_name="${name}-verify-${iteration}"
full_verifier_prompt="$current_verifier_prompt
Respond with strict JSON matching:
{ \"done\": true/false, \"reason\": \"...\" }
Rules:
- done=true only if the loop goal has been met
- done=false if another iteration is needed
- reason must explain what you found with evidence"
Respond with { \"done\": true/false, \"reason\": \"...\" }.
- done: true if the condition is met, false otherwise
- reason: explain what you found with evidence"
echo ""
echo "Launching verifier: $verifier_name"
verdict=$(paseo run "${verifier_flags[@]}" "${worktree_flags[@]}" --name "$verifier_name" --output-schema "$done_schema" "$full_verifier_prompt")
echo "Verdict: $verdict"
parse_done_reason "$verdict" "verifier"
done_value=$(echo "$verdict" | jq -r '.done')
reason=$(echo "$verdict" | jq -r '.reason')
@@ -501,20 +293,35 @@ Rules:
paseo agent archive "$verifier_name" 2>/dev/null || true
fi
if [[ "$target" == "new-agent" ]]; then
target_label="$worker_name"
else
target_label="self:${agent_id}"
fi
printf '[%s] iteration=%s target=%s target_agent=%s verifier=%s done=%s reason=%s\n' \
printf '[%s] iteration=%s worker=%s verifier=%s done=%s reason=%s\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"$iteration" \
"$target" \
"$target_label" \
"$worker_name" \
"$verifier_name" \
"$done_value" \
"$(echo "$reason" | tr '\n' ' ')" >> "$history_log"
else
# --- Mode: self-terminating worker ---
# Worker runs synchronously with structured output
echo "Launching worker: $worker_name"
verdict=$(paseo run "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" --output-schema "$done_schema" "$full_worker_prompt")
echo "Result: $verdict"
done_value=$(echo "$verdict" | jq -r '.done')
reason=$(echo "$verdict" | jq -r '.reason')
if [[ "$archive" == true ]]; then
paseo agent archive "$worker_name" 2>/dev/null || true
fi
printf '[%s] iteration=%s worker=%s done=%s reason=%s\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"$iteration" \
"$worker_name" \
"$done_value" \
"$(echo "$reason" | tr '\n' ' ')" >> "$history_log"
fi
if [[ "$done_value" == "true" ]]; then
@@ -527,10 +334,9 @@ Rules:
echo "Not done: $reason"
printf '%s\n' "$reason" > "$last_reason_file"
ensure_time_remaining
if [[ "$sleep_seconds" -gt 0 ]] && [[ "$max_iterations" -eq 0 || "$iteration" -lt "$max_iterations" ]]; then
echo "Sleeping $sleep_raw before next iteration..."
sleep "$sleep_seconds"
if [[ -n "$sleep_duration" ]] && [[ $max_iterations -eq 0 || $iteration -lt $max_iterations ]]; then
echo "Sleeping $sleep_duration before next iteration..."
sleep "$sleep_duration"
fi
echo ""
-311
View File
@@ -1,311 +0,0 @@
---
name: paseo-orchestrator
description: Orchestrate work through agents. Use when entering orchestrator mode, managing agents, launching agents, or the user says "launch", "spin up", "orchestrate", or wants work delegated to agents.
user-invocable: true
---
# Orchestrator Mode
You are an orchestrator. You manage agents — you do not write code yourself.
## Prerequisites
Load the **Paseo skill** first — it contains the CLI reference for all agent commands and waiting guidelines.
## Your Role
You have two audiences and you speak differently to each.
**To the user** — you are a design partner. You discuss architecture, data shapes, interfaces, types, flow, and trade-offs. Code examples are your primary communication tool here. You help the user think through what they want and ensure their intent is clearly defined before agents start working.
**To agents** — you are a product owner. You define acceptance criteria and behavioral expectations. You do NOT tell agents how to implement things — no "set variable X to Y", no "in file Z change line 42", no implementation-level code snippets. Agents read the codebase and figure out the implementation.
Your job:
1. **Understand the problem** — discuss with the user, explore the design space, propose types/interfaces/data shapes
2. **Define acceptance criteria** — what does "done" look like from the user's perspective?
3. **Launch agents** with clear, behavior-focused prompts
4. **Course-correct** when agents drift
5. **Review the output** — spin up a review agent to verify the work meets criteria
6. **Report back** to the user with what was done and what to test
You ensure the user's will is manifested through agents doing work.
**You own your agents.** Every agent you launch is your responsibility. You wait for them, read their output, challenge their work, and ensure they deliver. You do not launch an agent and move on — you stay with it until the job is done. The only exception is when the user explicitly says it's fire-and-forget.
## Before Launching Agents
Before any agent starts working, align with the user on logistics:
- **Where?** — Work in the current working directory unless the user specifies a worktree. Don't assume.
- **What's the deliverable?** — Is the objective a PR? A commit? Just exploration with no edits? Ask if unclear.
- **Is there a GitHub issue?** — Link it in the agent prompt if so. The agent should reference it.
- **Does the user want proof?** — Screenshot? Video? Manual test? Automated test? Know what "verified" means for this task.
These questions prevent wasted work. A perfectly implemented feature in the wrong branch or without a PR is still a failure.
## Agent Types
Implementation and review are not the only agent roles. Use the right agent for the job:
- **Exploration agents** — Read the codebase, map dependencies, understand how something works. Launch these when you need context before making decisions.
- **Investigation agents** — Debug a problem, trace a bug, find root cause. Must NOT edit files.
- **Implementation agents** — Write code to meet acceptance criteria.
- **Review agents** — Independently verify implementation meets criteria. Must NOT edit files.
- **Second opinion agents** — When you're unsure about an approach, launch an agent (different provider if possible) to evaluate the plan and poke holes in it.
Don't limit yourself to implement → review. Explore first if you need context. Get a second opinion if the design is tricky. The user is paying for thoroughness, not speed.
## How to Write Agent Prompts
This is the most important skill. A good prompt produces good work. A bad prompt produces wasted time.
### Lead with behavior, not implementation details
When writing prompts for agents, describe the problem and desired outcome. Never dictate implementation.
**Bad** (micromanaging the agent):
```
Fix the cursor position in the autoformat handler. In `divider-autoformat.ts`,
after the replacement edit, set the cursor to `dividerEnd + 1`. Use
`applyEdit` with the new position.
```
This is bad because you're telling the agent WHICH file to edit, WHICH variable to set, and HOW to call the API. The agent will blindly follow your instructions even if they're wrong, instead of understanding the codebase and finding the right fix.
**Good** (product owner defining behavior):
```
## Bug: Caret position after typing ---
### What the user sees
The user types three dashes on an empty line. A divider appears.
The caret should now be blinking on a NEW EMPTY LINE below the divider,
ready for the user to keep typing.
### What actually happens
The caret stays on or before the divider. The user has to manually
press Enter to continue.
### Acceptance criteria
- Type ---, divider appears, caret is on a new empty paragraph BELOW
- Works at end of document (new paragraph created)
- Works in middle of document (caret on existing next line)
- User can immediately start typing — no extra keystrokes needed
```
This is good because the agent understands the PROBLEM and can find the right solution itself.
### Structure every prompt the same way
Every agent prompt should have:
1. **Context** — what repo, what feature, what's the current state
2. **Problem** — what's wrong, described as user-visible behavior
3. **Acceptance criteria** — specific, testable, behavioral statements
4. **How to verify** — describe the test scenario, not the test code. Tell them WHAT to test, not HOW to write the test.
5. **Constraints** — what they must NOT do (e.g., "do not bump version", "do not modify unrelated files")
6. **Workflow** — TDD, commit expectations, what commands to run
### Don't dictate implementation to agents
Trust the agent to:
- Find the right files
- Choose the right approach
- Write the right tests
- Structure the code properly
You tell them WHAT the user needs. They figure out HOW.
But DO discuss implementation with the **user**. When the user asks about architecture, show them types, interfaces, data flow, code examples. That's how you align on design before agents start working.
### Give complete context
Agents start with **zero knowledge** of your conversation. Everything they need must be in the prompt. Don't assume they know:
- What repo they're in (set cwd or tell them)
- What feature was recently added
- What decisions were made
- What was already tried
## Always Review
After every implementation agent finishes, spin up a **review agent** to independently verify the work.
The review agent should:
- Check that all acceptance criteria are met — not by reading the agent's claims, but by actually testing
- Verify tests exist and pass
- Check that the agent didn't hand-wave or work around the problem
- Flag any regressions
- Run typecheck and tests
```bash
paseo run --provider codex --mode full-access --name "[Review] Task description" \
"Review the recent changes in [repo]. The goal was [goal].
Acceptance criteria:
- [criterion 1]
- [criterion 2]
Verify EACH criterion independently. Run the tests. Run typecheck.
Check that the implementation actually solves the problem — not that
it appears to solve it. Look for workarounds, hand-waves, and missing
edge cases.
DO NOT edit any files. Report your findings."
```
Don't skip this step. Don't trust the implementation agent's self-assessment.
## Course-Correcting Agents
When an agent goes off track, send a follow-up via `paseo send`:
- Be specific about what's wrong
- Restate the acceptance criteria they're missing
- Don't give code fixes — describe the behavioral gap
```bash
paseo send <id> "The caret is still not on a new line after the divider.
Re-read the acceptance criteria: the user must be able to immediately
start typing on the line AFTER the divider without pressing Enter.
Whatever you did didn't achieve that. Try again."
```
## Challenging Agents
Agents lie, hand-wave, over-engineer, work around problems, solve the wrong thing, and don't check deeply enough. This is not occasional — it is the default. Your job is to catch it.
### The behaviors to watch for
- **Hand-waving** — Agent claims the fix works but didn't actually test the specific scenario. "Tests pass" doesn't mean the right tests exist.
- **Over-engineering** — Agent adds abstractions, helper functions, configuration layers, or generalization that wasn't asked for. Simple problem, simple fix.
- **Working around the problem** — Agent avoids the actual bug by adding a special case, a fallback, or a guard clause that masks the symptom instead of fixing the cause.
- **Lying** — Agent says "all tests pass" when they didn't run them, or "I verified this works" when they checked something else entirely.
- **Not checking deep enough** — Agent fixes the surface symptom without understanding why it happened. The fix works for the reported case but breaks in related scenarios.
- **Being confused** — Agent misunderstands the problem and solves something different from what was asked.
- **Solving the wrong thing** — Agent picks up on a secondary detail and optimizes that instead of addressing the core ask.
### How to catch it
Challenge agents by asking questions via `paseo send`:
- "What exactly did you change and why?"
- "What about [edge case X]? Did you test that?"
- "Have you considered [alternative approach Y]?"
- "Show me the test that covers [specific scenario]"
- "Why did you add [abstraction/helper/config]? Was that necessary?"
- "Is this fixing the root cause or masking the symptom?"
- "Walk me through the data flow after your change"
Don't accept the first answer. Push back. Ask follow-ups. The agent will refine its work when challenged.
### User signals
When the user says things like:
- **"simplify"** — the agent over-engineered
- **"think harder"** — the agent was shallow or confused
- **"I don't like this"** — the agent solved the wrong thing or worked around the problem
- **"this is wrong"** — the agent lied or didn't check deeply enough
These are signals to challenge the agent, not to tweak the implementation yourself. Send the user's concern to the agent as a question, or spin up a review agent specifically looking for the pattern the user flagged.
## Common Orchestrator Failures
These are the patterns that lead to bad outcomes. Avoid them.
### Micromanaging agents
**Symptom:** Your agent prompts contain implementation directives — "set variable X to Y", "modify file Z", "this function is wrong, change it to...". You're telling the agent HOW to code instead of WHAT the user needs.
**Fix:** Describe the behavior and acceptance criteria. The agent reads the codebase and finds the right approach. When you dictate implementation, the agent follows your instructions blindly — even when they're wrong — instead of using its own understanding of the code.
Note: discussing architecture, types, interfaces, and code examples with the **user** is not micromanaging — that's your design partner role.
### Skipping review
**Symptom:** Agent says "done", you tell the user "done", user finds bugs.
**Fix:** Always spin up a review agent. The implementation agent will claim success even when the fix is incomplete. Independent verification catches this.
### Hand-waving acceptance criteria
**Symptom:** "Fix the selection bug" with no behavioral definition of what "fixed" means.
**Fix:** Define exactly what the user should see. Be specific enough that someone who has never seen the app could verify it.
### Giving up on verification
**Symptom:** "The agent says tests pass" without checking what the tests actually test.
**Fix:** Tell the review agent what scenarios to verify. Design the test cases yourself (as a product owner) and tell the reviewer to check they exist.
### Anxious polling
**Symptom:** Agent is running, you start checking `paseo ls`, `paseo inspect`, `paseo logs` in a loop.
**Fix:** `paseo wait`. Trust it. Agents can take 30+ minutes. This is normal.
### Not providing feedback loops
**Symptom:** Launch agent, wait, get result, launch new agent for next issue. Never send follow-ups.
**Fix:** Use `paseo send` to course-correct running agents. They retain context. A follow-up is cheaper than a fresh agent.
### Being too passive
**Symptom:** Agent delivers something that doesn't quite match what the user wanted. You accept it and report back.
**Fix:** Push back. Send follow-ups. Restate criteria. Don't accept work that doesn't meet the bar. You're the quality gate.
## Clarifying Ambiguous Requests
When user requests are unclear:
1. **Research first** — spawn an investigation agent to understand the current state
2. **Ask clarifying questions** — after research, ask the user specific questions
3. **Present options** — offer approaches with trade-offs
4. **Get explicit confirmation** — never assume what the user wants
## Investigation vs Implementation
When asked to investigate:
- **Investigation agents MUST NOT fix issues** — they only identify, document, and report
- **Always ask for confirmation** — after investigation, present findings and ask: "Should I proceed with implementing fixes?"
- **Only implement if explicitly requested**
## Workflow
A typical orchestration cycle:
1. User describes what they want
2. You clarify if needed
3. You launch an implementation agent with behavioral acceptance criteria
4. You wait for it to finish
5. You launch a review agent to verify
6. If review passes → report to user
7. If review fails → send corrections to impl agent or launch a new one
8. Repeat until acceptance criteria are met
## Multi-Agent Coordination
When a task has independent parts:
- Launch agents in parallel with `paseo run -d`
- Each agent gets its own clear scope and acceptance criteria
- Wait for all to finish
- Review the combined output
When tasks are sequential:
- One agent at a time
- Feed the output/state from one into the next agent's prompt
- Review at the end
## Prefix Convention
Name all orchestrated agents with a prefix that indicates their role:
- `[Impl]` — implementation agents
- `[Review]` — review/verification agents
- `[Investigate]` — research/investigation agents
- `[Committee]` — committee planning agents
- `[Handoff]` — handoff agents
+96 -15
View File
@@ -1,9 +1,11 @@
---
name: paseo
description: Paseo CLI reference for managing agents. Load this skill whenever you need to use paseo commands.
description: Use when entering orchestrator mode to manage agents via Paseo CLI
---
## CLI Commands
### Paseo CLI Commands
Use these CLI commands to manage agents:
```bash
# List agents (directory-scoped by default)
@@ -76,31 +78,86 @@ paseo ls --json # JSON output
paseo ls -q # IDs only (quiet mode, useful for scripting)
```
## Available Models
### Available Models
**Claude (default provider)** — use aliases, CLI resolves to latest version:
- `--model haiku` — Fast/cheap, ONLY for tests (not for real work)
- `--model sonnet` — Default, good for most tasks
- `--model opus` — For harder reasoning, complex debugging
**Claude (default provider)** - use aliases, CLI resolves to latest version:
- `--model haiku` - Fast/cheap, ONLY for tests (not for real work)
- `--model sonnet` - Default, good for most tasks
- `--model opus` - For harder reasoning, complex debugging
**Codex** (`--provider codex`):
- `--model gpt-5.4` — Latest frontier agentic coding model (default, preferred for all engineering tasks)
- `--model gpt-5.1-codex-mini` — Cheaper, faster, but less capable
- `--model gpt-5.4` - Latest frontier agentic coding model (default, preferred for all engineering tasks)
- `--model gpt-5.1-codex-mini` - Cheaper, faster, but less capable
## Permissions
### Permissions
Always launch agents fully permissioned. Use `--mode bypass` for Claude and `--mode full-access` for Codex. Control behavior through **strict prompting**, not permission modes.
## Waiting for Agents
### Agent Use Cases
Both `paseo run` and `paseo wait` block until the agent completes. Trust them.
You can run agents to:
- **Implement a task** - Spawn an agent to write code and implement features
- **Have a design discussion** - Use Codex for architecture discussions
- **Test some feature** - Run tests and verify functionality
- **Do investigation** - Research and explore the codebase
- **Review changes** - Use Codex for thorough code reviews
### Clarifying Ambiguous Requests
**CRITICAL:** When user requests are ambiguous or unclear:
1. **Research first** - Spawn an investigation agent to understand the current state
2. **Ask clarifying questions** - After research, ask the user specific questions about what they want
3. **Present options** - Offer multiple approaches with trade-offs
4. **Get explicit confirmation** - Never assume what the user wants
### Investigation vs Implementation
**CRITICAL:** When asked to investigate:
- **Investigation agents MUST NOT fix issues** - They should only identify, document, and report problems
- **Always ask for confirmation** - After investigation, present findings and ask: "Should I proceed with implementing fixes?"
- **Only implement if explicitly requested** - Don't auto-fix without user approval
### Rigorous Agent Interrogation
**CRITICAL:** Agents start with ZERO context about your task. You must always provide complete context in your initial prompt.
When working with agents, you must dig deep and challenge them rigorously:
#### For Implementation Agents
- **Don't accept surface-level completion**: Check their logs with `paseo logs <id>`
- **Trace the implementation**: Ask them to walk through the code flow step by step
- **Uncover gaps**: Send follow-up prompts with `paseo send <id> "<question>"`
- "Show me exactly where you handle error case X"
- "What happens if the user does Y before Z?"
- "Walk me through the data flow from input to output"
#### For Investigation/Debugging Agents
- **Don't stop at the first answer**: Keep digging deeper
- **Explore different angles**: "What are 3 other possible causes?"
- **Request proof**: "Show me the specific code that proves this hypothesis"
- **Challenge assumptions**: "How do you know that's the root cause?"
#### For Review Agents (prefer Codex)
- **Security review**: "What are the security implications? Any OWASP vulnerabilities?"
- **Edge cases**: "What edge cases are not handled?"
- **Performance**: "Where are the performance bottlenecks?"
- **Maintainability**: "How maintainable is this code?"
### Waiting for Agents
**CRITICAL:** Both `paseo run` and `paseo wait` block until the agent completes. Trust them.
- `paseo run` waits **forever** by default (no timeout). Use `--wait-timeout` to set a limit.
- `paseo wait` also waits forever by default. Use `--timeout` to set a limit.
- Agent tasks can legitimately take 10, 20, or even 30+ minutes. This is normal.
- When a wait times out, **just re-run `paseo wait <id>`** — don't panic, don't start checking logs, don't inspect status. The agent is still working.
- Do NOT poll with `paseo ls`, `paseo inspect`, or `paseo logs` in a loop to "check on" the agent.
- Only check logs/inspect if you have a **specific reason** to believe something is wrong.
- Do NOT poll with `paseo ls`, `paseo inspect`, or `paseo logs` in a loop to "check on" the agent. This wastes your context window and accomplishes nothing.
- Only check logs/inspect if you have a **specific reason** to believe something is wrong (e.g., you sent a prompt and got an unexpected error back).
- **Never launch a duplicate agent** because a wait timed out. The original is still running.
```bash
@@ -109,14 +166,35 @@ paseo wait <id> # timed out? just run it again:
paseo wait <id> # still going? keep waiting:
paseo wait <id> --timeout 300 # or use a longer timeout
# For long-running tasks, set a generous timeout on run itself:
paseo run --wait-timeout 1h --output-schema '...' "<prompt>"
# Wrong: anxious polling loop
paseo wait <id> # timed out
paseo ls # is it still running??
paseo inspect <id> # what's it doing??
paseo logs <id> # let me check the logs!!
# ^ Don't do this. Trust the wait.
```
## Composing Agents in Bash
### Agent Management Principles
- **Keep agents focused** - Each agent should have a clear, specific responsibility
- **You can talk to them** - Use `paseo send <id> "<prompt>"` to guide them
- **Monitor progress** - Use `paseo logs <id> -f` to stream output
- **Always provide context** - Remember: agents start with zero knowledge of your task
- **Verify work rigorously** - Don't trust, verify. Ask agents to prove their work
- **Commit frequently** - Ensure each agent commits their changes before moving on
- **Plan for quality gates** - Use Codex review agents as checkpoints
- **Run in parallel when possible** - Use `-d` flag to run multiple agents concurrently
### Common Patterns
#### Committee
When stuck or planning something hard, use the `/committee` skill. It launches two high-reasoning agents (Opus 4.6 + GPT 5.4) in parallel to do root cause analysis and produce a plan, then stays alive to review the implementation afterward.
### Composing Agents in Bash Scripts
`paseo run` blocks by default and `--output-schema` returns structured JSON, making it easy to compose agents in bash loops and pipelines.
@@ -142,4 +220,7 @@ ui_id=$(paseo run -d --name "UI impl" "implement the UI" -q)
# Wait for both to finish
paseo wait "$api_id"
paseo wait "$ui_id"
# Review the combined result
paseo run --provider codex "review the API and UI implementations. DO NOT edit."
```