@henryqw/pi-subagent 27.6.0 → 27.6.1
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 +2 -2
- package/docs/orchestration.md +1 -1
- package/extensions/subagent-command.ts +25 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,9 +137,9 @@ The extension sends follow-ups when a candidate becomes ready, a worker needs at
|
|
|
137
137
|
|
|
138
138
|
### Inspect work
|
|
139
139
|
|
|
140
|
-
Open `/subagent` to browse direct work on this session branch and isolated requests in the current canonical Git checkout, including other sessions. **Refresh** reloads saved status without recovery or mutation. Invalid state IDs stay preserved with a warning. Direct recovery remains available outside Git or without isolated inventory.
|
|
140
|
+
Open `/subagent` to browse direct work on this session branch and isolated requests in the current canonical Git checkout, including other sessions. The menu groups rows under **Direct**, **Isolated**, and **Orphaned** headers. Each isolated row shows the request ID, the status, and a short goal summary. **Refresh** or a header reloads saved status without recovery or mutation. Invalid state IDs stay preserved with a warning. Direct recovery remains available outside Git or without isolated inventory.
|
|
141
141
|
|
|
142
|
-
Requests whose parent checkout was removed show
|
|
142
|
+
Requests whose parent checkout was removed show in the **Orphaned · parent checkout removed** group from any checkout of the same repository. Every isolated request, including an orphaned request, has **Close and delete worktrees (discard work)**. Select it to review owned resources and confirm deletion. **This permanently deletes committed, uncommitted, and ignored content in the listed checkouts.** Cancel makes no change.
|
|
143
143
|
|
|
144
144
|
Only shown resources can be removed; live jobs, uncertain ownership, or other Herdr panes can block removal. Agents and tools cannot trigger this action. See [Close and delete isolated worktrees](./docs/orchestration.md#close-and-delete-isolated-worktrees) for guards and partial cleanup.
|
|
145
145
|
|
package/docs/orchestration.md
CHANGED
|
@@ -271,7 +271,7 @@ After readiness, use `subagent_stage revise` with the exact candidate, generatio
|
|
|
271
271
|
|
|
272
272
|
To select work by label, open `/subagent` without arguments. The menu shows direct records on this session branch and isolated requests in the current canonical Git checkout. This includes requests launched in other sessions. Select an isolated request. Then select **Inspect status and recovery**.
|
|
273
273
|
|
|
274
|
-
The menu also shows
|
|
274
|
+
The menu also shows requests whose saved parent checkout no longer exists, in their own **Orphaned · parent checkout removed** group. You can open this view from any checkout of the same repository. The saved worker or integration path identifies the primary repository. Orphaned requests have the same **Close and delete worktrees (discard work)** action, but no inspection or follow-up actions. The extension reads their original state directories; it does not recreate the removed parent checkout. Unreadable files stay preserved and are reported.
|
|
275
275
|
|
|
276
276
|
This launches a Herdr tab without focus, with the configured `scout` Role on the configured `fast` route. The tab runs a read-access task with the resources of the Role. Its prompt contains bounded saved status, including retained paths, but not the raw launch environment or session transcripts. The analysis stays in that tab and does not queue a Main turn. The tab shows in the direct list with its own reference and name, such as `D9.1 · scout · Inspect <request ID>`. The extension records the tab and session file for recovery, even if the launch becomes uncertain.
|
|
277
277
|
|
|
@@ -101,6 +101,13 @@ export interface SubagentCommandAdapter {
|
|
|
101
101
|
const clean = (text: string, max = 110, multiline = false): string => [...text.replace(multiline ? /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, " ")].slice(0, max).join("");
|
|
102
102
|
const ROLES_REPORT_CODE_POINTS = 16_000;
|
|
103
103
|
const errorText = (error: unknown): string => clean(error instanceof Error ? error.message : String(error), 500);
|
|
104
|
+
/** One-line summary of free text such as a goal; the ellipsis marks a cut. */
|
|
105
|
+
const summary = (text: string, max = 60): string => {
|
|
106
|
+
const chars = [...clean(text, Number.MAX_SAFE_INTEGER).replace(/\s+/g, " ").trim()];
|
|
107
|
+
return chars.length > max ? `${chars.slice(0, max - 1).join("")}…` : chars.join("");
|
|
108
|
+
};
|
|
109
|
+
// Identity and status first so clipping cuts only the goal summary.
|
|
110
|
+
const isolatedLabel = ({ id, status, name }: IsolatedRequest): string => `${id} · ${status} · ${summary(name)}`;
|
|
104
111
|
const completed = (status: string) => ["completed", "success", "failed", "failure", "aborted", "integrated"].includes(status);
|
|
105
112
|
const priority = (status: string) => /attention|blocked|retained|failed/i.test(status) ? 0 : completed(status) ? 2 : 1;
|
|
106
113
|
|
|
@@ -162,8 +169,9 @@ function discardReview(plan: DiscardPlan) {
|
|
|
162
169
|
|
|
163
170
|
type Choice<T> = { label: string; value: T };
|
|
164
171
|
async function choose<T>(ctx: ExtensionContext, title: string, items: readonly Choice<T>[]): Promise<T | undefined> {
|
|
165
|
-
// Numeric suffixes
|
|
166
|
-
const
|
|
172
|
+
// Numeric suffixes only tell duplicate labels apart. Never parse display text into an identity.
|
|
173
|
+
const clipped = items.map((item) => clean(item.label, 145));
|
|
174
|
+
const labels = clipped.map((label, index) => clipped.indexOf(label) === clipped.lastIndexOf(label) ? label : `${label} [${index + 1}]`);
|
|
167
175
|
const selected = await ctx.ui.select(title, labels);
|
|
168
176
|
const index = labels.indexOf(selected ?? "");
|
|
169
177
|
return index < 0 ? undefined : items[index]!.value;
|
|
@@ -244,7 +252,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
244
252
|
if (!ctx.hasUI) {
|
|
245
253
|
const inventory = await adapter.isolated(ctx.cwd, current);
|
|
246
254
|
const report = fitItems("Isolated requests (report only):", inventory.requests,
|
|
247
|
-
(request) => clean(`${request.orphaned ? "orphaned · " : ""}${request
|
|
255
|
+
(request) => clean(`${request.orphaned ? "orphaned · " : ""}${isolatedLabel(request)}`, 500), ROLES_REPORT_CODE_POINTS,
|
|
248
256
|
"open /subagent with interactive UI to inspect them.");
|
|
249
257
|
pi.sendMessage({ customType: "pi-subagent-discard", content: `${report.text}\nUnreadable state files preserved: ${inventory.invalidIds.length}.\nRemoval requires interactive confirmation.`, display: true });
|
|
250
258
|
return;
|
|
@@ -262,19 +270,21 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
262
270
|
if (unavailable) ctx.ui.notify(`Isolated inventory unavailable (${unavailable}). Direct branch recovery remains available; check the canonical Git checkout/configuration.`, "warning");
|
|
263
271
|
if (inventory?.invalidIds.length) for (const id of inventory.invalidIds) ctx.ui.notify(`Unreadable isolated state ID: ${JSON.stringify(id)}. Preserve this file; inspect the state store before making changes.`, "warning");
|
|
264
272
|
type Selection = { kind: "direct"; task: DirectTask } | { kind: "isolated"; request: IsolatedRequest; root: string } | { kind: "refresh" } | { kind: "history" } | { kind: "close" };
|
|
273
|
+
// A header row selects Refresh; rows sort by status priority within their group.
|
|
274
|
+
const group = <T extends { status: string }>(title: string, items: readonly T[], choice: (item: T) => Choice<Selection>): Choice<Selection>[] =>
|
|
275
|
+
items.length ? [{ label: `── ${title} (${items.length}) ──`, value: { kind: "refresh" } },
|
|
276
|
+
...[...items].sort((a, b) => priority(a.status) - priority(b.status)).map(choice)] : [];
|
|
277
|
+
const requests = inventory?.requests ?? [];
|
|
278
|
+
const isolatedChoice = (request: IsolatedRequest): Choice<Selection> => ({ label: ` ${isolatedLabel(request)}`, value: { kind: "isolated", request, root: inventory!.root } });
|
|
265
279
|
const selections: Choice<Selection>[] = [
|
|
266
280
|
// One row per workflow; new records keep opaque IDs in details. Status is last so clipping preserves the reference.
|
|
267
|
-
...direct.filter((task) => history || !completed(task.status))
|
|
268
|
-
...(
|
|
269
|
-
|
|
270
|
-
{ label:
|
|
271
|
-
{ label: "
|
|
281
|
+
...group("Direct", direct.filter((task) => history || !completed(task.status)), (task) => ({ label: ` ${directLabel(task)} · ${task.status}${costSuffix(task.cost)}${task.workers.length || task.name === task.id ? "" : ` · ${task.id}`}`, value: { kind: "direct", task } })),
|
|
282
|
+
...group("Isolated", requests.filter((request) => !request.orphaned && (history || !completed(request.status) || /retained/i.test(request.status))), isolatedChoice),
|
|
283
|
+
...group("Orphaned · parent checkout removed", requests.filter((request) => request.orphaned), isolatedChoice),
|
|
284
|
+
{ label: "Refresh", value: { kind: "refresh" } },
|
|
285
|
+
{ label: history ? "Active work" : "Completed / history (including retained work)", value: { kind: "history" } },
|
|
286
|
+
{ label: "Close", value: { kind: "close" } },
|
|
272
287
|
];
|
|
273
|
-
const taskChoices = selections.splice(0, selections.length - 3);
|
|
274
|
-
const rank = ({ value }: Choice<Selection>) => value.kind === "direct" ? priority(value.task.status)
|
|
275
|
-
: value.kind === "isolated" ? priority(value.request.status) : 3;
|
|
276
|
-
taskChoices.sort((a, b) => rank(a) - rank(b));
|
|
277
|
-
selections.unshift(...taskChoices);
|
|
278
288
|
if (!unavailable && !direct.length && !inventory?.requests.length && !inventory?.invalidIds.length) ctx.ui.notify("No direct tasks recorded on this session branch or isolated requests in this checkout. Ask Main to start delegation.", "info");
|
|
279
289
|
const selected = await choose(ctx, history ? "Subagent · completed / history" : "Subagent · current work", selections);
|
|
280
290
|
if (!valid() || !selected || selected.kind === "close") return;
|
|
@@ -325,7 +335,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
325
335
|
...(eligible.length ? [{ label: "Send follow-up instructions", value: "send" as const }, { label: "Edit queued instructions (withdraw ALL pending; Cancel leaves withdrawn)", value: "edit" as const }] : []),
|
|
326
336
|
{ label: "Back / refresh", value: "back" },
|
|
327
337
|
];
|
|
328
|
-
const action = await choose(ctx,
|
|
338
|
+
const action = await choose(ctx, `${request.orphaned ? "Orphaned" : "Isolated"} · ${request.id} · ${summary(request.name)}`, actions);
|
|
329
339
|
if (!valid() || !action) return;
|
|
330
340
|
if (action === "back") break;
|
|
331
341
|
if (action === "discard") {
|
|
@@ -356,7 +366,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
356
366
|
}
|
|
357
367
|
const live = request.tasks.filter((task) => task.kind === "changeset" && adapter.canFollowup(root, request.id, task.id));
|
|
358
368
|
if (!live.length) { ctx.ui.notify("No locally active unsealed changeset remains. Refresh to see current status.", "warning"); continue; }
|
|
359
|
-
const task = await choose(ctx, `Select task for ${action === "edit" ? "withdrawal and edit" : "follow-up"}`, live.map((item) => ({ label: `${item.
|
|
369
|
+
const task = await choose(ctx, `Select task for ${action === "edit" ? "withdrawal and edit" : "follow-up"}`, live.map((item) => ({ label: `${item.id} · ${item.status} · ${summary(item.name)}`, value: item })));
|
|
360
370
|
if (!valid() || !task) return;
|
|
361
371
|
let prefill = "";
|
|
362
372
|
if (action === "edit") {
|