@bumpyclock/pi-tasque 0.1.0 → 0.2.1
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 +168 -140
- package/package.json +24 -6
- package/src/bridge/bridge-tool.ts +10 -13
- package/src/bridge/import-tsq.ts +5 -30
- package/src/bridge/promote-todo.ts +1 -4
- package/src/bridge/types.ts +11 -12
- package/src/durable-tasks/bulk-contract.ts +176 -0
- package/src/durable-tasks/cache.ts +1 -4
- package/src/durable-tasks/change-command-builder.ts +315 -0
- package/src/durable-tasks/handoff-guard.ts +329 -0
- package/src/durable-tasks/project.ts +71 -0
- package/src/durable-tasks/runner.ts +1 -4
- package/src/durable-tasks/status.ts +20 -5
- package/src/durable-tasks/task-mappers.ts +160 -0
- package/src/durable-tasks/task-schema.ts +193 -0
- package/src/durable-tasks/task-tool.ts +197 -0
- package/src/durable-tasks/task-validation.ts +123 -0
- package/src/durable-tasks/tools-bulk.ts +141 -0
- package/src/durable-tasks/tools-change.ts +111 -382
- package/src/durable-tasks/tools-claim.ts +15 -43
- package/src/durable-tasks/tools-handoff.ts +95 -0
- package/src/durable-tasks/tools-query.ts +58 -29
- package/src/durable-tasks/tools-spec.ts +230 -0
- package/src/durable-tasks/tools-tree-create.ts +221 -0
- package/src/guidelines/internal-tools.ts +33 -0
- package/src/guidelines/task.ts +5 -0
- package/src/guidelines/todo.ts +5 -0
- package/src/index.ts +2 -13
- package/src/session-todos/state/replay.ts +10 -13
- package/src/session-todos/todo-overlay.ts +1 -1
- package/src/session-todos/todo.ts +11 -15
- package/src/session-todos/tool/types.ts +7 -7
- package/src/shared/error-utils.ts +29 -0
- package/src/shared/validation.ts +25 -0
|
@@ -1,60 +1,62 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
3
|
defineTool,
|
|
4
|
+
type AgentToolResult,
|
|
4
5
|
type ExtensionAPI,
|
|
5
6
|
type ExtensionContext,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { type Static, Type } from "typebox";
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
CHANGE_TASKS_PROMPT_GUIDELINES,
|
|
11
|
+
CHANGE_TASKS_PROMPT_SNIPPET,
|
|
12
|
+
} from "../guidelines/internal-tools.js";
|
|
13
|
+
import {
|
|
14
|
+
asRecord,
|
|
15
|
+
copyKnownErrorFields,
|
|
16
|
+
} from "../shared/error-utils.js";
|
|
10
17
|
import {
|
|
11
18
|
errorToolDetails,
|
|
12
19
|
okToolDetails,
|
|
13
20
|
textToolResult,
|
|
14
21
|
} from "../shared/tool-result.js";
|
|
22
|
+
import {
|
|
23
|
+
buildMutationCommand,
|
|
24
|
+
TSQ_CHANGE_ACTIONS,
|
|
25
|
+
type TsqChangeAction,
|
|
26
|
+
} from "./change-command-builder.js";
|
|
27
|
+
import { runQueuedMutation } from "./mutation-queue.js";
|
|
28
|
+
import { runTsqJson } from "./runner.js";
|
|
15
29
|
|
|
16
30
|
export const TSQ_CHANGE_TOOL_NAME = "tsq_change";
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
"create",
|
|
20
|
-
"note",
|
|
21
|
-
"done",
|
|
22
|
-
"reopen",
|
|
23
|
-
"defer",
|
|
24
|
-
"start",
|
|
25
|
-
"claim_assign_only",
|
|
26
|
-
"block",
|
|
27
|
-
"unblock",
|
|
28
|
-
"order",
|
|
29
|
-
"unorder",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
|
-
export type TsqChangeAction = (typeof TSQ_CHANGE_ACTIONS)[number];
|
|
32
|
+
export type { TsqChangeAction } from "./change-command-builder.js";
|
|
33
33
|
|
|
34
34
|
export const TsqChangeParamsSchema = Type.Object(
|
|
35
35
|
{
|
|
36
36
|
action: StringEnum(TSQ_CHANGE_ACTIONS, {
|
|
37
|
-
description: "Durable
|
|
37
|
+
description: "Durable task mutation to run",
|
|
38
38
|
}),
|
|
39
39
|
title: Type.Optional(
|
|
40
40
|
Type.String({ description: "Task title (required for create)" }),
|
|
41
41
|
),
|
|
42
42
|
id: Type.Optional(
|
|
43
43
|
Type.String({
|
|
44
|
-
description: "
|
|
44
|
+
description: "Durable task id for lifecycle/note/claim actions",
|
|
45
45
|
}),
|
|
46
46
|
),
|
|
47
47
|
kind: Type.Optional(
|
|
48
|
-
Type.String({ description: "
|
|
48
|
+
Type.String({ description: "Durable task kind (required for create)" }),
|
|
49
49
|
),
|
|
50
50
|
priority: Type.Optional(
|
|
51
|
-
Type.Integer({
|
|
51
|
+
Type.Integer({
|
|
52
|
+
description: "Durable task priority (required for create)",
|
|
53
|
+
}),
|
|
52
54
|
),
|
|
53
55
|
description: Type.Optional(
|
|
54
56
|
Type.String({ description: "Task description (create only)" }),
|
|
55
57
|
),
|
|
56
58
|
parent: Type.Optional(
|
|
57
|
-
Type.String({ description: "Parent
|
|
59
|
+
Type.String({ description: "Parent durable task id (create only)" }),
|
|
58
60
|
),
|
|
59
61
|
planned: Type.Optional(
|
|
60
62
|
Type.Boolean({ description: "Mark created task planned" }),
|
|
@@ -109,367 +111,120 @@ export type TsqChangeDetails = ReturnType<
|
|
|
109
111
|
typeof okToolDetails<TsqChangeSuccessData>
|
|
110
112
|
>;
|
|
111
113
|
|
|
112
|
-
type ValidationResult =
|
|
113
|
-
| {
|
|
114
|
-
readonly ok: true;
|
|
115
|
-
readonly action: TsqChangeAction;
|
|
116
|
-
readonly argv: string[];
|
|
117
|
-
}
|
|
118
|
-
| { readonly ok: false; readonly message: string };
|
|
119
|
-
|
|
120
114
|
export function registerTsqChangeTool(pi: ExtensionAPI): void {
|
|
121
115
|
pi.registerTool(
|
|
122
116
|
defineTool({
|
|
123
117
|
name: TSQ_CHANGE_TOOL_NAME,
|
|
124
|
-
label: "
|
|
118
|
+
label: "Task Change",
|
|
125
119
|
description:
|
|
126
|
-
"
|
|
127
|
-
promptSnippet:
|
|
128
|
-
|
|
129
|
-
promptGuidelines: [
|
|
130
|
-
"Use tsq_change only for explicit durable Tasque mutations; do not use it as a raw tsq passthrough.",
|
|
131
|
-
"Use todo for current-session checklist steps; use tsq_change when durable Tasque state must change.",
|
|
132
|
-
"Use block/unblock for hard blockers and order/unorder for sequencing where one task should happen after another.",
|
|
133
|
-
"Use tsq_query with action deps or show to inspect durable graph state before or after edge changes.",
|
|
134
|
-
],
|
|
120
|
+
"Mutate durable tasks: lifecycle, notes, ownership, dependencies, and sequencing.",
|
|
121
|
+
promptSnippet: CHANGE_TASKS_PROMPT_SNIPPET,
|
|
122
|
+
promptGuidelines: CHANGE_TASKS_PROMPT_GUIDELINES,
|
|
135
123
|
parameters: TsqChangeParamsSchema,
|
|
136
124
|
executionMode: "sequential",
|
|
137
125
|
|
|
138
126
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
139
|
-
|
|
140
|
-
params as Readonly<Record<string, unknown>>,
|
|
141
|
-
);
|
|
142
|
-
if (!command.ok) {
|
|
143
|
-
return validationErrorResult(command.message);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const result = await runMutation(pi, ctx, command.argv, signal);
|
|
148
|
-
return textToolResult(
|
|
149
|
-
formatSuccess(command.action, params, result),
|
|
150
|
-
okToolDetails({
|
|
151
|
-
action: command.action,
|
|
152
|
-
argv: command.argv,
|
|
153
|
-
result,
|
|
154
|
-
}),
|
|
155
|
-
);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
const message = getErrorMessage(error);
|
|
158
|
-
return textToolResult(
|
|
159
|
-
`Error: ${message}`,
|
|
160
|
-
errorToolDetails({
|
|
161
|
-
code: getErrorCode(error),
|
|
162
|
-
message,
|
|
163
|
-
details: {
|
|
164
|
-
action: command.action,
|
|
165
|
-
argv: command.argv,
|
|
166
|
-
error: serializeError(error),
|
|
167
|
-
},
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
127
|
+
return executeTsqChange(pi, params as TsqChangeParams, signal, ctx);
|
|
171
128
|
},
|
|
172
129
|
}),
|
|
173
130
|
);
|
|
174
131
|
}
|
|
175
132
|
|
|
176
|
-
function
|
|
133
|
+
export async function executeTsqChange(
|
|
177
134
|
pi: ExtensionAPI,
|
|
178
|
-
|
|
179
|
-
argv: readonly string[],
|
|
135
|
+
params: TsqChangeParams,
|
|
180
136
|
signal: AbortSignal | undefined,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
137
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
138
|
+
): Promise<AgentToolResult<TsqChangeDetails>> {
|
|
139
|
+
const command = buildMutationCommand(
|
|
140
|
+
params as Readonly<Record<string, unknown>>,
|
|
185
141
|
);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
case "block":
|
|
215
|
-
return buildBlockArgv(params, action, "block");
|
|
216
|
-
case "unblock":
|
|
217
|
-
return buildBlockArgv(params, action, "unblock");
|
|
218
|
-
case "order":
|
|
219
|
-
return buildOrderArgv(params, action, "order");
|
|
220
|
-
case "unorder":
|
|
221
|
-
return buildOrderArgv(params, action, "unorder");
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function buildCreateArgv(
|
|
226
|
-
params: Readonly<Record<string, unknown>>,
|
|
227
|
-
action: TsqChangeAction,
|
|
228
|
-
): ValidationResult {
|
|
229
|
-
const title = requireNonEmptyString(params, "title");
|
|
230
|
-
if (!title.ok) {
|
|
231
|
-
return title;
|
|
232
|
-
}
|
|
233
|
-
const kind = requireNonEmptyString(params, "kind");
|
|
234
|
-
if (!kind.ok) {
|
|
235
|
-
return kind;
|
|
236
|
-
}
|
|
237
|
-
const priority = requireInteger(params, "priority");
|
|
238
|
-
if (!priority.ok) {
|
|
239
|
-
return priority;
|
|
240
|
-
}
|
|
241
|
-
const planned = getOptionalBoolean(params, "planned");
|
|
242
|
-
if (!planned.ok) {
|
|
243
|
-
return planned;
|
|
244
|
-
}
|
|
245
|
-
const needsPlan = getOptionalBoolean(params, "needsPlan");
|
|
246
|
-
if (!needsPlan.ok) {
|
|
247
|
-
return needsPlan;
|
|
248
|
-
}
|
|
249
|
-
if (planned.value === true && needsPlan.value === true) {
|
|
250
|
-
return {
|
|
251
|
-
ok: false,
|
|
252
|
-
message: "planned and needsPlan cannot both be true",
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const argv = ["create", `--kind=${kind.value}`, "-p", String(priority.value)];
|
|
257
|
-
const description = appendOptionalStringFlag(
|
|
258
|
-
argv,
|
|
259
|
-
params,
|
|
260
|
-
"description",
|
|
261
|
-
"--description",
|
|
262
|
-
);
|
|
263
|
-
if (description !== undefined) {
|
|
264
|
-
return description;
|
|
265
|
-
}
|
|
266
|
-
const parent = appendOptionalStringFlag(argv, params, "parent", "--parent");
|
|
267
|
-
if (parent !== undefined) {
|
|
268
|
-
return parent;
|
|
269
|
-
}
|
|
270
|
-
if (planned.value === true) {
|
|
271
|
-
argv.push("--planned");
|
|
272
|
-
} else if (needsPlan.value === true) {
|
|
273
|
-
argv.push("--needs-plan");
|
|
274
|
-
}
|
|
275
|
-
argv.push("--", title.value);
|
|
276
|
-
|
|
277
|
-
return { ok: true, action, argv };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function buildNoteArgv(
|
|
281
|
-
params: Readonly<Record<string, unknown>>,
|
|
282
|
-
action: TsqChangeAction,
|
|
283
|
-
): ValidationResult {
|
|
284
|
-
const id = requireNonEmptyString(params, "id");
|
|
285
|
-
if (!id.ok) {
|
|
286
|
-
return id;
|
|
287
|
-
}
|
|
288
|
-
const note = requireNonEmptyString(params, "note");
|
|
289
|
-
if (!note.ok) {
|
|
290
|
-
return note;
|
|
291
|
-
}
|
|
292
|
-
return { ok: true, action, argv: ["note", id.value, "--", note.value] };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function buildOptionalNoteArgv(
|
|
296
|
-
params: Readonly<Record<string, unknown>>,
|
|
297
|
-
action: TsqChangeAction,
|
|
298
|
-
command: "done" | "defer",
|
|
299
|
-
): ValidationResult {
|
|
300
|
-
const id = requireNonEmptyString(params, "id");
|
|
301
|
-
if (!id.ok) {
|
|
302
|
-
return id;
|
|
303
|
-
}
|
|
304
|
-
const argv = [command, id.value];
|
|
305
|
-
const note = getOptionalNonEmptyString(params, "note");
|
|
306
|
-
if (!note.ok) {
|
|
307
|
-
return note;
|
|
308
|
-
}
|
|
309
|
-
if (note.value !== undefined) {
|
|
310
|
-
argv.push(`--note=${note.value}`);
|
|
311
|
-
}
|
|
312
|
-
return { ok: true, action, argv };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function buildIdOnlyArgv(
|
|
316
|
-
params: Readonly<Record<string, unknown>>,
|
|
317
|
-
action: TsqChangeAction,
|
|
318
|
-
command: "reopen" | "start",
|
|
319
|
-
): ValidationResult {
|
|
320
|
-
const id = requireNonEmptyString(params, "id");
|
|
321
|
-
if (!id.ok) {
|
|
322
|
-
return id;
|
|
323
|
-
}
|
|
324
|
-
return { ok: true, action, argv: [command, id.value] };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function buildClaimAssignOnlyArgv(
|
|
328
|
-
params: Readonly<Record<string, unknown>>,
|
|
329
|
-
action: TsqChangeAction,
|
|
330
|
-
): ValidationResult {
|
|
331
|
-
const id = requireNonEmptyString(params, "id");
|
|
332
|
-
if (!id.ok) {
|
|
333
|
-
return id;
|
|
334
|
-
}
|
|
335
|
-
const assignee = requireNonEmptyString(params, "assignee");
|
|
336
|
-
if (!assignee.ok) {
|
|
337
|
-
return assignee;
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
ok: true,
|
|
341
|
-
action,
|
|
342
|
-
argv: ["claim", id.value, `--assignee=${assignee.value}`],
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function buildBlockArgv(
|
|
347
|
-
params: Readonly<Record<string, unknown>>,
|
|
348
|
-
action: TsqChangeAction,
|
|
349
|
-
command: "block" | "unblock",
|
|
350
|
-
): ValidationResult {
|
|
351
|
-
const child = requireNonEmptyString(params, "child");
|
|
352
|
-
if (!child.ok) {
|
|
353
|
-
return child;
|
|
354
|
-
}
|
|
355
|
-
const blocker = requireNonEmptyString(params, "blocker");
|
|
356
|
-
if (!blocker.ok) {
|
|
357
|
-
return blocker;
|
|
358
|
-
}
|
|
359
|
-
if (child.value === blocker.value) {
|
|
360
|
-
return { ok: false, message: "child and blocker cannot be the same task" };
|
|
361
|
-
}
|
|
362
|
-
return {
|
|
363
|
-
ok: true,
|
|
364
|
-
action,
|
|
365
|
-
argv: [command, child.value, "by", blocker.value],
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function buildOrderArgv(
|
|
370
|
-
params: Readonly<Record<string, unknown>>,
|
|
371
|
-
action: TsqChangeAction,
|
|
372
|
-
command: "order" | "unorder",
|
|
373
|
-
): ValidationResult {
|
|
374
|
-
const later = requireNonEmptyString(params, "later");
|
|
375
|
-
if (!later.ok) {
|
|
376
|
-
return later;
|
|
377
|
-
}
|
|
378
|
-
const earlier = requireNonEmptyString(params, "earlier");
|
|
379
|
-
if (!earlier.ok) {
|
|
380
|
-
return earlier;
|
|
381
|
-
}
|
|
382
|
-
if (later.value === earlier.value) {
|
|
383
|
-
return { ok: false, message: "later and earlier cannot be the same task" };
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
ok: true,
|
|
387
|
-
action,
|
|
388
|
-
argv: [command, later.value, "after", earlier.value],
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function appendOptionalStringFlag(
|
|
393
|
-
argv: string[],
|
|
394
|
-
params: Readonly<Record<string, unknown>>,
|
|
395
|
-
field: "description" | "parent",
|
|
396
|
-
flag: string,
|
|
397
|
-
): ValidationResult | undefined {
|
|
398
|
-
const value = getOptionalNonEmptyString(params, field);
|
|
399
|
-
if (!value.ok) {
|
|
400
|
-
return value;
|
|
401
|
-
}
|
|
402
|
-
if (value.value !== undefined) {
|
|
403
|
-
argv.push(`${flag}=${value.value}`);
|
|
142
|
+
if (!command.ok) {
|
|
143
|
+
return validationErrorResult(command.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const result = await runMutation(pi, ctx, command.argv, signal);
|
|
148
|
+
return textToolResult(
|
|
149
|
+
formatSuccess(command.action, params, result),
|
|
150
|
+
okToolDetails({
|
|
151
|
+
action: command.action,
|
|
152
|
+
argv: command.argv,
|
|
153
|
+
result,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const message = getErrorMessage(error);
|
|
158
|
+
return textToolResult(
|
|
159
|
+
`Error: ${message}`,
|
|
160
|
+
errorToolDetails({
|
|
161
|
+
code: getErrorCode(error),
|
|
162
|
+
message,
|
|
163
|
+
details: {
|
|
164
|
+
action: command.action,
|
|
165
|
+
argv: command.argv,
|
|
166
|
+
error: serializeError(error),
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
404
170
|
}
|
|
405
|
-
return undefined;
|
|
406
171
|
}
|
|
407
172
|
|
|
408
|
-
|
|
409
|
-
params: Readonly<Record<string, unknown>>,
|
|
410
|
-
field: string,
|
|
411
|
-
):
|
|
412
|
-
| { readonly ok: true; readonly value: string }
|
|
413
|
-
| { readonly ok: false; readonly message: string } {
|
|
414
|
-
const value = params[field];
|
|
415
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
416
|
-
return { ok: false, message: `${field} is required` };
|
|
417
|
-
}
|
|
418
|
-
return { ok: true, value };
|
|
419
|
-
}
|
|
173
|
+
// --- mark_planned helper (tsq-5.2) ---
|
|
420
174
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
):
|
|
425
|
-
| { readonly ok: true; readonly value: string | undefined }
|
|
426
|
-
| { readonly ok: false; readonly message: string } {
|
|
427
|
-
const value = params[field];
|
|
428
|
-
if (value === undefined) {
|
|
429
|
-
return { ok: true, value: undefined };
|
|
430
|
-
}
|
|
431
|
-
if (typeof value !== "string") {
|
|
432
|
-
return { ok: false, message: `${field} must be a string` };
|
|
433
|
-
}
|
|
434
|
-
if (value.trim().length === 0) {
|
|
435
|
-
return { ok: true, value: undefined };
|
|
436
|
-
}
|
|
437
|
-
return { ok: true, value };
|
|
175
|
+
export interface TsqMarkPlannedSuccessData {
|
|
176
|
+
readonly argv: readonly string[];
|
|
177
|
+
readonly result: unknown;
|
|
438
178
|
}
|
|
439
179
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
):
|
|
444
|
-
| { readonly ok: true; readonly value: number }
|
|
445
|
-
| { readonly ok: false; readonly message: string } {
|
|
446
|
-
const value = params[field];
|
|
447
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
448
|
-
return { ok: false, message: `${field} is required` };
|
|
449
|
-
}
|
|
450
|
-
return { ok: true, value };
|
|
451
|
-
}
|
|
180
|
+
export type TsqMarkPlannedDetails = ReturnType<
|
|
181
|
+
typeof okToolDetails<TsqMarkPlannedSuccessData>
|
|
182
|
+
>;
|
|
452
183
|
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
184
|
+
export async function executeTsqMarkPlanned(
|
|
185
|
+
pi: ExtensionAPI,
|
|
186
|
+
taskId: string,
|
|
187
|
+
signal: AbortSignal | undefined,
|
|
188
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
189
|
+
): Promise<AgentToolResult<TsqMarkPlannedDetails>> {
|
|
190
|
+
const trimmed = taskId.trim();
|
|
191
|
+
if (trimmed.length === 0) {
|
|
192
|
+
return validationErrorResult("task id is required");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const argv = ["planned", trimmed];
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const result = await runMutation(pi, ctx, argv, signal);
|
|
199
|
+
return textToolResult(
|
|
200
|
+
`Marked ${trimmed} as planned`,
|
|
201
|
+
okToolDetails({ argv, result }),
|
|
202
|
+
);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const message = getErrorMessage(error);
|
|
205
|
+
return textToolResult(
|
|
206
|
+
`Error: ${message}`,
|
|
207
|
+
errorToolDetails({
|
|
208
|
+
code: getErrorCode(error),
|
|
209
|
+
message,
|
|
210
|
+
details: {
|
|
211
|
+
argv,
|
|
212
|
+
error: serializeError(error),
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
465
216
|
}
|
|
466
|
-
return { ok: true, value };
|
|
467
217
|
}
|
|
468
218
|
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
219
|
+
function runMutation(
|
|
220
|
+
pi: ExtensionAPI,
|
|
221
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
222
|
+
argv: readonly string[],
|
|
223
|
+
signal: AbortSignal | undefined,
|
|
224
|
+
): Promise<unknown> {
|
|
225
|
+
const options = signal === undefined ? {} : { signal };
|
|
226
|
+
return runQueuedMutation(ctx.cwd, () =>
|
|
227
|
+
runTsqJson(pi, { cwd: ctx.cwd }, argv, options),
|
|
473
228
|
);
|
|
474
229
|
}
|
|
475
230
|
|
|
@@ -546,13 +301,6 @@ function extractTaskLike(result: unknown): {
|
|
|
546
301
|
};
|
|
547
302
|
}
|
|
548
303
|
|
|
549
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
550
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
551
|
-
return undefined;
|
|
552
|
-
}
|
|
553
|
-
return value as Record<string, unknown>;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
304
|
function getErrorMessage(error: unknown): string {
|
|
557
305
|
if (error instanceof Error) {
|
|
558
306
|
return error.message;
|
|
@@ -579,22 +327,3 @@ function serializeError(error: unknown): Record<string, unknown> {
|
|
|
579
327
|
}
|
|
580
328
|
return { value: String(error) };
|
|
581
329
|
}
|
|
582
|
-
|
|
583
|
-
function copyKnownErrorFields(error: Error): Record<string, unknown> {
|
|
584
|
-
const record = error as unknown as Record<string, unknown>;
|
|
585
|
-
const output: Record<string, unknown> = {};
|
|
586
|
-
for (const key of [
|
|
587
|
-
"code",
|
|
588
|
-
"command",
|
|
589
|
-
"details",
|
|
590
|
-
"stderr",
|
|
591
|
-
"stdout",
|
|
592
|
-
"killed",
|
|
593
|
-
"args",
|
|
594
|
-
] as const) {
|
|
595
|
-
if (record[key] !== undefined) {
|
|
596
|
-
output[key] = record[key];
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return output;
|
|
600
|
-
}
|
|
@@ -5,12 +5,21 @@ import {
|
|
|
5
5
|
type ExtensionContext,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { type Static, Type } from "typebox";
|
|
8
|
+
import {
|
|
9
|
+
CLAIM_TASK_PROMPT_GUIDELINES,
|
|
10
|
+
CLAIM_TASK_PROMPT_SNIPPET,
|
|
11
|
+
} from "../guidelines/internal-tools.js";
|
|
8
12
|
import {
|
|
9
13
|
applyTaskMutation,
|
|
10
14
|
type Op,
|
|
11
15
|
} from "../session-todos/state/state-reducer.js";
|
|
12
16
|
import { commitState, getState } from "../session-todos/state/store.js";
|
|
13
17
|
import type { Task } from "../session-todos/tool/types.js";
|
|
18
|
+
import {
|
|
19
|
+
asRecord,
|
|
20
|
+
copyKnownErrorFields,
|
|
21
|
+
isRecord,
|
|
22
|
+
} from "../shared/error-utils.js";
|
|
14
23
|
import {
|
|
15
24
|
errorToolDetails,
|
|
16
25
|
okToolDetails,
|
|
@@ -24,7 +33,7 @@ export const TSQ_CLAIM_TOOL_NAME = "tsq_claim";
|
|
|
24
33
|
|
|
25
34
|
export const TsqClaimParamsSchema = Type.Object(
|
|
26
35
|
{
|
|
27
|
-
id: Type.String({ description: "Named
|
|
36
|
+
id: Type.String({ description: "Named durable task id to claim." }),
|
|
28
37
|
assignee: Type.Optional(
|
|
29
38
|
Type.String({
|
|
30
39
|
description: "Agent or role claiming the task. Defaults to pi.",
|
|
@@ -38,8 +47,7 @@ export const TsqClaimParamsSchema = Type.Object(
|
|
|
38
47
|
),
|
|
39
48
|
requireSpec: Type.Optional(
|
|
40
49
|
Type.Boolean({
|
|
41
|
-
description:
|
|
42
|
-
"Require an attached Tasque spec before the claim succeeds.",
|
|
50
|
+
description: "Require an attached task spec before the claim succeeds.",
|
|
43
51
|
}),
|
|
44
52
|
),
|
|
45
53
|
createTodo: Type.Optional(
|
|
@@ -95,16 +103,10 @@ export function registerTsqClaimTool(pi: ExtensionAPI): void {
|
|
|
95
103
|
pi.registerTool(
|
|
96
104
|
defineTool({
|
|
97
105
|
name: TSQ_CLAIM_TOOL_NAME,
|
|
98
|
-
label: "
|
|
99
|
-
description:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"Use tsq_claim for named durable Tasque ownership. Provide id; assignee defaults to pi; start defaults to true.",
|
|
103
|
-
promptGuidelines: [
|
|
104
|
-
"Pass your own role/name as assignee when available, e.g. developer, worker, oracle.",
|
|
105
|
-
"Use createTodo only when you want one session todo linked to the claimed durable task.",
|
|
106
|
-
"Completing a linked session todo does not mark the Tasque task done; durable completion must be explicit.",
|
|
107
|
-
],
|
|
106
|
+
label: "Task Claim",
|
|
107
|
+
description: "Claim named durable task ownership.",
|
|
108
|
+
promptSnippet: CLAIM_TASK_PROMPT_SNIPPET,
|
|
109
|
+
promptGuidelines: CLAIM_TASK_PROMPT_GUIDELINES,
|
|
108
110
|
parameters: TsqClaimParamsSchema,
|
|
109
111
|
executionMode: "sequential",
|
|
110
112
|
|
|
@@ -394,33 +396,3 @@ function serializeError(error: unknown): Record<string, unknown> {
|
|
|
394
396
|
}
|
|
395
397
|
return { value: String(error) };
|
|
396
398
|
}
|
|
397
|
-
|
|
398
|
-
function copyKnownErrorFields(error: Error): Record<string, unknown> {
|
|
399
|
-
const record = error as unknown as Record<string, unknown>;
|
|
400
|
-
const output: Record<string, unknown> = {};
|
|
401
|
-
for (const key of [
|
|
402
|
-
"code",
|
|
403
|
-
"command",
|
|
404
|
-
"details",
|
|
405
|
-
"stderr",
|
|
406
|
-
"stdout",
|
|
407
|
-
"killed",
|
|
408
|
-
"args",
|
|
409
|
-
] as const) {
|
|
410
|
-
if (record[key] !== undefined) {
|
|
411
|
-
output[key] = record[key];
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return output;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
418
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
421
|
-
return value as Record<string, unknown>;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
425
|
-
return asRecord(value) !== undefined;
|
|
426
|
-
}
|