pi-better-harness 0.3.11 → 0.3.12
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-goal/README.md +1 -1
- package/node_modules/pi-better-goal/package.json +4 -4
- package/node_modules/pi-better-goal/src/command-binding.ts +37 -0
- package/node_modules/pi-better-goal/src/goal-state.ts +13 -0
- package/node_modules/pi-better-goal/src/index.ts +52 -29
- package/node_modules/pi-better-goal/src/types.ts +2 -0
- package/package.json +2 -2
|
@@ -28,7 +28,7 @@ metadata:
|
|
|
28
28
|
workflow-role: coordinator
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Invoke the skill with Pi's `/skill:name` command
|
|
31
|
+
Invoke the skill with Pi's `/skill:name` command, or supervise it with `/goal /skill:name task`. The goal extension resolves the command against Pi's registry, persists its source, and expands the skill on kickoff and continuation (including after session resume). A bound command that disappears or changes source pauses the goal. `/goal /template task` also re-expands prompt templates on continuation; `/goal /extension-command task` dispatches the extension command once, then continues with ordinary goal supervision to avoid repeating side effects. Plain-language goals work as before. This command binding requires Pi 0.84.4 or later. Legacy slash-shaped goals without a binding pause on resume rather than running without the skill. `/workflow` shows a coordinator skill 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
32
|
|
|
33
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. The former `pi-better-plan-workflow: coordinator` metadata remains supported for installed skills.
|
|
34
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-goal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi extension for goal tracking with background-aware continuation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"verify": "npm run typecheck && npm test"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@earendil-works/pi-coding-agent": "
|
|
45
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
46
46
|
"@earendil-works/pi-tui": "*",
|
|
47
47
|
"typebox": "*"
|
|
48
48
|
},
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
62
|
-
"@earendil-works/pi-tui": "^0.
|
|
61
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
62
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
63
63
|
"@types/node": "^25.0.0",
|
|
64
64
|
"tsx": "^4.22.3",
|
|
65
65
|
"typebox": "^1.1.39",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export interface GoalCommandBinding {
|
|
5
|
+
name: string;
|
|
6
|
+
source: "skill" | "prompt" | "extension";
|
|
7
|
+
path: string;
|
|
8
|
+
args: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function leadingCommand(text: string): { name: string; args: string } | null {
|
|
12
|
+
const match = /^\/([^\s/]+)(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
13
|
+
return match ? { name: match[1]!, args: match[2]?.trim() ?? "" } : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveGoalCommand(pi: ExtensionAPI, text: string): GoalCommandBinding | null {
|
|
17
|
+
const parsed = leadingCommand(text);
|
|
18
|
+
if (!parsed) return null;
|
|
19
|
+
if (parsed.name === "goal") throw new Error("A goal cannot invoke /goal recursively.");
|
|
20
|
+
const command = pi.getCommands().find((item) => item.name === parsed.name);
|
|
21
|
+
if (!command) throw new Error(`Unknown command /${parsed.name}.`);
|
|
22
|
+
if (!command.sourceInfo.path) throw new Error(`Command /${parsed.name} has no stable source path.`);
|
|
23
|
+
return { name: command.name, source: command.source, path: command.sourceInfo.path, args: parsed.args };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function commandAvailable(pi: ExtensionAPI, binding: GoalCommandBinding): boolean {
|
|
27
|
+
return (binding.source === "extension" || existsSync(binding.path)) &&
|
|
28
|
+
pi.getCommands().some((command) => command.name === binding.name &&
|
|
29
|
+
command.source === binding.source && command.sourceInfo.path === binding.path);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function commandInvocation(binding: GoalCommandBinding, continuation = false): string {
|
|
33
|
+
const args = continuation
|
|
34
|
+
? `${binding.args ? `${binding.args}\n\n` : ""}Continue the existing goal using the current session state. Do not restart completed work.`
|
|
35
|
+
: binding.args;
|
|
36
|
+
return `/${binding.name}${args ? ` ${args}` : ""}`;
|
|
37
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { GoalCommandBinding } from "./command-binding.js";
|
|
2
3
|
|
|
3
4
|
import { EXTENSION_NAME, type GoalSnapshot, type GoalStatus } from "./types.js";
|
|
4
5
|
|
|
@@ -14,6 +15,7 @@ interface SessionEntryLike {
|
|
|
14
15
|
interface GoalLike {
|
|
15
16
|
goalId: string;
|
|
16
17
|
objective: string;
|
|
18
|
+
command?: unknown;
|
|
17
19
|
status: GoalStatus;
|
|
18
20
|
tokenBudget?: number | null;
|
|
19
21
|
usage?: { tokensUsed?: number; activeSeconds?: number };
|
|
@@ -96,6 +98,14 @@ function unixSeconds(): number {
|
|
|
96
98
|
return Math.floor(Date.now() / 1000);
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
function isCommandBinding(value: unknown): value is GoalCommandBinding {
|
|
102
|
+
if (!value || typeof value !== "object") return false;
|
|
103
|
+
const binding = value as Partial<GoalCommandBinding>;
|
|
104
|
+
return typeof binding.name === "string" && binding.name.length > 0 &&
|
|
105
|
+
(binding.source === "skill" || binding.source === "prompt" || binding.source === "extension") &&
|
|
106
|
+
typeof binding.path === "string" && binding.path.length > 0 && typeof binding.args === "string";
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
function nextGoalId(): string {
|
|
100
110
|
return `goal_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
101
111
|
}
|
|
@@ -107,6 +117,7 @@ function normalizeGoal(goal: GoalLike): GoalSnapshot {
|
|
|
107
117
|
return {
|
|
108
118
|
goalId: goal.goalId,
|
|
109
119
|
objective: goal.objective,
|
|
120
|
+
...(isCommandBinding(goal.command) ? { command: goal.command } : {}),
|
|
110
121
|
status: goal.status,
|
|
111
122
|
tokenBudget: typeof goal.tokenBudget === "number" ? goal.tokenBudget : null,
|
|
112
123
|
usage: {
|
|
@@ -134,10 +145,12 @@ export function createGoalSnapshot(
|
|
|
134
145
|
objective: string,
|
|
135
146
|
tokenBudget: number | null = null,
|
|
136
147
|
now = unixSeconds(),
|
|
148
|
+
command?: GoalCommandBinding,
|
|
137
149
|
): GoalSnapshot {
|
|
138
150
|
return {
|
|
139
151
|
goalId: nextGoalId(),
|
|
140
152
|
objective,
|
|
153
|
+
...(command ? { command } : {}),
|
|
141
154
|
status: "active",
|
|
142
155
|
tokenBudget,
|
|
143
156
|
usage: { tokensUsed: 0, activeSeconds: 0 },
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
+
import { commandAvailable, commandInvocation, resolveGoalCommand } from "./command-binding.js";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
collectActivitySnapshot,
|
|
@@ -276,22 +277,35 @@ export default function (pi: ExtensionAPI): void {
|
|
|
276
277
|
notifyGoal(ctx, "Goal paused (interrupted).");
|
|
277
278
|
};
|
|
278
279
|
|
|
279
|
-
const
|
|
280
|
+
const boundCommandReady = (goal: GoalSnapshot, ctx: ExtensionContext): boolean => {
|
|
281
|
+
if (!goal.command || commandAvailable(pi, goal.command)) return true;
|
|
282
|
+
setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
283
|
+
notifyGoal(ctx, `Goal paused: /${goal.command.name} is no longer registered at its original source.`, "error");
|
|
284
|
+
return false;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const sendGoalContinuation = (goal: GoalSnapshot, content: string, kind: string, snapshot?: ActivitySnapshot): void => {
|
|
288
|
+
if (goal.command?.source === "skill" || goal.command?.source === "prompt") {
|
|
289
|
+
pi.sendUserMessage(`${commandInvocation(goal.command, true)}\n\n${content}`, {
|
|
290
|
+
deliverAs: "followUp", expandPromptTemplates: true,
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
pi.sendMessage({
|
|
294
|
+
customType: EXTENSION_NAME, content, display: false,
|
|
295
|
+
details: snapshot ? { kind, snapshot } : { kind, goalId: goal.goalId },
|
|
296
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const queueGoalContinuation = (goal: GoalSnapshot, ctx: ExtensionContext): void => {
|
|
280
301
|
if (!isPokeable(goal) || continuationQueuedFor === goal.goalId) {
|
|
281
302
|
return;
|
|
282
303
|
}
|
|
304
|
+
if (!boundCommandReady(goal, ctx)) return;
|
|
283
305
|
clearIdleContinuation();
|
|
284
306
|
continuationQueuedFor = goal.goalId;
|
|
285
307
|
resetContinuationState(goal);
|
|
286
|
-
|
|
287
|
-
{
|
|
288
|
-
customType: EXTENSION_NAME,
|
|
289
|
-
content: continuationPrompt(goal, currentCtx ? getWorkflow(currentCtx) : null),
|
|
290
|
-
display: false,
|
|
291
|
-
details: { kind: "continuation", goalId: goal.goalId },
|
|
292
|
-
},
|
|
293
|
-
{ triggerTurn: true, deliverAs: "followUp" },
|
|
294
|
-
);
|
|
308
|
+
sendGoalContinuation(goal, continuationPrompt(goal, getWorkflow(ctx)), "continuation");
|
|
295
309
|
};
|
|
296
310
|
|
|
297
311
|
function clearIdleContinuation(): void {
|
|
@@ -338,6 +352,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
338
352
|
if (!isPokeable(goal) || goal.goalId !== goalId || continuationQueuedFor === goal.goalId || foregroundRunning) {
|
|
339
353
|
return;
|
|
340
354
|
}
|
|
355
|
+
if (!boundCommandReady(goal, ctx)) return;
|
|
341
356
|
const snapshot = await collectActivitySnapshot(ctx, providers.values(), foregroundRunning);
|
|
342
357
|
latestSnapshot = snapshot;
|
|
343
358
|
pi.events.emit(EVENT_ACTIVITY, snapshot);
|
|
@@ -354,17 +369,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
354
369
|
? "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" +
|
|
355
370
|
continuationPrompt(goal, getWorkflow(ctx))
|
|
356
371
|
: continuationPrompt(goal, getWorkflow(ctx));
|
|
357
|
-
|
|
358
|
-
{
|
|
359
|
-
customType: EXTENSION_NAME,
|
|
360
|
-
content,
|
|
361
|
-
display: false,
|
|
362
|
-
details: kind === "background-drained"
|
|
363
|
-
? { kind, snapshot: priorSnapshot ?? snapshot }
|
|
364
|
-
: { kind, goalId: goal.goalId },
|
|
365
|
-
},
|
|
366
|
-
{ triggerTurn: true, deliverAs: "followUp" },
|
|
367
|
-
);
|
|
372
|
+
sendGoalContinuation(goal, content, kind, kind === "background-drained" ? priorSnapshot ?? snapshot : undefined);
|
|
368
373
|
};
|
|
369
374
|
|
|
370
375
|
const startOrReplaceGoal = (
|
|
@@ -373,9 +378,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
373
378
|
ctx: ExtensionContext,
|
|
374
379
|
source: "command",
|
|
375
380
|
): 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
|
-
}
|
|
379
381
|
const objectiveError = validateObjective(objective);
|
|
380
382
|
if (objectiveError) {
|
|
381
383
|
throw new Error(objectiveError);
|
|
@@ -384,9 +386,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
384
386
|
if (budgetError) {
|
|
385
387
|
throw new Error(budgetError);
|
|
386
388
|
}
|
|
387
|
-
const
|
|
389
|
+
const command = resolveGoalCommand(pi, objective);
|
|
390
|
+
if (command && !commandAvailable(pi, command)) {
|
|
391
|
+
throw new Error(`Command /${command.name} is unavailable at its registered source.`);
|
|
392
|
+
}
|
|
393
|
+
const goal = createGoalSnapshot(objective.trim(), tokenBudget, undefined, command ?? undefined);
|
|
394
|
+
const owner = command?.source === "skill"
|
|
395
|
+
? workflowOwnerFromSkill(command.name.slice("skill:".length), command.path)
|
|
396
|
+
: null;
|
|
397
|
+
if (getWorkflow(ctx)) recordWorkflow(null);
|
|
388
398
|
setGoal(goal, ctx, source);
|
|
389
|
-
|
|
399
|
+
if (command) {
|
|
400
|
+
if (owner) recordWorkflow(owner);
|
|
401
|
+
pi.sendUserMessage(commandInvocation(command), { deliverAs: "followUp", expandPromptTemplates: true });
|
|
402
|
+
if (command.source === "extension") scheduleIdleContinuation(goal, ctx, "continuation");
|
|
403
|
+
} else {
|
|
404
|
+
queueGoalContinuation(goal, ctx);
|
|
405
|
+
}
|
|
390
406
|
return goal;
|
|
391
407
|
};
|
|
392
408
|
|
|
@@ -588,13 +604,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
588
604
|
notifyGoal(ctx, "Only paused goals can be resumed.", "warning");
|
|
589
605
|
return;
|
|
590
606
|
}
|
|
591
|
-
if (skillCommandName(current.objective)) {
|
|
607
|
+
if (!current.command && skillCommandName(current.objective)) {
|
|
592
608
|
notifyGoal(ctx, "Invoke the skill directly; this legacy slash-command goal cannot resume as plain text.", "error");
|
|
593
609
|
return;
|
|
594
610
|
}
|
|
611
|
+
if (current.command && !commandAvailable(pi, current.command)) {
|
|
612
|
+
notifyGoal(ctx, `Cannot resume: /${current.command.name} is no longer registered at its original source.`, "error");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
595
615
|
const goal = goalWithStatus(current, "active");
|
|
596
616
|
setGoal(goal, ctx, "command");
|
|
597
|
-
queueGoalContinuation(goal);
|
|
617
|
+
queueGoalContinuation(goal, ctx);
|
|
598
618
|
notifyGoal(ctx, "Goal resumed.");
|
|
599
619
|
return;
|
|
600
620
|
}
|
|
@@ -752,7 +772,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
752
772
|
currentCtx = ctx;
|
|
753
773
|
foregroundRunning = !ctx.isIdle();
|
|
754
774
|
const restoredGoal = getGoal(ctx);
|
|
755
|
-
if (restoredGoal?.status === "active" && skillCommandName(restoredGoal.objective)) {
|
|
775
|
+
if (restoredGoal?.status === "active" && !restoredGoal.command && skillCommandName(restoredGoal.objective)) {
|
|
756
776
|
setGoal(goalWithStatus(restoredGoal, "paused"), ctx, "runtime");
|
|
757
777
|
notifyGoal(ctx, "Goal paused: invoke its skill directly before resuming.", "error");
|
|
758
778
|
}
|
|
@@ -760,6 +780,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
760
780
|
installGoalWidget(ctx);
|
|
761
781
|
latestSnapshot = await publishSnapshot(ctx);
|
|
762
782
|
syncPollingState();
|
|
783
|
+
if (restoredGoal?.status === "active" && restoredGoal.command && boundCommandReady(restoredGoal, ctx) && !foregroundRunning && !latestSnapshot.backgroundRunning) {
|
|
784
|
+
scheduleIdleContinuation(restoredGoal, ctx, "continuation");
|
|
785
|
+
}
|
|
763
786
|
});
|
|
764
787
|
|
|
765
788
|
pi.on("input", async (event, ctx) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { GoalCommandBinding } from "./command-binding.js";
|
|
2
3
|
|
|
3
4
|
export const EXTENSION_NAME = "pi-better-goal";
|
|
4
5
|
export const EXTENSION_VERSION = "0.1.0";
|
|
@@ -72,6 +73,7 @@ export interface GoalUsage {
|
|
|
72
73
|
export interface GoalSnapshot {
|
|
73
74
|
goalId: string;
|
|
74
75
|
objective: string;
|
|
76
|
+
command?: GoalCommandBinding;
|
|
75
77
|
status: GoalStatus;
|
|
76
78
|
tokenBudget: number | null;
|
|
77
79
|
usage: GoalUsage;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"pi-better-background-tasks": "0.2.13",
|
|
54
|
-
"pi-better-goal": "0.
|
|
54
|
+
"pi-better-goal": "0.4.0",
|
|
55
55
|
"pi-better-plan": "0.2.1",
|
|
56
56
|
"pi-better-sandbox": "0.3.0",
|
|
57
57
|
"pi-better-ssh": "0.1.1",
|