pi-orchestrator-config 4.2.0 → 4.3.2
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 +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- package/extensions/coms/coms-net.ts +0 -1589
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extensions/pitasks/index.ts — Owned task management system.
|
|
3
|
+
* Based on @tintinweb/pi-tasks (MIT license).
|
|
4
|
+
* Removed: TaskExecute, auto-cascade, subagent RPC, reminder cadence.
|
|
5
|
+
* Added: store export for coms direct access.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { TaskStore } from "./task-store.js";
|
|
11
|
+
import { TaskWidget } from "./task-widget.js";
|
|
12
|
+
import { registerTaskTools } from "./task-tools.js";
|
|
13
|
+
import { createLogger } from "../shared/logger.js";
|
|
14
|
+
import { getSetting } from "../orchestrator/project-settings.js";
|
|
15
|
+
import { shouldSkipOneshotRegister } from "../shared/oneshot.js";
|
|
16
|
+
import {
|
|
17
|
+
pendingReminderContent,
|
|
18
|
+
staleReminderContent,
|
|
19
|
+
shouldFireReminder,
|
|
20
|
+
selectActiveTasks,
|
|
21
|
+
} from "./reminders.js";
|
|
22
|
+
|
|
23
|
+
// Re-export reminder pure helpers so consumers (and tests) can import from index.
|
|
24
|
+
export {
|
|
25
|
+
pendingReminderContent,
|
|
26
|
+
staleReminderContent,
|
|
27
|
+
shouldFireReminder,
|
|
28
|
+
selectActiveTasks,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const log = createLogger("pitasks");
|
|
32
|
+
|
|
33
|
+
// Module-level refs — survive closure replacement on /reload
|
|
34
|
+
let currentUiCtx: any = null;
|
|
35
|
+
let currentWidget: TaskWidget | null = null;
|
|
36
|
+
|
|
37
|
+
/** Exported store instance — set during extension init. */
|
|
38
|
+
export let taskStore: TaskStore;
|
|
39
|
+
|
|
40
|
+
/** Create a task directly — for use by other extensions (e.g., coms auto-create). */
|
|
41
|
+
export function createTask(subject: string, description: string, createdBy?: any, metadata?: Record<string, any>): any {
|
|
42
|
+
return taskStore.create(subject, description, createdBy || { type: "local", origin: "system", session: "", project: "" }, undefined, metadata);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Get a task by ID — for use by other extensions (e.g., coms heartbeat). */
|
|
46
|
+
export function getTask(id: string): any {
|
|
47
|
+
return taskStore?.get(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** List all tasks — for use by other extensions (e.g., coms heartbeat). */
|
|
51
|
+
export function listTasks(): any[] {
|
|
52
|
+
return taskStore?.list() ?? [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Update a task — for use by other extensions. */
|
|
56
|
+
export function updateTask(id: string, fields: any): any {
|
|
57
|
+
return taskStore.update(id, fields);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Delete a task — for use by other extensions. */
|
|
61
|
+
export function deleteTask(id: string): boolean {
|
|
62
|
+
return taskStore.delete(id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Session-targeted task operations (for cross-session/peer access) ────
|
|
66
|
+
|
|
67
|
+
/** Create a task on a specific session's store. Used by coms for sender-side task creation. */
|
|
68
|
+
export function createTaskForSession(sessionId: string, subject: string, description: string, createdBy?: any, metadata?: Record<string, any>, targetCwd?: string, activeForm?: string): any {
|
|
69
|
+
const base = targetCwd || process.cwd();
|
|
70
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
71
|
+
const store = new TaskStore(storePath);
|
|
72
|
+
return store.create(subject, description, createdBy || { type: "local", origin: "system", session: "", project: "" }, activeForm, metadata);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Create multiple tasks on a specific session's store. Used by coms for bulk task creation. */
|
|
76
|
+
export function createTasksForSession(sessionId: string, tasks: Array<{ subject: string; description: string; createdBy: any; blockedBy?: string[]; activeForm?: string; metadata?: Record<string, any> }>, targetCwd?: string): any[] {
|
|
77
|
+
const base = targetCwd || process.cwd();
|
|
78
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
79
|
+
const store = new TaskStore(storePath);
|
|
80
|
+
return store.createTasks(tasks);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Get a task from a specific session's store. */
|
|
84
|
+
export function getTaskForSession(sessionId: string, taskId: string, targetCwd?: string): any {
|
|
85
|
+
const base = targetCwd || process.cwd();
|
|
86
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
87
|
+
const store = new TaskStore(storePath);
|
|
88
|
+
return store.get(taskId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** List tasks from a specific session's store. */
|
|
92
|
+
export function listTasksForSession(sessionId: string, targetCwd?: string): any[] {
|
|
93
|
+
const base = targetCwd || process.cwd();
|
|
94
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
95
|
+
const store = new TaskStore(storePath);
|
|
96
|
+
return store.list();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Update a task on a specific session's store. */
|
|
100
|
+
export function updateTaskForSession(sessionId: string, taskId: string, fields: any, targetCwd?: string): any {
|
|
101
|
+
const base = targetCwd || process.cwd();
|
|
102
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
103
|
+
const store = new TaskStore(storePath);
|
|
104
|
+
return store.update(taskId, fields);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Update multiple tasks on a specific session's store. Used by coms for bulk updates. */
|
|
108
|
+
export function updateTasksForSession(sessionId: string, updates: Array<{ id: string; fields: Record<string, any> }>, targetCwd?: string): any[] {
|
|
109
|
+
const base = targetCwd || process.cwd();
|
|
110
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
111
|
+
const store = new TaskStore(storePath);
|
|
112
|
+
return store.updateTasks(updates);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Delete a task from a specific session's store. */
|
|
116
|
+
export function deleteTaskForSession(sessionId: string, taskId: string, targetCwd?: string): boolean {
|
|
117
|
+
const base = targetCwd || process.cwd();
|
|
118
|
+
const storePath = join(base, ".pi", "tasks", `tasks-${sessionId.replace(/[/\\]/g, "_")}.json`);
|
|
119
|
+
const store = new TaskStore(storePath);
|
|
120
|
+
return store.delete(taskId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default function (pi: ExtensionAPI) {
|
|
124
|
+
if (process.env.PI_SUBAGENT_CHILD === "1") return;
|
|
125
|
+
if (shouldSkipOneshotRegister(log)) return;
|
|
126
|
+
let shuttingDown = false;
|
|
127
|
+
const taskScope = "session";
|
|
128
|
+
const piTasks = process.env.PI_TASKS;
|
|
129
|
+
|
|
130
|
+
function resolveStorePath(sessionId?: string): string | undefined {
|
|
131
|
+
if (piTasks === "off") return undefined;
|
|
132
|
+
if (piTasks?.startsWith("/")) return piTasks;
|
|
133
|
+
if (piTasks?.startsWith(".")) return resolve(piTasks);
|
|
134
|
+
if (piTasks) return piTasks;
|
|
135
|
+
if (taskScope === "session" && sessionId) {
|
|
136
|
+
return join(process.cwd(), ".pi", "tasks", `tasks-${sessionId}.json`);
|
|
137
|
+
}
|
|
138
|
+
if (taskScope === "session") return undefined;
|
|
139
|
+
return join(process.cwd(), ".pi", "tasks", "tasks.json");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let store = new TaskStore(resolveStorePath());
|
|
143
|
+
taskStore = store;
|
|
144
|
+
const widget = new TaskWidget(store);
|
|
145
|
+
currentWidget = widget;
|
|
146
|
+
const instanceId = Math.random().toString(36).slice(2, 8);
|
|
147
|
+
(globalThis as any).__pitasks_active_instance = instanceId;
|
|
148
|
+
|
|
149
|
+
let storeUpgraded = false;
|
|
150
|
+
let persistedTasksShown = false;
|
|
151
|
+
|
|
152
|
+
function upgradeStoreIfNeeded(ctx: any): void {
|
|
153
|
+
if (storeUpgraded) return;
|
|
154
|
+
if (taskScope === "session" && !piTasks) {
|
|
155
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
156
|
+
const path = resolveStorePath(sessionId);
|
|
157
|
+
store = new TaskStore(path);
|
|
158
|
+
taskStore = store;
|
|
159
|
+
widget.setStore(store);
|
|
160
|
+
store.setOnChange(() => { widget.update(); });
|
|
161
|
+
log.debug("store upgraded", path);
|
|
162
|
+
}
|
|
163
|
+
storeUpgraded = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function showPersistedTasks(isResume = false): void {
|
|
167
|
+
if (persistedTasksShown) return;
|
|
168
|
+
persistedTasksShown = true;
|
|
169
|
+
const tasks = store.list();
|
|
170
|
+
if (tasks.length > 0) {
|
|
171
|
+
if (!isResume && tasks.every(t => t.status === "completed")) {
|
|
172
|
+
store.clearCompleted();
|
|
173
|
+
if (taskScope === "session") store.deleteFileIfEmpty();
|
|
174
|
+
} else {
|
|
175
|
+
widget.update();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Register tools
|
|
181
|
+
registerTaskTools(pi, () => store, widget);
|
|
182
|
+
|
|
183
|
+
// Time-based GC: auto-clear completed tasks after configured minutes
|
|
184
|
+
let gcTimer: ReturnType<typeof setInterval> | null = null;
|
|
185
|
+
const startGcTimer = () => {
|
|
186
|
+
if (gcTimer) clearInterval(gcTimer);
|
|
187
|
+
gcTimer = setInterval(() => {
|
|
188
|
+
try {
|
|
189
|
+
if (!getSetting(process.cwd(), "task_auto_clear_enabled")) return;
|
|
190
|
+
const minutes = getSetting(process.cwd(), "task_auto_clear_minutes");
|
|
191
|
+
const threshold = minutes * 60000;
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
const tasks = store.list();
|
|
194
|
+
let cleared = false;
|
|
195
|
+
for (const task of tasks) {
|
|
196
|
+
if (task.status === "completed" && task.statusHistory?.completed_at) {
|
|
197
|
+
if (now - new Date(task.statusHistory.completed_at).getTime() > threshold) {
|
|
198
|
+
store.delete(task.id);
|
|
199
|
+
cleared = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (cleared) widget.update();
|
|
204
|
+
} catch {}
|
|
205
|
+
}, 60000);
|
|
206
|
+
if (gcTimer.unref) gcTimer.unref();
|
|
207
|
+
};
|
|
208
|
+
startGcTimer();
|
|
209
|
+
|
|
210
|
+
// Time-based reminder: periodic nudge when tasks exist but none in_progress
|
|
211
|
+
let reminderTimer: ReturnType<typeof setInterval> | null = null;
|
|
212
|
+
let lastReminderAt = 0;
|
|
213
|
+
let lastStaleReminderAt = 0;
|
|
214
|
+
let agentBusy = false; // true while the agent is actively running a turn
|
|
215
|
+
const startReminderTimer = () => {
|
|
216
|
+
if (reminderTimer) clearInterval(reminderTimer);
|
|
217
|
+
reminderTimer = setInterval(() => {
|
|
218
|
+
// Pending-only reminder: nudge when tasks exist but none in_progress
|
|
219
|
+
try {
|
|
220
|
+
if (getSetting(process.cwd(), "task_reminder_enabled")) {
|
|
221
|
+
const minutes = getSetting(process.cwd(), "task_reminder_interval_minutes");
|
|
222
|
+
const threshold = minutes * 60000;
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (now - lastReminderAt >= threshold) {
|
|
225
|
+
const tasks = store.list();
|
|
226
|
+
const { active, anyInProgress } = selectActiveTasks(tasks);
|
|
227
|
+
if (active.length > 0 && !anyInProgress) {
|
|
228
|
+
if (!shouldFireReminder(agentBusy, active.length)) {
|
|
229
|
+
log.debug("reminder_skipped", "busy");
|
|
230
|
+
} else {
|
|
231
|
+
lastReminderAt = now;
|
|
232
|
+
try {
|
|
233
|
+
pi.sendMessage({
|
|
234
|
+
customType: "task-focus-reminder",
|
|
235
|
+
content: pendingReminderContent(active.length),
|
|
236
|
+
display: true,
|
|
237
|
+
}, { triggerTurn: true });
|
|
238
|
+
log.info("reminder_fired", "active", active.length);
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {}
|
|
245
|
+
|
|
246
|
+
// Stale in_progress reminder
|
|
247
|
+
try {
|
|
248
|
+
if (!getSetting(process.cwd(), "task_stale_in_progress_enabled")) return;
|
|
249
|
+
const staleMinutes = getSetting(process.cwd(), "task_stale_in_progress_minutes");
|
|
250
|
+
const staleThreshold = staleMinutes * 60000;
|
|
251
|
+
const now2 = Date.now();
|
|
252
|
+
if (now2 - lastStaleReminderAt < staleThreshold) return;
|
|
253
|
+
const allTasks = store.list();
|
|
254
|
+
const staleTasks = allTasks.filter(t =>
|
|
255
|
+
t.status === "in_progress" &&
|
|
256
|
+
t.statusHistory?.in_progress_at &&
|
|
257
|
+
now2 - new Date(t.statusHistory.in_progress_at).getTime() > staleThreshold
|
|
258
|
+
);
|
|
259
|
+
if (staleTasks.length > 0) {
|
|
260
|
+
if (!shouldFireReminder(agentBusy, staleTasks.length)) {
|
|
261
|
+
log.debug("reminder_skipped", "busy");
|
|
262
|
+
} else {
|
|
263
|
+
lastStaleReminderAt = now2;
|
|
264
|
+
try {
|
|
265
|
+
pi.sendMessage({
|
|
266
|
+
customType: "task-focus-reminder",
|
|
267
|
+
content: staleReminderContent(staleTasks.length),
|
|
268
|
+
display: true,
|
|
269
|
+
}, { triggerTurn: true });
|
|
270
|
+
log.info("stale_reminder_fired", "stale", staleTasks.length);
|
|
271
|
+
} catch {}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch {}
|
|
275
|
+
}, 60000);
|
|
276
|
+
if (reminderTimer.unref) reminderTimer.unref();
|
|
277
|
+
};
|
|
278
|
+
startReminderTimer();
|
|
279
|
+
|
|
280
|
+
pi.on("session_shutdown", () => {
|
|
281
|
+
shuttingDown = true;
|
|
282
|
+
if (gcTimer) { clearInterval(gcTimer); gcTimer = null; }
|
|
283
|
+
if (reminderTimer) { clearInterval(reminderTimer); reminderTimer = null; }
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
pi.on("turn_start", async (_event: any, ctx: any) => {
|
|
287
|
+
if (shuttingDown) return;
|
|
288
|
+
agentBusy = true;
|
|
289
|
+
log.debug("agentBusy", "turn_start");
|
|
290
|
+
widget.setUICtx(ctx.ui);
|
|
291
|
+
currentUiCtx = ctx.ui;
|
|
292
|
+
upgradeStoreIfNeeded(ctx);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
pi.on("turn_end", async (event: any) => {
|
|
296
|
+
if (shuttingDown) return;
|
|
297
|
+
agentBusy = false;
|
|
298
|
+
log.debug("agentBusy", "turn_end");
|
|
299
|
+
const msg = (event as any).message;
|
|
300
|
+
if (msg?.role === "assistant" && msg.usage) {
|
|
301
|
+
widget.addTokenUsage(msg.usage.input ?? 0, msg.usage.output ?? 0);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// agent_settled is guaranteed to fire even if turn_end is skipped (abort/error).
|
|
306
|
+
// Reset agentBusy defensively so reminders are never suppressed forever.
|
|
307
|
+
pi.on("agent_settled", async () => {
|
|
308
|
+
if (shuttingDown) return;
|
|
309
|
+
agentBusy = false;
|
|
310
|
+
log.debug("agentBusy", "agent_settled reset");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
pi.on("session_start", async (event: any, ctx: any) => {
|
|
314
|
+
shuttingDown = false;
|
|
315
|
+
agentBusy = false;
|
|
316
|
+
log.debug("agentBusy", "session_start reset");
|
|
317
|
+
widget.setUICtx(ctx.ui);
|
|
318
|
+
currentUiCtx = ctx.ui;
|
|
319
|
+
const reason = (event as any).reason;
|
|
320
|
+
log.debug("session_start", reason);
|
|
321
|
+
const isSwitch = reason === "new" || reason === "resume" || reason === "fork";
|
|
322
|
+
const forkSeed = reason === "fork" ? store.snapshot() : undefined;
|
|
323
|
+
if (isSwitch) {
|
|
324
|
+
storeUpgraded = false;
|
|
325
|
+
persistedTasksShown = false;
|
|
326
|
+
if (reason === "new") store.clearAll();
|
|
327
|
+
}
|
|
328
|
+
// Re-register command on reload to replace stale registration from old runtime
|
|
329
|
+
if (reason === "reload") {
|
|
330
|
+
log.debug("session_start reload — re-registering command");
|
|
331
|
+
registerTasksCommand();
|
|
332
|
+
}
|
|
333
|
+
upgradeStoreIfNeeded(ctx);
|
|
334
|
+
if (forkSeed?.tasks.length) store.seed(forkSeed);
|
|
335
|
+
showPersistedTasks(reason === "reload" || reason === "resume" || reason === "fork");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
pi.on("before_agent_start", async (_event: any, ctx: any) => {
|
|
339
|
+
if (shuttingDown) return;
|
|
340
|
+
agentBusy = true;
|
|
341
|
+
log.debug("agentBusy", "before_agent_start");
|
|
342
|
+
widget.setUICtx(ctx.ui);
|
|
343
|
+
currentUiCtx = ctx.ui;
|
|
344
|
+
upgradeStoreIfNeeded(ctx);
|
|
345
|
+
showPersistedTasks();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
pi.on("tool_execution_start", async (_event: any, ctx: any) => {
|
|
349
|
+
if (shuttingDown) return;
|
|
350
|
+
widget.setUICtx(ctx.ui);
|
|
351
|
+
currentUiCtx = ctx.ui;
|
|
352
|
+
upgradeStoreIfNeeded(ctx);
|
|
353
|
+
widget.update();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// /tasks command — extracted so it can be re-registered on reload
|
|
357
|
+
let tasksCmdHandler: any;
|
|
358
|
+
function registerTasksCommand(): void {
|
|
359
|
+
log.debug("registerTasksCommand called");
|
|
360
|
+
pi.registerCommand("tasks", {
|
|
361
|
+
description: "Manage tasks — view, create, clear completed",
|
|
362
|
+
handler: tasksCmdHandler = async (_args: string, ctx: any) => {
|
|
363
|
+
log.debug("/tasks handler called");
|
|
364
|
+
const ui = ctx.ui;
|
|
365
|
+
const mainMenu = async (): Promise<void> => {
|
|
366
|
+
const tasks = taskStore.list();
|
|
367
|
+
const taskCount = tasks.length;
|
|
368
|
+
const completedCount = tasks.filter(t => t.status === "completed").length;
|
|
369
|
+
const choices = [`View all tasks (${taskCount})`, "Create task"];
|
|
370
|
+
if (completedCount > 0) choices.push(`Clear completed (${completedCount})`);
|
|
371
|
+
if (taskCount > 0) choices.push(`Clear all (${taskCount})`);
|
|
372
|
+
const choice = await ui.select("Tasks", choices);
|
|
373
|
+
if (!choice) return;
|
|
374
|
+
if (choice.startsWith("View")) await viewTasks();
|
|
375
|
+
else if (choice === "Create task") await createTask();
|
|
376
|
+
else if (choice.startsWith("Clear completed")) { taskStore.clearCompleted(); if (taskScope === "session") taskStore.deleteFileIfEmpty(); currentWidget?.update(); await mainMenu(); }
|
|
377
|
+
else if (choice.startsWith("Clear all")) { taskStore.clearAll(); if (taskScope === "session") taskStore.deleteFileIfEmpty(); currentWidget?.update(); await mainMenu(); }
|
|
378
|
+
};
|
|
379
|
+
const viewTasks = async (): Promise<void> => {
|
|
380
|
+
const tasks = taskStore.list();
|
|
381
|
+
if (tasks.length === 0) { await ui.select("No tasks", ["← Back"]); return mainMenu(); }
|
|
382
|
+
const statusIcon = (s: string) => s === "completed" ? "✔" : s === "in_progress" ? "◼" : "◻";
|
|
383
|
+
const choices = tasks.map(t => `${statusIcon(t.status)} #${t.id} [${t.status}] ${t.subject}`);
|
|
384
|
+
choices.push("← Back");
|
|
385
|
+
const selected = await ui.select("Tasks", choices);
|
|
386
|
+
if (!selected || selected === "← Back") return mainMenu();
|
|
387
|
+
const match = selected.match(/#(\d+)/);
|
|
388
|
+
if (match) await viewTaskDetail(match[1]);
|
|
389
|
+
else return viewTasks();
|
|
390
|
+
};
|
|
391
|
+
const viewTaskDetail = async (taskId: string): Promise<void> => {
|
|
392
|
+
const task = taskStore.get(taskId);
|
|
393
|
+
if (!task) return viewTasks();
|
|
394
|
+
const actions: string[] = [];
|
|
395
|
+
if (task.status === "pending") actions.push("▸ Start (in_progress)");
|
|
396
|
+
if (task.status === "in_progress") actions.push("✓ Complete");
|
|
397
|
+
actions.push("✗ Delete", "← Back");
|
|
398
|
+
const title = `#${task.id} [${task.status}] ${task.subject}\n${task.description}`;
|
|
399
|
+
const action = await ui.select(title, actions);
|
|
400
|
+
if (action === "▸ Start (in_progress)") { taskStore.update(taskId, { status: "in_progress" }); currentWidget?.setActiveTask(taskId); currentWidget?.update(); return viewTasks(); }
|
|
401
|
+
else if (action === "✓ Complete") { taskStore.update(taskId, { status: "completed" }); currentWidget?.setActiveTask(taskId, false); currentWidget?.update(); return viewTasks(); }
|
|
402
|
+
else if (action === "✗ Delete") { taskStore.update(taskId, { status: "deleted" }); currentWidget?.setActiveTask(taskId, false); currentWidget?.update(); return viewTasks(); }
|
|
403
|
+
return viewTasks();
|
|
404
|
+
};
|
|
405
|
+
const createTask = async (): Promise<void> => {
|
|
406
|
+
const subject = await ui.input("Task subject");
|
|
407
|
+
if (!subject) return mainMenu();
|
|
408
|
+
const description = await ui.input("Task description");
|
|
409
|
+
if (!description) return mainMenu();
|
|
410
|
+
taskStore.create(subject, description, { type: "local", origin: "user", session: "", project: process.cwd() });
|
|
411
|
+
currentWidget?.update();
|
|
412
|
+
return mainMenu();
|
|
413
|
+
};
|
|
414
|
+
await mainMenu();
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
registerTasksCommand();
|
|
419
|
+
// Expose handler to pidash for browser command dispatch
|
|
420
|
+
try {
|
|
421
|
+
if ((globalThis as any).__pitasks_pidash_listener) {
|
|
422
|
+
try { pi.events.removeListener("pidash:request-commands", (globalThis as any).__pitasks_pidash_listener); } catch {}
|
|
423
|
+
}
|
|
424
|
+
const registerWithPidash = () => pi.events.emit("pidash:register-command", { name: "tasks", handler: tasksCmdHandler });
|
|
425
|
+
(globalThis as any).__pitasks_pidash_listener = registerWithPidash;
|
|
426
|
+
registerWithPidash();
|
|
427
|
+
pi.events.on("pidash:request-commands", registerWithPidash);
|
|
428
|
+
} catch {}
|
|
429
|
+
log.debug("extension factory complete");
|
|
430
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extensions/pitasks/reminders.ts — Pure helpers for the task-focus reminder.
|
|
3
|
+
* Kept dependency-free so both the extension timer and unit tests can import
|
|
4
|
+
* the SAME production logic (no mirrored copies). The shared logger is
|
|
5
|
+
* dependency-light (node builtins only) so importing it here does not pull in
|
|
6
|
+
* pi-tui and keeps the module test-importable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createLogger } from "../shared/logger.js";
|
|
10
|
+
|
|
11
|
+
const log = createLogger("pitasks-reminders");
|
|
12
|
+
|
|
13
|
+
/** Content for the pending-only reminder (tasks exist but none in_progress). */
|
|
14
|
+
export function pendingReminderContent(activeCount: number): string {
|
|
15
|
+
log.debug("pendingReminderContent", activeCount);
|
|
16
|
+
return `⚠️ You have ${activeCount} active task(s) and none are in progress. Check your TaskList and resume your workflow.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Content for the stale in_progress reminder. */
|
|
20
|
+
export function staleReminderContent(staleCount: number): string {
|
|
21
|
+
log.debug("staleReminderContent", staleCount);
|
|
22
|
+
return `⚠️ You have ${staleCount} task(s) stuck in progress. Check your TaskList and update their status.`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Gate: fire the reminder only when the agent is idle and there are matching tasks. */
|
|
26
|
+
export function shouldFireReminder(agentBusy: boolean, matchingCount: number): boolean {
|
|
27
|
+
const result = matchingCount > 0 && !agentBusy;
|
|
28
|
+
log.debug("shouldFireReminder", agentBusy, matchingCount, result);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Select active tasks (pending or in_progress) and whether any are in_progress. */
|
|
33
|
+
export function selectActiveTasks(tasks: Array<{ status: string }>): { active: any[]; anyInProgress: boolean } {
|
|
34
|
+
const active = tasks.filter(t => t.status === "pending" || t.status === "in_progress");
|
|
35
|
+
log.debug("selectActiveTasks", tasks.length, active.length);
|
|
36
|
+
return { active, anyInProgress: active.some(t => t.status === "in_progress") };
|
|
37
|
+
}
|