@trevonistrevon/pi-loop 0.5.2 → 0.5.4
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/dist/commands/loop-command.js +4 -5
- package/dist/commands/tasks-command.js +3 -3
- package/dist/goal-store.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/monitor-manager.js +4 -4
- package/dist/store.js +1 -0
- package/dist/task-store.js +1 -0
- package/dist/tools/loop-tools.js +7 -5
- package/dist/tools/monitor-tools.js +3 -3
- package/dist/tools/native-task-tools.js +5 -2
- package/dist/ui/widget.js +8 -1
- package/package.json +1 -1
- package/src/commands/loop-command.ts +4 -5
- package/src/commands/tasks-command.ts +3 -3
- package/src/goal-store.ts +1 -0
- package/src/index.ts +2 -2
- package/src/monitor-manager.ts +2 -3
- package/src/store.ts +1 -0
- package/src/task-store.ts +1 -0
- package/src/tools/loop-tools.ts +6 -5
- package/src/tools/monitor-tools.ts +3 -3
- package/src/tools/native-task-tools.ts +5 -2
- package/src/ui/widget.ts +8 -1
|
@@ -36,7 +36,7 @@ export function registerLoopCommand(options) {
|
|
|
36
36
|
async function viewLoops(ui) {
|
|
37
37
|
const loops = getStore().list();
|
|
38
38
|
if (loops.length === 0) {
|
|
39
|
-
await ui.select("No
|
|
39
|
+
await ui.select("No loops configured", ["< Back"]);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
const choices = loops.map((l) => {
|
|
@@ -49,7 +49,7 @@ export function registerLoopCommand(options) {
|
|
|
49
49
|
return `${icon} #${l.id} [${l.status}] ${l.prompt.slice(0, 50)} (${triggerDesc})`;
|
|
50
50
|
});
|
|
51
51
|
choices.push("< Back");
|
|
52
|
-
const selected = await ui.select("
|
|
52
|
+
const selected = await ui.select("Loops", choices);
|
|
53
53
|
if (!selected || selected === "< Back")
|
|
54
54
|
return;
|
|
55
55
|
const match = selected.match(/#(\d+)/);
|
|
@@ -99,7 +99,7 @@ export function registerLoopCommand(options) {
|
|
|
99
99
|
const choice = await ui.select("Loop", [
|
|
100
100
|
"Create scheduled loop",
|
|
101
101
|
"Create event-triggered loop",
|
|
102
|
-
"View
|
|
102
|
+
"View loops",
|
|
103
103
|
"Settings",
|
|
104
104
|
]);
|
|
105
105
|
if (!choice)
|
|
@@ -108,7 +108,7 @@ export function registerLoopCommand(options) {
|
|
|
108
108
|
return scheduleLoop(ui);
|
|
109
109
|
if (choice.startsWith("Create event"))
|
|
110
110
|
return eventLoop(ui);
|
|
111
|
-
if (choice.startsWith("View
|
|
111
|
+
if (choice.startsWith("View loops"))
|
|
112
112
|
return viewLoops(ui);
|
|
113
113
|
return settings(ui);
|
|
114
114
|
}
|
|
@@ -136,7 +136,6 @@ export function registerLoopCommand(options) {
|
|
|
136
136
|
const choice = await ui.select("Loop mode", [
|
|
137
137
|
`Scheduled: "${trimmed.slice(0, 50)}"`,
|
|
138
138
|
`Event-triggered: "${trimmed.slice(0, 50)}"`,
|
|
139
|
-
`Self-paced: "${trimmed.slice(0, 50)}"`,
|
|
140
139
|
]);
|
|
141
140
|
if (!choice)
|
|
142
141
|
return;
|
|
@@ -28,7 +28,7 @@ export function registerTasksCommand(options) {
|
|
|
28
28
|
const backlog = await emitCreated(entry);
|
|
29
29
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
30
30
|
if (backlog.created && backlog.entry) {
|
|
31
|
-
ui.notify(`
|
|
31
|
+
ui.notify(`Backlog worker loop #${backlog.entry.id} created`, "info");
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
async function viewNativeTasks(ui) {
|
|
@@ -44,7 +44,7 @@ export function registerTasksCommand(options) {
|
|
|
44
44
|
});
|
|
45
45
|
choices.unshift("+ Create task");
|
|
46
46
|
choices.push("< Back");
|
|
47
|
-
const selected = await ui.select("
|
|
47
|
+
const selected = await ui.select("Tasks", choices);
|
|
48
48
|
if (!selected || selected === "< Back")
|
|
49
49
|
return;
|
|
50
50
|
if (selected === "+ Create task") {
|
|
@@ -107,7 +107,7 @@ export function registerTasksCommand(options) {
|
|
|
107
107
|
const backlog = await emitCreated(entry);
|
|
108
108
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
109
109
|
if (backlog.created && backlog.entry) {
|
|
110
|
-
ctx.ui.notify(`
|
|
110
|
+
ctx.ui.notify(`Backlog worker loop #${backlog.entry.id} created`, "info");
|
|
111
111
|
}
|
|
112
112
|
return;
|
|
113
113
|
}
|
package/dist/goal-store.js
CHANGED
|
@@ -7,6 +7,7 @@ const LOCK_RETRY_MS = 50;
|
|
|
7
7
|
const LOCK_MAX_RETRIES = 100;
|
|
8
8
|
const MAX_GOALS = 200;
|
|
9
9
|
function acquireLock(lockPath) {
|
|
10
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
10
11
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
11
12
|
try {
|
|
12
13
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* MonitorStop — Stop a running monitor
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
|
-
* /loop — Schedule
|
|
14
|
-
* /
|
|
13
|
+
* /loop — Schedule or manage re-wake loops: /loop [interval] [prompt]
|
|
14
|
+
* /tasks — View or manage native fallback tasks when pi-tasks is absent
|
|
15
15
|
*/
|
|
16
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
export default function (pi: ExtensionAPI): void;
|
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* MonitorStop — Stop a running monitor
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
|
-
* /loop — Schedule
|
|
14
|
-
* /
|
|
13
|
+
* /loop — Schedule or manage re-wake loops: /loop [interval] [prompt]
|
|
14
|
+
* /tasks — View or manage native fallback tasks when pi-tasks is absent
|
|
15
15
|
*/
|
|
16
16
|
import { registerLoopCommand } from "./commands/loop-command.js";
|
|
17
17
|
import { registerTasksCommand } from "./commands/tasks-command.js";
|
package/dist/monitor-manager.js
CHANGED
|
@@ -114,10 +114,8 @@ export class MonitorManager {
|
|
|
114
114
|
exitCode: code,
|
|
115
115
|
outputLines: current.outputLines,
|
|
116
116
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
callback();
|
|
120
|
-
}
|
|
117
|
+
for (const callback of bp.completionCallbacks)
|
|
118
|
+
callback();
|
|
121
119
|
bp.completionCallbacks = [];
|
|
122
120
|
for (const resolve of bp.waiters)
|
|
123
121
|
resolve();
|
|
@@ -157,6 +155,8 @@ export class MonitorManager {
|
|
|
157
155
|
monitorId: id,
|
|
158
156
|
error: err.message,
|
|
159
157
|
});
|
|
158
|
+
for (const callback of bp.completionCallbacks)
|
|
159
|
+
callback();
|
|
160
160
|
bp.completionCallbacks = [];
|
|
161
161
|
for (const resolve of bp.waiters)
|
|
162
162
|
resolve();
|
package/dist/store.js
CHANGED
|
@@ -7,6 +7,7 @@ const LOCK_RETRY_MS = 50;
|
|
|
7
7
|
const LOCK_MAX_RETRIES = 100;
|
|
8
8
|
const MAX_LOOPS = 25;
|
|
9
9
|
function acquireLock(lockPath) {
|
|
10
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
10
11
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
11
12
|
try {
|
|
12
13
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
package/dist/task-store.js
CHANGED
|
@@ -7,6 +7,7 @@ const LOCK_RETRY_MS = 50;
|
|
|
7
7
|
const LOCK_MAX_RETRIES = 100;
|
|
8
8
|
const MAX_TASKS = 200;
|
|
9
9
|
function acquireLock(lockPath) {
|
|
10
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
10
11
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
11
12
|
try {
|
|
12
13
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
package/dist/tools/loop-tools.js
CHANGED
|
@@ -96,7 +96,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
96
96
|
"## Task-driven workflows",
|
|
97
97
|
"Do not rely on a past 'tasks:created' event to replay. If tasks already exist, bootstrap the first pass in the current turn or use a hybrid/event loop that can catch future task creation and a cron safety-net.",
|
|
98
98
|
"Use autoTask only when you want the loop itself to create a task on each fire. For processing an existing task backlog, leave autoTask off and have the loop run TaskList to pick the next pending task.",
|
|
99
|
-
"Set taskBacklog: true for
|
|
99
|
+
"Set taskBacklog: true for backlog worker loops that process the existing pending queue. Backlog worker loops bootstrap against existing pending tasks and auto-delete when the queue reaches zero.",
|
|
100
100
|
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
101
101
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
102
102
|
],
|
|
@@ -170,10 +170,10 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
170
170
|
return Promise.resolve(textResult(`Loop #${entry.id} created: ${entry.prompt.slice(0, 60)}\n` +
|
|
171
171
|
`Trigger: ${triggerDesc}\n` +
|
|
172
172
|
`Recurring: ${entry.recurring}\n` +
|
|
173
|
-
(entry.autoTask ? "Auto-task: enabled\n" : "") +
|
|
174
|
-
(entry.taskBacklog ? "
|
|
175
|
-
(bootstrapped ? "
|
|
176
|
-
(isTaskSystemReady() ? "" : "
|
|
173
|
+
(entry.autoTask ? "Auto-create task: enabled\n" : "") +
|
|
174
|
+
(entry.taskBacklog ? "Backlog worker: enabled\n" : "") +
|
|
175
|
+
(bootstrapped ? "Backlog: initial wake queued for existing pending tasks\n" : "") +
|
|
176
|
+
(isTaskSystemReady() ? "" : "Task system: not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available\n") +
|
|
177
177
|
`ID: ${entry.id} (use LoopDelete to cancel)`));
|
|
178
178
|
},
|
|
179
179
|
});
|
|
@@ -205,6 +205,8 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
205
205
|
}
|
|
206
206
|
if (entry.autoTask)
|
|
207
207
|
line += " [auto-task]";
|
|
208
|
+
if (entry.taskBacklog)
|
|
209
|
+
line += " [backlog-worker]";
|
|
208
210
|
lines.push(line);
|
|
209
211
|
}
|
|
210
212
|
return Promise.resolve(textResult(lines.join("\n")));
|
|
@@ -55,7 +55,7 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
55
55
|
const doneTrigger = { type: "event", source: "monitor:done", filter: JSON.stringify({ monitorId: entry.id }) };
|
|
56
56
|
const doneLoop = getStore().create(doneTrigger, params.onDone, { recurring: false });
|
|
57
57
|
handleMonitorDoneLoop(doneLoop, entry.id);
|
|
58
|
-
onDoneMsg = `\
|
|
58
|
+
onDoneMsg = `\nCompletion wake loop #${doneLoop.id}: fires when the monitor completes — no polling needed`;
|
|
59
59
|
}
|
|
60
60
|
return Promise.resolve(textResult(`Monitor #${entry.id} started: ${entry.command.slice(0, 60)}\n` +
|
|
61
61
|
`Output stream: monitor:output (monitorId: ${entry.id})\n` +
|
|
@@ -70,10 +70,10 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
70
70
|
execute() {
|
|
71
71
|
const monitors = getMonitorManager().list();
|
|
72
72
|
if (monitors.length === 0)
|
|
73
|
-
return Promise.resolve(textResult("No monitors
|
|
73
|
+
return Promise.resolve(textResult("No monitors."));
|
|
74
74
|
const lines = [];
|
|
75
75
|
for (const m of monitors) {
|
|
76
|
-
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "
|
|
76
|
+
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "x";
|
|
77
77
|
const age = Date.now() - m.startedAt;
|
|
78
78
|
const ageStr = formatRemaining(age);
|
|
79
79
|
let line = `${icon} #${m.id} [${m.status}] ${m.command.slice(0, 60)} — ${m.outputLines} lines (${ageStr})`;
|
|
@@ -15,7 +15,10 @@ Fields:
|
|
|
15
15
|
promptGuidelines: [
|
|
16
16
|
"Use TaskCreate to track complex multi-step work across turns.",
|
|
17
17
|
"When the user gives a broad goal, use multiple TaskCreate calls to decompose it into a small backlog of concrete tasks rather than one oversized task.",
|
|
18
|
+
"If the user supplies a shared goal or meta-goal, preserve it explicitly using the user's wording and tie each created task back to that goal in its description.",
|
|
19
|
+
"If several tasks share one goal, keep subjects short and put the shared goal in the first sentence of each description or as an equivalent explicit framing.",
|
|
18
20
|
"Prefer 2-5 tasks that separate investigation, implementation, validation, and reporting or commit-prep when those phases are distinct.",
|
|
21
|
+
"When the user asks to break work into tasks, create the backlog directly and do not pivot to loops, monitors, or other automation unless the user also asked for ongoing automation.",
|
|
19
22
|
"Make each `subject` a short verb-object action.",
|
|
20
23
|
"Make each `description` include the expected artifact, outcome, or done condition so another turn can pick the task up cleanly.",
|
|
21
24
|
"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.",
|
|
@@ -36,7 +39,7 @@ Fields:
|
|
|
36
39
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
37
40
|
updateWidget();
|
|
38
41
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
39
|
-
? `\
|
|
42
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
40
43
|
: "";
|
|
41
44
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
42
45
|
},
|
|
@@ -105,7 +108,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
105
108
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
106
109
|
const statusMsg = status ? ` → ${status}` : "";
|
|
107
110
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
108
|
-
? `\
|
|
111
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
109
112
|
: "";
|
|
110
113
|
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
|
|
111
114
|
},
|
package/dist/ui/widget.js
CHANGED
|
@@ -22,7 +22,7 @@ export class LoopWidget {
|
|
|
22
22
|
this.uiCtx.setStatus("loops", this.computeStatus());
|
|
23
23
|
}
|
|
24
24
|
computeStatus() {
|
|
25
|
-
const loops = this.store.list().filter(
|
|
25
|
+
const loops = this.store.list().filter(isStatusVisibleLoop);
|
|
26
26
|
const monitors = this.monitorManager.list();
|
|
27
27
|
const taskSummary = this.taskSummaryProvider?.() ?? { count: 0 };
|
|
28
28
|
if (loops.length === 0 && monitors.length === 0 && taskSummary.count === 0) {
|
|
@@ -47,3 +47,10 @@ export class LoopWidget {
|
|
|
47
47
|
function formatCount(count, noun) {
|
|
48
48
|
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
49
49
|
}
|
|
50
|
+
function isStatusVisibleLoop(loop) {
|
|
51
|
+
if (loop.status !== "active")
|
|
52
|
+
return false;
|
|
53
|
+
if (loop.recurring)
|
|
54
|
+
return true;
|
|
55
|
+
return !(loop.trigger.type === "event" && loop.trigger.source === "monitor:done");
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -66,7 +66,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
66
66
|
async function viewLoops(ui: ExtensionUIContext) {
|
|
67
67
|
const loops = getStore().list();
|
|
68
68
|
if (loops.length === 0) {
|
|
69
|
-
await ui.select("No
|
|
69
|
+
await ui.select("No loops configured", ["< Back"]);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -81,7 +81,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
81
81
|
});
|
|
82
82
|
choices.push("< Back");
|
|
83
83
|
|
|
84
|
-
const selected = await ui.select("
|
|
84
|
+
const selected = await ui.select("Loops", choices);
|
|
85
85
|
if (!selected || selected === "< Back") return;
|
|
86
86
|
|
|
87
87
|
const match = selected.match(/#(\d+)/);
|
|
@@ -136,14 +136,14 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
136
136
|
const choice = await ui.select("Loop", [
|
|
137
137
|
"Create scheduled loop",
|
|
138
138
|
"Create event-triggered loop",
|
|
139
|
-
"View
|
|
139
|
+
"View loops",
|
|
140
140
|
"Settings",
|
|
141
141
|
]);
|
|
142
142
|
|
|
143
143
|
if (!choice) return;
|
|
144
144
|
if (choice.startsWith("Create scheduled")) return scheduleLoop(ui);
|
|
145
145
|
if (choice.startsWith("Create event")) return eventLoop(ui);
|
|
146
|
-
if (choice.startsWith("View
|
|
146
|
+
if (choice.startsWith("View loops")) return viewLoops(ui);
|
|
147
147
|
return settings(ui);
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -173,7 +173,6 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
173
173
|
const choice = await ui.select("Loop mode", [
|
|
174
174
|
`Scheduled: "${trimmed.slice(0, 50)}"`,
|
|
175
175
|
`Event-triggered: "${trimmed.slice(0, 50)}"`,
|
|
176
|
-
`Self-paced: "${trimmed.slice(0, 50)}"`,
|
|
177
176
|
]);
|
|
178
177
|
|
|
179
178
|
if (!choice) return;
|
|
@@ -45,7 +45,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
45
45
|
const backlog = await emitCreated(entry);
|
|
46
46
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
47
47
|
if (backlog.created && backlog.entry) {
|
|
48
|
-
ui.notify(`
|
|
48
|
+
ui.notify(`Backlog worker loop #${backlog.entry.id} created`, "info");
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -64,7 +64,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
64
64
|
choices.unshift("+ Create task");
|
|
65
65
|
choices.push("< Back");
|
|
66
66
|
|
|
67
|
-
const selected = await ui.select("
|
|
67
|
+
const selected = await ui.select("Tasks", choices);
|
|
68
68
|
if (!selected || selected === "< Back") return;
|
|
69
69
|
if (selected === "+ Create task") {
|
|
70
70
|
await createNativeTaskInteractively(ui);
|
|
@@ -125,7 +125,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
125
125
|
const backlog = await emitCreated(entry);
|
|
126
126
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
127
127
|
if (backlog.created && backlog.entry) {
|
|
128
|
-
ctx.ui.notify(`
|
|
128
|
+
ctx.ui.notify(`Backlog worker loop #${backlog.entry.id} created`, "info");
|
|
129
129
|
}
|
|
130
130
|
return;
|
|
131
131
|
}
|
package/src/goal-store.ts
CHANGED
|
@@ -17,6 +17,7 @@ const LOCK_MAX_RETRIES = 100;
|
|
|
17
17
|
const MAX_GOALS = 200;
|
|
18
18
|
|
|
19
19
|
function acquireLock(lockPath: string): void {
|
|
20
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
20
21
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
21
22
|
try {
|
|
22
23
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* MonitorStop — Stop a running monitor
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
|
-
* /loop — Schedule
|
|
14
|
-
* /
|
|
13
|
+
* /loop — Schedule or manage re-wake loops: /loop [interval] [prompt]
|
|
14
|
+
* /tasks — View or manage native fallback tasks when pi-tasks is absent
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
package/src/monitor-manager.ts
CHANGED
|
@@ -128,9 +128,7 @@ export class MonitorManager {
|
|
|
128
128
|
exitCode: code,
|
|
129
129
|
outputLines: current.outputLines,
|
|
130
130
|
});
|
|
131
|
-
|
|
132
|
-
for (const callback of bp.completionCallbacks) callback();
|
|
133
|
-
}
|
|
131
|
+
for (const callback of bp.completionCallbacks) callback();
|
|
134
132
|
bp.completionCallbacks = [];
|
|
135
133
|
for (const resolve of bp.waiters) resolve();
|
|
136
134
|
bp.waiters = [];
|
|
@@ -171,6 +169,7 @@ export class MonitorManager {
|
|
|
171
169
|
monitorId: id,
|
|
172
170
|
error: err.message,
|
|
173
171
|
});
|
|
172
|
+
for (const callback of bp.completionCallbacks) callback();
|
|
174
173
|
bp.completionCallbacks = [];
|
|
175
174
|
for (const resolve of bp.waiters) resolve();
|
|
176
175
|
bp.waiters = [];
|
package/src/store.ts
CHANGED
|
@@ -10,6 +10,7 @@ const LOCK_MAX_RETRIES = 100;
|
|
|
10
10
|
const MAX_LOOPS = 25;
|
|
11
11
|
|
|
12
12
|
function acquireLock(lockPath: string): void {
|
|
13
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
13
14
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
14
15
|
try {
|
|
15
16
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
package/src/task-store.ts
CHANGED
|
@@ -10,6 +10,7 @@ const LOCK_MAX_RETRIES = 100;
|
|
|
10
10
|
const MAX_TASKS = 200;
|
|
11
11
|
|
|
12
12
|
function acquireLock(lockPath: string): void {
|
|
13
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
13
14
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
14
15
|
try {
|
|
15
16
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
package/src/tools/loop-tools.ts
CHANGED
|
@@ -149,7 +149,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
149
149
|
"## Task-driven workflows",
|
|
150
150
|
"Do not rely on a past 'tasks:created' event to replay. If tasks already exist, bootstrap the first pass in the current turn or use a hybrid/event loop that can catch future task creation and a cron safety-net.",
|
|
151
151
|
"Use autoTask only when you want the loop itself to create a task on each fire. For processing an existing task backlog, leave autoTask off and have the loop run TaskList to pick the next pending task.",
|
|
152
|
-
"Set taskBacklog: true for
|
|
152
|
+
"Set taskBacklog: true for backlog worker loops that process the existing pending queue. Backlog worker loops bootstrap against existing pending tasks and auto-delete when the queue reaches zero.",
|
|
153
153
|
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
154
154
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
155
155
|
],
|
|
@@ -229,10 +229,10 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
229
229
|
`Loop #${entry.id} created: ${entry.prompt.slice(0, 60)}\n` +
|
|
230
230
|
`Trigger: ${triggerDesc}\n` +
|
|
231
231
|
`Recurring: ${entry.recurring}\n` +
|
|
232
|
-
(entry.autoTask ? "Auto-task: enabled\n" : "") +
|
|
233
|
-
(entry.taskBacklog ? "
|
|
234
|
-
(bootstrapped ? "
|
|
235
|
-
(isTaskSystemReady() ? "" : "
|
|
232
|
+
(entry.autoTask ? "Auto-create task: enabled\n" : "") +
|
|
233
|
+
(entry.taskBacklog ? "Backlog worker: enabled\n" : "") +
|
|
234
|
+
(bootstrapped ? "Backlog: initial wake queued for existing pending tasks\n" : "") +
|
|
235
|
+
(isTaskSystemReady() ? "" : "Task system: not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available\n") +
|
|
236
236
|
`ID: ${entry.id} (use LoopDelete to cancel)`
|
|
237
237
|
));
|
|
238
238
|
},
|
|
@@ -266,6 +266,7 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
266
266
|
line += ` next: ${formatRemaining(remaining)}`;
|
|
267
267
|
}
|
|
268
268
|
if (entry.autoTask) line += " [auto-task]";
|
|
269
|
+
if (entry.taskBacklog) line += " [backlog-worker]";
|
|
269
270
|
lines.push(line);
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -85,7 +85,7 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
85
85
|
const doneTrigger: Trigger = { type: "event", source: "monitor:done", filter: JSON.stringify({ monitorId: entry.id }) };
|
|
86
86
|
const doneLoop = getStore().create(doneTrigger, params.onDone, { recurring: false });
|
|
87
87
|
handleMonitorDoneLoop(doneLoop, entry.id);
|
|
88
|
-
onDoneMsg = `\
|
|
88
|
+
onDoneMsg = `\nCompletion wake loop #${doneLoop.id}: fires when the monitor completes — no polling needed`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
return Promise.resolve(textResult(
|
|
@@ -103,11 +103,11 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
103
103
|
parameters: Type.Object({}),
|
|
104
104
|
execute() {
|
|
105
105
|
const monitors = getMonitorManager().list();
|
|
106
|
-
if (monitors.length === 0) return Promise.resolve(textResult("No monitors
|
|
106
|
+
if (monitors.length === 0) return Promise.resolve(textResult("No monitors."));
|
|
107
107
|
|
|
108
108
|
const lines: string[] = [];
|
|
109
109
|
for (const m of monitors) {
|
|
110
|
-
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "
|
|
110
|
+
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "x";
|
|
111
111
|
const age = Date.now() - m.startedAt;
|
|
112
112
|
const ageStr = formatRemaining(age);
|
|
113
113
|
let line = `${icon} #${m.id} [${m.status}] ${m.command.slice(0, 60)} — ${m.outputLines} lines (${ageStr})`;
|
|
@@ -32,7 +32,10 @@ Fields:
|
|
|
32
32
|
promptGuidelines: [
|
|
33
33
|
"Use TaskCreate to track complex multi-step work across turns.",
|
|
34
34
|
"When the user gives a broad goal, use multiple TaskCreate calls to decompose it into a small backlog of concrete tasks rather than one oversized task.",
|
|
35
|
+
"If the user supplies a shared goal or meta-goal, preserve it explicitly using the user's wording and tie each created task back to that goal in its description.",
|
|
36
|
+
"If several tasks share one goal, keep subjects short and put the shared goal in the first sentence of each description or as an equivalent explicit framing.",
|
|
35
37
|
"Prefer 2-5 tasks that separate investigation, implementation, validation, and reporting or commit-prep when those phases are distinct.",
|
|
38
|
+
"When the user asks to break work into tasks, create the backlog directly and do not pivot to loops, monitors, or other automation unless the user also asked for ongoing automation.",
|
|
36
39
|
"Make each `subject` a short verb-object action.",
|
|
37
40
|
"Make each `description` include the expected artifact, outcome, or done condition so another turn can pick the task up cleanly.",
|
|
38
41
|
"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.",
|
|
@@ -54,7 +57,7 @@ Fields:
|
|
|
54
57
|
updateWidget();
|
|
55
58
|
|
|
56
59
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
57
|
-
? `\
|
|
60
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
58
61
|
: "";
|
|
59
62
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
60
63
|
},
|
|
@@ -121,7 +124,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
121
124
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
122
125
|
const statusMsg = status ? ` → ${status}` : "";
|
|
123
126
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
124
|
-
? `\
|
|
127
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
125
128
|
: "";
|
|
126
129
|
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
|
|
127
130
|
},
|
package/src/ui/widget.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { MonitorManager } from "../monitor-manager.js";
|
|
3
3
|
import type { LoopStore } from "../store.js";
|
|
4
|
+
import type { LoopEntry } from "../types.js";
|
|
4
5
|
|
|
5
6
|
interface TaskSummary {
|
|
6
7
|
count: number;
|
|
@@ -34,7 +35,7 @@ export class LoopWidget {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
private computeStatus(): string | undefined {
|
|
37
|
-
const loops = this.store.list().filter(
|
|
38
|
+
const loops = this.store.list().filter(isStatusVisibleLoop);
|
|
38
39
|
const monitors = this.monitorManager.list();
|
|
39
40
|
const taskSummary = this.taskSummaryProvider?.() ?? { count: 0 };
|
|
40
41
|
|
|
@@ -60,3 +61,9 @@ export class LoopWidget {
|
|
|
60
61
|
function formatCount(count: number, noun: string): string {
|
|
61
62
|
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
62
63
|
}
|
|
64
|
+
|
|
65
|
+
function isStatusVisibleLoop(loop: LoopEntry): boolean {
|
|
66
|
+
if (loop.status !== "active") return false;
|
|
67
|
+
if (loop.recurring) return true;
|
|
68
|
+
return !(loop.trigger.type === "event" && loop.trigger.source === "monitor:done");
|
|
69
|
+
}
|