pi-do-always 0.12.0 → 0.15.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 +31 -25
- package/extensions/pi-do-always/do-always.json +20 -1
- package/extensions/pi-do-always/index.ts +739 -29
- package/extensions/pi-do-always/tasks.ts +231 -3
- package/package.json +1 -1
- package/pi.image.png +0 -0
|
@@ -37,6 +37,21 @@ export interface DoAlwaysTask {
|
|
|
37
37
|
* Omitted/undefined always shows the task. Evaluated by `evaluateWhen`.
|
|
38
38
|
*/
|
|
39
39
|
when?: string | Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Hide the task from the selector and the `list` commands. Hidden tasks
|
|
42
|
+
* are not standalone user-visible entries; they can still be resolved by
|
|
43
|
+
* name on the command line and are offered as candidates by the commit
|
|
44
|
+
* picker. The built-in "Review commits" uses this: it is a pick-after-
|
|
45
|
+
* browse option, not an entry point ("Browse commits" is the entry).
|
|
46
|
+
*/
|
|
47
|
+
hidden?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Exclude the task from the commit picker (the "run on the selected
|
|
50
|
+
* commits" list shown after a commit selection). For Plan tasks that
|
|
51
|
+
* operate on the working tree or the whole project rather than on a set
|
|
52
|
+
* of commits. The task is otherwise unaffected (selector, lists, CLI).
|
|
53
|
+
*/
|
|
54
|
+
notForCommits?: boolean;
|
|
40
55
|
/**
|
|
41
56
|
* Extra selection-time guards, evaluated alongside the legacy `requireDirty`
|
|
42
57
|
* (see `evaluateGuards`). Each guard blocks the task (with a message, not a
|
|
@@ -44,6 +59,14 @@ export interface DoAlwaysTask {
|
|
|
44
59
|
* compatibility; new guards use this array so the set is extensible.
|
|
45
60
|
*/
|
|
46
61
|
guards?: Guard[];
|
|
62
|
+
/**
|
|
63
|
+
* A browser to open on selection instead of injecting the prompt directly.
|
|
64
|
+
* Currently only "commits": opens the date-grouped commit browser, and the
|
|
65
|
+
* selected commits drive the review prompt (see `fillPrompt` in index.ts).
|
|
66
|
+
* The built-in "Review commits" task also triggers the browser by name, so
|
|
67
|
+
* configs that predate this field keep working.
|
|
68
|
+
*/
|
|
69
|
+
browser?: BrowserType;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
/** The set of known guard types (used for validation at parse time). */
|
|
@@ -66,6 +89,13 @@ export interface Guard {
|
|
|
66
89
|
value?: string;
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
/** The set of known browser types (used for validation at parse time). */
|
|
93
|
+
export const BROWSER_TYPES = [
|
|
94
|
+
"commits",
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
export type BrowserType = (typeof BROWSER_TYPES)[number];
|
|
98
|
+
|
|
69
99
|
/**
|
|
70
100
|
* A config file can be a bare array of tasks, or {"tasks": [...], "shortcut": ...}.
|
|
71
101
|
* `shortcut` is a key id string (e.g. "f4", "ctrl+shift+p"), or null to disable
|
|
@@ -144,6 +174,12 @@ export interface TaskContext {
|
|
|
144
174
|
stagedFiles: string[];
|
|
145
175
|
/** Modified-but-unstaged files. */
|
|
146
176
|
unstagedFiles: string[];
|
|
177
|
+
/**
|
|
178
|
+
* Commits selected in the commit browser, formatted for prompt injection
|
|
179
|
+
* (see `formatSelectedCommits`), or "none" when no selection is active.
|
|
180
|
+
* Context builds always set "none"; the browser flow overrides it at render.
|
|
181
|
+
*/
|
|
182
|
+
selectedCommits: string;
|
|
147
183
|
/** True when cwd is inside a git working tree (authoritative, not inferred from the branch name). */
|
|
148
184
|
isGitRepo: boolean;
|
|
149
185
|
}
|
|
@@ -166,6 +202,7 @@ export const PROMPT_CONTEXT_KEYS = [
|
|
|
166
202
|
"repo",
|
|
167
203
|
"staged_files",
|
|
168
204
|
"unstaged_files",
|
|
205
|
+
"selected_commits",
|
|
169
206
|
] as const;
|
|
170
207
|
|
|
171
208
|
/** A fully populated prompt context: one entry per PROMPT_CONTEXT_KEYS. */
|
|
@@ -193,6 +230,7 @@ export function toPromptContext(ctx: TaskContext): PromptContext {
|
|
|
193
230
|
repo: ctx.repo,
|
|
194
231
|
staged_files: formatFileLines(ctx.stagedFiles),
|
|
195
232
|
unstaged_files: formatFileLines(ctx.unstagedFiles),
|
|
233
|
+
selected_commits: ctx.selectedCommits,
|
|
196
234
|
};
|
|
197
235
|
}
|
|
198
236
|
|
|
@@ -315,6 +353,7 @@ export const DEFAULT_TASKS: DoAlwaysTask[] = [
|
|
|
315
353
|
category: "Plan",
|
|
316
354
|
description: "Review the current code changes (Plan)",
|
|
317
355
|
requireDirty: true,
|
|
356
|
+
notForCommits: true,
|
|
318
357
|
prompt:
|
|
319
358
|
"Review the changes on branch {{branch}} ({{files_changed_count}} changed files: {{files_changed}}). " +
|
|
320
359
|
"Change summary: {{diff_stat}}. Last commit: {{last_commit}}. " +
|
|
@@ -326,6 +365,7 @@ export const DEFAULT_TASKS: DoAlwaysTask[] = [
|
|
|
326
365
|
name: "Review code",
|
|
327
366
|
category: "Plan",
|
|
328
367
|
description: "Review the whole project's code quality (Plan)",
|
|
368
|
+
notForCommits: true,
|
|
329
369
|
prompt:
|
|
330
370
|
"Review this project's code holistically: identify code smells, dead code, duplication, awkward architecture or patterns, maintainability issues, inconsistencies, and missing or unclear documentation. " +
|
|
331
371
|
"Prioritize by impact, propose a plan for the fixes, and summarize your findings. Do not make any changes yet.",
|
|
@@ -378,7 +418,7 @@ export const DEFAULT_TASKS: DoAlwaysTask[] = [
|
|
|
378
418
|
description: "Prepare a release (version, changelog, tag)",
|
|
379
419
|
when: "git",
|
|
380
420
|
prompt:
|
|
381
|
-
"Prepare a release for this project (branch {{branch}}): check `git log` since the last tag,
|
|
421
|
+
"Prepare a release for this project (branch {{branch}}): check `git log` since the last tag, then bump the version in package.json (or the equivalent location) to the next version, and add a changelog entry under that exact version summarizing the changes. The changelog entry must use the same version number now set in package.json — never add a changelog section for a version that package.json does not yet contain, and never leave an 'Unreleased' or placeholder version heading. Create a git tag if git present. Do not push.",
|
|
382
422
|
},
|
|
383
423
|
{
|
|
384
424
|
name: "Commit",
|
|
@@ -393,9 +433,29 @@ export const DEFAULT_TASKS: DoAlwaysTask[] = [
|
|
|
393
433
|
name: "Propose features",
|
|
394
434
|
category: "Plan",
|
|
395
435
|
description: "Propose new features (Plan)",
|
|
436
|
+
notForCommits: true,
|
|
396
437
|
prompt:
|
|
397
438
|
"Review this project and propose new features that would add value. For each idea, describe the problem it solves, the user benefit, and a rough implementation approach. Prioritize by impact and effort. Do not make any changes yet. Try to evaluate how many lines this will be in terms of changes, whether this will break APIs, or introduce compatibility issues.",
|
|
398
439
|
},
|
|
440
|
+
{
|
|
441
|
+
name: "Review commits",
|
|
442
|
+
category: "Plan",
|
|
443
|
+
description: "Review the selected commits (Plan)",
|
|
444
|
+
browser: "commits",
|
|
445
|
+
hidden: true,
|
|
446
|
+
prompt:
|
|
447
|
+
"Review the selected commits: {{selected_commits}}. Inspect each with `git show <hash>`, " +
|
|
448
|
+
"double-check the changes for bugs, edge cases, security issues, and consistency with the rest of the codebase. " +
|
|
449
|
+
"Summarize your findings per commit and propose a plan for any fixes if needed. Do not make any changes yet.",
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "Browse commits",
|
|
453
|
+
category: "Browse",
|
|
454
|
+
description: "Browse and select commits, then pick a task to run on them",
|
|
455
|
+
browser: "commits",
|
|
456
|
+
when: "git",
|
|
457
|
+
prompt: "Browse recent commits, select some, then pick a task to run on them.",
|
|
458
|
+
},
|
|
399
459
|
];
|
|
400
460
|
|
|
401
461
|
/**
|
|
@@ -427,7 +487,7 @@ export function parseConfig(
|
|
|
427
487
|
const tasks: DoAlwaysTask[] = [];
|
|
428
488
|
for (const entry of list) {
|
|
429
489
|
const t = entry as Partial<DoAlwaysTask> | null;
|
|
430
|
-
if (t && typeof t.name === "string" && t.name.length > 0 && typeof t.prompt === "string" && t.prompt.length > 0) {
|
|
490
|
+
if (t && typeof t.name === "string" && t.name.length > 0 && typeof t.prompt === "string" && (t.prompt.length > 0 || t.autoRun === true)) {
|
|
431
491
|
const task: DoAlwaysTask = {
|
|
432
492
|
name: t.name,
|
|
433
493
|
prompt: t.prompt,
|
|
@@ -436,6 +496,15 @@ export function parseConfig(
|
|
|
436
496
|
if (typeof t.category === "string" && t.category.trim() !== "") task.category = t.category.trim();
|
|
437
497
|
if (typeof t.autoRun === "boolean") task.autoRun = t.autoRun;
|
|
438
498
|
if (typeof t.requireDirty === "boolean") task.requireDirty = t.requireDirty;
|
|
499
|
+
if (typeof t.hidden === "boolean") task.hidden = t.hidden;
|
|
500
|
+
if (typeof t.notForCommits === "boolean") task.notForCommits = t.notForCommits;
|
|
501
|
+
if (t.browser !== undefined) {
|
|
502
|
+
if (typeof t.browser === "string" && BROWSER_TYPES.includes(t.browser as BrowserType)) {
|
|
503
|
+
task.browser = t.browser as BrowserType;
|
|
504
|
+
} else {
|
|
505
|
+
onError(`do-always: ignoring invalid "browser" in ${path} (expected one of: ${BROWSER_TYPES.join(", ")})`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
439
508
|
if (t.guards !== undefined) {
|
|
440
509
|
if (Array.isArray(t.guards)) {
|
|
441
510
|
const guards: Guard[] = [];
|
|
@@ -665,8 +734,17 @@ export function evaluateWhen(task: DoAlwaysTask, ctx: TaskContext): boolean {
|
|
|
665
734
|
return true;
|
|
666
735
|
}
|
|
667
736
|
|
|
737
|
+
/**
|
|
738
|
+
* Whether a task is visible in the selector and the lists: not `hidden` and
|
|
739
|
+
* its `when` condition passes. Single source of truth so the TUI selector and
|
|
740
|
+
* the `/do-always <n>` numbering can never diverge.
|
|
741
|
+
*/
|
|
742
|
+
export function isTaskVisible(task: DoAlwaysTask, ctx: TaskContext): boolean {
|
|
743
|
+
return !task.hidden && evaluateWhen(task, ctx);
|
|
744
|
+
}
|
|
745
|
+
|
|
668
746
|
/** Default order for category headers in the selector. Exported for tests. */
|
|
669
|
-
export const DEFAULT_CATEGORY_ORDER = ["Plan", "Do", "Docs", "Ops", "Other"];
|
|
747
|
+
export const DEFAULT_CATEGORY_ORDER = ["Plan", "Browse", "Do", "Docs", "Ops", "Other"];
|
|
670
748
|
|
|
671
749
|
/** A category group: a display name and the tasks that belong to it. */
|
|
672
750
|
interface TaskGroup {
|
|
@@ -1263,3 +1341,153 @@ function outcomeToMarker(outcome: ChainStepOutcome): string {
|
|
|
1263
1341
|
throw new Error(`unexpected outcome: ${outcome as string}`);
|
|
1264
1342
|
}
|
|
1265
1343
|
}
|
|
1344
|
+
|
|
1345
|
+
// ── Commit browser types ─────────────────────────────────────────────────
|
|
1346
|
+
//
|
|
1347
|
+
// Types and constants for the date-grouped commit browser (used by
|
|
1348
|
+
// the "Review commits" task in index.ts). Pure data — no Pi dependencies.
|
|
1349
|
+
|
|
1350
|
+
/** Maximum commits shown per page in the date-grouped browser. */
|
|
1351
|
+
export const COMMIT_BROWSER_MAX = 20;
|
|
1352
|
+
|
|
1353
|
+
/** Maximum commits that can be selected for a review chain. */
|
|
1354
|
+
export const COMMIT_SELECT_MAX = 20;
|
|
1355
|
+
|
|
1356
|
+
/** A single commit as returned by `git log --format`. */
|
|
1357
|
+
export interface CommitInfo {
|
|
1358
|
+
/** Full 40-char SHA. */
|
|
1359
|
+
hash: string;
|
|
1360
|
+
/** Short 7-char SHA. */
|
|
1361
|
+
shortHash: string;
|
|
1362
|
+
/** Commit subject line. */
|
|
1363
|
+
subject: string;
|
|
1364
|
+
/** Commit date (YYYY-MM-DD). */
|
|
1365
|
+
date: string;
|
|
1366
|
+
/** Commit author name. */
|
|
1367
|
+
author: string;
|
|
1368
|
+
/** Number of files changed in this commit. */
|
|
1369
|
+
filesChanged: number;
|
|
1370
|
+
/** Insertions in this commit. */
|
|
1371
|
+
insertions: number;
|
|
1372
|
+
/** Deletions in this commit. */
|
|
1373
|
+
deletions: number;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** A commit selected by the user for review. */
|
|
1377
|
+
export interface SelectedCommit extends CommitInfo {
|
|
1378
|
+
/** 1-based position in the user's selection order. */
|
|
1379
|
+
selectionOrder: number;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/** Group of commits sharing the same date. */
|
|
1383
|
+
export interface DateGroup {
|
|
1384
|
+
/** Date string (YYYY-MM-DD). */
|
|
1385
|
+
date: string;
|
|
1386
|
+
/** Number of commits in this group. */
|
|
1387
|
+
count: number;
|
|
1388
|
+
/** Commits in this group (newest first). */
|
|
1389
|
+
commits: CommitInfo[];
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Parse git log output formatted with:
|
|
1394
|
+
* `COMMIT%x09%H%x09%h%x09%s%x09%ad%x09%an` and optional `--shortstat`.
|
|
1395
|
+
*/
|
|
1396
|
+
export function parseGitLogOutput(output: string): CommitInfo[] {
|
|
1397
|
+
if (!output) return [];
|
|
1398
|
+
const commits: CommitInfo[] = [];
|
|
1399
|
+
let current: CommitInfo | null = null;
|
|
1400
|
+
for (const rawLine of output.split("\n")) {
|
|
1401
|
+
const line = rawLine.trim();
|
|
1402
|
+
if (line.startsWith("COMMIT\t")) {
|
|
1403
|
+
if (current) commits.push(current);
|
|
1404
|
+
const parts = line.split("\t");
|
|
1405
|
+
if (parts.length >= 6) {
|
|
1406
|
+
const hash = parts[1];
|
|
1407
|
+
const shortHash = parts[2];
|
|
1408
|
+
const date = parts[parts.length - 2];
|
|
1409
|
+
const author = parts[parts.length - 1];
|
|
1410
|
+
const subject = parts.slice(3, parts.length - 2).join("\t") || "(no subject)";
|
|
1411
|
+
current = {
|
|
1412
|
+
hash,
|
|
1413
|
+
shortHash,
|
|
1414
|
+
subject,
|
|
1415
|
+
date,
|
|
1416
|
+
author,
|
|
1417
|
+
filesChanged: 0,
|
|
1418
|
+
insertions: 0,
|
|
1419
|
+
deletions: 0,
|
|
1420
|
+
};
|
|
1421
|
+
} else {
|
|
1422
|
+
current = null;
|
|
1423
|
+
}
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (current && line.includes("changed")) {
|
|
1427
|
+
const mFiles = line.match(/(\d+) file/);
|
|
1428
|
+
const mIns = line.match(/(\d+) insertion/);
|
|
1429
|
+
const mDel = line.match(/(\d+) deletion/);
|
|
1430
|
+
if (mFiles) current.filesChanged = Number(mFiles[1]);
|
|
1431
|
+
if (mIns) current.insertions = Number(mIns[1]);
|
|
1432
|
+
if (mDel) current.deletions = Number(mDel[1]);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (current) commits.push(current);
|
|
1436
|
+
return commits;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/** Group commits by date (preserving existing order within and between groups). */
|
|
1440
|
+
export function groupCommitsByDate(commits: CommitInfo[]): DateGroup[] {
|
|
1441
|
+
const groups = new Map<string, CommitInfo[]>();
|
|
1442
|
+
for (const c of commits) {
|
|
1443
|
+
if (!groups.has(c.date)) groups.set(c.date, []);
|
|
1444
|
+
groups.get(c.date)!.push(c);
|
|
1445
|
+
}
|
|
1446
|
+
return [...groups.entries()].map(([date, groupCommits]) => ({
|
|
1447
|
+
date,
|
|
1448
|
+
count: groupCommits.length,
|
|
1449
|
+
commits: groupCommits,
|
|
1450
|
+
}));
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Format the selected commits as a numbered detail block (hash, subject,
|
|
1455
|
+
* author, date, stats per commit). Injected into task prompts via the
|
|
1456
|
+
* `{{selected_commits}}` placeholder; composed into the full review prompt by
|
|
1457
|
+
* `formatCommitReviewPrompt`.
|
|
1458
|
+
*/
|
|
1459
|
+
export function formatSelectedCommits(commits: SelectedCommit[]): string {
|
|
1460
|
+
const count = commits.length;
|
|
1461
|
+
return commits
|
|
1462
|
+
.map((c, i) => {
|
|
1463
|
+
const num = count > 1 ? `${i + 1}. ` : "";
|
|
1464
|
+
return (
|
|
1465
|
+
`${num}Commit ${c.shortHash} (${c.hash})\n` +
|
|
1466
|
+
` Subject: ${c.subject}\n` +
|
|
1467
|
+
` Author: ${c.author} on ${c.date}\n` +
|
|
1468
|
+
` Stats: ${c.filesChanged} file${c.filesChanged !== 1 ? "s" : ""} changed, +${c.insertions}/-${c.deletions} lines`
|
|
1469
|
+
);
|
|
1470
|
+
})
|
|
1471
|
+
.join("\n\n");
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Build the review prompt for the selected commits to send to the agent.
|
|
1476
|
+
*/
|
|
1477
|
+
export function formatCommitReviewPrompt(commits: SelectedCommit[]): string {
|
|
1478
|
+
const count = commits.length;
|
|
1479
|
+
const commitDetails = formatSelectedCommits(commits);
|
|
1480
|
+
|
|
1481
|
+
const showCmd = count === 1 ? `git show ${commits[0].hash}` : "git show <hash>";
|
|
1482
|
+
|
|
1483
|
+
return (
|
|
1484
|
+
`Review the following git commit${count !== 1 ? "s" : ""}:\n\n` +
|
|
1485
|
+
`${commitDetails}\n\n` +
|
|
1486
|
+
`Please inspect the changes using \`${showCmd}\` (and any related files in the repository). ` +
|
|
1487
|
+
`Double-check the changes for:\n` +
|
|
1488
|
+
`1. Bugs, edge cases, potential regressions, and logic errors.\n` +
|
|
1489
|
+
`2. Code quality, security, and unintended side effects.\n` +
|
|
1490
|
+
`3. Consistency with surrounding project patterns and tests.\n\n` +
|
|
1491
|
+
`Summarize your findings for each commit and propose a plan for any fixes if needed. Do not make changes yet.`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
package/package.json
CHANGED
package/pi.image.png
CHANGED
|
Binary file
|