@stevecastaneda/paseo-progress 1.13.1 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server/store.ts CHANGED
@@ -5,30 +5,24 @@
5
5
  import { execFile } from "node:child_process";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { realpathSync } from "node:fs";
8
- import { appendFile, mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
8
+ import { lstat, mkdir, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
- import { basename, dirname, join, normalize, resolve } from "node:path";
10
+ import { basename, dirname, join, normalize, resolve, sep } from "node:path";
11
11
  import { promisify } from "node:util";
12
- import { LEGACY_FOLDER, PANEL_OPENED_NAME, PROGRESS_FILE_NAME } from "../shared/events.ts";
13
- import { withLock } from "./lock.ts";
14
- import { outside } from "./paths.ts";
12
+ import { latestRun, parseProgress } from "../shared/dashboard.ts";
13
+ import { hasGitEntry, inGitFolder, outside } from "./paths.ts";
15
14
 
16
- const LOCK_NAME = `${PROGRESS_FILE_NAME}.lock`;
17
15
  const SAVE_RUNS_KEY = "paseo-progress.saveRuns";
18
- // Exactly what 0.1.x wrote into .scratch/.gitignore. Only this is ever removed.
19
- const LEGACY_GITIGNORE = `# Added by the progress plugin for Paseo so its files stay out of git.
20
- ${PROGRESS_FILE_NAME}
21
- ${LOCK_NAME}/
22
- ${PANEL_OPENED_NAME}
23
- .gitignore
24
- `;
25
16
 
26
17
  // `directory` holds the working files. `shown` names it for people.
27
18
  export type Store = { directory: string; shown: string };
28
19
 
29
- export async function git(cwd: string, args: string[]): Promise<{ code: number; stdout: string }> {
20
+ // Git runs in the worktree through -C, started from the folder Node itself
21
+ // lives in: Windows looks for a program in the folder it starts in before
22
+ // PATH, and a cloned repo can commit a git.exe.
23
+ export async function git(root: string, args: string[]): Promise<{ code: number; stdout: string }> {
30
24
  try {
31
- const { stdout } = await promisify(execFile)("git", args, { cwd });
25
+ const { stdout } = await promisify(execFile)("git", ["-C", resolve(root), ...args], { cwd: dirname(process.execPath) });
32
26
  return { code: 0, stdout };
33
27
  } catch (error) {
34
28
  const failed = error as { code?: number | string; stdout?: string };
@@ -36,10 +30,6 @@ export async function git(cwd: string, args: string[]): Promise<{ code: number;
36
30
  }
37
31
  }
38
32
 
39
- async function exists(path: string): Promise<boolean> {
40
- return Boolean(await stat(path).catch(() => null));
41
- }
42
-
43
33
  // The plugin's per-user folder on this computer.
44
34
  export function stateRoot(home = homedir()): string {
45
35
  return join(home, ".local", "state", "paseo-progress");
@@ -68,76 +58,37 @@ export function outsideDirectory(root: string, home = homedir()): string {
68
58
  return join(stateRoot(home), `${basename(real)}-${hash}`);
69
59
  }
70
60
 
71
- // Whether git tracks a 0.1.x progress file, per worktree, until the file changes.
72
- const trackedLegacy = new Map<string, { mtimeMs: number; tracked: boolean }>();
73
-
74
- async function legacyTracked(root: string, mtimeMs: number): Promise<boolean> {
75
- const cached = trackedLegacy.get(root);
76
- if (cached && cached.mtimeMs === mtimeMs) return cached.tracked;
77
- const { code } = await git(root, ["ls-files", "--error-unmatch", "--", `${LEGACY_FOLDER}/${PROGRESS_FILE_NAME}`]);
78
- trackedLegacy.set(root, { mtimeMs, tracked: code === 0 });
79
- return code === 0;
80
- }
81
-
82
- // The worktree's working files. A run 0.1.x left in .scratch moves outside the
83
- // first time anything asks, unless git tracks it: then it stays and is used in
84
- // place, so nothing changes for a repo that commits it.
85
- export async function findStore(root: string, home = homedir()): Promise<Store> {
61
+ // The worktree's working files, always outside the repo. Progress never reads
62
+ // a repo's .scratch, where 0.1.x kept them: a cloned repo could put anything there.
63
+ export function findStore(root: string, home = homedir()): Store {
86
64
  const directory = outsideDirectory(root, home);
87
- const store = { directory, shown: directory.startsWith(home) ? `~${directory.slice(home.length)}` : directory };
88
- const legacy = join(root, LEGACY_FOLDER);
89
- const info = await stat(join(legacy, PROGRESS_FILE_NAME)).catch(() => null);
90
- if (!info) return store;
91
- if (await legacyTracked(root, info.mtimeMs)) return { directory: legacy, shown: LEGACY_FOLDER };
92
- await moveLegacy(legacy, directory);
93
- return store;
94
- }
95
-
96
- // Appends the old file's lines to the new one rather than replacing it, so a
97
- // command that wrote to the old place mid-move loses nothing: the next move
98
- // picks those lines up. Then tidies what 0.1.x left behind.
99
- async function moveLegacy(legacy: string, directory: string): Promise<void> {
100
- await mkdir(directory, { recursive: true });
101
- await withLock(join(legacy, LOCK_NAME), async () => {
102
- const text = await readFile(join(legacy, PROGRESS_FILE_NAME), "utf8").catch(() => null);
103
- if (text === null) return;
104
- await withLock(join(directory, LOCK_NAME), async () => {
105
- const current = await readFile(join(directory, PROGRESS_FILE_NAME), "utf8").catch(() => "");
106
- const lead = current && !current.endsWith("\n") ? "\n" : "";
107
- await appendFile(join(directory, PROGRESS_FILE_NAME), `${lead}${text}`);
108
- });
109
- await rm(join(legacy, PROGRESS_FILE_NAME));
110
- if (await exists(join(legacy, PANEL_OPENED_NAME))) {
111
- await writeFile(join(directory, PANEL_OPENED_NAME), "");
112
- await rm(join(legacy, PANEL_OPENED_NAME));
113
- }
114
- });
115
- const ignore = await readFile(join(legacy, ".gitignore"), "utf8").catch(() => null);
116
- if (ignore === LEGACY_GITIGNORE) await rm(join(legacy, ".gitignore"));
117
- // Only if nothing else is in there; the user's own files keep the folder.
118
- await rmdir(legacy).catch(() => {});
65
+ return { directory, shown: directory.startsWith(home) ? `~${directory.slice(home.length)}` : directory };
119
66
  }
120
67
 
121
68
  // The repo's saved-history folder, kept in its local git config, which every
122
69
  // worktree of the repo shares and nothing commits. Polls cost one stat; git
123
- // runs again only when the config file changes.
124
- const configFiles = new Map<string, string | null>();
70
+ // runs again only when the config file changes. A worktree with no .git
71
+ // entry is no repo; when git fails in one that has it, the next poll asks again.
72
+ const configFiles = new Map<string, string>();
125
73
  const savedFolders = new Map<string, { mtimeMs: number; folder: string | null }>();
126
74
 
127
75
  async function configFile(root: string): Promise<string | null> {
76
+ if (!hasGitEntry(root)) return null;
128
77
  if (!configFiles.has(root)) {
129
78
  const { code, stdout } = await git(root, ["rev-parse", "--git-common-dir"]);
130
- configFiles.set(root, code === 0 ? join(resolve(root, stdout.trim()), "config") : null);
79
+ if (code !== 0) return null;
80
+ configFiles.set(root, join(resolve(root, stdout.trim()), "config"));
131
81
  }
132
82
  return configFiles.get(root)!;
133
83
  }
134
84
 
135
- // Whether the worktree is in a git repo, which saving run history needs.
85
+ // Whether git answers for the worktree's repo, which saving run history needs.
136
86
  export async function inGitRepo(root: string): Promise<boolean> {
137
87
  return (await configFile(root)) !== null;
138
88
  }
139
89
 
140
- // The folder runs are saved to, relative to the worktree root, or null.
90
+ // The folder runs are saved to, relative to the worktree root, or null. A
91
+ // folder set outside Paseo gets the same check as one picked in it.
141
92
  export async function historyFolder(root: string): Promise<string | null> {
142
93
  const file = await configFile(root);
143
94
  if (file === null) return null;
@@ -145,7 +96,8 @@ export async function historyFolder(root: string): Promise<string | null> {
145
96
  const cached = savedFolders.get(file);
146
97
  if (cached && cached.mtimeMs === mtimeMs) return cached.folder;
147
98
  const { stdout } = await git(root, ["config", "--get", SAVE_RUNS_KEY]);
148
- const folder = stdout.trim() || null;
99
+ const checked = checkFolder(stdout);
100
+ const folder = "folder" in checked ? checked.folder : null;
149
101
  savedFolders.set(file, { mtimeMs, folder });
150
102
  return folder;
151
103
  }
@@ -156,7 +108,7 @@ export function checkFolder(raw: string): { folder: string } | { error: string }
156
108
  if (!trimmed) return { error: "Choose a folder" };
157
109
  const folder = normalize(trimmed);
158
110
  if (outside(folder) || folder === ".") return { error: "Choose a folder inside the repo" };
159
- if (folder === ".git" || folder.startsWith(".git/")) return { error: "Choose a folder outside .git" };
111
+ if (inGitFolder(folder)) return { error: "Choose a folder outside .git" };
160
112
  return { folder };
161
113
  }
162
114
 
@@ -189,27 +141,32 @@ export function runFileName(startedAt: string, title: string): string {
189
141
  export async function saveRunCopy(root: string, text: string): Promise<void> {
190
142
  const folder = await historyFolder(root);
191
143
  if (!folder) return;
192
- const lines = text.split("\n").filter((line) => line.trim());
193
- let start = -1;
194
- let event: { ts?: string; title?: string } | null = null;
195
- for (let index = lines.length - 1; index >= 0; index--) {
196
- try {
197
- const parsed = JSON.parse(lines[index]);
198
- if (parsed?.type === "run.start") {
199
- start = index;
200
- event = parsed;
201
- break;
202
- }
203
- } catch {}
144
+ // The run the dashboard shows. A start line it can't read is no run.
145
+ const [start] = latestRun(parseProgress(text).events);
146
+ if (start?.event.type !== "run.start") return;
147
+ const lines = text.split("\n").slice(start.line - 1).filter((line) => line.trim());
148
+ const directory = await runsFolder(root, folder);
149
+ if (!directory) return;
150
+ // A rename replaces a link at the run's file instead of writing through it.
151
+ await writeAtomic(join(directory, runFileName(start.event.ts, start.event.title)), `${lines.join("\n")}\n`);
152
+ }
153
+
154
+ // The saved-runs folder in the real worktree, made one step at a time, or null
155
+ // when any step is a link or a file: a cloned repo can commit a link that leads
156
+ // out of it.
157
+ async function runsFolder(root: string, folder: string): Promise<string | null> {
158
+ let directory = await realpath(root);
159
+ for (const name of folder.split(sep)) {
160
+ directory = join(directory, name);
161
+ const info = await lstat(directory).catch(() => null);
162
+ if (!info) await mkdir(directory);
163
+ else if (!info.isDirectory()) return null;
204
164
  }
205
- if (start < 0 || !event?.ts || !event.title) return;
206
- const directory = join(root, folder);
207
- await mkdir(directory, { recursive: true });
208
- await writeFile(join(directory, runFileName(event.ts, event.title)), `${lines.slice(start).join("\n")}\n`);
165
+ return directory;
209
166
  }
210
167
 
211
168
  // The folders directly inside one folder of the worktree ("" is the top), for
212
- // the folder picker. .git is left out; so are links, which could leave the repo.
169
+ // the folder picker. .git folders are left out; so are links, which could leave the repo.
213
170
  export async function listFolders(root: string, folder: string): Promise<{ folder: string; folders: string[] }> {
214
171
  const checked = folder === "" ? { folder: "" } : checkFolder(folder);
215
172
  if ("error" in checked) throw new Error(checked.error);
@@ -217,6 +174,6 @@ export async function listFolders(root: string, folder: string): Promise<{ folde
217
174
  if (error.code === "ENOENT") return [];
218
175
  throw error;
219
176
  });
220
- const folders = entries.filter((entry) => entry.isDirectory() && !(checked.folder === "" && entry.name === ".git")).map((entry) => entry.name);
177
+ const folders = entries.filter((entry) => entry.isDirectory() && !inGitFolder(entry.name)).map((entry) => entry.name);
221
178
  return { folder: checked.folder, folders: folders.sort((a, b) => a.localeCompare(b)) };
222
179
  }
package/server/tool.ts ADDED
@@ -0,0 +1,174 @@
1
+ import type { AgentToolContext, AgentToolDefinition, AgentToolResult, PluginHandlerContext, PluginServerContext } from "@getpaseo/plugin/server";
2
+ import * as sdk from "@getpaseo/plugin/server";
3
+ import { z } from "zod";
4
+ import { DELIVERABLE_KINDS, ticketStatusSchema } from "../shared/events.ts";
5
+ import type { ToolsState } from "../shared/rpc.ts";
6
+ import { PROGRESS_TOOL_NAMES, type ProgressToolName } from "../shared/tools.ts";
7
+ import { commandSummary, runCli } from "./cli.ts";
8
+
9
+ // Paseo supplies this SDK at runtime. Paseo 0.13's has no defineAgentTool and
10
+ // can't give agents plugin tools; reading the name calls nothing, so it's safe there.
11
+ export const agentToolsSupported = "defineAgentTool" in sdk;
12
+
13
+ // Importing defineAgentTool would stop Progress loading on Paseo 0.13. This
14
+ // mirrors its typing; Paseo validates the input before execute.
15
+ type Tool = Omit<AgentToolDefinition, "name">;
16
+
17
+ function agentTool<Input extends z.ZodType>(tool: Omit<Tool, "input" | "execute"> & {
18
+ input: Input;
19
+ execute(input: z.output<Input>, context: AgentToolContext): Promise<AgentToolResult>;
20
+ }): Tool {
21
+ return { ...tool, execute: (input, context) => tool.execute(input as z.output<Input>, context) };
22
+ }
23
+
24
+ async function run(args: string[], { agent }: AgentToolContext): Promise<AgentToolResult> {
25
+ const lines: string[] = [];
26
+ const code = await runCli(args, { cwd: agent.cwd, agentId: agent.id, out: (line) => lines.push(line) });
27
+ return { text: lines.join("\n"), isError: code !== 0 };
28
+ }
29
+
30
+ type Flags = Record<string, string | number | string[] | undefined>;
31
+
32
+ // Flags go as --name=value so a value starting with "-" stays a value, and the
33
+ // positionals follow "--" so a title starting with "-" isn't read as a flag.
34
+ function commandArgs(command: string, positionals: string[], flags: Flags = {}): string[] {
35
+ const options = Object.entries(flags).flatMap(([name, value]) => (value === undefined ? [] : [value].flat().map((entry) => `--${name}=${entry}`)));
36
+ return [...command.split(" "), ...options, "--", ...positionals];
37
+ }
38
+
39
+ // The command's --help summary, with its flags renamed to this tool's fields.
40
+ const FIELD_FOR_FLAG: Record<string, string> = { estimate: "estimateMinutes", option: "options", file: "files" };
41
+ function toolSummary(command: string): string {
42
+ return commandSummary(command).replace(/--([a-z]+(?:-[a-z]+)*)/g, (_, flag: string) =>
43
+ FIELD_FOR_FLAG[flag] ?? flag.replace(/-([a-z])/g, (_m, letter: string) => letter.toUpperCase()));
44
+ }
45
+
46
+ function commandTool<Input extends z.ZodType>(title: string, command: string, input: Input, args: (input: z.output<Input>) => string[], extra = "") {
47
+ return agentTool({
48
+ title: `Progress: ${title}`,
49
+ description: `Progress dashboard for this worktree in Paseo. ${toolSummary(command)}${extra}`,
50
+ input,
51
+ execute: (value, context) => run(args(value), context),
52
+ });
53
+ }
54
+
55
+ const ticketId = (what: string) => z.string().describe(`${what}, like T03`);
56
+ const estimate = z.number().int().describe("Whole minutes, like 90");
57
+ const source = z.string().describe("Spec file path or http(s) link where the ticket is written up");
58
+
59
+ // Keyed by name, so the compiler holds this to the shared list.
60
+ const tools: Record<ProgressToolName, Tool> = {
61
+ progress_show: commandTool("Show", "show", z.object({}), () => ["show"]),
62
+ progress_start: commandTool("Start run", "start",
63
+ z.object({
64
+ title: z.string(),
65
+ subtitle: z.string().optional(),
66
+ itemLabel: z.string().optional().describe('What to call each item instead of "Ticket", like Step'),
67
+ }),
68
+ ({ title, subtitle, itemLabel }) => commandArgs("start", [title], { subtitle, "item-label": itemLabel })),
69
+ progress_ticket_add: commandTool("Add ticket", "ticket add",
70
+ z.object({
71
+ title: z.string().describe('Like "Ticket 01: Export button"'),
72
+ estimateMinutes: estimate,
73
+ status: ticketStatusSchema.optional(),
74
+ waitsFor: ticketId("A ticket this one can't start before").optional(),
75
+ source: source.optional(),
76
+ }),
77
+ ({ title, estimateMinutes, status, waitsFor, source }) => commandArgs("ticket add", [title], { estimate: estimateMinutes, status, "waits-for": waitsFor, source })),
78
+ progress_ticket_update: commandTool("Update ticket", "ticket update",
79
+ z.object({
80
+ id: ticketId("The ticket"),
81
+ status: ticketStatusSchema.optional(),
82
+ stage: z.string().optional().describe('The step in progress, like Build or Fixes; "" clears it'),
83
+ title: z.string().optional(),
84
+ estimateMinutes: estimate.optional(),
85
+ note: z.string().optional(),
86
+ waitsFor: z.string().optional().describe('A ticket this one can\'t start before, like T02; "" clears it'),
87
+ source: z.string().optional().describe('Spec file path or http(s) link where the ticket is written up; "" clears it'),
88
+ }),
89
+ ({ id, status, stage, title, estimateMinutes, note, waitsFor, source }) =>
90
+ commandArgs("ticket update", [id], { status, stage, title, estimate: estimateMinutes, note, "waits-for": waitsFor, source })),
91
+ progress_question_ask: commandTool("Ask question", "question ask",
92
+ z.object({
93
+ title: z.string().describe("A short title"),
94
+ question: z.string(),
95
+ options: z.array(z.object({
96
+ letter: z.string().describe("A, B, C, ..."),
97
+ label: z.string(),
98
+ consequence: z.string().describe("What picking it means for the user"),
99
+ })),
100
+ recommend: z.string().describe("The option letter you would pick"),
101
+ big: z.string().optional().describe("Only for a big question: which test it meets, in a few plain words"),
102
+ background: z.string().optional(),
103
+ files: z.array(z.object({
104
+ target: z.string().describe("A file path or http(s) link, like a screenshot"),
105
+ label: z.string().optional(),
106
+ })).optional(),
107
+ ticket: ticketId("The ticket it's about").optional(),
108
+ }),
109
+ ({ title, question, options, recommend, big, background, files, ticket }) => commandArgs("question ask", [title, question], {
110
+ option: options.map(({ letter, label, consequence }) => `${letter}=${label} | ${consequence}`),
111
+ recommend,
112
+ big,
113
+ background,
114
+ file: files?.map(({ target, label }) => (label ? `${target}=${label}` : target)),
115
+ ticket,
116
+ })),
117
+ progress_question_answer: commandTool("Record answer", "question answer",
118
+ z.object({
119
+ id: z.string().describe("The question, like Q2"),
120
+ choice: z.string().describe("An option letter, or the user's own short answer"),
121
+ words: z.string().optional().describe("What the user said"),
122
+ }),
123
+ ({ id, choice, words }) => commandArgs("question answer", [id, choice], { words })),
124
+ progress_deliverable_add: commandTool("Add deliverable", "deliverable add",
125
+ z.object({
126
+ title: z.string(),
127
+ target: z.string().describe("A file or folder path, or an http(s) link"),
128
+ ticket: ticketId("The ticket it belongs to").optional(),
129
+ kind: z.enum(DELIVERABLE_KINDS).optional(),
130
+ }),
131
+ ({ title, target, ticket, kind }) => commandArgs("deliverable add", [title, target], { ticket, kind })),
132
+ progress_activity_add: commandTool("Log activity", "activity add",
133
+ z.object({ text: z.string(), ticket: ticketId("The ticket it's about").optional() }),
134
+ ({ text, ticket }) => commandArgs("activity add", [text], { ticket })),
135
+ progress_ticker: commandTool("Set ticker", "ticker set",
136
+ z.object({ text: z.string() }),
137
+ ({ text }) => (text.trim() ? commandArgs("ticker set", [text]) : commandArgs("ticker clear", [])),
138
+ ' Pass "" to hide it.'),
139
+ progress_finish: commandTool("Finish run", "finish",
140
+ z.object({ outcome: z.string().describe("One line") }),
141
+ ({ outcome }) => commandArgs("finish", [outcome])),
142
+ paseo_progress: agentTool({
143
+ title: "Progress",
144
+ description: [
145
+ "Record progress for this worktree on the Progress panel in Paseo. Works like the paseo-progress command:",
146
+ 'pass its arguments as a list, like ["ticket", "add", "Fix login", "--estimate", "30"].',
147
+ 'Pass ["--help"] to list the commands, or add "--help" after any command for its options.',
148
+ ].join(" "),
149
+ input: z.object({ args: z.array(z.string()).min(1).describe("Arguments for paseo-progress, one per item") }),
150
+ execute: ({ args }, context) => run(args, context),
151
+ }),
152
+ };
153
+
154
+ export const progressTools: AgentToolDefinition[] = PROGRESS_TOOL_NAMES.map((name) => ({ name, ...tools[name] }));
155
+
156
+ export function attachProgressTools(server: PluginServerContext): void {
157
+ server.before("agent.session_open", ({ request }) => {
158
+ // Paseo 0.13 has no tools list and fails the session open if a plugin adds one.
159
+ if (!Array.isArray(request.tools)) return;
160
+ return { ...request, tools: [...request.tools, ...progressTools] };
161
+ });
162
+ }
163
+
164
+ // Tools reach agents only while the host's "Enable Paseo tools" setting is on,
165
+ // which needs Paseo's tools (mcp.enabled) on as well.
166
+ export async function toolsState(supported: boolean, paseo: PluginHandlerContext["paseo"]): Promise<ToolsState> {
167
+ if (!supported) return "unsupported";
168
+ const { config } = await paseo.config.get();
169
+ return config.mcp.enabled !== false && config.mcp.injectIntoAgents ? "on" : "off";
170
+ }
171
+
172
+ export async function handleToolsStatus(_input: unknown, { paseo }: PluginHandlerContext): Promise<{ state: ToolsState }> {
173
+ return { state: await toolsState(agentToolsSupported, paseo) };
174
+ }
package/shared/events.ts CHANGED
@@ -7,8 +7,6 @@ import { QUESTION_MODES } from "./question-modes.ts";
7
7
  export const PROGRESS_FILE_NAME = "progress.jsonl";
8
8
  // Present once someone has opened the Progress panel for this worktree.
9
9
  export const PANEL_OPENED_NAME = "progress-panel-opened";
10
- // Where 0.1.x kept each worktree's files. store.ts moves them out.
11
- export const LEGACY_FOLDER = ".scratch";
12
10
  export const EVENT_VERSION = 1;
13
11
 
14
12
  export const TICKET_STATUSES = ["not_started", "working", "blocked", "done", "skipped"] as const;
package/shared/rpc.ts CHANGED
@@ -69,6 +69,17 @@ export const installLauncher = defineRpc({
69
69
  output: launcherStatus,
70
70
  });
71
71
 
72
+ // Whether agents get the Progress tools: "unsupported" before Paseo 0.14, then
73
+ // whether the host's "Enable Paseo tools" setting is "on" or "off".
74
+ export const TOOLS_STATES = ["unsupported", "off", "on"] as const;
75
+ export type ToolsState = (typeof TOOLS_STATES)[number];
76
+
77
+ export const getToolsStatus = defineRpc({
78
+ name: "progress.tools.status",
79
+ input: z.object({}),
80
+ output: z.object({ state: z.enum(TOOLS_STATES) }),
81
+ });
82
+
72
83
  // The paseo-progress skill, linked into each agent's skills folder.
73
84
  const skillState = z.enum(["missing", "current", "outdated", "foreign"]);
74
85
  const skillStatus = z.object({
@@ -0,0 +1,23 @@
1
+ // The tools Progress hands agents on Paseo 0.14 and later. The server defines
2
+ // one tool per name, and the chat draws Progress's own row for a call to any of them.
3
+ export const PROGRESS_TOOL_NAMES = [
4
+ "progress_show",
5
+ "progress_start",
6
+ "progress_ticket_add",
7
+ "progress_ticket_update",
8
+ "progress_question_ask",
9
+ "progress_question_answer",
10
+ "progress_deliverable_add",
11
+ "progress_activity_add",
12
+ "progress_ticker",
13
+ "progress_finish",
14
+ "paseo_progress",
15
+ ] as const;
16
+
17
+ export type ProgressToolName = (typeof PROGRESS_TOOL_NAMES)[number];
18
+
19
+ // Chats record a tool under the agent's own prefix: Claude Code's
20
+ // mcp__paseo__progress_ticket_add, or Codex's paseo.progress_ticket_add.
21
+ export function progressToolName(name: string): ProgressToolName | undefined {
22
+ return PROGRESS_TOOL_NAMES.find((tool) => name === tool || name.endsWith(`__${tool}`) || name.endsWith(`.${tool}`));
23
+ }
@@ -1,15 +1,19 @@
1
1
  ---
2
2
  name: paseo-progress
3
- description: Keep the Paseo Progress dashboard live with the paseo-progress command. Use when working through a spec or a set of tickets, on any job over 5 steps or 30 minutes, or when the user asks for a progress dashboard.
3
+ description: Keep the Paseo Progress dashboard live with the Progress tools or the paseo-progress command. Use when working through a spec or a set of tickets, on any job over 5 steps or 30 minutes, or when the user asks for a progress dashboard.
4
4
  ---
5
5
 
6
6
  # Paseo progress
7
7
 
8
- The user watches a live dashboard in Paseo for this worktree. Keep it live: run `paseo-progress` from inside the worktree the moment each change happens, so the dashboard is a side effect of the work. Write every line for a product manager, in plain words. `paseo-progress --help` is the reference for every command and flag.
8
+ The user watches a live dashboard in Paseo for this worktree. Keep it live: record each change the moment it happens, so the dashboard is a side effect of the work. Write every line for a product manager, in plain words.
9
+
10
+ Use the Progress tools when you have them: `progress_show`, `progress_start`, `progress_ticket_add`, `progress_ticket_update`, `progress_question_ask`, `progress_question_answer`, `progress_deliverable_add`, `progress_activity_add`, `progress_ticker`, `progress_finish`, and `paseo_progress` for any other command. Paseo hands them to agents when the user turns on Paseo tools. Claude Code names them like `mcp__paseo__progress_show` and keeps them hidden until you search, so search your tools for `progress_` before your first update. Otherwise, run the `paseo-progress` command from inside the worktree. When a tool call says the tool isn't found, use the command for the rest of that reply.
11
+
12
+ The tools and the command take the same values and print the same answers. The steps below use the command's words: `--estimate` is `estimateMinutes` in the tools, `--waits-for` is `waitsFor`. `paseo-progress --help` is the reference for every command and flag.
9
13
 
10
14
  ## 1. Set up
11
15
 
12
- Run `paseo-progress show` and continue an open run for this job. A Finished run is closed, so `start` a new one, then add every ticket in order:
16
+ Run `show` and continue an open run for this job. A Finished run is closed, so `start` a new one, then add every ticket in order:
13
17
 
14
18
  - Title each `"Ticket 01: <title>"`. The dashboard shortens titles to that prefix.
15
19
  - Give each an `--estimate`, plus `--waits-for` when it can't start before another and `--source` when a spec file or issue describes it.
@@ -27,16 +31,20 @@ Move each ticket through `--status working --stage Build`, then the stages `Revi
27
31
  - Put this in a subagent's prompt, with the worktree path and ticket id filled in. While you wait on it, its updates are the dashboard's only sign the work is moving:
28
32
 
29
33
  ```text
30
- Record progress on the Paseo dashboard as you work:
31
- - at each new stage: cd <worktree> && paseo-progress ticket update T03 --stage <stage>
32
- - between stages: cd <worktree> && paseo-progress ticker set "<what you are doing now>"
34
+ Record progress on the Paseo dashboard as you work. Use the Progress tools if you
35
+ have them (search your tools for progress_), otherwise the paseo-progress command.
36
+ If a tool call says the tool isn't found, use the command for the rest of that reply.
37
+ - at each new stage: progress_ticket_update with id T03 and stage <stage>,
38
+ or: cd <worktree> && paseo-progress ticket update T03 --stage <stage>
39
+ - between stages: progress_ticker with "<what you are doing now>",
40
+ or: cd <worktree> && paseo-progress ticker set "<what you are doing now>"
33
41
  ```
34
42
 
35
43
  Done when every ticket is done or skipped, or waits on a question.
36
44
 
37
45
  ## 3. Ask
38
46
 
39
- When a decision belongs to the user, ask it with `question ask`, lettered `--option`s and your `--recommend`. Read `paseo-progress question ask --help` before your first question. It defines a big question, which you mark with `--big "<the test it meets>"`.
47
+ When a decision belongs to the user, ask it with `question ask`, lettered `--option`s and your `--recommend`. Read the `progress_question_ask` description or `paseo-progress question ask --help` before your first question. Both define a big question, which you mark with `--big "<the test it meets>"`.
40
48
 
41
49
  The user picks the question mode in Paseo's Settings, and `question ask` prints what that mode means for this question. Do what it prints. Record the user's reply ("Q7 A", or their own words) with `question answer Q7 A --words "<their words>"`, and do what that prints too.
42
50
 
@@ -45,6 +53,6 @@ The user picks the question mode in Paseo's Settings, and `question ask` prints
45
53
 
46
54
  ## 4. Finish
47
55
 
48
- The job is over when every ticket is done or skipped and every question is answered. While questions wait on the user, leave the run open and tell the user in chat which ones. Then run `paseo-progress finish "<one-line outcome>"`, and settle whatever it lists if it refuses.
56
+ The job is over when every ticket is done or skipped and every question is answered. While questions wait on the user, leave the run open and tell the user in chat which ones. Then `finish` with a one-line outcome, and settle whatever it lists if it refuses.
49
57
 
50
58
  Done when `show` says Finished with the right outcome.