pi-better-harness 0.17.0 → 0.17.1
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.
|
@@ -36,7 +36,7 @@ For a DAG, assign stable ids to prerequisite steps and list those ids in depende
|
|
|
36
36
|
|
|
37
37
|
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.
|
|
38
38
|
|
|
39
|
-
For `rush-issues
|
|
39
|
+
For any workflow using the shared task-plan contract (including `resolve-issues` and `rush-issues`), call `sync_workflow_plan` once with the absolute `.resolve-issues/rush/<run-id>/task-plan.json` path and its persisted `planRevision`. This binds the run to the session and shows its fleet stages and units in the widget, `/plan`, and `get_plan`; the binding survives Pi session resume, and ownership release restores the prior generic checklist. After that, record every transition with `update_plan` and a `workflow` object instead of editing `task-plan.json` or the profiling log by hand:
|
|
40
40
|
|
|
41
41
|
```json
|
|
42
42
|
{ "workflow": {
|
|
@@ -50,7 +50,9 @@ For `rush-issues`, call `sync_workflow_plan` once with the absolute `.resolve-is
|
|
|
50
50
|
} }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
`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
|
|
53
|
+
`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`.
|
|
54
|
+
|
|
55
|
+
The binding records the active workflow's name, which is shown in the plan view. A different workflow cannot read or update that binding; a new invocation or ownership release invalidates it. The skill name is not an allowlist: explicit binding opts a workflow into the shared schema and transition rules, while path confinement to `.resolve-issues/rush/<run-id>/task-plan.json`, run identity, size limits, and revision checks still apply. Workflows using other schemas or storage locations retain their own planning and must not bind incompatible state.
|
|
54
56
|
|
|
55
57
|
## Plan Display
|
|
56
58
|
|
|
@@ -134,7 +134,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
134
134
|
const refresh = (force = false): void => refreshWidget?.(force);
|
|
135
135
|
|
|
136
136
|
const loadRushPlan = (ctx: ExtensionContext): RushPlan | null => {
|
|
137
|
-
if (!rushBinding || workflowPlanOwner(ctx) !==
|
|
137
|
+
if (!rushBinding || workflowPlanOwner(ctx) !== rushBinding.owner) return null;
|
|
138
138
|
try {
|
|
139
139
|
const plan = readRushPlan(rushBinding.path, ctx.cwd);
|
|
140
140
|
if (plan.runId !== rushBinding.runId) throw new Error("Rush run identity changed.");
|
|
@@ -181,7 +181,8 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
181
181
|
const state = reconstructPlanState(ctx.sessionManager.getBranch());
|
|
182
182
|
currentPlan = state.plan;
|
|
183
183
|
displayedWorkflowOwner = workflowPlanOwner(ctx);
|
|
184
|
-
|
|
184
|
+
const binding = workflowBinding(ctx.sessionManager.getBranch());
|
|
185
|
+
rushBinding = binding?.owner === displayedWorkflowOwner ? binding : null;
|
|
185
186
|
rushPlan = null;
|
|
186
187
|
rushError = null;
|
|
187
188
|
if (rushBinding) loadRushPlan(ctx);
|
|
@@ -213,13 +214,13 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
213
214
|
refreshWidget = localRefresh;
|
|
214
215
|
return {
|
|
215
216
|
render(width: number): string[] {
|
|
216
|
-
if (displayMode === "hidden") return [];
|
|
217
|
+
if (displayMode === "hidden" || displayedWorkflowOwner !== workflowPlanOwner(ctx)) return [];
|
|
217
218
|
if (displayedWorkflowOwner) {
|
|
218
|
-
if (
|
|
219
|
+
if (rushBinding?.owner !== displayedWorkflowOwner) return [];
|
|
219
220
|
const fg = typeof theme?.fg === "function"
|
|
220
221
|
? (color: string, value: string) => theme.fg(color as never, value)
|
|
221
222
|
: undefined;
|
|
222
|
-
return rushPlan ? ["", ...renderRushPlan(rushPlan, width, false, fg)] : [];
|
|
223
|
+
return rushPlan ? ["", ...renderRushPlan(rushPlan, width, false, fg, displayedWorkflowOwner)] : [];
|
|
223
224
|
}
|
|
224
225
|
if (!currentPlan) return [];
|
|
225
226
|
return ["", ...renderCompactPlan(currentPlan, width, theme as never)];
|
|
@@ -237,20 +238,20 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
237
238
|
const showFullPlan = async (ctx: ExtensionContext): Promise<void> => {
|
|
238
239
|
const owner = workflowPlanOwner(ctx);
|
|
239
240
|
if (owner) {
|
|
240
|
-
const plan =
|
|
241
|
+
const plan = loadRushPlan(ctx);
|
|
241
242
|
if (!plan) {
|
|
242
|
-
ctx.ui.notify(owner
|
|
243
|
+
ctx.ui.notify(`${owner} plan unavailable: ${rushError ?? "not bound"}`, "warning");
|
|
243
244
|
return;
|
|
244
245
|
}
|
|
245
246
|
if (ctx.mode !== "tui") {
|
|
246
|
-
ctx.ui.notify(renderRushPlan(plan, 160, true).join("\n"), "info");
|
|
247
|
+
ctx.ui.notify(renderRushPlan(plan, 160, true, undefined, owner).join("\n"), "info");
|
|
247
248
|
return;
|
|
248
249
|
}
|
|
249
250
|
await ctx.ui.custom<void>((tui, theme, _keys, done) => {
|
|
250
251
|
const fg = typeof theme?.fg === "function"
|
|
251
252
|
? (color: string, value: string) => theme.fg(color as never, value)
|
|
252
253
|
: undefined;
|
|
253
|
-
const component = createRushPlanComponent(plan, () => done(), fg);
|
|
254
|
+
const component = createRushPlanComponent(plan, () => done(), fg, owner);
|
|
254
255
|
return { render: (width) => component.render(width), handleInput(data) {
|
|
255
256
|
component.handleInput?.(data);
|
|
256
257
|
tui.requestRender();
|
|
@@ -284,13 +285,14 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
284
285
|
pi.registerTool({
|
|
285
286
|
name: "sync_workflow_plan",
|
|
286
287
|
label: "Sync Workflow Plan",
|
|
287
|
-
description: "Bind and display
|
|
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.",
|
|
288
289
|
parameters: Type.Object({
|
|
289
290
|
path: Type.String({ description: "Absolute path to .resolve-issues/rush/<run-id>/task-plan.json" }),
|
|
290
291
|
revision: Type.Integer({ minimum: 0, description: "Persisted planRevision to display" }),
|
|
291
292
|
}),
|
|
292
293
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
293
|
-
|
|
294
|
+
const owner = workflowPlanOwner(ctx);
|
|
295
|
+
if (!owner) throw new Error("Only an active workflow can sync its plan.");
|
|
294
296
|
const plan = readRushPlan(params.path, ctx.cwd);
|
|
295
297
|
if (plan.planRevision !== params.revision) {
|
|
296
298
|
rushPlan = null;
|
|
@@ -298,22 +300,23 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
298
300
|
refresh(true);
|
|
299
301
|
throw new Error(`Rush plan ${rushError}.`);
|
|
300
302
|
}
|
|
301
|
-
const binding: WorkflowPlanBinding = { owner
|
|
302
|
-
if (!rushBinding || rushBinding.path !== binding.path || rushBinding.runId !== binding.runId) {
|
|
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) {
|
|
303
305
|
pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "set", ...binding });
|
|
304
306
|
}
|
|
307
|
+
displayedWorkflowOwner = owner;
|
|
305
308
|
rushBinding = binding;
|
|
306
309
|
rushPlan = plan;
|
|
307
310
|
rushError = null;
|
|
308
311
|
refresh(true);
|
|
309
|
-
return { content: [{ type: "text", text: `Showing
|
|
312
|
+
return { content: [{ type: "text", text: `Showing ${owner} rev ${plan.planRevision}: ${plan.issues.length} units.` }], details: { runId: plan.runId, revision: plan.planRevision } };
|
|
310
313
|
},
|
|
311
314
|
});
|
|
312
315
|
|
|
313
316
|
pi.registerTool({
|
|
314
317
|
name: "update_plan",
|
|
315
318
|
label: "Update Plan",
|
|
316
|
-
description: "Create or atomically replace the current structured execution plan and its step statuses. While
|
|
319
|
+
description: "Create or atomically replace the current structured execution plan and its step statuses. While a workflow owns a bound plan, send workflow instead of plan to save one task-plan transition.",
|
|
317
320
|
promptSnippet: "Create and update a persistent structured execution plan",
|
|
318
321
|
promptGuidelines: [
|
|
319
322
|
"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.",
|
|
@@ -322,7 +325,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
322
325
|
"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.",
|
|
323
326
|
"Use a generic plan as a milestone ledger only when no workflow owns planning. Otherwise follow the workflow's task plan and foreground role.",
|
|
324
327
|
"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.",
|
|
325
|
-
"While
|
|
328
|
+
"While a workflow owns the plan and it is bound with sync_workflow_plan, record every transition with update_plan's workflow field instead of editing task-plan.json or its profiling log; it saves the rows, the next planRevision, and the matching profiling event together.",
|
|
326
329
|
],
|
|
327
330
|
parameters: UpdatePlanSchema,
|
|
328
331
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -330,9 +333,8 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
330
333
|
const input = params as { explanation?: string; plan?: PlanStepInput[]; workflow?: RushPlanUpdate };
|
|
331
334
|
if (input.workflow !== undefined) {
|
|
332
335
|
if (!owner) throw new Error("No workflow owns the task plan; send plan instead of workflow.");
|
|
333
|
-
if (owner !== "rush-issues") throw new Error(`${owner} owns the task plan and does not accept update_plan workflow changes.`);
|
|
334
336
|
if (input.plan !== undefined) throw new Error("Send either plan or workflow, not both.");
|
|
335
|
-
if (!rushBinding) throw new Error(
|
|
337
|
+
if (!rushBinding || rushBinding.owner !== owner) throw new Error(`No ${owner} plan is bound; call sync_workflow_plan with the task-plan.json path first.`);
|
|
336
338
|
const result = applyRushPlanUpdate(rushBinding.path, ctx.cwd, rushBinding.runId, input.workflow);
|
|
337
339
|
rushPlan = result.plan;
|
|
338
340
|
rushError = null;
|
|
@@ -342,7 +344,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
342
344
|
const logNote = result.logAheadRevision === undefined ? ""
|
|
343
345
|
: ` The profiling log already held rev ${result.logAheadRevision} (an earlier write stopped after logging); this event supersedes it.`;
|
|
344
346
|
return {
|
|
345
|
-
content: [{ type: "text", text: `Saved
|
|
347
|
+
content: [{ type: "text", text: `Saved ${owner} rev ${result.revision} (${input.workflow.event.trim()})${rows.length ? `: ${rows.join(", ")}` : ""}${input.workflow.decision ? `${rows.length ? ";" : ":"} decision ${input.workflow.decision.id}` : ""}. Profiling: ${result.profilingPath}.${logNote}` }],
|
|
346
348
|
details: {
|
|
347
349
|
ok: true, runId: result.plan.runId, revision: result.revision, changed: result.changed, profilingPath: result.profilingPath,
|
|
348
350
|
...(result.logAheadRevision === undefined ? {} : { logAheadRevision: result.logAheadRevision }),
|
|
@@ -350,9 +352,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
350
352
|
};
|
|
351
353
|
}
|
|
352
354
|
if (owner) {
|
|
353
|
-
throw new Error(owner
|
|
354
|
-
? "rush-issues owns the task plan. Send workflow (bound with sync_workflow_plan) instead of plan."
|
|
355
|
-
: `${owner} owns the task plan. Update its workflow plan instead of update_plan.`);
|
|
355
|
+
throw new Error(`${owner} owns the task plan. Send workflow (bound with sync_workflow_plan) instead of plan.`);
|
|
356
356
|
}
|
|
357
357
|
if (input.plan === undefined) throw new Error("plan is required: send the full list of steps.");
|
|
358
358
|
const plan = replacePlan(currentPlan, input.plan, input.explanation);
|
|
@@ -387,10 +387,10 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
387
387
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
388
388
|
const owner = workflowPlanOwner(ctx);
|
|
389
389
|
if (owner) {
|
|
390
|
-
const workflowPlan =
|
|
390
|
+
const workflowPlan = loadRushPlan(ctx);
|
|
391
391
|
return {
|
|
392
392
|
content: [{ type: "text", text: workflowPlan
|
|
393
|
-
? renderRushPlan(workflowPlan, 160, true).join("\n")
|
|
393
|
+
? renderRushPlan(workflowPlan, 160, true, undefined, owner).join("\n")
|
|
394
394
|
: `${owner} owns the task plan; ${rushError ?? "consult its persisted workflow state"}.` }],
|
|
395
395
|
details: { hasPlan: !!workflowPlan, plan: workflowPlan, progress: null, workflowOwner: owner },
|
|
396
396
|
};
|
|
@@ -28,7 +28,7 @@ export interface RushPlan {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface WorkflowPlanBinding {
|
|
31
|
-
owner:
|
|
31
|
+
owner: string;
|
|
32
32
|
path: string;
|
|
33
33
|
runId: string;
|
|
34
34
|
}
|
|
@@ -113,9 +113,9 @@ export function workflowBinding(entries: Iterable<{ type: string; customType?: s
|
|
|
113
113
|
const data = entry.data;
|
|
114
114
|
if (!isRecord(data) || data.version !== 1) continue;
|
|
115
115
|
if (data.kind === "clear") binding = null;
|
|
116
|
-
else if (data.kind === "set" && data.owner === "
|
|
116
|
+
else if (data.kind === "set" && typeof data.owner === "string" && data.owner.length > 0 &&
|
|
117
117
|
typeof data.path === "string" && typeof data.runId === "string") {
|
|
118
|
-
binding = { owner:
|
|
118
|
+
binding = { owner: data.owner, path: data.path, runId: data.runId };
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
return binding;
|
|
@@ -133,7 +133,7 @@ function workflowStatus(status: string): DisplayStatus {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
function presentRushPlan(plan: RushPlan): PlanPresentation {
|
|
136
|
+
function presentRushPlan(plan: RushPlan, owner: string): PlanPresentation {
|
|
137
137
|
// The warehouse canary only applies to data work; hide it otherwise.
|
|
138
138
|
const stages = ["explore", "implement", "review", "ci", ...(plan.warehouseCanaryRequired ? ["canary"] : [])];
|
|
139
139
|
const fleet = stages.map((stage) => {
|
|
@@ -142,7 +142,7 @@ function presentRushPlan(plan: RushPlan): PlanPresentation {
|
|
|
142
142
|
const style = statusStyle(display);
|
|
143
143
|
return { color: style.color, text: ` ${stage} ${style.glyph}${display === "unknown" ? ` ${planText(status)}` : ""}` };
|
|
144
144
|
});
|
|
145
|
-
const metadata = [[{ color: "dim", text:
|
|
145
|
+
const metadata = [[{ color: "dim", text: `${planText(owner)} · rev ${plan.planRevision} ·` }, ...fleet]];
|
|
146
146
|
if (plan.spec?.title) metadata.push([{ color: "dim", text: plan.spec.title }]);
|
|
147
147
|
return {
|
|
148
148
|
metadata,
|
|
@@ -164,16 +164,18 @@ export function renderRushPlan(
|
|
|
164
164
|
width: number,
|
|
165
165
|
full = false,
|
|
166
166
|
fg?: (color: string, value: string) => string,
|
|
167
|
+
owner = "rush-issues",
|
|
167
168
|
): string[] {
|
|
168
169
|
const theme = fg ? { fg } : plainPlanTheme;
|
|
169
|
-
return renderPlanPresentation(presentRushPlan(plan), width, theme, full);
|
|
170
|
+
return renderPlanPresentation(presentRushPlan(plan, owner), width, theme, full);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
export function createRushPlanComponent(
|
|
173
174
|
plan: RushPlan,
|
|
174
175
|
onClose: () => void,
|
|
175
176
|
fg?: (color: string, value: string) => string,
|
|
177
|
+
owner = "rush-issues",
|
|
176
178
|
): Component {
|
|
177
179
|
const theme = fg ? { fg } : plainPlanTheme;
|
|
178
|
-
return createPlanPresentationComponent(presentRushPlan(plan), theme, onClose);
|
|
180
|
+
return createPlanPresentationComponent(presentRushPlan(plan, owner), theme, onClose);
|
|
179
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
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",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"pi-better-background-tasks": "0.6.3",
|
|
56
56
|
"pi-better-goal": "0.6.1",
|
|
57
|
-
"pi-better-plan": "0.5.
|
|
57
|
+
"pi-better-plan": "0.5.3",
|
|
58
58
|
"pi-better-sandbox": "0.9.0",
|
|
59
59
|
"pi-better-ssh": "0.1.1",
|
|
60
60
|
"pi-better-subagents": "0.13.0",
|