@trevonistrevon/pi-loop 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.js +58 -7
- package/package.json +1 -1
- package/src/index.ts +62 -7
package/README.md
CHANGED
|
@@ -107,6 +107,8 @@ Only task counts and the single active/next task are shown in the widget so atte
|
|
|
107
107
|
| `PI_LOOP_SCOPE` | `memory` (ephemeral), `session` (per-session file), `project` (shared) | `session` |
|
|
108
108
|
| `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
|
|
109
109
|
|
|
110
|
+
In `session` scope (default), loop and task files are saved per session ID (e.g. `.pi/tasks/tasks-<sessionId>.json`) so concurrent sessions and worktree agents do not share state. In `memory` scope nothing persists to disk.
|
|
111
|
+
|
|
110
112
|
## Limits
|
|
111
113
|
|
|
112
114
|
25 active loops, 25 running monitors. Recurring loops expire after 7 days.
|
package/dist/index.js
CHANGED
|
@@ -53,9 +53,14 @@ export default function (pi) {
|
|
|
53
53
|
return undefined;
|
|
54
54
|
return join(process.cwd(), ".pi", "loops", "loops.json");
|
|
55
55
|
}
|
|
56
|
-
function resolveTaskStorePath() {
|
|
56
|
+
function resolveTaskStorePath(sessionId) {
|
|
57
57
|
if (loopScope === "memory")
|
|
58
58
|
return undefined;
|
|
59
|
+
if (loopScope === "session" && sessionId) {
|
|
60
|
+
return join(process.cwd(), ".pi", "tasks", `tasks-${sessionId}.json`);
|
|
61
|
+
}
|
|
62
|
+
if (loopScope === "session")
|
|
63
|
+
return undefined;
|
|
59
64
|
return join(process.cwd(), ".pi", "tasks", "tasks.json");
|
|
60
65
|
}
|
|
61
66
|
let store = new LoopStore(resolveStorePath());
|
|
@@ -319,6 +324,7 @@ export default function (pi) {
|
|
|
319
324
|
let storeUpgraded = false;
|
|
320
325
|
let persistedShown = false;
|
|
321
326
|
let _latestCtx;
|
|
327
|
+
let _sessionId;
|
|
322
328
|
function upgradeStoreIfNeeded(ctx) {
|
|
323
329
|
if (storeUpgraded)
|
|
324
330
|
return;
|
|
@@ -347,6 +353,7 @@ export default function (pi) {
|
|
|
347
353
|
}
|
|
348
354
|
pi.on("turn_start", async (_event, ctx) => {
|
|
349
355
|
_latestCtx = ctx;
|
|
356
|
+
_sessionId = ctx.sessionManager.getSessionId();
|
|
350
357
|
widget.setUICtx(ctx.ui);
|
|
351
358
|
upgradeStoreIfNeeded(ctx);
|
|
352
359
|
widget.update();
|
|
@@ -381,6 +388,7 @@ export default function (pi) {
|
|
|
381
388
|
triggerSystem.stop();
|
|
382
389
|
agentRunning = false;
|
|
383
390
|
pendingNotifications.clear();
|
|
391
|
+
_sessionId = undefined;
|
|
384
392
|
const isResume = event?.reason === "resume";
|
|
385
393
|
storeUpgraded = false;
|
|
386
394
|
persistedShown = false;
|
|
@@ -918,6 +926,34 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
918
926
|
const active = loops.filter(l => l.status === "active").length;
|
|
919
927
|
ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
|
|
920
928
|
}
|
|
929
|
+
const AUTO_TASK_WORKER_THRESHOLD = 5;
|
|
930
|
+
const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.";
|
|
931
|
+
function findAutoTaskWorkerLoop() {
|
|
932
|
+
return store.list().find(entry => entry.status === "active"
|
|
933
|
+
&& entry.prompt === AUTO_TASK_WORKER_PROMPT
|
|
934
|
+
&& triggerHasEventSource(entry.trigger, "tasks:created"));
|
|
935
|
+
}
|
|
936
|
+
async function ensureAutoTaskWorkerLoop(taskStore) {
|
|
937
|
+
if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD)
|
|
938
|
+
return { created: false };
|
|
939
|
+
const existing = findAutoTaskWorkerLoop();
|
|
940
|
+
if (existing)
|
|
941
|
+
return { entry: existing, created: false };
|
|
942
|
+
const trigger = {
|
|
943
|
+
type: "hybrid",
|
|
944
|
+
cron: "*/5 * * * *",
|
|
945
|
+
event: { source: "tasks:created" },
|
|
946
|
+
debounceMs: 30000,
|
|
947
|
+
};
|
|
948
|
+
const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
|
|
949
|
+
recurring: true,
|
|
950
|
+
maxFires: 30,
|
|
951
|
+
});
|
|
952
|
+
triggerSystem.add(entry);
|
|
953
|
+
await maybeBootstrapTaskLoop(entry);
|
|
954
|
+
widget.update();
|
|
955
|
+
return { entry, created: true };
|
|
956
|
+
}
|
|
921
957
|
async function createNativeTaskInteractively(ui) {
|
|
922
958
|
if (!nativeTaskStore) {
|
|
923
959
|
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
@@ -928,8 +964,18 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
928
964
|
return;
|
|
929
965
|
const description = await ui.input("Task description") || subject;
|
|
930
966
|
const entry = nativeTaskStore.create(subject, description);
|
|
967
|
+
pi.events.emit("tasks:created", {
|
|
968
|
+
taskId: entry.id,
|
|
969
|
+
subject: entry.subject,
|
|
970
|
+
description: entry.description,
|
|
971
|
+
status: entry.status,
|
|
972
|
+
});
|
|
973
|
+
const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
|
|
931
974
|
widget.update();
|
|
932
975
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
976
|
+
if (worker.created && worker.entry) {
|
|
977
|
+
ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
|
|
978
|
+
}
|
|
933
979
|
}
|
|
934
980
|
async function viewNativeTasks(ui) {
|
|
935
981
|
if (!nativeTaskStore) {
|
|
@@ -995,7 +1041,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
995
1041
|
setTimeout(async () => {
|
|
996
1042
|
if (tasksAvailable || nativeTasksRegistered)
|
|
997
1043
|
return;
|
|
998
|
-
nativeTaskStore = new TaskStore(resolveTaskStorePath());
|
|
1044
|
+
nativeTaskStore = new TaskStore(resolveTaskStorePath(_sessionId));
|
|
999
1045
|
nativeTasksRegistered = true;
|
|
1000
1046
|
const taskStore = nativeTaskStore;
|
|
1001
1047
|
pi.registerCommand("tasks", {
|
|
@@ -1014,8 +1060,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1014
1060
|
description: entry.description,
|
|
1015
1061
|
status: entry.status,
|
|
1016
1062
|
});
|
|
1063
|
+
const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
|
|
1017
1064
|
widget.update();
|
|
1018
1065
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
1066
|
+
if (worker.created && worker.entry) {
|
|
1067
|
+
ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
|
|
1068
|
+
}
|
|
1019
1069
|
return;
|
|
1020
1070
|
}
|
|
1021
1071
|
await viewNativeTasks(ctx.ui);
|
|
@@ -1032,13 +1082,14 @@ Fields:
|
|
|
1032
1082
|
- metadata: optional tags/metadata`,
|
|
1033
1083
|
promptGuidelines: [
|
|
1034
1084
|
"Use TaskCreate to track complex multi-step work across turns.",
|
|
1085
|
+
"Break work into small, independently completable tasks. A task should be finishable in one focused session — if a task would take multiple turns, split it further.",
|
|
1035
1086
|
"TaskCreate accepts `subject` and `description` parameters only — do not invent extra fields unless the schema explicitly adds them.",
|
|
1036
1087
|
],
|
|
1037
1088
|
parameters: Type.Object({
|
|
1038
1089
|
subject: Type.String({ description: "Brief actionable title for the task" }),
|
|
1039
1090
|
description: Type.String({ description: "Detailed description of what needs to be done" }),
|
|
1040
1091
|
}),
|
|
1041
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1092
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1042
1093
|
const entry = taskStore.create(params.subject, params.description);
|
|
1043
1094
|
pi.events.emit("tasks:created", {
|
|
1044
1095
|
taskId: entry.id,
|
|
@@ -1046,12 +1097,12 @@ Fields:
|
|
|
1046
1097
|
description: entry.description,
|
|
1047
1098
|
status: entry.status,
|
|
1048
1099
|
});
|
|
1100
|
+
const worker = await ensureAutoTaskWorkerLoop(taskStore);
|
|
1049
1101
|
widget.update();
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
? `\n(${pending} pending tasks — consider creating a worker loop: LoopCreate trigger='tasks:created' recurring: true maxFires: 30 prompt='Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.')`
|
|
1102
|
+
const autoLoopMsg = worker.created && worker.entry
|
|
1103
|
+
? `\nWorker loop #${worker.entry.id} auto-created`
|
|
1053
1104
|
: "";
|
|
1054
|
-
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${
|
|
1105
|
+
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
1055
1106
|
},
|
|
1056
1107
|
});
|
|
1057
1108
|
pi.registerTool({
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -73,8 +73,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
73
73
|
return join(process.cwd(), ".pi", "loops", "loops.json");
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function resolveTaskStorePath(): string | undefined {
|
|
76
|
+
function resolveTaskStorePath(sessionId?: string): string | undefined {
|
|
77
77
|
if (loopScope === "memory") return undefined;
|
|
78
|
+
if (loopScope === "session" && sessionId) {
|
|
79
|
+
return join(process.cwd(), ".pi", "tasks", `tasks-${sessionId}.json`);
|
|
80
|
+
}
|
|
81
|
+
if (loopScope === "session") return undefined;
|
|
78
82
|
return join(process.cwd(), ".pi", "tasks", "tasks.json");
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -349,6 +353,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
353
|
let storeUpgraded = false;
|
|
350
354
|
let persistedShown = false;
|
|
351
355
|
let _latestCtx: ExtensionContext | undefined;
|
|
356
|
+
let _sessionId: string | undefined;
|
|
352
357
|
|
|
353
358
|
function upgradeStoreIfNeeded(ctx: ExtensionContext) {
|
|
354
359
|
if (storeUpgraded) return;
|
|
@@ -378,6 +383,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
383
|
|
|
379
384
|
pi.on("turn_start", async (_event, ctx) => {
|
|
380
385
|
_latestCtx = ctx;
|
|
386
|
+
_sessionId = ctx.sessionManager.getSessionId();
|
|
381
387
|
widget.setUICtx(ctx.ui);
|
|
382
388
|
upgradeStoreIfNeeded(ctx);
|
|
383
389
|
widget.update();
|
|
@@ -417,6 +423,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
417
423
|
triggerSystem.stop();
|
|
418
424
|
agentRunning = false;
|
|
419
425
|
pendingNotifications.clear();
|
|
426
|
+
_sessionId = undefined;
|
|
420
427
|
|
|
421
428
|
const isResume = event?.reason === "resume";
|
|
422
429
|
storeUpgraded = false;
|
|
@@ -998,6 +1005,39 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
998
1005
|
ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
|
|
999
1006
|
}
|
|
1000
1007
|
|
|
1008
|
+
const AUTO_TASK_WORKER_THRESHOLD = 5;
|
|
1009
|
+
const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.";
|
|
1010
|
+
|
|
1011
|
+
function findAutoTaskWorkerLoop(): LoopEntry | undefined {
|
|
1012
|
+
return store.list().find(entry =>
|
|
1013
|
+
entry.status === "active"
|
|
1014
|
+
&& entry.prompt === AUTO_TASK_WORKER_PROMPT
|
|
1015
|
+
&& triggerHasEventSource(entry.trigger, "tasks:created")
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function ensureAutoTaskWorkerLoop(taskStore: TaskStore): Promise<{ entry?: LoopEntry; created: boolean }> {
|
|
1020
|
+
if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD) return { created: false };
|
|
1021
|
+
|
|
1022
|
+
const existing = findAutoTaskWorkerLoop();
|
|
1023
|
+
if (existing) return { entry: existing, created: false };
|
|
1024
|
+
|
|
1025
|
+
const trigger: Trigger = {
|
|
1026
|
+
type: "hybrid",
|
|
1027
|
+
cron: "*/5 * * * *",
|
|
1028
|
+
event: { source: "tasks:created" },
|
|
1029
|
+
debounceMs: 30000,
|
|
1030
|
+
};
|
|
1031
|
+
const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
|
|
1032
|
+
recurring: true,
|
|
1033
|
+
maxFires: 30,
|
|
1034
|
+
});
|
|
1035
|
+
triggerSystem.add(entry);
|
|
1036
|
+
await maybeBootstrapTaskLoop(entry);
|
|
1037
|
+
widget.update();
|
|
1038
|
+
return { entry, created: true };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1001
1041
|
async function createNativeTaskInteractively(ui: ExtensionUIContext) {
|
|
1002
1042
|
if (!nativeTaskStore) {
|
|
1003
1043
|
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
@@ -1008,8 +1048,18 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1008
1048
|
if (!subject) return;
|
|
1009
1049
|
const description = await ui.input("Task description") || subject;
|
|
1010
1050
|
const entry = nativeTaskStore.create(subject, description);
|
|
1051
|
+
pi.events.emit("tasks:created", {
|
|
1052
|
+
taskId: entry.id,
|
|
1053
|
+
subject: entry.subject,
|
|
1054
|
+
description: entry.description,
|
|
1055
|
+
status: entry.status,
|
|
1056
|
+
});
|
|
1057
|
+
const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
|
|
1011
1058
|
widget.update();
|
|
1012
1059
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
1060
|
+
if (worker.created && worker.entry) {
|
|
1061
|
+
ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
|
|
1062
|
+
}
|
|
1013
1063
|
}
|
|
1014
1064
|
|
|
1015
1065
|
async function viewNativeTasks(ui: ExtensionUIContext): Promise<void> {
|
|
@@ -1076,7 +1126,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1076
1126
|
|
|
1077
1127
|
setTimeout(async () => {
|
|
1078
1128
|
if (tasksAvailable || nativeTasksRegistered) return;
|
|
1079
|
-
nativeTaskStore = new TaskStore(resolveTaskStorePath());
|
|
1129
|
+
nativeTaskStore = new TaskStore(resolveTaskStorePath(_sessionId));
|
|
1080
1130
|
nativeTasksRegistered = true;
|
|
1081
1131
|
const taskStore = nativeTaskStore;
|
|
1082
1132
|
|
|
@@ -1096,8 +1146,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1096
1146
|
description: entry.description,
|
|
1097
1147
|
status: entry.status,
|
|
1098
1148
|
});
|
|
1149
|
+
const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
|
|
1099
1150
|
widget.update();
|
|
1100
1151
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
1152
|
+
if (worker.created && worker.entry) {
|
|
1153
|
+
ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
|
|
1154
|
+
}
|
|
1101
1155
|
return;
|
|
1102
1156
|
}
|
|
1103
1157
|
await viewNativeTasks(ctx.ui);
|
|
@@ -1115,13 +1169,14 @@ Fields:
|
|
|
1115
1169
|
- metadata: optional tags/metadata`,
|
|
1116
1170
|
promptGuidelines: [
|
|
1117
1171
|
"Use TaskCreate to track complex multi-step work across turns.",
|
|
1172
|
+
"Break work into small, independently completable tasks. A task should be finishable in one focused session — if a task would take multiple turns, split it further.",
|
|
1118
1173
|
"TaskCreate accepts `subject` and `description` parameters only — do not invent extra fields unless the schema explicitly adds them.",
|
|
1119
1174
|
],
|
|
1120
1175
|
parameters: Type.Object({
|
|
1121
1176
|
subject: Type.String({ description: "Brief actionable title for the task" }),
|
|
1122
1177
|
description: Type.String({ description: "Detailed description of what needs to be done" }),
|
|
1123
1178
|
}),
|
|
1124
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1179
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1125
1180
|
const entry = taskStore.create(params.subject, params.description);
|
|
1126
1181
|
pi.events.emit("tasks:created", {
|
|
1127
1182
|
taskId: entry.id,
|
|
@@ -1129,13 +1184,13 @@ Fields:
|
|
|
1129
1184
|
description: entry.description,
|
|
1130
1185
|
status: entry.status,
|
|
1131
1186
|
});
|
|
1187
|
+
const worker = await ensureAutoTaskWorkerLoop(taskStore);
|
|
1132
1188
|
widget.update();
|
|
1133
1189
|
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
? `\n(${pending} pending tasks — consider creating a worker loop: LoopCreate trigger='tasks:created' recurring: true maxFires: 30 prompt='Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.')`
|
|
1190
|
+
const autoLoopMsg = worker.created && worker.entry
|
|
1191
|
+
? `\nWorker loop #${worker.entry.id} auto-created`
|
|
1137
1192
|
: "";
|
|
1138
|
-
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${
|
|
1193
|
+
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
1139
1194
|
},
|
|
1140
1195
|
});
|
|
1141
1196
|
|