@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.
@@ -1,232 +1,44 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
1
  import {
3
2
  defineTool,
4
3
  type AgentToolResult,
5
4
  type ExtensionAPI,
6
5
  type ExtensionContext,
7
6
  } from "@earendil-works/pi-coding-agent";
8
- import { type Static, Type } from "typebox";
9
7
  import { executeTaskBridge } from "../bridge/bridge-tool.js";
10
- import type { TaskBridgeHandlers, TaskBridgeParams } from "../bridge/types.js";
8
+ import type { TaskBridgeHandlers } from "../bridge/types.js";
11
9
  import {
12
10
  TASK_PROMPT_GUIDELINES,
13
11
  TASK_PROMPT_SNIPPET,
14
12
  } from "../guidelines/task.js";
13
+ import { isRecord } from "../shared/error-utils.js";
15
14
  import { errorToolDetails, textToolResult } from "../shared/tool-result.js";
16
15
  import { importTsqHandler } from "../bridge/import-tsq.js";
17
16
  import { promoteTodoHandler } from "../bridge/promote-todo.js";
18
- import {
19
- collectHandoffStatus,
20
- type HandoffCheckResult,
21
- } from "./handoff-guard.js";
17
+ import type { BulkItem, CreateTreeNode } from "./bulk-contract.js";
22
18
  import { resolveProjectRoot } from "./project.js";
23
19
  import {
24
- executeTsqChange,
25
- executeTsqMarkPlanned,
26
- type TsqChangeParams,
27
- } from "./tools-change.js";
28
- import { executeTsqSpec, type SpecMode } from "./tools-spec.js";
20
+ TASK_TOOL_NAME,
21
+ TaskParamsSchema,
22
+ type TaskAction,
23
+ type TaskParams,
24
+ } from "./task-schema.js";
25
+ import { actionUsesTasque, validateTaskParams } from "./task-validation.js";
29
26
  import {
30
- BULK_ITEM_ACTIONS,
31
- validateBulkItems,
32
- validateCreateTreeNode,
33
- type BulkItem,
34
- type CreateTreeNode,
35
- } from "./bulk-contract.js";
27
+ toBridgeParams,
28
+ toChangeParams,
29
+ toClaimParams,
30
+ toQueryParams,
31
+ } from "./task-mappers.js";
36
32
  import { executeBulk } from "./tools-bulk.js";
37
- import { executeCreateTree } from "./tools-tree-create.js";
33
+ import { executeTsqChange, executeTsqMarkPlanned } from "./tools-change.js";
38
34
  import { executeTsqClaim } from "./tools-claim.js";
39
- import { executeTsqQuery, type TsqQueryParams } from "./tools-query.js";
40
-
41
- export const TASK_TOOL_NAME = "task";
42
-
43
- const TASK_ACTIONS = [
44
- "doctor",
45
- "find",
46
- "show",
47
- "deps",
48
- "notes",
49
- "similar",
50
- "create",
51
- "note",
52
- "finish",
53
- "reopen",
54
- "defer",
55
- "start",
56
- "claim",
57
- "block",
58
- "unblock",
59
- "order",
60
- "unorder",
61
- "spec",
62
- "mark_planned",
63
- "bulk",
64
- "create_tree",
65
- "handoff_check",
66
- "link",
67
- "list_links",
68
- "promote",
69
- "import",
70
- ] as const;
71
-
72
- const FIND_TARGETS = ["ready", "open"] as const;
73
- const VIEW_MODES = ["list", "tree"] as const;
74
- const SPEC_MODES = ["show", "check", "set", "update"] as const;
75
- const BRIDGE_DESTINATIONS = ["todo"] as const;
76
-
77
- const BulkItemParamsSchema = Type.Object({
78
- action: StringEnum(BULK_ITEM_ACTIONS, {
79
- description:
80
- "Bulk item action: start, finish, reopen, defer, note, or mark_planned.",
81
- }),
82
- task: Type.String({ description: "Durable task id for this bulk item." }),
83
- because: Type.Optional(
84
- Type.String({
85
- description:
86
- "Note/reason text. Required for note; optional for finish/defer.",
87
- }),
88
- ),
89
- });
90
-
91
- const CreateTreeNodeParamsSchema = Type.Object({
92
- title: Type.String({ description: "Durable task title for this node." }),
93
- kind: Type.String({ description: "Durable task kind for this node." }),
94
- priority: Type.Integer({
95
- description: "Durable task priority for this node.",
96
- }),
97
- description: Type.Optional(
98
- Type.String({ description: "Task description for this node." }),
99
- ),
100
- planned: Type.Optional(
101
- Type.Boolean({ description: "Mark this node planned." }),
102
- ),
103
- needsPlan: Type.Optional(
104
- Type.Boolean({ description: "Mark this node as needing planning." }),
105
- ),
106
- children: Type.Optional(
107
- Type.Array(
108
- Type.Unknown({
109
- description:
110
- "Child create-tree nodes with the same shape: { title, kind, priority, description?, planned?, needsPlan?, children? }.",
111
- }),
112
- { description: "Nested child task nodes." },
113
- ),
114
- ),
115
- });
116
-
117
- export type TaskAction = (typeof TASK_ACTIONS)[number];
118
-
119
- export const TaskParamsSchema = Type.Object(
120
- {
121
- action: StringEnum(TASK_ACTIONS, {
122
- description: "Durable task action to run.",
123
- }),
124
- task: Type.Optional(
125
- Type.String({
126
- description: "Durable task id for existing tasks, or title for create.",
127
- }),
128
- ),
129
- tasks: Type.Optional(
130
- StringEnum(FIND_TARGETS, {
131
- description: "Task set for find actions.",
132
- }),
133
- ),
134
- view: Type.Optional(
135
- StringEnum(VIEW_MODES, {
136
- description: "Find output view.",
137
- }),
138
- ),
139
- lane: Type.Optional(
140
- Type.String({ description: "Ready-task lane, e.g. planning or coding." }),
141
- ),
142
- for: Type.Optional(
143
- Type.String({ description: "Assignee or owner for claim/find/import." }),
144
- ),
145
- query: Type.Optional(
146
- Type.String({ description: "Search text for similar task lookup." }),
147
- ),
148
- with: Type.Optional(
149
- Type.Array(
150
- Type.String({ description: "Extra context to include, e.g. spec." }),
151
- ),
152
- ),
153
- kind: Type.Optional(Type.String({ description: "Durable task kind." })),
154
- priority: Type.Optional(
155
- Type.Integer({ description: "Durable task priority." }),
156
- ),
157
- description: Type.Optional(
158
- Type.String({ description: "Task description for create/promote." }),
159
- ),
160
- under: Type.Optional(
161
- Type.String({
162
- description: "Parent durable task id for create/promote.",
163
- }),
164
- ),
165
- planned: Type.Optional(
166
- Type.Boolean({ description: "Mark created/promoted task planned." }),
167
- ),
168
- needsPlan: Type.Optional(
169
- Type.Boolean({
170
- description: "Mark created/promoted task as needing planning.",
171
- }),
172
- ),
173
- because: Type.Optional(
174
- Type.String({
175
- description: "Note or reason text for lifecycle actions.",
176
- }),
177
- ),
178
- by: Type.Optional(
179
- Type.String({
180
- description: "Blocking durable task id for block/unblock.",
181
- }),
182
- ),
183
- after: Type.Optional(
184
- Type.String({
185
- description: "Earlier durable task id for order/unorder.",
186
- }),
187
- ),
188
- start: Type.Optional(
189
- Type.Boolean({
190
- description: "Start task while claiming. Defaults true.",
191
- }),
192
- ),
193
- requireSpec: Type.Optional(
194
- Type.Boolean({ description: "Require an attached spec before claim." }),
195
- ),
196
- todo: Type.Optional(
197
- Type.Union([
198
- Type.Boolean({ description: "Create a linked todo for claim." }),
199
- Type.Integer({ description: "Session todo id for link/promote." }),
200
- ]),
201
- ),
202
- mode: Type.Optional(
203
- StringEnum(SPEC_MODES, {
204
- description: "Spec operation mode for spec action.",
205
- }),
206
- ),
207
- text: Type.Optional(
208
- Type.String({
209
- description: "Spec text content for spec set/update.",
210
- }),
211
- ),
212
- to: Type.Optional(
213
- StringEnum(BRIDGE_DESTINATIONS, {
214
- description: "Bridge destination for import.",
215
- }),
216
- ),
217
- items: Type.Optional(
218
- Type.Array(BulkItemParamsSchema, {
219
- description:
220
- "Bulk lifecycle items. Each item has { action, task, because? }.",
221
- minItems: 1,
222
- }),
223
- ),
224
- root: Type.Optional(CreateTreeNodeParamsSchema),
225
- },
226
- { additionalProperties: false },
227
- );
35
+ import { executeHandoffCheck } from "./tools-handoff.js";
36
+ import { executeTsqQuery } from "./tools-query.js";
37
+ import { executeTsqSpec, type SpecMode } from "./tools-spec.js";
38
+ import { executeCreateTree } from "./tools-tree-create.js";
228
39
 
229
- export type TaskParams = Static<typeof TaskParamsSchema>;
40
+ // --- Re-exports for backward compatibility ---
41
+ export { TASK_TOOL_NAME, TaskParamsSchema, type TaskParams, type TaskAction } from "./task-schema.js";
230
42
 
231
43
  const DEFAULT_HANDLERS: TaskBridgeHandlers = {
232
44
  promote_todo: promoteTodoHandler,
@@ -344,381 +156,6 @@ async function dispatchTaskAction(
344
156
  }
345
157
  }
346
158
 
347
- async function executeHandoffCheck(
348
- pi: ExtensionAPI,
349
- signal: AbortSignal | undefined,
350
- ctx: ExtensionContext,
351
- ): Promise<AgentToolResult<unknown>> {
352
- const result = await collectHandoffStatus({
353
- pi,
354
- cwd: ctx.cwd,
355
- ...(signal != null ? { signal } : {}),
356
- });
357
-
358
- if (!result.ok) {
359
- return textToolResult(
360
- `Error: ${result.message}`,
361
- errorToolDetails({ code: result.code, message: result.message }),
362
- );
363
- }
364
-
365
- return textToolResult(formatHandoffText(result), {
366
- ok: true,
367
- ready: result.ready,
368
- ...formatHandoffDetails(result),
369
- });
370
- }
371
-
372
- function formatHandoffText(result: HandoffCheckResult & { ok: true }): string {
373
- const lines: string[] = [
374
- result.ready
375
- ? "Handoff ready: all session todos complete and linked tasks resolved."
376
- : "Handoff not ready.",
377
- ];
378
-
379
- if ("todoBlockers" in result && result.todoBlockers?.length) {
380
- lines.push("", "Todo blockers:");
381
- for (const b of result.todoBlockers) {
382
- lines.push(`- #${b.todoId} "${b.subject}" — ${b.reason}`);
383
- }
384
- }
385
-
386
- if ("linkedBlockers" in result && result.linkedBlockers?.length) {
387
- lines.push("", "Linked task blockers:");
388
- for (const b of result.linkedBlockers) {
389
- lines.push(`- ${b.tsqId} (todo #${b.todoId}) — ${b.status}`);
390
- }
391
- }
392
-
393
- if ("linkedWarnings" in result && result.linkedWarnings?.length) {
394
- lines.push("", "Warnings:");
395
- for (const w of result.linkedWarnings) {
396
- lines.push(`- ${w.tsqId} (todo #${w.todoId}) — ${w.status}`);
397
- }
398
- }
399
-
400
- if ("readErrors" in result && result.readErrors?.length) {
401
- lines.push("", "Read errors:");
402
- for (const e of result.readErrors) {
403
- lines.push(`- ${e.tsqId} — ${e.code}: ${e.message}`);
404
- }
405
- }
406
-
407
- return lines.join("\n");
408
- }
409
-
410
- function formatHandoffDetails(
411
- result: HandoffCheckResult & { ok: true },
412
- ): Record<string, unknown> {
413
- const details: Record<string, unknown> = {};
414
- if (result.projectRoot !== undefined) {
415
- details.projectRoot = result.projectRoot;
416
- }
417
- if ("todoBlockers" in result && result.todoBlockers?.length) {
418
- details.todoBlockers = result.todoBlockers;
419
- }
420
- if ("linkedBlockers" in result && result.linkedBlockers?.length) {
421
- details.linkedBlockers = result.linkedBlockers;
422
- }
423
- if ("linkedWarnings" in result && result.linkedWarnings?.length) {
424
- details.linkedWarnings = result.linkedWarnings;
425
- }
426
- if ("readErrors" in result && result.readErrors?.length) {
427
- details.readErrors = result.readErrors;
428
- }
429
- return details;
430
- }
431
-
432
- function toQueryParams(params: TaskParams): TsqQueryParams {
433
- switch (params.action) {
434
- case "doctor":
435
- return { action: "doctor" };
436
- case "find":
437
- if (params.view === "tree") {
438
- return definedParams<TsqQueryParams>({
439
- action: "find_tree",
440
- id: params.task,
441
- });
442
- }
443
- return params.tasks === "open"
444
- ? definedParams<TsqQueryParams>({
445
- action: "find_open",
446
- assignee: params.for,
447
- })
448
- : definedParams<TsqQueryParams>({
449
- action: "find_ready",
450
- lane: params.lane,
451
- assignee: params.for,
452
- });
453
- case "show":
454
- return definedParams<TsqQueryParams>({
455
- action: hasWith(params, "spec") ? "show_with_spec" : "show",
456
- id: params.task,
457
- });
458
- case "deps":
459
- return definedParams<TsqQueryParams>({ action: "deps", id: params.task });
460
- case "notes":
461
- return definedParams<TsqQueryParams>({
462
- action: "notes",
463
- id: params.task,
464
- });
465
- case "similar":
466
- return definedParams<TsqQueryParams>({
467
- action: "similar",
468
- query: params.query,
469
- });
470
- default:
471
- throw new Error(`Unsupported query action: ${params.action}`);
472
- }
473
- }
474
-
475
- function toChangeParams(params: TaskParams): TsqChangeParams {
476
- switch (params.action) {
477
- case "create":
478
- return definedParams<TsqChangeParams>({
479
- action: "create",
480
- title: params.task,
481
- kind: params.kind,
482
- priority: params.priority,
483
- description: params.description,
484
- parent: params.under,
485
- planned: params.planned,
486
- needsPlan: params.needsPlan,
487
- });
488
- case "note":
489
- return definedParams<TsqChangeParams>({
490
- action: "note",
491
- id: params.task,
492
- note: params.because,
493
- });
494
- case "finish":
495
- return definedParams<TsqChangeParams>({
496
- action: "done",
497
- id: params.task,
498
- note: params.because,
499
- });
500
- case "reopen":
501
- case "start":
502
- return definedParams<TsqChangeParams>({
503
- action: params.action,
504
- id: params.task,
505
- });
506
- case "defer":
507
- return definedParams<TsqChangeParams>({
508
- action: "defer",
509
- id: params.task,
510
- note: params.because,
511
- });
512
- case "block":
513
- return definedParams<TsqChangeParams>({
514
- action: "block",
515
- child: params.task,
516
- blocker: params.by,
517
- });
518
- case "unblock":
519
- return definedParams<TsqChangeParams>({
520
- action: "unblock",
521
- child: params.task,
522
- blocker: params.by,
523
- });
524
- case "order":
525
- return definedParams<TsqChangeParams>({
526
- action: "order",
527
- later: params.task,
528
- earlier: params.after,
529
- });
530
- case "unorder":
531
- return definedParams<TsqChangeParams>({
532
- action: "unorder",
533
- later: params.task,
534
- earlier: params.after,
535
- });
536
- default:
537
- throw new Error(`Unsupported change action: ${params.action}`);
538
- }
539
- }
540
-
541
- function toClaimParams(params: TaskParams): Readonly<Record<string, unknown>> {
542
- return definedParams<Readonly<Record<string, unknown>>>({
543
- id: params.task,
544
- assignee: params.for,
545
- start: params.start,
546
- requireSpec: params.requireSpec,
547
- createTodo: params.todo === true,
548
- });
549
- }
550
-
551
- function toBridgeParams(params: TaskParams): TaskBridgeParams {
552
- switch (params.action) {
553
- case "link":
554
- return definedParams<TaskBridgeParams>({
555
- action: "link",
556
- todoId: getTodoId(params.todo),
557
- tsqId: params.task,
558
- });
559
- case "list_links":
560
- return { action: "list_links" };
561
- case "promote":
562
- return definedParams<TaskBridgeParams>({
563
- action: "promote_todo",
564
- todoId: getTodoId(params.todo),
565
- assignee: params.for,
566
- kind: params.kind,
567
- priority: params.priority,
568
- description: params.description,
569
- parent: params.under,
570
- planned: params.planned,
571
- needsPlan: params.needsPlan,
572
- });
573
- case "import":
574
- return definedParams<TaskBridgeParams>({
575
- action: "import_tsq",
576
- tsqId: params.task,
577
- owner: params.for,
578
- });
579
- default:
580
- throw new Error(`Unsupported bridge action: ${params.action}`);
581
- }
582
- }
583
-
584
- function definedParams<T>(params: Record<string, unknown>): T {
585
- const output: Record<string, unknown> = {};
586
- for (const [key, value] of Object.entries(params)) {
587
- if (value !== undefined) {
588
- output[key] = value;
589
- }
590
- }
591
- return output as T;
592
- }
593
-
594
- function validateTaskParams(
595
- params: TaskParams,
596
- ): { readonly ok: true } | { readonly ok: false; readonly message: string } {
597
- switch (params.action) {
598
- case "find":
599
- if (params.view === "tree") return { ok: true };
600
- if (params.tasks === undefined) return fieldRequired("tasks");
601
- return { ok: true };
602
- case "show":
603
- case "deps":
604
- case "notes":
605
- case "note":
606
- case "finish":
607
- case "reopen":
608
- case "defer":
609
- case "start":
610
- case "claim":
611
- case "import":
612
- return requireStringField(params.task, "task");
613
- case "create": {
614
- const task = requireStringField(params.task, "task");
615
- if (!task.ok) return task;
616
- const kind = requireStringField(params.kind, "kind");
617
- if (!kind.ok) return kind;
618
- return typeof params.priority === "number"
619
- ? { ok: true }
620
- : fieldRequired("priority");
621
- }
622
- case "similar":
623
- return requireStringField(params.query, "query");
624
- case "block":
625
- case "unblock": {
626
- const task = requireStringField(params.task, "task");
627
- if (!task.ok) return task;
628
- return requireStringField(params.by, "by");
629
- }
630
- case "order":
631
- case "unorder": {
632
- const task = requireStringField(params.task, "task");
633
- if (!task.ok) return task;
634
- return requireStringField(params.after, "after");
635
- }
636
- case "link": {
637
- const todo = requireTodoId(params.todo);
638
- if (!todo.ok) return todo;
639
- return requireStringField(params.task, "task");
640
- }
641
- case "promote":
642
- return requireTodoId(params.todo);
643
- case "mark_planned":
644
- return requireStringField(params.task, "task");
645
- case "spec": {
646
- const task = requireStringField(params.task, "task");
647
- if (!task.ok) return task;
648
- const mode = params.mode as string | undefined;
649
- if (mode === undefined || mode.trim().length === 0)
650
- return fieldRequired("mode");
651
- const isRead = mode === "show" || mode === "check";
652
- const isWrite = mode === "set" || mode === "update";
653
- if (!isRead && !isWrite)
654
- return {
655
- ok: false,
656
- message: "mode must be show, check, set, or update",
657
- };
658
- if (isRead && params.text !== undefined)
659
- return {
660
- ok: false,
661
- message: `spec ${mode} does not accept text`,
662
- };
663
- if (isWrite) {
664
- const text = params.text?.trim();
665
- if (text === undefined || text.length === 0)
666
- return {
667
- ok: false,
668
- message: `spec ${mode} requires text`,
669
- };
670
- }
671
- return { ok: true };
672
- }
673
- case "bulk":
674
- return validateBulkItems(params.items);
675
- case "create_tree":
676
- return validateCreateTreeNode(params.root);
677
- case "handoff_check":
678
- case "doctor":
679
- case "list_links":
680
- return { ok: true };
681
- }
682
- }
683
-
684
- function actionUsesTasque(action: TaskAction): boolean {
685
- return (
686
- action !== "link" && action !== "list_links" && action !== "handoff_check"
687
- );
688
- }
689
-
690
- function hasWith(params: TaskParams, value: string): boolean {
691
- return Array.isArray(params.with) && params.with.includes(value);
692
- }
693
-
694
- function getTodoId(value: boolean | number | undefined): number | undefined {
695
- return typeof value === "number" ? value : undefined;
696
- }
697
-
698
- function requireTodoId(
699
- value: boolean | number | undefined,
700
- ): { readonly ok: true } | { readonly ok: false; readonly message: string } {
701
- return typeof value === "number" && Number.isInteger(value) && value >= 1
702
- ? { ok: true }
703
- : fieldRequired("todo");
704
- }
705
-
706
- function requireStringField(
707
- value: string | undefined,
708
- field: string,
709
- ): { readonly ok: true } | { readonly ok: false; readonly message: string } {
710
- return typeof value === "string" && value.trim().length > 0
711
- ? { ok: true }
712
- : fieldRequired(field);
713
- }
714
-
715
- function fieldRequired(field: string): {
716
- readonly ok: false;
717
- readonly message: string;
718
- } {
719
- return { ok: false, message: `${field} is required` };
720
- }
721
-
722
159
  function validationErrorResult(message: string): AgentToolResult<unknown> {
723
160
  return errorResult("validation_error", message);
724
161
  }
@@ -758,7 +195,3 @@ function serializeError(error: unknown): Record<string, unknown> {
758
195
  }
759
196
  return { value: String(error) };
760
197
  }
761
-
762
- function isRecord(value: unknown): value is Record<string, unknown> {
763
- return typeof value === "object" && value !== null && !Array.isArray(value);
764
- }