@ai-setting/roy-task-plugin 0.7.1 → 0.7.2
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.
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `findOpenTask` — the Stop hook's "is a task still open?" lookup.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `stop-check.mjs` so it can be unit-tested without spawning a
|
|
5
|
+
* hook process (Task #4624, CV finding).
|
|
6
|
+
*
|
|
7
|
+
* ## Why the shape matters
|
|
8
|
+
*
|
|
9
|
+
* `TaskBackend.listTasks` is a *paged view*: it applies the status filter in
|
|
10
|
+
* memory, then slices `[offset, offset+limit)`. It promises neither an order
|
|
11
|
+
* nor that one page holds every match. So "take one page, then filter in the
|
|
12
|
+
* caller" silently misses an open task that sits behind a page of newer
|
|
13
|
+
* completed tasks — the exact regression this module exists to prevent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The statuses that count as "still open".
|
|
18
|
+
*
|
|
19
|
+
* Ordered by how recently a task is likely to have been touched; the search
|
|
20
|
+
* does not depend on the order because it scans every open status.
|
|
21
|
+
*/
|
|
22
|
+
export const OPEN_STATUSES = ["active", "todo"];
|
|
23
|
+
|
|
24
|
+
/** Page size used while draining the open statuses. */
|
|
25
|
+
const PAGE_SIZE = 500;
|
|
26
|
+
|
|
27
|
+
/** Read an update timestamp from either roy shape (`updated_at` / `updatedAt`). */
|
|
28
|
+
export function updatedAtOf(task) {
|
|
29
|
+
const raw = task?.updatedAt ?? task?.updated_at ?? 0;
|
|
30
|
+
if (typeof raw === "number") return raw;
|
|
31
|
+
const parsed = Date.parse(String(raw));
|
|
32
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The most recently updated task that is still `todo` / `active`, or `null`.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ listTasks: (options?: object) => Promise<{ tasks?: any[], total?: number }> }} backend
|
|
39
|
+
*/
|
|
40
|
+
export async function findOpenTask(backend) {
|
|
41
|
+
let newest = null;
|
|
42
|
+
let newestAt = Number.NEGATIVE_INFINITY;
|
|
43
|
+
|
|
44
|
+
for (const status of OPEN_STATUSES) {
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (;;) {
|
|
47
|
+
const page = await backend.listTasks({ status, limit: PAGE_SIZE, offset });
|
|
48
|
+
const tasks = page?.tasks ?? [];
|
|
49
|
+
|
|
50
|
+
for (const task of tasks) {
|
|
51
|
+
if (task.status !== status) continue;
|
|
52
|
+
const at = updatedAtOf(task);
|
|
53
|
+
if (at > newestAt || (at === newestAt && newest && task.id > newest.id)) {
|
|
54
|
+
newest = task;
|
|
55
|
+
newestAt = at;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
offset += tasks.length;
|
|
60
|
+
const total = page?.total ?? offset;
|
|
61
|
+
if (tasks.length === 0 || offset >= total) break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return newest;
|
|
66
|
+
}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import { createServer, defaultTaskDbPath, BridgeError } from "../mcp/bridge.mjs";
|
|
42
|
+
import { findOpenTask } from "./find-open-task.mjs";
|
|
42
43
|
|
|
43
44
|
/** Read stdin fully, with a hard cap so a huge transcript cannot stall us. */
|
|
44
45
|
async function readStdin(limitBytes = 2 * 1024 * 1024) {
|
|
@@ -131,26 +132,6 @@ function parseTranscript(raw) {
|
|
|
131
132
|
return entries;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
/** The most recently updated task that is still open. */
|
|
135
|
-
/**
|
|
136
|
-
* Find the most recently touched task that is still open.
|
|
137
|
-
*
|
|
138
|
-
* Task #4619 — this used to be a hand-written SQL query against roy's own
|
|
139
|
-
* database (`SELECT id, title, status FROM task WHERE status IN ('todo',
|
|
140
|
-
* 'active') ORDER BY updated_at DESC LIMIT 1`). The *query* is task
|
|
141
|
-
* semantics, so it belongs to core, not to the plugin.
|
|
142
|
-
*
|
|
143
|
-
* It is now expressed against core's `TaskBackend`, which owns the status
|
|
144
|
-
* vocabulary and the ordering.
|
|
145
|
-
*/
|
|
146
|
-
async function findOpenTask(backend) {
|
|
147
|
-
const result = await backend.listTasks({ limit: 200 });
|
|
148
|
-
const open = result.tasks
|
|
149
|
-
.filter((t) => t.status === "todo" || t.status === "active")
|
|
150
|
-
.sort((a, b) => (b.updatedAt ?? b.updated_at ?? 0) - (a.updatedAt ?? a.updated_at ?? 0));
|
|
151
|
-
return open[0] ?? null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
135
|
function approve() {
|
|
155
136
|
process.stdout.write(JSON.stringify({ decision: "approve" }));
|
|
156
137
|
process.exit(0);
|
package/dist/cli.js
CHANGED