@bumpyclock/pi-tasque 0.1.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/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/README.md +315 -0
- package/package.json +39 -0
- package/src/bridge/bridge-tool.ts +185 -0
- package/src/bridge/import-tsq.ts +502 -0
- package/src/bridge/link-store.ts +97 -0
- package/src/bridge/promote-todo.ts +331 -0
- package/src/bridge/types.ts +156 -0
- package/src/durable-tasks/cache.ts +167 -0
- package/src/durable-tasks/mutation-queue.ts +30 -0
- package/src/durable-tasks/runner.ts +234 -0
- package/src/durable-tasks/status.ts +184 -0
- package/src/durable-tasks/tools-change.ts +600 -0
- package/src/durable-tasks/tools-claim.ts +426 -0
- package/src/durable-tasks/tools-query.ts +496 -0
- package/src/durable-tasks/types.ts +193 -0
- package/src/index.ts +21 -0
- package/src/session-todos/state/invariants.ts +17 -0
- package/src/session-todos/state/replay.ts +272 -0
- package/src/session-todos/state/selectors.ts +140 -0
- package/src/session-todos/state/state-reducer.ts +292 -0
- package/src/session-todos/state/state.ts +69 -0
- package/src/session-todos/state/store.ts +37 -0
- package/src/session-todos/state/task-graph.ts +58 -0
- package/src/session-todos/todo-overlay.ts +223 -0
- package/src/session-todos/todo.ts +239 -0
- package/src/session-todos/tool/response-envelope.ts +143 -0
- package/src/session-todos/tool/types.ts +149 -0
- package/src/session-todos/view/format.ts +264 -0
- package/src/shared/tool-result.ts +81 -0
- package/src/shared/truncation.ts +150 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
selectTasksByStatus,
|
|
7
|
+
selectTodoCounts,
|
|
8
|
+
selectVisibleTasks,
|
|
9
|
+
} from "./state/selectors.js";
|
|
10
|
+
import { applyTaskMutation } from "./state/state-reducer.js";
|
|
11
|
+
import { commitState, getState, replaceState } from "./state/store.js";
|
|
12
|
+
import { replayFromBranch } from "./state/replay.js";
|
|
13
|
+
import { buildToolResult } from "./tool/response-envelope.js";
|
|
14
|
+
import {
|
|
15
|
+
COMMAND_NAME,
|
|
16
|
+
ERR_REQUIRES_INTERACTIVE,
|
|
17
|
+
MSG_NO_TODOS,
|
|
18
|
+
TOOL_LABEL,
|
|
19
|
+
TOOL_NAME,
|
|
20
|
+
TodoParamsSchema,
|
|
21
|
+
type TaskAction,
|
|
22
|
+
type TaskMutationParams,
|
|
23
|
+
} from "./tool/types.js";
|
|
24
|
+
import { TodoOverlay } from "./todo-overlay.js";
|
|
25
|
+
import {
|
|
26
|
+
formatCommandTaskLine,
|
|
27
|
+
formatStatusLabel,
|
|
28
|
+
renderTodoCall,
|
|
29
|
+
renderTodoResult,
|
|
30
|
+
} from "./view/format.js";
|
|
31
|
+
|
|
32
|
+
const SECTION_PENDING = "── Pending ──";
|
|
33
|
+
const SECTION_IN_PROGRESS = "── In Progress ──";
|
|
34
|
+
const SECTION_COMPLETED = "── Completed ──";
|
|
35
|
+
const TODO_AFFECTING_TOOLS = new Set(["todo", "task_bridge", "tsq_claim"]);
|
|
36
|
+
|
|
37
|
+
export const TODO_PROMPT_SNIPPET =
|
|
38
|
+
"Manage current-session tactical todos for multi-step execution.";
|
|
39
|
+
|
|
40
|
+
export const TODO_PROMPT_GUIDELINES: string[] = [
|
|
41
|
+
"Use `todo` for current-session tactical work: inspect, edit, verify, and handoff steps for the task in front of you.",
|
|
42
|
+
"Use `todo` for session work, not durable backlog; use Tasque/tsq tools for durable ownership, specs, and cross-session work.",
|
|
43
|
+
"Mark one task in_progress before starting it, complete it when verified, and keep blocked or partial work out of completed.",
|
|
44
|
+
"Use blockedBy for session-local dependencies; list hides deleted tombstones unless includeDeleted is true.",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export { isTransitionValid } from "./state/invariants.js";
|
|
48
|
+
export { applyTaskMutation } from "./state/state-reducer.js";
|
|
49
|
+
export {
|
|
50
|
+
__resetState,
|
|
51
|
+
commitState,
|
|
52
|
+
getNextId,
|
|
53
|
+
getState,
|
|
54
|
+
getTodos,
|
|
55
|
+
} from "./state/store.js";
|
|
56
|
+
export { deriveBlocks, detectCycle } from "./state/task-graph.js";
|
|
57
|
+
export type {
|
|
58
|
+
Task,
|
|
59
|
+
TaskAction,
|
|
60
|
+
TaskDetails,
|
|
61
|
+
TaskMutationParams,
|
|
62
|
+
TaskStatus,
|
|
63
|
+
} from "./tool/types.js";
|
|
64
|
+
export { COMMAND_NAME, TOOL_NAME } from "./tool/types.js";
|
|
65
|
+
|
|
66
|
+
/** Rebuild live todo state from the current branch replay snapshot. */
|
|
67
|
+
export function reconstructTodoState(
|
|
68
|
+
ctx: Parameters<typeof replayFromBranch>[0],
|
|
69
|
+
): void {
|
|
70
|
+
replaceState(replayFromBranch(ctx));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function registerTodoTool(pi: ExtensionAPI): void {
|
|
74
|
+
pi.registerTool({
|
|
75
|
+
name: TOOL_NAME,
|
|
76
|
+
label: TOOL_LABEL,
|
|
77
|
+
description:
|
|
78
|
+
"Manage current-session todos for tactical execution. Actions: create, update, list, get, delete, clear. Use for this session's working checklist, not durable backlog.",
|
|
79
|
+
promptSnippet: TODO_PROMPT_SNIPPET,
|
|
80
|
+
promptGuidelines: TODO_PROMPT_GUIDELINES,
|
|
81
|
+
parameters: TodoParamsSchema,
|
|
82
|
+
|
|
83
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
84
|
+
const { action, ...mutationParams } = params as TaskMutationParams & {
|
|
85
|
+
action: TaskAction;
|
|
86
|
+
};
|
|
87
|
+
const result = applyTaskMutation(getState(), action, mutationParams);
|
|
88
|
+
commitState(result.state);
|
|
89
|
+
return buildToolResult(action, mutationParams, result.state, result.op);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
renderCall(args, theme, _context) {
|
|
93
|
+
return renderTodoCall(
|
|
94
|
+
args as TaskMutationParams & { action: TaskAction },
|
|
95
|
+
theme,
|
|
96
|
+
getState(),
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
renderResult(result, _opts, theme, _context) {
|
|
101
|
+
return renderTodoResult(result, theme);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
107
|
+
pi.registerCommand(COMMAND_NAME, {
|
|
108
|
+
description: "Show current-session todos grouped by status",
|
|
109
|
+
handler: async (_args, ctx) => {
|
|
110
|
+
if (!ctx.hasUI) {
|
|
111
|
+
ctx.ui.notify(ERR_REQUIRES_INTERACTIVE, "error");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const state = getState();
|
|
116
|
+
const visible = selectVisibleTasks(state);
|
|
117
|
+
if (visible.length === 0) {
|
|
118
|
+
ctx.ui.notify(MSG_NO_TODOS, "info");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const groups = selectTasksByStatus(state);
|
|
123
|
+
const counts = selectTodoCounts(state);
|
|
124
|
+
const header: string[] = [];
|
|
125
|
+
if (counts.completed > 0) {
|
|
126
|
+
header.push(
|
|
127
|
+
`${counts.completed}/${counts.total} ${formatStatusLabel("completed")}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (counts.inProgress > 0) {
|
|
131
|
+
header.push(`${counts.inProgress} ${formatStatusLabel("in_progress")}`);
|
|
132
|
+
}
|
|
133
|
+
if (counts.pending > 0) {
|
|
134
|
+
header.push(`${counts.pending} ${formatStatusLabel("pending")}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lines = [header.join(" · ")];
|
|
138
|
+
if (groups.pending.length > 0) {
|
|
139
|
+
lines.push(SECTION_PENDING);
|
|
140
|
+
for (const task of groups.pending) {
|
|
141
|
+
lines.push(formatCommandTaskLine(task, "○"));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (groups.inProgress.length > 0) {
|
|
145
|
+
lines.push(SECTION_IN_PROGRESS);
|
|
146
|
+
for (const task of groups.inProgress) {
|
|
147
|
+
lines.push(formatCommandTaskLine(task, "◐"));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (groups.completed.length > 0) {
|
|
151
|
+
lines.push(SECTION_COMPLETED);
|
|
152
|
+
for (const task of groups.completed) {
|
|
153
|
+
lines.push(formatCommandTaskLine(task, "✓"));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function registerSessionTodoModule(pi: ExtensionAPI): void {
|
|
163
|
+
registerTodoTool(pi);
|
|
164
|
+
registerTodosCommand(pi);
|
|
165
|
+
registerTodoLifecycle(pi);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function registerTodoLifecycle(pi: ExtensionAPI): void {
|
|
169
|
+
const overlay = new TodoOverlay();
|
|
170
|
+
|
|
171
|
+
function bindUiAndUpdate(ctx: ExtensionContext): void {
|
|
172
|
+
if (!hasOverlayUi(ctx)) {
|
|
173
|
+
overlay.dispose();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
overlay.setUICtx(ctx.ui);
|
|
177
|
+
overlay.update();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function replayAndUpdate(
|
|
181
|
+
ctx: ExtensionContext,
|
|
182
|
+
options: { readonly resetCompletedDisplayState: boolean },
|
|
183
|
+
): void {
|
|
184
|
+
reconstructTodoState(ctx);
|
|
185
|
+
if (options.resetCompletedDisplayState) {
|
|
186
|
+
overlay.resetCompletedDisplayState();
|
|
187
|
+
}
|
|
188
|
+
bindUiAndUpdate(ctx);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
pi.on("session_start", (_event, ctx) => {
|
|
192
|
+
replayAndUpdate(ctx, { resetCompletedDisplayState: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
196
|
+
replayAndUpdate(ctx, { resetCompletedDisplayState: false });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
200
|
+
replayAndUpdate(ctx, { resetCompletedDisplayState: true });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
204
|
+
if (!TODO_AFFECTING_TOOLS.has(event.toolName)) return;
|
|
205
|
+
if (!isSuccessfulToolExecutionResult(event)) return;
|
|
206
|
+
bindUiAndUpdate(ctx);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
210
|
+
if (!hasOverlayUi(ctx)) return;
|
|
211
|
+
overlay.setUICtx(ctx.ui);
|
|
212
|
+
overlay.hideCompletedTasksFromPreviousTurn();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.on("session_shutdown", () => {
|
|
216
|
+
overlay.dispose();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hasOverlayUi(ctx: ExtensionContext): boolean {
|
|
221
|
+
return ctx.hasUI === true && ctx.ui !== undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isSuccessfulToolExecutionResult(event: {
|
|
225
|
+
readonly isError: boolean;
|
|
226
|
+
readonly result: unknown;
|
|
227
|
+
}): boolean {
|
|
228
|
+
if (event.isError) return false;
|
|
229
|
+
const result = event.result;
|
|
230
|
+
if (!isRecord(result)) return true;
|
|
231
|
+
const details = result.details;
|
|
232
|
+
if (!isRecord(details)) return true;
|
|
233
|
+
if (details.ok === false) return false;
|
|
234
|
+
return details.error === undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
238
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
239
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { cloneTaskState, type TaskState } from "../state/state.js";
|
|
2
|
+
import type { Op } from "../state/state-reducer.js";
|
|
3
|
+
import { deriveBlocks } from "../state/task-graph.js";
|
|
4
|
+
import type {
|
|
5
|
+
Task,
|
|
6
|
+
TaskAction,
|
|
7
|
+
TaskDetails,
|
|
8
|
+
TaskMutationParams,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
export interface TodoToolResult {
|
|
12
|
+
content: Array<{ type: "text"; text: string }>;
|
|
13
|
+
details: TaskDetails;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatListLine(task: Task): string {
|
|
17
|
+
const activeForm =
|
|
18
|
+
task.status === "in_progress" && task.activeForm
|
|
19
|
+
? ` (${task.activeForm})`
|
|
20
|
+
: "";
|
|
21
|
+
const blockedBy = task.blockedBy?.length
|
|
22
|
+
? ` ⛓ ${task.blockedBy.map((id) => `#${id}`).join(",")}`
|
|
23
|
+
: "";
|
|
24
|
+
return `[${task.status}] #${task.id} ${task.subject}${activeForm}${blockedBy}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatGetLines(task: Task, state: TaskState): string {
|
|
28
|
+
const blocks = deriveBlocks(state.tasks).get(task.id) ?? [];
|
|
29
|
+
const lines = [`#${task.id} [${task.status}] ${task.subject}`];
|
|
30
|
+
if (task.description) lines.push(` description: ${task.description}`);
|
|
31
|
+
if (task.activeForm) lines.push(` activeForm: ${task.activeForm}`);
|
|
32
|
+
if (task.blockedBy?.length) {
|
|
33
|
+
lines.push(
|
|
34
|
+
` blockedBy: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (blocks.length) {
|
|
38
|
+
lines.push(` blocks: ${blocks.map((id) => `#${id}`).join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
if (task.owner) lines.push(` owner: ${task.owner}`);
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
46
|
+
const prototype = Object.getPrototypeOf(value);
|
|
47
|
+
return prototype === Object.prototype || prototype === null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cloneDetailValue(value: unknown): unknown {
|
|
51
|
+
if (value === null || typeof value !== "object") return value;
|
|
52
|
+
|
|
53
|
+
if (typeof globalThis.structuredClone === "function") {
|
|
54
|
+
try {
|
|
55
|
+
return globalThis.structuredClone(value);
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall back for values structuredClone cannot copy, like functions.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(value)) return value.map(cloneDetailValue);
|
|
62
|
+
|
|
63
|
+
if (isPlainObject(value)) {
|
|
64
|
+
const cloned: Record<string, unknown> = {};
|
|
65
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
66
|
+
cloned[key] = cloneDetailValue(nestedValue);
|
|
67
|
+
}
|
|
68
|
+
return cloned;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cloneParams(params: TaskMutationParams): Record<string, unknown> {
|
|
75
|
+
const cloned: Record<string, unknown> = {};
|
|
76
|
+
for (const [key, value] of Object.entries(params)) {
|
|
77
|
+
cloned[key] = cloneDetailValue(value);
|
|
78
|
+
}
|
|
79
|
+
return cloned;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** LLM-facing text for todo reducer operations. */
|
|
83
|
+
export function formatContent(op: Op, state: TaskState): string {
|
|
84
|
+
switch (op.kind) {
|
|
85
|
+
case "create": {
|
|
86
|
+
const task = state.tasks.find((candidate) => candidate.id === op.taskId);
|
|
87
|
+
if (!task) return `Created #${op.taskId}`;
|
|
88
|
+
return `Created #${task.id}: ${task.subject} (pending)`;
|
|
89
|
+
}
|
|
90
|
+
case "update": {
|
|
91
|
+
const transition =
|
|
92
|
+
op.fromStatus === op.toStatus
|
|
93
|
+
? ""
|
|
94
|
+
: ` (${op.fromStatus} → ${op.toStatus})`;
|
|
95
|
+
return `Updated #${op.id}${transition}`;
|
|
96
|
+
}
|
|
97
|
+
case "delete":
|
|
98
|
+
return `Deleted #${op.id}: ${op.subject}`;
|
|
99
|
+
case "clear":
|
|
100
|
+
return `Cleared ${op.count} tasks`;
|
|
101
|
+
case "list": {
|
|
102
|
+
let tasks = state.tasks;
|
|
103
|
+
if (!op.includeDeleted) {
|
|
104
|
+
tasks = tasks.filter((task) => task.status !== "deleted");
|
|
105
|
+
}
|
|
106
|
+
if (op.statusFilter) {
|
|
107
|
+
tasks = tasks.filter((task) => task.status === op.statusFilter);
|
|
108
|
+
}
|
|
109
|
+
return tasks.length === 0
|
|
110
|
+
? "No tasks"
|
|
111
|
+
: tasks.map(formatListLine).join("\n");
|
|
112
|
+
}
|
|
113
|
+
case "get":
|
|
114
|
+
return formatGetLines(op.task, state);
|
|
115
|
+
case "error":
|
|
116
|
+
return `Error: ${op.message}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compatible `todo` result envelope. `details` is the replay snapshot consumed
|
|
122
|
+
* by session branch replay, so keep field names stable.
|
|
123
|
+
*/
|
|
124
|
+
export function buildToolResult(
|
|
125
|
+
action: TaskAction,
|
|
126
|
+
params: TaskMutationParams,
|
|
127
|
+
state: TaskState,
|
|
128
|
+
op: Op,
|
|
129
|
+
): TodoToolResult {
|
|
130
|
+
const snapshot = cloneTaskState(state);
|
|
131
|
+
const details: TaskDetails = {
|
|
132
|
+
action,
|
|
133
|
+
params: cloneParams(params),
|
|
134
|
+
tasks: snapshot.tasks,
|
|
135
|
+
nextId: snapshot.nextId,
|
|
136
|
+
...(op.kind === "error" ? { error: op.message } : {}),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: formatContent(op, state) }],
|
|
141
|
+
details,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Tool / command identity — verbatim string boundaries.
|
|
6
|
+
// Tool name "todo" is the persistence key for branch replay (filtering
|
|
7
|
+
// `toolResult.toolName === "todo"`). DO NOT rename.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const TOOL_NAME = "todo";
|
|
11
|
+
export const TOOL_LABEL = "Todo";
|
|
12
|
+
export const COMMAND_NAME = "todos";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// User-facing strings (kept stable for /todos UX parity).
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const ERR_REQUIRES_INTERACTIVE = "/todos requires interactive mode";
|
|
19
|
+
export const MSG_NO_TODOS = "No todos yet. Ask the agent to add some!";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Public domain types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export type TaskStatus = "pending" | "in_progress" | "completed" | "deleted";
|
|
26
|
+
|
|
27
|
+
export type TaskAction =
|
|
28
|
+
| "create"
|
|
29
|
+
| "update"
|
|
30
|
+
| "list"
|
|
31
|
+
| "get"
|
|
32
|
+
| "delete"
|
|
33
|
+
| "clear";
|
|
34
|
+
|
|
35
|
+
export interface Task {
|
|
36
|
+
id: number;
|
|
37
|
+
subject: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
activeForm?: string;
|
|
40
|
+
status: TaskStatus;
|
|
41
|
+
blockedBy?: number[];
|
|
42
|
+
owner?: string;
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Persistence + replay snapshot. Every successful `todo` tool call returns this
|
|
48
|
+
* shape under `details`; replay reads the latest one from the branch to
|
|
49
|
+
* reconstruct module state. Field order and field names are pinned by
|
|
50
|
+
* cross-version replay compatibility.
|
|
51
|
+
*/
|
|
52
|
+
export interface TaskDetails {
|
|
53
|
+
action: TaskAction;
|
|
54
|
+
params: Record<string, unknown>;
|
|
55
|
+
tasks: Task[];
|
|
56
|
+
nextId: number;
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Open-shape input bag the reducer accepts. Stays an interface so the index
|
|
62
|
+
* signature (`[key: string]: unknown`) lets the runtime pass through TypeBox
|
|
63
|
+
* `Static<typeof TodoParamsSchema>` without `as` casts.
|
|
64
|
+
*/
|
|
65
|
+
export interface TaskMutationParams {
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
subject?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
activeForm?: string;
|
|
70
|
+
status?: TaskStatus;
|
|
71
|
+
blockedBy?: number[];
|
|
72
|
+
addBlockedBy?: number[];
|
|
73
|
+
removeBlockedBy?: number[];
|
|
74
|
+
owner?: string;
|
|
75
|
+
metadata?: Record<string, unknown>;
|
|
76
|
+
id?: number;
|
|
77
|
+
includeDeleted?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// TypeBox parameter schema — every `description` doubles as LLM-facing prompt
|
|
82
|
+
// copy. Field order and wording stay compatible with rpiv-todo v1.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
export const TodoParamsSchema = Type.Object({
|
|
86
|
+
action: StringEnum([
|
|
87
|
+
"create",
|
|
88
|
+
"update",
|
|
89
|
+
"list",
|
|
90
|
+
"get",
|
|
91
|
+
"delete",
|
|
92
|
+
"clear",
|
|
93
|
+
] as const),
|
|
94
|
+
subject: Type.Optional(
|
|
95
|
+
Type.String({ description: "Task subject line (required for create)" }),
|
|
96
|
+
),
|
|
97
|
+
description: Type.Optional(
|
|
98
|
+
Type.String({ description: "Long-form task description" }),
|
|
99
|
+
),
|
|
100
|
+
activeForm: Type.Optional(
|
|
101
|
+
Type.String({
|
|
102
|
+
description:
|
|
103
|
+
"Present-continuous spinner label shown while status is in_progress (e.g. 'writing tests')",
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
status: Type.Optional(
|
|
107
|
+
StringEnum(["pending", "in_progress", "completed", "deleted"] as const, {
|
|
108
|
+
description: "Target status (update) or list filter (list)",
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
blockedBy: Type.Optional(
|
|
112
|
+
Type.Array(Type.Number(), {
|
|
113
|
+
description: "Initial blockedBy ids (create only)",
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
addBlockedBy: Type.Optional(
|
|
117
|
+
Type.Array(Type.Number(), {
|
|
118
|
+
description: "Task ids to add to blockedBy (update only, additive merge)",
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
removeBlockedBy: Type.Optional(
|
|
122
|
+
Type.Array(Type.Number(), {
|
|
123
|
+
description:
|
|
124
|
+
"Task ids to remove from blockedBy (update only, additive merge)",
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
owner: Type.Optional(
|
|
128
|
+
Type.String({ description: "Agent/owner assigned to this task" }),
|
|
129
|
+
),
|
|
130
|
+
metadata: Type.Optional(
|
|
131
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
132
|
+
description:
|
|
133
|
+
"Arbitrary metadata; pass null value for a key to delete that key on update",
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
id: Type.Optional(
|
|
137
|
+
Type.Number({
|
|
138
|
+
description: "Task id (required for update, get, delete)",
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
includeDeleted: Type.Optional(
|
|
142
|
+
Type.Boolean({
|
|
143
|
+
description:
|
|
144
|
+
"If true, list action returns deleted (tombstoned) tasks as well. Default: false.",
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export type TodoParams = Static<typeof TodoParamsSchema>;
|