@henryqw/pi-subagent 27.8.0 → 27.9.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
@@ -155,6 +155,10 @@ The extension sends follow-ups when a candidate becomes ready, a worker needs at
155
155
 
156
156
  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.
157
157
 
158
+ Before the menu opens, and after each **Refresh**, a **Current subagent work** report shows which subagents are working. The extension calculates it from local and saved evidence without a model call. Without interactive UI, `/subagent` sends this report before the request list.
159
+
160
+ A direct workflow is working only when this Pi process owns it and has a running worker. Direct work with a delivered result shows its outcome under **Completed / history** and is not in the report. Other recorded-only direct work shows as unverified. An isolated request is working only when its status is active and a live run holds the repository lease. Without a live lease, an active request shows as interrupted; run `/subagent recover`.
161
+
158
162
  Each row starts with a status symbol: `●` active, `✓` completed, `⚠` needs attention, `✗` failed. In the terminal UI, `/subagent` reports (Roles, details, reviews, and results) show as colored transcript entries. The model does not get these entries. In RPC mode, they show as notifications. Without interactive UI, they show as plain text messages.
159
163
 
160
164
  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.
package/dist/runner.d.ts CHANGED
@@ -332,6 +332,8 @@ export declare class IsolatedRunner {
332
332
  invalidIds: string[];
333
333
  states: RunState[];
334
334
  }>;
335
+ /** True while a live productive run holds this root's lease; the lease does not name its request. */
336
+ hasProductiveRunLease(root: string): Promise<boolean>;
335
337
  /** Classify orphaned work under the same repository lease as productive execution.
336
338
  * Status remains read-only; Main alone chooses any subsequent action. */
337
339
  recoverRepository(root: string): Promise<{
package/dist/runner.js CHANGED
@@ -639,6 +639,10 @@ export class IsolatedRunner {
639
639
  })),
640
640
  })) };
641
641
  }
642
+ /** True while a live productive run holds this root's lease; the lease does not name its request. */
643
+ async hasProductiveRunLease(root) {
644
+ return await this.store.hasProductiveRunLease(root);
645
+ }
642
646
  /** Classify orphaned work under the same repository lease as productive execution.
643
647
  * Status remains read-only; Main alone chooses any subsequent action. */
644
648
  async recoverRepository(root) {
@@ -269,7 +269,7 @@ After readiness, use `subagent_stage revise` with the exact candidate, generatio
269
269
 
270
270
  ### Inspect and revise active work
271
271
 
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**.
272
+ To select work by label, open `/subagent` without arguments. First, a **Current subagent work** report shows the work that local and saved evidence proves is running: locally owned direct workflows with a running worker, and active isolated requests while a live run holds the repository lease. The report lists active requests without a live lease as interrupted, and recorded-only direct work as unverified. A live lease does not identify its request, so several active requests in one checkout all show as working. 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
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
 
@@ -842,7 +842,8 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
842
842
  catch (error) { console.error("Pi Subagent workspace widget update failed.", error); }
843
843
  }
844
844
  const orphans = await getComponents().runner.listOrphanRequests(root);
845
- return { root, requests: [...requests, ...orphans.requests], invalidIds: [...new Set([...invalidIds, ...orphans.invalidIds])] };
845
+ const leaseLive = await getComponents().runner.hasProductiveRunLease(root);
846
+ return { root, leaseLive, requests: [...requests, ...orphans.requests], invalidIds: [...new Set([...invalidIds, ...orphans.invalidIds])] };
846
847
  },
847
848
  async recover(cwd) {
848
849
  const root = await lookupRoot(cwd);
@@ -56,6 +56,8 @@ export interface IsolatedRequest {
56
56
  }
57
57
  export interface IsolatedInventory {
58
58
  root: string;
59
+ /** A live productive run holds this root's lease; the lease does not name its request. */
60
+ leaseLive: boolean;
59
61
  requests: readonly IsolatedRequest[];
60
62
  invalidIds: readonly string[];
61
63
  }
@@ -291,6 +293,41 @@ function directReport(task: DirectTask): Report {
291
293
  ] };
292
294
  }
293
295
 
296
+ const WORKING_REPORT_CODE_POINTS = 4_000;
297
+ /** Current work from local and saved evidence only. Recorded-only direct work and isolated work without a live lease never count as working. */
298
+ export function workingReport(direct: readonly DirectTask[], inventory: IsolatedInventory | undefined): Report {
299
+ const working: Row[] = [];
300
+ const attention: Row[] = [];
301
+ const unverified: Row[] = [];
302
+ for (const task of direct) {
303
+ const text = `${directLabel(task)} · ${task.status}`;
304
+ // Read workflows list their running workers; write workflows stay observed locally until their workers are proved stopped.
305
+ if (task.status === "observed locally" && task.stoppable?.length !== 0) working.push({ text: task.stoppable ? `${directLabel(task)} · ${task.stoppable.length} running` : text });
306
+ else if (/retained|attention/.test(task.status)) attention.push({ text });
307
+ else if (completed(task.status)) continue;
308
+ else unverified.push({ text: task.status === "observed locally" ? `${text}; no running worker` : text });
309
+ }
310
+ for (const request of inventory?.requests ?? []) {
311
+ const active = !request.orphaned && ["pending", "running"].includes(request.status);
312
+ if (active && inventory!.leaseLive) working.push({ text: isolatedLabel(request) });
313
+ else if (active) attention.push({ text: `${isolatedLabel(request)} · interrupted; run /subagent recover` });
314
+ else if (!request.orphaned && (request.status === "needs_attention" || /retained/.test(request.status))) attention.push({ text: isolatedLabel(request) });
315
+ }
316
+ const sections: Section[] = [
317
+ working.length ? { heading: `Working (${working.length})`, tone: "info", rows: working } : { rows: [{ text: inventory ? "No subagents are working." : "No direct subagents are working. Isolated status is unavailable." }] },
318
+ { heading: `Needs attention (${attention.length})`, tone: "warning", rows: attention },
319
+ { heading: `Unverified (${unverified.length})`, note: "not proof of running or completion", tone: "warning", rows: unverified },
320
+ ];
321
+ const head: Report = { tone: attention.length ? "warning" : "info", title: "Current subagent work", sections: [] };
322
+ const items = sections.flatMap((section) => section.rows.map((row, index) => ({ section, index, row })));
323
+ const fitted = fitItems(reportMarkdown(head), items, ({ section, index, row }) => index ? sectionMarkdown({ rows: [row] }) : `\n${sectionMarkdown({ ...section, rows: [row] })}`,
324
+ WORKING_REPORT_CODE_POINTS, "open /subagent with interactive UI to see them.");
325
+ return { ...head, sections: [...sections.flatMap((section) => {
326
+ const rows = fitted.shown.filter((item) => item.section === section).map(({ row }) => row);
327
+ return rows.length ? [{ ...section, rows }] : [];
328
+ }), ...moreSection(fitted.more)] };
329
+ }
330
+
294
331
  type Choice<T> = { label: string; value: T };
295
332
  async function choose<T>(ctx: ExtensionContext, title: string, items: readonly Choice<T>[]): Promise<T | undefined> {
296
333
  // Numeric suffixes only tell duplicate labels apart. Never parse display text into an identity.
@@ -462,6 +499,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
462
499
  }
463
500
  if (!ctx.hasUI) {
464
501
  const inventory = await adapter.isolated(ctx.cwd, current);
502
+ deliver(ctx, workingReport(adapter.direct(ctx), inventory));
465
503
  const head: Report = { tone: "info", title: "Isolated requests (report only)", sections: [] };
466
504
  const row = (request: IsolatedRequest): Row => ({ text: `${glyph(request.status)} ${request.orphaned ? "orphaned · " : ""}${isolatedLabel(request)}` });
467
505
  const listed = fitItems(`${reportMarkdown(head)}\n`, inventory.requests, (request) => sectionMarkdown({ rows: [row(request)] }), ROLES_REPORT_CODE_POINTS,
@@ -473,6 +511,8 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
473
511
  return;
474
512
  }
475
513
  let history = false;
514
+ // The working report shows on open and on each Refresh, not after other menu actions.
515
+ let report = true;
476
516
  for (;;) {
477
517
  if (!valid()) return;
478
518
  // Direct recovery is independent of Git and isolated config/inventory failures.
@@ -482,6 +522,8 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
482
522
  try { inventory = await adapter.isolated(ctx.cwd, current); }
483
523
  catch (error) { unavailable = errorText(error); }
484
524
  if (!valid()) return;
525
+ if (report) deliver(ctx, workingReport(direct, inventory));
526
+ report = false;
485
527
  if (unavailable) ctx.ui.notify(`Isolated inventory unavailable (${unavailable}). Direct branch recovery remains available; check the canonical Git checkout/configuration.`, "warning");
486
528
  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");
487
529
  type Selection = { kind: "direct"; task: DirectTask } | { kind: "isolated"; request: IsolatedRequest; root: string } | { kind: "refresh" } | { kind: "history" } | { kind: "many" } | { kind: "close" };
@@ -509,7 +551,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
509
551
  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");
510
552
  const selected = await choose(ctx, history ? "Subagent · completed / history" : "Subagent · current work", selections);
511
553
  if (!valid() || !selected || selected.kind === "close") return;
512
- if (selected.kind === "refresh") continue;
554
+ if (selected.kind === "refresh") { report = true; continue; }
513
555
  if (selected.kind === "history") { history = !history; continue; }
514
556
  if (selected.kind === "many") {
515
557
  const picked = await chooseMany(ctx, "Subagent · select work", rows.map(({ label, value }) => ({ label: label.trim(), value })), [], valid);
@@ -300,7 +300,10 @@ export default function subagentExtension(pi: ExtensionAPI): void {
300
300
  const local = directTasks.get(record.taskId);
301
301
  let task = grouped.get(record.taskId);
302
302
  if (!task) grouped.set(record.taskId, task = { id: record.taskId, name: record.taskId,
303
- status: local?.recovery ? "admission retained" : local ? "observed locally" : "recorded only; not proof of running or completion",
303
+ status: local?.recovery ? "admission retained" : local ? "observed locally"
304
+ // A recovery result did not prove worker termination, so it stays visible for recovery.
305
+ : results.get(record.taskId)?.recovery ? "needs attention: worker termination unproved"
306
+ : results.get(record.taskId)?.outcome ?? "recorded only; not proof of running or completion",
304
307
  ...(results.get(record.taskId)?.cost === undefined ? {} : { cost: results.get(record.taskId)!.cost }),
305
308
  ...(local?.recovery ? { recovery: local.recovery } : {}), canClose: Boolean(local?.close),
306
309
  ...(local?.stopWorker ? { stoppable: local.stopWorker.running() } : {}), workers: [], tabs: [] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "27.8.0",
3
+ "version": "27.9.0",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",