@ferris1225/pi-subagents 4.1.4 → 4.1.5

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Stable logical-thread generation lifecycle for background sub-agents.
3
3
  *
4
- * Dispatch owns workflow policy and internal role briefs; this module owns one
4
+ * Dispatch owns workflow policy, the live stage projection, and internal role
5
+ * briefs; this module owns one
5
6
  * stable parent generation end to end: managed-repository lane use,
6
7
  * worktree setup/finalization after downstream review, queue/process ownership,
7
8
  * retained-session resume/fork, and guarded one-time terminal publication.
@@ -88,6 +89,16 @@ export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
88
89
  return isWriteCapableAgent(agent);
89
90
  }
90
91
 
92
+ /** A direct reviewer otherwise cannot infer enabled-role availability from its
93
+ * isolated task. Managed internal gates receive the same contract in their
94
+ * generated briefs. Advisory reviews still emit neither machine marker. */
95
+ function withEnabledDocumenterReviewContract(agent: AgentConfig): AgentConfig {
96
+ return {
97
+ ...agent,
98
+ systemPrompt: `${agent.systemPrompt.trimEnd()}\n\nRuntime workflow context: documenter is enabled. In gate reviews, documentation drift is non-gating: emit DOCUMENTATION: NEEDED with ## Documentation notes, or DOCUMENTATION: CLEAN when no sync is needed. Advisory reviews still emit neither VERDICT nor DOCUMENTATION markers.`.trim(),
99
+ };
100
+ }
101
+
91
102
  interface DispatchEnvironment {
92
103
  ctx: ExtensionContext;
93
104
  config: SubagentsConfig;
@@ -249,7 +260,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
249
260
  if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
250
261
  const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
251
262
  resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
252
- const agent = resolveLiveAgentTools(discoveredAgent);
263
+ const resolvedAgent = resolveLiveAgentTools(discoveredAgent);
264
+ const agent = agentName === "reviewer" && runAgents.some((candidate) => candidate.name === "documenter")
265
+ ? withEnabledDocumenterReviewContract(resolvedAgent)
266
+ : resolvedAgent;
253
267
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
254
268
  return {
255
269
  ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
@@ -1052,8 +1066,8 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
1052
1066
  !thread.retired;
1053
1067
  try {
1054
1068
  // For isolated writers this is deliberately after the managed reviewer
1055
- // and documentation stages: every child sees the same worktree, then one
1056
- // lifecycle owner integrates the complete writer+fixes+docs state exactly once.
1069
+ // and any needed documentation stage: every child sees the same worktree,
1070
+ // then one lifecycle owner integrates the complete settled state exactly once.
1057
1071
  await thread.finalizeIsolation(generation, result);
1058
1072
  if (!ownsSettlement()) return;
1059
1073
  if (workflowOutcome && isolation === "worktree") {
package/src/widget.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  monitor,
11
11
  statusIcon,
12
12
  type RunView,
13
+ type WorkflowStage,
13
14
  } from "./monitor.ts";
14
15
 
15
16
  export const SUBAGENTS_WIDGET_ID = "pi-subagents";
@@ -50,7 +51,9 @@ function runPrimaryLine(
50
51
  prefix: string,
51
52
  ): string {
52
53
  const dim = (text: string): string => theme.fg("dim", text);
53
- const icon = statusIcon(run.status, theme);
54
+ const icon = run.managedWorkflow && run.status === "running"
55
+ ? theme.fg("accent", theme.bold("◆"))
56
+ : statusIcon(run.status, theme);
54
57
  const displayName = run.managedWorkflow ? `${run.agent} workflow` : run.agent;
55
58
  const name = theme.fg("accent", theme.bold(displayName));
56
59
  const identity = `${prefix}${icon} ${name}`;
@@ -72,7 +75,9 @@ function runPrimaryLine(
72
75
  : undefined;
73
76
  const taskSource = run.parentRunId !== undefined
74
77
  ? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
75
- : formatTaskSummary(run.task, 64);
78
+ : run.managedWorkflow
79
+ ? run.label ?? formatTaskSummary(run.task, 64)
80
+ : formatTaskSummary(run.task, 64);
76
81
  const taskDesiredSource = [continuation, taskSource]
77
82
  .filter((part): part is string => Boolean(part))
78
83
  .join(" · ");
@@ -129,6 +134,46 @@ function runPrimaryLine(
129
134
  return compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
130
135
  }
131
136
 
137
+ function workflowStageToken(stage: WorkflowStage, theme: Theme): string {
138
+ const content = (icon: string): string => `${icon} ${stage.relation}`;
139
+ switch (stage.status) {
140
+ case "done":
141
+ return theme.fg("success", content("✓"));
142
+ case "active":
143
+ return theme.fg("accent", theme.bold(content("●")));
144
+ case "changes":
145
+ return theme.fg("warning", content("!"));
146
+ case "failed":
147
+ return theme.fg("error", content("✗"));
148
+ default:
149
+ return theme.fg("dim", content("○"));
150
+ }
151
+ }
152
+
153
+ /** Render the real/currently planned stage sequence. On narrow terminals the
154
+ * active (or next actionable) stage becomes the left edge so it survives before
155
+ * older history; the workflow-wide elapsed tail is independently preserved on
156
+ * the primary line. */
157
+ function workflowTimelineLine(run: RunView, theme: Theme, width: number): string | undefined {
158
+ const stages = run.workflowStages;
159
+ const indent = " ";
160
+ if (!stages || stages.length === 0 || width <= visibleWidth(indent)) return undefined;
161
+ const separator = theme.fg("dim", " ─ ");
162
+ const render = (items: readonly WorkflowStage[]): string =>
163
+ items.map((stage) => workflowStageToken(stage, theme)).join(separator);
164
+ const full = `${indent}${render(stages)}`;
165
+ if (visibleWidth(full) <= width) return full;
166
+
167
+ let focusIndex = stages.findIndex((stage) => stage.status === "active");
168
+ if (focusIndex === -1) {
169
+ focusIndex = stages.findLastIndex((stage) => stage.status === "changes" || stage.status === "failed");
170
+ }
171
+ if (focusIndex === -1) focusIndex = stages.findIndex((stage) => stage.status === "pending");
172
+ if (focusIndex === -1) focusIndex = stages.length - 1;
173
+ const omittedPrefix = focusIndex > 0 ? theme.fg("dim", "… ─ ") : "";
174
+ return truncateToWidth(`${indent}${omittedPrefix}${render(stages.slice(focusIndex))}`, width, "…");
175
+ }
176
+
132
177
  function runActivityLine(run: RunView, theme: Theme, width: number, indent: string): string[] {
133
178
  const dim = (text: string): string => theme.fg("dim", text);
134
179
  const activity = run.activity?.trim();
@@ -140,9 +185,10 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
140
185
  return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
141
186
  }
142
187
 
143
- /** Render active runs as a tree: main-agent dispatches are roots and managed
144
- * documenter/reviewer/fix steps nest under the stable parent row. Fork labels
145
- * include their source id; other control ids remain available through status. */
188
+ /** Render active runs as compact workflow-aware trees. Stable managed parents
189
+ * retain their stage timeline while the current internal child supplies exact
190
+ * model/thinking/activity telemetry. Fork labels include their source id; other
191
+ * control ids remain available through status. */
146
192
  export function formatActiveRunLines(
147
193
  runs: readonly RunView[],
148
194
  theme: Theme,
@@ -166,9 +212,14 @@ export function formatActiveRunLines(
166
212
  for (const root of roots) {
167
213
  const children = childrenOf.get(root.id) ?? [];
168
214
  lines.push(runPrimaryLine(root, theme, width, now, ""));
169
- // The parent's managed-workflow placeholder is redundant while a child
170
- // row shows live progress; keep it only between stages.
171
- if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
215
+ const hasTimeline = Boolean(root.managedWorkflow && root.workflowStages?.length);
216
+ const timeline = hasTimeline ? workflowTimelineLine(root, theme, width) : undefined;
217
+ if (timeline) lines.push(timeline);
218
+ // A parent placeholder ("managed workflow running") would duplicate the
219
+ // timeline. Standalone roots keep their useful activity line as before.
220
+ if (children.length === 0 && !hasTimeline) {
221
+ lines.push(...runActivityLine(root, theme, width, " "));
222
+ }
172
223
  children.forEach((child, index) => {
173
224
  const connector = index === children.length - 1 ? "└ " : "├ ";
174
225
  lines.push(runPrimaryLine(child, theme, width, now, theme.fg("dim", ` ${connector}`)));