@bumpyclock/pi-tasque 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/README.md +315 -0
- package/package.json +39 -0
- package/src/bridge/bridge-tool.ts +185 -0
- package/src/bridge/import-tsq.ts +502 -0
- package/src/bridge/link-store.ts +97 -0
- package/src/bridge/promote-todo.ts +331 -0
- package/src/bridge/types.ts +156 -0
- package/src/durable-tasks/cache.ts +167 -0
- package/src/durable-tasks/mutation-queue.ts +30 -0
- package/src/durable-tasks/runner.ts +234 -0
- package/src/durable-tasks/status.ts +184 -0
- package/src/durable-tasks/tools-change.ts +600 -0
- package/src/durable-tasks/tools-claim.ts +426 -0
- package/src/durable-tasks/tools-query.ts +496 -0
- package/src/durable-tasks/types.ts +193 -0
- package/src/index.ts +21 -0
- package/src/session-todos/state/invariants.ts +17 -0
- package/src/session-todos/state/replay.ts +272 -0
- package/src/session-todos/state/selectors.ts +140 -0
- package/src/session-todos/state/state-reducer.ts +292 -0
- package/src/session-todos/state/state.ts +69 -0
- package/src/session-todos/state/store.ts +37 -0
- package/src/session-todos/state/task-graph.ts +58 -0
- package/src/session-todos/todo-overlay.ts +223 -0
- package/src/session-todos/todo.ts +239 -0
- package/src/session-todos/tool/response-envelope.ts +143 -0
- package/src/session-todos/tool/types.ts +149 -0
- package/src/session-todos/view/format.ts +264 -0
- package/src/shared/tool-result.ts +81 -0
- package/src/shared/truncation.ts +150 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolResult,
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { type Static, Type } from "typebox";
|
|
8
|
+
import { truncatedTextToolResult } from "../shared/tool-result.js";
|
|
9
|
+
import type { TruncatedText } from "../shared/truncation.js";
|
|
10
|
+
import { runTsqJson } from "./runner.js";
|
|
11
|
+
import type {
|
|
12
|
+
TsqDepTreeNode,
|
|
13
|
+
TsqDoctorData,
|
|
14
|
+
TsqNotesData,
|
|
15
|
+
TsqQueryData,
|
|
16
|
+
TsqShowData,
|
|
17
|
+
TsqSimilarData,
|
|
18
|
+
TsqTask,
|
|
19
|
+
TsqTaskListResult,
|
|
20
|
+
TsqTaskTreeNode,
|
|
21
|
+
TsqTreeResult,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
export const TSQ_QUERY_TOOL_NAME = "tsq_query";
|
|
25
|
+
|
|
26
|
+
export const TSQ_QUERY_ACTIONS = [
|
|
27
|
+
"doctor",
|
|
28
|
+
"find_ready",
|
|
29
|
+
"find_open",
|
|
30
|
+
"show",
|
|
31
|
+
"show_with_spec",
|
|
32
|
+
"deps",
|
|
33
|
+
"notes",
|
|
34
|
+
"find_tree",
|
|
35
|
+
"similar",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export type TsqQueryAction = (typeof TSQ_QUERY_ACTIONS)[number];
|
|
39
|
+
|
|
40
|
+
export const TsqQueryParamsSchema = Type.Object(
|
|
41
|
+
{
|
|
42
|
+
action: StringEnum(TSQ_QUERY_ACTIONS, {
|
|
43
|
+
description: "Read-only Tasque query to run.",
|
|
44
|
+
}),
|
|
45
|
+
id: Type.Optional(
|
|
46
|
+
Type.String({
|
|
47
|
+
description:
|
|
48
|
+
"Tasque task id. Required for show, show_with_spec, deps, and notes.",
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
lane: Type.Optional(
|
|
52
|
+
Type.String({
|
|
53
|
+
description: "Ready-task lane filter, e.g. planning or coding.",
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
assignee: Type.Optional(
|
|
57
|
+
Type.String({ description: "Assignee filter for find actions." }),
|
|
58
|
+
),
|
|
59
|
+
status: Type.Optional(
|
|
60
|
+
Type.String({
|
|
61
|
+
description:
|
|
62
|
+
"Reserved status filter; find_open already queries open tasks.",
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
tree: Type.Optional(
|
|
66
|
+
Type.Boolean({
|
|
67
|
+
description: "Request tree output when supported by the action.",
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
depth: Type.Optional(
|
|
71
|
+
Type.Integer({
|
|
72
|
+
description: "Dependency traversal depth for deps.",
|
|
73
|
+
minimum: 1,
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
query: Type.Optional(
|
|
77
|
+
Type.String({ description: "Search text for similar task lookup." }),
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
{ additionalProperties: false },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export type TsqQueryParams = Static<typeof TsqQueryParamsSchema>;
|
|
84
|
+
|
|
85
|
+
export interface TsqQueryDetails {
|
|
86
|
+
readonly [key: string]: unknown;
|
|
87
|
+
readonly ok: true;
|
|
88
|
+
readonly action: TsqQueryAction;
|
|
89
|
+
readonly argv: readonly string[];
|
|
90
|
+
readonly data: TsqQueryData;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const MAX_RENDERED_ITEMS = 12;
|
|
94
|
+
const MAX_CONTENT_LINES = 24;
|
|
95
|
+
const MAX_CONTENT_CHARS = 4_000;
|
|
96
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 10_000;
|
|
97
|
+
|
|
98
|
+
export function registerTsqQueryTool(pi: ExtensionAPI): void {
|
|
99
|
+
pi.registerTool({
|
|
100
|
+
name: TSQ_QUERY_TOOL_NAME,
|
|
101
|
+
label: "Tasque Query",
|
|
102
|
+
description:
|
|
103
|
+
"Run read-only Tasque durable-task queries through tsq --format json. Does not mutate Tasque state.",
|
|
104
|
+
promptSnippet:
|
|
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
|
+
],
|
|
110
|
+
parameters: TsqQueryParamsSchema,
|
|
111
|
+
executionMode: "parallel",
|
|
112
|
+
async execute(
|
|
113
|
+
_toolCallId,
|
|
114
|
+
params,
|
|
115
|
+
signal,
|
|
116
|
+
_onUpdate,
|
|
117
|
+
ctx,
|
|
118
|
+
): Promise<
|
|
119
|
+
AgentToolResult<TsqQueryDetails & { readonly truncation: TruncatedText }>
|
|
120
|
+
> {
|
|
121
|
+
return executeTsqQuery(pi, params, signal, ctx);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function executeTsqQuery(
|
|
127
|
+
pi: ExtensionAPI,
|
|
128
|
+
params: TsqQueryParams,
|
|
129
|
+
signal: AbortSignal | undefined,
|
|
130
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
131
|
+
): Promise<
|
|
132
|
+
AgentToolResult<TsqQueryDetails & { readonly truncation: TruncatedText }>
|
|
133
|
+
> {
|
|
134
|
+
const argv = buildTsqQueryArgv(params);
|
|
135
|
+
const data = await runTsqJson<TsqQueryData>(pi, { cwd: ctx.cwd }, argv, {
|
|
136
|
+
timeout: DEFAULT_QUERY_TIMEOUT_MS,
|
|
137
|
+
...(signal === undefined ? {} : { signal }),
|
|
138
|
+
});
|
|
139
|
+
const text = formatTsqQueryResult(params, data);
|
|
140
|
+
|
|
141
|
+
const details: TsqQueryDetails = {
|
|
142
|
+
ok: true,
|
|
143
|
+
action: params.action,
|
|
144
|
+
argv,
|
|
145
|
+
data,
|
|
146
|
+
};
|
|
147
|
+
return truncatedTextToolResult(text, details, {
|
|
148
|
+
maxLines: MAX_CONTENT_LINES,
|
|
149
|
+
maxChars: MAX_CONTENT_CHARS,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildTsqQueryArgv(params: TsqQueryParams): string[] {
|
|
154
|
+
const depth = validateDepth(params.depth);
|
|
155
|
+
switch (params.action) {
|
|
156
|
+
case "doctor":
|
|
157
|
+
return ["doctor"];
|
|
158
|
+
case "find_ready":
|
|
159
|
+
return [
|
|
160
|
+
"find",
|
|
161
|
+
"ready",
|
|
162
|
+
...optionalPair("--lane", params.lane),
|
|
163
|
+
...optionalPair("--assignee", params.assignee),
|
|
164
|
+
...(params.tree === true ? ["--tree"] : []),
|
|
165
|
+
];
|
|
166
|
+
case "find_open":
|
|
167
|
+
return [
|
|
168
|
+
"find",
|
|
169
|
+
"open",
|
|
170
|
+
...optionalPair("--assignee", params.assignee),
|
|
171
|
+
...(params.tree === true ? ["--tree"] : []),
|
|
172
|
+
];
|
|
173
|
+
case "show":
|
|
174
|
+
return ["show", requireString(params.id, "id", params.action)];
|
|
175
|
+
case "show_with_spec":
|
|
176
|
+
return [
|
|
177
|
+
"show",
|
|
178
|
+
requireString(params.id, "id", params.action),
|
|
179
|
+
"--with-spec",
|
|
180
|
+
];
|
|
181
|
+
case "deps":
|
|
182
|
+
return [
|
|
183
|
+
"deps",
|
|
184
|
+
requireString(params.id, "id", params.action),
|
|
185
|
+
...optionalDepth(depth),
|
|
186
|
+
];
|
|
187
|
+
case "notes":
|
|
188
|
+
return ["notes", requireString(params.id, "id", params.action)];
|
|
189
|
+
case "find_tree":
|
|
190
|
+
return ["find", "open", "--tree"];
|
|
191
|
+
case "similar":
|
|
192
|
+
return [
|
|
193
|
+
"find",
|
|
194
|
+
"similar",
|
|
195
|
+
requireString(params.query, "query", params.action),
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatTsqQueryResult(
|
|
201
|
+
params: TsqQueryParams,
|
|
202
|
+
data: TsqQueryData,
|
|
203
|
+
): string {
|
|
204
|
+
switch (params.action) {
|
|
205
|
+
case "doctor":
|
|
206
|
+
return formatDoctor(data);
|
|
207
|
+
case "find_ready":
|
|
208
|
+
case "find_open":
|
|
209
|
+
return params.tree === true || hasTreeData(data)
|
|
210
|
+
? formatTree(data, `${params.action} tree`)
|
|
211
|
+
: formatTaskList(params.action, getTaskList(data));
|
|
212
|
+
case "show":
|
|
213
|
+
case "show_with_spec":
|
|
214
|
+
return formatShow(data);
|
|
215
|
+
case "deps":
|
|
216
|
+
return formatDeps(data);
|
|
217
|
+
case "notes":
|
|
218
|
+
return formatNotes(data);
|
|
219
|
+
case "find_tree":
|
|
220
|
+
return formatTree(data);
|
|
221
|
+
case "similar":
|
|
222
|
+
return formatSimilar(data);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatDoctor(data: TsqQueryData): string {
|
|
227
|
+
const doctor = data as TsqDoctorData;
|
|
228
|
+
const issueCount = Array.isArray(doctor.issues)
|
|
229
|
+
? doctor.issues.length
|
|
230
|
+
: undefined;
|
|
231
|
+
const parts = [
|
|
232
|
+
"Tasque doctor",
|
|
233
|
+
formatMetric(doctor.tasks, "tasks"),
|
|
234
|
+
formatMetric(doctor.events, "events"),
|
|
235
|
+
issueCount === undefined ? undefined : `${issueCount} issues`,
|
|
236
|
+
].filter((part): part is string => part !== undefined);
|
|
237
|
+
return parts.join(" · ");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatTaskList(
|
|
241
|
+
action: TsqQueryAction,
|
|
242
|
+
tasks: readonly TsqTask[],
|
|
243
|
+
): string {
|
|
244
|
+
const lines = [
|
|
245
|
+
`Tasque ${action}: ${formatCount(tasks.length, "task")}${formatLimitNotice(tasks.length)}`,
|
|
246
|
+
...tasks.slice(0, MAX_RENDERED_ITEMS).map(formatTaskLine),
|
|
247
|
+
];
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatShow(data: TsqQueryData): string {
|
|
252
|
+
const show = data as Partial<TsqShowData>;
|
|
253
|
+
const task = show.task;
|
|
254
|
+
if (isTsqTask(task)) {
|
|
255
|
+
const parts = [
|
|
256
|
+
task.id,
|
|
257
|
+
task.status,
|
|
258
|
+
task.planning_state,
|
|
259
|
+
formatAssignee(task.assignee),
|
|
260
|
+
`p${task.priority}`,
|
|
261
|
+
]
|
|
262
|
+
.filter((part): part is string => part !== undefined && part.length > 0)
|
|
263
|
+
.join(" ");
|
|
264
|
+
const extra = [
|
|
265
|
+
formatCount(show.blockers?.length ?? 0, "blocker"),
|
|
266
|
+
formatCount(show.dependents?.length ?? 0, "dependent"),
|
|
267
|
+
show.spec?.path === undefined ? undefined : `spec ${show.spec.path}`,
|
|
268
|
+
]
|
|
269
|
+
.filter((part): part is string => part !== undefined)
|
|
270
|
+
.join(" · ");
|
|
271
|
+
return [`Tasque task: ${parts}: ${task.title}`, extra]
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.join("\n");
|
|
274
|
+
}
|
|
275
|
+
return "Tasque task: no task data returned";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatDeps(data: TsqQueryData): string {
|
|
279
|
+
const root = (data as Partial<{ root: TsqDepTreeNode }>).root;
|
|
280
|
+
if (root === undefined) {
|
|
281
|
+
return "Tasque deps: no dependency data returned";
|
|
282
|
+
}
|
|
283
|
+
const lines = flattenDepTree(root).slice(0, MAX_RENDERED_ITEMS);
|
|
284
|
+
return [
|
|
285
|
+
`Tasque deps: ${root.id}${formatLimitNotice(countDepTree(root))}`,
|
|
286
|
+
...lines.map(
|
|
287
|
+
({ node, indent }) =>
|
|
288
|
+
`${" ".repeat(indent)}${formatTaskLine(node.task)}`,
|
|
289
|
+
),
|
|
290
|
+
].join("\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatNotes(data: TsqQueryData): string {
|
|
294
|
+
const notesData = data as Partial<TsqNotesData>;
|
|
295
|
+
const notes = notesData.notes ?? [];
|
|
296
|
+
const taskId = notesData.task_id ?? "task";
|
|
297
|
+
return [
|
|
298
|
+
`Tasque notes for ${taskId}: ${formatCount(notes.length, "note")}${formatLimitNotice(notes.length)}`,
|
|
299
|
+
...notes.slice(0, MAX_RENDERED_ITEMS).map((note) => {
|
|
300
|
+
const firstLine = note.text.split(/\r?\n/u)[0] ?? "";
|
|
301
|
+
return `${note.ts} ${note.actor}: ${truncateInline(firstLine, 120)}`;
|
|
302
|
+
}),
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatTree(data: TsqQueryData, label = "tree"): string {
|
|
307
|
+
const roots = getTreeRoots(data);
|
|
308
|
+
const flattened = roots.flatMap((root) => flattenTaskTree(root));
|
|
309
|
+
return [
|
|
310
|
+
`Tasque ${label}: ${formatCount(flattened.length, "task")}${formatLimitNotice(flattened.length)}`,
|
|
311
|
+
...flattened
|
|
312
|
+
.slice(0, MAX_RENDERED_ITEMS)
|
|
313
|
+
.map(
|
|
314
|
+
({ node, indent }) =>
|
|
315
|
+
`${" ".repeat(indent)}${formatTaskLine(node.task)}`,
|
|
316
|
+
),
|
|
317
|
+
].join("\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatSimilar(data: TsqQueryData): string {
|
|
321
|
+
const similar = data as Partial<TsqSimilarData>;
|
|
322
|
+
const candidates = similar.candidates ?? [];
|
|
323
|
+
return [
|
|
324
|
+
`Tasque similar: ${formatCount(candidates.length, "candidate")}${formatLimitNotice(candidates.length)}`,
|
|
325
|
+
...candidates.slice(0, MAX_RENDERED_ITEMS).map(formatCandidateLine),
|
|
326
|
+
].join("\n");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getTaskList(data: TsqQueryData): readonly TsqTask[] {
|
|
330
|
+
if (Array.isArray(data)) {
|
|
331
|
+
return data.filter(isTsqTask);
|
|
332
|
+
}
|
|
333
|
+
const tasks = (data as Partial<TsqTaskListResult>).tasks;
|
|
334
|
+
return Array.isArray(tasks) ? tasks.filter(isTsqTask) : [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getTreeRoots(data: TsqQueryData): readonly TsqTaskTreeNode[] {
|
|
338
|
+
if (Array.isArray(data)) {
|
|
339
|
+
return data.filter(isTaskTreeNode);
|
|
340
|
+
}
|
|
341
|
+
const tree = (data as Partial<TsqTreeResult>).tree;
|
|
342
|
+
return Array.isArray(tree) ? tree.filter(isTaskTreeNode) : [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function hasTreeData(data: TsqQueryData): boolean {
|
|
346
|
+
return Array.isArray(data)
|
|
347
|
+
? data.some(isTaskTreeNode)
|
|
348
|
+
: isRecord(data) && Array.isArray(data.tree);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function formatTaskLine(task: TsqTask): string {
|
|
352
|
+
const assignee = formatAssignee(task.assignee);
|
|
353
|
+
return [
|
|
354
|
+
task.id,
|
|
355
|
+
task.status,
|
|
356
|
+
`p${task.priority}`,
|
|
357
|
+
assignee,
|
|
358
|
+
truncateInline(task.title, 100),
|
|
359
|
+
]
|
|
360
|
+
.filter((part): part is string => part !== undefined && part.length > 0)
|
|
361
|
+
.join(" ");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function formatCandidateLine(candidate: unknown): string {
|
|
365
|
+
if (isTsqTask(candidate)) {
|
|
366
|
+
return formatTaskLine(candidate);
|
|
367
|
+
}
|
|
368
|
+
if (isRecord(candidate)) {
|
|
369
|
+
const taskValue = candidate.task;
|
|
370
|
+
if (isTsqTask(taskValue)) {
|
|
371
|
+
const score = candidate.score;
|
|
372
|
+
return `${formatTaskLine(taskValue)}${typeof score === "number" ? ` score ${score}` : ""}`;
|
|
373
|
+
}
|
|
374
|
+
const id = typeof candidate.id === "string" ? candidate.id : undefined;
|
|
375
|
+
const title =
|
|
376
|
+
typeof candidate.title === "string" ? candidate.title : undefined;
|
|
377
|
+
if (id !== undefined || title !== undefined) {
|
|
378
|
+
return [id, title]
|
|
379
|
+
.filter((part): part is string => part !== undefined)
|
|
380
|
+
.join(" ");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return truncateInline(JSON.stringify(candidate), 140);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function flattenTaskTree(
|
|
387
|
+
node: TsqTaskTreeNode,
|
|
388
|
+
indent = 0,
|
|
389
|
+
): readonly { readonly node: TsqTaskTreeNode; readonly indent: number }[] {
|
|
390
|
+
return [
|
|
391
|
+
{ node, indent },
|
|
392
|
+
...node.children.flatMap((child) => flattenTaskTree(child, indent + 1)),
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function flattenDepTree(
|
|
397
|
+
node: TsqDepTreeNode,
|
|
398
|
+
indent = 0,
|
|
399
|
+
): readonly { readonly node: TsqDepTreeNode; readonly indent: number }[] {
|
|
400
|
+
return [
|
|
401
|
+
{ node, indent },
|
|
402
|
+
...node.children.flatMap((child) => flattenDepTree(child, indent + 1)),
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function countDepTree(node: TsqDepTreeNode): number {
|
|
407
|
+
return (
|
|
408
|
+
1 + node.children.reduce((count, child) => count + countDepTree(child), 0)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function optionalPair(flag: string, value: string | undefined): string[] {
|
|
413
|
+
const trimmed = value?.trim();
|
|
414
|
+
return trimmed === undefined || trimmed.length === 0 ? [] : [flag, trimmed];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function optionalDepth(depth: number | undefined): string[] {
|
|
418
|
+
return depth === undefined ? [] : ["--depth", String(depth)];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function validateDepth(depth: unknown): number | undefined {
|
|
422
|
+
if (depth === undefined) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
if (typeof depth !== "number" || !Number.isInteger(depth) || depth < 1) {
|
|
426
|
+
throw new Error("tsq_query depth must be an integer >= 1");
|
|
427
|
+
}
|
|
428
|
+
return depth;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function requireString(
|
|
432
|
+
value: string | undefined,
|
|
433
|
+
field: "id" | "query",
|
|
434
|
+
action: TsqQueryAction,
|
|
435
|
+
): string {
|
|
436
|
+
const trimmed = value?.trim();
|
|
437
|
+
if (trimmed === undefined || trimmed.length === 0) {
|
|
438
|
+
throw new Error(`tsq_query action ${action} requires ${field}`);
|
|
439
|
+
}
|
|
440
|
+
return trimmed;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function formatCount(count: number, noun: string): string {
|
|
444
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function formatMetric(
|
|
448
|
+
value: number | undefined,
|
|
449
|
+
noun: string,
|
|
450
|
+
): string | undefined {
|
|
451
|
+
return typeof value === "number"
|
|
452
|
+
? formatCount(value, noun.slice(0, -1))
|
|
453
|
+
: undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function formatLimitNotice(total: number): string {
|
|
457
|
+
return total > MAX_RENDERED_ITEMS
|
|
458
|
+
? ` (showing first ${MAX_RENDERED_ITEMS})`
|
|
459
|
+
: "";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function formatAssignee(
|
|
463
|
+
assignee: string | null | undefined,
|
|
464
|
+
): string | undefined {
|
|
465
|
+
return assignee === undefined || assignee === null || assignee.length === 0
|
|
466
|
+
? undefined
|
|
467
|
+
: `@${assignee}`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function truncateInline(text: string | undefined, maxChars: number): string {
|
|
471
|
+
if (text === undefined) {
|
|
472
|
+
return "";
|
|
473
|
+
}
|
|
474
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars - 1)}…`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isTsqTask(value: unknown): value is TsqTask {
|
|
478
|
+
return (
|
|
479
|
+
isRecord(value) &&
|
|
480
|
+
typeof value.id === "string" &&
|
|
481
|
+
typeof value.title === "string" &&
|
|
482
|
+
typeof value.status === "string" &&
|
|
483
|
+
typeof value.planning_state === "string" &&
|
|
484
|
+
typeof value.priority === "number"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function isTaskTreeNode(value: unknown): value is TsqTaskTreeNode {
|
|
489
|
+
return (
|
|
490
|
+
isRecord(value) && isTsqTask(value.task) && Array.isArray(value.children)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
495
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
496
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export const TSQ_SCHEMA_VERSION = 1 as const;
|
|
2
|
+
|
|
3
|
+
export type JsonValue =
|
|
4
|
+
| string
|
|
5
|
+
| number
|
|
6
|
+
| boolean
|
|
7
|
+
| null
|
|
8
|
+
| readonly JsonValue[]
|
|
9
|
+
| { readonly [key: string]: JsonValue };
|
|
10
|
+
|
|
11
|
+
export interface TsqOk<TData = unknown> {
|
|
12
|
+
readonly schema_version: typeof TSQ_SCHEMA_VERSION;
|
|
13
|
+
readonly command: string;
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly data: TData;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TsqErr<TDetails = JsonValue> {
|
|
19
|
+
readonly schema_version: typeof TSQ_SCHEMA_VERSION;
|
|
20
|
+
readonly command: string;
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly error: TsqEnvelopeError<TDetails>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TsqEnvelopeError<TDetails = JsonValue> {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly message: string;
|
|
28
|
+
readonly details?: TDetails;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type TsqEnvelope<TData = unknown, TErrorDetails = JsonValue> =
|
|
32
|
+
| TsqOk<TData>
|
|
33
|
+
| TsqErr<TErrorDetails>;
|
|
34
|
+
|
|
35
|
+
export type TsqTaskKind = "task" | "feature" | "epic" | (string & {});
|
|
36
|
+
export type TsqTaskStatus =
|
|
37
|
+
| "open"
|
|
38
|
+
| "in_progress"
|
|
39
|
+
| "blocked"
|
|
40
|
+
| "closed"
|
|
41
|
+
| "canceled"
|
|
42
|
+
| "deferred"
|
|
43
|
+
| (string & {});
|
|
44
|
+
export type TsqPlanningState = "needs_planning" | "planned" | (string & {});
|
|
45
|
+
export type TsqDependencyType = "blocks" | "starts_after" | (string & {});
|
|
46
|
+
export type TsqDependencyDirection = "up" | "down" | "both" | (string & {});
|
|
47
|
+
|
|
48
|
+
export interface TsqTaskNote {
|
|
49
|
+
readonly event_id: string;
|
|
50
|
+
readonly ts: string;
|
|
51
|
+
readonly actor: string;
|
|
52
|
+
readonly text: string;
|
|
53
|
+
readonly [key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TsqTask {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly alias?: string;
|
|
59
|
+
readonly title: string;
|
|
60
|
+
readonly description?: string | null;
|
|
61
|
+
readonly kind: TsqTaskKind;
|
|
62
|
+
readonly status: TsqTaskStatus;
|
|
63
|
+
readonly planning_state: TsqPlanningState;
|
|
64
|
+
readonly priority: number;
|
|
65
|
+
readonly parent_id?: string | null;
|
|
66
|
+
readonly assignee?: string | null;
|
|
67
|
+
readonly labels: readonly string[];
|
|
68
|
+
readonly notes: readonly TsqTaskNote[];
|
|
69
|
+
readonly created_at: string;
|
|
70
|
+
readonly updated_at: string;
|
|
71
|
+
readonly closed_at?: string | null;
|
|
72
|
+
readonly spec_path?: string | null;
|
|
73
|
+
readonly spec_fingerprint?: string | null;
|
|
74
|
+
readonly spec_attached_at?: string | null;
|
|
75
|
+
readonly spec_attached_by?: string | null;
|
|
76
|
+
readonly external_ref?: string | null;
|
|
77
|
+
readonly discovered_from?: string | null;
|
|
78
|
+
readonly superseded_by?: string | null;
|
|
79
|
+
readonly duplicate_of?: string | null;
|
|
80
|
+
readonly replies_to?: string | null;
|
|
81
|
+
readonly [key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TsqDependencyRef {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
readonly dep_type: TsqDependencyType;
|
|
87
|
+
readonly [key: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface TsqDependencyEdge {
|
|
91
|
+
readonly blocker: string;
|
|
92
|
+
readonly dep_type: TsqDependencyType;
|
|
93
|
+
readonly [key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TsqTaskTreeNode {
|
|
97
|
+
readonly task: TsqTask;
|
|
98
|
+
readonly children: readonly TsqTaskTreeNode[];
|
|
99
|
+
readonly blocker_edges: readonly TsqDependencyRef[];
|
|
100
|
+
readonly dependent_edges: readonly TsqDependencyRef[];
|
|
101
|
+
readonly blockers: readonly string[];
|
|
102
|
+
readonly dependents: readonly string[];
|
|
103
|
+
readonly [key: string]: unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface TsqDepTreeNode {
|
|
107
|
+
readonly id: string;
|
|
108
|
+
readonly task: TsqTask;
|
|
109
|
+
readonly direction: TsqDependencyDirection;
|
|
110
|
+
readonly depth: number;
|
|
111
|
+
readonly dep_type?: TsqDependencyType | null;
|
|
112
|
+
readonly children: readonly TsqDepTreeNode[];
|
|
113
|
+
readonly [key: string]: unknown;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface TsqDepsData {
|
|
117
|
+
readonly root: TsqDepTreeNode;
|
|
118
|
+
readonly [key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TsqSpecContent {
|
|
122
|
+
readonly path: string;
|
|
123
|
+
readonly fingerprint: string;
|
|
124
|
+
readonly content: string;
|
|
125
|
+
readonly [key: string]: unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface TsqEventRecord {
|
|
129
|
+
readonly id?: string;
|
|
130
|
+
readonly event_id?: string;
|
|
131
|
+
readonly ts: string;
|
|
132
|
+
readonly actor: string;
|
|
133
|
+
readonly type: string;
|
|
134
|
+
readonly task_id: string;
|
|
135
|
+
readonly payload: Record<string, unknown>;
|
|
136
|
+
readonly [key: string]: unknown;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface TsqShowData {
|
|
140
|
+
readonly task: TsqTask;
|
|
141
|
+
readonly blockers: readonly string[];
|
|
142
|
+
readonly dependents: readonly string[];
|
|
143
|
+
readonly blocker_edges: readonly TsqDependencyRef[];
|
|
144
|
+
readonly dependent_edges: readonly TsqDependencyRef[];
|
|
145
|
+
readonly ready?: boolean;
|
|
146
|
+
readonly links?: Readonly<Record<string, readonly string[]>>;
|
|
147
|
+
readonly history?: readonly TsqEventRecord[];
|
|
148
|
+
readonly spec?: TsqSpecContent;
|
|
149
|
+
readonly [key: string]: unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface TsqDoctorData {
|
|
153
|
+
readonly tasks?: number;
|
|
154
|
+
readonly events?: number;
|
|
155
|
+
readonly snapshot_loaded?: boolean;
|
|
156
|
+
readonly issues?: readonly unknown[];
|
|
157
|
+
readonly [key: string]: unknown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface TsqTaskListResult {
|
|
161
|
+
readonly tasks: readonly TsqTask[];
|
|
162
|
+
readonly [key: string]: unknown;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface TsqTreeResult {
|
|
166
|
+
readonly tree: readonly TsqTaskTreeNode[];
|
|
167
|
+
readonly [key: string]: unknown;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface TsqSimilarData {
|
|
171
|
+
readonly candidates: readonly unknown[];
|
|
172
|
+
readonly [key: string]: unknown;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface TsqNotesData {
|
|
176
|
+
readonly task_id: string;
|
|
177
|
+
readonly notes: readonly TsqTaskNote[];
|
|
178
|
+
readonly [key: string]: unknown;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type TsqTaskListData = readonly TsqTask[];
|
|
182
|
+
export type TsqTreeData = readonly TsqTaskTreeNode[];
|
|
183
|
+
|
|
184
|
+
export type TsqQueryData =
|
|
185
|
+
| TsqDoctorData
|
|
186
|
+
| TsqTaskListResult
|
|
187
|
+
| TsqTaskListData
|
|
188
|
+
| TsqShowData
|
|
189
|
+
| TsqDepsData
|
|
190
|
+
| TsqNotesData
|
|
191
|
+
| TsqTreeResult
|
|
192
|
+
| TsqTreeData
|
|
193
|
+
| TsqSimilarData;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerTaskBridgeTool } from "./bridge/bridge-tool.js";
|
|
3
|
+
import { importTsqHandler } from "./bridge/import-tsq.js";
|
|
4
|
+
import { promoteTodoHandler } from "./bridge/promote-todo.js";
|
|
5
|
+
import { registerTasqueStatusLifecycle } from "./durable-tasks/status.js";
|
|
6
|
+
import { registerTsqChangeTool } from "./durable-tasks/tools-change.js";
|
|
7
|
+
import { registerTsqClaimTool } from "./durable-tasks/tools-claim.js";
|
|
8
|
+
import { registerTsqQueryTool } from "./durable-tasks/tools-query.js";
|
|
9
|
+
import { registerSessionTodoModule } from "./session-todos/todo.js";
|
|
10
|
+
|
|
11
|
+
export default function piTasqueExtension(pi: ExtensionAPI): void {
|
|
12
|
+
registerSessionTodoModule(pi);
|
|
13
|
+
registerTsqQueryTool(pi);
|
|
14
|
+
registerTsqChangeTool(pi);
|
|
15
|
+
registerTsqClaimTool(pi);
|
|
16
|
+
registerTaskBridgeTool(pi, {
|
|
17
|
+
promote_todo: promoteTodoHandler,
|
|
18
|
+
import_tsq: importTsqHandler,
|
|
19
|
+
});
|
|
20
|
+
registerTasqueStatusLifecycle(pi);
|
|
21
|
+
}
|