@bumpyclock/pi-tasque 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -140
- package/package.json +24 -6
- package/src/bridge/bridge-tool.ts +10 -13
- package/src/bridge/import-tsq.ts +5 -30
- package/src/bridge/promote-todo.ts +1 -4
- package/src/bridge/types.ts +11 -12
- package/src/durable-tasks/bulk-contract.ts +176 -0
- package/src/durable-tasks/cache.ts +1 -4
- package/src/durable-tasks/change-command-builder.ts +315 -0
- package/src/durable-tasks/handoff-guard.ts +329 -0
- package/src/durable-tasks/project.ts +71 -0
- package/src/durable-tasks/runner.ts +1 -4
- package/src/durable-tasks/status.ts +20 -5
- package/src/durable-tasks/task-mappers.ts +160 -0
- package/src/durable-tasks/task-schema.ts +193 -0
- package/src/durable-tasks/task-tool.ts +197 -0
- package/src/durable-tasks/task-validation.ts +123 -0
- package/src/durable-tasks/tools-bulk.ts +141 -0
- package/src/durable-tasks/tools-change.ts +111 -382
- package/src/durable-tasks/tools-claim.ts +15 -43
- package/src/durable-tasks/tools-handoff.ts +95 -0
- package/src/durable-tasks/tools-query.ts +58 -29
- package/src/durable-tasks/tools-spec.ts +230 -0
- package/src/durable-tasks/tools-tree-create.ts +221 -0
- package/src/guidelines/internal-tools.ts +33 -0
- package/src/guidelines/task.ts +5 -0
- package/src/guidelines/todo.ts +5 -0
- package/src/index.ts +2 -13
- package/src/session-todos/state/replay.ts +10 -13
- package/src/session-todos/todo-overlay.ts +1 -1
- package/src/session-todos/todo.ts +11 -15
- package/src/session-todos/tool/types.ts +7 -7
- package/src/shared/error-utils.ts +29 -0
- package/src/shared/validation.ts +25 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only handoff readiness checker.
|
|
3
|
+
*
|
|
4
|
+
* Collects session todo state and linked durable task statuses to produce a
|
|
5
|
+
* structured ready/not-ready report. Never mutates session todos or durable
|
|
6
|
+
* task state. Uses only read-only `show` CLI calls for linked tasks.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { deriveTaskLinks } from "../bridge/link-store.js";
|
|
13
|
+
import { selectVisibleTasks } from "../session-todos/state/selectors.js";
|
|
14
|
+
import { getState } from "../session-todos/state/store.js";
|
|
15
|
+
import type { Task } from "../session-todos/tool/types.js";
|
|
16
|
+
import { resolveProjectRoot } from "./project.js";
|
|
17
|
+
import { TsqCommandError, TsqProcessError, runTsqJson } from "./runner.js";
|
|
18
|
+
import type { TsqShowData, TsqTaskStatus } from "./types.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Status classification
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Durable task statuses that count as "ready" (work complete). */
|
|
25
|
+
const READY_STATUSES: ReadonlySet<TsqTaskStatus> = new Set(["closed"]);
|
|
26
|
+
|
|
27
|
+
/** Durable task statuses that count as "warning" (not blocking). */
|
|
28
|
+
const WARNING_STATUSES: ReadonlySet<TsqTaskStatus> = new Set(["canceled"]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Classify a durable task status for handoff readiness.
|
|
32
|
+
*
|
|
33
|
+
* - `"ready"` — closed, work done.
|
|
34
|
+
* - `"blocker"` — open/in_progress/blocked/deferred/unknown/missing.
|
|
35
|
+
* - `"warning"` — canceled (notable but not blocking).
|
|
36
|
+
*/
|
|
37
|
+
export function classifyDurableStatus(
|
|
38
|
+
status: string | undefined | null,
|
|
39
|
+
): "ready" | "blocker" | "warning" {
|
|
40
|
+
if (status == null || status.trim().length === 0) return "blocker";
|
|
41
|
+
if (READY_STATUSES.has(status)) return "ready";
|
|
42
|
+
if (WARNING_STATUSES.has(status)) return "warning";
|
|
43
|
+
return "blocker";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Read-error classification
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error codes from linked `tsq show` that are *actionable* (user can fix them)
|
|
52
|
+
* rather than infrastructure failures. These become `ok:true, ready:false` with
|
|
53
|
+
* structured `readErrors`, not `ok:false`.
|
|
54
|
+
*/
|
|
55
|
+
const ACTIONABLE_ERROR_CODES: ReadonlySet<string> = new Set([
|
|
56
|
+
"not_found",
|
|
57
|
+
"task_not_found",
|
|
58
|
+
"validation_error",
|
|
59
|
+
"read_error",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Classify a tsq CLI error code from a linked `show` call.
|
|
64
|
+
*
|
|
65
|
+
* - `"actionable"` — not-found, validation, read-envelope → ok:true, ready:false
|
|
66
|
+
* - `"internal"` — process/timeout/abort/invalid-JSON → ok:false
|
|
67
|
+
*/
|
|
68
|
+
export function classifyReadError(code: string): "actionable" | "internal" {
|
|
69
|
+
return ACTIONABLE_ERROR_CODES.has(code.toLowerCase())
|
|
70
|
+
? "actionable"
|
|
71
|
+
: "internal";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Result types
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export interface HandoffTodoBlocker {
|
|
79
|
+
readonly todoId: number;
|
|
80
|
+
readonly subject: string;
|
|
81
|
+
readonly status: string;
|
|
82
|
+
readonly reason: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface HandoffLinkedBlocker {
|
|
86
|
+
readonly todoId: number;
|
|
87
|
+
readonly tsqId: string;
|
|
88
|
+
readonly status: string;
|
|
89
|
+
readonly classification: "blocker" | "warning";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface HandoffReadError {
|
|
93
|
+
readonly tsqId: string;
|
|
94
|
+
readonly code: string;
|
|
95
|
+
readonly message: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface HandoffReadyResult {
|
|
99
|
+
readonly ok: true;
|
|
100
|
+
readonly ready: true;
|
|
101
|
+
readonly projectRoot?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface HandoffNotReadyResult {
|
|
105
|
+
readonly ok: true;
|
|
106
|
+
readonly ready: false;
|
|
107
|
+
readonly projectRoot?: string;
|
|
108
|
+
readonly todoBlockers?: readonly HandoffTodoBlocker[];
|
|
109
|
+
readonly linkedBlockers?: readonly HandoffLinkedBlocker[];
|
|
110
|
+
readonly linkedWarnings?: readonly HandoffLinkedBlocker[];
|
|
111
|
+
readonly readErrors?: readonly HandoffReadError[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface HandoffInternalError {
|
|
115
|
+
readonly ok: false;
|
|
116
|
+
readonly code: string;
|
|
117
|
+
readonly message: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type HandoffCheckResult =
|
|
121
|
+
| HandoffReadyResult
|
|
122
|
+
| HandoffNotReadyResult
|
|
123
|
+
| HandoffInternalError;
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Collector options
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export interface CollectHandoffOptions {
|
|
130
|
+
readonly pi: ExtensionAPI;
|
|
131
|
+
readonly cwd: string;
|
|
132
|
+
readonly signal?: AbortSignal;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Collector implementation
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Collect handoff readiness from session todo state and linked durable tasks.
|
|
141
|
+
*
|
|
142
|
+
* Read-only: never mutates session todos or durable task state.
|
|
143
|
+
* Only uses `tsq show <id>` (read-only) for linked tasks.
|
|
144
|
+
*/
|
|
145
|
+
export async function collectHandoffStatus(
|
|
146
|
+
options: CollectHandoffOptions,
|
|
147
|
+
): Promise<HandoffCheckResult> {
|
|
148
|
+
const { pi, cwd, signal } = options;
|
|
149
|
+
|
|
150
|
+
// 1. Read session todo state (snapshot, no mutation)
|
|
151
|
+
const state = getState();
|
|
152
|
+
const visibleTodos = selectVisibleTasks(state);
|
|
153
|
+
|
|
154
|
+
// 2. Collect todo blockers
|
|
155
|
+
const todoBlockers = collectTodoBlockers(visibleTodos);
|
|
156
|
+
|
|
157
|
+
// 3. Derive todo↔task links
|
|
158
|
+
const links = deriveTaskLinks(state);
|
|
159
|
+
|
|
160
|
+
// 4. If links exist, resolve project root and read linked task statuses
|
|
161
|
+
if (links.length > 0) {
|
|
162
|
+
let projectRoot: string;
|
|
163
|
+
try {
|
|
164
|
+
projectRoot = await resolveProjectRoot(
|
|
165
|
+
pi,
|
|
166
|
+
cwd,
|
|
167
|
+
signal != null ? { signal } : {},
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
code: "project_resolution_error",
|
|
173
|
+
message:
|
|
174
|
+
err instanceof Error ? err.message : "Unable to resolve project root",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const linkedBlockers: HandoffLinkedBlocker[] = [];
|
|
179
|
+
const linkedWarnings: HandoffLinkedBlocker[] = [];
|
|
180
|
+
const readErrors: HandoffReadError[] = [];
|
|
181
|
+
|
|
182
|
+
for (const link of links) {
|
|
183
|
+
const result = await readLinkedTaskStatus(
|
|
184
|
+
pi,
|
|
185
|
+
projectRoot,
|
|
186
|
+
link.tsqId,
|
|
187
|
+
link.todoId,
|
|
188
|
+
signal,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (result.type === "internal_error") {
|
|
192
|
+
return { ok: false, code: result.code, message: result.message };
|
|
193
|
+
}
|
|
194
|
+
if (result.type === "read_error") {
|
|
195
|
+
readErrors.push(result.error);
|
|
196
|
+
} else if (result.type === "blocker") {
|
|
197
|
+
linkedBlockers.push(result.entry);
|
|
198
|
+
} else if (result.type === "warning") {
|
|
199
|
+
linkedWarnings.push(result.entry);
|
|
200
|
+
}
|
|
201
|
+
// "ready" → no action needed
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hasBlockers =
|
|
205
|
+
todoBlockers.length > 0 ||
|
|
206
|
+
linkedBlockers.length > 0 ||
|
|
207
|
+
readErrors.length > 0;
|
|
208
|
+
const hasWarnings = linkedWarnings.length > 0;
|
|
209
|
+
|
|
210
|
+
if (!hasBlockers && !hasWarnings) {
|
|
211
|
+
return { ok: true, ready: true, projectRoot };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
ready: !hasBlockers,
|
|
217
|
+
projectRoot,
|
|
218
|
+
...(todoBlockers.length > 0 ? { todoBlockers } : {}),
|
|
219
|
+
...(linkedBlockers.length > 0 ? { linkedBlockers } : {}),
|
|
220
|
+
...(linkedWarnings.length > 0 ? { linkedWarnings } : {}),
|
|
221
|
+
...(readErrors.length > 0 ? { readErrors } : {}),
|
|
222
|
+
} as HandoffCheckResult;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// No links: todo-only readiness (no git root needed)
|
|
226
|
+
if (todoBlockers.length > 0) {
|
|
227
|
+
return { ok: true, ready: false, todoBlockers };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { ok: true, ready: true };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Internal helpers
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
function collectTodoBlockers(todos: readonly Task[]): HandoffTodoBlocker[] {
|
|
238
|
+
const blockers: HandoffTodoBlocker[] = [];
|
|
239
|
+
|
|
240
|
+
for (const todo of todos) {
|
|
241
|
+
if (todo.status === "pending") {
|
|
242
|
+
const hasUnresolvedBlockers =
|
|
243
|
+
todo.blockedBy != null && todo.blockedBy.length > 0;
|
|
244
|
+
blockers.push({
|
|
245
|
+
todoId: todo.id,
|
|
246
|
+
subject: todo.subject,
|
|
247
|
+
status: "pending",
|
|
248
|
+
reason: hasUnresolvedBlockers ? "blocked" : "pending",
|
|
249
|
+
});
|
|
250
|
+
} else if (todo.status === "in_progress") {
|
|
251
|
+
blockers.push({
|
|
252
|
+
todoId: todo.id,
|
|
253
|
+
subject: todo.subject,
|
|
254
|
+
status: "in_progress",
|
|
255
|
+
reason: "in_progress",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return blockers;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
type LinkedReadResult =
|
|
264
|
+
| { type: "ready" }
|
|
265
|
+
| { type: "blocker"; entry: HandoffLinkedBlocker }
|
|
266
|
+
| { type: "warning"; entry: HandoffLinkedBlocker }
|
|
267
|
+
| { type: "read_error"; error: HandoffReadError }
|
|
268
|
+
| { type: "internal_error"; code: string; message: string };
|
|
269
|
+
|
|
270
|
+
async function readLinkedTaskStatus(
|
|
271
|
+
pi: ExtensionAPI,
|
|
272
|
+
projectRoot: string,
|
|
273
|
+
tsqId: string,
|
|
274
|
+
todoId: number,
|
|
275
|
+
signal?: AbortSignal,
|
|
276
|
+
): Promise<LinkedReadResult> {
|
|
277
|
+
try {
|
|
278
|
+
const data = await runTsqJson<TsqShowData>(
|
|
279
|
+
pi,
|
|
280
|
+
{ cwd: projectRoot },
|
|
281
|
+
["show", tsqId],
|
|
282
|
+
signal != null ? { signal } : {},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const status = data.task?.status;
|
|
286
|
+
const classification = classifyDurableStatus(status);
|
|
287
|
+
|
|
288
|
+
if (classification === "ready") return { type: "ready" };
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
type: classification,
|
|
292
|
+
entry: {
|
|
293
|
+
todoId,
|
|
294
|
+
tsqId,
|
|
295
|
+
status: status ?? "unknown",
|
|
296
|
+
classification,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (err instanceof TsqCommandError) {
|
|
301
|
+
const errClass = classifyReadError(err.code);
|
|
302
|
+
if (errClass === "actionable") {
|
|
303
|
+
return {
|
|
304
|
+
type: "read_error",
|
|
305
|
+
error: { tsqId, code: err.code, message: err.message },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
type: "internal_error",
|
|
310
|
+
code: err.code,
|
|
311
|
+
message: err.message,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (err instanceof TsqProcessError) {
|
|
316
|
+
return {
|
|
317
|
+
type: "internal_error",
|
|
318
|
+
code: "process_error",
|
|
319
|
+
message: err.message,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
type: "internal_error",
|
|
325
|
+
code: "unknown",
|
|
326
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_GIT_TIMEOUT_MS = 5_000;
|
|
4
|
+
|
|
5
|
+
export interface ProjectResolutionOptions {
|
|
6
|
+
readonly signal?: AbortSignal;
|
|
7
|
+
readonly timeout?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ProjectResolutionError extends Error {
|
|
11
|
+
override readonly name = "ProjectResolutionError";
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
readonly code: number;
|
|
14
|
+
readonly stderr: string;
|
|
15
|
+
readonly stdout: string;
|
|
16
|
+
readonly killed: boolean;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
cwd: string,
|
|
20
|
+
result: {
|
|
21
|
+
readonly code: number;
|
|
22
|
+
readonly stderr: string;
|
|
23
|
+
readonly stdout: string;
|
|
24
|
+
readonly killed: boolean;
|
|
25
|
+
},
|
|
26
|
+
) {
|
|
27
|
+
super(buildProjectResolutionMessage(cwd, result));
|
|
28
|
+
this.cwd = cwd;
|
|
29
|
+
this.code = result.code;
|
|
30
|
+
this.stderr = result.stderr;
|
|
31
|
+
this.stdout = result.stdout;
|
|
32
|
+
this.killed = result.killed;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function resolveProjectRoot(
|
|
37
|
+
pi: ExtensionAPI,
|
|
38
|
+
cwd: string,
|
|
39
|
+
options: ProjectResolutionOptions = {},
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], {
|
|
42
|
+
cwd,
|
|
43
|
+
timeout: options.timeout ?? DEFAULT_GIT_TIMEOUT_MS,
|
|
44
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const projectRoot = result.stdout.trim();
|
|
48
|
+
if (result.code !== 0 || result.killed || projectRoot.length === 0) {
|
|
49
|
+
throw new ProjectResolutionError(cwd, result);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return projectRoot;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildProjectResolutionMessage(
|
|
56
|
+
cwd: string,
|
|
57
|
+
result: {
|
|
58
|
+
readonly code: number;
|
|
59
|
+
readonly stderr: string;
|
|
60
|
+
readonly stdout: string;
|
|
61
|
+
readonly killed: boolean;
|
|
62
|
+
},
|
|
63
|
+
): string {
|
|
64
|
+
if (result.killed) {
|
|
65
|
+
return `Unable to resolve Tasque project root from ${cwd}: git rev-parse timed out`;
|
|
66
|
+
}
|
|
67
|
+
const detail = (result.stderr || result.stdout).replace(/\s+/gu, " ").trim();
|
|
68
|
+
return detail.length === 0
|
|
69
|
+
? `Unable to resolve Tasque project root from ${cwd}`
|
|
70
|
+
: `Unable to resolve Tasque project root from ${cwd}: ${detail}`;
|
|
71
|
+
}
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ExecResult,
|
|
4
4
|
ExtensionAPI,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { isRecord } from "../shared/error-utils.js";
|
|
6
7
|
import {
|
|
7
8
|
TSQ_SCHEMA_VERSION,
|
|
8
9
|
type JsonValue,
|
|
@@ -228,7 +229,3 @@ function buildProcessErrorMessage(result: ExecResult): string {
|
|
|
228
229
|
}
|
|
229
230
|
return `tsq failed with exit code ${result.code}${killed}: ${summary}`;
|
|
230
231
|
}
|
|
231
|
-
|
|
232
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
233
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
234
|
-
}
|
|
@@ -2,16 +2,18 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { isRecord } from "../shared/error-utils.js";
|
|
5
6
|
import {
|
|
6
7
|
createTasqueStatusCache,
|
|
7
8
|
formatTasqueStatusText,
|
|
8
9
|
refreshTasqueStatusCache,
|
|
9
10
|
type TasqueStatusCache,
|
|
10
11
|
} from "./cache.js";
|
|
12
|
+
import { resolveProjectRoot } from "./project.js";
|
|
11
13
|
|
|
12
14
|
export const TASQUE_STATUS_KEY = "pi-tasque";
|
|
13
15
|
|
|
14
|
-
const MUTATING_TOOL_NAMES = new Set(["
|
|
16
|
+
const MUTATING_TOOL_NAMES = new Set(["task"]);
|
|
15
17
|
const DEFAULT_INTERVAL_MS = 60_000;
|
|
16
18
|
|
|
17
19
|
export interface TasqueStatusLifecycleOptions {
|
|
@@ -56,12 +58,25 @@ export function registerTasqueStatusLifecycle(
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
let refreshPromise: Promise<void>;
|
|
59
|
-
refreshPromise =
|
|
60
|
-
...(options.now === undefined ? {} : { now: options.now }),
|
|
61
|
+
refreshPromise = resolveProjectRoot(pi, ctx.cwd, {
|
|
61
62
|
...(options.refreshTimeoutMs === undefined
|
|
62
63
|
? {}
|
|
63
64
|
: { timeout: options.refreshTimeoutMs }),
|
|
64
65
|
})
|
|
66
|
+
.then((projectRoot) =>
|
|
67
|
+
refreshTasqueStatusCache(pi, { cwd: projectRoot }, cache, {
|
|
68
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
69
|
+
...(options.refreshTimeoutMs === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: { timeout: options.refreshTimeoutMs }),
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
.catch((error) =>
|
|
75
|
+
createTasqueStatusCache({
|
|
76
|
+
...cache.state,
|
|
77
|
+
error: getErrorMessage(error),
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
65
80
|
.then((nextCache) => {
|
|
66
81
|
if (!statusActive || generation !== lifecycleGeneration) {
|
|
67
82
|
return;
|
|
@@ -179,6 +194,6 @@ function hasStatusUi(ctx: ExtensionContext): boolean {
|
|
|
179
194
|
);
|
|
180
195
|
}
|
|
181
196
|
|
|
182
|
-
function
|
|
183
|
-
return
|
|
197
|
+
function getErrorMessage(error: unknown): string {
|
|
198
|
+
return error instanceof Error ? error.message : String(error);
|
|
184
199
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { TaskBridgeParams } from "../bridge/types.js";
|
|
2
|
+
import { definedParams } from "../shared/validation.js";
|
|
3
|
+
import type { TaskParams } from "./task-schema.js";
|
|
4
|
+
import { getTodoId, hasWith } from "./task-validation.js";
|
|
5
|
+
import type { TsqChangeParams } from "./tools-change.js";
|
|
6
|
+
import type { TsqQueryParams } from "./tools-query.js";
|
|
7
|
+
|
|
8
|
+
export function toQueryParams(params: TaskParams): TsqQueryParams {
|
|
9
|
+
switch (params.action) {
|
|
10
|
+
case "doctor":
|
|
11
|
+
return { action: "doctor" };
|
|
12
|
+
case "find":
|
|
13
|
+
if (params.view === "tree") {
|
|
14
|
+
return definedParams<TsqQueryParams>({
|
|
15
|
+
action: "find_tree",
|
|
16
|
+
id: params.task,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return params.tasks === "open"
|
|
20
|
+
? definedParams<TsqQueryParams>({
|
|
21
|
+
action: "find_open",
|
|
22
|
+
assignee: params.for,
|
|
23
|
+
})
|
|
24
|
+
: definedParams<TsqQueryParams>({
|
|
25
|
+
action: "find_ready",
|
|
26
|
+
lane: params.lane,
|
|
27
|
+
assignee: params.for,
|
|
28
|
+
});
|
|
29
|
+
case "show":
|
|
30
|
+
return definedParams<TsqQueryParams>({
|
|
31
|
+
action: hasWith(params, "spec") ? "show_with_spec" : "show",
|
|
32
|
+
id: params.task,
|
|
33
|
+
});
|
|
34
|
+
case "deps":
|
|
35
|
+
return definedParams<TsqQueryParams>({ action: "deps", id: params.task });
|
|
36
|
+
case "notes":
|
|
37
|
+
return definedParams<TsqQueryParams>({
|
|
38
|
+
action: "notes",
|
|
39
|
+
id: params.task,
|
|
40
|
+
});
|
|
41
|
+
case "similar":
|
|
42
|
+
return definedParams<TsqQueryParams>({
|
|
43
|
+
action: "similar",
|
|
44
|
+
query: params.query,
|
|
45
|
+
});
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unsupported query action: ${params.action}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function toChangeParams(params: TaskParams): TsqChangeParams {
|
|
52
|
+
switch (params.action) {
|
|
53
|
+
case "create":
|
|
54
|
+
return definedParams<TsqChangeParams>({
|
|
55
|
+
action: "create",
|
|
56
|
+
title: params.task,
|
|
57
|
+
kind: params.kind,
|
|
58
|
+
priority: params.priority,
|
|
59
|
+
description: params.description,
|
|
60
|
+
parent: params.under,
|
|
61
|
+
planned: params.planned,
|
|
62
|
+
needsPlan: params.needsPlan,
|
|
63
|
+
});
|
|
64
|
+
case "note":
|
|
65
|
+
return definedParams<TsqChangeParams>({
|
|
66
|
+
action: "note",
|
|
67
|
+
id: params.task,
|
|
68
|
+
note: params.because,
|
|
69
|
+
});
|
|
70
|
+
case "finish":
|
|
71
|
+
return definedParams<TsqChangeParams>({
|
|
72
|
+
action: "done",
|
|
73
|
+
id: params.task,
|
|
74
|
+
note: params.because,
|
|
75
|
+
});
|
|
76
|
+
case "reopen":
|
|
77
|
+
case "start":
|
|
78
|
+
return definedParams<TsqChangeParams>({
|
|
79
|
+
action: params.action,
|
|
80
|
+
id: params.task,
|
|
81
|
+
});
|
|
82
|
+
case "defer":
|
|
83
|
+
return definedParams<TsqChangeParams>({
|
|
84
|
+
action: "defer",
|
|
85
|
+
id: params.task,
|
|
86
|
+
note: params.because,
|
|
87
|
+
});
|
|
88
|
+
case "block":
|
|
89
|
+
return definedParams<TsqChangeParams>({
|
|
90
|
+
action: "block",
|
|
91
|
+
child: params.task,
|
|
92
|
+
blocker: params.by,
|
|
93
|
+
});
|
|
94
|
+
case "unblock":
|
|
95
|
+
return definedParams<TsqChangeParams>({
|
|
96
|
+
action: "unblock",
|
|
97
|
+
child: params.task,
|
|
98
|
+
blocker: params.by,
|
|
99
|
+
});
|
|
100
|
+
case "order":
|
|
101
|
+
return definedParams<TsqChangeParams>({
|
|
102
|
+
action: "order",
|
|
103
|
+
later: params.task,
|
|
104
|
+
earlier: params.after,
|
|
105
|
+
});
|
|
106
|
+
case "unorder":
|
|
107
|
+
return definedParams<TsqChangeParams>({
|
|
108
|
+
action: "unorder",
|
|
109
|
+
later: params.task,
|
|
110
|
+
earlier: params.after,
|
|
111
|
+
});
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Unsupported change action: ${params.action}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function toClaimParams(
|
|
118
|
+
params: TaskParams,
|
|
119
|
+
): Readonly<Record<string, unknown>> {
|
|
120
|
+
return definedParams<Readonly<Record<string, unknown>>>({
|
|
121
|
+
id: params.task,
|
|
122
|
+
assignee: params.for,
|
|
123
|
+
start: params.start,
|
|
124
|
+
requireSpec: params.requireSpec,
|
|
125
|
+
createTodo: params.todo === true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function toBridgeParams(params: TaskParams): TaskBridgeParams {
|
|
130
|
+
switch (params.action) {
|
|
131
|
+
case "link":
|
|
132
|
+
return definedParams<TaskBridgeParams>({
|
|
133
|
+
action: "link",
|
|
134
|
+
todoId: getTodoId(params.todo),
|
|
135
|
+
tsqId: params.task,
|
|
136
|
+
});
|
|
137
|
+
case "list_links":
|
|
138
|
+
return { action: "list_links" };
|
|
139
|
+
case "promote":
|
|
140
|
+
return definedParams<TaskBridgeParams>({
|
|
141
|
+
action: "promote_todo",
|
|
142
|
+
todoId: getTodoId(params.todo),
|
|
143
|
+
assignee: params.for,
|
|
144
|
+
kind: params.kind,
|
|
145
|
+
priority: params.priority,
|
|
146
|
+
description: params.description,
|
|
147
|
+
parent: params.under,
|
|
148
|
+
planned: params.planned,
|
|
149
|
+
needsPlan: params.needsPlan,
|
|
150
|
+
});
|
|
151
|
+
case "import":
|
|
152
|
+
return definedParams<TaskBridgeParams>({
|
|
153
|
+
action: "import_tsq",
|
|
154
|
+
tsqId: params.task,
|
|
155
|
+
owner: params.for,
|
|
156
|
+
});
|
|
157
|
+
default:
|
|
158
|
+
throw new Error(`Unsupported bridge action: ${params.action}`);
|
|
159
|
+
}
|
|
160
|
+
}
|