pi-better-harness 0.29.1 → 0.30.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 +2 -2
- package/extensions/minimal-output/hook.ts +3 -1
- package/extensions/minimal-output/tool-identity.ts +12 -8
- package/node_modules/pi-better-plan/README.md +1 -1
- package/node_modules/pi-better-plan/package.json +1 -1
- package/node_modules/pi-better-plan/src/cancelled-rows.ts +36 -0
- package/node_modules/pi-better-plan/src/index.ts +59 -17
- package/node_modules/pi-better-plan/src/plan-presentation.ts +13 -4
- package/node_modules/pi-better-plan/src/workflow-plan.ts +14 -6
- package/node_modules/pi-better-subagents/README.md +15 -2
- package/node_modules/pi-better-subagents/config.ts +33 -6
- package/node_modules/pi-better-subagents/docs/usage.md +1 -0
- package/node_modules/pi-better-subagents/effective-settings.ts +63 -0
- package/node_modules/pi-better-subagents/index.ts +38 -13
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -69,8 +69,8 @@ The bundled harness provides **Tool output: Normal / Minimal** in
|
|
|
69
69
|
`/tool-output` (toggle) remain compatibility shortcuts to the same preference.
|
|
70
70
|
Minimal mode folds each tool call into a single-line header, including running
|
|
71
71
|
calls. The tool-specific icon and tool name share a state color: accent while
|
|
72
|
-
running, muted when completed, and error-colored when failed. In fullscreen Pi, a soft highlight travels through visible running
|
|
73
|
-
command/path
|
|
72
|
+
running, muted when completed, and error-colored when failed. In fullscreen Pi, a soft highlight travels through the entire visible running tool name and its
|
|
73
|
+
inline command/path details; details stay dim between highlight passes. Scrollback terminals keep static state colors.
|
|
74
74
|
Choose **Tool animation: Off** in `/harness-settings` for static running headers.
|
|
75
75
|
Running and failed calls retain text labels so state never depends on color or
|
|
76
76
|
motion alone. Tool rows are indented two columns past
|
|
@@ -174,8 +174,10 @@ function compactCall(component: ToolComponent, width: number, theme?: Theme, ind
|
|
|
174
174
|
const suffix = failed ? " (failed)" : running ? " (running)" : "";
|
|
175
175
|
const icon = TOOL_ICONS[component.toolName] ?? "\u25c7";
|
|
176
176
|
const title = ` ${icon} ${label}`;
|
|
177
|
+
const visibleDetail = animationTime === undefined ? detail : truncateToWidth(detail,
|
|
178
|
+
Math.max(0, width - Math.min(indent, Math.max(0, width - 1)) - visibleWidth(title) - 2));
|
|
177
179
|
const styled = theme
|
|
178
|
-
? ` ${toolIdentity(icon, label, failed ? "error" : running ? "accent" : "muted", theme, animationTime
|
|
180
|
+
? ` ${toolIdentity(icon, label, failed ? "error" : running ? "accent" : "muted", theme, animationTime, visibleDetail)}${theme.fg(failed ? "error" : "dim", suffix)}`
|
|
179
181
|
: `${title}${detail ? ` ${detail}` : ""}${suffix}`;
|
|
180
182
|
return [truncateToWidth(`${" ".repeat(Math.min(indent, Math.max(0, width - 1)))}${styled}`, width)];
|
|
181
183
|
}
|
|
@@ -6,7 +6,7 @@ const concreteColors = (theme: Theme) => (theme as Theme & { colors?: Record<str
|
|
|
6
6
|
const PERIOD = 2400;
|
|
7
7
|
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
8
8
|
|
|
9
|
-
function themeRgb(theme: Theme, token: "accent" | "text"): RGB | undefined {
|
|
9
|
+
function themeRgb(theme: Theme, token: "accent" | "text" | "dim"): RGB | undefined {
|
|
10
10
|
const color = concreteColors(theme)?.[token];
|
|
11
11
|
if (color?.kind === "rgb" && color.r !== undefined && color.g !== undefined && color.b !== undefined) {
|
|
12
12
|
return [color.r, color.g, color.b];
|
|
@@ -43,23 +43,27 @@ function foreground(color: RGB, mode: "truecolor" | "256color"): string {
|
|
|
43
43
|
return `\x1b[38;5;${index}m`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function toolIdentity(icon: string, label: string, tone: "accent" | "muted" | "error", theme: Theme, time?: number): string {
|
|
47
|
-
|
|
46
|
+
export function toolIdentity(icon: string, label: string, tone: "accent" | "muted" | "error", theme: Theme, time?: number, detail = ""): string {
|
|
47
|
+
const staticHeader = () => theme.fg(tone, `${icon} ${label}`) + (detail ? ` ${theme.fg("dim", detail)}` : "");
|
|
48
|
+
if (tone !== "accent" || time === undefined || !supportsToolShimmer(theme)) return staticHeader();
|
|
48
49
|
const base = themeRgb(theme, "accent");
|
|
49
50
|
const light = themeRgb(theme, "text");
|
|
50
|
-
|
|
51
|
-
const
|
|
51
|
+
const letters = Array.from(graphemes.segment(label + (detail ? ` ${detail}` : "")), part => part.segment);
|
|
52
|
+
const nameLength = Array.from(graphemes.segment(label)).length;
|
|
52
53
|
const radius = Math.max(2, Math.min(4, letters.length / 3));
|
|
53
54
|
const position = (time % PERIOD) / PERIOD * (letters.length + radius * 2) - radius;
|
|
54
55
|
const mode = theme.getColorMode();
|
|
56
|
+
const detailBase = detail ? themeRgb(theme, "dim") : undefined;
|
|
55
57
|
const name = letters.map((letter, index) => {
|
|
58
|
+
const token = index < nameLength ? "accent" : "dim";
|
|
59
|
+
const letterBase = token === "accent" ? base : detailBase;
|
|
56
60
|
const distance = Math.abs(index - position) / radius;
|
|
57
61
|
const shine = distance < 1 ? (1 + Math.cos(distance * Math.PI)) * 0.48 : 0;
|
|
58
|
-
if (!base || !light) {
|
|
62
|
+
if (!base || !light || !letterBase) {
|
|
59
63
|
const intensity = shine > 0.72 ? "\x1b[1m" : shine > 0.18 ? "" : "\x1b[2m";
|
|
60
|
-
return
|
|
64
|
+
return `${theme.getFgAnsi(token)}\x1b[22m${intensity}${letter}`;
|
|
61
65
|
}
|
|
62
|
-
const color =
|
|
66
|
+
const color = letterBase.map((value, channel) => Math.round(value + (light[channel] - value) * shine)) as RGB;
|
|
63
67
|
return `${foreground(color, mode)}${letter}`;
|
|
64
68
|
}).join("");
|
|
65
69
|
return theme.fg(tone, `${icon} \x1b[22m${name}\x1b[22m`);
|
|
@@ -16,7 +16,7 @@ By default, you see up to five steps around the current work. The view starts at
|
|
|
16
16
|
|
|
17
17
|
Click the plan to expand or collapse it in Pi's fullscreen mode. In regular terminal mode, use `/plan expand` or `/plan collapse`. For the full plan with details, run `/plan`.
|
|
18
18
|
|
|
19
|
-
Rows show `✓` for done, `●` for active, `○` for pending, and `!` for blocked. Pi marks steps complete explicitly; the count is not an estimate of effort.
|
|
19
|
+
Rows show `✓` for done, `●` for active, `○` for pending, and `!` for blocked. Pi marks steps complete explicitly; the count is not an estimate of effort. The completion count is completed items / total eligible items: canceled workflow items (`cancelled` or `canceled`) are excluded from the total, while skipped and not-applicable items remain in it. Canceled items remain visible for 30 seconds after the cancellation is first observed, then disappear from the widget and plan lists without deleting their workflow records. This delay survives session reloads; reactivating an item makes it visible again. An all-canceled plan shows `0/0` complete.
|
|
20
20
|
|
|
21
21
|
Your plan and display settings survive reload and follow the session branch. A completed checklist clears after 30 seconds. A released workflow plan stays visible for reference, but cannot be edited until the workflow resumes.
|
|
22
22
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CANCELLED_ROW_RETENTION_MS, isRecord, type RushPlan, type WorkflowPlanBinding } from "./workflow-plan.js";
|
|
2
|
+
|
|
3
|
+
export const CANCELLED_ROWS_ENTRY = "pi-better-plan-cancelled-rows";
|
|
4
|
+
|
|
5
|
+
export function observeCancelledRows(
|
|
6
|
+
plan: RushPlan,
|
|
7
|
+
binding: WorkflowPlanBinding,
|
|
8
|
+
entries: Iterable<{ type: string; customType?: string; data?: unknown }>,
|
|
9
|
+
append: (type: string, data: unknown) => void,
|
|
10
|
+
now = Date.now(),
|
|
11
|
+
): void {
|
|
12
|
+
// Keep display deadlines in branch history, not in the workflow-owned task plan.
|
|
13
|
+
let previous: Record<string, unknown> = {};
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const data = entry.data;
|
|
16
|
+
if (entry.type === "custom" && entry.customType === CANCELLED_ROWS_ENTRY && isRecord(data) &&
|
|
17
|
+
data.version === 1 && data.path === binding.path && data.runId === binding.runId &&
|
|
18
|
+
data.owner === binding.owner && isRecord(data.rows)) previous = data.rows;
|
|
19
|
+
}
|
|
20
|
+
const rows: Record<string, number> = Object.create(null);
|
|
21
|
+
for (const unit of plan.issues) {
|
|
22
|
+
if (unit.status !== "cancelled" && unit.status !== "canceled") continue;
|
|
23
|
+
const saved = previous[unit.id];
|
|
24
|
+
unit.cancelledAt = typeof saved === "number" && Number.isFinite(saved) && saved >= 0 ? saved : now;
|
|
25
|
+
rows[unit.id] = unit.cancelledAt;
|
|
26
|
+
}
|
|
27
|
+
if (JSON.stringify(rows) !== JSON.stringify(previous)) {
|
|
28
|
+
append(CANCELLED_ROWS_ENTRY, { version: 1, ...binding, rows });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function cancelledRowsClearDelay(plan: RushPlan, now = Date.now()): number | null {
|
|
33
|
+
const deadlines = plan.issues.flatMap(unit => unit.cancelledAt === undefined ? [] : [unit.cancelledAt + CANCELLED_ROW_RETENTION_MS])
|
|
34
|
+
.filter(deadline => deadline > now);
|
|
35
|
+
return deadlines.length ? Math.min(...deadlines) - now : null;
|
|
36
|
+
}
|
|
@@ -18,8 +18,9 @@ import { createPlanWidget } from "./plan-widget.js";
|
|
|
18
18
|
import { COMPACT_PLAN_ROWS } from "./plan-presentation.js";
|
|
19
19
|
import {
|
|
20
20
|
createRushPlanComponent, readRushPlan, renderRushPlan, workflowBinding,
|
|
21
|
-
WORKFLOW_PLAN_ENTRY, type RushPlan, type WorkflowPlanBinding,
|
|
21
|
+
WORKFLOW_PLAN_ENTRY, visibleRushUnits, type RushPlan, type WorkflowPlanBinding,
|
|
22
22
|
} from "./workflow-plan.js";
|
|
23
|
+
import { cancelledRowsClearDelay, observeCancelledRows } from "./cancelled-rows.js";
|
|
23
24
|
import { applyRushPlanUpdate, type RushPlanUpdate } from "./workflow-plan-update.js";
|
|
24
25
|
import {
|
|
25
26
|
EXTENSION_NAME,
|
|
@@ -131,18 +132,45 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
131
132
|
let rushError: string | null = null;
|
|
132
133
|
let refreshWidget: ((force?: boolean) => void) | undefined;
|
|
133
134
|
let completedPlanClearTimer: ReturnType<typeof setTimeout> | undefined;
|
|
135
|
+
let cancelledRowsTimer: ReturnType<typeof setTimeout> | undefined;
|
|
136
|
+
let refreshFullPlan: (() => void) | undefined;
|
|
137
|
+
const cancelCancelledRowsTimer = (): void => {
|
|
138
|
+
if (cancelledRowsTimer !== undefined) clearTimeout(cancelledRowsTimer);
|
|
139
|
+
cancelledRowsTimer = undefined;
|
|
140
|
+
};
|
|
141
|
+
const scheduleCancelledRowsClear = (): void => {
|
|
142
|
+
cancelCancelledRowsTimer();
|
|
143
|
+
if (!rushPlan) return;
|
|
144
|
+
const delay = cancelledRowsClearDelay(rushPlan);
|
|
145
|
+
if (delay === null) return;
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
if (cancelledRowsTimer !== timer) return;
|
|
148
|
+
cancelledRowsTimer = undefined;
|
|
149
|
+
refresh(true);
|
|
150
|
+
scheduleCancelledRowsClear();
|
|
151
|
+
}, delay);
|
|
152
|
+
cancelledRowsTimer = timer;
|
|
153
|
+
timer.unref?.();
|
|
154
|
+
};
|
|
155
|
+
const observeCancellations = (plan: RushPlan, binding: WorkflowPlanBinding, ctx: ExtensionContext): void => {
|
|
156
|
+
observeCancelledRows(plan, binding, ctx.sessionManager.getBranch(), (type, data) => pi.appendEntry(type, data));
|
|
157
|
+
};
|
|
134
158
|
const unsubscribeWorkflow = pi.events.on("pi-better-workflow:changed", (owner) => {
|
|
135
159
|
displayedWorkflowOwner = owner && typeof owner === "object" && "name" in owner && typeof owner.name === "string"
|
|
136
160
|
? owner.name : null;
|
|
137
161
|
if (displayedWorkflowOwner) {
|
|
162
|
+
cancelCancelledRowsTimer();
|
|
138
163
|
rushBinding = null;
|
|
139
164
|
rushPlan = null;
|
|
140
165
|
rushError = null;
|
|
141
166
|
}
|
|
142
|
-
|
|
167
|
+
refresh(true);
|
|
143
168
|
});
|
|
144
169
|
|
|
145
|
-
const refresh = (force = false): void =>
|
|
170
|
+
const refresh = (force = false): void => {
|
|
171
|
+
refreshWidget?.(force);
|
|
172
|
+
refreshFullPlan?.();
|
|
173
|
+
};
|
|
146
174
|
|
|
147
175
|
const loadRushPlan = (ctx: ExtensionContext): RushPlan | null => {
|
|
148
176
|
const owner = workflowPlanOwner(ctx);
|
|
@@ -152,33 +180,39 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
152
180
|
if (!rushBinding) {
|
|
153
181
|
rushPlan = null;
|
|
154
182
|
rushError = null;
|
|
183
|
+
cancelCancelledRowsTimer();
|
|
155
184
|
return null;
|
|
156
185
|
}
|
|
157
186
|
try {
|
|
158
187
|
const plan = readRushPlan(rushBinding.path, ctx.cwd);
|
|
159
188
|
if (plan.runId !== rushBinding.runId) throw new Error("Rush run identity changed.");
|
|
189
|
+
observeCancellations(plan, rushBinding, ctx);
|
|
160
190
|
rushPlan = plan;
|
|
161
191
|
rushError = null;
|
|
162
192
|
} catch (error) {
|
|
163
193
|
rushPlan = null;
|
|
164
194
|
rushError = error instanceof Error ? error.message : String(error);
|
|
165
195
|
}
|
|
196
|
+
scheduleCancelledRowsClear();
|
|
166
197
|
refresh(true);
|
|
167
198
|
return rushPlan;
|
|
168
199
|
};
|
|
169
200
|
|
|
170
|
-
const displayWorkflowPlan = (plan: RushPlan, binding: WorkflowPlanBinding, readOnly = false): void => {
|
|
201
|
+
const displayWorkflowPlan = (plan: RushPlan, binding: WorkflowPlanBinding, ctx: ExtensionContext, readOnly = false): void => {
|
|
171
202
|
if (!rushBinding || rushBinding.owner !== binding.owner || rushBinding.path !== binding.path || rushBinding.runId !== binding.runId) {
|
|
172
203
|
pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "set", ...binding });
|
|
173
204
|
}
|
|
174
205
|
displayedWorkflowOwner = readOnly ? null : binding.owner;
|
|
175
206
|
rushBinding = binding;
|
|
207
|
+
observeCancellations(plan, binding, ctx);
|
|
176
208
|
rushPlan = plan;
|
|
177
209
|
rushError = null;
|
|
210
|
+
scheduleCancelledRowsClear();
|
|
178
211
|
refresh(true);
|
|
179
212
|
};
|
|
180
213
|
|
|
181
214
|
const clearWorkflowPlan = (): void => {
|
|
215
|
+
cancelCancelledRowsTimer();
|
|
182
216
|
if (rushBinding) pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "clear" });
|
|
183
217
|
rushBinding = null;
|
|
184
218
|
rushPlan = null;
|
|
@@ -215,6 +249,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
215
249
|
|
|
216
250
|
const restore = (ctx: ExtensionContext): void => {
|
|
217
251
|
cancelCompletedPlanClear();
|
|
252
|
+
cancelCancelledRowsTimer();
|
|
218
253
|
const state = reconstructPlanState(ctx.sessionManager.getBranch());
|
|
219
254
|
currentPlan = state.plan;
|
|
220
255
|
displayedWorkflowOwner = workflowPlanOwner(ctx);
|
|
@@ -271,7 +306,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
271
306
|
if (!currentPlan) return [];
|
|
272
307
|
return renderCompactPlan(currentPlan, width, theme as never, expanded);
|
|
273
308
|
},
|
|
274
|
-
() => (rushBinding ? rushPlan
|
|
309
|
+
() => (rushBinding && rushPlan ? visibleRushUnits(rushPlan).length : currentPlan?.steps.length ?? 0) > COMPACT_PLAN_ROWS,
|
|
275
310
|
() => expanded,
|
|
276
311
|
() => setExpanded(!expanded),
|
|
277
312
|
);
|
|
@@ -300,16 +335,21 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
300
335
|
ctx.ui.notify(renderRushPlan(plan, 160, true, undefined, planOwner, !owner).join("\n"), "info");
|
|
301
336
|
return;
|
|
302
337
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
component
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
try {
|
|
339
|
+
await ctx.ui.custom<void>((tui, theme, _keys, done) => {
|
|
340
|
+
refreshFullPlan = () => tui.requestRender();
|
|
341
|
+
const fg = typeof theme?.fg === "function"
|
|
342
|
+
? (color: string, value: string) => theme.fg(color as never, value)
|
|
343
|
+
: undefined;
|
|
344
|
+
const component = createRushPlanComponent(() => rushPlan ?? { ...plan, issues: [] }, () => done(), fg, planOwner, !owner);
|
|
345
|
+
return { render: (width) => component.render(width), handleInput(data) {
|
|
346
|
+
component.handleInput?.(data);
|
|
347
|
+
tui.requestRender();
|
|
348
|
+
}, invalidate: () => component.invalidate() };
|
|
349
|
+
});
|
|
350
|
+
} finally {
|
|
351
|
+
refreshFullPlan = undefined;
|
|
352
|
+
}
|
|
313
353
|
return;
|
|
314
354
|
}
|
|
315
355
|
if (!currentPlan) {
|
|
@@ -384,14 +424,14 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
384
424
|
}
|
|
385
425
|
binding = { owner: owner ?? binding!.owner, path, runId: plan.runId };
|
|
386
426
|
if (transition.event === undefined) {
|
|
387
|
-
displayWorkflowPlan(plan, binding, !owner);
|
|
427
|
+
displayWorkflowPlan(plan, binding, ctx, !owner);
|
|
388
428
|
return { content: [{ type: "text", text: `Showing ${binding.owner} rev ${plan.planRevision}: ${plan.issues.length} units.${owner ? "" : " Read-only handoff."}` }], details: { ok: true, runId: plan.runId, revision: plan.planRevision, ...(!owner ? { readOnly: true } : {}) } };
|
|
389
429
|
}
|
|
390
430
|
}
|
|
391
431
|
if (!binding) throw new Error(`No ${owner} plan is bound; send workflow.path and workflow.revision to update_plan first.`);
|
|
392
432
|
if (transition.event === undefined) throw new Error("workflow.path is required to bind or reload a plan; otherwise send workflow.event and changes.");
|
|
393
433
|
const result = applyRushPlanUpdate(binding.path, ctx.cwd, binding.runId, { ...transition, event: transition.event });
|
|
394
|
-
displayWorkflowPlan(result.plan, binding);
|
|
434
|
+
displayWorkflowPlan(result.plan, binding, ctx);
|
|
395
435
|
const rows = result.changed.map(({ target, id, added }) =>
|
|
396
436
|
`${added ? "+" : ""}${id === null ? "run" : target === "unit" ? `#${id}` : id}`);
|
|
397
437
|
const logNote = result.logAheadRevision === undefined ? ""
|
|
@@ -521,6 +561,8 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
521
561
|
});
|
|
522
562
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
523
563
|
cancelCompletedPlanClear();
|
|
564
|
+
cancelCancelledRowsTimer();
|
|
565
|
+
refreshFullPlan = undefined;
|
|
524
566
|
if (typeof unsubscribeWorkflow === "function") unsubscribeWorkflow();
|
|
525
567
|
refreshWidget = undefined;
|
|
526
568
|
try {
|
|
@@ -12,6 +12,8 @@ export interface PlanRow {
|
|
|
12
12
|
label: string;
|
|
13
13
|
title: string;
|
|
14
14
|
status: DisplayStatus;
|
|
15
|
+
// Workflow cancellations keep skipped styling but are excluded from the completion total.
|
|
16
|
+
cancelled?: boolean;
|
|
15
17
|
statusLabel?: string;
|
|
16
18
|
details?: string[];
|
|
17
19
|
}
|
|
@@ -65,7 +67,8 @@ export function renderPlanPresentation(
|
|
|
65
67
|
): string[] {
|
|
66
68
|
const size = Math.max(1, Math.floor(width));
|
|
67
69
|
const count = (status: DisplayStatus) => plan.rows.filter((row) => row.status === status).length;
|
|
68
|
-
const
|
|
70
|
+
const total = plan.rows.filter((row) => !row.cancelled).length;
|
|
71
|
+
const summary = [`${count("completed")}/${total} complete`];
|
|
69
72
|
for (const [status, label] of [
|
|
70
73
|
["in_progress", "in progress"], ["implemented", "implemented"], ["reviewed", "review passed"],
|
|
71
74
|
["blocked", "blocked"], ["failed", "failed"],
|
|
@@ -129,16 +132,22 @@ export function renderPlanPresentation(
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
export function createPlanPresentationComponent(
|
|
132
|
-
|
|
135
|
+
source: PlanPresentation | (() => PlanPresentation), theme: PlanRenderTheme, onClose: () => void,
|
|
133
136
|
): Component {
|
|
137
|
+
const presentation = () => typeof source === "function" ? source() : source;
|
|
138
|
+
const plan = presentation();
|
|
134
139
|
let selected = plan.rows.findIndex((row) => row.status === "in_progress");
|
|
135
140
|
if (selected < 0) selected = plan.rows.findIndex((row) => row.status !== "completed");
|
|
136
141
|
if (selected < 0) selected = Math.max(0, plan.rows.length - 1);
|
|
137
142
|
return {
|
|
138
|
-
render: (width) =>
|
|
143
|
+
render: (width) => {
|
|
144
|
+
const current = presentation();
|
|
145
|
+
selected = Math.min(selected, Math.max(0, current.rows.length - 1));
|
|
146
|
+
return renderPlanPresentation(current, width, theme, true, selected);
|
|
147
|
+
},
|
|
139
148
|
handleInput(data) {
|
|
140
149
|
if (matchesKey(data, "up")) selected = Math.max(0, selected - 1);
|
|
141
|
-
else if (matchesKey(data, "down")) selected = Math.min(Math.max(0,
|
|
150
|
+
else if (matchesKey(data, "down")) selected = Math.min(Math.max(0, presentation().rows.length - 1), selected + 1);
|
|
142
151
|
else if (matchesKey(data, "escape") || matchesKey(data, "left") || matchesKey(data, "ctrl+c")) onClose();
|
|
143
152
|
},
|
|
144
153
|
invalidate() {},
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
} from "./plan-presentation.js";
|
|
8
8
|
|
|
9
9
|
export const WORKFLOW_PLAN_ENTRY = "pi-better-workflow-plan";
|
|
10
|
+
export const CANCELLED_ROW_RETENTION_MS = 30_000;
|
|
10
11
|
|
|
11
12
|
interface RushUnit {
|
|
12
13
|
id: string;
|
|
13
14
|
title: string;
|
|
14
15
|
stage: string;
|
|
15
16
|
status: string;
|
|
17
|
+
cancelledAt?: number;
|
|
16
18
|
dependsOn: string[];
|
|
17
19
|
worker?: string | number | undefined;
|
|
18
20
|
note?: string | undefined;
|
|
@@ -134,7 +136,7 @@ function workflowStatus(status: string): DisplayStatus {
|
|
|
134
136
|
case "pending": return "pending";
|
|
135
137
|
case "blocked": return "blocked";
|
|
136
138
|
case "failed": return "failed";
|
|
137
|
-
case "skipped": case "not-applicable": case "cancelled": return "skipped";
|
|
139
|
+
case "skipped": case "not-applicable": case "cancelled": case "canceled": return "skipped";
|
|
138
140
|
default: return "unknown";
|
|
139
141
|
}
|
|
140
142
|
}
|
|
@@ -162,19 +164,25 @@ function presentRushPlan(plan: RushPlan, owner: string, readOnly = false): PlanP
|
|
|
162
164
|
if (plan.spec?.title) metadata.push([{ color: "dim", text: plan.spec.title }]);
|
|
163
165
|
return {
|
|
164
166
|
metadata,
|
|
165
|
-
rows: plan.
|
|
167
|
+
rows: visibleRushUnits(plan).map((unit) => {
|
|
166
168
|
const status = unitStatus(unit);
|
|
169
|
+
const cancelled = unit.status === "cancelled" || unit.status === "canceled";
|
|
167
170
|
const details = [`${unit.stage} · ${unit.status}${unit.delivery ? ` · ${unit.delivery}` : ""}${unit.worker !== undefined ? ` · worker ${unit.worker}` : ""}`];
|
|
168
171
|
if (unit.dependsOn.length) details.push(`after: ${unit.dependsOn.map((id) => `#${id}`).join(", ")}`);
|
|
169
172
|
if (unit.note) details.push(unit.note);
|
|
170
173
|
return {
|
|
171
|
-
label: `#${unit.id}`, title: unit.title, status, details,
|
|
172
|
-
...(status === "unknown" ||
|
|
174
|
+
label: `#${unit.id}`, title: unit.title, status, cancelled, details,
|
|
175
|
+
...(status === "unknown" || cancelled ? { statusLabel: unit.status } : {}),
|
|
173
176
|
};
|
|
174
177
|
}),
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
export function visibleRushUnits(plan: RushPlan, now = Date.now()): RushPlan["issues"] {
|
|
182
|
+
return plan.issues.filter(unit => (unit.status !== "cancelled" && unit.status !== "canceled") ||
|
|
183
|
+
unit.cancelledAt === undefined || now < unit.cancelledAt + CANCELLED_ROW_RETENTION_MS);
|
|
184
|
+
}
|
|
185
|
+
|
|
178
186
|
export function renderRushPlan(
|
|
179
187
|
plan: RushPlan,
|
|
180
188
|
width: number,
|
|
@@ -189,12 +197,12 @@ export function renderRushPlan(
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
export function createRushPlanComponent(
|
|
192
|
-
|
|
200
|
+
source: RushPlan | (() => RushPlan),
|
|
193
201
|
onClose: () => void,
|
|
194
202
|
fg?: (color: string, value: string) => string,
|
|
195
203
|
owner = "rush-issues",
|
|
196
204
|
readOnly = false,
|
|
197
205
|
): Component {
|
|
198
206
|
const theme = fg ? { fg } : plainPlanTheme;
|
|
199
|
-
return createPlanPresentationComponent(presentRushPlan(
|
|
207
|
+
return createPlanPresentationComponent(() => presentRushPlan(typeof source === "function" ? source() : source, owner, readOnly), theme, onClose);
|
|
200
208
|
}
|
|
@@ -42,8 +42,21 @@ this session and persist across reloads; Ctrl+S saves both as defaults, and
|
|
|
42
42
|
Reset to defaults clears
|
|
43
43
|
session overrides. `/subagents mode manual|adaptive|coordinator` and
|
|
44
44
|
`/subagents cap <number>` are command equivalents; `/subagents save` persists
|
|
45
|
-
both. Lowering the cap blocks new single and batch launches
|
|
46
|
-
|
|
45
|
+
both. Lowering the cap blocks new single and batch launches, including launches
|
|
46
|
+
still preparing, without stopping running work. Saved configuration changes are
|
|
47
|
+
picked up on subsequent reads; explicit session overrides still take precedence.
|
|
48
|
+
Before every model request, including continuations after tool calls,
|
|
49
|
+
the foreground receives one temporary settings block with the current mode, cap,
|
|
50
|
+
setting sources, revision, and running/reserved/available slots. The latest block
|
|
51
|
+
replaces older snapshots and delegation guidance; it is not saved as conversation
|
|
52
|
+
history. Changes reach the model at its next request, not during an in-flight
|
|
53
|
+
response, and are not broadcast to running children. The cap is a ceiling, not a
|
|
54
|
+
target number of agents. `subagent_settings` inspects the same effective state
|
|
55
|
+
without changing it. Its revision is a stable fingerprint of mode, cap, and
|
|
56
|
+
sources; occupancy changes do not change the revision. Admission rechecks capacity
|
|
57
|
+
when spawning because the snapshot can become stale.
|
|
58
|
+
|
|
59
|
+
The navigator's `main` row retains its mode-only controls:
|
|
47
60
|
`m` cycles the session mode, `ctrl+s` saves that default, and `x`
|
|
48
61
|
does not stop it. Manual delegates only on explicit user or workflow request, even with a
|
|
49
62
|
plan. Adaptive delegates substantial independent work when useful. Coordinator
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `defaultTools` absent → the built-in SAFE_DEFAULT_TOOLS.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
@@ -89,19 +89,23 @@ export const SAFE_DEFAULT_TOOLS = "read, bash, edit, write, web_search, web_fetc
|
|
|
89
89
|
export const SAFE_CLEAN_TOOLS = "read, bash";
|
|
90
90
|
|
|
91
91
|
let cached: SubagentConfig | undefined;
|
|
92
|
+
let cachedIdentity: string | undefined;
|
|
93
|
+
let configOverrideForTests = false;
|
|
92
94
|
let configPathForTests: string | undefined;
|
|
93
95
|
let bundledConfigPathForTests: string | undefined;
|
|
94
96
|
|
|
95
97
|
/** Test seam. Pass undefined to reload config.json on the next loadConfig call. */
|
|
96
98
|
export function setConfigForTests(next: SubagentConfig | undefined): void {
|
|
97
99
|
cached = next;
|
|
100
|
+
cachedIdentity = undefined;
|
|
101
|
+
configOverrideForTests = next !== undefined;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/** Test seam. Isolate user config, optionally supplying a package fallback. */
|
|
101
105
|
export function setConfigPathForTests(path: string | undefined, bundledPath?: string): void {
|
|
102
106
|
configPathForTests = path;
|
|
103
107
|
bundledConfigPathForTests = bundledPath;
|
|
104
|
-
|
|
108
|
+
setConfigForTests(undefined);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
export function configPath(): string {
|
|
@@ -128,8 +132,12 @@ function savedConfig(): SubagentConfig {
|
|
|
128
132
|
return user === undefined ? {} : parseConfig(user);
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
function bundledConfigPath(): string | undefined {
|
|
136
|
+
return bundledConfigPathForTests ?? (configPathForTests ? undefined : join(selfDir(), "config.json"));
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
function bundledConfig(): SubagentConfig {
|
|
132
|
-
const path =
|
|
140
|
+
const path = bundledConfigPath();
|
|
133
141
|
if (!path) return {};
|
|
134
142
|
try {
|
|
135
143
|
return readConfig(path);
|
|
@@ -138,12 +146,31 @@ function bundledConfig(): SubagentConfig {
|
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
function configIdentity(): string {
|
|
150
|
+
const paths = [configPath(), bundledConfigPath(),
|
|
151
|
+
configPathForTests ? undefined : join(piAgentDir(), "extensions", "pi-better-subagents-config.json")];
|
|
152
|
+
return JSON.stringify(paths.map(path => {
|
|
153
|
+
if (!path) return null;
|
|
154
|
+
try {
|
|
155
|
+
const stat = statSync(path, { bigint: true });
|
|
156
|
+
return [path, stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].map(String);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return [path, (error as NodeJS.ErrnoException).code];
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/** Saved user choices take precedence; missing/invalid files contribute no keys. */
|
|
142
164
|
export function loadConfig(): SubagentConfig {
|
|
143
|
-
if (
|
|
165
|
+
if (configOverrideForTests) return cached!;
|
|
166
|
+
const identity = configIdentity();
|
|
167
|
+
if (cached && cachedIdentity === identity) return cached;
|
|
144
168
|
let user: SubagentConfig = {};
|
|
145
169
|
try { user = savedConfig(); } catch { /* retain package fallback */ }
|
|
146
170
|
cached = { ...bundledConfig(), ...user };
|
|
171
|
+
// Keep the pre-read identity: migration or a concurrent replacement must
|
|
172
|
+
// trigger another read rather than label an older value with a newer stat.
|
|
173
|
+
cachedIdentity = identity;
|
|
147
174
|
return cached;
|
|
148
175
|
}
|
|
149
176
|
|
|
@@ -162,10 +189,10 @@ export function writeSubagentSettings(settings: { delegationMode: DelegationMode
|
|
|
162
189
|
|
|
163
190
|
function writeConfigSettings(settings: Partial<SubagentConfig>, path: string): void {
|
|
164
191
|
if (path === configPath()) savedConfig();
|
|
165
|
-
|
|
192
|
+
updateHarnessSetting("subagents", previous => ({
|
|
166
193
|
...(previous === undefined ? {} : parseConfig(previous)), ...settings,
|
|
167
194
|
}), { agentDir: () => dirname(path) });
|
|
168
|
-
if (path === configPath())
|
|
195
|
+
if (path === configPath()) setConfigForTests(undefined);
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
/** Normalize a comma/space tool list to pi's bare comma form: "a, b" → "a,b". */
|
|
@@ -97,6 +97,7 @@ thresholds continue to take precedence for subagent health.
|
|
|
97
97
|
|------|---------|--------------|
|
|
98
98
|
| `subagent_spawn` | never | Launch a task in a background subagent; returns a run id at once. Params: `prompt`, `name`, `model`, `tools` (allowlist), `exclude_tools`, `sandbox`, `sandbox_dir`, `callback`, `clean`, `cwd`, `git_clone_workspace`, `approve`, `allow_nested`, and the [timing](#run-timing-deadline-ceiling-stuck) overrides `deadline_minutes`, `grace_minutes`, `max_minutes`, `stuck_minutes`. |
|
|
99
99
|
| `subagent_spawn_batch` | never | Launch several independent subagents at once. Each job becomes a normal run. Params: `batchName`, `shared` (options applied to every job), `jobs[]` (each needs `prompt`; same optional params as `subagent_spawn`), `onCapacity` (`reject` or `launch-available`). |
|
|
100
|
+
| `subagent_settings` | never | Read-only effective settings and capacity snapshot. No parameters. Returns `delegationMode`, `maxConcurrent`, per-setting `sources` (`session` or `config`), a stable settings `revision`, `runningCount`, `pendingCount`, `occupiedSlots`, and `availableSlots`. The limit is a ceiling, not a target; spawn admission rechecks live capacity. |
|
|
100
101
|
| `subagent_list` | never | Compact current-session list (default 10 rows / 1 KiB page). Params: `all` (machine-global / foreign session), `limit` (default 10, max 100), `cursor`, `max_bytes` (max 4 KiB), `status` (`running`, `completed`, `failed`, `killed`, `exited`, durable `orphaned`, `lost`). Incident counts are compact; no spend/tool histories. |
|
|
101
102
|
| `subagent_output` | never | Bounded current-session excerpt (default 1 KiB / 10 lines). Params: `id`, `lines` (`tail_lines` is a deprecated alias), `cursor` (a returned `nextCursor`, `statusCursor`, or `incidentCursor`), `max_bytes` (max 4 KiB; raw 16 KiB default, 64 KiB max), `mode` (`raw` pages retained log bytes), `include` (`["cost"]`, `["tools"]`, or both: opt-in spend and tool-count lines), `all` (foreign-session id). Missing/unreadable logs are gaps, not empty healthy output. |
|
|
102
103
|
| `subagent_result` | never | Finished-run answer for the current session (2048-byte page). Params: `id`, `cursor` (a returned `nextCursor` continues the answer; `statusCursor` / `incidentCursor` also accepted), `max_bytes` (maximum 8192 bytes), `lines` (optional line cap per answer page), `mode` (`raw`), `include` (`["cost"]`, `["tools"]`, or both), `all` (foreign-session id). Failures and exceptional lifecycle facts come before progress; no tool-name histories. TUI folding is display-only. |
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { delegationPrompt, type DelegationMode } from "./delegation.ts";
|
|
3
|
+
|
|
4
|
+
export const SUBAGENT_SETTINGS_CONTEXT = "pi-better-subagents-settings";
|
|
5
|
+
|
|
6
|
+
export interface EffectiveSubagentSettings {
|
|
7
|
+
revision: string;
|
|
8
|
+
delegationMode: DelegationMode;
|
|
9
|
+
maxConcurrent: number;
|
|
10
|
+
sources: { delegationMode: "session" | "config"; maxConcurrent: "session" | "config" };
|
|
11
|
+
runningCount: number;
|
|
12
|
+
pendingCount: number;
|
|
13
|
+
occupiedSlots: number;
|
|
14
|
+
availableSlots: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function effectiveSubagentSettings(
|
|
18
|
+
settings: Pick<EffectiveSubagentSettings, "delegationMode" | "maxConcurrent" | "sources">,
|
|
19
|
+
runningCount: number,
|
|
20
|
+
pendingCount: number,
|
|
21
|
+
): EffectiveSubagentSettings {
|
|
22
|
+
const revision = createHash("sha256").update(JSON.stringify([
|
|
23
|
+
settings.delegationMode, settings.maxConcurrent, settings.sources.delegationMode, settings.sources.maxConcurrent,
|
|
24
|
+
])).digest("hex").slice(0, 12);
|
|
25
|
+
const occupiedSlots = runningCount + pendingCount;
|
|
26
|
+
return { ...settings, revision, runningCount, pendingCount, occupiedSlots,
|
|
27
|
+
availableSlots: Math.max(0, settings.maxConcurrent - occupiedSlots) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatSubagentSettings(settings: EffectiveSubagentSettings): string {
|
|
31
|
+
return [
|
|
32
|
+
`Subagent settings: revision ${settings.revision}`,
|
|
33
|
+
`Delegation: ${settings.delegationMode} (${settings.sources.delegationMode})`,
|
|
34
|
+
`Concurrency limit: ${settings.maxConcurrent} (${settings.sources.maxConcurrent})`,
|
|
35
|
+
`Running: ${settings.runningCount}; reserved: ${settings.pendingCount}`,
|
|
36
|
+
`Occupied slots: ${settings.occupiedSlots}`,
|
|
37
|
+
`Available slots: ${settings.availableSlots}`,
|
|
38
|
+
"The limit is a ceiling, not a target. Launch ready, independent, useful work within available capacity. " +
|
|
39
|
+
"This is a point-in-time snapshot; spawn admission rechecks capacity. Lowering the limit does not stop existing runs.",
|
|
40
|
+
delegationPrompt(settings.delegationMode),
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function subagentSettingsTool(Type: any, read: () => EffectiveSubagentSettings) {
|
|
45
|
+
return {
|
|
46
|
+
name: "subagent_settings",
|
|
47
|
+
label: "Subagent Settings",
|
|
48
|
+
description: "Inspect effective subagent delegation mode, concurrency limit, setting sources, revision, and running/reserved/available slots. Read-only; does not change settings or launch work. The limit is a ceiling, not a target.",
|
|
49
|
+
parameters: Type.Object({}),
|
|
50
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
51
|
+
outputSchema: Type.Object({
|
|
52
|
+
revision: Type.String(), delegationMode: Type.String(), maxConcurrent: Type.Number({ minimum: 1, multipleOf: 1 }),
|
|
53
|
+
sources: Type.Object({ delegationMode: Type.String(), maxConcurrent: Type.String() }),
|
|
54
|
+
runningCount: Type.Number({ minimum: 0, multipleOf: 1 }), pendingCount: Type.Number({ minimum: 0, multipleOf: 1 }),
|
|
55
|
+
occupiedSlots: Type.Number({ minimum: 0, multipleOf: 1 }), availableSlots: Type.Number({ minimum: 0, multipleOf: 1 }),
|
|
56
|
+
}),
|
|
57
|
+
async execute() {
|
|
58
|
+
const settings = read();
|
|
59
|
+
return { content: [{ type: "text" as const, text: formatSubagentSettings(settings) }],
|
|
60
|
+
details: settings, structuredContent: settings };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -37,6 +37,7 @@ import { parseRun, readRunTranscript, resetParseRunCursor, type Usage } from "./
|
|
|
37
37
|
import { finalizeRun as finalizeRunCore } from "./finalization.ts";
|
|
38
38
|
import { loadConfig, writeDelegationMode, writeSubagentSettings, isConcurrencyCap, normalizeConcurrencyCap, normalizeTools, resolveExtensionPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS } from "./config.ts";
|
|
39
39
|
import { createSubagentSettingsPage } from "./settings-page.ts";
|
|
40
|
+
import { effectiveSubagentSettings, formatSubagentSettings, subagentSettingsTool, SUBAGENT_SETTINGS_CONTEXT } from "./effective-settings.ts";
|
|
40
41
|
import { STEER_FILE_ENV, readSteerReceipt } from "./child-steer.ts";
|
|
41
42
|
import {
|
|
42
43
|
decideTiming,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
type TimingStopReason,
|
|
54
55
|
} from "./timing.ts";
|
|
55
56
|
import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
|
|
56
|
-
import { DELEGATION_MODES, DELEGATION_MODE_REQUEST,
|
|
57
|
+
import { DELEGATION_MODES, DELEGATION_MODE_REQUEST, isDelegationMode, normalizeDelegationMode, type DelegationMode } from "./delegation.ts";
|
|
57
58
|
import { appendExecutionPolicy } from "./shared-execution-policy.ts";
|
|
58
59
|
import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
59
60
|
import { prepareTaskRuntime } from "./task-policy.ts";
|
|
@@ -186,6 +187,7 @@ const SUBAGENT_TOOLS = [
|
|
|
186
187
|
"subagent_spawn",
|
|
187
188
|
"subagent_spawn_batch",
|
|
188
189
|
"subagent_list",
|
|
190
|
+
"subagent_settings",
|
|
189
191
|
"subagent_output",
|
|
190
192
|
"subagent_stop",
|
|
191
193
|
"subagent_result",
|
|
@@ -1522,6 +1524,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1522
1524
|
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: null });
|
|
1523
1525
|
};
|
|
1524
1526
|
const activeDelegationMode = (): DelegationMode => delegationOverride ?? normalizeDelegationMode(loadConfig().delegationMode);
|
|
1527
|
+
const countRunning = () =>
|
|
1528
|
+
listActiveMetasForParent(process.pid).filter((meta) => effectiveStatus(meta) === "running").length;
|
|
1529
|
+
const effectiveSettings = () => effectiveSubagentSettings({
|
|
1530
|
+
delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap(),
|
|
1531
|
+
sources: { delegationMode: delegationOverride ? "session" : "config",
|
|
1532
|
+
maxConcurrent: capOverride !== undefined ? "session" : "config" },
|
|
1533
|
+
}, countRunning(), getSharedCapacityGate(countRunning).pending);
|
|
1525
1534
|
const saveSettings = (): { ok: boolean; message: string } => {
|
|
1526
1535
|
try {
|
|
1527
1536
|
writeSubagentSettings({ delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap() });
|
|
@@ -1846,6 +1855,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1846
1855
|
|
|
1847
1856
|
let spawned: ReturnType<typeof spawnDetached>;
|
|
1848
1857
|
try {
|
|
1858
|
+
// Reservations prevent competing admissions, but do not grandfather
|
|
1859
|
+
// unstarted work after a cap reduction. No await separates this live
|
|
1860
|
+
// occupancy check, child creation, and the running metadata below.
|
|
1861
|
+
const maxConcurrent = activeConcurrencyCap();
|
|
1862
|
+
if (countRunning() >= maxConcurrent) {
|
|
1863
|
+
throw new Error(`Max concurrent subagents (${maxConcurrent}) reached. Stop or let some finish first.`);
|
|
1864
|
+
}
|
|
1849
1865
|
spawned = spawnDetached({ file: cmd.file, fileArgs: cmd.fileArgs, cwd, logPath: logPathFor(id),
|
|
1850
1866
|
env: { [STEER_FILE_ENV]: steerPathFor(id) } });
|
|
1851
1867
|
} catch (error) {
|
|
@@ -1956,8 +1972,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1956
1972
|
if (blankSelector) throw new Error(blankSelector);
|
|
1957
1973
|
|
|
1958
1974
|
const maxConcurrent = activeConcurrencyCap();
|
|
1959
|
-
const countRunning = () =>
|
|
1960
|
-
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
1961
1975
|
// Shared with batch-spawn: reserve before any async work so an interleaved
|
|
1962
1976
|
// batch cannot oversubscribe after this check and before writeMeta.
|
|
1963
1977
|
const gate = getSharedCapacityGate(countRunning);
|
|
@@ -2093,8 +2107,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
2093
2107
|
};
|
|
2094
2108
|
|
|
2095
2109
|
const cfg = loadConfig();
|
|
2096
|
-
const countRunning = () =>
|
|
2097
|
-
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
2098
2110
|
const launchAvailable = p.onCapacity === "launch-available";
|
|
2099
2111
|
// Shared with single-spawn. Reservations count against maxConcurrent so a
|
|
2100
2112
|
// concurrent single spawn cannot take a slot the batch already admitted.
|
|
@@ -2248,6 +2260,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2248
2260
|
getActiveOrigin: (ctx?: unknown) => activeCallbackOrigin ?? verifiedOriginFromContext(ctx as ExtensionContext | undefined),
|
|
2249
2261
|
};
|
|
2250
2262
|
pi.registerTool(subagentListTool(Type, toolSession));
|
|
2263
|
+
pi.registerTool(subagentSettingsTool(Type, effectiveSettings));
|
|
2251
2264
|
pi.registerTool(subagentOutputTool(Type, toolSession));
|
|
2252
2265
|
const acceptanceResultTool = subagentResultTool(Type, toolSession);
|
|
2253
2266
|
pi.registerTool(acceptanceResultTool);
|
|
@@ -2288,13 +2301,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2288
2301
|
const openSettings = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
2289
2302
|
if (ctx.mode === "tui" && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
2290
2303
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => createSubagentSettingsPage(theme, {
|
|
2291
|
-
get: () =>
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2304
|
+
get: () => {
|
|
2305
|
+
const settings = effectiveSettings();
|
|
2306
|
+
return {
|
|
2307
|
+
mode: settings.delegationMode, maxConcurrent: settings.maxConcurrent,
|
|
2308
|
+
modeSource: settings.sources.delegationMode,
|
|
2309
|
+
capSource: settings.sources.maxConcurrent,
|
|
2310
|
+
defaultMode: normalizeDelegationMode(loadConfig().delegationMode),
|
|
2311
|
+
defaultCap: normalizeConcurrencyCap(loadConfig().maxConcurrent),
|
|
2312
|
+
};
|
|
2313
|
+
},
|
|
2298
2314
|
changeMode, changeCap, save: saveSettings, reset: resetSettings,
|
|
2299
2315
|
}, () => { refreshBackgroundWorkNavigator(ctx); tui.requestRender(); }, () => done()));
|
|
2300
2316
|
} else {
|
|
@@ -2354,9 +2370,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2354
2370
|
pi.registerTool(discoveryTool as Parameters<ExtensionAPI["registerTool"]>[0]);
|
|
2355
2371
|
|
|
2356
2372
|
pi.on("before_agent_start", (event) => ({
|
|
2357
|
-
systemPrompt: `${appendExecutionPolicy(event.systemPrompt)}\n\
|
|
2373
|
+
systemPrompt: `${appendExecutionPolicy(event.systemPrompt)}\n\nUse the latest Subagent settings context block as the authoritative delegation mode and capacity snapshot. Its delegation guidance replaces earlier mode-specific guidance, including guidance in a plan. Inspect subagent_settings when needed; never treat the concurrency limit as a target number of agents.`,
|
|
2358
2374
|
}));
|
|
2359
2375
|
|
|
2376
|
+
pi.on("context", (event) => {
|
|
2377
|
+
const settings = effectiveSettings();
|
|
2378
|
+
return { messages: [
|
|
2379
|
+
...event.messages.filter(message => message.role !== "custom" || message.customType !== SUBAGENT_SETTINGS_CONTEXT),
|
|
2380
|
+
{ role: "custom" as const, customType: SUBAGENT_SETTINGS_CONTEXT,
|
|
2381
|
+
content: formatSubagentSettings(settings), display: false, details: settings, timestamp: Date.now() },
|
|
2382
|
+
] };
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2360
2385
|
// ---- live-status lifecycle -----------------------------------------
|
|
2361
2386
|
pi.on("agent_start", async (_event, ctx) => {
|
|
2362
2387
|
setCallbackBatchContext(pi, ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"pi-better-background-tasks": "0.9.0",
|
|
57
57
|
"pi-better-goal": "0.9.0",
|
|
58
|
-
"pi-better-plan": "0.9.
|
|
58
|
+
"pi-better-plan": "0.9.1",
|
|
59
59
|
"pi-better-sandbox": "0.12.0",
|
|
60
60
|
"pi-better-ssh": "0.1.2",
|
|
61
|
-
"pi-better-subagents": "0.
|
|
61
|
+
"pi-better-subagents": "0.19.0",
|
|
62
62
|
"proper-lockfile": "^4.1.2",
|
|
63
63
|
"smol-toml": "1.9.0",
|
|
64
64
|
"yaml": "^2.9.1"
|