@bumpyclock/pi-tasque 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -140
- package/package.json +24 -6
- package/src/bridge/bridge-tool.ts +10 -13
- package/src/bridge/types.ts +11 -12
- package/src/durable-tasks/bulk-contract.ts +176 -0
- package/src/durable-tasks/handoff-guard.ts +329 -0
- package/src/durable-tasks/project.ts +71 -0
- package/src/durable-tasks/status.ts +21 -3
- package/src/durable-tasks/task-tool.ts +764 -0
- package/src/durable-tasks/tools-bulk.ts +144 -0
- package/src/durable-tasks/tools-change.ts +107 -51
- package/src/durable-tasks/tools-claim.ts +10 -13
- package/src/durable-tasks/tools-query.ts +57 -25
- package/src/durable-tasks/tools-spec.ts +230 -0
- package/src/durable-tasks/tools-tree-create.ts +227 -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 +10 -11
- package/src/session-todos/tool/types.ts +7 -7
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk lifecycle executor for durable tasks (tsq-6.2).
|
|
3
|
+
*
|
|
4
|
+
* Runs validated BulkItem[] sequentially with fail-fast semantics.
|
|
5
|
+
* Reuses executeTsqChange / executeTsqMarkPlanned — no raw CLI argv
|
|
6
|
+
* in the public contract.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AgentToolResult,
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
ExtensionContext,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { BulkItem, BulkItemAction, BulkResult } from "./bulk-contract.js";
|
|
15
|
+
import type { StandardToolDetails } from "../shared/tool-result.js";
|
|
16
|
+
import { okToolDetails, textToolResult } from "../shared/tool-result.js";
|
|
17
|
+
import {
|
|
18
|
+
executeTsqChange,
|
|
19
|
+
executeTsqMarkPlanned,
|
|
20
|
+
type TsqChangeAction,
|
|
21
|
+
} from "./tools-change.js";
|
|
22
|
+
|
|
23
|
+
// ── Action mapping ─────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const BULK_TO_CHANGE_ACTION: Record<
|
|
26
|
+
Exclude<BulkItemAction, "mark_planned">,
|
|
27
|
+
TsqChangeAction
|
|
28
|
+
> = {
|
|
29
|
+
start: "start",
|
|
30
|
+
finish: "done",
|
|
31
|
+
reopen: "reopen",
|
|
32
|
+
defer: "defer",
|
|
33
|
+
note: "note",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── Public executor ────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute a validated bulk lifecycle operation.
|
|
40
|
+
*
|
|
41
|
+
* Items run sequentially via the existing mutation queue.
|
|
42
|
+
* On first failure, remaining items are marked skipped (no rollback).
|
|
43
|
+
* Result details are always `ok: true` — failure info lives in `BulkResult.failed`.
|
|
44
|
+
*/
|
|
45
|
+
export async function executeBulk(
|
|
46
|
+
pi: ExtensionAPI,
|
|
47
|
+
items: readonly BulkItem[],
|
|
48
|
+
signal: AbortSignal | undefined,
|
|
49
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
50
|
+
): Promise<AgentToolResult<StandardToolDetails<BulkResult>>> {
|
|
51
|
+
const completed: string[] = [];
|
|
52
|
+
let failed: BulkResult["failed"];
|
|
53
|
+
const skipped: string[] = [];
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < items.length; i++) {
|
|
56
|
+
const item = items[i]!;
|
|
57
|
+
const result = await executeOneItem(pi, item, signal, ctx);
|
|
58
|
+
|
|
59
|
+
if (isResultOk(result)) {
|
|
60
|
+
completed.push(item.task);
|
|
61
|
+
} else {
|
|
62
|
+
failed = { task: item.task, error: extractErrorMessage(result) };
|
|
63
|
+
for (let j = i + 1; j < items.length; j++) {
|
|
64
|
+
skipped.push(items[j]!.task);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bulkResult: BulkResult = {
|
|
71
|
+
completed,
|
|
72
|
+
...(failed !== undefined ? { failed } : {}),
|
|
73
|
+
skipped,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return textToolResult(
|
|
77
|
+
formatBulkText(bulkResult, items.length),
|
|
78
|
+
okToolDetails(bulkResult),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Single-item dispatch ───────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function executeOneItem(
|
|
85
|
+
pi: ExtensionAPI,
|
|
86
|
+
item: BulkItem,
|
|
87
|
+
signal: AbortSignal | undefined,
|
|
88
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
89
|
+
): Promise<AgentToolResult<unknown>> {
|
|
90
|
+
if (item.action === "mark_planned") {
|
|
91
|
+
return executeTsqMarkPlanned(pi, item.task, signal, ctx);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const action = BULK_TO_CHANGE_ACTION[item.action];
|
|
95
|
+
const params = buildChangeParams(action, item);
|
|
96
|
+
return executeTsqChange(pi, params, signal, ctx);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildChangeParams(
|
|
100
|
+
action: TsqChangeAction,
|
|
101
|
+
item: BulkItem,
|
|
102
|
+
): { action: TsqChangeAction; id: string; note?: string } {
|
|
103
|
+
if (item.because !== undefined) {
|
|
104
|
+
return { action, id: item.task, note: item.because };
|
|
105
|
+
}
|
|
106
|
+
return { action, id: item.task };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Result inspection ──────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function isResultOk(result: AgentToolResult<unknown>): boolean {
|
|
112
|
+
const d = result.details;
|
|
113
|
+
return isRecord(d) && d.ok === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractErrorMessage(result: AgentToolResult<unknown>): string {
|
|
117
|
+
const d = result.details;
|
|
118
|
+
if (isRecord(d) && isRecord(d.error)) {
|
|
119
|
+
const msg = d.error.message;
|
|
120
|
+
if (typeof msg === "string") return msg;
|
|
121
|
+
}
|
|
122
|
+
return "unknown error";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Formatting ─────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function formatBulkText(result: BulkResult, total: number): string {
|
|
128
|
+
if (result.failed === undefined) {
|
|
129
|
+
return `Bulk: ${result.completed.length}/${total} completed`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parts = [`Bulk: ${result.completed.length}/${total} completed`];
|
|
133
|
+
parts.push("1 failed");
|
|
134
|
+
if (result.skipped.length > 0) {
|
|
135
|
+
parts.push(`${result.skipped.length} skipped`);
|
|
136
|
+
}
|
|
137
|
+
return `${parts.join(", ")}. Failed: ${result.failed.task} \u2014 ${result.failed.error}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Utilities ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
143
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
144
|
+
}
|
|
@@ -1,17 +1,22 @@
|
|
|
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";
|
|
10
13
|
import {
|
|
11
14
|
errorToolDetails,
|
|
12
15
|
okToolDetails,
|
|
13
16
|
textToolResult,
|
|
14
17
|
} from "../shared/tool-result.js";
|
|
18
|
+
import { runQueuedMutation } from "./mutation-queue.js";
|
|
19
|
+
import { runTsqJson } from "./runner.js";
|
|
15
20
|
|
|
16
21
|
export const TSQ_CHANGE_TOOL_NAME = "tsq_change";
|
|
17
22
|
|
|
@@ -34,27 +39,29 @@ export type TsqChangeAction = (typeof TSQ_CHANGE_ACTIONS)[number];
|
|
|
34
39
|
export const TsqChangeParamsSchema = Type.Object(
|
|
35
40
|
{
|
|
36
41
|
action: StringEnum(TSQ_CHANGE_ACTIONS, {
|
|
37
|
-
description: "Durable
|
|
42
|
+
description: "Durable task mutation to run",
|
|
38
43
|
}),
|
|
39
44
|
title: Type.Optional(
|
|
40
45
|
Type.String({ description: "Task title (required for create)" }),
|
|
41
46
|
),
|
|
42
47
|
id: Type.Optional(
|
|
43
48
|
Type.String({
|
|
44
|
-
description: "
|
|
49
|
+
description: "Durable task id for lifecycle/note/claim actions",
|
|
45
50
|
}),
|
|
46
51
|
),
|
|
47
52
|
kind: Type.Optional(
|
|
48
|
-
Type.String({ description: "
|
|
53
|
+
Type.String({ description: "Durable task kind (required for create)" }),
|
|
49
54
|
),
|
|
50
55
|
priority: Type.Optional(
|
|
51
|
-
Type.Integer({
|
|
56
|
+
Type.Integer({
|
|
57
|
+
description: "Durable task priority (required for create)",
|
|
58
|
+
}),
|
|
52
59
|
),
|
|
53
60
|
description: Type.Optional(
|
|
54
61
|
Type.String({ description: "Task description (create only)" }),
|
|
55
62
|
),
|
|
56
63
|
parent: Type.Optional(
|
|
57
|
-
Type.String({ description: "Parent
|
|
64
|
+
Type.String({ description: "Parent durable task id (create only)" }),
|
|
58
65
|
),
|
|
59
66
|
planned: Type.Optional(
|
|
60
67
|
Type.Boolean({ description: "Mark created task planned" }),
|
|
@@ -121,61 +128,110 @@ export function registerTsqChangeTool(pi: ExtensionAPI): void {
|
|
|
121
128
|
pi.registerTool(
|
|
122
129
|
defineTool({
|
|
123
130
|
name: TSQ_CHANGE_TOOL_NAME,
|
|
124
|
-
label: "
|
|
131
|
+
label: "Task Change",
|
|
125
132
|
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
|
-
],
|
|
133
|
+
"Mutate durable tasks: lifecycle, notes, ownership, dependencies, and sequencing.",
|
|
134
|
+
promptSnippet: CHANGE_TASKS_PROMPT_SNIPPET,
|
|
135
|
+
promptGuidelines: CHANGE_TASKS_PROMPT_GUIDELINES,
|
|
135
136
|
parameters: TsqChangeParamsSchema,
|
|
136
137
|
executionMode: "sequential",
|
|
137
138
|
|
|
138
139
|
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
|
-
}
|
|
140
|
+
return executeTsqChange(pi, params as TsqChangeParams, signal, ctx);
|
|
171
141
|
},
|
|
172
142
|
}),
|
|
173
143
|
);
|
|
174
144
|
}
|
|
175
145
|
|
|
146
|
+
export async function executeTsqChange(
|
|
147
|
+
pi: ExtensionAPI,
|
|
148
|
+
params: TsqChangeParams,
|
|
149
|
+
signal: AbortSignal | undefined,
|
|
150
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
151
|
+
): Promise<AgentToolResult<TsqChangeDetails>> {
|
|
152
|
+
const command = buildMutationCommand(
|
|
153
|
+
params as Readonly<Record<string, unknown>>,
|
|
154
|
+
);
|
|
155
|
+
if (!command.ok) {
|
|
156
|
+
return validationErrorResult(command.message);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const result = await runMutation(pi, ctx, command.argv, signal);
|
|
161
|
+
return textToolResult(
|
|
162
|
+
formatSuccess(command.action, params, result),
|
|
163
|
+
okToolDetails({
|
|
164
|
+
action: command.action,
|
|
165
|
+
argv: command.argv,
|
|
166
|
+
result,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = getErrorMessage(error);
|
|
171
|
+
return textToolResult(
|
|
172
|
+
`Error: ${message}`,
|
|
173
|
+
errorToolDetails({
|
|
174
|
+
code: getErrorCode(error),
|
|
175
|
+
message,
|
|
176
|
+
details: {
|
|
177
|
+
action: command.action,
|
|
178
|
+
argv: command.argv,
|
|
179
|
+
error: serializeError(error),
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- mark_planned helper (tsq-5.2) ---
|
|
187
|
+
|
|
188
|
+
export interface TsqMarkPlannedSuccessData {
|
|
189
|
+
readonly argv: readonly string[];
|
|
190
|
+
readonly result: unknown;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type TsqMarkPlannedDetails = ReturnType<
|
|
194
|
+
typeof okToolDetails<TsqMarkPlannedSuccessData>
|
|
195
|
+
>;
|
|
196
|
+
|
|
197
|
+
export async function executeTsqMarkPlanned(
|
|
198
|
+
pi: ExtensionAPI,
|
|
199
|
+
taskId: string,
|
|
200
|
+
signal: AbortSignal | undefined,
|
|
201
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
202
|
+
): Promise<AgentToolResult<TsqMarkPlannedDetails>> {
|
|
203
|
+
const trimmed = taskId.trim();
|
|
204
|
+
if (trimmed.length === 0) {
|
|
205
|
+
return validationErrorResult("task id is required");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const argv = ["planned", trimmed];
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const result = await runMutation(pi, ctx, argv, signal);
|
|
212
|
+
return textToolResult(
|
|
213
|
+
`Marked ${trimmed} as planned`,
|
|
214
|
+
okToolDetails({ argv, result }),
|
|
215
|
+
);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message = getErrorMessage(error);
|
|
218
|
+
return textToolResult(
|
|
219
|
+
`Error: ${message}`,
|
|
220
|
+
errorToolDetails({
|
|
221
|
+
code: getErrorCode(error),
|
|
222
|
+
message,
|
|
223
|
+
details: {
|
|
224
|
+
argv,
|
|
225
|
+
error: serializeError(error),
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
176
232
|
function runMutation(
|
|
177
233
|
pi: ExtensionAPI,
|
|
178
|
-
ctx: ExtensionContext,
|
|
234
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
179
235
|
argv: readonly string[],
|
|
180
236
|
signal: AbortSignal | undefined,
|
|
181
237
|
): Promise<unknown> {
|
|
@@ -192,7 +248,7 @@ function buildMutationCommand(
|
|
|
192
248
|
if (!isTsqChangeAction(action)) {
|
|
193
249
|
return {
|
|
194
250
|
ok: false,
|
|
195
|
-
message: "action must be a supported
|
|
251
|
+
message: "action must be a supported durable task mutation",
|
|
196
252
|
};
|
|
197
253
|
}
|
|
198
254
|
|
|
@@ -5,6 +5,10 @@ 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,
|
|
@@ -24,7 +28,7 @@ export const TSQ_CLAIM_TOOL_NAME = "tsq_claim";
|
|
|
24
28
|
|
|
25
29
|
export const TsqClaimParamsSchema = Type.Object(
|
|
26
30
|
{
|
|
27
|
-
id: Type.String({ description: "Named
|
|
31
|
+
id: Type.String({ description: "Named durable task id to claim." }),
|
|
28
32
|
assignee: Type.Optional(
|
|
29
33
|
Type.String({
|
|
30
34
|
description: "Agent or role claiming the task. Defaults to pi.",
|
|
@@ -38,8 +42,7 @@ export const TsqClaimParamsSchema = Type.Object(
|
|
|
38
42
|
),
|
|
39
43
|
requireSpec: Type.Optional(
|
|
40
44
|
Type.Boolean({
|
|
41
|
-
description:
|
|
42
|
-
"Require an attached Tasque spec before the claim succeeds.",
|
|
45
|
+
description: "Require an attached task spec before the claim succeeds.",
|
|
43
46
|
}),
|
|
44
47
|
),
|
|
45
48
|
createTodo: Type.Optional(
|
|
@@ -95,16 +98,10 @@ export function registerTsqClaimTool(pi: ExtensionAPI): void {
|
|
|
95
98
|
pi.registerTool(
|
|
96
99
|
defineTool({
|
|
97
100
|
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
|
-
],
|
|
101
|
+
label: "Task Claim",
|
|
102
|
+
description: "Claim named durable task ownership.",
|
|
103
|
+
promptSnippet: CLAIM_TASK_PROMPT_SNIPPET,
|
|
104
|
+
promptGuidelines: CLAIM_TASK_PROMPT_GUIDELINES,
|
|
108
105
|
parameters: TsqClaimParamsSchema,
|
|
109
106
|
executionMode: "sequential",
|
|
110
107
|
|
|
@@ -5,6 +5,10 @@ import type {
|
|
|
5
5
|
ExtensionContext,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { type Static, Type } from "typebox";
|
|
8
|
+
import {
|
|
9
|
+
READ_TASKS_PROMPT_GUIDELINES,
|
|
10
|
+
READ_TASKS_PROMPT_SNIPPET,
|
|
11
|
+
} from "../guidelines/internal-tools.js";
|
|
8
12
|
import { truncatedTextToolResult } from "../shared/tool-result.js";
|
|
9
13
|
import type { TruncatedText } from "../shared/truncation.js";
|
|
10
14
|
import { runTsqJson } from "./runner.js";
|
|
@@ -40,12 +44,12 @@ export type TsqQueryAction = (typeof TSQ_QUERY_ACTIONS)[number];
|
|
|
40
44
|
export const TsqQueryParamsSchema = Type.Object(
|
|
41
45
|
{
|
|
42
46
|
action: StringEnum(TSQ_QUERY_ACTIONS, {
|
|
43
|
-
description: "Read-only
|
|
47
|
+
description: "Read-only durable task query to run.",
|
|
44
48
|
}),
|
|
45
49
|
id: Type.Optional(
|
|
46
50
|
Type.String({
|
|
47
51
|
description:
|
|
48
|
-
"
|
|
52
|
+
"Durable task id. Required for show, spec details, deps, and notes.",
|
|
49
53
|
}),
|
|
50
54
|
),
|
|
51
55
|
lane: Type.Optional(
|
|
@@ -98,15 +102,10 @@ const DEFAULT_QUERY_TIMEOUT_MS = 10_000;
|
|
|
98
102
|
export function registerTsqQueryTool(pi: ExtensionAPI): void {
|
|
99
103
|
pi.registerTool({
|
|
100
104
|
name: TSQ_QUERY_TOOL_NAME,
|
|
101
|
-
label: "
|
|
102
|
-
description:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"Use tsq_query for fresh read-only Tasque task state such as doctor, ready/open lists, show, deps, notes, tree, and similar lookups.",
|
|
106
|
-
promptGuidelines: [
|
|
107
|
-
"tsq_query is read-only; use mutation tools for lifecycle or notes.",
|
|
108
|
-
"Use show_with_spec only when spec content is needed; regular show is more concise.",
|
|
109
|
-
],
|
|
105
|
+
label: "Task Query",
|
|
106
|
+
description: "Read durable task state. Does not mutate tasks.",
|
|
107
|
+
promptSnippet: READ_TASKS_PROMPT_SNIPPET,
|
|
108
|
+
promptGuidelines: READ_TASKS_PROMPT_GUIDELINES,
|
|
110
109
|
parameters: TsqQueryParamsSchema,
|
|
111
110
|
executionMode: "parallel",
|
|
112
111
|
async execute(
|
|
@@ -132,10 +131,11 @@ export async function executeTsqQuery(
|
|
|
132
131
|
AgentToolResult<TsqQueryDetails & { readonly truncation: TruncatedText }>
|
|
133
132
|
> {
|
|
134
133
|
const argv = buildTsqQueryArgv(params);
|
|
135
|
-
const
|
|
134
|
+
const rawData = await runTsqJson<TsqQueryData>(pi, { cwd: ctx.cwd }, argv, {
|
|
136
135
|
timeout: DEFAULT_QUERY_TIMEOUT_MS,
|
|
137
136
|
...(signal === undefined ? {} : { signal }),
|
|
138
137
|
});
|
|
138
|
+
const data = normalizeQueryData(params, rawData);
|
|
139
139
|
const text = formatTsqQueryResult(params, data);
|
|
140
140
|
|
|
141
141
|
const details: TsqQueryDetails = {
|
|
@@ -197,6 +197,24 @@ export function buildTsqQueryArgv(params: TsqQueryParams): string[] {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
function normalizeQueryData(
|
|
201
|
+
params: TsqQueryParams,
|
|
202
|
+
data: TsqQueryData,
|
|
203
|
+
): TsqQueryData {
|
|
204
|
+
if (params.action !== "find_tree") {
|
|
205
|
+
return data;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const id = params.id?.trim();
|
|
209
|
+
if (id === undefined || id.length === 0) {
|
|
210
|
+
return data;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const found = findTaskTreeNode(getTreeRoots(data), id);
|
|
214
|
+
const filtered = found === undefined ? [] : [found];
|
|
215
|
+
return Array.isArray(data) ? filtered : { tree: filtered };
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
function formatTsqQueryResult(
|
|
201
219
|
params: TsqQueryParams,
|
|
202
220
|
data: TsqQueryData,
|
|
@@ -229,7 +247,7 @@ function formatDoctor(data: TsqQueryData): string {
|
|
|
229
247
|
? doctor.issues.length
|
|
230
248
|
: undefined;
|
|
231
249
|
const parts = [
|
|
232
|
-
"
|
|
250
|
+
"Task doctor",
|
|
233
251
|
formatMetric(doctor.tasks, "tasks"),
|
|
234
252
|
formatMetric(doctor.events, "events"),
|
|
235
253
|
issueCount === undefined ? undefined : `${issueCount} issues`,
|
|
@@ -242,7 +260,7 @@ function formatTaskList(
|
|
|
242
260
|
tasks: readonly TsqTask[],
|
|
243
261
|
): string {
|
|
244
262
|
const lines = [
|
|
245
|
-
`
|
|
263
|
+
`Task ${action}: ${formatCount(tasks.length, "task")}${formatLimitNotice(tasks.length)}`,
|
|
246
264
|
...tasks.slice(0, MAX_RENDERED_ITEMS).map(formatTaskLine),
|
|
247
265
|
];
|
|
248
266
|
return lines.join("\n");
|
|
@@ -268,21 +286,19 @@ function formatShow(data: TsqQueryData): string {
|
|
|
268
286
|
]
|
|
269
287
|
.filter((part): part is string => part !== undefined)
|
|
270
288
|
.join(" · ");
|
|
271
|
-
return [`
|
|
272
|
-
.filter(Boolean)
|
|
273
|
-
.join("\n");
|
|
289
|
+
return [`Task: ${parts}: ${task.title}`, extra].filter(Boolean).join("\n");
|
|
274
290
|
}
|
|
275
|
-
return "
|
|
291
|
+
return "Task: no task data returned";
|
|
276
292
|
}
|
|
277
293
|
|
|
278
294
|
function formatDeps(data: TsqQueryData): string {
|
|
279
295
|
const root = (data as Partial<{ root: TsqDepTreeNode }>).root;
|
|
280
296
|
if (root === undefined) {
|
|
281
|
-
return "
|
|
297
|
+
return "Task deps: no dependency data returned";
|
|
282
298
|
}
|
|
283
299
|
const lines = flattenDepTree(root).slice(0, MAX_RENDERED_ITEMS);
|
|
284
300
|
return [
|
|
285
|
-
`
|
|
301
|
+
`Task deps: ${root.id}${formatLimitNotice(countDepTree(root))}`,
|
|
286
302
|
...lines.map(
|
|
287
303
|
({ node, indent }) =>
|
|
288
304
|
`${" ".repeat(indent)}${formatTaskLine(node.task)}`,
|
|
@@ -295,7 +311,7 @@ function formatNotes(data: TsqQueryData): string {
|
|
|
295
311
|
const notes = notesData.notes ?? [];
|
|
296
312
|
const taskId = notesData.task_id ?? "task";
|
|
297
313
|
return [
|
|
298
|
-
`
|
|
314
|
+
`Task notes for ${taskId}: ${formatCount(notes.length, "note")}${formatLimitNotice(notes.length)}`,
|
|
299
315
|
...notes.slice(0, MAX_RENDERED_ITEMS).map((note) => {
|
|
300
316
|
const firstLine = note.text.split(/\r?\n/u)[0] ?? "";
|
|
301
317
|
return `${note.ts} ${note.actor}: ${truncateInline(firstLine, 120)}`;
|
|
@@ -307,7 +323,7 @@ function formatTree(data: TsqQueryData, label = "tree"): string {
|
|
|
307
323
|
const roots = getTreeRoots(data);
|
|
308
324
|
const flattened = roots.flatMap((root) => flattenTaskTree(root));
|
|
309
325
|
return [
|
|
310
|
-
`
|
|
326
|
+
`Task ${label}: ${formatCount(flattened.length, "task")}${formatLimitNotice(flattened.length)}`,
|
|
311
327
|
...flattened
|
|
312
328
|
.slice(0, MAX_RENDERED_ITEMS)
|
|
313
329
|
.map(
|
|
@@ -321,7 +337,7 @@ function formatSimilar(data: TsqQueryData): string {
|
|
|
321
337
|
const similar = data as Partial<TsqSimilarData>;
|
|
322
338
|
const candidates = similar.candidates ?? [];
|
|
323
339
|
return [
|
|
324
|
-
`
|
|
340
|
+
`Task similar: ${formatCount(candidates.length, "candidate")}${formatLimitNotice(candidates.length)}`,
|
|
325
341
|
...candidates.slice(0, MAX_RENDERED_ITEMS).map(formatCandidateLine),
|
|
326
342
|
].join("\n");
|
|
327
343
|
}
|
|
@@ -342,6 +358,22 @@ function getTreeRoots(data: TsqQueryData): readonly TsqTaskTreeNode[] {
|
|
|
342
358
|
return Array.isArray(tree) ? tree.filter(isTaskTreeNode) : [];
|
|
343
359
|
}
|
|
344
360
|
|
|
361
|
+
function findTaskTreeNode(
|
|
362
|
+
roots: readonly TsqTaskTreeNode[],
|
|
363
|
+
id: string,
|
|
364
|
+
): TsqTaskTreeNode | undefined {
|
|
365
|
+
for (const root of roots) {
|
|
366
|
+
if (root.task.id === id) {
|
|
367
|
+
return root;
|
|
368
|
+
}
|
|
369
|
+
const child = findTaskTreeNode(root.children, id);
|
|
370
|
+
if (child !== undefined) {
|
|
371
|
+
return child;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
345
377
|
function hasTreeData(data: TsqQueryData): boolean {
|
|
346
378
|
return Array.isArray(data)
|
|
347
379
|
? data.some(isTaskTreeNode)
|
|
@@ -423,7 +455,7 @@ function validateDepth(depth: unknown): number | undefined {
|
|
|
423
455
|
return undefined;
|
|
424
456
|
}
|
|
425
457
|
if (typeof depth !== "number" || !Number.isInteger(depth) || depth < 1) {
|
|
426
|
-
throw new Error("
|
|
458
|
+
throw new Error("task dependency depth must be an integer >= 1");
|
|
427
459
|
}
|
|
428
460
|
return depth;
|
|
429
461
|
}
|
|
@@ -435,7 +467,7 @@ function requireString(
|
|
|
435
467
|
): string {
|
|
436
468
|
const trimmed = value?.trim();
|
|
437
469
|
if (trimmed === undefined || trimmed.length === 0) {
|
|
438
|
-
throw new Error(`
|
|
470
|
+
throw new Error(`task action ${action} requires ${field}`);
|
|
439
471
|
}
|
|
440
472
|
return trimmed;
|
|
441
473
|
}
|