@stevecastaneda/paseo-progress 1.10.0 → 1.11.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.
package/README.md CHANGED
@@ -145,6 +145,10 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
145
145
 
146
146
  ## Changelog
147
147
 
148
+ ### 1.11.0
149
+
150
+ - [Improved] **Agents can change their recommendation.** When you ask an agent about an open question and explaining it changes the agent's mind, it can now change its recommendation, and the question card shows the new one. In Keep building mode, the agent also switches the work it already built to the new recommendation.
151
+
148
152
  ### 1.10.0
149
153
 
150
154
  - [Improved] **Progress opens in Explorer on phones.** On Paseo 0.13 or later, the phone app opens Progress in Explorer, as the desktop app does, and lists it in Explorer's New tab menu. If a Progress tab from an earlier version is still open on your phone, you can close it.
@@ -19796,14 +19796,14 @@ var eventSchema = external_exports.discriminatedUnion("type", [
19796
19796
  // True when the agent had to undo work built on another option.
19797
19797
  changedCourse: external_exports.boolean().optional()
19798
19798
  }),
19799
- // Adds detail to an open question; its reference and default stay.
19800
19799
  external_exports.object({
19801
19800
  ...base,
19802
19801
  type: external_exports.literal("question.update"),
19803
19802
  id: text,
19804
19803
  question: text.optional(),
19805
19804
  background: text.optional(),
19806
- files: external_exports.array(fileLinkSchema).optional()
19805
+ files: external_exports.array(fileLinkSchema).optional(),
19806
+ recommend: text.optional()
19807
19807
  }),
19808
19808
  external_exports.object({ ...base, type: external_exports.literal("question.remove"), id: text }),
19809
19809
  // `path` is relative to the worktree root, or absolute when outside it.
@@ -19971,6 +19971,7 @@ function reduceProgress(parsed, now) {
19971
19971
  if (event.question !== void 0) question.question = event.question;
19972
19972
  if (event.background !== void 0) question.background = event.background;
19973
19973
  if (event.files !== void 0) question.files = [...question.files, ...event.files];
19974
+ if (event.recommend !== void 0) question.recommended = event.recommend;
19974
19975
  break;
19975
19976
  }
19976
19977
  case "question.remove":
@@ -20702,6 +20703,13 @@ function existingQuestion(state, id) {
20702
20703
  }
20703
20704
  return question;
20704
20705
  }
20706
+ function recommendOption(recommend, letters) {
20707
+ if (!letters.length) return recommend;
20708
+ if (!/^[A-Za-z]$/.test(recommend) || !letters.includes(recommend.toUpperCase())) {
20709
+ throw new UsageError(`--recommend must be one of the option letters: ${letters.join(", ")}.`);
20710
+ }
20711
+ return recommend.toUpperCase();
20712
+ }
20705
20713
  function existingTicket(state, id) {
20706
20714
  const wanted = required2(id, "ticket id").toUpperCase();
20707
20715
  const ticket = state.dashboard.tickets.find((candidate) => candidate.id.toUpperCase() === wanted);
@@ -20840,13 +20848,7 @@ ${modeLine(mode)}`
20840
20848
  const letters = options.map((option) => option.letter);
20841
20849
  const duplicate = letters.find((letter, index) => letters.indexOf(letter) !== index);
20842
20850
  if (duplicate) throw new UsageError(`Option ${duplicate} is given twice.`);
20843
- let recommend = required2(stringOption(values.recommend), "--recommend (the option you would pick)");
20844
- if (options.length) {
20845
- if (!/^[A-Za-z]$/.test(recommend) || !letters.includes(recommend.toUpperCase())) {
20846
- throw new UsageError(`--recommend must be one of the option letters: ${letters.join(", ")}.`);
20847
- }
20848
- recommend = recommend.toUpperCase();
20849
- }
20851
+ const recommend = recommendOption(required2(stringOption(values.recommend), "--recommend (the option you would pick)"), letters);
20850
20852
  const id = state.nextIds.question;
20851
20853
  const big = values.big === void 0 ? void 0 : required2(stringOption(values.big), "--big reason (why the question is big)");
20852
20854
  const rules = QUESTION_MODE_RULES[mode];
@@ -20892,21 +20894,32 @@ ${modeLine(mode)}`
20892
20894
  }
20893
20895
  },
20894
20896
  "question update": {
20895
- usage: `question update <id> [--background <text>] [--question <text>] [--file <path or http(s) URL>[=<label>] ...]`,
20896
- summary: "Add detail to a question: replace its background or wording, or add files. Its reference and default stay.",
20897
- options: { background: { type: "string" }, question: { type: "string" }, file: { type: "string", multiple: true } },
20898
- run({ positionals, values, state, cwd, root }) {
20897
+ usage: `question update <id> [--recommend <letter or word>] [--background <text>] [--question <text>] [--file <path or http(s) URL>[=<label>] ...]`,
20898
+ summary: "Add detail to a question: change your recommendation while it's open, replace its background or wording, or add files. Its reference stays.",
20899
+ options: { recommend: { type: "string" }, background: { type: "string" }, question: { type: "string" }, file: { type: "string", multiple: true } },
20900
+ run({ positionals, values, state, cwd, root, mode }) {
20899
20901
  const question = existingQuestion(state, positionals[0]);
20900
20902
  const files = listOption(values.file).map((raw) => parseFile(raw, cwd, root));
20903
+ let recommend;
20904
+ if (values.recommend !== void 0) {
20905
+ if (question.answer) throw new UsageError(`${question.id} was already answered, so its recommendation can't change.`);
20906
+ const letters = question.options.map((option) => option.letter);
20907
+ recommend = recommendOption(required2(stringOption(values.recommend), "--recommend (the option you would pick)"), letters);
20908
+ if (recommend === question.recommended) throw new UsageError(`${question.id} already recommends ${recommend}.`);
20909
+ }
20901
20910
  const event = {
20902
20911
  type: "question.update",
20903
20912
  id: question.id,
20904
20913
  question: stringOption(values.question),
20905
20914
  background: stringOption(values.background),
20906
- files: files.length ? files : void 0
20915
+ files: files.length ? files : void 0,
20916
+ recommend
20907
20917
  };
20908
- if (!event.question && !event.background && !event.files) throw new UsageError("Nothing to change. Pass --background, --question, or --file.");
20909
- return { event, message: `Updated ${question.id} (${question.title})` };
20918
+ if (!event.question && !event.background && !event.files && !recommend) throw new UsageError("Nothing to change. Pass --recommend, --background, --question, or --file.");
20919
+ if (!recommend) return { event, message: `Updated ${question.id} (${question.title})` };
20920
+ const rules = QUESTION_MODE_RULES[mode];
20921
+ const next = question.state === "building" ? `Undo the work built on ${question.recommended} and build ${recommend} instead.` : `Mode: ${rules.label}. ${rules.askRule({ recommend, big: Boolean(question.big) })}`;
20922
+ return { event, message: `${question.id} now recommends ${recommend} (was ${question.recommended}). ${next}` };
20910
20923
  }
20911
20924
  },
20912
20925
  "question remove": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the paseo-progress command.",
5
5
  "license": "MIT",
6
6
  "author": "Steve Castaneda",
package/server/cli.ts CHANGED
@@ -121,6 +121,14 @@ function existingQuestion(state: State, id: string | undefined) {
121
121
  return question;
122
122
  }
123
123
 
124
+ function recommendOption(recommend: string, letters: string[]): string {
125
+ if (!letters.length) return recommend;
126
+ if (!/^[A-Za-z]$/.test(recommend) || !letters.includes(recommend.toUpperCase())) {
127
+ throw new UsageError(`--recommend must be one of the option letters: ${letters.join(", ")}.`);
128
+ }
129
+ return recommend.toUpperCase();
130
+ }
131
+
124
132
  function existingTicket(state: State, id: string | undefined) {
125
133
  const wanted = required(id, "ticket id").toUpperCase();
126
134
  const ticket = state.dashboard.tickets.find((candidate) => candidate.id.toUpperCase() === wanted);
@@ -269,13 +277,7 @@ const commands: Record<string, Command> = {
269
277
  const letters = options.map((option) => option.letter);
270
278
  const duplicate = letters.find((letter, index) => letters.indexOf(letter) !== index);
271
279
  if (duplicate) throw new UsageError(`Option ${duplicate} is given twice.`);
272
- let recommend = required(stringOption(values.recommend), "--recommend (the option you would pick)");
273
- if (options.length) {
274
- if (!/^[A-Za-z]$/.test(recommend) || !letters.includes(recommend.toUpperCase())) {
275
- throw new UsageError(`--recommend must be one of the option letters: ${letters.join(", ")}.`);
276
- }
277
- recommend = recommend.toUpperCase();
278
- }
280
+ const recommend = recommendOption(required(stringOption(values.recommend), "--recommend (the option you would pick)"), letters);
279
281
  const id = state.nextIds.question;
280
282
  const big = values.big === undefined ? undefined : required(stringOption(values.big), "--big reason (why the question is big)");
281
283
  const rules = QUESTION_MODE_RULES[mode];
@@ -323,21 +325,34 @@ const commands: Record<string, Command> = {
323
325
  },
324
326
  },
325
327
  "question update": {
326
- usage: `question update <id> [--background <text>] [--question <text>] [--file <path or http(s) URL>[=<label>] ...]`,
327
- summary: "Add detail to a question: replace its background or wording, or add files. Its reference and default stay.",
328
- options: { background: { type: "string" }, question: { type: "string" }, file: { type: "string", multiple: true } },
329
- run({ positionals, values, state, cwd, root }) {
328
+ usage: `question update <id> [--recommend <letter or word>] [--background <text>] [--question <text>] [--file <path or http(s) URL>[=<label>] ...]`,
329
+ summary: "Add detail to a question: change your recommendation while it's open, replace its background or wording, or add files. Its reference stays.",
330
+ options: { recommend: { type: "string" }, background: { type: "string" }, question: { type: "string" }, file: { type: "string", multiple: true } },
331
+ run({ positionals, values, state, cwd, root, mode }) {
330
332
  const question = existingQuestion(state, positionals[0]);
331
333
  const files = listOption(values.file).map((raw) => parseFile(raw, cwd, root));
334
+ let recommend: string | undefined;
335
+ if (values.recommend !== undefined) {
336
+ if (question.answer) throw new UsageError(`${question.id} was already answered, so its recommendation can't change.`);
337
+ const letters = question.options.map((option) => option.letter);
338
+ recommend = recommendOption(required(stringOption(values.recommend), "--recommend (the option you would pick)"), letters);
339
+ if (recommend === question.recommended) throw new UsageError(`${question.id} already recommends ${recommend}.`);
340
+ }
332
341
  const event = {
333
342
  type: "question.update" as const,
334
343
  id: question.id,
335
344
  question: stringOption(values.question),
336
345
  background: stringOption(values.background),
337
346
  files: files.length ? files : undefined,
347
+ recommend,
338
348
  };
339
- if (!event.question && !event.background && !event.files) throw new UsageError("Nothing to change. Pass --background, --question, or --file.");
340
- return { event, message: `Updated ${question.id} (${question.title})` };
349
+ if (!event.question && !event.background && !event.files && !recommend) throw new UsageError("Nothing to change. Pass --recommend, --background, --question, or --file.");
350
+ if (!recommend) return { event, message: `Updated ${question.id} (${question.title})` };
351
+ const rules = QUESTION_MODE_RULES[mode];
352
+ const next = question.state === "building"
353
+ ? `Undo the work built on ${question.recommended} and build ${recommend} instead.`
354
+ : `Mode: ${rules.label}. ${rules.askRule({ recommend, big: Boolean(question.big) })}`;
355
+ return { event, message: `${question.id} now recommends ${recommend} (was ${question.recommended}). ${next}` };
341
356
  },
342
357
  },
343
358
  "question remove": {
@@ -308,6 +308,7 @@ export function reduceProgress(parsed: { events: ParsedLine[]; issues: FileIssue
308
308
  if (event.question !== undefined) question.question = event.question;
309
309
  if (event.background !== undefined) question.background = event.background;
310
310
  if (event.files !== undefined) question.files = [...question.files, ...event.files];
311
+ if (event.recommend !== undefined) question.recommended = event.recommend;
311
312
  break;
312
313
  }
313
314
  case "question.remove":
package/shared/events.ts CHANGED
@@ -112,7 +112,6 @@ export const eventSchema = z.discriminatedUnion("type", [
112
112
  // True when the agent had to undo work built on another option.
113
113
  changedCourse: z.boolean().optional(),
114
114
  }),
115
- // Adds detail to an open question; its reference and default stay.
116
115
  z.object({
117
116
  ...base,
118
117
  type: z.literal("question.update"),
@@ -120,6 +119,7 @@ export const eventSchema = z.discriminatedUnion("type", [
120
119
  question: text.optional(),
121
120
  background: text.optional(),
122
121
  files: z.array(fileLinkSchema).optional(),
122
+ recommend: text.optional(),
123
123
  }),
124
124
  z.object({ ...base, type: z.literal("question.remove"), id: text }),
125
125
  // `path` is relative to the worktree root, or absolute when outside it.
@@ -41,7 +41,7 @@ When a decision belongs to the user, ask it with `question ask`, lettered `--opt
41
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.
42
42
 
43
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`.
44
+ - When the user asks about a question, answer in chat and add the explanation with `question update Q7 --background`. If explaining it changes your mind, also pass `--recommend` with the new option, say in `--background` what changed, and do what it prints.
45
45
 
46
46
  ## 4. Finish
47
47