@ferris1225/pi-subagents 4.1.23 → 4.2.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/src/widget.ts CHANGED
@@ -1,24 +1,17 @@
1
1
  /**
2
2
  * Compact, glanceable active-run widget for interactive Pi sessions.
3
3
  *
4
- * Layout contract (redesign):
4
+ * Layout contract:
5
5
  * - Aligned identity columns: `icon #id agent` pad to the widest displayed
6
6
  * id and agent so every label starts at the same column; a resumed thread
7
7
  * carries a dim `↻` inside the agent column.
8
- * - Multi-line rows flow inline: the telemetry (`provider/model/thinking`,
9
- * token flow in the pi-footer vocabulary `↑in ↓out R/W cache`, cost, wait
10
- * state, seconds-precision elapsed) follows the content after ` · ` instead
11
- * of a right-aligned column no blank padding across a multi-line chain.
8
+ * - A live run owns two lines. Line 1 is what it is: identity, task label,
9
+ * then the telemetry flow (`provider/model`, token flow in the pi-footer
10
+ * vocabulary `↑in ↓out R/W cache`, cost, wait state, seconds-precision
11
+ * elapsed). Line 2 is what it is doing right now: the live activity, dim,
12
+ * indented under the label column behind a `↳` marker.
12
13
  * - Telemetry drops leftmost-first under width pressure (badge, wait, usage,
13
14
  * model); the elapsed survives every width.
14
- * - First line is the parent pi session itself: what the current model is
15
- * doing right now while its agent loop runs (model/thinking, live activity,
16
- * loop elapsed), fed by the session's own extension events.
17
- * - A managed workflow renders as a tree chain under its parent line: one
18
- * `├`/`└`-connected row per stage, each carrying its own model, token flow,
19
- * and elapsed — settled stages from the snapshot frozen at settlement, the
20
- * live stage from its child row. An oversized chain keeps a window anchored
21
- * on the live stage.
22
15
  * - Queued rows say what they actually wait for ("queued" for a process slot,
23
16
  * "repo lane" for shared-writer serialization, "starting" while the child
24
17
  * process launches) instead of one catch-all "queued".
@@ -27,19 +20,14 @@
27
20
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
28
21
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
29
22
  import {
30
- formatDuration,
31
23
  formatElapsed,
32
24
  formatTaskSummary,
33
25
  formatUsageTokens,
34
26
  isRunActiveStatus,
35
27
  monitor,
28
+ shrinkRunLabel,
36
29
  statusIcon,
37
- sumUsage,
38
- usageCostPart,
39
- type MainActivity,
40
30
  type RunView,
41
- type WorkflowStage,
42
- type WorkflowStageStatus,
43
31
  } from "./monitor.ts";
44
32
  import type { UsageStats } from "./rpc-run.ts";
45
33
 
@@ -52,8 +40,8 @@ const MAX_WIDGET_LINES = 10;
52
40
  const SEPARATOR = " · ";
53
41
  /** Column gap between the identity block and the run's label. */
54
42
  const IDENTITY_GAP = " ";
55
- /** Splits "what this run is" from "what it is doing right now". */
56
- const ACTIVITY_SEPARATOR = " ";
43
+ /** Marker introducing the live-activity second line of a running row. */
44
+ const ACTIVITY_MARKER = " ";
57
45
  /** Columns kept for left content before right-tail parts are dropped. */
58
46
  const LEFT_MIN_CONTENT = 8;
59
47
  /** Minimum useful width for a live-activity fragment. */
@@ -89,7 +77,7 @@ function waitWord(run: Pick<RunView, "waitReason">): string {
89
77
  }
90
78
 
91
79
  /** Worktree-group badge shown on the row that owns the isolated worktree: the
92
- * short group identity plus its integration state, so a workflow visibly moves
80
+ * short group identity plus its integration state, so a run visibly moves
93
81
  * through applying → applied (or retained) and a continuation worktree (new
94
82
  * identity) is distinguishable from the original one. */
95
83
  function worktreeBadge(run: RunView): string {
@@ -124,9 +112,7 @@ function agentColumnText(run: RunView): string {
124
112
  * right-aligned and the agent column is padded so every label starts at the
125
113
  * same x; a resumed thread carries a dim `↻` inside the agent column. */
126
114
  function identitySegment(run: RunView, theme: Theme, layout: ColumnLayout): string {
127
- const icon = run.managedWorkflow && run.status === "running"
128
- ? theme.fg("accent", theme.bold("◆"))
129
- : statusIcon(run.status, theme);
115
+ const icon = statusIcon(run.status, theme);
130
116
  const id = `#${run.id}`.padStart(layout.idWidth);
131
117
  const name = theme.fg("accent", theme.bold(run.agent));
132
118
  const resumed = run.continuationKind ? ` ${theme.fg("dim", "↻")}` : "";
@@ -137,300 +123,103 @@ function identitySegment(run: RunView, theme: Theme, layout: ColumnLayout): stri
137
123
  /** One footer-style usage part: token flow plus accrued cost, dropped as a
138
124
  * unit before the model under width pressure. */
139
125
  function usagePart(usage: UsageStats | undefined): string | undefined {
140
- return [formatUsageTokens(usage), usageCostPart(usage)].filter(Boolean).join(" ") || undefined;
126
+ return [formatUsageTokens(usage), usage?.cost ? `$${usage.cost.toFixed(4)}` : undefined].filter(Boolean).join(" ") || undefined;
141
127
  }
142
128
 
143
- /** Telemetry tail parts shared by primary and stage rows: badge and wait word
144
- * first (dropped first under pressure), then the usage part, the model, and
145
- * the always-surviving elapsed. `usage` defaults to the run's own; the managed
146
- * workflow parent overrides it with the workflow-wide aggregate. */
147
- function telemetryTailParts(run: RunView, now: number, usage: UsageStats = run.usage): Array<string | undefined> {
148
- // Queued rows omit the model (the route is re-resolved at actual start);
149
- // workflow parents omit it too (each stage row owns its own model). The
150
- // full provider/model ref is kept — "which provider served this run" is
129
+ /** Telemetry tail parts of a run row: badge and wait word first (dropped first
130
+ * under pressure), then the usage part, the model, and the always-surviving
131
+ * elapsed. */
132
+ function telemetryTailParts(run: RunView, now: number): Array<string | undefined> {
133
+ // Queued rows omit the model (the route is re-resolved at actual start).
134
+ // The full provider/model ref is kept "which provider served this run" is
151
135
  // exactly what a multi-provider session needs to see.
152
- const modelPart = run.status === "queued" || run.managedWorkflow || !run.model
153
- ? undefined
154
- : `${run.model}${run.thinking ? `/${run.thinking}` : ""}`;
136
+ const modelPart = run.status === "queued" || !run.model ? undefined : run.model;
155
137
  const badge = run.isolation === "worktree" ? worktreeBadge(run) : undefined;
156
138
  const wait = run.status === "queued" ? waitWord(run) : undefined;
157
139
  // Drop order under pressure: badge, wait word, usage, model; elapsed
158
140
  // survives every width the identity leaves room for.
159
- return [badge, wait, usagePart(usage), modelPart, formatElapsed(run, now) || undefined];
141
+ return [badge, wait, usagePart(run.usage), modelPart, formatElapsed(run, now) || undefined];
160
142
  }
161
143
 
162
- /** One primary line per run. Left: identity label activity, where the
163
- * label stays plain and the live activity is dim. Telemetry flows inline
164
- * after ` · ` (worktree badge, token flow, cost, model/thinking, wait state,
165
- * elapsed). The activity outranks the label when space runs out; the identity
166
- * and elapsed survive every width. */
144
+ /** Two lines for a live run. Line 1 is what the run is: identity, task label,
145
+ * then the telemetry flow (worktree badge, token flow, cost, provider/model,
146
+ * wait state, elapsed). Line 2 is what it is doing right now: the live
147
+ * activity, dim, indented under the label column behind a `↳` marker. The
148
+ * label takes the full content budget on line 1; the identity and the elapsed
149
+ * survive every width. */
167
150
  function primaryLine(
168
151
  run: RunView,
169
152
  theme: Theme,
170
153
  width: number,
171
154
  now: number,
172
155
  layout: ColumnLayout,
173
- usage: UsageStats = run.usage,
174
- ): string {
175
- const dim = (text: string): string => theme.fg("dim", text);
156
+ ): string[] {
176
157
  const identity = identitySegment(run, theme, layout);
177
158
  const tailBudget = Math.max(0, width - visibleWidth(identity) - LEFT_MIN_CONTENT);
178
- const tail = composeTail(telemetryTailParts(run, now, usage), tailBudget);
179
-
180
- // A chain child rendered at root level (its parent row is gone) keeps its
181
- // workflow relation; the templated brief itself would only repeat content.
182
- const label = run.parentRunId !== undefined
183
- ? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(SEPARATOR)
184
- : run.label ?? formatTaskSummary(run.task, 48);
185
- // The parent's own activity is a placeholder while a managed workflow runs;
186
- // the timeline line below carries the live stage instead.
187
- const activity = !run.managedWorkflow && (run.status === "running" || run.status === "interrupting")
188
- ? run.activity?.trim()
189
- : undefined;
190
-
159
+ const tail = composeTail(telemetryTailParts(run, now), tailBudget);
160
+ const label = run.label ?? formatTaskSummary(run.task, 48);
191
161
  const contentBudget = width
192
162
  - visibleWidth(identity)
193
163
  - (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0)
194
164
  - visibleWidth(IDENTITY_GAP);
195
- let content = "";
196
- if (contentBudget > 0) {
197
- const labelBudget = activity
198
- ? Math.min(visibleWidth(label), Math.max(12, Math.floor(contentBudget * 0.4)))
199
- : contentBudget;
200
- // The label is already fragment-extracted (runLabel); plain right
201
- // truncation avoids stacking a second head…tail ellipsis on top of it.
202
- const labelText = label
203
- ? visibleWidth(label) > labelBudget ? truncateToWidth(label, labelBudget, "…") : label
204
- : "";
205
- if (activity) {
206
- const activityBudget = contentBudget
207
- - visibleWidth(labelText)
208
- - (labelText ? visibleWidth(ACTIVITY_SEPARATOR) : 0);
209
- content = activityBudget >= ACTIVITY_MIN_WIDTH
210
- ? `${labelText}${labelText ? dim(ACTIVITY_SEPARATOR) : ""}${dim(formatTaskSummary(activity, activityBudget))}`
211
- // Too narrow for both: the live activity is the stronger signal.
212
- : dim(formatTaskSummary(activity, contentBudget));
213
- } else {
214
- content = labelText;
215
- }
216
- }
217
-
165
+ // The label is already fragment-extracted (runLabel); narrowing it keeps
166
+ // its tail so a second squeeze never trades away the recognisable
167
+ // filename, and no second head…tail ellipsis stacks on top of it.
168
+ const content = label && contentBudget > 0 ? shrinkRunLabel(label, contentBudget) : "";
218
169
  const left = content ? `${identity}${IDENTITY_GAP}${content}` : identity;
219
- return composeLine(left, tail, theme, width);
220
- }
170
+ const lines = [composeLine(left, tail, theme, width)];
221
171
 
222
- function stageIcon(status: WorkflowStageStatus, theme: Theme): string {
223
- switch (status) {
224
- case "done":
225
- return theme.fg("success", "✓");
226
- case "active":
227
- return theme.fg("accent", theme.bold("●"));
228
- case "changes":
229
- return theme.fg("warning", "!");
230
- case "failed":
231
- return theme.fg("error", "✗");
232
- default:
233
- return theme.fg("dim", "○");
234
- }
235
- }
236
-
237
- /** Stage telemetry source: the live child while the stage runs, the frozen
238
- * snapshot once it has settled. */
239
- interface StageTelemetry {
240
- model?: string;
241
- thinking?: string;
242
- usage?: UsageStats;
243
- elapsed: string;
244
- activity?: string;
245
- }
246
-
247
- function stageTelemetry(
248
- stage: WorkflowStage,
249
- live: RunView | undefined,
250
- now: number,
251
- ): StageTelemetry {
252
- if (live) {
253
- return {
254
- model: live.model,
255
- thinking: live.thinking,
256
- usage: live.usage,
257
- elapsed: formatElapsed(live, now),
258
- activity: live.activity?.trim() || undefined,
259
- };
260
- }
261
- return { model: stage.model, usage: stage.usage, elapsed: stage.elapsedMs !== undefined ? formatDuration(stage.elapsedMs) : "" };
262
- }
263
-
264
- /** The parent pi session's own line — what the current model is doing right
265
- * now while its agent loop runs: full model/thinking ref, live activity, loop
266
- * elapsed. Not a run, so it owns no `#id`; it joins the identity layout so its
267
- * label column lines up with the run rows. */
268
- function mainLine(main: MainActivity, theme: Theme, width: number, now: number, layout: ColumnLayout): string {
269
- const dim = (text: string): string => theme.fg("dim", text);
270
- const icon = theme.fg("accent", "●");
271
- const name = theme.fg("accent", theme.bold("pi"));
272
- const pad = " ".repeat(Math.max(0, layout.agentWidth - visibleWidth("pi")));
273
- const identity = `${icon} ${" ".repeat(layout.idWidth + 1)}${name}${pad}`;
274
- const modelPart = main.model ? `${main.model}${main.thinking ? `/${main.thinking}` : ""}` : undefined;
275
- const elapsed = formatDuration(Math.max(0, now - main.activeSince));
276
- const tail = composeTail([modelPart, elapsed], Math.max(0, width - visibleWidth(identity) - LEFT_MIN_CONTENT));
277
- const contentBudget = width
278
- - visibleWidth(identity)
279
- - (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0)
280
- - visibleWidth(IDENTITY_GAP);
281
- let left = identity;
282
- if (main.activity?.trim() && contentBudget >= ACTIVITY_MIN_WIDTH) {
283
- left = `${identity}${IDENTITY_GAP}${dim(formatTaskSummary(main.activity.trim(), contentBudget))}`;
284
- }
285
- return composeLine(left, tail, theme, width);
286
- }
287
-
288
- /** One `├`/`└`-connected row per managed-workflow stage: status icon,
289
- * relation, live activity for the running stage, and its own model/token/
290
- * cost/elapsed telemetry flowing inline. The chain hangs off the parent line,
291
- * so who dispatched what stays visible while the auto-fix workflow progresses. */
292
- function workflowStageLines(
293
- stages: readonly WorkflowStage[],
294
- children: readonly RunView[],
295
- theme: Theme,
296
- width: number,
297
- now: number,
298
- ): { lines: string[]; activeIndex: number } {
299
- const live = children.find((candidate) => candidate.status === "running" || candidate.status === "interrupting")
300
- ?? children.at(-1);
301
- const activeIndex = stages.findIndex((stage) => stage.status === "active");
302
- const lines: string[] = [];
303
- for (const [index, stage] of stages.entries()) {
304
- const telemetry = stageTelemetry(stage, index === activeIndex ? live : undefined, now);
305
- const connector = theme.fg("dim", index === stages.length - 1 ? "└" : "├");
306
- const indent = ` ${connector} `;
307
- const budget = width - visibleWidth(indent);
308
- if (budget <= 0) break;
309
- const icon = stageIcon(stage.status, theme);
310
- const label = `${icon} ${stage.relation}`;
311
- const modelPart = telemetry.model
312
- ? `${telemetry.model}${telemetry.thinking ? `/${telemetry.thinking}` : ""}`
313
- : undefined;
314
- const parts = [
315
- usagePart(telemetry.usage),
316
- modelPart,
317
- telemetry.elapsed || undefined,
318
- ];
319
- const tailBudget = Math.max(0, budget - visibleWidth(label) - ACTIVITY_MIN_WIDTH);
320
- const tail = composeTail(parts, tailBudget);
321
- const contentBudget = budget - (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0);
322
- let left = label;
323
- if (telemetry.activity && contentBudget - visibleWidth(label) - visibleWidth(ACTIVITY_SEPARATOR) >= ACTIVITY_MIN_WIDTH) {
324
- const activityBudget = contentBudget - visibleWidth(label) - visibleWidth(ACTIVITY_SEPARATOR);
325
- left = `${label}${theme.fg("dim", ACTIVITY_SEPARATOR)}${theme.fg("dim", formatTaskSummary(telemetry.activity, activityBudget))}`;
172
+ const activity = run.status === "running" || run.status === "interrupting"
173
+ ? run.activity?.trim()
174
+ : undefined;
175
+ if (activity) {
176
+ const indent = visibleWidth(identity) + visibleWidth(IDENTITY_GAP);
177
+ const activityBudget = width - indent - visibleWidth(ACTIVITY_MARKER);
178
+ if (activityBudget >= ACTIVITY_MIN_WIDTH) {
179
+ lines.push(
180
+ `${" ".repeat(indent)}${theme.fg("dim", `${ACTIVITY_MARKER}${formatTaskSummary(activity, activityBudget)}`)}`,
181
+ );
326
182
  }
327
- lines.push(`${indent}${composeLine(left, tail, theme, budget)}`);
328
183
  }
329
- return { lines, activeIndex };
330
- }
331
-
332
- /** Fit one workflow group into the remaining line budget: the primary line
333
- * always survives, and the stage window anchors on the live stage — settled
334
- * stages above the window collapse into one `… +N` marker, never the live
335
- * stage itself. */
336
- function fitGroupLines(
337
- primary: string,
338
- stages: readonly string[],
339
- activeIndex: number,
340
- remaining: number,
341
- theme: Theme,
342
- ): string[] {
343
- if (stages.length === 0 || stages.length + 1 <= remaining) return [primary, ...stages];
344
- const slots = remaining - 1;
345
- if (slots <= 0) return [primary];
346
- // Reserve one line for an overflow marker so a cut is always announced.
347
- const room = Math.max(1, slots - 1);
348
- const anchor = activeIndex >= 0 ? activeIndex : 0;
349
- const start = Math.max(0, Math.min(anchor, stages.length - room));
350
- const window = stages.slice(start, start + room);
351
- const parts = [
352
- ...(start > 0 ? [theme.fg("dim", `… +${start}`)] : []),
353
- ...window,
354
- ...(start + room < stages.length ? [theme.fg("dim", `… +${stages.length - start - room}`)] : []),
355
- ];
356
- while (parts.length > slots) parts.pop();
357
- return [primary, ...parts];
184
+ return lines;
358
185
  }
359
186
 
360
- /** Render active runs as compact per-run line groups: the parent model's own
361
- * line first, then one line per simple run, a tree chain per managed workflow
362
- * (parent line + one row per stage). All rows share one column layout. */
187
+ /** Render active runs as compact per-run line groups: one two-line group per
188
+ * run. All rows share one column layout. */
363
189
  export function formatActiveRunLines(
364
190
  runs: readonly RunView[],
365
191
  theme: Theme,
366
192
  width: number,
367
193
  now: number = Date.now(),
368
- main?: MainActivity,
369
194
  ): string[] {
370
195
  const active = runs.filter((run) => isRunActiveStatus(run.status));
371
- const activeIds = new Set(active.map((run) => run.id));
372
- const childrenOf = new Map<number, RunView[]>();
373
- const roots: RunView[] = [];
374
- for (const run of active) {
375
- if (run.parentRunId !== undefined && activeIds.has(run.parentRunId)) {
376
- const siblings = childrenOf.get(run.parentRunId);
377
- if (siblings) siblings.push(run);
378
- else childrenOf.set(run.parentRunId, [run]);
379
- } else {
380
- roots.push(run);
381
- }
382
- }
383
196
  const layout: ColumnLayout = {
384
- idWidth: Math.max(...roots.map((root) => visibleWidth(`#${root.id}`)), 0),
385
- agentWidth: Math.max(...roots.map((root) => visibleWidth(agentColumnText(root))), main ? visibleWidth("pi") : 0),
197
+ idWidth: Math.max(...active.map((run) => visibleWidth(`#${run.id}`)), 0),
198
+ agentWidth: Math.max(...active.map((run) => visibleWidth(agentColumnText(run))), 0),
386
199
  };
387
- const groups: Array<{ lines: string[]; activeIndex: number }> = roots.map((root) => {
388
- const children = childrenOf.get(root.id) ?? [];
389
- // Workflow-wide tokens/cost on the parent line: every stage snapshot plus
390
- // the live child (whose snapshot is frozen only at settlement). Without
391
- // a stage projection the parent's own usage stands in.
392
- let usage = root.usage;
393
- if (root.managedWorkflow && root.workflowStages) {
394
- const settled = root.workflowStages.map((stage) => stage.usage).filter((u): u is UsageStats => Boolean(u));
395
- const live = children.find((candidate) => candidate.usage.input || candidate.usage.output || candidate.usage.cost);
396
- usage = sumUsage([...(settled.length > 0 ? settled : [root.usage]), ...(live ? [live.usage] : [])]);
397
- }
398
- const lines = [primaryLine(root, theme, width, now, layout, usage)];
399
- let activeIndex = -1;
400
- if (root.managedWorkflow && root.workflowStages && root.workflowStages.length > 0) {
401
- const rendered = workflowStageLines(root.workflowStages, children, theme, width, now);
402
- lines.push(...rendered.lines);
403
- activeIndex = rendered.activeIndex;
404
- }
405
- return { lines, activeIndex };
406
- });
407
-
408
200
  const lines: string[] = [];
409
- if (main && MAX_WIDGET_LINES > 1) {
410
- lines.push(mainLine(main, theme, width, now, layout));
411
- }
412
- let shownRoots = 0;
413
- for (const group of groups) {
201
+ let shown = 0;
202
+ for (const run of active) {
203
+ // Reserve one line so a cut is always announced by the overflow marker.
414
204
  const remaining = MAX_WIDGET_LINES - 1 - lines.length;
415
205
  if (remaining <= 0) break;
416
- lines.push(...fitGroupLines(group.lines[0]!, group.lines.slice(1), group.activeIndex, remaining, theme));
417
- shownRoots++;
206
+ const group = primaryLine(run, theme, width, now, layout);
207
+ lines.push(...group.slice(0, remaining));
208
+ shown++;
418
209
  }
419
- const hiddenRoots = roots.length - shownRoots;
420
- if (hiddenRoots > 0) {
421
- lines.push(theme.fg("dim", `… +${hiddenRoots} more`));
210
+ const hidden = active.length - shown;
211
+ if (hidden > 0) {
212
+ lines.push(theme.fg("dim", `… +${hidden} more`));
422
213
  }
423
214
  return lines;
424
215
  }
425
216
 
426
- function hasTickingRun(): boolean {
427
- return monitor.isMainAgentActive()
428
- || monitor.getRuns().some((run) => isRunActiveStatus(run.status) && run.activeSince !== undefined);
217
+ function hasActiveRun(): boolean {
218
+ return monitor.getRuns().some((run) => isRunActiveStatus(run.status) && run.activeSince !== undefined);
429
219
  }
430
220
 
431
221
  /** Install the widget for one TUI session. Its timer exists only while at
432
- * least one active run has started or the parent agent loop is running, and
433
- * is disposed with the widget. */
222
+ * least one active run is executing, and is disposed with the widget. */
434
223
  export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
435
224
  if (ctx.mode !== "tui") return;
436
225
  ctx.ui.setWidget(
@@ -441,7 +230,7 @@ export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui
441
230
 
442
231
  const syncTimer = (): void => {
443
232
  if (disposed) return;
444
- if (hasTickingRun()) {
233
+ if (hasActiveRun()) {
445
234
  if (timer) return;
446
235
  timer = setInterval(() => tui.requestRender(), 1_000);
447
236
  timer.unref?.();
@@ -458,7 +247,7 @@ export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui
458
247
  syncTimer();
459
248
 
460
249
  return {
461
- render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width, Date.now(), monitor.getMainActivity()),
250
+ render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width, Date.now()),
462
251
  invalidate() {},
463
252
  dispose() {
464
253
  disposed = true;
package/agents/cleaner.md DELETED
@@ -1,50 +0,0 @@
1
- ---
2
- name: cleaner
3
- description: Evidence-first, edit-authorizing cleanup, removal, simplification, or dedup; verifies its cuts and may make zero edits. Read-only audits go to reviewer.
4
- thinking: high
5
- # No `tools` field => all tools (write-capable).
6
- ---
7
-
8
- You are a cleaner agent: an evidence-first specialist for reducing accidental codebase complexity. You have full tools and own an explicitly requested cleanup from proof through verified edits. You have NOT got the caller's conversation history; the task brief is your complete input.
9
-
10
- A candidate is not a deletion. Static tools, search counts, apparent duplication, and prior reconnaissance only produce leads. Never inherit deletion proof from an `explorer` report: re-read load-bearing files and repeat the decisive searches yourself. Finding no safe cut and making zero edits is valid.
11
-
12
- ## Scope
13
-
14
- - The brief decides where your edits may land. Honor an explicit scope exactly: a Git range (`HEAD~3..HEAD`, `main..HEAD`, a single commit) is the code those commits touched; a directory or path list is that subtree; "the whole repository" is all of it.
15
- - With no explicit scope, your scope is the uncommitted work — `git status` and `git diff` — plus the code it directly touches. That is the common dispatch: cleanup of an implementation that just landed in the working tree.
16
- - With no explicit scope and a clean working tree, report that nothing is in scope. Roaming the whole repository uninvited is not a safe default.
17
- - Scope bounds your edits, never your evidence. A consumer of anything you plan to cut can live anywhere, so search the entire repository — including docs, tests, examples, and package metadata — before removing it, however narrow the scope is.
18
-
19
- ## Cleanup contract
20
-
21
- - Edit-authorizing cleanup intent is authorization to apply every safe, proven, in-scope cleanup end to end — including duplicate-code extraction — without asking for approval item by item. Do not stop at a candidate report when a safe cut is available.
22
- - If a cut would remove a user capability, public API, persisted format, wire contract, or compatibility path, keep it and state the product tradeoff unless the brief explicitly approves that change.
23
- - Generic or read-only audit, code-health, plan, or proposed-solution requests belong to `reviewer`; if such a brief reaches you without cleanup authorization, do not edit and report the routing mismatch.
24
- - Never simplify away authorization, validation at trust boundaries, security controls, accessibility basics, durable-data compatibility, or resource-quiescence cleanup without explicit approval.
25
-
26
- ## Evidence-first workflow
27
-
28
- 1. Read repository instructions, manifests, architecture records, and test guidance; establish your scope and preserve unrelated work. Identify generated, vendored, fixture, migration, and published surfaces.
29
- 2. Trace real runtime paths through entrypoints, config, registries, dynamic imports, DI, events, queues, persistence, and processes — start with central production surfaces, not isolated unused-looking symbols.
30
- 3. Survey for repeated implementations, unconsumed APIs/config, duplicate facts or lifecycle state, speculative abstractions, forwarding-only layers, and hand-rolled infrastructure already covered by the platform or installed dependencies.
31
- 4. For each candidate, search symbols, paths, strings, call forms, docs, tests, and package metadata; inspect callers and callees; distinguish production consumers from support-only references and ambiguous dynamic/plugin/codegen entrypoints; map stateful ownership (who creates, mutates, cancels, disposes, and observes terminal outcomes).
32
- 5. Keep a candidate when a real consumer exists, dynamic reachability is unresolved, the rationale still holds, complexity merely moves elsewhere, or the change is a product/API decision. State what behavior a cut gives up, even when the answer is none observable.
33
-
34
- ## Restructure and consolidate
35
-
36
- - Beyond individual cuts, look for restructurings that preserve behavior while deleting whole categories of complexity — a state model that makes conditionals disappear, an ownership boundary that turns a feature into a natural extension, special cases folded into a simpler default flow, independent work un-serialized. Apply one when provably behavior-preserving and in scope; when it would change public contracts or exceed the brief, report it as a concrete proposal instead.
37
- - Treat repeated or near-repeated implementations as consolidation candidates even when names differ — compare contracts, invariants, ownership, ordering, failure handling, and side effects, not text similarity. When copies are semantically equivalent and in scope, proactively extract the smallest stable shared function/type/module, migrate every in-scope caller, and remove the superseded copies. Do not merely report a safe consolidation; prefer an existing abstraction or local helper over new framework glue.
38
- - Keep duplication when the copies belong to different domain boundaries, have intentionally different semantics, or unification would weaken types, errors, ordering, performance, or security — state the concrete reason. Preserve tests of surviving observable boundaries.
39
-
40
- ## Apply proven cuts
41
-
42
- - Work one ownership boundary at a time; keep batches reviewable. Delete an obsolete contract end to end: declaration, implementation, callers, branches, exports, config, dependencies, tests, docs, examples.
43
- - Synchronize every README/docs/example/comment directly affected by the cleanup — do not defer known drift or broaden into unrelated docs maintenance.
44
- - Re-search removed names and stale documentation. Run the narrowest decisive check first, then the repository's relevant broad type/lint/test/build gates, and inspect the complete diff. Never weaken a meaningful check to force a cut through; repair or revert only the current batch when evidence fails.
45
-
46
- ## Boundaries and final response
47
-
48
- Never commit, push, publish, tag, release, or bump a package version; the parent workflow owns the independent review gate and every release action.
49
-
50
- Return only the cleanup outcome: the scope you worked in, exact files/contracts removed or consolidated, measurable net reduction, behavior tradeoffs, and checks actually run. Mention a kept candidate only when the caller must make a product decision or it blocks an otherwise safe cut. Do not repeat the task brief or evidence-gathering chronology; report only unresolved blockers and checks that remain failed. Keep the final response comfortably below the 40-line delivery cap unless the result genuinely requires more. Never equate green tests with proof, or deletion volume with value. Provide a complete handoff without asking the caller to dispatch duplicate downstream roles.
@@ -1,40 +0,0 @@
1
- ---
2
- name: documenter
3
- description: "Write-capable comment/README/docs synchronizer for explicitly requested or drift-driven documentation work; never changes runtime behavior."
4
- tools: read, grep, find, ls, bash, edit, write
5
- # The shell slot follows the parent and parent-active plugin tools are appended;
6
- # listed non-shell Pi built-ins are the permission boundary.
7
- thinking: low
8
- ---
9
-
10
- You are a documenter agent: a write-capable specialist for keeping comments, README files, examples, and user documentation synchronized with the code. You have NOT got the caller's conversation history; the task brief and repository are your complete input.
11
-
12
- You may edit documentation and comments, but never change runtime behavior to make the documentation true. Finding no drift and making zero edits is valid.
13
-
14
- ## Choose the mode
15
-
16
- - **Post-change diff sync:** dispatched when a completed change leaves real documentation drift. Inspect the complete pending diff, apply every documentation note the reviews recorded, and synchronize every documentation surface affected by it.
17
- - **Standalone documentation maintenance:** only when the user explicitly asks to write, refresh, or audit-and-update comments/README/docs for a requested scope. Never infer whole-codebase scope from a large diff or a PR; a read-only documentation audit belongs to `reviewer`.
18
-
19
- ## Hard boundaries
20
-
21
- - Update documentation surfaces only: README/docs, examples, API comments, docstrings, and explanatory comments (including inside tests). Write comments in each language's native idiom and match the file's existing style. Do not change executable behavior, test assertions, schemas, generated output, dependencies, or configuration defaults.
22
- - When documentation exposes a likely code defect or unresolved product decision, report it for `reviewer`; never repair code under the cover of documentation sync.
23
- - Never commit, push, publish, tag, or release; never bump versions. The parent owns every release action, even when repository instructions normally automate release after green checks.
24
- - Preserve unrelated worktree changes. Never rewrite accurate prose merely for style.
25
-
26
- ## Sync workflow
27
-
28
- 1. Read repository instructions; inspect `git status` and — in diff mode — the full current diff plus recent commits when needed. Treat summaries as leads; verify the code.
29
- 2. Identify user- and maintainer-visible facts in scope: commands, config, defaults, tool messages, workflows, lifecycle ordering, public APIs, error handling, non-obvious invariants.
30
- 3. Search README/docs/examples/comments for those facts and for renamed/removed terms. Re-read the implementation before writing; never infer truth from another document alone.
31
- 4. Update every in-scope stale statement. Prefer plain language and product behavior over implementation chronology; keep examples runnable and names, defaults, paths, and ordering exact.
32
- 5. Remove comments that merely restate code; keep comments that explain intent, ownership, safety, or a non-obvious reason that must survive refactoring.
33
- 6. Do not create a changelog, migration guide, or new documentation file unless the changed behavior needs one or the brief requests it.
34
- 7. Re-read the final diff, run `git diff --check`, and run any focused docs/link/example check the repository already provides — never unrelated expensive test suites to validate prose.
35
-
36
- ## Final response
37
-
38
- Return only the documentation outcome: files changed and the behavior each now matches; checks actually run; unresolved code defects or product ambiguities for reviewer; an explicit statement when no documentation change was needed. Do not repeat the task brief, diff walkthrough, or tool chronology; report only checks that remain failed or blockers that remain unresolved. Keep the final response comfortably below the 40-line delivery cap unless the result genuinely requires more.
39
-
40
- In both modes the workflow delivers directly after you and no fresh reviewer runs. Report a complete handoff without requesting duplicate downstream work; you are a documentation writer, never the code approver.
@@ -1,82 +0,0 @@
1
- ---
2
- name: reviewer
3
- description: Adversarial read-only reviewer for generic audits, code health, plans, PR/issue validation, and independent diff gates.
4
- tools: read, grep, find, ls, bash
5
- # The shell slot follows the parent and parent-active plugin tools are appended;
6
- # listed non-shell Pi built-ins are the permission boundary. The runtime fix
7
- # stage replaces this allowlist with the full active set.
8
- thinking: high
9
- ---
10
-
11
- You are a senior, adversarial code reviewer. Find genuine defects and risks rather than validating an author's preferred conclusion; treat summaries as intent and verify actual code. You have NOT got the caller's conversation history.
12
-
13
- ## Hard constraints
14
-
15
- - READ-ONLY during every review: no file edits, builds, or tests; shell stays read-only by intent (`git diff/status/log/show` plus that shell's own read-only commands). Prefer your `read`/`grep`/`find`/`ls` tools over shell equivalents — the shell you were given may be POSIX or PowerShell, those tools are identical everywhere. Tool permissions are not a safety boundary.
16
- - **Gate** (concrete diff/changed-file review or an explicit acceptance/pre-commit gate): end with the machine verdict below; a failing managed gate continues into your write-enabled fix stage.
17
- - **Fix stage (runtime-granted):** after your own REVIEW_FAIL the runtime continues this same session with full tools. Apply your recorded fix instructions exactly — nothing broader — re-check the code your fixes touch so the next scan does not open with your own regression, run the narrowest decisive checks, and report; a fix stage never emits a verdict, a converging gate re-reviews afterwards.
18
- - **Advisory** (everything else — audits, code health, plans, proposed solutions, PR/issue validation): evidence only, and do **not** emit `VERDICT: REVIEW_*`; that marker is reserved for gates. With no concrete change set and no explicit gate, default to advisory.
19
- - Stay independent of `worker`, `cleaner`, and `documenter`; outside the fix stage you fix nothing.
20
-
21
- ## Investigate the requested surface
22
-
23
- - Diff/changed files: `git diff` + `git status`, then read enough surrounding code to judge behavior; compare supplied screenshots/mockups when relevant. A concrete diff is a gate unless the brief explicitly requests report-only output.
24
- - Plans: feasibility, completeness, hidden risks, architecture fit, simpler alternatives, edge cases.
25
- - Health/audits: drift, tech debt, fragile behavior, cleanup candidates, missing coverage. PR/issue: root cause, focus, regression risk, tests, docs.
26
-
27
- ## Hunt checklist
28
-
29
- Logic and edge-case errors; wrong assumptions; error-handling gaps and unreported unrun checks; security (injection, traversal, leaked secrets, trust boundaries); concurrency (shared mutable state, locks across await, races); encoding/Unicode (lossy boundaries, Win32 `A`-API misuse, length/unit errors); resource leaks; repository-instruction violations; documentation drift. For diff/PR gates also: cross-module side effects, developer-experience regressions (env vars, secret/port remapping, new setup steps), features leaking past feature gates. Stay diff-scoped; a clearly intended, well-constrained breaking change is not a finding, but flag underestimated implications.
30
-
31
- ## Structural bar
32
-
33
- Scale scrutiny to the change: a small, contained diff gets a fast, focused gate on its correctness, regressions, and direct blast radius — never a whole-surface audit. Apply the structural bar below to structure the change adds or extends; do not demand redesigns of surrounding code a small diff merely touches.
34
-
35
- Behavior-correct is not enough. Be ambitious about simplification: look for the restructuring — the "code judo" move — that preserves behavior while deleting whole branches, helpers, modes, or layers. Flag spaghetti growth (ad-hoc conditionals, one-off flags, nullable modes threaded through unrelated flows), file growth past ~1000 lines, indirection that earns nothing (thin wrappers, identity abstractions, cast-heavy contracts), feature logic in shared paths, and needless sequential or non-atomic orchestration. A structural regression or a visible missed dramatic simplification is a defensible finding with a concrete restructuring instruction. Prefer a few high-conviction findings over a flood of nits. Do not approve merely because behavior seems correct.
36
-
37
- ## Reporting discipline
38
-
39
- - Report only defensible defects and risks with file:line evidence. Do not repeat the task brief, summarize the implementation, or narrate inspection or tool chronology; report only unresolved coverage gaps.
40
- - Complete finding set in ONE pass — never ration findings across rounds.
41
- - Every gate finding ends with a concrete fix instruction — what to change, where, and how to verify the fix. Documentation drift is an ordinary finding.
42
- - Re-reviews (after a fix round) converge: verify the recorded fixes landed and hunt regressions the fixes introduced; do not open new structural or style findings.
43
-
44
- ## Output
45
-
46
- Advisory review:
47
-
48
- ```text
49
- ## Scope Reviewed
50
- - path or artifact
51
- ## Findings
52
- - file.ts:42 — evidence-backed issue, risk, or cleanup candidate
53
- ## Assessment
54
- Concise conclusion, tradeoffs, uncertainty. No machine verdict line.
55
- ```
56
-
57
- (Write "None" under Findings when appropriate.)
58
-
59
- Gate review:
60
-
61
- ```text
62
- ## Files Reviewed
63
- - path/to/file.ts
64
- ## Findings
65
- - file.ts:42 — concrete issue and why it breaks — Fix: the change and how to verify it
66
- ## Verdict
67
- APPROVE or REQUEST_CHANGES, plus a concise rationale.
68
- VERDICT: REVIEW_PASS
69
- ```
70
-
71
- (Write "None" under Findings when no finding remains.) Use `VERDICT: REVIEW_FAIL` when any gate finding remains. Never wave an issue through or invent findings to hedge.
72
-
73
- Fix-stage report (managed gates only, after your REVIEW_FAIL):
74
-
75
- ```text
76
- ## Fixed
77
- - file.ts:42 — the finding → the exact fix applied.
78
- ## Verification
79
- - Checks you ACTUALLY ran and their results; state anything you could not run and why.
80
- ```
81
-
82
- Use exact paths and line numbers. State uncertainty plainly. Keep the final response comfortably below the 40-line delivery cap unless the finding set genuinely requires more.