pi-better-harness 0.13.1 → 0.14.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.
|
@@ -17,7 +17,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
17
17
|
- Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
|
|
18
18
|
- A compact goal widget that does not replace Pi's footer.
|
|
19
19
|
- Background activity tracking for subagents and other registered providers.
|
|
20
|
-
- A progress-aware follow-up loop
|
|
20
|
+
- A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (30s through 300s by default), followed by a hold recoverable with `/goal resume`. Delays saturate at Node's timer limit, and observed background drains reset progress before callback turns can cancel the wake. Pi's network retry policy remains independent.
|
|
21
21
|
- An observable-progress stall state for active goals.
|
|
22
22
|
|
|
23
23
|
## Skill-Owned Workflows
|
|
@@ -51,7 +51,8 @@ import {
|
|
|
51
51
|
|
|
52
52
|
const POLL_INTERVAL_MS = 2_000;
|
|
53
53
|
const DEFAULT_IDLE_CONTINUATION_DELAY_MS = 30_000;
|
|
54
|
-
const DEFAULT_MAX_NO_PROGRESS_RETRIES =
|
|
54
|
+
const DEFAULT_MAX_NO_PROGRESS_RETRIES = 10;
|
|
55
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
55
56
|
const WAKE_DISABLED =
|
|
56
57
|
process.env.PI_BETTER_GOAL_DISABLE_WAKE === "1" ||
|
|
57
58
|
process.env.PI_BETTER_EXTENSION_DISABLE_WAKE === "1";
|
|
@@ -121,6 +122,14 @@ function parseDurationEnv(raw: string | undefined, fallback: number): number {
|
|
|
121
122
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Linear backoff for no-progress retries: each identical outcome waits one more
|
|
127
|
+
* grace period than the last (30s, 60s, 90s, ... by default).
|
|
128
|
+
*/
|
|
129
|
+
export function idleContinuationDelay(noProgressRetries: number, baseMs = IDLE_CONTINUATION_DELAY_MS): number {
|
|
130
|
+
return Math.min(MAX_TIMER_DELAY_MS, baseMs * (1 + Math.max(0, noProgressRetries)));
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
function parseRetryLimit(raw: string | undefined, fallback: number): number {
|
|
125
134
|
if (raw === undefined || raw.trim() === "") {
|
|
126
135
|
return fallback;
|
|
@@ -257,13 +266,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
257
266
|
});
|
|
258
267
|
|
|
259
268
|
let currentCtx: ExtensionContext | undefined;
|
|
269
|
+
let executionGeneration = 0;
|
|
270
|
+
let agentGeneration = -1;
|
|
271
|
+
let sessionGeneration = 0;
|
|
272
|
+
let wakeGeneration = 0;
|
|
273
|
+
let snapshotSequence = 0;
|
|
274
|
+
let lastAppliedSnapshotSequence = 0;
|
|
260
275
|
let foregroundRunning = false;
|
|
261
276
|
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
262
277
|
let collecting = false;
|
|
263
278
|
let collectionPending = false;
|
|
264
279
|
let latestSnapshot: ActivitySnapshot | null = null;
|
|
265
280
|
let backgroundDrainTracker: BackgroundDrainTracker | null = null;
|
|
266
|
-
let lastWakeSignature = "";
|
|
267
281
|
let lastAttentionSignature = "";
|
|
268
282
|
let continuationQueuedFor: string | null = null;
|
|
269
283
|
let idleContinuationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -330,6 +344,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
330
344
|
};
|
|
331
345
|
|
|
332
346
|
const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
347
|
+
executionGeneration += 1;
|
|
348
|
+
lastAgentEvidence = null;
|
|
333
349
|
const previous = getGoal(ctx);
|
|
334
350
|
const wasVisible = previous !== null && isGoalClockVisible(previous);
|
|
335
351
|
pi.appendEntry(EXTENSION_NAME, goalSetEntry(goal, source));
|
|
@@ -344,6 +360,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
344
360
|
};
|
|
345
361
|
|
|
346
362
|
const clearGoal = (ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
363
|
+
executionGeneration += 1;
|
|
364
|
+
lastAgentEvidence = null;
|
|
347
365
|
const current = getGoal(ctx);
|
|
348
366
|
const wasVisible = current !== null && isGoalClockVisible(current);
|
|
349
367
|
pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
|
|
@@ -385,21 +403,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
385
403
|
|
|
386
404
|
/**
|
|
387
405
|
* The one resume path shared by `/goal resume`, the hotkey, and `goal_resume`:
|
|
388
|
-
* reactivate
|
|
406
|
+
* reactivate a paused goal or reopen an active no-progress hold.
|
|
389
407
|
*/
|
|
390
408
|
const resumeGoal = (
|
|
391
409
|
ctx: ExtensionContext,
|
|
392
410
|
source: GoalEntrySource,
|
|
393
411
|
): { ok: true; goal: GoalSnapshot } | { ok: false; message: string } => {
|
|
394
412
|
const current = getGoal(ctx);
|
|
395
|
-
|
|
396
|
-
|
|
413
|
+
const held = current?.status === "active" && currentContinuationState(ctx, current.goalId)?.blocked === true;
|
|
414
|
+
if (!current || (current.status !== "paused" && !held)) {
|
|
415
|
+
return { ok: false, message: "Only paused or no-progress-held goals can be resumed." };
|
|
397
416
|
}
|
|
398
417
|
const blocker = resumeBlocker(current);
|
|
399
418
|
if (blocker) {
|
|
400
419
|
return { ok: false, message: blocker };
|
|
401
420
|
}
|
|
402
|
-
const goal = goalWithStatus(current, "active");
|
|
421
|
+
const goal = held ? current : goalWithStatus(current, "active");
|
|
403
422
|
setGoal(goal, ctx, source);
|
|
404
423
|
queueGoalContinuation(goal, ctx);
|
|
405
424
|
return { ok: true, goal };
|
|
@@ -437,6 +456,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
437
456
|
};
|
|
438
457
|
|
|
439
458
|
function clearIdleContinuation(): void {
|
|
459
|
+
wakeGeneration += 1;
|
|
440
460
|
if (idleContinuationTimer) {
|
|
441
461
|
clearTimeout(idleContinuationTimer);
|
|
442
462
|
idleContinuationTimer = undefined;
|
|
@@ -457,7 +477,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
457
477
|
return;
|
|
458
478
|
}
|
|
459
479
|
const signature = `${goal.goalId}:${kind}`;
|
|
460
|
-
if (idleContinuationTimer && idleContinuationSignature === signature
|
|
480
|
+
if (idleContinuationTimer && (idleContinuationSignature === signature ||
|
|
481
|
+
(kind === "continuation" && idleContinuationSignature === `${goal.goalId}:background-drained`))) {
|
|
461
482
|
return;
|
|
462
483
|
}
|
|
463
484
|
clearIdleContinuation();
|
|
@@ -466,7 +487,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
466
487
|
idleContinuationTimer = undefined;
|
|
467
488
|
idleContinuationSignature = "";
|
|
468
489
|
void sendIdleContinuationAfterAudit(goal.goalId, ctx, kind, snapshot);
|
|
469
|
-
},
|
|
490
|
+
}, idleContinuationDelay(kind === "continuation" ? currentContinuationState(ctx, goal.goalId)?.noProgressRetries ?? 0 : 0));
|
|
470
491
|
idleContinuationTimer.unref?.();
|
|
471
492
|
};
|
|
472
493
|
|
|
@@ -476,22 +497,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
476
497
|
kind: "continuation" | "background-drained",
|
|
477
498
|
priorSnapshot?: ActivitySnapshot,
|
|
478
499
|
): Promise<void> => {
|
|
500
|
+
const execution = executionGeneration;
|
|
501
|
+
const wake = wakeGeneration;
|
|
502
|
+
const auditIsCurrent = (): boolean => {
|
|
503
|
+
if (execution !== executionGeneration || wake !== wakeGeneration) return false;
|
|
504
|
+
const current = currentGoalSnapshot(ctx);
|
|
505
|
+
return isPokeable(current) && current.goalId === goalId &&
|
|
506
|
+
continuationQueuedFor !== goalId && !isForegroundBusy(ctx);
|
|
507
|
+
};
|
|
479
508
|
const goal = currentGoalSnapshot(ctx);
|
|
480
|
-
if (!
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
509
|
+
if (!auditIsCurrent() || !isPokeable(goal)) return;
|
|
483
510
|
if (!boundCommandReady(goal, ctx)) return;
|
|
484
|
-
const snapshot = await
|
|
485
|
-
|
|
486
|
-
pi.events.emit(EVENT_ACTIVITY, snapshot);
|
|
487
|
-
if (snapshot.foregroundRunning || snapshot.backgroundRunning) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
511
|
+
const snapshot = await publishSnapshot(ctx);
|
|
512
|
+
if (!snapshot || !auditIsCurrent() || snapshot.foregroundRunning || snapshot.backgroundRunning) return;
|
|
490
513
|
|
|
491
|
-
// A background drain is a meaningful external state transition and reopens a held goal.
|
|
492
|
-
if (kind === "background-drained") {
|
|
493
|
-
resetContinuationState(goal);
|
|
494
|
-
}
|
|
495
514
|
continuationQueuedFor = goal.goalId;
|
|
496
515
|
const content = kind === "background-drained"
|
|
497
516
|
? "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" +
|
|
@@ -534,34 +553,38 @@ export default function (pi: ExtensionAPI): void {
|
|
|
534
553
|
return goal;
|
|
535
554
|
};
|
|
536
555
|
|
|
537
|
-
const publishSnapshot = async (ctx: ExtensionContext): Promise<ActivitySnapshot> => {
|
|
556
|
+
const publishSnapshot = async (ctx: ExtensionContext): Promise<ActivitySnapshot | null> => {
|
|
557
|
+
const execution = executionGeneration;
|
|
558
|
+
const sequence = ++snapshotSequence;
|
|
538
559
|
const snapshot = await collectActivitySnapshot(ctx, providers.values(), foregroundRunning);
|
|
560
|
+
if (execution !== executionGeneration) return null;
|
|
561
|
+
if (sequence < lastAppliedSnapshotSequence) return latestSnapshot;
|
|
562
|
+
lastAppliedSnapshotSequence = sequence;
|
|
539
563
|
latestSnapshot = snapshot;
|
|
540
|
-
pi.events.emit(EVENT_ACTIVITY, snapshot);
|
|
541
|
-
|
|
542
|
-
const attention = terminalAttentionSignature(snapshot);
|
|
543
|
-
if (attention && attention !== lastAttentionSignature) {
|
|
544
|
-
lastAttentionSignature = attention;
|
|
545
|
-
pi.events.emit(EVENT_TERMINAL_ATTENTION, snapshot);
|
|
546
|
-
}
|
|
547
564
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
}
|
|
565
|
+
const goal = currentGoalSnapshot(ctx);
|
|
566
|
+
const wakePlan = planBackgroundDrainWake(backgroundDrainTracker, goal, snapshot);
|
|
567
|
+
backgroundDrainTracker = wakePlan.nextTracker;
|
|
568
|
+
if (isPokeable(goal) && wakePlan.wakeSignature) {
|
|
569
|
+
// Persist external progress now: a foreground/callback turn may cancel its delayed wake.
|
|
570
|
+
clearIdleContinuation();
|
|
571
|
+
resetContinuationState(goal);
|
|
572
|
+
lastAgentEvidence = null;
|
|
573
|
+
scheduleIdleContinuation(goal, ctx, "background-drained", snapshot);
|
|
559
574
|
}
|
|
560
575
|
|
|
561
576
|
if (snapshot.foregroundRunning || snapshot.backgroundRunning) {
|
|
562
577
|
clearIdleContinuation();
|
|
563
578
|
}
|
|
564
579
|
|
|
580
|
+
pi.events.emit(EVENT_ACTIVITY, snapshot);
|
|
581
|
+
if (execution !== executionGeneration) return null;
|
|
582
|
+
const attention = terminalAttentionSignature(snapshot);
|
|
583
|
+
if (attention && attention !== lastAttentionSignature) {
|
|
584
|
+
lastAttentionSignature = attention;
|
|
585
|
+
pi.events.emit(EVENT_TERMINAL_ATTENTION, snapshot);
|
|
586
|
+
if (execution !== executionGeneration) return null;
|
|
587
|
+
}
|
|
565
588
|
applyStatus(ctx);
|
|
566
589
|
|
|
567
590
|
return snapshot;
|
|
@@ -628,7 +651,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
628
651
|
return;
|
|
629
652
|
}
|
|
630
653
|
collecting = true;
|
|
654
|
+
const session = sessionGeneration;
|
|
631
655
|
void publishSnapshot(ctx).finally(() => {
|
|
656
|
+
if (session !== sessionGeneration) return;
|
|
632
657
|
collecting = false;
|
|
633
658
|
if (collectionPending) {
|
|
634
659
|
collectionPending = false;
|
|
@@ -910,7 +935,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
910
935
|
handler: async (_args, ctx) => {
|
|
911
936
|
currentCtx = ctx;
|
|
912
937
|
const snapshot = await publishSnapshot(ctx);
|
|
913
|
-
|
|
938
|
+
if (!snapshot) return;
|
|
939
|
+
ctx.ui.notify(formatSnapshot(snapshot), "info");
|
|
914
940
|
},
|
|
915
941
|
});
|
|
916
942
|
|
|
@@ -949,6 +975,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
949
975
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
950
976
|
currentCtx = ctx;
|
|
951
977
|
const snapshot = await publishSnapshot(ctx);
|
|
978
|
+
if (!snapshot) {
|
|
979
|
+
return { content: [{ type: "text", text: "Activity changed during collection; inspect again." }], details: { stale: true } };
|
|
980
|
+
}
|
|
952
981
|
return {
|
|
953
982
|
content: [{ type: "text", text: formatSnapshot(snapshot) }],
|
|
954
983
|
details: snapshot,
|
|
@@ -957,6 +986,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
957
986
|
});
|
|
958
987
|
|
|
959
988
|
pi.on("session_start", async (_event, ctx) => {
|
|
989
|
+
executionGeneration += 1;
|
|
990
|
+
sessionGeneration += 1;
|
|
991
|
+
clearIdleContinuation();
|
|
992
|
+
continuationQueuedFor = null;
|
|
993
|
+
backgroundDrainTracker = null;
|
|
994
|
+
lastAgentEvidence = null;
|
|
995
|
+
collecting = false;
|
|
996
|
+
collectionPending = false;
|
|
960
997
|
currentCtx = ctx;
|
|
961
998
|
foregroundRunning = !ctx.isIdle();
|
|
962
999
|
const restoredGoal = getGoal(ctx);
|
|
@@ -967,9 +1004,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
967
1004
|
pi.events.emit(EVENT_READY, { version: EXTENSION_VERSION });
|
|
968
1005
|
syncResumeTool(getGoal(ctx));
|
|
969
1006
|
installGoalWidget(ctx);
|
|
970
|
-
|
|
1007
|
+
const snapshot = await publishSnapshot(ctx);
|
|
1008
|
+
if (!snapshot) return;
|
|
971
1009
|
syncPollingState();
|
|
972
|
-
if (restoredGoal?.status === "active" && restoredGoal.command && boundCommandReady(restoredGoal, ctx) && !foregroundRunning && !
|
|
1010
|
+
if (restoredGoal?.status === "active" && restoredGoal.command && boundCommandReady(restoredGoal, ctx) && !foregroundRunning && !snapshot.backgroundRunning) {
|
|
973
1011
|
scheduleIdleContinuation(restoredGoal, ctx, "continuation");
|
|
974
1012
|
}
|
|
975
1013
|
});
|
|
@@ -995,6 +1033,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
995
1033
|
// conversation. Only /goal resume, the hotkey, or goal_resume resume it.
|
|
996
1034
|
const goal = getGoal(ctx);
|
|
997
1035
|
if (goal?.status === "active") {
|
|
1036
|
+
executionGeneration += 1;
|
|
1037
|
+
clearIdleContinuation();
|
|
1038
|
+
lastAgentEvidence = null;
|
|
998
1039
|
resetContinuationState(goal);
|
|
999
1040
|
}
|
|
1000
1041
|
});
|
|
@@ -1004,6 +1045,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1004
1045
|
const goal = currentGoalSnapshot(ctx);
|
|
1005
1046
|
const owner = getWorkflow(ctx);
|
|
1006
1047
|
const snapshot = await publishSnapshot(ctx);
|
|
1048
|
+
if (!snapshot) return;
|
|
1007
1049
|
const pausedInstruction = agentResumable(goal) ? pausedGoalPrompt(goal!) : "";
|
|
1008
1050
|
if (!isPokeable(goal) && !owner) {
|
|
1009
1051
|
return pausedInstruction ? { systemPrompt: `${event.systemPrompt}\n\n${pausedInstruction}` } : undefined;
|
|
@@ -1049,6 +1091,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1049
1091
|
});
|
|
1050
1092
|
|
|
1051
1093
|
pi.on("agent_start", async (_event, ctx) => {
|
|
1094
|
+
executionGeneration += 1;
|
|
1095
|
+
agentGeneration = executionGeneration;
|
|
1052
1096
|
currentCtx = ctx;
|
|
1053
1097
|
foregroundRunning = true;
|
|
1054
1098
|
clearIdleContinuation();
|
|
@@ -1066,7 +1110,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1066
1110
|
});
|
|
1067
1111
|
|
|
1068
1112
|
pi.on("agent_end", async (event, ctx) => {
|
|
1069
|
-
lastAgentEvidence = continuationEvidence(event.messages);
|
|
1113
|
+
lastAgentEvidence = agentGeneration === executionGeneration ? continuationEvidence(event.messages) : null;
|
|
1070
1114
|
// `escape` is a pi-reserved built-in shortcut (`app.interrupt`), so extensions
|
|
1071
1115
|
// cannot register it. Observe the interrupt instead: when a running turn is
|
|
1072
1116
|
// aborted (escape / ctrl+c while streaming), pause the active goal so it does
|
|
@@ -1080,6 +1124,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1080
1124
|
if (!BLOCKING_QUESTION_TOOLS.has(event.toolName)) return;
|
|
1081
1125
|
currentCtx = ctx;
|
|
1082
1126
|
const snapshot = await publishSnapshot(ctx);
|
|
1127
|
+
if (!snapshot) return;
|
|
1083
1128
|
const active = activeItemsByKey(snapshot);
|
|
1084
1129
|
if (active.size > 0) pendingQuestions.set(event.toolCallId, active);
|
|
1085
1130
|
});
|
|
@@ -1090,6 +1135,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1090
1135
|
pendingQuestions.delete(event.toolCallId);
|
|
1091
1136
|
currentCtx = ctx;
|
|
1092
1137
|
const snapshot = await publishSnapshot(ctx);
|
|
1138
|
+
if (!snapshot) return;
|
|
1093
1139
|
const finished = finishedSinceQuestion(activeAtStart, snapshot);
|
|
1094
1140
|
if (finished.length === 0) return;
|
|
1095
1141
|
// Steering is drained right after this tool batch, so the model sees the
|
|
@@ -1103,15 +1149,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1103
1149
|
});
|
|
1104
1150
|
|
|
1105
1151
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
1152
|
+
executionGeneration += 1;
|
|
1106
1153
|
currentCtx = ctx;
|
|
1107
1154
|
foregroundRunning = false;
|
|
1108
1155
|
const snapshot = await publishSnapshot(ctx);
|
|
1156
|
+
if (!snapshot) return;
|
|
1109
1157
|
const goal = getGoal(ctx);
|
|
1110
1158
|
if (!isPokeable(goal) || snapshot.backgroundRunning) {
|
|
1111
1159
|
return;
|
|
1112
1160
|
}
|
|
1161
|
+
const evidence = lastAgentEvidence;
|
|
1162
|
+
if (!evidence) {
|
|
1163
|
+
scheduleIdleContinuation(goal, ctx, "continuation", snapshot);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1113
1166
|
const previous = currentContinuationState(ctx, goal.goalId) ?? createContinuationState(goal.goalId);
|
|
1114
|
-
const evidence = lastAgentEvidence ?? continuationEvidence([]);
|
|
1115
1167
|
const repeated = previous.lastEvidenceSignature === evidence.signature;
|
|
1116
1168
|
const noProgressRetries = repeated ? previous.noProgressRetries + 1 : 0;
|
|
1117
1169
|
const blocked = repeated && noProgressRetries >= MAX_NO_PROGRESS_RETRIES;
|
|
@@ -1142,6 +1194,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1142
1194
|
});
|
|
1143
1195
|
|
|
1144
1196
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1197
|
+
executionGeneration += 1;
|
|
1198
|
+
sessionGeneration += 1;
|
|
1145
1199
|
stopPolling();
|
|
1146
1200
|
foregroundRunning = false;
|
|
1147
1201
|
pendingQuestions.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"pi-better-background-tasks": "0.6.2",
|
|
56
|
-
"pi-better-goal": "0.
|
|
56
|
+
"pi-better-goal": "0.6.0",
|
|
57
57
|
"pi-better-plan": "0.5.1",
|
|
58
58
|
"pi-better-sandbox": "0.8.0",
|
|
59
59
|
"pi-better-ssh": "0.1.1",
|