pi-better-harness 0.3.8 → 0.3.10
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/node_modules/pi-better-background-tasks/package.json +1 -1
- package/node_modules/pi-better-background-tasks/src/shared-navigator.ts +19 -7
- package/node_modules/pi-better-goal/README.md +13 -0
- package/node_modules/pi-better-goal/package.json +4 -1
- package/node_modules/pi-better-goal/src/index.ts +99 -6
- package/node_modules/pi-better-goal/src/workflow.ts +63 -0
- package/node_modules/pi-better-plan/README.md +8 -1
- package/node_modules/pi-better-plan/package.json +1 -1
- package/node_modules/pi-better-plan/src/index.ts +59 -12
- package/node_modules/pi-better-plan/src/plan-state.ts +50 -6
- package/node_modules/pi-better-plan/src/types.ts +3 -0
- package/node_modules/pi-better-subagents/index.ts +1 -1
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/shared-navigator.ts +19 -7
- package/package.json +6 -5
|
@@ -918,7 +918,7 @@ function createOverlayComponent(
|
|
|
918
918
|
width,
|
|
919
919
|
deps.truncate,
|
|
920
920
|
fg,
|
|
921
|
-
{ minRows: detailRows, bottomFooter: false, logTailRows },
|
|
921
|
+
{ minRows: detailRows, maxRows: detailRows, bottomFooter: false, logTailRows },
|
|
922
922
|
);
|
|
923
923
|
} else {
|
|
924
924
|
transcriptDetail = null;
|
|
@@ -1003,21 +1003,33 @@ function buildTranscriptDetailLines(
|
|
|
1003
1003
|
width: number,
|
|
1004
1004
|
truncate: (s: string, width: number) => string,
|
|
1005
1005
|
fg: (color: string, value: string) => string,
|
|
1006
|
-
options: { minRows?: number; bottomFooter?: boolean; logTailRows?: number } = {},
|
|
1006
|
+
options: { minRows?: number; maxRows?: number; bottomFooter?: boolean; logTailRows?: number } = {},
|
|
1007
1007
|
): string[] {
|
|
1008
1008
|
const actions = [...(detail.footerActions ?? ["x close"]), "Esc close"].join(" · ");
|
|
1009
1009
|
const tailRows = options.logTailRows ?? DEFAULT_LOG_TAIL_ROWS;
|
|
1010
|
-
|
|
1010
|
+
let shownTranscriptLines = transcriptLines.length ? transcriptLines.slice(-tailRows) : [" (no transcript yet)"];
|
|
1011
1011
|
const lines: string[] = [
|
|
1012
1012
|
fg("accent", rule(detail.title, width)),
|
|
1013
1013
|
dim(` ← main · ${actions}`, fg),
|
|
1014
1014
|
"",
|
|
1015
1015
|
` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`,
|
|
1016
1016
|
];
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1017
|
+
const optionalMetadata = [
|
|
1018
|
+
...(detail.subtitle ? [` summary ${detail.subtitle}`] : []),
|
|
1019
|
+
...detail.metadata.map((item) => ` ${item.label.padEnd(8, " ").slice(0, 8)} ${item.value}`),
|
|
1020
|
+
];
|
|
1021
|
+
const transcriptHeader = ["", dim(section(`transcript · latest ${tailRows} rows`, width), fg)];
|
|
1022
|
+
if (detail.transcriptDiagnostic) transcriptHeader.push(` ${dim(detail.transcriptDiagnostic, fg)}`);
|
|
1023
|
+
if (options.maxRows !== undefined) {
|
|
1024
|
+
const fixedRows = lines.length + transcriptHeader.length + 1;
|
|
1025
|
+
const transcriptBudget = Math.max(1, options.maxRows - fixedRows);
|
|
1026
|
+
shownTranscriptLines = shownTranscriptLines.slice(-transcriptBudget);
|
|
1027
|
+
const metadataBudget = Math.max(0, options.maxRows - fixedRows - shownTranscriptLines.length);
|
|
1028
|
+
lines.push(...optionalMetadata.slice(0, metadataBudget));
|
|
1029
|
+
} else {
|
|
1030
|
+
lines.push(...optionalMetadata);
|
|
1031
|
+
}
|
|
1032
|
+
lines.push(...transcriptHeader);
|
|
1021
1033
|
lines.push(...shownTranscriptLines);
|
|
1022
1034
|
lines.push("");
|
|
1023
1035
|
if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
|
|
@@ -19,6 +19,19 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
19
19
|
- A progress-aware follow-up loop that holds after repeated identical outcomes.
|
|
20
20
|
- An observable-progress stall state for active goals.
|
|
21
21
|
|
|
22
|
+
## Skill-Owned Workflows
|
|
23
|
+
|
|
24
|
+
Skills that own execution and their own task plan can opt in through `SKILL.md` frontmatter:
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
metadata:
|
|
28
|
+
pi-better-plan-workflow: coordinator
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Invoke the skill with Pi's `/skill:name` command. The goal extension checks Pi's skill-command provenance, persists the owner on the session branch, and restores its instructions on resumed turns. It does not infer ownership from ordinary text or skill names. A goal such as `/goal /skill:name task` is rejected; invoke `/skill:name task` directly and use `/goal` only for a plain-language objective. Legacy slash-shaped goals pause on resume rather than running without the skill. `/workflow` shows the owner; call `release_workflow` after the workflow's completion audit (or use `/workflow clear` to release it manually). Completing an active goal also releases ownership.
|
|
32
|
+
|
|
33
|
+
When `pi-better-plan` is installed, it defers its prompt, checklist, and `update_plan` tool to a skill-owned task plan. Skills without this metadata retain normal goal and plan behavior. The skill itself owns its planning format and worker policy; the harness does not enumerate skills or impose a shared workflow schema.
|
|
34
|
+
|
|
22
35
|
## Install
|
|
23
36
|
|
|
24
37
|
```sh
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-goal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pi extension for goal tracking with background-aware continuation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -67,5 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=22.0.0"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"yaml": "^2.9.1"
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
} from "./goal-clock.js";
|
|
34
35
|
import { createRenderScheduler } from "./shared-render-scheduler.js";
|
|
35
36
|
import { collectSubagentActivity } from "./subagents.js";
|
|
37
|
+
import { currentWorkflowOwner, skillCommandName, workflowEntry, workflowOwnerFromSkill, WORKFLOW_ENTRY_TYPE } from "./workflow.js";
|
|
36
38
|
import {
|
|
37
39
|
EVENT_ACTIVITY,
|
|
38
40
|
EVENT_READY,
|
|
@@ -109,13 +111,15 @@ function wasTurnAborted(messages: readonly unknown[]): boolean {
|
|
|
109
111
|
return false;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function continuationPrompt(goal: GoalSnapshot): string {
|
|
114
|
+
function continuationPrompt(goal: GoalSnapshot, owner: ReturnType<typeof currentWorkflowOwner> = null): string {
|
|
113
115
|
return [
|
|
114
116
|
"Continue working toward the active thread goal.",
|
|
115
117
|
"",
|
|
116
118
|
`Goal: ${goal.objective}`,
|
|
117
119
|
"",
|
|
118
|
-
|
|
120
|
+
owner
|
|
121
|
+
? `Continue the ${owner.name} workflow as its coordinator. Its own task plan is authoritative; do not implement product code in the parent. Mark the goal complete only after the workflow's completion audit.`
|
|
122
|
+
: "Keep working through clear low-risk next steps. Do not stop at a plan. Mark the goal complete only after an evidence-backed completion audit proves no required work remains.",
|
|
119
123
|
].join("\n");
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -185,6 +189,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
185
189
|
let lastAgentEvidence: ContinuationEvidence | null = null;
|
|
186
190
|
|
|
187
191
|
const getGoal = (ctx: ExtensionContext): GoalSnapshot | null => currentGoalSnapshot(ctx);
|
|
192
|
+
const getWorkflow = (ctx: ExtensionContext) => currentWorkflowOwner(ctx.sessionManager.getBranch());
|
|
193
|
+
const workflowAvailable = (owner: NonNullable<ReturnType<typeof currentWorkflowOwner>>): boolean => {
|
|
194
|
+
const command = pi.getCommands?.().find((item) => item.name === `skill:${owner.name}` && item.source === "skill");
|
|
195
|
+
if (command?.sourceInfo.path !== owner.path) return false;
|
|
196
|
+
try {
|
|
197
|
+
return workflowOwnerFromSkill(owner.name, owner.path)?.planOwner === owner.planOwner;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const recordWorkflow = (owner: ReturnType<typeof currentWorkflowOwner>): void => {
|
|
203
|
+
pi.appendEntry(WORKFLOW_ENTRY_TYPE, workflowEntry(owner));
|
|
204
|
+
pi.events.emit("pi-better-workflow:changed", owner);
|
|
205
|
+
};
|
|
188
206
|
|
|
189
207
|
/** Only active goals receive autonomous pokes; paused, complete, and budget-limited goals never do. */
|
|
190
208
|
const isPokeable = (goal: GoalSnapshot | null): goal is GoalSnapshot => goal?.status === "active";
|
|
@@ -241,6 +259,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
241
259
|
const current = getGoal(ctx);
|
|
242
260
|
const wasVisible = current !== null && isGoalClockVisible(current);
|
|
243
261
|
pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
|
|
262
|
+
if (getWorkflow(ctx)) recordWorkflow(null);
|
|
244
263
|
continuationQueuedFor = null;
|
|
245
264
|
backgroundDrainTracker = null;
|
|
246
265
|
clearIdleContinuation();
|
|
@@ -267,7 +286,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
267
286
|
pi.sendMessage(
|
|
268
287
|
{
|
|
269
288
|
customType: EXTENSION_NAME,
|
|
270
|
-
content: continuationPrompt(goal),
|
|
289
|
+
content: continuationPrompt(goal, currentCtx ? getWorkflow(currentCtx) : null),
|
|
271
290
|
display: false,
|
|
272
291
|
details: { kind: "continuation", goalId: goal.goalId },
|
|
273
292
|
},
|
|
@@ -333,8 +352,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
333
352
|
continuationQueuedFor = goal.goalId;
|
|
334
353
|
const content = kind === "background-drained"
|
|
335
354
|
? "Background activity for the active goal is no longer running. Inspect any subagent callbacks or final results, then continue the completion audit before marking the goal complete.\n\n" +
|
|
336
|
-
continuationPrompt(goal)
|
|
337
|
-
: continuationPrompt(goal);
|
|
355
|
+
continuationPrompt(goal, getWorkflow(ctx))
|
|
356
|
+
: continuationPrompt(goal, getWorkflow(ctx));
|
|
338
357
|
pi.sendMessage(
|
|
339
358
|
{
|
|
340
359
|
customType: EXTENSION_NAME,
|
|
@@ -354,6 +373,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
354
373
|
ctx: ExtensionContext,
|
|
355
374
|
source: "command",
|
|
356
375
|
): GoalSnapshot => {
|
|
376
|
+
if (skillCommandName(objective)) {
|
|
377
|
+
throw new Error("A skill command cannot be a goal objective. Invoke /skill:name directly so Pi loads the skill, then set a plain-language goal if needed.");
|
|
378
|
+
}
|
|
357
379
|
const objectiveError = validateObjective(objective);
|
|
358
380
|
if (objectiveError) {
|
|
359
381
|
throw new Error(objectiveError);
|
|
@@ -566,6 +588,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
566
588
|
notifyGoal(ctx, "Only paused goals can be resumed.", "warning");
|
|
567
589
|
return;
|
|
568
590
|
}
|
|
591
|
+
if (skillCommandName(current.objective)) {
|
|
592
|
+
notifyGoal(ctx, "Invoke the skill directly; this legacy slash-command goal cannot resume as plain text.", "error");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
569
595
|
const goal = goalWithStatus(current, "active");
|
|
570
596
|
setGoal(goal, ctx, "command");
|
|
571
597
|
queueGoalContinuation(goal);
|
|
@@ -585,6 +611,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
585
611
|
return;
|
|
586
612
|
}
|
|
587
613
|
setGoal(goalWithStatus(current, "complete"), ctx, "command");
|
|
614
|
+
if (getWorkflow(ctx)) recordWorkflow(null);
|
|
588
615
|
notifyGoal(ctx, "Goal marked complete.");
|
|
589
616
|
return;
|
|
590
617
|
}
|
|
@@ -662,6 +689,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
662
689
|
}
|
|
663
690
|
const goal = goalWithStatus(current, "complete");
|
|
664
691
|
setGoal(goal, ctx, "tool");
|
|
692
|
+
if (getWorkflow(ctx)) recordWorkflow(null);
|
|
665
693
|
return {
|
|
666
694
|
content: [{ type: "text", text: "Goal marked complete." }],
|
|
667
695
|
details: { ok: true, goal },
|
|
@@ -678,6 +706,32 @@ export default function (pi: ExtensionAPI): void {
|
|
|
678
706
|
},
|
|
679
707
|
});
|
|
680
708
|
|
|
709
|
+
pi.registerCommand("workflow", {
|
|
710
|
+
description: "Inspect or clear the active skill-owned workflow",
|
|
711
|
+
handler: async (args, ctx) => {
|
|
712
|
+
if (args.trim() === "clear") {
|
|
713
|
+
recordWorkflow(null);
|
|
714
|
+
notifyGoal(ctx, "Workflow ownership cleared.");
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const owner = getWorkflow(ctx);
|
|
718
|
+
notifyGoal(ctx, owner ? `Workflow: ${owner.name} (${owner.path}); plan owned by workflow.` : "No workflow owns this session.");
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
pi.registerTool({
|
|
723
|
+
name: "release_workflow",
|
|
724
|
+
label: "Release Workflow",
|
|
725
|
+
description: "Release the active skill-owned workflow after its final handoff and completion audit.",
|
|
726
|
+
parameters: Type.Object({}),
|
|
727
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
728
|
+
const owner = getWorkflow(ctx);
|
|
729
|
+
if (!owner) return { content: [{ type: "text", text: "No workflow owns this session." }], details: { released: false } };
|
|
730
|
+
recordWorkflow(null);
|
|
731
|
+
return { content: [{ type: "text", text: `Released ${owner.name} workflow ownership.` }], details: { released: true, owner: owner.name } };
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
|
|
681
735
|
pi.registerTool({
|
|
682
736
|
name: "get_background_activity",
|
|
683
737
|
label: "Get Background Activity",
|
|
@@ -697,6 +751,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
697
751
|
pi.on("session_start", async (_event, ctx) => {
|
|
698
752
|
currentCtx = ctx;
|
|
699
753
|
foregroundRunning = !ctx.isIdle();
|
|
754
|
+
const restoredGoal = getGoal(ctx);
|
|
755
|
+
if (restoredGoal?.status === "active" && skillCommandName(restoredGoal.objective)) {
|
|
756
|
+
setGoal(goalWithStatus(restoredGoal, "paused"), ctx, "runtime");
|
|
757
|
+
notifyGoal(ctx, "Goal paused: invoke its skill directly before resuming.", "error");
|
|
758
|
+
}
|
|
700
759
|
pi.events.emit(EVENT_READY, { version: EXTENSION_VERSION });
|
|
701
760
|
installGoalWidget(ctx);
|
|
702
761
|
latestSnapshot = await publishSnapshot(ctx);
|
|
@@ -707,6 +766,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
707
766
|
if (event.source === "extension") {
|
|
708
767
|
return;
|
|
709
768
|
}
|
|
769
|
+
const skillName = skillCommandName(event.text ?? "");
|
|
770
|
+
if (skillName) {
|
|
771
|
+
const command = pi.getCommands?.().find((item) => item.name === `skill:${skillName}` && item.source === "skill");
|
|
772
|
+
if (command) {
|
|
773
|
+
try {
|
|
774
|
+
const owner = workflowOwnerFromSkill(skillName, command.sourceInfo.path);
|
|
775
|
+
if (owner) recordWorkflow(owner);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
notifyGoal(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
778
|
+
return { action: "handled" as const };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
710
782
|
const goal = getGoal(ctx);
|
|
711
783
|
if (goal?.status === "active") {
|
|
712
784
|
resetContinuationState(goal);
|
|
@@ -716,11 +788,32 @@ export default function (pi: ExtensionAPI): void {
|
|
|
716
788
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
717
789
|
currentCtx = ctx;
|
|
718
790
|
const goal = currentGoalSnapshot(ctx);
|
|
791
|
+
const owner = getWorkflow(ctx);
|
|
719
792
|
const snapshot = await publishSnapshot(ctx);
|
|
720
|
-
if (!isPokeable(goal)) {
|
|
793
|
+
if (!isPokeable(goal) && !owner) {
|
|
721
794
|
return;
|
|
722
795
|
}
|
|
723
796
|
|
|
797
|
+
if (owner) {
|
|
798
|
+
if (!workflowAvailable(owner)) {
|
|
799
|
+
if (isPokeable(goal)) setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
800
|
+
recordWorkflow(null);
|
|
801
|
+
return { systemPrompt: `${event.systemPrompt}\n\nWorkflow ${owner.name} is no longer a registered, valid skill. Stop work and ask the user to reinvoke the skill.` };
|
|
802
|
+
}
|
|
803
|
+
let instructions: string;
|
|
804
|
+
try {
|
|
805
|
+
instructions = readFileSync(owner.path, "utf8");
|
|
806
|
+
} catch {
|
|
807
|
+
if (isPokeable(goal)) setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
808
|
+
return { systemPrompt: `${event.systemPrompt}\n\nWorkflow ${owner.name} is unavailable. Stop work and ask the user to restore or reinvoke the skill.` };
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
systemPrompt: `${event.systemPrompt}\n\nActive workflow: ${owner.name} (${owner.path}). Its task plan owns planning and the parent is a coordinator, not a product-code implementer. Follow the workflow instructions below, including on resumed turns:\n\n${instructions}` +
|
|
812
|
+
(isPokeable(goal) ? `\n\nActive objective: ${goal.objective}. Complete it only after the workflow completion audit.` : ""),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (!isPokeable(goal)) return;
|
|
724
817
|
const backgroundInstruction = snapshot.backgroundRunning
|
|
725
818
|
? ` The goal still has delegated background work running (${summarizeActiveBackground(snapshot)}). Foreground idleness alone is not goal completion; keep any structured plan current and do not mark verification, the plan, or the goal complete until every relevant delegated task reaches a terminal state and its result or failure has been inspected and integrated.`
|
|
726
819
|
: "";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
|
|
4
|
+
export const WORKFLOW_ENTRY_TYPE = "pi-better-workflow";
|
|
5
|
+
|
|
6
|
+
export interface WorkflowOwner {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
role: "coordinator";
|
|
10
|
+
planOwner: "workflow";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SessionEntryLike {
|
|
14
|
+
type: string;
|
|
15
|
+
customType?: string;
|
|
16
|
+
data?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function workflowOwnerFromSkill(name: string, path: string): WorkflowOwner | null {
|
|
20
|
+
const source = readFileSync(path, "utf8");
|
|
21
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source);
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
const frontmatter: unknown = parse(match[1]!);
|
|
24
|
+
if (!frontmatter || typeof frontmatter !== "object") return null;
|
|
25
|
+
const metadata = (frontmatter as { metadata?: unknown }).metadata;
|
|
26
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
27
|
+
const fields = metadata as Record<string, unknown>;
|
|
28
|
+
if (fields["pi-better-plan-workflow"] === undefined) {
|
|
29
|
+
if (fields["pi-better-workflow-role"] !== undefined || fields["pi-better-plan-owner"] !== undefined) {
|
|
30
|
+
throw new Error(`Outdated workflow metadata in ${path}. Use pi-better-plan-workflow: coordinator.`);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (fields["pi-better-plan-workflow"] !== "coordinator") {
|
|
35
|
+
throw new Error(`Invalid workflow metadata in ${path}. Expected pi-better-plan-workflow: coordinator.`);
|
|
36
|
+
}
|
|
37
|
+
return { name, path, role: "coordinator", planOwner: "workflow" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function workflowEntry(owner: WorkflowOwner | null) {
|
|
41
|
+
return owner === null
|
|
42
|
+
? { version: 1, kind: "clear" as const }
|
|
43
|
+
: { version: 1, kind: "set" as const, owner };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function currentWorkflowOwner(entries: Iterable<SessionEntryLike>): WorkflowOwner | null {
|
|
47
|
+
let owner: WorkflowOwner | null = null;
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entry.type !== "custom" || entry.customType !== WORKFLOW_ENTRY_TYPE) continue;
|
|
50
|
+
const data = entry.data as { version?: unknown; kind?: unknown; owner?: Partial<WorkflowOwner> } | null;
|
|
51
|
+
if (data?.version !== 1) continue;
|
|
52
|
+
if (data.kind === "clear") owner = null;
|
|
53
|
+
else if (data.kind === "set" && typeof data.owner?.name === "string" &&
|
|
54
|
+
typeof data.owner.path === "string" && data.owner.role === "coordinator" && data.owner.planOwner === "workflow") {
|
|
55
|
+
owner = data.owner as WorkflowOwner;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return owner;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function skillCommandName(text: string): string | null {
|
|
62
|
+
return /^\/skill:([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s|$)/.exec(text.trim())?.[1] ?? null;
|
|
63
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
## What It Does
|
|
6
6
|
|
|
7
7
|
- Gives models `update_plan` and `get_plan` tools for atomic, explicit progress updates.
|
|
8
|
+
- Supports dependency edges (`id` and `dependsOn`) so independent ready steps can run concurrently.
|
|
8
9
|
- Shows the complete checklist of completed, active, pending, and blocked steps above the editor.
|
|
9
10
|
- Persists plan state and display preferences on the active Pi session branch.
|
|
10
11
|
- Keeps a completed plan visible for 30 seconds, then clears it automatically.
|
|
@@ -16,7 +17,13 @@ Plan progress is checklist progress, not an estimate of effort. The extension ne
|
|
|
16
17
|
|
|
17
18
|
Use the plan as the foreground coordinator's milestone ledger. Delegate independent, sufficiently substantial work early with subagents, and use background tasks for long-running processes or repeated checks. Keep doing unblocked foreground work after launch; do not poll workers.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
Before the first implementation milestone, check for an independent task that can run alongside foreground work. Launch a bounded subagent task when available; otherwise state the specific dependency or shared-worktree constraint that rules delegation out. The plan records milestones, not worker scheduling.
|
|
21
|
+
|
|
22
|
+
Independent foreground and delegated milestones may both be `in_progress`. Use steps for distinct deliverables, not individual worker processes; worker tools and the background-work navigator own run status. Complete verification and the plan only after every relevant delegated task is terminal and its result or failure has been inspected and integrated.
|
|
23
|
+
|
|
24
|
+
For a DAG, assign stable ids to prerequisite steps and list those ids in dependent steps' `dependsOn`. Dependencies must exist in the same plan; cycles and starting or completing a step before its prerequisites are complete are rejected. `get_plan` reports pending steps whose prerequisites are complete as ready. Plans without edges keep their existing behavior.
|
|
25
|
+
|
|
26
|
+
When an explicitly invoked skill declares `pi-better-plan-workflow: coordinator` in its metadata, that skill's task plan takes precedence. The generic checklist stays persisted but is hidden, and `update_plan` refuses competing updates until workflow ownership is released. Rush-style workflows can use their own dependency graph without copying it into this plan.
|
|
20
27
|
|
|
21
28
|
## Install
|
|
22
29
|
|
|
@@ -28,8 +28,10 @@ interface GetPlanDetails {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const PlanStepSchema = Type.Object({
|
|
31
|
+
id: Type.Optional(Type.String({ description: "Stable step id used by dependsOn (letters, digits, underscore, hyphen; max 64 characters)." })),
|
|
31
32
|
step: Type.String({ description: "A concise, verifiable execution step." }),
|
|
32
33
|
status: StringEnum(["pending", "in_progress", "completed", "blocked"] as const),
|
|
34
|
+
dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Ids of prerequisite steps in this plan. All must be completed before this step starts." })),
|
|
33
35
|
});
|
|
34
36
|
|
|
35
37
|
const UpdatePlanSchema = Type.Object({
|
|
@@ -39,11 +41,31 @@ const UpdatePlanSchema = Type.Object({
|
|
|
39
41
|
|
|
40
42
|
const LEGACY_PLAN_NAV_STATUS_KEY = "pi-better-plan-nav";
|
|
41
43
|
|
|
44
|
+
function workflowPlanOwner(ctx: ExtensionContext): string | null {
|
|
45
|
+
let owner: string | null = null;
|
|
46
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
47
|
+
if (entry.type !== "custom" || entry.customType !== "pi-better-workflow") continue;
|
|
48
|
+
const data = entry.data as { version?: unknown; kind?: unknown; owner?: { name?: unknown; planOwner?: unknown } } | null;
|
|
49
|
+
if (data?.version !== 1) continue;
|
|
50
|
+
if (data.kind === "clear") owner = null;
|
|
51
|
+
else if (data.kind === "set" && data.owner?.planOwner === "workflow" && typeof data.owner.name === "string") {
|
|
52
|
+
owner = data.owner.name;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return owner;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
export default function planExtension(pi: ExtensionAPI): void {
|
|
43
59
|
let currentPlan: PlanSnapshot | null = null;
|
|
44
60
|
let displayMode: PlanDisplayMode = "auto";
|
|
61
|
+
let displayedWorkflowOwner: string | null = null;
|
|
45
62
|
let refreshWidget: ((force?: boolean) => void) | undefined;
|
|
46
63
|
let completedPlanClearTimer: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
+
const unsubscribeWorkflow = pi.events.on("pi-better-workflow:changed", (owner) => {
|
|
65
|
+
displayedWorkflowOwner = owner && typeof owner === "object" && "name" in owner && typeof owner.name === "string"
|
|
66
|
+
? owner.name : null;
|
|
67
|
+
refreshWidget?.(true);
|
|
68
|
+
});
|
|
47
69
|
|
|
48
70
|
const refresh = (force = false): void => refreshWidget?.(force);
|
|
49
71
|
|
|
@@ -79,6 +101,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
79
101
|
cancelCompletedPlanClear();
|
|
80
102
|
const state = reconstructPlanState(ctx.sessionManager.getBranch());
|
|
81
103
|
currentPlan = state.plan;
|
|
104
|
+
displayedWorkflowOwner = workflowPlanOwner(ctx);
|
|
82
105
|
displayMode = state.displayMode;
|
|
83
106
|
if (ctx.hasUI) ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
|
|
84
107
|
refresh(true);
|
|
@@ -107,7 +130,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
107
130
|
refreshWidget = localRefresh;
|
|
108
131
|
return {
|
|
109
132
|
render(width: number): string[] {
|
|
110
|
-
if (!currentPlan || displayMode === "hidden") return [];
|
|
133
|
+
if (!currentPlan || displayMode === "hidden" || displayedWorkflowOwner) return [];
|
|
111
134
|
return renderCompactPlan(currentPlan, width, theme as never);
|
|
112
135
|
},
|
|
113
136
|
invalidate() {},
|
|
@@ -150,13 +173,17 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
150
173
|
description: "Create or atomically replace the current structured execution plan and its step statuses.",
|
|
151
174
|
promptSnippet: "Create and update a persistent structured execution plan",
|
|
152
175
|
promptGuidelines: [
|
|
153
|
-
"Use update_plan for work with three or more meaningful steps
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
176
|
+
"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.",
|
|
177
|
+
"For generic plans only, mark independent foreground and delegated milestones in_progress concurrently when work is actually underway. Do not mark a delegated step completed until its result or failure has been inspected and integrated.",
|
|
178
|
+
"For generic plans with prerequisites, give steps stable ids and dependsOn edges. Start only ready steps whose prerequisites are completed; independent ready steps may run in parallel, including in subagents.",
|
|
179
|
+
"For generic plans only, before the first implementation milestone identify independent substantial work. Launch a bounded subagent task while you continue another, or state the concrete reason delegation is unsuitable.",
|
|
180
|
+
"Use a generic plan as the foreground coordinator's milestone ledger only when no workflow owns planning. Otherwise follow the workflow's task plan and foreground role.",
|
|
181
|
+
"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.",
|
|
157
182
|
],
|
|
158
183
|
parameters: UpdatePlanSchema,
|
|
159
|
-
async execute(_toolCallId, params) {
|
|
184
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
185
|
+
const owner = workflowPlanOwner(ctx);
|
|
186
|
+
if (owner) throw new Error(`${owner} owns the task plan. Update its workflow plan instead of update_plan.`);
|
|
160
187
|
const input = params as { explanation?: string; plan: PlanStepInput[] };
|
|
161
188
|
const plan = replacePlan(currentPlan, input.plan, input.explanation);
|
|
162
189
|
persistPlan(plan);
|
|
@@ -185,7 +212,12 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
185
212
|
description: "Inspect the current structured execution plan and derived checklist progress.",
|
|
186
213
|
promptSnippet: "Inspect the current structured execution plan and progress",
|
|
187
214
|
parameters: Type.Object({}),
|
|
188
|
-
async execute() {
|
|
215
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
216
|
+
const owner = workflowPlanOwner(ctx);
|
|
217
|
+
if (owner) return {
|
|
218
|
+
content: [{ type: "text", text: `${owner} owns the task plan; consult its persisted workflow state.` }],
|
|
219
|
+
details: { hasPlan: false, plan: null, progress: null, workflowOwner: owner },
|
|
220
|
+
};
|
|
189
221
|
if (!currentPlan) {
|
|
190
222
|
return {
|
|
191
223
|
content: [{ type: "text", text: "No plan is set." }],
|
|
@@ -235,7 +267,8 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
235
267
|
pi.on("session_tree", async (_event, ctx) => {
|
|
236
268
|
restore(ctx);
|
|
237
269
|
});
|
|
238
|
-
pi.on("before_agent_start", async (event) => {
|
|
270
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
271
|
+
if (workflowPlanOwner(ctx)) return;
|
|
239
272
|
if (!currentPlan) return;
|
|
240
273
|
return {
|
|
241
274
|
systemPrompt: `${event.systemPrompt}\n\n${planPrompt(currentPlan)}`,
|
|
@@ -243,6 +276,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
243
276
|
});
|
|
244
277
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
245
278
|
cancelCompletedPlanClear();
|
|
279
|
+
if (typeof unsubscribeWorkflow === "function") unsubscribeWorkflow();
|
|
246
280
|
refreshWidget = undefined;
|
|
247
281
|
try {
|
|
248
282
|
ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
|
|
@@ -256,24 +290,37 @@ function formatProgress(plan: PlanSnapshot): string {
|
|
|
256
290
|
const current = progress.activeIndex === null ? null : plan.steps[progress.activeIndex];
|
|
257
291
|
const status = [`Plan updated: ${progress.completed}/${progress.total} steps completed`];
|
|
258
292
|
if (progress.blocked > 0) status.push(`${progress.blocked} blocked`);
|
|
259
|
-
if (
|
|
293
|
+
if (progress.inProgress > 1) status.push(`${progress.inProgress} steps in progress`);
|
|
294
|
+
else if (current) status.push(`Current step: ${current.step}`);
|
|
295
|
+
if (plan.steps.some((item) => item.dependsOn?.length)) status.push(`${progress.readyIndices.length} ready`);
|
|
260
296
|
return status.join(". ") + ".";
|
|
261
297
|
}
|
|
262
298
|
|
|
299
|
+
function stepLine(item: PlanSnapshot["steps"][number], index: number): string {
|
|
300
|
+
return `${index + 1}. [${item.status}] ${item.step}${item.dependsOn?.length ? ` (after: ${item.dependsOn.join(", ")})` : ""}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
263
303
|
function formatPlan(plan: PlanSnapshot): string {
|
|
264
304
|
const progress = planProgress(plan);
|
|
265
305
|
return [
|
|
266
306
|
`Plan: ${progress.completed}/${progress.total} steps completed${progress.blocked ? `, ${progress.blocked} blocked` : ""}`,
|
|
267
|
-
...plan.steps.map(
|
|
307
|
+
...plan.steps.map(stepLine),
|
|
308
|
+
...(plan.steps.some((item) => item.dependsOn?.length)
|
|
309
|
+
? [`Ready: ${progress.readyIndices.map((index) => plan.steps[index]!.step).join(", ") || "none"}`]
|
|
310
|
+
: []),
|
|
268
311
|
].join("\n");
|
|
269
312
|
}
|
|
270
313
|
|
|
271
314
|
function planPrompt(plan: PlanSnapshot): string {
|
|
272
315
|
return [
|
|
273
316
|
"Current structured execution plan:",
|
|
274
|
-
...plan.steps.map(
|
|
317
|
+
...plan.steps.map(stepLine),
|
|
318
|
+
...(plan.steps.some((item) => item.dependsOn?.length)
|
|
319
|
+
? [`Ready pending steps: ${planProgress(plan).readyIndices.map((index) => plan.steps[index]!.step).join(", ") || "none"}. Start only steps whose prerequisites are complete.`]
|
|
320
|
+
: []),
|
|
275
321
|
"Use update_plan immediately when a step completes, becomes blocked, or the scope changes.",
|
|
276
|
-
"
|
|
322
|
+
"Before starting the first implementation milestone, identify independent substantial work. If a subagent is available, launch one bounded task early while you continue another; otherwise briefly state the concrete dependency or shared-worktree constraint that makes delegation unsuitable.",
|
|
323
|
+
"Treat the plan as the foreground coordinator's milestone ledger: delegate independent, sufficiently substantial work early, mark distinct foreground and delegated milestones in_progress concurrently, and continue unblocked foreground work instead of waiting or polling.",
|
|
277
324
|
"Do not complete verification or the plan until relevant delegated work is terminal, its results or failures have been inspected and integrated, and the outcome is evidence-backed.",
|
|
278
325
|
].join("\n");
|
|
279
326
|
}
|
|
@@ -4,6 +4,7 @@ import { EXTENSION_NAME } from "./types.js";
|
|
|
4
4
|
const MAX_STEPS = 50;
|
|
5
5
|
const MAX_STEP_CHARS = 500;
|
|
6
6
|
const MAX_EXPLANATION_CHARS = 2_000;
|
|
7
|
+
const STEP_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
7
8
|
export const COMPLETED_PLAN_RETENTION_MS = 30_000;
|
|
8
9
|
|
|
9
10
|
interface SessionEntryLike {
|
|
@@ -32,8 +33,8 @@ export function validatePlanInput(steps: readonly PlanStepInput[], explanation?:
|
|
|
32
33
|
return `Plan explanation cannot exceed ${MAX_EXPLANATION_CHARS} characters.`;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
let inProgress = 0;
|
|
36
36
|
const normalized = new Set<string>();
|
|
37
|
+
const ids = new Map<string, number>();
|
|
37
38
|
for (let index = 0; index < steps.length; index += 1) {
|
|
38
39
|
const item = steps[index]!;
|
|
39
40
|
const text = item.step.trim();
|
|
@@ -44,9 +45,43 @@ export function validatePlanInput(steps: readonly PlanStepInput[], explanation?:
|
|
|
44
45
|
const key = text.toLocaleLowerCase();
|
|
45
46
|
if (normalized.has(key)) return `Plan step ${index + 1} duplicates an earlier step.`;
|
|
46
47
|
normalized.add(key);
|
|
47
|
-
if (item.
|
|
48
|
+
if (item.id !== undefined) {
|
|
49
|
+
if (!STEP_ID.test(item.id)) return `Plan step ${index + 1} has an invalid id.`;
|
|
50
|
+
if (ids.has(item.id)) return `Plan step ${index + 1} duplicates id ${item.id}.`;
|
|
51
|
+
ids.set(item.id, index);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
56
|
+
const item = steps[index]!;
|
|
57
|
+
const seen = new Set<string>();
|
|
58
|
+
for (const dependency of item.dependsOn ?? []) {
|
|
59
|
+
if (seen.has(dependency)) return `Plan step ${index + 1} repeats dependency ${dependency}.`;
|
|
60
|
+
seen.add(dependency);
|
|
61
|
+
const prerequisite = ids.get(dependency);
|
|
62
|
+
if (prerequisite === undefined) return `Plan step ${index + 1} references unknown dependency ${dependency}. Use explicit ids for dependencies.`;
|
|
63
|
+
if ((item.status === "in_progress" || item.status === "completed") && steps[prerequisite]!.status !== "completed") {
|
|
64
|
+
return `Plan step ${index + 1} cannot be ${item.status} until dependency ${dependency} is completed.`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const visited = new Set<string>();
|
|
69
|
+
const visiting = new Set<string>();
|
|
70
|
+
const visit = (id: string): boolean => {
|
|
71
|
+
if (visiting.has(id)) return true;
|
|
72
|
+
if (visited.has(id)) return false;
|
|
73
|
+
visiting.add(id);
|
|
74
|
+
const item = steps[ids.get(id)!]!;
|
|
75
|
+
for (const dependency of item.dependsOn ?? []) {
|
|
76
|
+
if (visit(dependency)) return true;
|
|
77
|
+
}
|
|
78
|
+
visiting.delete(id);
|
|
79
|
+
visited.add(id);
|
|
80
|
+
return false;
|
|
81
|
+
};
|
|
82
|
+
for (const id of ids.keys()) {
|
|
83
|
+
if (visit(id)) return `Plan dependencies contain a cycle involving ${id}.`;
|
|
48
84
|
}
|
|
49
|
-
if (inProgress > 1) return "A plan can have at most one step in progress.";
|
|
50
85
|
return null;
|
|
51
86
|
}
|
|
52
87
|
|
|
@@ -65,11 +100,15 @@ export function replacePlan(
|
|
|
65
100
|
const normalizedSteps: PlanStep[] = steps.map((item, index) => {
|
|
66
101
|
const step = item.step.trim();
|
|
67
102
|
return {
|
|
68
|
-
id: reusableIds.get(step.toLocaleLowerCase()) ?? `step_${revision}_${index + 1}`,
|
|
103
|
+
id: item.id ?? reusableIds.get(step.toLocaleLowerCase()) ?? `step_${revision}_${index + 1}`,
|
|
69
104
|
step,
|
|
70
105
|
status: item.status,
|
|
106
|
+
...(item.dependsOn?.length ? { dependsOn: [...item.dependsOn] } : {}),
|
|
71
107
|
};
|
|
72
108
|
});
|
|
109
|
+
if (new Set(normalizedSteps.map((item) => item.id)).size !== normalizedSteps.length) {
|
|
110
|
+
throw new Error("Plan step ids must be unique, including generated ids.");
|
|
111
|
+
}
|
|
73
112
|
const isComplete = normalizedSteps.every((item) => item.status === "completed");
|
|
74
113
|
const currentIsComplete = current?.steps.every((item) => item.status === "completed") === true;
|
|
75
114
|
|
|
@@ -98,14 +137,19 @@ export function planProgress(plan: PlanSnapshot): PlanProgress {
|
|
|
98
137
|
const pending = plan.steps.filter((item) => item.status === "pending").length;
|
|
99
138
|
const blocked = plan.steps.filter((item) => item.status === "blocked").length;
|
|
100
139
|
const inProgress = plan.steps.filter((item) => item.status === "in_progress").length;
|
|
101
|
-
const activeIndex = plan.steps.findIndex((item) => item.status === "in_progress"
|
|
140
|
+
const activeIndex = plan.steps.findIndex((item) => item.status === "in_progress");
|
|
141
|
+
const blockedIndex = plan.steps.findIndex((item) => item.status === "blocked");
|
|
142
|
+
const completedIds = new Set(plan.steps.filter((item) => item.status === "completed").map((item) => item.id));
|
|
143
|
+
const readyIndices = plan.steps.flatMap((item, index) =>
|
|
144
|
+
item.status === "pending" && (item.dependsOn ?? []).every((id) => completedIds.has(id)) ? [index] : []);
|
|
102
145
|
return {
|
|
103
146
|
total: plan.steps.length,
|
|
104
147
|
completed,
|
|
105
148
|
pending,
|
|
106
149
|
blocked,
|
|
107
150
|
inProgress,
|
|
108
|
-
activeIndex: activeIndex >= 0 ? activeIndex : null,
|
|
151
|
+
activeIndex: activeIndex >= 0 ? activeIndex : blockedIndex >= 0 ? blockedIndex : null,
|
|
152
|
+
readyIndices,
|
|
109
153
|
state:
|
|
110
154
|
completed === plan.steps.length
|
|
111
155
|
? "complete"
|
|
@@ -4,8 +4,10 @@ export type PlanStepStatus = "pending" | "in_progress" | "completed" | "blocked"
|
|
|
4
4
|
export type PlanDisplayMode = "auto" | "on" | "off" | "hidden";
|
|
5
5
|
|
|
6
6
|
export interface PlanStepInput {
|
|
7
|
+
id?: string;
|
|
7
8
|
step: string;
|
|
8
9
|
status: PlanStepStatus;
|
|
10
|
+
dependsOn?: string[];
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface PlanStep extends PlanStepInput {
|
|
@@ -30,6 +32,7 @@ export interface PlanProgress {
|
|
|
30
32
|
blocked: number;
|
|
31
33
|
inProgress: number;
|
|
32
34
|
activeIndex: number | null;
|
|
35
|
+
readyIndices: number[];
|
|
33
36
|
state: "draft" | "in_progress" | "blocked" | "complete";
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -128,7 +128,7 @@ const SUBAGENT_TOOLS = [
|
|
|
128
128
|
];
|
|
129
129
|
|
|
130
130
|
const SUBAGENT_ORCHESTRATION_GUIDELINES = [
|
|
131
|
-
"When a structured plan is active, keep it as the parent-owned coordinator ledger:
|
|
131
|
+
"When a structured plan is active, keep it as the parent-owned coordinator ledger: mark distinct delegated and foreground deliverables in_progress concurrently, continue unblocked foreground work, and update the plan after integrating each result or failure.",
|
|
132
132
|
"Do not treat launching a subagent as completion of the parent milestone; relevant terminal results must be inspected and integrated before verification or completion.",
|
|
133
133
|
];
|
|
134
134
|
|
|
@@ -918,7 +918,7 @@ function createOverlayComponent(
|
|
|
918
918
|
width,
|
|
919
919
|
deps.truncate,
|
|
920
920
|
fg,
|
|
921
|
-
{ minRows: detailRows, bottomFooter: false, logTailRows },
|
|
921
|
+
{ minRows: detailRows, maxRows: detailRows, bottomFooter: false, logTailRows },
|
|
922
922
|
);
|
|
923
923
|
} else {
|
|
924
924
|
transcriptDetail = null;
|
|
@@ -1003,21 +1003,33 @@ function buildTranscriptDetailLines(
|
|
|
1003
1003
|
width: number,
|
|
1004
1004
|
truncate: (s: string, width: number) => string,
|
|
1005
1005
|
fg: (color: string, value: string) => string,
|
|
1006
|
-
options: { minRows?: number; bottomFooter?: boolean; logTailRows?: number } = {},
|
|
1006
|
+
options: { minRows?: number; maxRows?: number; bottomFooter?: boolean; logTailRows?: number } = {},
|
|
1007
1007
|
): string[] {
|
|
1008
1008
|
const actions = [...(detail.footerActions ?? ["x close"]), "Esc close"].join(" · ");
|
|
1009
1009
|
const tailRows = options.logTailRows ?? DEFAULT_LOG_TAIL_ROWS;
|
|
1010
|
-
|
|
1010
|
+
let shownTranscriptLines = transcriptLines.length ? transcriptLines.slice(-tailRows) : [" (no transcript yet)"];
|
|
1011
1011
|
const lines: string[] = [
|
|
1012
1012
|
fg("accent", rule(detail.title, width)),
|
|
1013
1013
|
dim(` ← main · ${actions}`, fg),
|
|
1014
1014
|
"",
|
|
1015
1015
|
` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`,
|
|
1016
1016
|
];
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1017
|
+
const optionalMetadata = [
|
|
1018
|
+
...(detail.subtitle ? [` summary ${detail.subtitle}`] : []),
|
|
1019
|
+
...detail.metadata.map((item) => ` ${item.label.padEnd(8, " ").slice(0, 8)} ${item.value}`),
|
|
1020
|
+
];
|
|
1021
|
+
const transcriptHeader = ["", dim(section(`transcript · latest ${tailRows} rows`, width), fg)];
|
|
1022
|
+
if (detail.transcriptDiagnostic) transcriptHeader.push(` ${dim(detail.transcriptDiagnostic, fg)}`);
|
|
1023
|
+
if (options.maxRows !== undefined) {
|
|
1024
|
+
const fixedRows = lines.length + transcriptHeader.length + 1;
|
|
1025
|
+
const transcriptBudget = Math.max(1, options.maxRows - fixedRows);
|
|
1026
|
+
shownTranscriptLines = shownTranscriptLines.slice(-transcriptBudget);
|
|
1027
|
+
const metadataBudget = Math.max(0, options.maxRows - fixedRows - shownTranscriptLines.length);
|
|
1028
|
+
lines.push(...optionalMetadata.slice(0, metadataBudget));
|
|
1029
|
+
} else {
|
|
1030
|
+
lines.push(...optionalMetadata);
|
|
1031
|
+
}
|
|
1032
|
+
lines.push(...transcriptHeader);
|
|
1021
1033
|
lines.push(...shownTranscriptLines);
|
|
1022
1034
|
lines.push("");
|
|
1023
1035
|
if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
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",
|
|
@@ -50,12 +50,13 @@
|
|
|
50
50
|
"test": "node --test test/*.test.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"pi-better-background-tasks": "0.2.
|
|
54
|
-
"pi-better-goal": "0.
|
|
55
|
-
"pi-better-plan": "0.
|
|
53
|
+
"pi-better-background-tasks": "0.2.13",
|
|
54
|
+
"pi-better-goal": "0.3.0",
|
|
55
|
+
"pi-better-plan": "0.2.0",
|
|
56
56
|
"pi-better-sandbox": "0.3.0",
|
|
57
57
|
"pi-better-ssh": "0.1.1",
|
|
58
|
-
"pi-better-subagents": "0.1.
|
|
58
|
+
"pi-better-subagents": "0.1.29",
|
|
59
|
+
"yaml": "^2.9.1"
|
|
59
60
|
},
|
|
60
61
|
"bundledDependencies": [
|
|
61
62
|
"pi-better-background-tasks",
|