@cmgmyr/hive 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/README.md +70 -0
- package/claude-plugin/.claude-plugin/plugin.json +7 -0
- package/claude-plugin/README.md +47 -0
- package/claude-plugin/hooks/hooks.json +14 -0
- package/claude-plugin/kickoff.mjs +41 -0
- package/claude-plugin/skills/cleanup/SKILL.md +60 -0
- package/dist/abi.js +220 -0
- package/dist/abiProbe.js +9 -0
- package/dist/backgroundTasks.js +51 -0
- package/dist/backup.js +378 -0
- package/dist/brief.js +113 -0
- package/dist/build-info.json +1 -0
- package/dist/claudeDir.js +5 -0
- package/dist/cli.js +2901 -0
- package/dist/codexHome.js +297 -0
- package/dist/config.js +72 -0
- package/dist/context.js +256 -0
- package/dist/contextCheckpoint.js +55 -0
- package/dist/dashboard.js +1158 -0
- package/dist/dataDir.js +89 -0
- package/dist/db.js +421 -0
- package/dist/dispatcher.js +150 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/harnesses.js +173 -0
- package/dist/help.js +353 -0
- package/dist/hook.js +197 -0
- package/dist/hooks.js +50 -0
- package/dist/index.js +34 -0
- package/dist/kickoff.js +191 -0
- package/dist/leadMessage.js +98 -0
- package/dist/mcpConfig.js +121 -0
- package/dist/processes.js +161 -0
- package/dist/profiles.js +278 -0
- package/dist/projectYml.js +254 -0
- package/dist/prompts.js +18 -0
- package/dist/ptys.js +146 -0
- package/dist/result.js +53 -0
- package/dist/scheduler.js +1622 -0
- package/dist/sessionProbe.js +131 -0
- package/dist/slug.js +29 -0
- package/dist/spawn.js +370 -0
- package/dist/stateProvenance.js +97 -0
- package/dist/statusline.js +82 -0
- package/dist/strictInput.js +52 -0
- package/dist/teardown.js +89 -0
- package/dist/tmux.js +1295 -0
- package/dist/tools/agents.js +1246 -0
- package/dist/tools/kv.js +89 -0
- package/dist/tools/leases.js +87 -0
- package/dist/tools/meta.js +183 -0
- package/dist/tools/pads.js +279 -0
- package/dist/tools/params.js +14 -0
- package/dist/tools/todos.js +382 -0
- package/dist/tools/wakes.js +466 -0
- package/dist/transcript.js +123 -0
- package/dist/version.js +59 -0
- package/package.json +62 -0
- package/profiles/orchestration/posture.md +27 -0
- package/profiles/orchestration/runbook.md +72 -0
- package/profiles/orchestration/worker.md +38 -0
- package/profiles/simple/posture.md +14 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { db } from "../db.js";
|
|
3
|
+
import { currentActor, effectiveProjectId, resolveProject } from "../context.js";
|
|
4
|
+
import { matchesAnyTag, parseTags, run } from "../result.js";
|
|
5
|
+
import { findUnsafeControlChar } from "../tmux.js";
|
|
6
|
+
import { SLUG_MAX_LEN, fallbackSlug } from "../slug.js";
|
|
7
|
+
import { idParam, limitParam, offsetParam, projectIdParam } from "./params.js";
|
|
8
|
+
const priorityParam = z.enum(["high", "medium", "low"]);
|
|
9
|
+
const slugParam = z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.max(SLUG_MAX_LEN)
|
|
13
|
+
.refine((s) => findUnsafeControlChar(s, new Set()) === null, {
|
|
14
|
+
message: "slug cannot contain control characters, including tabs or newlines",
|
|
15
|
+
})
|
|
16
|
+
.describe(`Short label, ~3-5 words (${SLUG_MAX_LEN} chars max). When set, it renders beside the id: bracketed in \`hive todos\`, \`hive todo <id>\`, and the session-start digest, and as the row's whole label in the dashboard. Left unset, those surfaces fall back to the title - shown in full in \`hive todos\`/\`hive todo <id>\`/the digest, truncated to this same length in the dashboard. Free text, not a pad-style slug. Pass "" to clear a previously-set slug back to unset.`)
|
|
17
|
+
.optional();
|
|
18
|
+
export { SLUG_MAX_LEN, fallbackSlug };
|
|
19
|
+
export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
|
|
20
|
+
const statusParam = z.enum(TODO_STATUSES);
|
|
21
|
+
const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
|
|
22
|
+
WHERE b.todo_id = t.id AND bt.status != 'completed'`;
|
|
23
|
+
export const OPEN_BLOCKERS_SQL = `SELECT 1 ${OPEN_BLOCKERS_FROM_WHERE}`;
|
|
24
|
+
const LIVE_DEPENDENTS_SQL = `SELECT t.id AS todo_id FROM todo_blockers b JOIN todos t ON t.id = b.todo_id
|
|
25
|
+
WHERE b.blocker_id = ? AND t.status != 'completed' AND t.archived_at IS NULL`;
|
|
26
|
+
export const COMMENT_COUNT_SQL = `(SELECT COUNT(*) FROM todo_comments c WHERE c.todo_id = t.id)`;
|
|
27
|
+
const SUMMARY_SQL = `
|
|
28
|
+
SELECT t.*,
|
|
29
|
+
(SELECT COUNT(*) ${OPEN_BLOCKERS_FROM_WHERE}) AS open_blockers,
|
|
30
|
+
${COMMENT_COUNT_SQL} AS comment_count
|
|
31
|
+
FROM todos t`;
|
|
32
|
+
function getTodo(projectId, todoId) {
|
|
33
|
+
const row = db
|
|
34
|
+
.prepare(`${SUMMARY_SQL} WHERE t.project_id = ? AND t.id = ?`)
|
|
35
|
+
.get(projectId, todoId);
|
|
36
|
+
if (!row)
|
|
37
|
+
throw new Error(`No todo with id ${todoId} in project ${projectId}. Call todo_list.`);
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
function summarize(row) {
|
|
41
|
+
return {
|
|
42
|
+
todo_id: row.id,
|
|
43
|
+
title: row.title,
|
|
44
|
+
slug: row.slug || fallbackSlug(row.title) || `todo ${row.id}`,
|
|
45
|
+
status: row.status,
|
|
46
|
+
priority: row.priority,
|
|
47
|
+
tags: parseTags(row.tags),
|
|
48
|
+
archived: row.archived_at != null,
|
|
49
|
+
is_blocked: (row.open_blockers ?? 0) > 0,
|
|
50
|
+
open_blockers: row.open_blockers ?? 0,
|
|
51
|
+
comment_count: row.comment_count ?? 0,
|
|
52
|
+
updated_at: row.updated_at,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function listTodoSummaries(projectId, filter = {}) {
|
|
56
|
+
const limit = Math.min(filter.limit ?? 50, 200);
|
|
57
|
+
const offset = filter.offset ?? 0;
|
|
58
|
+
let sql = `${SUMMARY_SQL} WHERE t.project_id = ?`;
|
|
59
|
+
const params = [projectId];
|
|
60
|
+
if (!filter.includeArchived) {
|
|
61
|
+
sql += " AND t.archived_at IS NULL";
|
|
62
|
+
}
|
|
63
|
+
if (filter.statuses && filter.statuses.length > 0) {
|
|
64
|
+
sql += ` AND t.status IN (${filter.statuses.map(() => "?").join(",")})`;
|
|
65
|
+
params.push(...filter.statuses);
|
|
66
|
+
}
|
|
67
|
+
if (filter.priority) {
|
|
68
|
+
sql += " AND t.priority = ?";
|
|
69
|
+
params.push(filter.priority);
|
|
70
|
+
}
|
|
71
|
+
if (filter.query) {
|
|
72
|
+
sql += " AND (t.title LIKE ? OR t.body LIKE ? OR t.slug LIKE ?)";
|
|
73
|
+
params.push(`%${filter.query}%`, `%${filter.query}%`, `%${filter.query}%`);
|
|
74
|
+
}
|
|
75
|
+
sql += " ORDER BY t.updated_at DESC";
|
|
76
|
+
let rows = db.prepare(sql).all(...params).filter((r) => matchesAnyTag(r.tags, filter.tags));
|
|
77
|
+
if (filter.isBlocked != null) {
|
|
78
|
+
rows = rows.filter((r) => ((r.open_blockers ?? 0) > 0) === filter.isBlocked);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
total_count: rows.length,
|
|
82
|
+
offset,
|
|
83
|
+
limit,
|
|
84
|
+
todos: rows.slice(offset, offset + limit).map(summarize),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function getTodoDetail(projectId, todoId, includeComments) {
|
|
88
|
+
const todo = getTodo(projectId, todoId);
|
|
89
|
+
const blockers = db
|
|
90
|
+
.prepare(`SELECT t.id, t.title, t.status FROM todo_blockers b
|
|
91
|
+
JOIN todos t ON t.id = b.blocker_id WHERE b.todo_id = ?`)
|
|
92
|
+
.all(todo.id);
|
|
93
|
+
const blocking = db
|
|
94
|
+
.prepare(`SELECT t.id, t.title, t.status FROM todo_blockers b
|
|
95
|
+
JOIN todos t ON t.id = b.todo_id WHERE b.blocker_id = ?`)
|
|
96
|
+
.all(todo.id);
|
|
97
|
+
const result = {
|
|
98
|
+
...summarize(todo),
|
|
99
|
+
body: todo.body,
|
|
100
|
+
created_at: todo.created_at,
|
|
101
|
+
completed_at: todo.completed_at,
|
|
102
|
+
blockers,
|
|
103
|
+
blocking,
|
|
104
|
+
};
|
|
105
|
+
if (includeComments) {
|
|
106
|
+
result.comments = db
|
|
107
|
+
.prepare("SELECT id, author, body, created_at FROM todo_comments WHERE todo_id = ? ORDER BY created_at")
|
|
108
|
+
.all(todo.id);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function transitiveBlockers(startId) {
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
const stack = [startId];
|
|
115
|
+
const stmt = db.prepare("SELECT blocker_id FROM todo_blockers WHERE todo_id = ?");
|
|
116
|
+
while (stack.length > 0) {
|
|
117
|
+
const current = stack.pop();
|
|
118
|
+
for (const row of stmt.all(current)) {
|
|
119
|
+
if (!seen.has(row.blocker_id)) {
|
|
120
|
+
seen.add(row.blocker_id);
|
|
121
|
+
stack.push(row.blocker_id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return seen;
|
|
126
|
+
}
|
|
127
|
+
const addBlocker = db.transaction((projectId, todoId, blockerId) => {
|
|
128
|
+
if (todoId === blockerId)
|
|
129
|
+
throw new Error("A todo cannot block itself.");
|
|
130
|
+
getTodo(projectId, todoId);
|
|
131
|
+
const blocker = getTodo(projectId, blockerId);
|
|
132
|
+
if (blocker.archived_at != null) {
|
|
133
|
+
throw new Error(`Cannot add blocker ${blockerId} to todo ${todoId}: todo ${blockerId} is archived. Unarchive it first.`);
|
|
134
|
+
}
|
|
135
|
+
if (transitiveBlockers(blockerId).has(todoId)) {
|
|
136
|
+
throw new Error(`Adding blocker ${blockerId} to todo ${todoId} would create a dependency cycle.`);
|
|
137
|
+
}
|
|
138
|
+
db.prepare("INSERT OR IGNORE INTO todo_blockers (todo_id, blocker_id) VALUES (?, ?)").run(todoId, blockerId);
|
|
139
|
+
});
|
|
140
|
+
function touch(todoId) {
|
|
141
|
+
db.prepare("UPDATE todos SET updated_at = datetime('now') WHERE id = ?").run(todoId);
|
|
142
|
+
}
|
|
143
|
+
const archiveTodo = db.transaction((projectId, todoId, archived) => {
|
|
144
|
+
const todo = getTodo(projectId, todoId);
|
|
145
|
+
if ((todo.archived_at != null) === archived) {
|
|
146
|
+
return { todo_id: todo.id, archived };
|
|
147
|
+
}
|
|
148
|
+
if (archived && todo.status !== "completed") {
|
|
149
|
+
const dependents = db.prepare(LIVE_DEPENDENTS_SQL).all(todo.id);
|
|
150
|
+
if (dependents.length > 0) {
|
|
151
|
+
const ids = dependents.map((d) => d.todo_id).join(", ");
|
|
152
|
+
throw new Error(`Cannot archive todo ${todo.id}: it still blocks ${ids}. Complete or unblock ` +
|
|
153
|
+
`${dependents.length === 1 ? "that todo" : "those todos"} first.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
db.prepare(`UPDATE todos SET archived_at = CASE WHEN ? THEN datetime('now') ELSE NULL END,
|
|
157
|
+
updated_at = datetime('now') WHERE id = ?`).run(archived ? 1 : 0, todo.id);
|
|
158
|
+
return { todo_id: todo.id, archived };
|
|
159
|
+
});
|
|
160
|
+
const updateTodo = db.transaction((projectId, todoId, patch) => {
|
|
161
|
+
const todo = getTodo(projectId, todoId);
|
|
162
|
+
if (todo.archived_at != null && todo.status === "completed" && patch.status && patch.status !== "completed") {
|
|
163
|
+
throw new Error(`Cannot change todo ${todo.id}'s status away from completed while archived. Unarchive it first.`);
|
|
164
|
+
}
|
|
165
|
+
currentActor();
|
|
166
|
+
db.prepare(`UPDATE todos SET
|
|
167
|
+
title = COALESCE(?, title),
|
|
168
|
+
body = COALESCE(?, body),
|
|
169
|
+
priority = COALESCE(?, priority),
|
|
170
|
+
status = COALESCE(?, status),
|
|
171
|
+
tags = COALESCE(?, tags),
|
|
172
|
+
slug = COALESCE(?, slug),
|
|
173
|
+
completed_at = CASE WHEN ? = 'completed' THEN datetime('now')
|
|
174
|
+
WHEN ? IS NOT NULL THEN NULL
|
|
175
|
+
ELSE completed_at END,
|
|
176
|
+
updated_at = datetime('now')
|
|
177
|
+
WHERE id = ?`).run(patch.title ?? null, patch.body ?? null, patch.priority ?? null, patch.status ?? null, patch.tags ? JSON.stringify(patch.tags) : null, patch.slug ?? null, patch.status ?? null, patch.status ?? null, todo.id);
|
|
178
|
+
return { todo_id: todo.id };
|
|
179
|
+
});
|
|
180
|
+
const completeTodo = db.transaction((projectId, todoId, completed) => {
|
|
181
|
+
const todo = getTodo(projectId, todoId);
|
|
182
|
+
if (!completed && todo.archived_at != null) {
|
|
183
|
+
throw new Error(`Cannot reopen archived todo ${todo.id}. Unarchive it first.`);
|
|
184
|
+
}
|
|
185
|
+
currentActor();
|
|
186
|
+
db.prepare(`UPDATE todos SET status = ?,
|
|
187
|
+
completed_at = CASE WHEN ? THEN datetime('now') ELSE NULL END,
|
|
188
|
+
updated_at = datetime('now') WHERE id = ?`).run(completed ? "completed" : "open", completed ? 1 : 0, todo.id);
|
|
189
|
+
let newlyUnblocked = [];
|
|
190
|
+
if (completed) {
|
|
191
|
+
newlyUnblocked = db
|
|
192
|
+
.prepare(`${LIVE_DEPENDENTS_SQL}
|
|
193
|
+
AND NOT EXISTS (
|
|
194
|
+
SELECT 1 FROM todo_blockers b2 JOIN todos bt ON bt.id = b2.blocker_id
|
|
195
|
+
WHERE b2.todo_id = t.id AND bt.status != 'completed'
|
|
196
|
+
)`)
|
|
197
|
+
.all(todo.id).map((r) => r.todo_id);
|
|
198
|
+
}
|
|
199
|
+
return { todo_id: todo.id, completed, newly_unblocked: newlyUnblocked };
|
|
200
|
+
});
|
|
201
|
+
export function registerTodos(server) {
|
|
202
|
+
server.registerTool("todo_create", {
|
|
203
|
+
description: "Create a project-scoped todo. Pass a short slug to have it render beside the id wherever this todo is listed; left unset, those surfaces fall back to the title instead (truncated in the dashboard). Optionally pass blocked_by todo ids to encode ordering. Returns a slim receipt.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
title: z.string(),
|
|
206
|
+
body: z.string().optional().describe("Objective, owned files, acceptance criteria."),
|
|
207
|
+
priority: priorityParam.optional(),
|
|
208
|
+
tags: z.array(z.string()).optional(),
|
|
209
|
+
slug: slugParam,
|
|
210
|
+
blocked_by: z.array(idParam).optional(),
|
|
211
|
+
project_id: projectIdParam,
|
|
212
|
+
},
|
|
213
|
+
outputSchema: { project_id: idParam, todo_id: idParam },
|
|
214
|
+
}, (args) => run(() => {
|
|
215
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
216
|
+
currentActor();
|
|
217
|
+
const info = db
|
|
218
|
+
.prepare("INSERT INTO todos (project_id, title, body, priority, tags, slug) VALUES (?, ?, ?, ?, ?, ?)")
|
|
219
|
+
.run(projectId, args.title, args.body ?? "", args.priority ?? "medium", JSON.stringify(args.tags ?? []), args.slug ?? "");
|
|
220
|
+
const todoId = Number(info.lastInsertRowid);
|
|
221
|
+
for (const blockerId of args.blocked_by ?? []) {
|
|
222
|
+
addBlocker.immediate(projectId, todoId, blockerId);
|
|
223
|
+
}
|
|
224
|
+
return { project_id: projectId, todo_id: todoId };
|
|
225
|
+
}));
|
|
226
|
+
server.registerTool("todo_list", {
|
|
227
|
+
description: "List todo summaries. is_blocked=false finds dispatchable work. query matches title, body, and slug. Archived todos are excluded by default; include_archived=true retrieves them too.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
status: statusParam.optional(),
|
|
230
|
+
is_blocked: z.boolean().optional(),
|
|
231
|
+
priority: priorityParam.optional(),
|
|
232
|
+
query: z.string().optional(),
|
|
233
|
+
tags: z.array(z.string()).optional(),
|
|
234
|
+
include_archived: z.boolean().optional(),
|
|
235
|
+
limit: limitParam,
|
|
236
|
+
offset: offsetParam,
|
|
237
|
+
project_id: projectIdParam,
|
|
238
|
+
},
|
|
239
|
+
}, (args) => run(() => {
|
|
240
|
+
const project = resolveProject(args.project_id);
|
|
241
|
+
const result = listTodoSummaries(project.id, {
|
|
242
|
+
statuses: args.status ? [args.status] : undefined,
|
|
243
|
+
priority: args.priority,
|
|
244
|
+
query: args.query,
|
|
245
|
+
tags: args.tags,
|
|
246
|
+
isBlocked: args.is_blocked,
|
|
247
|
+
includeArchived: args.include_archived,
|
|
248
|
+
limit: args.limit,
|
|
249
|
+
offset: args.offset,
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
project_id: project.id,
|
|
253
|
+
project_name: project.name,
|
|
254
|
+
...result,
|
|
255
|
+
};
|
|
256
|
+
}));
|
|
257
|
+
server.registerTool("todo_get", {
|
|
258
|
+
description: "Read one todo in full: body, blockers, what it blocks, and optionally comments.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
todo_id: idParam,
|
|
261
|
+
include_comments: z.boolean().optional(),
|
|
262
|
+
project_id: projectIdParam,
|
|
263
|
+
},
|
|
264
|
+
}, (args) => run(() => {
|
|
265
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
266
|
+
return getTodoDetail(projectId, args.todo_id, args.include_comments ?? false);
|
|
267
|
+
}));
|
|
268
|
+
server.registerTool("todo_update", {
|
|
269
|
+
description: "Update todo fields. Omitted fields are preserved. Returns a slim receipt.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
todo_id: idParam,
|
|
272
|
+
title: z.string().optional(),
|
|
273
|
+
body: z.string().optional(),
|
|
274
|
+
priority: priorityParam.optional(),
|
|
275
|
+
status: statusParam.optional(),
|
|
276
|
+
tags: z.array(z.string()).optional(),
|
|
277
|
+
slug: slugParam,
|
|
278
|
+
project_id: projectIdParam,
|
|
279
|
+
},
|
|
280
|
+
outputSchema: { project_id: idParam, todo_id: idParam },
|
|
281
|
+
}, (args) => run(() => {
|
|
282
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
283
|
+
const result = updateTodo.immediate(projectId, args.todo_id, {
|
|
284
|
+
title: args.title,
|
|
285
|
+
body: args.body,
|
|
286
|
+
priority: args.priority,
|
|
287
|
+
status: args.status,
|
|
288
|
+
tags: args.tags,
|
|
289
|
+
slug: args.slug,
|
|
290
|
+
});
|
|
291
|
+
return { project_id: projectId, ...result };
|
|
292
|
+
}));
|
|
293
|
+
server.registerTool("todo_archive", {
|
|
294
|
+
description: "Archive a todo (or unarchive with archived=false), mirroring pad_archive. Archived todos are excluded from todo_list by default; todo_get always reaches them by id. Refuses when this todo still blocks another todo that is not completed.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
todo_id: idParam,
|
|
297
|
+
archived: z.boolean().optional().describe("Default true. Pass false to unarchive."),
|
|
298
|
+
project_id: projectIdParam,
|
|
299
|
+
},
|
|
300
|
+
outputSchema: { project_id: idParam, todo_id: idParam, archived: z.boolean() },
|
|
301
|
+
}, (args) => run(() => {
|
|
302
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
303
|
+
const result = archiveTodo.immediate(projectId, args.todo_id, args.archived ?? true);
|
|
304
|
+
return { project_id: projectId, ...result };
|
|
305
|
+
}));
|
|
306
|
+
server.registerTool("todo_complete", {
|
|
307
|
+
description: "Mark a todo complete (or reopen with completed=false). Returns todo ids that this completion newly unblocked.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
todo_id: idParam,
|
|
310
|
+
completed: z.boolean().optional().describe("Defaults to true."),
|
|
311
|
+
project_id: projectIdParam,
|
|
312
|
+
},
|
|
313
|
+
outputSchema: {
|
|
314
|
+
project_id: idParam,
|
|
315
|
+
todo_id: idParam,
|
|
316
|
+
completed: z.boolean(),
|
|
317
|
+
newly_unblocked: z.array(idParam),
|
|
318
|
+
},
|
|
319
|
+
}, (args) => run(() => {
|
|
320
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
321
|
+
const result = completeTodo.immediate(projectId, args.todo_id, args.completed ?? true);
|
|
322
|
+
return { project_id: projectId, ...result };
|
|
323
|
+
}));
|
|
324
|
+
server.registerTool("todo_comment", {
|
|
325
|
+
description: "Add a comment to a todo. Use for handoffs: changed files, tests run, decisions, remaining risk.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
todo_id: idParam,
|
|
328
|
+
body: z.string(),
|
|
329
|
+
project_id: projectIdParam,
|
|
330
|
+
},
|
|
331
|
+
outputSchema: { project_id: idParam, todo_id: idParam, comment_id: idParam },
|
|
332
|
+
}, (args) => run(() => {
|
|
333
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
334
|
+
const todo = getTodo(projectId, args.todo_id);
|
|
335
|
+
const info = db
|
|
336
|
+
.prepare("INSERT INTO todo_comments (todo_id, author, body) VALUES (?, ?, ?)")
|
|
337
|
+
.run(todo.id, currentActor(), args.body);
|
|
338
|
+
touch(todo.id);
|
|
339
|
+
return { project_id: projectId, todo_id: todo.id, comment_id: Number(info.lastInsertRowid) };
|
|
340
|
+
}));
|
|
341
|
+
server.registerTool("todo_block", {
|
|
342
|
+
description: "Add a blocker: todo_id cannot start until blocker_id completes. Cycles are rejected.",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
todo_id: idParam,
|
|
345
|
+
blocker_id: idParam,
|
|
346
|
+
project_id: projectIdParam,
|
|
347
|
+
},
|
|
348
|
+
outputSchema: { project_id: idParam, todo_id: idParam, blocker_id: idParam },
|
|
349
|
+
}, (args) => run(() => {
|
|
350
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
351
|
+
addBlocker.immediate(projectId, args.todo_id, args.blocker_id);
|
|
352
|
+
touch(args.todo_id);
|
|
353
|
+
return { project_id: projectId, todo_id: args.todo_id, blocker_id: args.blocker_id };
|
|
354
|
+
}));
|
|
355
|
+
server.registerTool("todo_unblock", {
|
|
356
|
+
description: "Remove one blocker relationship from a todo.",
|
|
357
|
+
inputSchema: {
|
|
358
|
+
todo_id: idParam,
|
|
359
|
+
blocker_id: idParam,
|
|
360
|
+
project_id: projectIdParam,
|
|
361
|
+
},
|
|
362
|
+
outputSchema: {
|
|
363
|
+
project_id: idParam,
|
|
364
|
+
todo_id: idParam,
|
|
365
|
+
blocker_id: idParam,
|
|
366
|
+
removed: z.boolean(),
|
|
367
|
+
},
|
|
368
|
+
}, (args) => run(() => {
|
|
369
|
+
const projectId = effectiveProjectId(args.project_id);
|
|
370
|
+
getTodo(projectId, args.todo_id);
|
|
371
|
+
const info = db
|
|
372
|
+
.prepare("DELETE FROM todo_blockers WHERE todo_id = ? AND blocker_id = ?")
|
|
373
|
+
.run(args.todo_id, args.blocker_id);
|
|
374
|
+
touch(args.todo_id);
|
|
375
|
+
return {
|
|
376
|
+
project_id: projectId,
|
|
377
|
+
todo_id: args.todo_id,
|
|
378
|
+
blocker_id: args.blocker_id,
|
|
379
|
+
removed: info.changes > 0,
|
|
380
|
+
};
|
|
381
|
+
}));
|
|
382
|
+
}
|