@stevecastaneda/paseo-progress 1.3.1 → 1.4.0

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.
@@ -0,0 +1,127 @@
1
+ // After an update, the panel shows what changed in this version, once. The
2
+ // notes come from the changelog in the plugin's own README, so they're
3
+ // written in one place. Whether they've been seen is kept per user on this
4
+ // computer.
5
+ import { readdir, readFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { z } from "zod";
9
+ import { NOTE_KINDS, type NoteKind, type WhatsNew, type WhatsNewNote } from "../shared/whats-new.ts";
10
+ import { spans } from "../shared/write-up.ts";
11
+ import { findPluginDirectory } from "./skill.ts";
12
+ import { stateRoot, writeAtomic } from "./store.ts";
13
+
14
+ export type Release = { version: string; readme: string | null };
15
+
16
+ const seenSchema = z.object({ seen: z.string() });
17
+ const packageSchema = z.object({ version: z.string() });
18
+
19
+ export function whatsNewPath(home = homedir()): string {
20
+ return join(stateRoot(home), "whats-new.json");
21
+ }
22
+
23
+ // The top-level bullets under `### <version>` in the README's `## Changelog`,
24
+ // or null when that version has none.
25
+ export function changelogNotes(readme: string, version: string): WhatsNewNote[] | null {
26
+ let section: "other" | "changelog" | "version" = "other";
27
+ const notes: string[] = [];
28
+ for (const line of readme.replace(/\r\n?/g, "\n").split("\n")) {
29
+ if (section === "version" && /^#{2,3}\s/.test(line)) break;
30
+ if (/^##\s/.test(line)) section = line.trim() === "## Changelog" ? "changelog" : "other";
31
+ else if (section === "changelog" && line.trim() === `### ${version}`) section = "version";
32
+ else if (section === "version" && /^[-*]\s/.test(line)) notes.push(line.slice(2).trim());
33
+ else if (section === "version" && notes.length && /^\s+\S/.test(line) && !/^\s+([-*+]|\d+[.)])\s/.test(line)) {
34
+ notes[notes.length - 1] += ` ${line.trim()}`;
35
+ }
36
+ }
37
+ return notes.length ? notes.map(note) : null;
38
+ }
39
+
40
+ // A bullet can open with a kind tag, like `[Fixed]`, and then a bold
41
+ // sentence, like `**Title.** Detail.`, which is its title.
42
+ function note(bullet: string): WhatsNewNote {
43
+ const tag = /^\[([a-z]+)\]\s*(.*)$/i.exec(bullet);
44
+ const kind = tag ? noteKind(tag[1]) : undefined;
45
+ const rest = kind ? tag![2] : bullet;
46
+ const text = rest.split(/(`[^`]+`)/).map((part, index) => index % 2 ? part : curly(part)).join("");
47
+ const lead = /^\*\*([^*]+?[.!?])\*\*\s*(.*)$/.exec(text);
48
+ return { ...(kind && { kind }), ...(lead ? { title: plain(lead[1]), body: lead[2] } : { body: text }) };
49
+ }
50
+
51
+ function noteKind(tag: string): NoteKind | undefined {
52
+ const key = tag.toLowerCase();
53
+ return Object.hasOwn(NOTE_KINDS, key) ? key as NoteKind : undefined;
54
+ }
55
+
56
+ // The README is typed with straight quotes; the panel shows typeset ones. Code keeps its own.
57
+ function curly(text: string): string {
58
+ return text.replace(/(^|[\s([{])"/g, "$1\u201c").replace(/"/g, "\u201d").replace(/(^|[\s([{])'/g, "$1\u2018").replace(/'/g, "\u2019");
59
+ }
60
+
61
+ function plain(markdown: string): string {
62
+ return spans(markdown).map((span) => span.text).join("");
63
+ }
64
+
65
+ // A note's title, or else the first sentence of its body, without Markdown.
66
+ export function headline(note: WhatsNewNote): string {
67
+ if (note.title) return note.title;
68
+ const body = plain(note.body);
69
+ return /^.*?[.!?](?=\s|$)/.exec(body)?.[0] ?? body;
70
+ }
71
+
72
+ async function readSeen(home: string): Promise<string | null> {
73
+ try {
74
+ return seenSchema.parse(JSON.parse(await readFile(whatsNewPath(home), "utf8"))).seen;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ export async function markWhatsNewSeen(version: string, home = homedir()): Promise<void> {
81
+ await writeAtomic(whatsNewPath(home), `${JSON.stringify({ seen: version })}\n`);
82
+ }
83
+
84
+ // A worktree's folder (see outsideDirectory) means the plugin was in use
85
+ // before this version, so this is an update, not a new install.
86
+ async function usedBefore(home: string): Promise<boolean> {
87
+ const entries = await readdir(stateRoot(home), { withFileTypes: true }).catch(() => []);
88
+ return entries.some((entry) => entry.isDirectory() && /-[0-9a-f]{8}$/.test(entry.name));
89
+ }
90
+
91
+ // What to show, or null. A new install has nothing to catch up on, so it
92
+ // records this version as seen.
93
+ export async function whatsNew({ version, readme }: Release, home = homedir()): Promise<WhatsNew | null> {
94
+ const notes = readme === null ? null : changelogNotes(readme, version);
95
+ if (!notes) return null;
96
+ const seen = await readSeen(home);
97
+ if (seen === version) return null;
98
+ if (seen === null && !(await usedBefore(home))) {
99
+ await markWhatsNewSeen(version, home);
100
+ return null;
101
+ }
102
+ return { version, headline: headline(notes[0]), notes };
103
+ }
104
+
105
+ // The server bundle can't read files beside it, so ask Paseo where the plugin
106
+ // is. Read once per load: an update reloads the bundle, which starts over.
107
+ let release: Promise<Release> | null = null;
108
+
109
+ function currentRelease(): Promise<Release> {
110
+ release ??= findPluginDirectory().then(async (directory) => ({
111
+ version: packageSchema.parse(JSON.parse(await readFile(join(directory, "package.json"), "utf8"))).version,
112
+ readme: await readFile(join(directory, "README.md"), "utf8").catch(() => null),
113
+ }));
114
+ release.catch(() => {
115
+ release = null;
116
+ });
117
+ return release;
118
+ }
119
+
120
+ export async function handleGetWhatsNew(): Promise<WhatsNew | null> {
121
+ return whatsNew(await currentRelease());
122
+ }
123
+
124
+ export async function handleDismissWhatsNew(input: { version: string }) {
125
+ await markWhatsNewSeen(input.version);
126
+ return {};
127
+ }
@@ -1,4 +1,5 @@
1
1
  import { EVENT_VERSION, eventSchema, type DELIVERABLE_KINDS, type FileLink, type ProgressEvent, type QuestionOption, type TicketStatus } from "./events.ts";
2
+ import { QUESTION_MODE_RULES, type QuestionState } from "./question-modes.ts";
2
3
 
3
4
  // One recorded change to a ticket, oldest first. `stage: ""` clears the stage.
4
5
  export interface TicketChange {
@@ -43,14 +44,18 @@ export interface Question {
43
44
  title: string;
44
45
  question: string;
45
46
  options: QuestionOption[];
46
- // The option the agent leans toward. Nothing is built until the user answers.
47
+ // The option the agent leans toward.
47
48
  recommended: string;
49
+ // What the agent does while the question is open.
50
+ state: QuestionState;
51
+ // Why the question holds even in Keep building mode.
52
+ big?: string;
48
53
  background?: string;
49
54
  files: FileLink[];
50
55
  raisedBy?: string;
51
56
  ticketId?: string;
52
57
  askedAt: string;
53
- // `changedCourse` only on older answers, from when agents built a default before asking.
58
+ // `changedCourse`: the agent had built another option and changed course.
54
59
  answer?: { choice: string; words?: string; changedCourse?: boolean; at: string };
55
60
  }
56
61
 
@@ -279,6 +284,8 @@ export function reduceProgress(parsed: { events: ParsedLine[]; issues: FileIssue
279
284
  question: event.question,
280
285
  options: event.options ?? [],
281
286
  recommended: event.recommend ?? event.default ?? "",
287
+ state: event.mode ? QUESTION_MODE_RULES[event.mode].state(Boolean(event.big)) : event.default !== undefined && !event.waits ? "building" : "waiting",
288
+ big: event.big,
282
289
  background: event.background,
283
290
  files: event.files ?? [],
284
291
  raisedBy: event.raisedBy,
package/shared/events.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { QUESTION_MODES } from "./question-modes.ts";
2
3
 
3
4
  // One line of `progress.jsonl`. The command-line tool writes these; the
4
5
  // dashboard reducer reads them. `v` lets later versions change a shape without
@@ -77,9 +78,11 @@ export const eventSchema = z.discriminatedUnion("type", [
77
78
  // A blocker that is not a ticket status, like a failing external service.
78
79
  z.object({ ...base, type: z.literal("stuck.set"), id: text, reason: text, ticket: text.optional() }),
79
80
  z.object({ ...base, type: z.literal("stuck.clear"), id: text }),
80
- // The agent asks, recommends an option, and builds none of them before the
81
- // answer: it works only on what the question doesn't affect. Older questions
82
- // carry `default` (the option the agent carried on with) and `waits` instead.
81
+ // The agent asks and recommends an option. `mode` is the question mode when
82
+ // asked; without it (0.3.0 to 1.3.x) the agent built nothing before the
83
+ // answer, like "work-around". `big` says why the question holds even in
84
+ // "keep-building" mode. Questions from before 0.3.0 carry `default`, the
85
+ // option the agent carried on with unless it `waits`.
83
86
  z.object({
84
87
  ...base,
85
88
  type: z.literal("question.ask"),
@@ -89,6 +92,8 @@ export const eventSchema = z.discriminatedUnion("type", [
89
92
  options: z.array(questionOptionSchema).max(26).optional(),
90
93
  // The agent's pick: an option letter, or a word such as "Looks right" when there are no options.
91
94
  recommend: text.optional(),
95
+ mode: z.enum(QUESTION_MODES).optional(),
96
+ big: text.optional(),
92
97
  default: text.optional(),
93
98
  waits: z.boolean().optional(),
94
99
  background: text.optional(),
@@ -104,7 +109,7 @@ export const eventSchema = z.discriminatedUnion("type", [
104
109
  choice: text,
105
110
  // The user's own words, quoted under the answer.
106
111
  words: text.optional(),
107
- // Older answers: true when the agent had to undo work built on its default.
112
+ // True when the agent had to undo work built on another option.
108
113
  changedCourse: z.boolean().optional(),
109
114
  }),
110
115
  // Adds detail to an open question; its reference and default stay.
@@ -0,0 +1,45 @@
1
+ // What an agent does when it asks the user a question, picked in Paseo's
2
+ // Settings. The command, the settings page, and the dashboard all read this
3
+ // table. No Paseo imports: the command's bundle includes this file.
4
+ export const QUESTION_MODES = ["stop", "work-around", "keep-building"] as const;
5
+ export type QuestionMode = (typeof QUESTION_MODES)[number];
6
+
7
+ // Also what a missing or damaged mirror means.
8
+ export const DEFAULT_QUESTION_MODE: QuestionMode = "work-around";
9
+
10
+ // An open question on the dashboard: the agent stopped all work, holds what the
11
+ // question affects, or builds its recommendation.
12
+ export type QuestionState = "stopped" | "waiting" | "building";
13
+
14
+ interface QuestionModeRules {
15
+ label: string;
16
+ hint: string;
17
+ // What the agent does until the answer. `question ask` prints it after "Mode: <label>."
18
+ askRule(question: { recommend: string; big: boolean }): string;
19
+ state(big: boolean): QuestionState;
20
+ }
21
+
22
+ const HOLD = "Build no option before the answer, not even your recommendation. Work on everything the question doesn't affect. If nothing else is left, end your turn, tell the user in chat which questions you're waiting on, and leave the run open.";
23
+
24
+ export const QUESTION_MODE_RULES: Record<QuestionMode, QuestionModeRules> = {
25
+ stop: {
26
+ label: "Stop",
27
+ hint: "Agents stop all work when they ask, until you answer.",
28
+ askRule: () => "Ask any other questions you already have, then stop all work and resume only after an answer. End your turn now, tell the user in chat which questions you're waiting on, and leave the run open.",
29
+ state: () => "stopped",
30
+ },
31
+ "work-around": {
32
+ label: "Work around",
33
+ hint: "Agents hold what your answer affects and keep working on the rest.",
34
+ askRule: () => HOLD,
35
+ state: () => "waiting",
36
+ },
37
+ "keep-building": {
38
+ label: "Keep building",
39
+ hint: "Agents build their recommended option while you decide, and change course if you pick another. Big questions still wait.",
40
+ askRule: ({ recommend, big }) => big
41
+ ? `This question is big, so hold it like Work around. ${HOLD}`
42
+ : `Build ${recommend} now and keep working. If the user picks another option, undo the work built on ${recommend} and build their choice first. If the rest of the work is done while it's still open, leave it open and tell the user in chat; don't withdraw it.`,
43
+ state: (big) => (big ? "waiting" : "building"),
44
+ },
45
+ };
package/shared/rpc.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineRpc } from "@getpaseo/plugin";
2
2
  import { z } from "zod";
3
3
  import type { Dashboard } from "./dashboard.ts";
4
+ import type { WhatsNew } from "./whats-new.ts";
4
5
 
5
6
  // `panelOpened`: the Progress panel has been opened for this worktree before.
6
7
  // `root`: the worktree root the file was read from, which stored paths are
@@ -163,3 +164,20 @@ export const readDeliverablePiece = defineRpc({
163
164
  }),
164
165
  output: z.object({ data: z.string(), bytes: z.number() }),
165
166
  });
167
+
168
+ // This version's changelog notes, once after an update; null otherwise.
169
+ // `headline` is the first sentence of the first note, without Markdown.
170
+ // A changelog bullet. `title` is its bold lead without Markdown, when it has
171
+ // one; `body` is the rest, Markdown kept.
172
+ export const getWhatsNew = defineRpc({
173
+ name: "progress.whats-new.get",
174
+ input: z.object({}),
175
+ output: z.custom<WhatsNew | null>(),
176
+ });
177
+
178
+ // The user closed it; it stays away for this version.
179
+ export const dismissWhatsNew = defineRpc({
180
+ name: "progress.whats-new.dismiss",
181
+ input: z.object({ version: z.string().min(1) }),
182
+ output: z.object({}),
183
+ });
@@ -0,0 +1,10 @@
1
+ import { defineSettings } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+ import { DEFAULT_QUESTION_MODE, QUESTION_MODES } from "./question-modes.ts";
4
+
5
+ export const questionSettings = defineSettings({
6
+ id: "questions",
7
+ scope: "host",
8
+ version: 1,
9
+ schema: z.object({ mode: z.enum(QUESTION_MODES).default(DEFAULT_QUESTION_MODE) }),
10
+ });
package/shared/show.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Dashboard } from "./dashboard.ts";
2
2
  import { headlineText, shortTitle } from "./dashboard.ts";
3
3
  import { formatAgo, formatClock, formatMinutes, formatWorkDone, minutesSince } from "./format.ts";
4
+ import type { QuestionState } from "./question-modes.ts";
4
5
 
5
6
  const STATUS: Record<string, string> = { not_started: "Not started", working: "Working", blocked: "Blocked", done: "Done", skipped: "Skipped" };
7
+ const QUESTION_STATE: Record<QuestionState, string> = { stopped: "agent stopped until the answer", waiting: "waiting for the answer", building: "building with it" };
6
8
 
7
9
  // The dashboard as plain text, so an agent can check what the user sees.
8
10
  export function dashboardText(dashboard: Dashboard, nowMs: number): string {
@@ -44,7 +46,7 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
44
46
  if (dashboard.questions.open.length) {
45
47
  lines.push("", "Questions waiting");
46
48
  for (const question of dashboard.questions.open) {
47
- lines.push(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}`);
49
+ lines.push(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}, ${QUESTION_STATE[question.state]}${question.big ? `. Big: ${question.big}` : ""}`);
48
50
  for (const option of question.options) lines.push(` ${option.letter}) ${option.label}${option.consequence ? `: ${option.consequence}` : ""}`);
49
51
  }
50
52
  }
@@ -0,0 +1,16 @@
1
+ // A changelog bullet can open with a tag, like `- [Fixed] **Title.** Detail.`,
2
+ // that says what kind of change it is. The parser and the dialog both read
3
+ // this table. `tone` is the hue the dialog colors the tag with.
4
+ export const NOTE_KINDS = {
5
+ new: { label: "New", tone: "green" },
6
+ improved: { label: "Improved", tone: "violet" },
7
+ fixed: { label: "Fixed", tone: "amber" },
8
+ } as const;
9
+
10
+ export type NoteTone = (typeof NOTE_KINDS)[NoteKind]["tone"];
11
+
12
+ export type NoteKind = keyof typeof NOTE_KINDS;
13
+
14
+ export type WhatsNewNote = { kind?: NoteKind; title?: string; body: string };
15
+
16
+ export type WhatsNew = { version: string; headline: string; notes: WhatsNewNote[] };
@@ -1,9 +1,9 @@
1
1
  // A ticket's write-up (its Markdown source file) as simple blocks the dialog
2
2
  // can draw without a Markdown renderer: headings, paragraphs, list items and
3
- // code. Inline marks are kept as spans: `code` stays code, **bold** and
4
- // _emphasis_ lose their markers, and [links](url) keep only their text.
3
+ // code. Inline marks are kept as spans: `code` stays code, **bold** stays
4
+ // bold, _emphasis_ loses its markers, and [links](url) keep only their text.
5
5
 
6
- export type Span = { text: string; code?: boolean };
6
+ export type Span = { text: string; code?: boolean; strong?: boolean };
7
7
 
8
8
  export type Block =
9
9
  | { kind: "heading"; level: number; spans: Span[] }
@@ -20,11 +20,12 @@ export function spans(text: string): Span[] {
20
20
  result.push({ text: part.slice(1, -1), code: true });
21
21
  return;
22
22
  }
23
- const plain = part
24
- .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
25
- .replace(/(\*\*|__)(.+?)\1/g, "$2")
26
- .replace(/(^|[\s(])[*_](\S(?:.*?\S)?)[*_](?=[\s).,;:!?]|$)/g, "$1$2");
27
- result.push({ text: plain });
23
+ part.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").split(/((?:\*\*|__).+?(?:\*\*|__))/).forEach((piece) => {
24
+ if (!piece) return;
25
+ const strong = /^(\*\*|__).+\1$/.test(piece);
26
+ const text = (strong ? piece.slice(2, -2) : piece).replace(/(^|[\s(])[*_](\S(?:.*?\S)?)[*_](?=[\s).,;:!?]|$)/g, "$1$2");
27
+ result.push(strong ? { text, strong } : { text });
28
+ });
28
29
  });
29
30
  return result;
30
31
  }
@@ -1,92 +1,50 @@
1
1
  ---
2
2
  name: paseo-progress
3
- description: Record progress on the Paseo Progress dashboard with the paseo-progress command. Use when implementing a spec or a set of tickets, running any job with more than 5 steps or over 30 minutes, or when the user asks for a progress dashboard.
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.
4
4
  ---
5
5
 
6
6
  # Paseo progress
7
7
 
8
- The user watches a live dashboard in Paseo, drawn from the progress file in this worktree. You keep it true by running `paseo-progress` the moment each change happens: the dashboard is a side effect of the work, never a summary written afterwards. Run it from inside the worktree you are working in: it records to that worktree's dashboard, and `show` names the worktree it read. `paseo-progress --help` lists every command; `paseo-progress <command> --help` gives its options.
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.
9
9
 
10
- The user answers questions by reference ("Q7 A") and reads everything else at a glance, so every line you record is written for a product manager: plain words, no jargon.
10
+ ## 1. Set up
11
11
 
12
- ## 1. Set up the run
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:
13
13
 
14
- Run `paseo-progress show`. When it shows an open run for this job, continue it. A run marked Finished is closed, even if it looks like your job; start a new one. Otherwise:
15
-
16
- 1. `paseo-progress start "<job title>"`, with `--subtitle` naming the spec or ticket folder.
17
- 2. Add every ticket in order with `paseo-progress ticket add "Ticket 01: <title>" --estimate <minutes>`, with `--waits-for T02` on any that can't start before another finishes. Keep the "Ticket NN:" prefix: the dashboard shortens titles to it. When a ticket is written up somewhere, a spec file or an issue, pass `--source <path or link>` so the user can open it from the ticket. For a job without tickets, add its steps instead and pass `--item-label Step` to `start`; give a step `--source` only when it has a write-up.
18
- 3. `paseo-progress activity add "<one line: what this run will do>"`.
14
+ - Title each `"Ticket 01: <title>"`. The dashboard shortens titles to that prefix.
15
+ - Give each an `--estimate`, plus `--waits-for` when it can't start before another and `--source` when a spec file or issue describes it.
16
+ - For a job without tickets, add its steps and pass `--item-label Step` to `start`.
19
17
 
20
18
  Done when `show` lists every ticket with an estimate.
21
19
 
22
- ## 2. Work each ticket
23
-
24
- Record each change as it happens, on the ticket's id from `show` (T01, T02, ...):
25
-
26
- | When | Command |
27
- | --- | --- |
28
- | You start a ticket | `ticket update T03 --status working --stage Build` |
29
- | You move to the next stage | `ticket update T03 --stage Review`, then `Fixes`, then `Verify` |
30
- | Something reviewable lands (a screenshot folder, a screen recording, a report, a preview URL) | `deliverable add "<what it shows>" <path or URL> --ticket T03` |
31
- | A milestone the user would want in the story | `activity add "<one line>" --ticket T03` |
32
- | Between bigger events | `ticker set "<what you are doing right now>"` |
33
- | The ticket is finished | `ticket update T03 --status done`, then start the next one |
34
- | The plan drops it | `ticket update T03 --status skipped --note "<why>"` |
35
-
36
- Pass `--ticket` on every activity, deliverable and question that belongs to a ticket. The user presses a ticket to see its story: each stage and how long it took, with the activity, deliverables and questions tagged to it. Leave it off only for run-wide notes. Stage changes and notes are timed from your `ticket update` calls, so make them when the change happens.
37
-
38
- Other sessions may add tickets (or steps) to the same run. When a command's output says another session added one, it is part of the plan: work it in order after your current one, the same as the ones you added.
39
-
40
- A working ticket turns stuck on its own once it goes longer than its estimate without an update: a status, stage or note change, or activity tagged to it. When a ticket will run long, keep updating its stage, or raise its estimate with `--estimate`.
41
-
42
- A subagent records the work you hand it. Paseo shows you as idle while you wait on it, so its updates are the only sign the work is still going; after 15 minutes without one, the dashboard warns that the work may have stopped. Mark the ticket working, then put this in the subagent's prompt with this worktree's absolute path and the ticket id filled in:
43
-
44
- ```text
45
- Record progress on the Paseo dashboard as you work:
46
- - at each new stage: cd <worktree> && paseo-progress ticket update T03 --stage <stage>
47
- - between stages: cd <worktree> && paseo-progress ticker set "<what you are doing now>"
48
- ```
49
-
50
- The `cd` puts the updates on this worktree's dashboard even when the subagent works in another checkout. When the subagent reports back, mark the ticket done.
51
-
52
- ## 3. Ask questions, and work around them
20
+ ## 2. Work
53
21
 
54
- When a decision belongs to the user, ask it, then park everything the answer affects:
22
+ Move each ticket through `--status working --stage Build`, then the stages `Review`, `Fixes` and `Verify`, then `--status done`. Along the way, record what the user would want to see: `deliverable add` for anything reviewable, `activity add` for milestones, `ticker set` for what you are doing right now. Tag each with `--ticket` unless it covers the whole run.
55
23
 
56
- ```sh
57
- paseo-progress question ask "<short title>" "<the question, in one or two sentences>" \
58
- --option "A=<choice> | <what happens if they pick it>" \
59
- --option "B=<choice> | <what happens if they pick it>" \
60
- --recommend <letter> --ticket T03 --raised-by "Ticket 03 design review" \
61
- [--background "<what they need to decide without opening the worktree>"] \
62
- [--file <path or http(s) URL>=<what it shows>]
63
- ```
24
+ - A ticket past its estimate with no update turns stuck. Keep its stage current, or raise its `--estimate`.
25
+ - A ticket another session adds is part of the plan. Work it in order.
26
+ - `--status blocked` and `stuck set` are for things outside the run, like a service that's down or missing access.
27
+ - 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:
64
28
 
65
- - `--recommend` is the option you would pick, so the user knows which way to lean.
66
- - Build no option before the user answers, not even the one you recommend. The point of asking is that the user decides before the work is done.
67
- - Keep working on whatever the question doesn't affect: other tickets, or other parts of this one. If nothing else is left, stop, tell the user in chat which questions you're waiting on, and leave the run open.
68
- - When the user replies ("Q7 A", or in their own words), run `question answer Q7 A --words "<their words>"`, then build what they chose.
69
- - When a question is about how something looks, attach the screenshot, mockup or preview link with `--file`. The user opens it from the question: images and videos preview in Paseo, links open in its browser. Add more later with `question update Q7 --file <path>=<what it shows>`.
70
- - When the user asks about a question, answer in chat and also put the explanation on the question with `question update Q7 --background "<text>"`, so the dashboard carries it too.
71
- - `question remove Q7` withdraws a question that no longer applies.
29
+ ```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>"
33
+ ```
72
34
 
73
- ## 4. Flag what is stuck
35
+ Done when every ticket is done or skipped, or waits on a question.
74
36
 
75
- - A ticket that has to wait for another ticket in this run is not stuck; that's just the order of work. Record it with `ticket add ... --waits-for T03` (or `ticket update T05 --waits-for T03`) and leave its status alone. The dashboard shows it calmly as waiting until T03 is done.
76
- - A ticket held up by something outside the run (a service, access, a decision nobody has made): `ticket update T03 --status blocked --note "<what it waits on>"`. That shows as stuck. Set it back to `working` when it moves again.
77
- - A blocker outside any ticket (a failing service, a missing key): `stuck set "<reason>" [--ticket T03]`, then `stuck clear S1` once it is gone.
37
+ ## 3. Ask
78
38
 
79
- ## 5. Finish
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>"`.
80
40
 
81
- When the job is over:
41
+ 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.
82
42
 
83
- 1. Mark the last ticket done or skipped.
84
- 2. Settle every open question: record the user's answer, or withdraw it with `question remove` when it no longer applies. Clear flagged blockers with `stuck clear`.
85
- 3. `paseo-progress finish "<one line: the outcome>"`. It refuses, and says what is still open, until the steps above are done.
86
- 4. Run `paseo-progress show`.
43
+ - When the question is about how something looks, attach the screenshot or link with `--file`.
44
+ - When the user asks about a question, answer in chat and add the explanation with `question update Q7 --background`.
87
45
 
88
- The finished run stays on the dashboard, marked Finished with its outcome, until the next `start`. It takes no more updates.
46
+ ## 4. Finish
89
47
 
90
- If the job pauses with questions only the user can answer, don't finish: leave the run open so the questions stay in front of them.
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.
91
49
 
92
50
  Done when `show` says Finished with the right outcome.