pi-better-harness 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -82,7 +82,9 @@ into one disclosure row showing the call count and any failures. Only the failur
|
|
|
82
82
|
count uses the error color; the disclosure and total stay muted. Assistant text
|
|
83
83
|
stays visible, and restored history uses the same folded view. In newer Pi
|
|
84
84
|
fullscreen mode, click a block disclosure to reveal its call rows, then click a
|
|
85
|
-
call to expand its original details.
|
|
85
|
+
call to expand its original details. Hover highlights compact call and disclosure
|
|
86
|
+
rows using the selection background (reverse video for themes without one), and
|
|
87
|
+
tool icons sit centered in a three-column gutter. Click the disclosure again to refold the
|
|
86
88
|
block; native expanded call/result clicks collapse individual details. Regular
|
|
87
89
|
terminals keep mouse input for terminal selection and scrollback; Ctrl+O expands
|
|
88
90
|
all original tool details in both modes and folds them again on the next toggle.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
1
|
+
import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -6,6 +6,11 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import { stripVTControlCharacters } from "node:util";
|
|
7
7
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
interface ToolUI {
|
|
10
|
+
requestRender(): void;
|
|
11
|
+
children?: TranscriptComponent[];
|
|
12
|
+
handleViewportInput?(data: string): unknown;
|
|
13
|
+
}
|
|
9
14
|
interface ToolComponent {
|
|
10
15
|
toolName: string;
|
|
11
16
|
toolCallId: string;
|
|
@@ -16,7 +21,7 @@ interface ToolComponent {
|
|
|
16
21
|
result?: { isError?: boolean };
|
|
17
22
|
showImages: boolean;
|
|
18
23
|
resultRendererComponent?: unknown;
|
|
19
|
-
ui:
|
|
24
|
+
ui: ToolUI;
|
|
20
25
|
invalidate(): void;
|
|
21
26
|
setExpanded(expanded: boolean): void;
|
|
22
27
|
render(width: number): string[];
|
|
@@ -30,7 +35,7 @@ interface TranscriptComponent extends Component {
|
|
|
30
35
|
interface ToolGroup { members: ToolComponent[]; open: boolean; expanded: number }
|
|
31
36
|
|
|
32
37
|
type Method = (this: ToolComponent, ...args: unknown[]) => unknown;
|
|
33
|
-
interface ToolMouseEvent { type: string; button: string; y: number; shift?: boolean; alt?: boolean; ctrl?: boolean }
|
|
38
|
+
interface ToolMouseEvent { type: string; button: string; x: number; y: number; screenX: number; screenY: number; width: number; shift?: boolean; alt?: boolean; ctrl?: boolean }
|
|
34
39
|
type MouseHandler = (this: ToolComponent, event: ToolMouseEvent) => unknown;
|
|
35
40
|
interface ToolPrototype {
|
|
36
41
|
handleMouse?: MouseHandler;
|
|
@@ -49,6 +54,7 @@ interface HookState {
|
|
|
49
54
|
restore(): void;
|
|
50
55
|
completeRun(): void;
|
|
51
56
|
restoreCompletedCalls(ids: Iterable<string>): void;
|
|
57
|
+
handleTerminalInput(data: string): void;
|
|
52
58
|
}
|
|
53
59
|
export interface MinimalOutputHook {
|
|
54
60
|
setEnabled(enabled: boolean): void;
|
|
@@ -90,9 +96,9 @@ function compactCall(component: ToolComponent, width: number, theme?: Theme, ind
|
|
|
90
96
|
: component.toolName.replace(/_/g, " ").replace(/^./, letter => letter.toUpperCase());
|
|
91
97
|
const suffix = failed ? " (failed)" : running ? " (running)" : "";
|
|
92
98
|
const icon = TOOL_ICONS[component.toolName] ?? "\u25c7";
|
|
93
|
-
const title =
|
|
99
|
+
const title = ` ${icon} ${label}`;
|
|
94
100
|
const styled = theme
|
|
95
|
-
?
|
|
101
|
+
? ` ${theme.fg(failed ? "error" : running ? "accent" : "muted", icon)} ${theme.fg("muted", label)}${detail ? ` ${theme.fg("dim", detail)}` : ""}${theme.fg(failed ? "error" : "dim", suffix)}`
|
|
96
102
|
: `${title}${detail ? ` ${detail}` : ""}${suffix}`;
|
|
97
103
|
return [truncateToWidth(`${" ".repeat(Math.min(indent, Math.max(0, width - 1)))}${styled}`, width)];
|
|
98
104
|
}
|
|
@@ -184,6 +190,25 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
184
190
|
...(typeof prototype.handleMouse === "function" ? { handleMouse: prototype.handleMouse } : {}),
|
|
185
191
|
};
|
|
186
192
|
let mouseHandling = false;
|
|
193
|
+
const inputWrappers = new Map<ToolUI, { original: NonNullable<ToolUI["handleViewportInput"]>; wrapper: NonNullable<ToolUI["handleViewportInput"]> }>();
|
|
194
|
+
let hovered: { component: ToolComponent; row: number; left: number; top: number; width: number } | undefined;
|
|
195
|
+
const clearHover = () => {
|
|
196
|
+
if (!hovered) return;
|
|
197
|
+
const component = hovered.component;
|
|
198
|
+
hovered = undefined;
|
|
199
|
+
component.ui.requestRender();
|
|
200
|
+
};
|
|
201
|
+
const highlight = (component: ToolComponent, lines: string[], width: number): string[] => {
|
|
202
|
+
if (hovered?.component !== component) return lines;
|
|
203
|
+
const theme = getTheme?.();
|
|
204
|
+
if (width !== hovered.width || hovered.row >= lines.length) { clearHover(); return lines; }
|
|
205
|
+
if (theme) {
|
|
206
|
+
const row = lines[hovered.row] + " ".repeat(Math.max(0, width - visibleWidth(lines[hovered.row])));
|
|
207
|
+
const shaded = theme.bg("selectedBg", row);
|
|
208
|
+
lines[hovered.row] = shaded.startsWith("\x1b[49m") ? theme.inverse(row) : shaded;
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
};
|
|
187
212
|
const seen = new WeakSet<ToolComponent>();
|
|
188
213
|
const components = new Set<WeakRef<ToolComponent>>();
|
|
189
214
|
let completed = new Map<string, object>();
|
|
@@ -246,6 +271,7 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
246
271
|
this.redraw();
|
|
247
272
|
},
|
|
248
273
|
redraw(collapse = false) {
|
|
274
|
+
clearHover();
|
|
249
275
|
for (const reference of components) {
|
|
250
276
|
const component = reference.deref();
|
|
251
277
|
if (!component) { components.delete(reference); continue; }
|
|
@@ -255,6 +281,11 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
255
281
|
}
|
|
256
282
|
},
|
|
257
283
|
restore() {
|
|
284
|
+
clearHover();
|
|
285
|
+
for (const [ui, { original, wrapper }] of inputWrappers) {
|
|
286
|
+
if (ui.handleViewportInput === wrapper) ui.handleViewportInput = original;
|
|
287
|
+
}
|
|
288
|
+
inputWrappers.clear();
|
|
258
289
|
for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "render"] as const) {
|
|
259
290
|
// Do not remove another extension's subsequently installed wrapper.
|
|
260
291
|
if (prototype[name] === wrappers[name]) Object.assign(prototype, { [name]: originals[name] });
|
|
@@ -263,10 +294,30 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
263
294
|
delete prototype[HOOK];
|
|
264
295
|
components.clear();
|
|
265
296
|
},
|
|
297
|
+
handleTerminalInput(data) {
|
|
298
|
+
if (!hovered) return;
|
|
299
|
+
// Pi routes moves only to the new target; observe input to clear a row on exit.
|
|
300
|
+
const motion = /^\x1b\[<35;(\d+);(\d+)M$/.exec(data);
|
|
301
|
+
if (motion) {
|
|
302
|
+
const x = Number(motion[1]) - 1;
|
|
303
|
+
const y = Number(motion[2]) - 1;
|
|
304
|
+
if (y === hovered.top && x >= hovered.left && x < hovered.left + hovered.width) return;
|
|
305
|
+
}
|
|
306
|
+
clearHover();
|
|
307
|
+
},
|
|
266
308
|
};
|
|
267
309
|
const current = state;
|
|
268
310
|
const wrappers = {
|
|
269
311
|
updateDisplay(this: ToolComponent, ...args: unknown[]) {
|
|
312
|
+
if (typeof this.ui.handleViewportInput === "function" && !inputWrappers.has(this.ui)) {
|
|
313
|
+
const original = this.ui.handleViewportInput;
|
|
314
|
+
const wrapper = function(this: ToolUI, data: string) {
|
|
315
|
+
current.handleTerminalInput(data);
|
|
316
|
+
return original.call(this, data);
|
|
317
|
+
};
|
|
318
|
+
inputWrappers.set(this.ui, { original, wrapper });
|
|
319
|
+
this.ui.handleViewportInput = wrapper;
|
|
320
|
+
}
|
|
270
321
|
if (!seen.has(this)) { seen.add(this); components.add(new WeakRef(this)); }
|
|
271
322
|
if (!current.enabled || this.expanded) return originals.updateDisplay.apply(this, args);
|
|
272
323
|
const showImages = this.showImages;
|
|
@@ -281,6 +332,7 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
281
332
|
}
|
|
282
333
|
},
|
|
283
334
|
setExpanded(this: ToolComponent, ...args: unknown[]) {
|
|
335
|
+
clearHover();
|
|
284
336
|
const group = groups.get(this);
|
|
285
337
|
const wasExpanded = this.expanded;
|
|
286
338
|
if (args[0] === false && !mouseHandling && group) group.open = false;
|
|
@@ -300,7 +352,7 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
300
352
|
const padding = layout?.anchor?.outputPad ?? 1;
|
|
301
353
|
const indent = Number.isFinite(padding) ? Math.max(0, Math.floor(padding)) + 2 : 3;
|
|
302
354
|
const group = groupFor(this, layout?.parent);
|
|
303
|
-
if (!group) return compactCall(this, width, getTheme?.(), indent);
|
|
355
|
+
if (!group) return highlight(this, compactCall(this, width, getTheme?.(), indent), width);
|
|
304
356
|
const open = group.open || group.expanded > 0;
|
|
305
357
|
const first = group.members[0] === this;
|
|
306
358
|
const lines: string[] = [];
|
|
@@ -315,11 +367,19 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
315
367
|
lines.push(truncateToWidth(`${" ".repeat(Math.min(indent, Math.max(0, width - 1)))}${styled}`, width));
|
|
316
368
|
}
|
|
317
369
|
if (open) lines.push(...compactCall(this, width, getTheme?.(), indent + 2));
|
|
318
|
-
return lines;
|
|
370
|
+
return highlight(this, lines, width);
|
|
319
371
|
},
|
|
320
372
|
};
|
|
321
373
|
const mouseWrapper: MouseHandler = function(event) {
|
|
322
374
|
if (current.enabled && !this.expanded) {
|
|
375
|
+
if (event.type === "move" && event.button === "none") {
|
|
376
|
+
const lines = this.render(event.width);
|
|
377
|
+
if (event.y < 0 || event.y >= lines.length) { clearHover(); return undefined; }
|
|
378
|
+
if (hovered?.component === this && hovered.row === event.y) return { handled: true, render: false };
|
|
379
|
+
clearHover();
|
|
380
|
+
hovered = { component: this, row: event.y, left: event.screenX - event.x, top: event.screenY, width: event.width };
|
|
381
|
+
return { handled: true, render: true };
|
|
382
|
+
}
|
|
323
383
|
if (event.type !== "click" || event.button !== "left" || event.shift || event.alt || event.ctrl) return undefined;
|
|
324
384
|
const group = groupFor(this, layoutFor(this)?.parent);
|
|
325
385
|
if (group?.members[0] === this && event.y === 0) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## What It Does
|
|
8
8
|
|
|
9
|
-
- Gives models `update_plan` and `get_plan`
|
|
9
|
+
- Gives models one `update_plan` interface for checklist updates, workflow binding, and workflow transitions, plus `get_plan` for inspection.
|
|
10
10
|
- Supports dependency edges (`id` and `dependsOn`) so independent ready steps can run concurrently.
|
|
11
11
|
- Shows the complete checklist of completed, active, pending, and blocked steps above the editor.
|
|
12
12
|
- Persists plan state and display preferences on the active Pi session branch.
|
|
@@ -38,7 +38,18 @@ For a DAG, assign stable ids to prerequisite steps and list those ids in depende
|
|
|
38
38
|
|
|
39
39
|
When an explicitly invoked skill declares `workflow-role: coordinator` in its metadata, that skill's task plan takes precedence. The generic checklist stays persisted but is hidden, and `update_plan` refuses generic checklist updates until workflow ownership is released. `/plan` never opens a stale generic checklist during workflow ownership.
|
|
40
40
|
|
|
41
|
-
For any workflow using the shared task-plan contract (including `resolve-issues` and `rush-issues`),
|
|
41
|
+
For any workflow using the shared task-plan contract (including `resolve-issues` and `rush-issues`), bind its plan through `update_plan` with the absolute `.resolve-issues/rush/<run-id>/task-plan.json` path and its persisted `planRevision`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{ "workflow": {
|
|
45
|
+
"path": "/absolute/project/.resolve-issues/rush/run-1/task-plan.json",
|
|
46
|
+
"revision": 12
|
|
47
|
+
} }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This binds the run to the session and shows its fleet stages and units in the widget, `/plan`, and `get_plan`. It does not rewrite the plan, increment its revision, or append a profiling event. The binding survives Pi session resume, and ownership release restores the prior generic checklist. Repeat this form to reload an externally saved checkpoint at its exact revision. `sync_workflow_plan` has been removed; migrate its old `{path, revision}` arguments into `update_plan`'s `workflow` object.
|
|
51
|
+
|
|
52
|
+
After binding, record every transition with `update_plan` and a `workflow` object instead of editing `task-plan.json` or the profiling log by hand:
|
|
42
53
|
|
|
43
54
|
```json
|
|
44
55
|
{ "workflow": {
|
|
@@ -52,6 +63,8 @@ For any workflow using the shared task-plan contract (including `resolve-issues`
|
|
|
52
63
|
} }
|
|
53
64
|
```
|
|
54
65
|
|
|
66
|
+
You may include `path` and `revision` alongside `event` and `changes` in the first call to bind and save a transition together. A rejected transition does not activate a new binding. Every successful binding or transition requests a TUI refresh; there is no separate synchronization tool to call afterward.
|
|
67
|
+
|
|
55
68
|
Each issue row shows where it is between code and delivery: `●` while it is being built, `◐ implemented` once `stage` is `done` with a `headSha`, `◑ review passed` while `reviewedHead` equals `headSha`, and `✓` only at `status: succeeded`, when the change has landed on the target branch by a merged PR or a direct push. Only `✓` rows count as complete.
|
|
56
69
|
|
|
57
70
|
`changes` addresses units, components, and fleet stages by id (a change without an id sets run-level fields); all changes in one call are one transition. For a scope change, a change with `target` `unit` or `component` and an `add` row appends a new row; existing rows cannot be removed or renamed. The tool checks ids, dependencies (including cycles), worker slots, and status/stage values against the shared issue-resolution contract (a fleet status of `n/a` is saved as `not-applicable`), refuses a stale `revision`, appends an optional `decision`, then saves the plan atomically with `planRevision + 1` and a new `updatedAt`, and appends one profiling event with the same revision to the log the run already uses (`profiling/run.jsonl` or `profiling.jsonl`). A rejected update changes nothing. The event is appended before the plan is renamed into place, so a crash between the two can leave the log one revision ahead; the next update notices, reuses that revision, and marks its event with `logAheadRevision`.
|
|
@@ -53,8 +53,9 @@ const WorkflowRowChangeSchema = Type.Object({
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
const WorkflowUpdateSchema = Type.Object({
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
path: Type.Optional(Type.String({ description: "Absolute path to .resolve-issues/rush/<run-id>/task-plan.json. Send with revision to bind or reload a workflow plan; omit event for a read-only bind." })),
|
|
57
|
+
event: Type.Optional(Type.String({ description: "Short name for a saved transition, e.g. unit-validated. Required when sending changes, decision, or profiling; omit for a read-only bind." })),
|
|
58
|
+
revision: Type.Optional(Type.Integer({ minimum: 0, description: "The planRevision you last saw. Required with path. The update is refused if the saved plan has a different revision." })),
|
|
58
59
|
changes: Type.Optional(Type.Array(WorkflowRowChangeSchema, { maxItems: 50, description: "Every row this transition changes. All of them are saved together as one revision." })),
|
|
59
60
|
decision: Type.Optional(Type.Object({
|
|
60
61
|
id: Type.String({ description: "New, unique decision id." }),
|
|
@@ -66,7 +67,7 @@ const WorkflowUpdateSchema = Type.Object({
|
|
|
66
67
|
additionalProperties: true,
|
|
67
68
|
description: "Extra fields for this transition's profiling event, e.g. outcome, wallMs, waitMs, headSha.",
|
|
68
69
|
})),
|
|
69
|
-
}, { description: "
|
|
70
|
+
}, { description: "Bind, reload, or update a workflow-owned task plan. Send this instead of plan while a workflow owns planning." });
|
|
70
71
|
|
|
71
72
|
const UpdatePlanSchema = Type.Object({
|
|
72
73
|
explanation: Type.Optional(Type.String({ description: "Why the plan or its status changed." })),
|
|
@@ -74,6 +75,8 @@ const UpdatePlanSchema = Type.Object({
|
|
|
74
75
|
workflow: Type.Optional(WorkflowUpdateSchema),
|
|
75
76
|
});
|
|
76
77
|
|
|
78
|
+
type WorkflowPlanInput = Omit<RushPlanUpdate, "event"> & { path?: string; event?: string };
|
|
79
|
+
|
|
77
80
|
const LEGACY_PLAN_NAV_STATUS_KEY = "pi-better-plan-nav";
|
|
78
81
|
const DELEGATION_MODE_REQUEST = "pi-better-subagents:delegation-mode-request";
|
|
79
82
|
type DelegationMode = "manual" | "adaptive" | "coordinator";
|
|
@@ -148,6 +151,17 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
148
151
|
return rushPlan;
|
|
149
152
|
};
|
|
150
153
|
|
|
154
|
+
const displayWorkflowPlan = (plan: RushPlan, binding: WorkflowPlanBinding): void => {
|
|
155
|
+
if (!rushBinding || rushBinding.owner !== binding.owner || rushBinding.path !== binding.path || rushBinding.runId !== binding.runId) {
|
|
156
|
+
pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "set", ...binding });
|
|
157
|
+
}
|
|
158
|
+
displayedWorkflowOwner = binding.owner;
|
|
159
|
+
rushBinding = binding;
|
|
160
|
+
rushPlan = plan;
|
|
161
|
+
rushError = null;
|
|
162
|
+
refresh(true);
|
|
163
|
+
};
|
|
164
|
+
|
|
151
165
|
const cancelCompletedPlanClear = (): void => {
|
|
152
166
|
if (completedPlanClearTimer) clearTimeout(completedPlanClearTimer);
|
|
153
167
|
completedPlanClearTimer = undefined;
|
|
@@ -282,41 +296,10 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
282
296
|
};
|
|
283
297
|
|
|
284
298
|
|
|
285
|
-
pi.registerTool({
|
|
286
|
-
name: "sync_workflow_plan",
|
|
287
|
-
label: "Sync Workflow Plan",
|
|
288
|
-
description: "Bind and display an active workflow's compatible persisted task plan at its exact checkpoint revision. Record later transitions with update_plan's workflow field.",
|
|
289
|
-
parameters: Type.Object({
|
|
290
|
-
path: Type.String({ description: "Absolute path to .resolve-issues/rush/<run-id>/task-plan.json" }),
|
|
291
|
-
revision: Type.Integer({ minimum: 0, description: "Persisted planRevision to display" }),
|
|
292
|
-
}),
|
|
293
|
-
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
294
|
-
const owner = workflowPlanOwner(ctx);
|
|
295
|
-
if (!owner) throw new Error("Only an active workflow can sync its plan.");
|
|
296
|
-
const plan = readRushPlan(params.path, ctx.cwd);
|
|
297
|
-
if (plan.planRevision !== params.revision) {
|
|
298
|
-
rushPlan = null;
|
|
299
|
-
rushError = `revision mismatch: expected ${params.revision}, found ${plan.planRevision}`;
|
|
300
|
-
refresh(true);
|
|
301
|
-
throw new Error(`Rush plan ${rushError}.`);
|
|
302
|
-
}
|
|
303
|
-
const binding: WorkflowPlanBinding = { owner, path: params.path, runId: plan.runId };
|
|
304
|
-
if (!rushBinding || rushBinding.owner !== owner || rushBinding.path !== binding.path || rushBinding.runId !== binding.runId) {
|
|
305
|
-
pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "set", ...binding });
|
|
306
|
-
}
|
|
307
|
-
displayedWorkflowOwner = owner;
|
|
308
|
-
rushBinding = binding;
|
|
309
|
-
rushPlan = plan;
|
|
310
|
-
rushError = null;
|
|
311
|
-
refresh(true);
|
|
312
|
-
return { content: [{ type: "text", text: `Showing ${owner} rev ${plan.planRevision}: ${plan.issues.length} units.` }], details: { runId: plan.runId, revision: plan.planRevision } };
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
|
|
316
299
|
pi.registerTool({
|
|
317
300
|
name: "update_plan",
|
|
318
301
|
label: "Update Plan",
|
|
319
|
-
description: "
|
|
302
|
+
description: "Universal plan interface: create or replace a generic checklist, bind or reload a workflow plan with workflow.path and revision, or save a workflow transition with event and changes. Every successful call refreshes the displayed plan.",
|
|
320
303
|
promptSnippet: "Create and update a persistent structured execution plan",
|
|
321
304
|
promptGuidelines: [
|
|
322
305
|
"Use update_plan for work with three or more meaningful steps unless a skill owns planning; update it immediately when a step completes, becomes blocked, or scope changes.",
|
|
@@ -325,26 +308,47 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
325
308
|
"For generic plans, follow the active delegation mode. Manual forbids proactive delegation even in plan mode; adaptive favors substantial independent work; coordinator delegates nontrivial role-owned tasks after agents_catalog discovery.",
|
|
326
309
|
"Use a generic plan as a milestone ledger only when no workflow owns planning. Otherwise follow the workflow's task plan and foreground role.",
|
|
327
310
|
"For generic plans, use separate steps for distinct deliverables, not one step per worker process. Before completing verification or the plan, inspect and integrate every relevant delegated result or failure.",
|
|
328
|
-
"While a workflow owns
|
|
311
|
+
"While a workflow owns planning, bind its persisted task plan with update_plan({workflow:{path,revision}}). Then record every transition with update_plan's workflow event and changes instead of editing task-plan.json or its profiling log; it saves the rows, the next planRevision, and the matching profiling event together. Path and revision can accompany the first transition to bind and update in one call.",
|
|
329
312
|
],
|
|
330
313
|
parameters: UpdatePlanSchema,
|
|
331
314
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
332
315
|
const owner = workflowPlanOwner(ctx);
|
|
333
|
-
const input = params as { explanation?: string; plan?: PlanStepInput[]; workflow?:
|
|
316
|
+
const input = params as { explanation?: string; plan?: PlanStepInput[]; workflow?: WorkflowPlanInput };
|
|
334
317
|
if (input.workflow !== undefined) {
|
|
335
318
|
if (!owner) throw new Error("No workflow owns the task plan; send plan instead of workflow.");
|
|
336
319
|
if (input.plan !== undefined) throw new Error("Send either plan or workflow, not both.");
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
320
|
+
const { path, ...transition } = input.workflow;
|
|
321
|
+
if (transition.event === undefined && (transition.changes !== undefined || transition.decision !== undefined || transition.profiling !== undefined)) {
|
|
322
|
+
throw new Error("workflow.event is required when sending changes, decision, or profiling.");
|
|
323
|
+
}
|
|
324
|
+
let binding = rushBinding?.owner === owner ? rushBinding : null;
|
|
325
|
+
if (path !== undefined) {
|
|
326
|
+
if (transition.revision === undefined) throw new Error("workflow.revision is required with workflow.path.");
|
|
327
|
+
const plan = readRushPlan(path, ctx.cwd);
|
|
328
|
+
if (plan.planRevision !== transition.revision) {
|
|
329
|
+
if (transition.event === undefined) {
|
|
330
|
+
rushPlan = null;
|
|
331
|
+
rushError = `revision mismatch: expected ${transition.revision}, found ${plan.planRevision}`;
|
|
332
|
+
refresh(true);
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`Rush plan revision mismatch: expected ${transition.revision}, found ${plan.planRevision}.`);
|
|
335
|
+
}
|
|
336
|
+
binding = { owner, path, runId: plan.runId };
|
|
337
|
+
if (transition.event === undefined) {
|
|
338
|
+
displayWorkflowPlan(plan, binding);
|
|
339
|
+
return { content: [{ type: "text", text: `Showing ${owner} rev ${plan.planRevision}: ${plan.issues.length} units.` }], details: { ok: true, runId: plan.runId, revision: plan.planRevision } };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!binding) throw new Error(`No ${owner} plan is bound; send workflow.path and workflow.revision to update_plan first.`);
|
|
343
|
+
if (transition.event === undefined) throw new Error("workflow.path is required to bind or reload a plan; otherwise send workflow.event and changes.");
|
|
344
|
+
const result = applyRushPlanUpdate(binding.path, ctx.cwd, binding.runId, { ...transition, event: transition.event });
|
|
345
|
+
displayWorkflowPlan(result.plan, binding);
|
|
342
346
|
const rows = result.changed.map(({ target, id, added }) =>
|
|
343
347
|
`${added ? "+" : ""}${id === null ? "run" : target === "unit" ? `#${id}` : id}`);
|
|
344
348
|
const logNote = result.logAheadRevision === undefined ? ""
|
|
345
349
|
: ` The profiling log already held rev ${result.logAheadRevision} (an earlier write stopped after logging); this event supersedes it.`;
|
|
346
350
|
return {
|
|
347
|
-
content: [{ type: "text", text: `Saved ${owner} rev ${result.revision} (${
|
|
351
|
+
content: [{ type: "text", text: `Saved ${owner} rev ${result.revision} (${transition.event.trim()})${rows.length ? `: ${rows.join(", ")}` : ""}${input.workflow.decision ? `${rows.length ? ";" : ":"} decision ${input.workflow.decision.id}` : ""}. Profiling: ${result.profilingPath}.${logNote}` }],
|
|
348
352
|
details: {
|
|
349
353
|
ok: true, runId: result.plan.runId, revision: result.revision, changed: result.changed, profilingPath: result.profilingPath,
|
|
350
354
|
...(result.logAheadRevision === undefined ? {} : { logAheadRevision: result.logAheadRevision }),
|
|
@@ -352,7 +356,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
352
356
|
};
|
|
353
357
|
}
|
|
354
358
|
if (owner) {
|
|
355
|
-
throw new Error(`${owner} owns the task plan. Send workflow
|
|
359
|
+
throw new Error(`${owner} owns the task plan. Send workflow instead of plan; include path and revision to bind its persisted task plan.`);
|
|
356
360
|
}
|
|
357
361
|
if (input.plan === undefined) throw new Error("plan is required: send the full list of steps.");
|
|
358
362
|
const plan = replacePlan(currentPlan, input.plan, input.explanation);
|
|
@@ -366,7 +370,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
366
370
|
renderCall(args, theme) {
|
|
367
371
|
return new Text(
|
|
368
372
|
theme.fg("toolTitle", theme.bold("update_plan ")) + theme.fg("muted", args.workflow
|
|
369
|
-
? `workflow ${args.workflow.event ?? ""}
|
|
373
|
+
? `workflow ${args.workflow.event ?? "bind"}`
|
|
370
374
|
: `${args.plan?.length ?? 0} steps`),
|
|
371
375
|
0,
|
|
372
376
|
0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"pi-better-background-tasks": "0.7.0",
|
|
57
57
|
"pi-better-goal": "0.8.1",
|
|
58
|
-
"pi-better-plan": "0.
|
|
58
|
+
"pi-better-plan": "0.7.0",
|
|
59
59
|
"pi-better-sandbox": "0.11.0",
|
|
60
60
|
"pi-better-ssh": "0.1.2",
|
|
61
61
|
"pi-better-subagents": "0.15.0",
|