@bumpyclock/pi-tasque 0.2.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/package.json +1 -1
- package/src/bridge/import-tsq.ts +5 -30
- package/src/bridge/promote-todo.ts +1 -4
- package/src/durable-tasks/cache.ts +1 -4
- package/src/durable-tasks/change-command-builder.ts +315 -0
- package/src/durable-tasks/runner.ts +1 -4
- package/src/durable-tasks/status.ts +1 -4
- 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 +21 -588
- package/src/durable-tasks/task-validation.ts +123 -0
- package/src/durable-tasks/tools-bulk.ts +1 -4
- package/src/durable-tasks/tools-change.ts +10 -337
- package/src/durable-tasks/tools-claim.ts +5 -30
- package/src/durable-tasks/tools-handoff.ts +95 -0
- package/src/durable-tasks/tools-query.ts +1 -4
- package/src/durable-tasks/tools-tree-create.ts +1 -7
- package/src/session-todos/todo.ts +1 -4
- package/src/shared/error-utils.ts +29 -0
- package/src/shared/validation.ts +25 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fieldRequired,
|
|
3
|
+
requireStringField,
|
|
4
|
+
} from "../shared/validation.js";
|
|
5
|
+
import {
|
|
6
|
+
validateBulkItems,
|
|
7
|
+
validateCreateTreeNode,
|
|
8
|
+
} from "./bulk-contract.js";
|
|
9
|
+
import type { TaskAction, TaskParams } from "./task-schema.js";
|
|
10
|
+
|
|
11
|
+
export function validateTaskParams(
|
|
12
|
+
params: TaskParams,
|
|
13
|
+
): { readonly ok: true } | { readonly ok: false; readonly message: string } {
|
|
14
|
+
switch (params.action) {
|
|
15
|
+
case "find":
|
|
16
|
+
if (params.view === "tree") return { ok: true };
|
|
17
|
+
if (params.tasks === undefined) return fieldRequired("tasks");
|
|
18
|
+
return { ok: true };
|
|
19
|
+
case "show":
|
|
20
|
+
case "deps":
|
|
21
|
+
case "notes":
|
|
22
|
+
case "note":
|
|
23
|
+
case "finish":
|
|
24
|
+
case "reopen":
|
|
25
|
+
case "defer":
|
|
26
|
+
case "start":
|
|
27
|
+
case "claim":
|
|
28
|
+
case "import":
|
|
29
|
+
return requireStringField(params.task, "task");
|
|
30
|
+
case "create": {
|
|
31
|
+
const task = requireStringField(params.task, "task");
|
|
32
|
+
if (!task.ok) return task;
|
|
33
|
+
const kind = requireStringField(params.kind, "kind");
|
|
34
|
+
if (!kind.ok) return kind;
|
|
35
|
+
return typeof params.priority === "number"
|
|
36
|
+
? { ok: true }
|
|
37
|
+
: fieldRequired("priority");
|
|
38
|
+
}
|
|
39
|
+
case "similar":
|
|
40
|
+
return requireStringField(params.query, "query");
|
|
41
|
+
case "block":
|
|
42
|
+
case "unblock": {
|
|
43
|
+
const task = requireStringField(params.task, "task");
|
|
44
|
+
if (!task.ok) return task;
|
|
45
|
+
return requireStringField(params.by, "by");
|
|
46
|
+
}
|
|
47
|
+
case "order":
|
|
48
|
+
case "unorder": {
|
|
49
|
+
const task = requireStringField(params.task, "task");
|
|
50
|
+
if (!task.ok) return task;
|
|
51
|
+
return requireStringField(params.after, "after");
|
|
52
|
+
}
|
|
53
|
+
case "link": {
|
|
54
|
+
const todo = requireTodoId(params.todo);
|
|
55
|
+
if (!todo.ok) return todo;
|
|
56
|
+
return requireStringField(params.task, "task");
|
|
57
|
+
}
|
|
58
|
+
case "promote":
|
|
59
|
+
return requireTodoId(params.todo);
|
|
60
|
+
case "mark_planned":
|
|
61
|
+
return requireStringField(params.task, "task");
|
|
62
|
+
case "spec": {
|
|
63
|
+
const task = requireStringField(params.task, "task");
|
|
64
|
+
if (!task.ok) return task;
|
|
65
|
+
const mode = params.mode as string | undefined;
|
|
66
|
+
if (mode === undefined || mode.trim().length === 0)
|
|
67
|
+
return fieldRequired("mode");
|
|
68
|
+
const isRead = mode === "show" || mode === "check";
|
|
69
|
+
const isWrite = mode === "set" || mode === "update";
|
|
70
|
+
if (!isRead && !isWrite)
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message: "mode must be show, check, set, or update",
|
|
74
|
+
};
|
|
75
|
+
if (isRead && params.text !== undefined)
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: `spec ${mode} does not accept text`,
|
|
79
|
+
};
|
|
80
|
+
if (isWrite) {
|
|
81
|
+
const text = params.text?.trim();
|
|
82
|
+
if (text === undefined || text.length === 0)
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
message: `spec ${mode} requires text`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { ok: true };
|
|
89
|
+
}
|
|
90
|
+
case "bulk":
|
|
91
|
+
return validateBulkItems(params.items);
|
|
92
|
+
case "create_tree":
|
|
93
|
+
return validateCreateTreeNode(params.root);
|
|
94
|
+
case "handoff_check":
|
|
95
|
+
case "doctor":
|
|
96
|
+
case "list_links":
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function actionUsesTasque(action: TaskAction): boolean {
|
|
102
|
+
return (
|
|
103
|
+
action !== "link" && action !== "list_links" && action !== "handoff_check"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function hasWith(params: TaskParams, value: string): boolean {
|
|
108
|
+
return Array.isArray(params.with) && params.with.includes(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getTodoId(
|
|
112
|
+
value: boolean | number | undefined,
|
|
113
|
+
): number | undefined {
|
|
114
|
+
return typeof value === "number" ? value : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function requireTodoId(
|
|
118
|
+
value: boolean | number | undefined,
|
|
119
|
+
): { readonly ok: true } | { readonly ok: false; readonly message: string } {
|
|
120
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1
|
|
121
|
+
? { ok: true }
|
|
122
|
+
: fieldRequired("todo");
|
|
123
|
+
}
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ExtensionContext,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import type { BulkItem, BulkItemAction, BulkResult } from "./bulk-contract.js";
|
|
15
|
+
import { isRecord } from "../shared/error-utils.js";
|
|
15
16
|
import type { StandardToolDetails } from "../shared/tool-result.js";
|
|
16
17
|
import { okToolDetails, textToolResult } from "../shared/tool-result.js";
|
|
17
18
|
import {
|
|
@@ -138,7 +139,3 @@ function formatBulkText(result: BulkResult, total: number): string {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
// ── Utilities ──────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
143
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
144
|
-
}
|
|
@@ -10,31 +10,26 @@ import {
|
|
|
10
10
|
CHANGE_TASKS_PROMPT_GUIDELINES,
|
|
11
11
|
CHANGE_TASKS_PROMPT_SNIPPET,
|
|
12
12
|
} from "../guidelines/internal-tools.js";
|
|
13
|
+
import {
|
|
14
|
+
asRecord,
|
|
15
|
+
copyKnownErrorFields,
|
|
16
|
+
} from "../shared/error-utils.js";
|
|
13
17
|
import {
|
|
14
18
|
errorToolDetails,
|
|
15
19
|
okToolDetails,
|
|
16
20
|
textToolResult,
|
|
17
21
|
} from "../shared/tool-result.js";
|
|
22
|
+
import {
|
|
23
|
+
buildMutationCommand,
|
|
24
|
+
TSQ_CHANGE_ACTIONS,
|
|
25
|
+
type TsqChangeAction,
|
|
26
|
+
} from "./change-command-builder.js";
|
|
18
27
|
import { runQueuedMutation } from "./mutation-queue.js";
|
|
19
28
|
import { runTsqJson } from "./runner.js";
|
|
20
29
|
|
|
21
30
|
export const TSQ_CHANGE_TOOL_NAME = "tsq_change";
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
"create",
|
|
25
|
-
"note",
|
|
26
|
-
"done",
|
|
27
|
-
"reopen",
|
|
28
|
-
"defer",
|
|
29
|
-
"start",
|
|
30
|
-
"claim_assign_only",
|
|
31
|
-
"block",
|
|
32
|
-
"unblock",
|
|
33
|
-
"order",
|
|
34
|
-
"unorder",
|
|
35
|
-
] as const;
|
|
36
|
-
|
|
37
|
-
export type TsqChangeAction = (typeof TSQ_CHANGE_ACTIONS)[number];
|
|
32
|
+
export type { TsqChangeAction } from "./change-command-builder.js";
|
|
38
33
|
|
|
39
34
|
export const TsqChangeParamsSchema = Type.Object(
|
|
40
35
|
{
|
|
@@ -116,14 +111,6 @@ export type TsqChangeDetails = ReturnType<
|
|
|
116
111
|
typeof okToolDetails<TsqChangeSuccessData>
|
|
117
112
|
>;
|
|
118
113
|
|
|
119
|
-
type ValidationResult =
|
|
120
|
-
| {
|
|
121
|
-
readonly ok: true;
|
|
122
|
-
readonly action: TsqChangeAction;
|
|
123
|
-
readonly argv: string[];
|
|
124
|
-
}
|
|
125
|
-
| { readonly ok: false; readonly message: string };
|
|
126
|
-
|
|
127
114
|
export function registerTsqChangeTool(pi: ExtensionAPI): void {
|
|
128
115
|
pi.registerTool(
|
|
129
116
|
defineTool({
|
|
@@ -241,294 +228,6 @@ function runMutation(
|
|
|
241
228
|
);
|
|
242
229
|
}
|
|
243
230
|
|
|
244
|
-
function buildMutationCommand(
|
|
245
|
-
params: Readonly<Record<string, unknown>>,
|
|
246
|
-
): ValidationResult {
|
|
247
|
-
const action = params.action;
|
|
248
|
-
if (!isTsqChangeAction(action)) {
|
|
249
|
-
return {
|
|
250
|
-
ok: false,
|
|
251
|
-
message: "action must be a supported durable task mutation",
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
switch (action) {
|
|
256
|
-
case "create":
|
|
257
|
-
return buildCreateArgv(params, action);
|
|
258
|
-
case "note":
|
|
259
|
-
return buildNoteArgv(params, action);
|
|
260
|
-
case "done":
|
|
261
|
-
return buildOptionalNoteArgv(params, action, "done");
|
|
262
|
-
case "reopen":
|
|
263
|
-
return buildIdOnlyArgv(params, action, "reopen");
|
|
264
|
-
case "defer":
|
|
265
|
-
return buildOptionalNoteArgv(params, action, "defer");
|
|
266
|
-
case "start":
|
|
267
|
-
return buildIdOnlyArgv(params, action, "start");
|
|
268
|
-
case "claim_assign_only":
|
|
269
|
-
return buildClaimAssignOnlyArgv(params, action);
|
|
270
|
-
case "block":
|
|
271
|
-
return buildBlockArgv(params, action, "block");
|
|
272
|
-
case "unblock":
|
|
273
|
-
return buildBlockArgv(params, action, "unblock");
|
|
274
|
-
case "order":
|
|
275
|
-
return buildOrderArgv(params, action, "order");
|
|
276
|
-
case "unorder":
|
|
277
|
-
return buildOrderArgv(params, action, "unorder");
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function buildCreateArgv(
|
|
282
|
-
params: Readonly<Record<string, unknown>>,
|
|
283
|
-
action: TsqChangeAction,
|
|
284
|
-
): ValidationResult {
|
|
285
|
-
const title = requireNonEmptyString(params, "title");
|
|
286
|
-
if (!title.ok) {
|
|
287
|
-
return title;
|
|
288
|
-
}
|
|
289
|
-
const kind = requireNonEmptyString(params, "kind");
|
|
290
|
-
if (!kind.ok) {
|
|
291
|
-
return kind;
|
|
292
|
-
}
|
|
293
|
-
const priority = requireInteger(params, "priority");
|
|
294
|
-
if (!priority.ok) {
|
|
295
|
-
return priority;
|
|
296
|
-
}
|
|
297
|
-
const planned = getOptionalBoolean(params, "planned");
|
|
298
|
-
if (!planned.ok) {
|
|
299
|
-
return planned;
|
|
300
|
-
}
|
|
301
|
-
const needsPlan = getOptionalBoolean(params, "needsPlan");
|
|
302
|
-
if (!needsPlan.ok) {
|
|
303
|
-
return needsPlan;
|
|
304
|
-
}
|
|
305
|
-
if (planned.value === true && needsPlan.value === true) {
|
|
306
|
-
return {
|
|
307
|
-
ok: false,
|
|
308
|
-
message: "planned and needsPlan cannot both be true",
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const argv = ["create", `--kind=${kind.value}`, "-p", String(priority.value)];
|
|
313
|
-
const description = appendOptionalStringFlag(
|
|
314
|
-
argv,
|
|
315
|
-
params,
|
|
316
|
-
"description",
|
|
317
|
-
"--description",
|
|
318
|
-
);
|
|
319
|
-
if (description !== undefined) {
|
|
320
|
-
return description;
|
|
321
|
-
}
|
|
322
|
-
const parent = appendOptionalStringFlag(argv, params, "parent", "--parent");
|
|
323
|
-
if (parent !== undefined) {
|
|
324
|
-
return parent;
|
|
325
|
-
}
|
|
326
|
-
if (planned.value === true) {
|
|
327
|
-
argv.push("--planned");
|
|
328
|
-
} else if (needsPlan.value === true) {
|
|
329
|
-
argv.push("--needs-plan");
|
|
330
|
-
}
|
|
331
|
-
argv.push("--", title.value);
|
|
332
|
-
|
|
333
|
-
return { ok: true, action, argv };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function buildNoteArgv(
|
|
337
|
-
params: Readonly<Record<string, unknown>>,
|
|
338
|
-
action: TsqChangeAction,
|
|
339
|
-
): ValidationResult {
|
|
340
|
-
const id = requireNonEmptyString(params, "id");
|
|
341
|
-
if (!id.ok) {
|
|
342
|
-
return id;
|
|
343
|
-
}
|
|
344
|
-
const note = requireNonEmptyString(params, "note");
|
|
345
|
-
if (!note.ok) {
|
|
346
|
-
return note;
|
|
347
|
-
}
|
|
348
|
-
return { ok: true, action, argv: ["note", id.value, "--", note.value] };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function buildOptionalNoteArgv(
|
|
352
|
-
params: Readonly<Record<string, unknown>>,
|
|
353
|
-
action: TsqChangeAction,
|
|
354
|
-
command: "done" | "defer",
|
|
355
|
-
): ValidationResult {
|
|
356
|
-
const id = requireNonEmptyString(params, "id");
|
|
357
|
-
if (!id.ok) {
|
|
358
|
-
return id;
|
|
359
|
-
}
|
|
360
|
-
const argv = [command, id.value];
|
|
361
|
-
const note = getOptionalNonEmptyString(params, "note");
|
|
362
|
-
if (!note.ok) {
|
|
363
|
-
return note;
|
|
364
|
-
}
|
|
365
|
-
if (note.value !== undefined) {
|
|
366
|
-
argv.push(`--note=${note.value}`);
|
|
367
|
-
}
|
|
368
|
-
return { ok: true, action, argv };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function buildIdOnlyArgv(
|
|
372
|
-
params: Readonly<Record<string, unknown>>,
|
|
373
|
-
action: TsqChangeAction,
|
|
374
|
-
command: "reopen" | "start",
|
|
375
|
-
): ValidationResult {
|
|
376
|
-
const id = requireNonEmptyString(params, "id");
|
|
377
|
-
if (!id.ok) {
|
|
378
|
-
return id;
|
|
379
|
-
}
|
|
380
|
-
return { ok: true, action, argv: [command, id.value] };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function buildClaimAssignOnlyArgv(
|
|
384
|
-
params: Readonly<Record<string, unknown>>,
|
|
385
|
-
action: TsqChangeAction,
|
|
386
|
-
): ValidationResult {
|
|
387
|
-
const id = requireNonEmptyString(params, "id");
|
|
388
|
-
if (!id.ok) {
|
|
389
|
-
return id;
|
|
390
|
-
}
|
|
391
|
-
const assignee = requireNonEmptyString(params, "assignee");
|
|
392
|
-
if (!assignee.ok) {
|
|
393
|
-
return assignee;
|
|
394
|
-
}
|
|
395
|
-
return {
|
|
396
|
-
ok: true,
|
|
397
|
-
action,
|
|
398
|
-
argv: ["claim", id.value, `--assignee=${assignee.value}`],
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function buildBlockArgv(
|
|
403
|
-
params: Readonly<Record<string, unknown>>,
|
|
404
|
-
action: TsqChangeAction,
|
|
405
|
-
command: "block" | "unblock",
|
|
406
|
-
): ValidationResult {
|
|
407
|
-
const child = requireNonEmptyString(params, "child");
|
|
408
|
-
if (!child.ok) {
|
|
409
|
-
return child;
|
|
410
|
-
}
|
|
411
|
-
const blocker = requireNonEmptyString(params, "blocker");
|
|
412
|
-
if (!blocker.ok) {
|
|
413
|
-
return blocker;
|
|
414
|
-
}
|
|
415
|
-
if (child.value === blocker.value) {
|
|
416
|
-
return { ok: false, message: "child and blocker cannot be the same task" };
|
|
417
|
-
}
|
|
418
|
-
return {
|
|
419
|
-
ok: true,
|
|
420
|
-
action,
|
|
421
|
-
argv: [command, child.value, "by", blocker.value],
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function buildOrderArgv(
|
|
426
|
-
params: Readonly<Record<string, unknown>>,
|
|
427
|
-
action: TsqChangeAction,
|
|
428
|
-
command: "order" | "unorder",
|
|
429
|
-
): ValidationResult {
|
|
430
|
-
const later = requireNonEmptyString(params, "later");
|
|
431
|
-
if (!later.ok) {
|
|
432
|
-
return later;
|
|
433
|
-
}
|
|
434
|
-
const earlier = requireNonEmptyString(params, "earlier");
|
|
435
|
-
if (!earlier.ok) {
|
|
436
|
-
return earlier;
|
|
437
|
-
}
|
|
438
|
-
if (later.value === earlier.value) {
|
|
439
|
-
return { ok: false, message: "later and earlier cannot be the same task" };
|
|
440
|
-
}
|
|
441
|
-
return {
|
|
442
|
-
ok: true,
|
|
443
|
-
action,
|
|
444
|
-
argv: [command, later.value, "after", earlier.value],
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function appendOptionalStringFlag(
|
|
449
|
-
argv: string[],
|
|
450
|
-
params: Readonly<Record<string, unknown>>,
|
|
451
|
-
field: "description" | "parent",
|
|
452
|
-
flag: string,
|
|
453
|
-
): ValidationResult | undefined {
|
|
454
|
-
const value = getOptionalNonEmptyString(params, field);
|
|
455
|
-
if (!value.ok) {
|
|
456
|
-
return value;
|
|
457
|
-
}
|
|
458
|
-
if (value.value !== undefined) {
|
|
459
|
-
argv.push(`${flag}=${value.value}`);
|
|
460
|
-
}
|
|
461
|
-
return undefined;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function requireNonEmptyString(
|
|
465
|
-
params: Readonly<Record<string, unknown>>,
|
|
466
|
-
field: string,
|
|
467
|
-
):
|
|
468
|
-
| { readonly ok: true; readonly value: string }
|
|
469
|
-
| { readonly ok: false; readonly message: string } {
|
|
470
|
-
const value = params[field];
|
|
471
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
472
|
-
return { ok: false, message: `${field} is required` };
|
|
473
|
-
}
|
|
474
|
-
return { ok: true, value };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function getOptionalNonEmptyString(
|
|
478
|
-
params: Readonly<Record<string, unknown>>,
|
|
479
|
-
field: string,
|
|
480
|
-
):
|
|
481
|
-
| { readonly ok: true; readonly value: string | undefined }
|
|
482
|
-
| { readonly ok: false; readonly message: string } {
|
|
483
|
-
const value = params[field];
|
|
484
|
-
if (value === undefined) {
|
|
485
|
-
return { ok: true, value: undefined };
|
|
486
|
-
}
|
|
487
|
-
if (typeof value !== "string") {
|
|
488
|
-
return { ok: false, message: `${field} must be a string` };
|
|
489
|
-
}
|
|
490
|
-
if (value.trim().length === 0) {
|
|
491
|
-
return { ok: true, value: undefined };
|
|
492
|
-
}
|
|
493
|
-
return { ok: true, value };
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function requireInteger(
|
|
497
|
-
params: Readonly<Record<string, unknown>>,
|
|
498
|
-
field: string,
|
|
499
|
-
):
|
|
500
|
-
| { readonly ok: true; readonly value: number }
|
|
501
|
-
| { readonly ok: false; readonly message: string } {
|
|
502
|
-
const value = params[field];
|
|
503
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
504
|
-
return { ok: false, message: `${field} is required` };
|
|
505
|
-
}
|
|
506
|
-
return { ok: true, value };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function getOptionalBoolean(
|
|
510
|
-
params: Readonly<Record<string, unknown>>,
|
|
511
|
-
field: string,
|
|
512
|
-
):
|
|
513
|
-
| { readonly ok: true; readonly value: boolean | undefined }
|
|
514
|
-
| { readonly ok: false; readonly message: string } {
|
|
515
|
-
const value = params[field];
|
|
516
|
-
if (value === undefined) {
|
|
517
|
-
return { ok: true, value: undefined };
|
|
518
|
-
}
|
|
519
|
-
if (typeof value !== "boolean") {
|
|
520
|
-
return { ok: false, message: `${field} must be a boolean` };
|
|
521
|
-
}
|
|
522
|
-
return { ok: true, value };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function isTsqChangeAction(value: unknown): value is TsqChangeAction {
|
|
526
|
-
return (
|
|
527
|
-
typeof value === "string" &&
|
|
528
|
-
(TSQ_CHANGE_ACTIONS as readonly string[]).includes(value)
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
231
|
function validationErrorResult(message: string) {
|
|
533
232
|
return textToolResult(
|
|
534
233
|
`Error: ${message}`,
|
|
@@ -602,13 +301,6 @@ function extractTaskLike(result: unknown): {
|
|
|
602
301
|
};
|
|
603
302
|
}
|
|
604
303
|
|
|
605
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
606
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
607
|
-
return undefined;
|
|
608
|
-
}
|
|
609
|
-
return value as Record<string, unknown>;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
304
|
function getErrorMessage(error: unknown): string {
|
|
613
305
|
if (error instanceof Error) {
|
|
614
306
|
return error.message;
|
|
@@ -635,22 +327,3 @@ function serializeError(error: unknown): Record<string, unknown> {
|
|
|
635
327
|
}
|
|
636
328
|
return { value: String(error) };
|
|
637
329
|
}
|
|
638
|
-
|
|
639
|
-
function copyKnownErrorFields(error: Error): Record<string, unknown> {
|
|
640
|
-
const record = error as unknown as Record<string, unknown>;
|
|
641
|
-
const output: Record<string, unknown> = {};
|
|
642
|
-
for (const key of [
|
|
643
|
-
"code",
|
|
644
|
-
"command",
|
|
645
|
-
"details",
|
|
646
|
-
"stderr",
|
|
647
|
-
"stdout",
|
|
648
|
-
"killed",
|
|
649
|
-
"args",
|
|
650
|
-
] as const) {
|
|
651
|
-
if (record[key] !== undefined) {
|
|
652
|
-
output[key] = record[key];
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return output;
|
|
656
|
-
}
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
} from "../session-todos/state/state-reducer.js";
|
|
16
16
|
import { commitState, getState } from "../session-todos/state/store.js";
|
|
17
17
|
import type { Task } from "../session-todos/tool/types.js";
|
|
18
|
+
import {
|
|
19
|
+
asRecord,
|
|
20
|
+
copyKnownErrorFields,
|
|
21
|
+
isRecord,
|
|
22
|
+
} from "../shared/error-utils.js";
|
|
18
23
|
import {
|
|
19
24
|
errorToolDetails,
|
|
20
25
|
okToolDetails,
|
|
@@ -391,33 +396,3 @@ function serializeError(error: unknown): Record<string, unknown> {
|
|
|
391
396
|
}
|
|
392
397
|
return { value: String(error) };
|
|
393
398
|
}
|
|
394
|
-
|
|
395
|
-
function copyKnownErrorFields(error: Error): Record<string, unknown> {
|
|
396
|
-
const record = error as unknown as Record<string, unknown>;
|
|
397
|
-
const output: Record<string, unknown> = {};
|
|
398
|
-
for (const key of [
|
|
399
|
-
"code",
|
|
400
|
-
"command",
|
|
401
|
-
"details",
|
|
402
|
-
"stderr",
|
|
403
|
-
"stdout",
|
|
404
|
-
"killed",
|
|
405
|
-
"args",
|
|
406
|
-
] as const) {
|
|
407
|
-
if (record[key] !== undefined) {
|
|
408
|
-
output[key] = record[key];
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return output;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
415
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
return value as Record<string, unknown>;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
422
|
-
return asRecord(value) !== undefined;
|
|
423
|
-
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { errorToolDetails, textToolResult } from "../shared/tool-result.js";
|
|
7
|
+
import {
|
|
8
|
+
collectHandoffStatus,
|
|
9
|
+
type HandoffCheckResult,
|
|
10
|
+
} from "./handoff-guard.js";
|
|
11
|
+
|
|
12
|
+
export async function executeHandoffCheck(
|
|
13
|
+
pi: ExtensionAPI,
|
|
14
|
+
signal: AbortSignal | undefined,
|
|
15
|
+
ctx: ExtensionContext,
|
|
16
|
+
): Promise<AgentToolResult<unknown>> {
|
|
17
|
+
const result = await collectHandoffStatus({
|
|
18
|
+
pi,
|
|
19
|
+
cwd: ctx.cwd,
|
|
20
|
+
...(signal != null ? { signal } : {}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
return textToolResult(
|
|
25
|
+
`Error: ${result.message}`,
|
|
26
|
+
errorToolDetails({ code: result.code, message: result.message }),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return textToolResult(formatHandoffText(result), {
|
|
31
|
+
ok: true,
|
|
32
|
+
ready: result.ready,
|
|
33
|
+
...formatHandoffDetails(result),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatHandoffText(result: HandoffCheckResult & { ok: true }): string {
|
|
38
|
+
const lines: string[] = [
|
|
39
|
+
result.ready
|
|
40
|
+
? "Handoff ready: all session todos complete and linked tasks resolved."
|
|
41
|
+
: "Handoff not ready.",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if ("todoBlockers" in result && result.todoBlockers?.length) {
|
|
45
|
+
lines.push("", "Todo blockers:");
|
|
46
|
+
for (const b of result.todoBlockers) {
|
|
47
|
+
lines.push(`- #${b.todoId} "${b.subject}" — ${b.reason}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ("linkedBlockers" in result && result.linkedBlockers?.length) {
|
|
52
|
+
lines.push("", "Linked task blockers:");
|
|
53
|
+
for (const b of result.linkedBlockers) {
|
|
54
|
+
lines.push(`- ${b.tsqId} (todo #${b.todoId}) — ${b.status}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ("linkedWarnings" in result && result.linkedWarnings?.length) {
|
|
59
|
+
lines.push("", "Warnings:");
|
|
60
|
+
for (const w of result.linkedWarnings) {
|
|
61
|
+
lines.push(`- ${w.tsqId} (todo #${w.todoId}) — ${w.status}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if ("readErrors" in result && result.readErrors?.length) {
|
|
66
|
+
lines.push("", "Read errors:");
|
|
67
|
+
for (const e of result.readErrors) {
|
|
68
|
+
lines.push(`- ${e.tsqId} — ${e.code}: ${e.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatHandoffDetails(
|
|
76
|
+
result: HandoffCheckResult & { ok: true },
|
|
77
|
+
): Record<string, unknown> {
|
|
78
|
+
const details: Record<string, unknown> = {};
|
|
79
|
+
if (result.projectRoot !== undefined) {
|
|
80
|
+
details.projectRoot = result.projectRoot;
|
|
81
|
+
}
|
|
82
|
+
if ("todoBlockers" in result && result.todoBlockers?.length) {
|
|
83
|
+
details.todoBlockers = result.todoBlockers;
|
|
84
|
+
}
|
|
85
|
+
if ("linkedBlockers" in result && result.linkedBlockers?.length) {
|
|
86
|
+
details.linkedBlockers = result.linkedBlockers;
|
|
87
|
+
}
|
|
88
|
+
if ("linkedWarnings" in result && result.linkedWarnings?.length) {
|
|
89
|
+
details.linkedWarnings = result.linkedWarnings;
|
|
90
|
+
}
|
|
91
|
+
if ("readErrors" in result && result.readErrors?.length) {
|
|
92
|
+
details.readErrors = result.readErrors;
|
|
93
|
+
}
|
|
94
|
+
return details;
|
|
95
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
READ_TASKS_PROMPT_GUIDELINES,
|
|
10
10
|
READ_TASKS_PROMPT_SNIPPET,
|
|
11
11
|
} from "../guidelines/internal-tools.js";
|
|
12
|
+
import { isRecord } from "../shared/error-utils.js";
|
|
12
13
|
import { truncatedTextToolResult } from "../shared/tool-result.js";
|
|
13
14
|
import type { TruncatedText } from "../shared/truncation.js";
|
|
14
15
|
import { runTsqJson } from "./runner.js";
|
|
@@ -522,7 +523,3 @@ function isTaskTreeNode(value: unknown): value is TsqTaskTreeNode {
|
|
|
522
523
|
isRecord(value) && isTsqTask(value.task) && Array.isArray(value.children)
|
|
523
524
|
);
|
|
524
525
|
}
|
|
525
|
-
|
|
526
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
527
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
528
|
-
}
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
import type { CreateTreeNode, CreateTreeResult } from "./bulk-contract.js";
|
|
18
18
|
import { runQueuedMutation } from "./mutation-queue.js";
|
|
19
19
|
import { runTsqJson } from "./runner.js";
|
|
20
|
+
import { asRecord } from "../shared/error-utils.js";
|
|
20
21
|
import {
|
|
21
22
|
okToolDetails,
|
|
22
23
|
textToolResult,
|
|
@@ -218,10 +219,3 @@ function formatResultText(result: CreateTreeResult): string {
|
|
|
218
219
|
function getErrorMessage(error: unknown): string {
|
|
219
220
|
return error instanceof Error ? error.message : String(error);
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
223
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
226
|
-
return value as Record<string, unknown>;
|
|
227
|
-
}
|