@evoclock/pi-agentic-driver 0.9.4 → 0.10.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.
- package/README.md +2 -1
- package/extensions/tasks.ts +163 -9
- package/package.json +1 -1
- package/skills/tasks/SKILL.md +16 -0
package/README.md
CHANGED
|
@@ -101,7 +101,8 @@ only, not a runtime dependency).
|
|
|
101
101
|
|
|
102
102
|
Plans stay close to the work. They do not become board work by accident. The tasks skill gives Pi a familiar session list. It adds an owner-approved way to keep selected tasks in the durable `TASKS.md` board.
|
|
103
103
|
|
|
104
|
-
- **Keep the working list simple.** Use `TaskCreate`, `TaskGet`, `TaskList`, and `TaskUpdate` as usual.
|
|
104
|
+
- **Keep the working list simple.** Use `TaskCreate`, `TaskGet`, `TaskList`, and `TaskUpdate` as usual; the above-editor widget and footer pill stay live as tasks change.
|
|
105
|
+
- **Keep the Claude-like view.** `/tasks` provides descriptions, active forms, and internal-task markers without mutating the plan.
|
|
105
106
|
- **Make durable work deliberate.** `TaskPromote` never runs automatically. It previews the exact cards and destination before the owner confirms.
|
|
106
107
|
- **Keep authority clear.** The trusted board writer creates and updates `TASKS.md`. The skill does not bypass it.
|
|
107
108
|
- **Make migration gentle.** Existing task files remain readable. Moving to the driver-owned skill does not strand current plans.
|
package/extensions/tasks.ts
CHANGED
|
@@ -63,8 +63,121 @@ export default async function tasksPi(pi) {
|
|
|
63
63
|
let highWaterMark = 0;
|
|
64
64
|
let taskListId = null;
|
|
65
65
|
let mirrorPath = null;
|
|
66
|
+
let lastCtx = null;
|
|
67
|
+
|
|
68
|
+
const WIDGET_KEY = "picc-tasks";
|
|
69
|
+
const TASK_ICONS = Object.freeze({ pending: "▫", in_progress: "▪", completed: "✓" });
|
|
70
|
+
|
|
71
|
+
const isVisible = (task) => task?.metadata?._internal !== true;
|
|
72
|
+
const unresolvedTaskIds = () => new Set(
|
|
73
|
+
tasks.filter((task) => task.status !== "completed").map((task) => task.id),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const renderTaskListLine = (task) => {
|
|
77
|
+
const parts = [
|
|
78
|
+
`${TASK_ICONS[task.status] ?? "?"} #${task.id}`,
|
|
79
|
+
`[${task.status}]`,
|
|
80
|
+
task.subject,
|
|
81
|
+
];
|
|
82
|
+
if (task.owner) parts.push(`(${task.owner})`);
|
|
83
|
+
let line = parts.join(" ");
|
|
84
|
+
const unresolved = unresolvedTaskIds();
|
|
85
|
+
const live = task.blockedBy.filter((id) => unresolved.has(id));
|
|
86
|
+
if (live.length > 0) line += ` [blocked by ${live.map((id) => `#${id}`).join(", ")}]`;
|
|
87
|
+
return line;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const renderTaskListLineForLLM = (task) => {
|
|
91
|
+
const parts = [`#${task.id}`, `[${task.status}]`, task.subject];
|
|
92
|
+
if (task.owner) parts.push(`(${task.owner})`);
|
|
93
|
+
let line = parts.join(" ");
|
|
94
|
+
const unresolved = unresolvedTaskIds();
|
|
95
|
+
const live = task.blockedBy.filter((id) => unresolved.has(id));
|
|
96
|
+
if (live.length > 0) line += ` [blocked by ${live.map((id) => `#${id}`).join(", ")}]`;
|
|
97
|
+
return line;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const themed = (theme, method, args, fallback) => {
|
|
101
|
+
try {
|
|
102
|
+
if (typeof theme?.[method] === "function") return theme[method](...args);
|
|
103
|
+
} catch { /* UI styling is best-effort */ }
|
|
104
|
+
return fallback;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const renderWidgetTaskLine = (theme, task, unresolved) => {
|
|
108
|
+
const live = task.blockedBy.filter((id) => unresolved.has(id));
|
|
109
|
+
const parts = [
|
|
110
|
+
TASK_ICONS[task.status] ?? "?",
|
|
111
|
+
`[${task.status}]`,
|
|
112
|
+
task.subject,
|
|
113
|
+
];
|
|
114
|
+
if (task.owner) parts.push(`(${task.owner})`);
|
|
115
|
+
let line = parts.join(" ");
|
|
116
|
+
if (live.length > 0) line += ` [blocked by ${live.map((id) => `#${id}`).join(", ")}]`;
|
|
117
|
+
if (task.status === "completed") {
|
|
118
|
+
return themed(theme, "strikethrough", [line], line);
|
|
119
|
+
}
|
|
120
|
+
if (task.status === "in_progress") {
|
|
121
|
+
return themed(theme, "bold", [line], line);
|
|
122
|
+
}
|
|
123
|
+
return themed(theme, "fg", [live.length > 0 ? "dim" : "text", line], line);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// UI is deliberately a best-effort projection of the task state. A context
|
|
127
|
+
// can become stale during /new, /fork, /resume, or /reload; task mutations
|
|
128
|
+
// must still succeed and the next session event will render the projection.
|
|
129
|
+
const refreshUI = (ctx = null) => {
|
|
130
|
+
if (ctx) lastCtx = ctx;
|
|
131
|
+
let uiHost;
|
|
132
|
+
try {
|
|
133
|
+
uiHost = ctx ?? lastCtx;
|
|
134
|
+
if (!uiHost || uiHost.hasUI === false || !uiHost.ui) return;
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const visible = tasks.filter(isVisible);
|
|
140
|
+
try {
|
|
141
|
+
if (typeof uiHost.ui.setWidget === "function") {
|
|
142
|
+
if (visible.length === 0) {
|
|
143
|
+
uiHost.ui.setWidget(WIDGET_KEY, undefined);
|
|
144
|
+
} else {
|
|
145
|
+
const counts = {
|
|
146
|
+
pending: visible.filter((task) => task.status === "pending").length,
|
|
147
|
+
in_progress: visible.filter((task) => task.status === "in_progress").length,
|
|
148
|
+
completed: visible.filter((task) => task.status === "completed").length,
|
|
149
|
+
};
|
|
150
|
+
const header = `Tasks ${counts.pending} pending · ${counts.in_progress} in progress · ${counts.completed} done`;
|
|
151
|
+
const unresolved = unresolvedTaskIds();
|
|
152
|
+
const theme = uiHost.ui.theme;
|
|
153
|
+
const rows = [...visible]
|
|
154
|
+
.sort((a, b) => Number(a.id) - Number(b.id) || a.id.localeCompare(b.id))
|
|
155
|
+
.map((task) => renderWidgetTaskLine(theme, task, unresolved));
|
|
156
|
+
uiHost.ui.setWidget(WIDGET_KEY, [themed(theme, "fg", ["dim", header], header), ...rows], { placement: "aboveEditor" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// A stale UI host must never turn a successful tool mutation into an error.
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (typeof uiHost.ui.setStatus === "function") {
|
|
165
|
+
if (visible.length === 0) {
|
|
166
|
+
uiHost.ui.setStatus(WIDGET_KEY, undefined);
|
|
167
|
+
} else {
|
|
168
|
+
const active = visible.filter((task) => task.status === "in_progress").length;
|
|
169
|
+
const done = visible.filter((task) => task.status === "completed").length;
|
|
170
|
+
const total = visible.length;
|
|
171
|
+
uiHost.ui.setStatus(WIDGET_KEY, active > 0 ? `${active} active / ${done}/${total} tasks` : `${done}/${total} tasks`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Footer refresh is independent of the widget and equally best-effort.
|
|
176
|
+
}
|
|
177
|
+
};
|
|
66
178
|
|
|
67
179
|
const syncState = (ctx) => {
|
|
180
|
+
if (ctx) lastCtx = ctx;
|
|
68
181
|
taskListId = core.resolveTaskListId({ session: ctx?.sessionManager?.getSessionId?.() ?? null });
|
|
69
182
|
mirrorPath = core.tasksMirrorPath(taskListId);
|
|
70
183
|
// Replay: the branch's last picc-tasks-state entry wins; disk mirror is
|
|
@@ -85,9 +198,11 @@ export default async function tasksPi(pi) {
|
|
|
85
198
|
} else {
|
|
86
199
|
tasks = fromBranch.tasks; highWaterMark = branchHwm;
|
|
87
200
|
}
|
|
201
|
+
refreshUI(ctx);
|
|
88
202
|
};
|
|
89
203
|
|
|
90
204
|
const commitChange = (ctx) => {
|
|
205
|
+
if (ctx) lastCtx = ctx;
|
|
91
206
|
const snapshot = { tasks: tasks.map((t) => ({ ...t })), highWaterMark };
|
|
92
207
|
try {
|
|
93
208
|
pi.appendEntry(TASK_STATE_ENTRY, snapshot);
|
|
@@ -95,6 +210,7 @@ export default async function tasksPi(pi) {
|
|
|
95
210
|
if (mirrorPath) {
|
|
96
211
|
try { core.persistSnapshot(mirrorPath, snapshot); } catch { /* non-fatal */ }
|
|
97
212
|
}
|
|
213
|
+
refreshUI(ctx);
|
|
98
214
|
};
|
|
99
215
|
|
|
100
216
|
const findTask = (id) => tasks.find((t) => t.id === id);
|
|
@@ -186,15 +302,18 @@ export default async function tasksPi(pi) {
|
|
|
186
302
|
description: "Use this tool to list all tasks in the task list.\n\n## When to Use This Tool\n\n- To see what tasks are available to work on (status: 'pending', no owner, not blocked)\n- To check overall progress on the project\n- To find tasks that are blocked and need dependencies resolved\n- After completing a task, to check for newly unblocked work or claim the next available task\n- **Prefer working on tasks in ID order** (lowest ID first) when multiple tasks are available, as earlier tasks often set up context for later ones\n\n## Output\n\nReturns a summary of each task:\n- **id**: Task identifier (use with TaskGet, TaskUpdate)\n- **subject**: Brief description of the task\n- **status**: 'pending', 'in_progress', or 'completed'\n- **owner**: Agent ID if assigned, empty if available\n- **blockedBy**: List of open task IDs that must be resolved first (tasks with blockedBy cannot be claimed until dependencies resolve)\n\nUse TaskGet with a specific task ID to view full details including description and comments.",
|
|
187
303
|
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
188
304
|
async execute() {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
305
|
+
const visible = tasks.filter(isVisible);
|
|
306
|
+
if (visible.length === 0) return { content: [{ type: "text", text: "No tasks found" }], details: { tasks: [] } };
|
|
307
|
+
const unresolved = unresolvedTaskIds();
|
|
308
|
+
const list = visible.map((t) => ({
|
|
309
|
+
id: t.id,
|
|
310
|
+
subject: t.subject,
|
|
311
|
+
status: t.status,
|
|
312
|
+
...(t.owner !== undefined ? { owner: t.owner } : {}),
|
|
313
|
+
blockedBy: t.blockedBy.filter((id) => unresolved.has(id)),
|
|
314
|
+
}));
|
|
315
|
+
const lines = list.map(renderTaskListLineForLLM);
|
|
316
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { tasks: list } };
|
|
198
317
|
},
|
|
199
318
|
});
|
|
200
319
|
|
|
@@ -560,7 +679,42 @@ export default async function tasksPi(pi) {
|
|
|
560
679
|
});
|
|
561
680
|
}
|
|
562
681
|
|
|
682
|
+
// Rich, read-only view for the owner. It intentionally includes internal
|
|
683
|
+
// tasks and presentation fields that the model-facing TaskList omits.
|
|
684
|
+
if (typeof pi?.registerCommand === "function") {
|
|
685
|
+
pi.registerCommand("tasks", {
|
|
686
|
+
description: "Show all tasks (richer view than the LLM-facing TaskList).",
|
|
687
|
+
argumentHint: "",
|
|
688
|
+
handler: async (_args, ctx) => {
|
|
689
|
+
try {
|
|
690
|
+
if (tasks.length === 0) {
|
|
691
|
+
ctx?.ui?.notify?.("No tasks yet. Use TaskCreate to add one.", "info");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const blocks = [];
|
|
695
|
+
for (const task of tasks) {
|
|
696
|
+
const internal = task.metadata?._internal === true ? " [internal]" : "";
|
|
697
|
+
blocks.push(`${renderTaskListLine(task)}${internal}`);
|
|
698
|
+
blocks.push(` ${task.description}`);
|
|
699
|
+
if (task.activeForm) blocks.push(` active: ${task.activeForm}`);
|
|
700
|
+
}
|
|
701
|
+
ctx?.ui?.notify?.(blocks.join("\n"), "info");
|
|
702
|
+
} catch (error) {
|
|
703
|
+
// Informational only: a stale context after session replacement is
|
|
704
|
+
// not a command failure and must never surface to the user/agent.
|
|
705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
706
|
+
if (!message.includes("stale after session replacement")) {
|
|
707
|
+
// Other UI failures are also non-fatal; the command has no state
|
|
708
|
+
// mutation to roll back.
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
563
715
|
pi.on?.("session_start", async (_event, ctx) => { syncState(ctx); });
|
|
716
|
+
pi.on?.("session_tree", async (_event, ctx) => { syncState(ctx); });
|
|
717
|
+
pi.on?.("session_shutdown", () => { lastCtx = null; });
|
|
564
718
|
syncState(typeof pi?.ctx !== "undefined" ? pi.ctx : null);
|
|
565
719
|
|
|
566
720
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evoclock/pi-agentic-driver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Guardrail extensions for Agentic Driver: advisory review, bounded Herdr communication, and guarded worker lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
package/skills/tasks/SKILL.md
CHANGED
|
@@ -15,6 +15,22 @@ copied; this is a clean AGPL-3.0-only reimplementation to the same behavior
|
|
|
15
15
|
spec. picc-tasks is an independent project by its author, used as reference;
|
|
16
16
|
it is not affiliated with, nor does it endorse, pi-agentic-driver.
|
|
17
17
|
|
|
18
|
+
## Interactive task view
|
|
19
|
+
|
|
20
|
+
The driver-owned skill preserves picc-tasks' owner-facing UI as well as its
|
|
21
|
+
session tools:
|
|
22
|
+
|
|
23
|
+
- the above-editor widget shows visible tasks with `▫`/`▪`/`✓` status icons,
|
|
24
|
+
status, subject, owner, live unresolved blockers, and a count header;
|
|
25
|
+
- the footer status pill shows active and done/total counts;
|
|
26
|
+
- `/tasks` is a richer, read-only view with descriptions, `activeForm`, and
|
|
27
|
+
`[internal]` markers.
|
|
28
|
+
|
|
29
|
+
The widget and pill refresh after every task mutation and on `session_start` or
|
|
30
|
+
`session_tree`. UI refresh is best-effort: a stale context after session
|
|
31
|
+
replacement is swallowed and never turns a successful mutation or `/tasks`
|
|
32
|
+
command into an error.
|
|
33
|
+
|
|
18
34
|
## What this skill owns
|
|
19
35
|
|
|
20
36
|
1. The **session task list** — working memory for the current session. Every
|