@henryqw/pi-subagent 27.5.2 → 27.5.3

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.
@@ -375,7 +375,7 @@ The report lists removed, skipped, and failed resources. After complete cleanup,
375
375
 
376
376
  ### Prune leftover worktrees
377
377
 
378
- Run `/subagent prune` to clean up leftover worker and integration checkouts under `.worktrees/`. Their branches have the name `pi-subagent/subagent-` plus 24 hex characters. The command also cleans up such branches that have no checkout. The review lists each item with its path, branch, and tip. For each item, it also shows why the command can remove the item, or it shows every exact blocker. The command can remove an item only when all of these conditions are true:
378
+ Run `/subagent prune` to clean up leftover worker and integration checkouts under `.worktrees/`. Their branches have the name `pi-subagent/subagent-` plus 24 hex characters. The command also cleans up such branches that have no checkout. The review starts with the number of removable and blocked items. It groups the items by the exact reason that the command can remove them, or by their exact blockers, and shows each reason once. Under each reason, it identifies each item by the first 8 of its 24 hex characters. The command can remove an item only when all of these conditions are true:
379
379
 
380
380
  - Its branch tip is already in Main's HEAD or in the local `origin` default-branch ref. The command does not fetch.
381
381
  - Its checkout has no tracked or untracked changes. No merge, rebase, cherry-pick, revert, or bisect is in progress.
@@ -387,7 +387,7 @@ One confirmation removes every listed removable item. When the command removes a
387
387
 
388
388
  If a top-level ignored entry is new or changed, the command skips the checkout. If the full list of ignored entries of a checkout does not fit in that dialog, the command excludes the checkout. The command blocks checkouts with initialized submodules. It also blocks owned checkouts that are detached or on another branch. Remove them manually.
389
389
 
390
- The list shows removable items first. The command never cuts a long list silently. The list ends with `+N more not shown`, and the command does not remove the items that it did not show in that run. Run `/subagent prune` again to review them.
390
+ The list shows removable items first. The command never cuts a long list silently. The list ends with `+N more not shown`, where `N` counts items, and the command does not remove the items that it did not show in that run. Run `/subagent prune` again to review them.
391
391
 
392
392
  The command checks each item again immediately before removal and skips an item that changed. If the session or branch changes during removal, the command skips the remaining steps. The command deletes a branch only if the branch still points at the reviewed tip. Then the command reconciles each aborted request that recorded a removed resource. It uses the same guarded path as a repeated `subagent_abort`, so the cleanup of that request no longer shows as pending.
393
393
 
@@ -100,13 +100,12 @@ const errorText = (error: unknown): string => clean(error instanceof Error ? err
100
100
  const completed = (status: string) => ["completed", "success", "failed", "failure", "aborted", "integrated"].includes(status);
101
101
  const priority = (status: string) => /attention|blocked|retained|failed/i.test(status) ? 0 : completed(status) ? 2 : 1;
102
102
 
103
- const ignoredSummary = ({ ignored }: PruneItem) => !ignored.length ? "none"
104
- : `${ignored.length} (${ignored.slice(0, 5).join(", ")}${ignored.length > 5 ? `, +${ignored.length - 5} more` : ""})`;
105
103
  const PRUNE_IGNORED_CONFIRM_CODE_POINTS = 4_000;
106
104
  const NOT_SHOWN = "they are not removed in this run; run /subagent prune again after this run.";
107
105
 
108
106
  /** Fit whole item lines within `max` code points; never cut silently, and report exactly which items were shown. */
109
- function fitItems<T>(header: string, items: readonly T[], line: (item: T) => string, max: number, omitted = NOT_SHOWN): { text: string; shown: T[] } {
107
+ function fitItems<T>(header: string, items: readonly T[], line: (item: T) => string, max: number, omitted = NOT_SHOWN,
108
+ count: (item: T) => number = () => 1): { text: string; shown: T[] } {
110
109
  const lines = [header];
111
110
  const shown: T[] = [];
112
111
  let size = [...header].length;
@@ -119,18 +118,30 @@ function fitItems<T>(header: string, items: readonly T[], line: (item: T) => str
119
118
  size += length;
120
119
  shown.push(item);
121
120
  }
122
- if (shown.length < items.length) lines.push(`+${items.length - shown.length} more not shown; ${omitted}`);
121
+ const hidden = items.slice(shown.length).reduce((sum, item) => sum + count(item), 0);
122
+ if (hidden) lines.push(`+${hidden} more not shown; ${omitted}`);
123
123
  return { text: lines.join("\n"), shown };
124
124
  }
125
125
 
126
- /** Prunable items come first so a long blocked list cannot hide them; only shown items may be removed. */
126
+ /** Groups items by exact reason, prunable groups first, in lines of 10 IDs so a long list is cut at whole lines.
127
+ * Only shown items may be removed. IDs are display-only; removal uses the reviewed items. */
127
128
  function pruneReview({ root, items }: PrunePlan): { text: string; shown: PruneItem[] } {
128
129
  if (!items.length) return { text: `No pi-subagent worktrees or branches in ${root}.`, shown: [] };
130
+ const groups = new Map<string, PruneItem[]>();
131
+ for (const item of [...items.filter((item) => item.prunable), ...items.filter((item) => !item.prunable)]) {
132
+ const title = item.prunable ? `✓ Prunable: ${item.reason}` : `✗ Blocked: ${item.path ? item.reason : `${item.reason}; no checkout`}`;
133
+ if (groups.has(title)) groups.get(title)!.push(item);
134
+ else groups.set(title, [item]);
135
+ }
136
+ const rows = [...groups].flatMap(([title, group]) => Array.from({ length: Math.ceil(group.length / 10) }, (_, index) => {
137
+ const chunk = group.slice(index * 10, index * 10 + 10);
138
+ const ids = chunk.map(({ branch }) => clean(branch.replace(/^pi-subagent\/subagent-/, "").slice(0, 8))).join(", ");
139
+ return { items: chunk, text: `${index ? "" : `${clean(title, 600)} (${group.length}):\n`} ${ids}` };
140
+ }));
129
141
  const prunable = items.filter((item) => item.prunable).length;
130
- return fitItems(`Subagent prune review in ${root}: ${prunable} prunable, ${items.length - prunable} blocked.`,
131
- [...items.filter((item) => item.prunable), ...items.filter((item) => !item.prunable)],
132
- (item) => clean(`${item.prunable ? "✓ prunable" : "✗ blocked"} · ${item.path ?? "(no checkout)"} · ${item.branch} · ${item.tip.slice(0, 12)} · ${item.reason} · ignored: ${ignoredSummary(item)}`, 600),
133
- ROLES_REPORT_CODE_POINTS);
142
+ const reviewed = fitItems(`Subagent prune review in ${root}: ${prunable} prunable, ${items.length - prunable} blocked. IDs are the first 8 characters after pi-subagent/subagent-.`,
143
+ rows, (row) => row.text, ROLES_REPORT_CODE_POINTS, NOT_SHOWN, (row) => row.items.length);
144
+ return { text: reviewed.text, shown: reviewed.shown.flatMap((row) => row.items) };
134
145
  }
135
146
 
136
147
  function discardReview(plan: DiscardPlan) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "27.5.2",
3
+ "version": "27.5.3",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",