@stevecastaneda/paseo-progress 1.3.0 → 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.
- package/OVERVIEW.md +1 -1
- package/README.md +34 -5
- package/client/launcher.tsx +2 -2
- package/client/panel.tsx +30 -10
- package/client/questions.tsx +20 -4
- package/client/settings.tsx +40 -0
- package/client/surfaces.ts +20 -8
- package/client/ticket-dialog.tsx +2 -1
- package/client/whats-new.tsx +120 -0
- package/client/write-up.tsx +3 -2
- package/dist/server/cli.js +140 -37
- package/images/1-panel.png +0 -0
- package/images/3-question.png +0 -0
- package/images/5-settings.png +0 -0
- package/index.client.tsx +12 -1
- package/index.server.ts +11 -2
- package/package.json +2 -1
- package/paseo-plugin.json +2 -1
- package/server/cli.ts +28 -11
- package/server/launcher.ts +81 -21
- package/server/question-mode.ts +61 -0
- package/server/skill.ts +15 -6
- package/server/store.ts +21 -4
- package/server/whats-new.ts +127 -0
- package/shared/dashboard.ts +9 -2
- package/shared/events.ts +9 -4
- package/shared/question-modes.ts +45 -0
- package/shared/rpc.ts +18 -0
- package/shared/settings.ts +10 -0
- package/shared/show.ts +3 -1
- package/shared/whats-new.ts +16 -0
- package/shared/write-up.ts +9 -8
- package/skills/paseo-progress/SKILL.md +26 -68
package/dist/server/cli.js
CHANGED
|
@@ -2007,8 +2007,8 @@ function emoji() {
|
|
|
2007
2007
|
}
|
|
2008
2008
|
var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
|
|
2009
2009
|
var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
|
2010
|
-
var mac = (
|
|
2011
|
-
const escapedDelim = escapeRegex(
|
|
2010
|
+
var mac = (delimiter) => {
|
|
2011
|
+
const escapedDelim = escapeRegex(delimiter ?? ":");
|
|
2012
2012
|
return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
|
|
2013
2013
|
};
|
|
2014
2014
|
var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
@@ -19678,6 +19678,31 @@ function date4(params) {
|
|
|
19678
19678
|
return _coercedDate(ZodDate, params);
|
|
19679
19679
|
}
|
|
19680
19680
|
|
|
19681
|
+
// shared/question-modes.ts
|
|
19682
|
+
var QUESTION_MODES = ["stop", "work-around", "keep-building"];
|
|
19683
|
+
var DEFAULT_QUESTION_MODE = "work-around";
|
|
19684
|
+
var 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.";
|
|
19685
|
+
var QUESTION_MODE_RULES = {
|
|
19686
|
+
stop: {
|
|
19687
|
+
label: "Stop",
|
|
19688
|
+
hint: "Agents stop all work when they ask, until you answer.",
|
|
19689
|
+
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.",
|
|
19690
|
+
state: () => "stopped"
|
|
19691
|
+
},
|
|
19692
|
+
"work-around": {
|
|
19693
|
+
label: "Work around",
|
|
19694
|
+
hint: "Agents hold what your answer affects and keep working on the rest.",
|
|
19695
|
+
askRule: () => HOLD,
|
|
19696
|
+
state: () => "waiting"
|
|
19697
|
+
},
|
|
19698
|
+
"keep-building": {
|
|
19699
|
+
label: "Keep building",
|
|
19700
|
+
hint: "Agents build their recommended option while you decide, and change course if you pick another. Big questions still wait.",
|
|
19701
|
+
askRule: ({ recommend, big }) => big ? `This question is big, so hold it like Work around. ${HOLD}` : `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.`,
|
|
19702
|
+
state: (big) => big ? "waiting" : "building"
|
|
19703
|
+
}
|
|
19704
|
+
};
|
|
19705
|
+
|
|
19681
19706
|
// shared/events.ts
|
|
19682
19707
|
var PROGRESS_FILE_NAME = "progress.jsonl";
|
|
19683
19708
|
var PANEL_OPENED_NAME = "progress-panel-opened";
|
|
@@ -19737,9 +19762,11 @@ var eventSchema = external_exports.discriminatedUnion("type", [
|
|
|
19737
19762
|
// A blocker that is not a ticket status, like a failing external service.
|
|
19738
19763
|
external_exports.object({ ...base, type: external_exports.literal("stuck.set"), id: text, reason: text, ticket: text.optional() }),
|
|
19739
19764
|
external_exports.object({ ...base, type: external_exports.literal("stuck.clear"), id: text }),
|
|
19740
|
-
// The agent asks
|
|
19741
|
-
//
|
|
19742
|
-
//
|
|
19765
|
+
// The agent asks and recommends an option. `mode` is the question mode when
|
|
19766
|
+
// asked; without it (0.3.0 to 1.3.x) the agent built nothing before the
|
|
19767
|
+
// answer, like "work-around". `big` says why the question holds even in
|
|
19768
|
+
// "keep-building" mode. Questions from before 0.3.0 carry `default`, the
|
|
19769
|
+
// option the agent carried on with unless it `waits`.
|
|
19743
19770
|
external_exports.object({
|
|
19744
19771
|
...base,
|
|
19745
19772
|
type: external_exports.literal("question.ask"),
|
|
@@ -19749,6 +19776,8 @@ var eventSchema = external_exports.discriminatedUnion("type", [
|
|
|
19749
19776
|
options: external_exports.array(questionOptionSchema).max(26).optional(),
|
|
19750
19777
|
// The agent's pick: an option letter, or a word such as "Looks right" when there are no options.
|
|
19751
19778
|
recommend: text.optional(),
|
|
19779
|
+
mode: external_exports.enum(QUESTION_MODES).optional(),
|
|
19780
|
+
big: text.optional(),
|
|
19752
19781
|
default: text.optional(),
|
|
19753
19782
|
waits: external_exports.boolean().optional(),
|
|
19754
19783
|
background: text.optional(),
|
|
@@ -19764,7 +19793,7 @@ var eventSchema = external_exports.discriminatedUnion("type", [
|
|
|
19764
19793
|
choice: text,
|
|
19765
19794
|
// The user's own words, quoted under the answer.
|
|
19766
19795
|
words: text.optional(),
|
|
19767
|
-
//
|
|
19796
|
+
// True when the agent had to undo work built on another option.
|
|
19768
19797
|
changedCourse: external_exports.boolean().optional()
|
|
19769
19798
|
}),
|
|
19770
19799
|
// Adds detail to an open question; its reference and default stay.
|
|
@@ -19918,6 +19947,8 @@ function reduceProgress(parsed, now) {
|
|
|
19918
19947
|
question: event.question,
|
|
19919
19948
|
options: event.options ?? [],
|
|
19920
19949
|
recommended: event.recommend ?? event.default ?? "",
|
|
19950
|
+
state: event.mode ? QUESTION_MODE_RULES[event.mode].state(Boolean(event.big)) : event.default !== void 0 && !event.waits ? "building" : "waiting",
|
|
19951
|
+
big: event.big,
|
|
19921
19952
|
background: event.background,
|
|
19922
19953
|
files: event.files ?? [],
|
|
19923
19954
|
raisedBy: event.raisedBy,
|
|
@@ -20250,7 +20281,7 @@ function findRoot(cwd) {
|
|
|
20250
20281
|
|
|
20251
20282
|
// server/launcher.ts
|
|
20252
20283
|
import { chmod, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
20253
|
-
import {
|
|
20284
|
+
import { basename, dirname as dirname2, join as join3, posix, win32 } from "node:path";
|
|
20254
20285
|
var PLUGIN_ID = "progress";
|
|
20255
20286
|
function launcherScript() {
|
|
20256
20287
|
return `#!/usr/bin/env node
|
|
@@ -20266,11 +20297,28 @@ const { spawnSync } = process.getBuiltinModule("node:child_process");
|
|
|
20266
20297
|
const { existsSync } = process.getBuiltinModule("node:fs");
|
|
20267
20298
|
const { join } = process.getBuiltinModule("node:path");
|
|
20268
20299
|
|
|
20300
|
+
const windows = process.platform === "win32";
|
|
20301
|
+
|
|
20302
|
+
function paseoCommands() {
|
|
20303
|
+
const env = process.env;
|
|
20304
|
+
const installed = windows
|
|
20305
|
+
? env.LOCALAPPDATA && join(env.LOCALAPPDATA, "Programs", "Paseo", "resources", "bin", "paseo.cmd")
|
|
20306
|
+
: "/Applications/Paseo.app/Contents/Resources/bin/paseo";
|
|
20307
|
+
return [env.PASEO_BIN, env.PASEO_CLI, "paseo", installed].filter(Boolean);
|
|
20308
|
+
}
|
|
20309
|
+
|
|
20310
|
+
// On Windows Paseo's command is paseo.cmd, which Node can start, or find on
|
|
20311
|
+
// PATH, only through cmd.exe.
|
|
20312
|
+
function askPaseo(cli) {
|
|
20313
|
+
return windows
|
|
20314
|
+
? spawnSync('"' + cli + '" plugin ls ${PLUGIN_ID} --json', { shell: true, encoding: "utf8" })
|
|
20315
|
+
: spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"], { encoding: "utf8" });
|
|
20316
|
+
}
|
|
20317
|
+
|
|
20269
20318
|
function pluginDirectory() {
|
|
20270
20319
|
if (process.env.PASEO_PROGRESS_PLUGIN_DIR) return process.env.PASEO_PROGRESS_PLUGIN_DIR;
|
|
20271
|
-
const
|
|
20272
|
-
|
|
20273
|
-
const result = spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"], { encoding: "utf8" });
|
|
20320
|
+
for (const cli of paseoCommands()) {
|
|
20321
|
+
const result = askPaseo(cli);
|
|
20274
20322
|
if (result.error || result.status !== 0) continue;
|
|
20275
20323
|
try {
|
|
20276
20324
|
const plugin = JSON.parse(result.stdout).find((entry) => entry.id === "${PLUGIN_ID}");
|
|
@@ -20295,29 +20343,50 @@ process.exit(run.status === null ? 1 : run.status);
|
|
|
20295
20343
|
`;
|
|
20296
20344
|
}
|
|
20297
20345
|
var MARKER = `Written by the ${PLUGIN_ID} Paseo plugin`;
|
|
20298
|
-
|
|
20299
|
-
const
|
|
20346
|
+
function launcherFiles(path, platform) {
|
|
20347
|
+
const files = [{ path, content: launcherScript() }];
|
|
20348
|
+
if (platform === "win32") {
|
|
20349
|
+
files.push({ path: `${path}.cmd`, content: `@echo off\r
|
|
20350
|
+
rem ${MARKER}. Lets PowerShell and cmd run ${basename(path)}.\r
|
|
20351
|
+
node "%~dp0${basename(path)}" %*\r
|
|
20352
|
+
exit /b %errorlevel%\r
|
|
20353
|
+
` });
|
|
20354
|
+
}
|
|
20355
|
+
return files;
|
|
20356
|
+
}
|
|
20357
|
+
async function fileState(file2) {
|
|
20358
|
+
const current = await readFile2(file2.path, "utf8").catch(() => null);
|
|
20300
20359
|
if (current === null) return "missing";
|
|
20301
|
-
if (current ===
|
|
20360
|
+
if (current === file2.content) return "current";
|
|
20302
20361
|
return current.includes(MARKER) ? "outdated" : "foreign";
|
|
20303
20362
|
}
|
|
20304
|
-
async function
|
|
20305
|
-
const
|
|
20306
|
-
|
|
20307
|
-
|
|
20308
|
-
|
|
20309
|
-
|
|
20310
|
-
await
|
|
20311
|
-
|
|
20363
|
+
async function fileStates(path, platform) {
|
|
20364
|
+
const files = launcherFiles(path, platform);
|
|
20365
|
+
const states = await Promise.all(files.map(fileState));
|
|
20366
|
+
return files.map((file2, index) => ({ ...file2, state: states[index] }));
|
|
20367
|
+
}
|
|
20368
|
+
async function installLauncher(path, platform = process.platform) {
|
|
20369
|
+
const files = await fileStates(path, platform);
|
|
20370
|
+
const foreign = files.find((file2) => file2.state === "foreign");
|
|
20371
|
+
if (foreign) throw new Error(`${foreign.path} already exists and was not written by this plugin. Move it, or pass another path.`);
|
|
20372
|
+
let wrote = false;
|
|
20373
|
+
for (const file2 of files) {
|
|
20374
|
+
if (file2.state === "current") continue;
|
|
20375
|
+
await mkdir3(dirname2(file2.path), { recursive: true });
|
|
20376
|
+
await writeFile2(file2.path, file2.content);
|
|
20377
|
+
await chmod(file2.path, 493);
|
|
20378
|
+
wrote = true;
|
|
20379
|
+
}
|
|
20380
|
+
return wrote;
|
|
20312
20381
|
}
|
|
20313
20382
|
|
|
20314
20383
|
// server/store.ts
|
|
20315
20384
|
import { execFile } from "node:child_process";
|
|
20316
|
-
import { createHash } from "node:crypto";
|
|
20385
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20317
20386
|
import { realpathSync } from "node:fs";
|
|
20318
|
-
import { appendFile as appendFile2, mkdir as mkdir4, readdir, readFile as readFile3, rm as rm2, rmdir, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
20387
|
+
import { appendFile as appendFile2, mkdir as mkdir4, readdir, readFile as readFile3, rename, rm as rm2, rmdir, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
20319
20388
|
import { homedir } from "node:os";
|
|
20320
|
-
import { basename, join as join4, normalize, resolve as resolve2 } from "node:path";
|
|
20389
|
+
import { basename as basename2, dirname as dirname3, join as join4, normalize, resolve as resolve2 } from "node:path";
|
|
20321
20390
|
import { promisify } from "node:util";
|
|
20322
20391
|
var LOCK_NAME2 = `${PROGRESS_FILE_NAME}.lock`;
|
|
20323
20392
|
var SAVE_RUNS_KEY = "paseo-progress.saveRuns";
|
|
@@ -20339,6 +20408,9 @@ async function git(cwd, args) {
|
|
|
20339
20408
|
async function exists2(path) {
|
|
20340
20409
|
return Boolean(await stat3(path).catch(() => null));
|
|
20341
20410
|
}
|
|
20411
|
+
function stateRoot(home = homedir()) {
|
|
20412
|
+
return join4(home, ".local", "state", "paseo-progress");
|
|
20413
|
+
}
|
|
20342
20414
|
function outsideDirectory(root, home = homedir()) {
|
|
20343
20415
|
let real = root;
|
|
20344
20416
|
try {
|
|
@@ -20346,7 +20418,7 @@ function outsideDirectory(root, home = homedir()) {
|
|
|
20346
20418
|
} catch {
|
|
20347
20419
|
}
|
|
20348
20420
|
const hash2 = createHash("sha1").update(real).digest("hex").slice(0, 8);
|
|
20349
|
-
return join4(home,
|
|
20421
|
+
return join4(stateRoot(home), `${basename2(real)}-${hash2}`);
|
|
20350
20422
|
}
|
|
20351
20423
|
var trackedLegacy = /* @__PURE__ */ new Map();
|
|
20352
20424
|
async function legacyTracked(root, mtimeMs) {
|
|
@@ -20436,6 +20508,22 @@ async function saveRunCopy(root, text2) {
|
|
|
20436
20508
|
`);
|
|
20437
20509
|
}
|
|
20438
20510
|
|
|
20511
|
+
// server/question-mode.ts
|
|
20512
|
+
import { readFile as readFile4, rm as rm3 } from "node:fs/promises";
|
|
20513
|
+
import { homedir as homedir2 } from "node:os";
|
|
20514
|
+
import { join as join5 } from "node:path";
|
|
20515
|
+
var mirrorSchema = external_exports.object({ questionMode: external_exports.enum(QUESTION_MODES) });
|
|
20516
|
+
function questionModePath(home = homedir2()) {
|
|
20517
|
+
return join5(stateRoot(home), "settings.json");
|
|
20518
|
+
}
|
|
20519
|
+
async function readQuestionMode(home = homedir2()) {
|
|
20520
|
+
try {
|
|
20521
|
+
return mirrorSchema.parse(JSON.parse(await readFile4(questionModePath(home), "utf8"))).questionMode;
|
|
20522
|
+
} catch {
|
|
20523
|
+
return DEFAULT_QUESTION_MODE;
|
|
20524
|
+
}
|
|
20525
|
+
}
|
|
20526
|
+
|
|
20439
20527
|
// shared/format.ts
|
|
20440
20528
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
20441
20529
|
function formatMinutes(totalMin) {
|
|
@@ -20466,6 +20554,7 @@ function minutesSince(iso, nowMs) {
|
|
|
20466
20554
|
|
|
20467
20555
|
// shared/show.ts
|
|
20468
20556
|
var STATUS = { not_started: "Not started", working: "Working", blocked: "Blocked", done: "Done", skipped: "Skipped" };
|
|
20557
|
+
var QUESTION_STATE = { stopped: "agent stopped until the answer", waiting: "waiting for the answer", building: "building with it" };
|
|
20469
20558
|
function dashboardText(dashboard, nowMs) {
|
|
20470
20559
|
if (!dashboard.run && !dashboard.tickets.length) return 'No run yet. Start one with: paseo-progress start "<title>"';
|
|
20471
20560
|
const lines = [];
|
|
@@ -20503,7 +20592,7 @@ function dashboardText(dashboard, nowMs) {
|
|
|
20503
20592
|
if (dashboard.questions.open.length) {
|
|
20504
20593
|
lines.push("", "Questions waiting");
|
|
20505
20594
|
for (const question of dashboard.questions.open) {
|
|
20506
|
-
lines.push(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}`);
|
|
20595
|
+
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}` : ""}`);
|
|
20507
20596
|
for (const option of question.options) lines.push(` ${option.letter}) ${option.label}${option.consequence ? `: ${option.consequence}` : ""}`);
|
|
20508
20597
|
}
|
|
20509
20598
|
}
|
|
@@ -20544,10 +20633,13 @@ function imageMimeType(path) {
|
|
|
20544
20633
|
}
|
|
20545
20634
|
|
|
20546
20635
|
// server/cli.ts
|
|
20547
|
-
import { homedir as
|
|
20636
|
+
import { homedir as homedir3 } from "node:os";
|
|
20548
20637
|
var UsageError = class extends Error {
|
|
20549
20638
|
};
|
|
20550
20639
|
var USAGE_NAME = "paseo-progress";
|
|
20640
|
+
function modeLine(mode) {
|
|
20641
|
+
return `Question mode: ${QUESTION_MODE_RULES[mode].label}, set by the user in Paseo's Settings. For each question, do what "question ask" prints.`;
|
|
20642
|
+
}
|
|
20551
20643
|
function required2(value, name) {
|
|
20552
20644
|
if (!value?.trim()) throw new UsageError(`Missing ${name}.`);
|
|
20553
20645
|
return value.trim();
|
|
@@ -20653,11 +20745,12 @@ var commands = {
|
|
|
20653
20745
|
usage: `start "<title>" [--subtitle <text>] [--item-label <word>]`,
|
|
20654
20746
|
summary: "Start a new run. Earlier runs stay in the file but leave the dashboard.",
|
|
20655
20747
|
options: { subtitle: { type: "string" }, "item-label": { type: "string" } },
|
|
20656
|
-
run({ positionals, values }) {
|
|
20748
|
+
run({ positionals, values, mode }) {
|
|
20657
20749
|
const title = required2(positionals[0], "run title");
|
|
20658
20750
|
return {
|
|
20659
20751
|
event: { type: "run.start", title, subtitle: stringOption(values.subtitle), itemLabel: stringOption(values["item-label"]) },
|
|
20660
|
-
message: `Started run: ${title}
|
|
20752
|
+
message: `Started run: ${title}
|
|
20753
|
+
${modeLine(mode)}`
|
|
20661
20754
|
};
|
|
20662
20755
|
}
|
|
20663
20756
|
},
|
|
@@ -20728,17 +20821,18 @@ var commands = {
|
|
|
20728
20821
|
}
|
|
20729
20822
|
},
|
|
20730
20823
|
"question ask": {
|
|
20731
|
-
usage: `question ask "<short title>" "<question>" [--option "A=<label> | <consequence>" ...] --recommend <letter or word> [--background <text>] [--file <path or http(s) URL>[=<label>] ...] [--raised-by <text>] [--ticket <id>]`,
|
|
20732
|
-
summary: "Ask the user a question. Prints its reference (Q1, Q2, ...). --recommend is the option you would pick.
|
|
20824
|
+
usage: `question ask "<short title>" "<question>" [--option "A=<label> | <consequence>" ...] --recommend <letter or word> [--big "<why, in a few plain words>"] [--background <text>] [--file <path or http(s) URL>[=<label>] ...] [--raised-by <text>] [--ticket <id>]`,
|
|
20825
|
+
summary: "Ask the user a question. Prints its reference (Q1, Q2, ...) and what to do until the answer, which depends on the question mode the user picks in Paseo's Settings: Stop (stop all work until an answer), Work around (build nothing the question affects, work on the rest), or Keep building (build your recommendation now, change course if the user picks another). --recommend is the option you would pick. Add --big with the reason when the question is big; it only changes what you do in Keep building, where a big question holds like Work around. A question is big when one of these holds, and only then: (1) building your recommendation before the answer would mean something leaves this computer: pushing, publishing, deploying, changing live data, sending messages, spending money; (2) the answer sets something people or programs outside this repo will rely on: a saved data format, a database schema, a public API, command or setting name, or removing or renaming something people already use; (3) most of the tickets or steps still ahead would be built on the answer. Every other question is small, large code changes included. When you can't name the test it meets, it's small. Name the test in the --big reason.",
|
|
20733
20826
|
options: {
|
|
20734
20827
|
option: { type: "string", multiple: true },
|
|
20735
20828
|
recommend: { type: "string" },
|
|
20829
|
+
big: { type: "string" },
|
|
20736
20830
|
background: { type: "string" },
|
|
20737
20831
|
file: { type: "string", multiple: true },
|
|
20738
20832
|
"raised-by": { type: "string" },
|
|
20739
20833
|
ticket: { type: "string" }
|
|
20740
20834
|
},
|
|
20741
|
-
run({ positionals, values, state, cwd, root }) {
|
|
20835
|
+
run({ positionals, values, state, cwd, root, mode }) {
|
|
20742
20836
|
const title = required2(positionals[0], "short title");
|
|
20743
20837
|
const ticket = values.ticket === void 0 ? void 0 : existingTicket(state, stringOption(values.ticket));
|
|
20744
20838
|
const question = required2(positionals[1], "question");
|
|
@@ -20754,6 +20848,9 @@ var commands = {
|
|
|
20754
20848
|
recommend = recommend.toUpperCase();
|
|
20755
20849
|
}
|
|
20756
20850
|
const id = state.nextIds.question;
|
|
20851
|
+
const big = values.big === void 0 ? void 0 : required2(stringOption(values.big), "--big reason (why the question is big)");
|
|
20852
|
+
const rules = QUESTION_MODE_RULES[mode];
|
|
20853
|
+
const rule = `Mode: ${rules.label}. ${rules.askRule({ recommend, big: Boolean(big) })}`;
|
|
20757
20854
|
return {
|
|
20758
20855
|
event: {
|
|
20759
20856
|
type: "question.ask",
|
|
@@ -20762,12 +20859,14 @@ var commands = {
|
|
|
20762
20859
|
question,
|
|
20763
20860
|
options: options.length ? options : void 0,
|
|
20764
20861
|
recommend,
|
|
20862
|
+
mode,
|
|
20863
|
+
big,
|
|
20765
20864
|
background: stringOption(values.background),
|
|
20766
20865
|
files: listOption(values.file).length ? listOption(values.file).map((raw) => parseFile(raw, cwd, root)) : void 0,
|
|
20767
20866
|
raisedBy: stringOption(values["raised-by"]),
|
|
20768
20867
|
ticket: ticket?.id
|
|
20769
20868
|
},
|
|
20770
|
-
message: `Asked ${id} (${title}). You recommend ${recommend}.
|
|
20869
|
+
message: `Asked ${id} (${title}). You recommend ${recommend}. ${rule}`
|
|
20771
20870
|
};
|
|
20772
20871
|
}
|
|
20773
20872
|
},
|
|
@@ -20784,9 +20883,11 @@ var commands = {
|
|
|
20784
20883
|
if (letters.length && !letters.includes(choice)) throw new UsageError(`${question.id} has options ${letters.join(", ")}, not ${choice}.`);
|
|
20785
20884
|
}
|
|
20786
20885
|
const asRecommended = choice.toLowerCase() === question.recommended.toLowerCase();
|
|
20886
|
+
const built = question.state === "building";
|
|
20887
|
+
const next = !built ? "Build it now." : asRecommended ? "Carry on with it." : `Undo the work built on your recommendation and build ${choice} before anything else.`;
|
|
20787
20888
|
return {
|
|
20788
|
-
event: { type: "question.answer", id: question.id, choice, words: stringOption(values.words) },
|
|
20789
|
-
message: `Answered ${question.id}: ${choice}${asRecommended ? " (as you recommended)" : ` (not your recommendation, ${question.recommended})`}.
|
|
20889
|
+
event: { type: "question.answer", id: question.id, choice, words: stringOption(values.words), changedCourse: built && !asRecommended ? true : void 0 },
|
|
20890
|
+
message: `Answered ${question.id}: ${choice}${asRecommended ? " (as you recommended)" : ` (not your recommendation, ${question.recommended})`}. ${next}`
|
|
20790
20891
|
};
|
|
20791
20892
|
}
|
|
20792
20893
|
},
|
|
@@ -20978,11 +21079,12 @@ ${INSTALL_SUMMARY}`);
|
|
|
20978
21079
|
const { dashboard } = await readProgress(store.directory, now());
|
|
20979
21080
|
const added = addedByOthersText(addedByOthers(await readLatestRun(store.directory), dashboard, options.agentId), dashboard);
|
|
20980
21081
|
out(`Worktree: ${root}
|
|
21082
|
+
${modeLine(await readQuestionMode())}
|
|
20981
21083
|
|
|
20982
21084
|
${dashboardText(dashboard, now().getTime())}${added}`);
|
|
20983
21085
|
return 0;
|
|
20984
21086
|
}
|
|
20985
|
-
const target = resolve3(options.cwd, (argv[1] ?? DEFAULT_LAUNCHER).replace(/^~(?=$|\/)/,
|
|
21087
|
+
const target = resolve3(options.cwd, (argv[1] ?? DEFAULT_LAUNCHER).replace(/^~(?=$|\/)/, homedir3()));
|
|
20986
21088
|
out(await installLauncher(target) ? `Installed the launcher at ${target}` : `The launcher at ${target} is already current`);
|
|
20987
21089
|
return 0;
|
|
20988
21090
|
}
|
|
@@ -21010,12 +21112,13 @@ ${command.summary}`);
|
|
|
21010
21112
|
}
|
|
21011
21113
|
const root = findRoot(options.cwd);
|
|
21012
21114
|
const store = await findStore(root);
|
|
21115
|
+
const mode = await readQuestionMode();
|
|
21013
21116
|
const message = await appendProgress(store.directory, now, (state) => {
|
|
21014
21117
|
const finished = state.dashboard.run?.finished;
|
|
21015
21118
|
if (finished && command !== commands.start) {
|
|
21016
21119
|
throw new UsageError(`The run "${state.dashboard.run.title}" is finished. Start new work with: ${USAGE_NAME} start "<title>"`);
|
|
21017
21120
|
}
|
|
21018
|
-
const { event, message: message2 } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root });
|
|
21121
|
+
const { event, message: message2 } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root, mode });
|
|
21019
21122
|
const added = command === commands.start ? "" : addedByOthersText(addedByOthers(state.events, state.dashboard, options.agentId), state.dashboard);
|
|
21020
21123
|
return { event: { ...event, by: options.agentId }, result: `${message2}${added}` };
|
|
21021
21124
|
}, (text2) => saveRunCopy(root, text2));
|
package/images/1-panel.png
CHANGED
|
Binary file
|
package/images/3-question.png
CHANGED
|
Binary file
|
|
Binary file
|
package/index.client.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
2
|
import { ProgressPanel } from "./client/panel";
|
|
3
3
|
import { contributePills, progressPanel } from "./client/pills";
|
|
4
|
+
import { QuestionSettings } from "./client/settings";
|
|
5
|
+
|
|
6
|
+
const SETTINGS_ID = "settings";
|
|
4
7
|
|
|
5
8
|
export default function contribute(client: PluginClientContext) {
|
|
6
9
|
const stopPills = contributePills(client);
|
|
@@ -11,7 +14,7 @@ export default function contribute(client: PluginClientContext) {
|
|
|
11
14
|
icon: "ListChecks",
|
|
12
15
|
context: "workspace",
|
|
13
16
|
locations: [panel.location],
|
|
14
|
-
Component: ProgressPanel
|
|
17
|
+
Component: (props) => <ProgressPanel {...props} openSettings={() => client.openSettings(SETTINGS_ID)} />,
|
|
15
18
|
});
|
|
16
19
|
const stopCommand = client.addCommandCenterItem({
|
|
17
20
|
id: "open-progress",
|
|
@@ -25,7 +28,15 @@ export default function contribute(client: PluginClientContext) {
|
|
|
25
28
|
},
|
|
26
29
|
});
|
|
27
30
|
|
|
31
|
+
const stopSettings = client.addSettingsScreen({
|
|
32
|
+
id: SETTINGS_ID,
|
|
33
|
+
title: "Settings",
|
|
34
|
+
icon: "Settings",
|
|
35
|
+
Component: QuestionSettings,
|
|
36
|
+
});
|
|
37
|
+
|
|
28
38
|
return () => {
|
|
39
|
+
void stopSettings();
|
|
29
40
|
void stopPanel();
|
|
30
41
|
void stopCommand();
|
|
31
42
|
stopPills();
|
package/index.server.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
|
2
2
|
import { handleGetAttention, handleGetDashboard, handleListFolders, handleMarkPanelOpened, handleSetHistoryFolder } from "./server/dashboard";
|
|
3
3
|
import { handleInstallLauncher, handleLauncherStatus } from "./server/launcher";
|
|
4
4
|
import { handleCanOpenOnHost, handleOpenDeliverable, handlePreviewDeliverable, handleReadDeliverablePiece } from "./server/open";
|
|
5
|
+
import { mirrorQuestionMode } from "./server/question-mode";
|
|
5
6
|
import { handleInstallSkill, handleSkillStatus } from "./server/skill";
|
|
6
|
-
import {
|
|
7
|
+
import { handleDismissWhatsNew, handleGetWhatsNew } from "./server/whats-new";
|
|
8
|
+
import { canOpenOnHost, dismissWhatsNew, getAttention, getDashboard, getLauncherStatus, getSkillStatus, getWhatsNew, installLauncher, installSkill, listFolders, markPanelOpened, openDeliverable, previewDeliverable, readDeliverablePiece, setHistoryFolder } from "./shared/rpc";
|
|
9
|
+
import { questionSettings } from "./shared/settings";
|
|
7
10
|
|
|
8
11
|
export default function contribute(server: PluginServerContext) {
|
|
9
12
|
server.handle(getDashboard, handleGetDashboard);
|
|
@@ -19,5 +22,11 @@ export default function contribute(server: PluginServerContext) {
|
|
|
19
22
|
server.handle(installSkill, handleInstallSkill);
|
|
20
23
|
server.handle(listFolders, handleListFolders);
|
|
21
24
|
server.handle(setHistoryFolder, handleSetHistoryFolder);
|
|
22
|
-
|
|
25
|
+
server.handle(getWhatsNew, handleGetWhatsNew);
|
|
26
|
+
server.handle(dismissWhatsNew, handleDismissWhatsNew);
|
|
27
|
+
// Settles a new install before any panel opens. Opening one makes a
|
|
28
|
+
// worktree folder, which would look like an earlier version's.
|
|
29
|
+
void handleGetWhatsNew().catch(() => {});
|
|
30
|
+
const mirror = mirrorQuestionMode(server.registerSettings(questionSettings));
|
|
31
|
+
return mirror.stop;
|
|
23
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stevecastaneda/paseo-progress",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"test": "node --test \"*/*.test.ts\"",
|
|
45
45
|
"dev": "node ../scripts/dev.mjs",
|
|
46
46
|
"check:install": "node ../scripts/check-install.mjs",
|
|
47
|
+
"check:windows": "node scripts/windows-check.mjs",
|
|
47
48
|
"build:cli": "rm -rf dist && esbuild server/cli.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/server/cli.js --log-level=warning",
|
|
48
49
|
"prepack": "npm run build:cli",
|
|
49
50
|
"postpack": "rm -rf dist"
|
package/paseo-plugin.json
CHANGED
package/server/cli.ts
CHANGED
|
@@ -6,10 +6,12 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import { isSettled, type Dashboard, type NextIds, type Ticket } from "../shared/dashboard.ts";
|
|
8
8
|
import { DELIVERABLE_KINDS, TICKET_STATUSES, type FileLink, type ProgressEvent } from "../shared/events.ts";
|
|
9
|
+
import { QUESTION_MODE_RULES, type QuestionMode } from "../shared/question-modes.ts";
|
|
9
10
|
import { appendProgress, InvalidEvent, readLatestRun, readProgress, type EventDraft } from "./progress-file.ts";
|
|
10
11
|
import { findRoot, outside } from "./paths.ts";
|
|
11
12
|
import { installLauncher } from "./launcher.ts";
|
|
12
13
|
import { findStore, saveRunCopy } from "./store.ts";
|
|
14
|
+
import { readQuestionMode } from "./question-mode.ts";
|
|
13
15
|
import { dashboardText } from "../shared/show.ts";
|
|
14
16
|
import { formatMinutes } from "../shared/format.ts";
|
|
15
17
|
import { imageMimeType } from "../shared/attachments.ts";
|
|
@@ -34,11 +36,15 @@ interface Command {
|
|
|
34
36
|
usage: string;
|
|
35
37
|
summary: string;
|
|
36
38
|
options?: Record<string, { type: "string" | "boolean"; multiple?: boolean }>;
|
|
37
|
-
run(args: { positionals: string[]; values: Record<string, string | boolean | Array<string | boolean> | undefined>; state: State; cwd: string; root: string }): { event: EventDraft; message: string };
|
|
39
|
+
run(args: { positionals: string[]; values: Record<string, string | boolean | Array<string | boolean> | undefined>; state: State; cwd: string; root: string; mode: QuestionMode }): { event: EventDraft; message: string };
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const USAGE_NAME = "paseo-progress";
|
|
41
43
|
|
|
44
|
+
function modeLine(mode: QuestionMode): string {
|
|
45
|
+
return `Question mode: ${QUESTION_MODE_RULES[mode].label}, set by the user in Paseo's Settings. For each question, do what "question ask" prints.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
function required(value: string | undefined, name: string): string {
|
|
43
49
|
if (!value?.trim()) throw new UsageError(`Missing ${name}.`);
|
|
44
50
|
return value.trim();
|
|
@@ -168,11 +174,11 @@ const commands: Record<string, Command> = {
|
|
|
168
174
|
usage: `start "<title>" [--subtitle <text>] [--item-label <word>]`,
|
|
169
175
|
summary: "Start a new run. Earlier runs stay in the file but leave the dashboard.",
|
|
170
176
|
options: { subtitle: { type: "string" }, "item-label": { type: "string" } },
|
|
171
|
-
run({ positionals, values }) {
|
|
177
|
+
run({ positionals, values, mode }) {
|
|
172
178
|
const title = required(positionals[0], "run title");
|
|
173
179
|
return {
|
|
174
180
|
event: { type: "run.start", title, subtitle: stringOption(values.subtitle), itemLabel: stringOption(values["item-label"]) },
|
|
175
|
-
message: `Started run: ${title}`,
|
|
181
|
+
message: `Started run: ${title}\n${modeLine(mode)}`,
|
|
176
182
|
};
|
|
177
183
|
},
|
|
178
184
|
},
|
|
@@ -244,17 +250,18 @@ const commands: Record<string, Command> = {
|
|
|
244
250
|
},
|
|
245
251
|
},
|
|
246
252
|
"question ask": {
|
|
247
|
-
usage: `question ask "<short title>" "<question>" [--option "A=<label> | <consequence>" ...] --recommend <letter or word> [--background <text>] [--file <path or http(s) URL>[=<label>] ...] [--raised-by <text>] [--ticket <id>]`,
|
|
248
|
-
summary: "Ask the user a question. Prints its reference (Q1, Q2, ...). --recommend is the option you would pick.
|
|
253
|
+
usage: `question ask "<short title>" "<question>" [--option "A=<label> | <consequence>" ...] --recommend <letter or word> [--big "<why, in a few plain words>"] [--background <text>] [--file <path or http(s) URL>[=<label>] ...] [--raised-by <text>] [--ticket <id>]`,
|
|
254
|
+
summary: "Ask the user a question. Prints its reference (Q1, Q2, ...) and what to do until the answer, which depends on the question mode the user picks in Paseo's Settings: Stop (stop all work until an answer), Work around (build nothing the question affects, work on the rest), or Keep building (build your recommendation now, change course if the user picks another). --recommend is the option you would pick. Add --big with the reason when the question is big; it only changes what you do in Keep building, where a big question holds like Work around. A question is big when one of these holds, and only then: (1) building your recommendation before the answer would mean something leaves this computer: pushing, publishing, deploying, changing live data, sending messages, spending money; (2) the answer sets something people or programs outside this repo will rely on: a saved data format, a database schema, a public API, command or setting name, or removing or renaming something people already use; (3) most of the tickets or steps still ahead would be built on the answer. Every other question is small, large code changes included. When you can't name the test it meets, it's small. Name the test in the --big reason.",
|
|
249
255
|
options: {
|
|
250
256
|
option: { type: "string", multiple: true },
|
|
251
257
|
recommend: { type: "string" },
|
|
258
|
+
big: { type: "string" },
|
|
252
259
|
background: { type: "string" },
|
|
253
260
|
file: { type: "string", multiple: true },
|
|
254
261
|
"raised-by": { type: "string" },
|
|
255
262
|
ticket: { type: "string" },
|
|
256
263
|
},
|
|
257
|
-
run({ positionals, values, state, cwd, root }) {
|
|
264
|
+
run({ positionals, values, state, cwd, root, mode }) {
|
|
258
265
|
const title = required(positionals[0], "short title");
|
|
259
266
|
const ticket = values.ticket === undefined ? undefined : existingTicket(state, stringOption(values.ticket));
|
|
260
267
|
const question = required(positionals[1], "question");
|
|
@@ -270,6 +277,9 @@ const commands: Record<string, Command> = {
|
|
|
270
277
|
recommend = recommend.toUpperCase();
|
|
271
278
|
}
|
|
272
279
|
const id = state.nextIds.question;
|
|
280
|
+
const big = values.big === undefined ? undefined : required(stringOption(values.big), "--big reason (why the question is big)");
|
|
281
|
+
const rules = QUESTION_MODE_RULES[mode];
|
|
282
|
+
const rule = `Mode: ${rules.label}. ${rules.askRule({ recommend, big: Boolean(big) })}`;
|
|
273
283
|
return {
|
|
274
284
|
event: {
|
|
275
285
|
type: "question.ask",
|
|
@@ -278,12 +288,14 @@ const commands: Record<string, Command> = {
|
|
|
278
288
|
question,
|
|
279
289
|
options: options.length ? options : undefined,
|
|
280
290
|
recommend,
|
|
291
|
+
mode,
|
|
292
|
+
big,
|
|
281
293
|
background: stringOption(values.background),
|
|
282
294
|
files: listOption(values.file).length ? listOption(values.file).map((raw) => parseFile(raw, cwd, root)) : undefined,
|
|
283
295
|
raisedBy: stringOption(values["raised-by"]),
|
|
284
296
|
ticket: ticket?.id,
|
|
285
297
|
},
|
|
286
|
-
message: `Asked ${id} (${title}). You recommend ${recommend}.
|
|
298
|
+
message: `Asked ${id} (${title}). You recommend ${recommend}. ${rule}`,
|
|
287
299
|
};
|
|
288
300
|
},
|
|
289
301
|
},
|
|
@@ -300,9 +312,13 @@ const commands: Record<string, Command> = {
|
|
|
300
312
|
if (letters.length && !letters.includes(choice)) throw new UsageError(`${question.id} has options ${letters.join(", ")}, not ${choice}.`);
|
|
301
313
|
}
|
|
302
314
|
const asRecommended = choice.toLowerCase() === question.recommended.toLowerCase();
|
|
315
|
+
const built = question.state === "building";
|
|
316
|
+
const next = !built ? "Build it now."
|
|
317
|
+
: asRecommended ? "Carry on with it."
|
|
318
|
+
: `Undo the work built on your recommendation and build ${choice} before anything else.`;
|
|
303
319
|
return {
|
|
304
|
-
event: { type: "question.answer", id: question.id, choice, words: stringOption(values.words) },
|
|
305
|
-
message: `Answered ${question.id}: ${choice}${asRecommended ? " (as you recommended)" : ` (not your recommendation, ${question.recommended})`}.
|
|
320
|
+
event: { type: "question.answer", id: question.id, choice, words: stringOption(values.words), changedCourse: built && !asRecommended ? true : undefined },
|
|
321
|
+
message: `Answered ${question.id}: ${choice}${asRecommended ? " (as you recommended)" : ` (not your recommendation, ${question.recommended})`}. ${next}`,
|
|
306
322
|
};
|
|
307
323
|
},
|
|
308
324
|
},
|
|
@@ -496,7 +512,7 @@ export async function runCli(argv: string[], options: CliOptions): Promise<numbe
|
|
|
496
512
|
const store = await findStore(root);
|
|
497
513
|
const { dashboard } = await readProgress(store.directory, now());
|
|
498
514
|
const added = addedByOthersText(addedByOthers(await readLatestRun(store.directory), dashboard, options.agentId), dashboard);
|
|
499
|
-
out(`Worktree: ${root}\n\n${dashboardText(dashboard, now().getTime())}${added}`);
|
|
515
|
+
out(`Worktree: ${root}\n${modeLine(await readQuestionMode())}\n\n${dashboardText(dashboard, now().getTime())}${added}`);
|
|
500
516
|
return 0;
|
|
501
517
|
}
|
|
502
518
|
const target = resolve(options.cwd, (argv[1] ?? DEFAULT_LAUNCHER).replace(/^~(?=$|\/)/, homedir()));
|
|
@@ -523,12 +539,13 @@ export async function runCli(argv: string[], options: CliOptions): Promise<numbe
|
|
|
523
539
|
}
|
|
524
540
|
const root = findRoot(options.cwd);
|
|
525
541
|
const store = await findStore(root);
|
|
542
|
+
const mode = await readQuestionMode();
|
|
526
543
|
const message = await appendProgress(store.directory, now, (state) => {
|
|
527
544
|
const finished = state.dashboard.run?.finished;
|
|
528
545
|
if (finished && command !== commands.start) {
|
|
529
546
|
throw new UsageError(`The run "${state.dashboard.run!.title}" is finished. Start new work with: ${USAGE_NAME} start "<title>"`);
|
|
530
547
|
}
|
|
531
|
-
const { event, message } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root });
|
|
548
|
+
const { event, message } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root, mode });
|
|
532
549
|
const added = command === commands.start ? "" : addedByOthersText(addedByOthers(state.events, state.dashboard, options.agentId), state.dashboard);
|
|
533
550
|
return { event: { ...event, by: options.agentId }, result: `${message}${added}` };
|
|
534
551
|
}, (text) => saveRunCopy(root, text));
|