@yattalo/task-system 0.4.0 → 0.5.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.
Files changed (57) hide show
  1. package/README.md +48 -0
  2. package/dashboard-app/assets/spa-entry-CnIKatv4.js +24 -0
  3. package/dashboard-app/assets/styles-CAIFwsCh.css +1 -0
  4. package/dashboard-app/index.html +14 -0
  5. package/dist/commands/dashboard.d.ts +2 -0
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/dashboard.js +133 -6
  8. package/dist/commands/dashboard.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +35 -1
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/generators/mgrep-setup.d.ts +6 -0
  13. package/dist/generators/mgrep-setup.d.ts.map +1 -0
  14. package/dist/generators/mgrep-setup.js +191 -0
  15. package/dist/generators/mgrep-setup.js.map +1 -0
  16. package/dist/generators/mgrep-skill.d.ts +6 -0
  17. package/dist/generators/mgrep-skill.d.ts.map +1 -0
  18. package/dist/generators/mgrep-skill.js +173 -0
  19. package/dist/generators/mgrep-skill.js.map +1 -0
  20. package/dist/generators/uca-functions.d.ts +8 -0
  21. package/dist/generators/uca-functions.d.ts.map +1 -0
  22. package/dist/generators/uca-functions.js +57 -0
  23. package/dist/generators/uca-functions.js.map +1 -0
  24. package/dist/generators/uca-reexports.d.ts +8 -0
  25. package/dist/generators/uca-reexports.d.ts.map +1 -0
  26. package/dist/generators/uca-reexports.js +112 -0
  27. package/dist/generators/uca-reexports.js.map +1 -0
  28. package/dist/generators/uca-schema.d.ts +8 -0
  29. package/dist/generators/uca-schema.d.ts.map +1 -0
  30. package/dist/generators/uca-schema.js +650 -0
  31. package/dist/generators/uca-schema.js.map +1 -0
  32. package/dist/index.js +3 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/presets/research.d.ts.map +1 -1
  35. package/dist/presets/research.js +10 -0
  36. package/dist/presets/research.js.map +1 -1
  37. package/dist/presets/software.d.ts.map +1 -1
  38. package/dist/presets/software.js +10 -0
  39. package/dist/presets/software.js.map +1 -1
  40. package/dist/utils/detect.d.ts.map +1 -1
  41. package/dist/utils/detect.js +15 -0
  42. package/dist/utils/detect.js.map +1 -1
  43. package/dist/utils/merge.d.ts.map +1 -1
  44. package/dist/utils/merge.js +2 -0
  45. package/dist/utils/merge.js.map +1 -1
  46. package/package.json +5 -3
  47. package/templates/uca/agents.ts +59 -0
  48. package/templates/uca/contextEntries.ts +125 -0
  49. package/templates/uca/cronManager.ts +255 -0
  50. package/templates/uca/cronUtils.ts +99 -0
  51. package/templates/uca/driftEvents.ts +106 -0
  52. package/templates/uca/heartbeats.ts +167 -0
  53. package/templates/uca/hooks.ts +430 -0
  54. package/templates/uca/memory.ts +326 -0
  55. package/templates/uca/sessionBridge.ts +238 -0
  56. package/templates/uca/skills.ts +284 -0
  57. package/templates/uca/ucaTasks.ts +500 -0
@@ -0,0 +1,500 @@
1
+ // ============================================================
2
+ // UCA Tasks — Full task CRUD with commit tracking & audit trail
3
+ // Generated by @yattalo/task-system — matches dashboard SPA API surface
4
+ // ============================================================
5
+
6
+ import { mutation, query } from "../_generated/server";
7
+ import { v } from "convex/values";
8
+
9
+ function makeId(prefix: string): string {
10
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11
+ }
12
+
13
+ function extractHashFromDescription(description?: string): string | undefined {
14
+ if (!description) return undefined;
15
+ const commitMatch = description.match(/\bcommit\s+([0-9a-f]{7,40})\b/i);
16
+ if (commitMatch?.[1]) return commitMatch[1].toLowerCase();
17
+ const trailerMatch = description.match(/\btask-done:\s*([0-9a-f]{7,40})\b/i);
18
+ if (trailerMatch?.[1]) return trailerMatch[1].toLowerCase();
19
+ return undefined;
20
+ }
21
+
22
+ function inferCommitHash(task: any, note?: string, explicit?: string): string | undefined {
23
+ if (explicit) return explicit.toLowerCase();
24
+ const fromNote = extractHashFromDescription(note);
25
+ if (fromNote) return fromNote;
26
+ const fromDescription = extractHashFromDescription(task.description);
27
+ if (fromDescription) return fromDescription;
28
+ const commits = Array.isArray(task.commits) ? task.commits : [];
29
+ if (commits.length > 0) {
30
+ const latest = [...commits].sort((a, b) => b.timestamp - a.timestamp)[0];
31
+ if (latest?.hash) return latest.hash.toLowerCase();
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ async function findTaskByTaskId(ctx: any, taskId: string): Promise<any | null> {
37
+ return await ctx.db.query("tasks").withIndex("by_taskId", (q: any) => q.eq("taskId", taskId)).first();
38
+ }
39
+
40
+ async function resolveTask(ctx: any, locator: { id?: any; taskId?: string }): Promise<any> {
41
+ if (locator.id) {
42
+ const task = await ctx.db.get(locator.id);
43
+ if (!task) throw new Error("Task not found");
44
+ return task;
45
+ }
46
+ if (locator.taskId) {
47
+ const task = await findTaskByTaskId(ctx, locator.taskId);
48
+ if (!task) throw new Error(`Task ${locator.taskId} not found`);
49
+ return task;
50
+ }
51
+ throw new Error("Either id or taskId must be provided");
52
+ }
53
+
54
+ async function insertCommitCaptureIfMissing(ctx: any, args: {
55
+ taskId: string;
56
+ hash: string;
57
+ message: string;
58
+ source: "status_done" | "hook" | "manual" | "backfill";
59
+ filesChanged?: string[];
60
+ capturedBy?: string;
61
+ capturedAt?: number;
62
+ }): Promise<void> {
63
+ const existing = await ctx.db
64
+ .query("commitCaptures")
65
+ .withIndex("by_taskId", (q: any) => q.eq("taskId", args.taskId))
66
+ .collect();
67
+ const duplicate = existing.some((capture: any) => capture.commitHash === args.hash);
68
+ if (duplicate) return;
69
+ await ctx.db.insert("commitCaptures", {
70
+ captureId: makeId("capture"),
71
+ taskId: args.taskId,
72
+ commitHash: args.hash,
73
+ message: args.message,
74
+ source: args.source,
75
+ filesChanged: args.filesChanged,
76
+ capturedBy: args.capturedBy,
77
+ capturedAt: args.capturedAt ?? Date.now(),
78
+ });
79
+ }
80
+
81
+ async function resolvePendingCommitRequests(ctx: any, taskId: string): Promise<number> {
82
+ const pending = await ctx.db
83
+ .query("pendingCommitCaptures")
84
+ .withIndex("by_taskId", (q: any) => q.eq("taskId", taskId))
85
+ .collect();
86
+ const unresolved = pending.filter((entry: any) => entry.status === "pending");
87
+ for (const entry of unresolved) {
88
+ await ctx.db.patch(entry._id, { status: "resolved", resolvedAt: Date.now() });
89
+ }
90
+ return unresolved.length;
91
+ }
92
+
93
+ async function ensurePendingCommitRequest(ctx: any, taskId: string, note?: string): Promise<void> {
94
+ const pending = await ctx.db
95
+ .query("pendingCommitCaptures")
96
+ .withIndex("by_taskId", (q: any) => q.eq("taskId", taskId))
97
+ .collect();
98
+ const unresolved = pending.find((entry: any) => entry.status === "pending");
99
+ if (unresolved) return;
100
+ await ctx.db.insert("pendingCommitCaptures", {
101
+ requestId: makeId("pcc"),
102
+ taskId,
103
+ status: "pending",
104
+ note,
105
+ requestedAt: Date.now(),
106
+ resolvedAt: undefined,
107
+ });
108
+ }
109
+
110
+ async function applyStatusTransition(
111
+ ctx: any,
112
+ task: any,
113
+ args: {
114
+ status: string;
115
+ agent?: string;
116
+ note?: string;
117
+ commitHash?: string;
118
+ commitMessage?: string;
119
+ filesChanged?: string[];
120
+ },
121
+ ): Promise<{ commitHash?: string; pendingCommitCapture: boolean }> {
122
+ const now = Date.now();
123
+ const history = Array.isArray(task.statusHistory) ? task.statusHistory : [];
124
+
125
+ const commitHash =
126
+ args.status === "done"
127
+ ? inferCommitHash(task, args.note, args.commitHash)
128
+ : args.commitHash;
129
+
130
+ const patch: Record<string, any> = {
131
+ status: args.status,
132
+ updatedAt: now,
133
+ statusHistory: [
134
+ ...history,
135
+ {
136
+ status: args.status,
137
+ timestamp: now,
138
+ agent: args.agent ?? task.agent,
139
+ note: args.note,
140
+ commitHash,
141
+ },
142
+ ],
143
+ };
144
+
145
+ if (args.status === "in_progress" && !task.startedAt) {
146
+ patch.startedAt = now;
147
+ }
148
+
149
+ if (args.status === "done") {
150
+ patch.completedAt = now;
151
+ if (task.startedAt) {
152
+ patch.actualHours = Math.round(((now - task.startedAt) / 3_600_000) * 100) / 100;
153
+ }
154
+ }
155
+
156
+ if (commitHash && args.status === "done") {
157
+ const commits = Array.isArray(task.commits) ? task.commits : [];
158
+ const duplicate = commits.some((commit: any) => commit.hash === commitHash);
159
+ if (!duplicate) {
160
+ patch.commits = [
161
+ ...commits,
162
+ {
163
+ hash: commitHash,
164
+ message: args.commitMessage ?? `Task ${task.taskId} completed`,
165
+ timestamp: now,
166
+ filesChanged: args.filesChanged,
167
+ agent: args.agent ?? task.agent,
168
+ },
169
+ ];
170
+ }
171
+ }
172
+
173
+ await ctx.db.patch(task._id, patch);
174
+
175
+ if (args.status === "done") {
176
+ if (commitHash) {
177
+ await insertCommitCaptureIfMissing(ctx, {
178
+ taskId: task.taskId,
179
+ hash: commitHash,
180
+ message: args.commitMessage ?? `Task ${task.taskId} completed`,
181
+ source: "status_done",
182
+ filesChanged: args.filesChanged,
183
+ capturedBy: args.agent ?? task.agent,
184
+ capturedAt: now,
185
+ });
186
+ await resolvePendingCommitRequests(ctx, task.taskId);
187
+ return { commitHash, pendingCommitCapture: false };
188
+ }
189
+ await ensurePendingCommitRequest(ctx, task.taskId, args.note ?? "Task marked done without commit hash");
190
+ return { pendingCommitCapture: true };
191
+ }
192
+
193
+ return { commitHash, pendingCommitCapture: false };
194
+ }
195
+
196
+ // ── Queries ────────────────────────────────────────────────
197
+
198
+ export const list = query({
199
+ handler: async (ctx) => {
200
+ return await ctx.db.query("tasks").collect();
201
+ },
202
+ });
203
+
204
+ export const getByTaskId = query({
205
+ args: { taskId: v.string() },
206
+ handler: async (ctx, { taskId }) => {
207
+ return await findTaskByTaskId(ctx, taskId);
208
+ },
209
+ });
210
+
211
+ export const stats = query({
212
+ handler: async (ctx) => {
213
+ const all = await ctx.db.query("tasks").collect();
214
+ const byStatus: Record<string, number> = {};
215
+ const byAgent: Record<string, number> = {};
216
+ const byPhase: Record<string, number> = {};
217
+
218
+ for (const task of all) {
219
+ byStatus[task.status] = (byStatus[task.status] ?? 0) + 1;
220
+ byAgent[task.agent] = (byAgent[task.agent] ?? 0) + 1;
221
+ if (task.phaseId) byPhase[task.phaseId] = (byPhase[task.phaseId] ?? 0) + 1;
222
+ }
223
+
224
+ const done = all.filter((task) => task.status === "done");
225
+ const withCommits = done.filter((task) => Array.isArray(task.commits) && task.commits.length > 0);
226
+ const pendingCaptures = await ctx.db
227
+ .query("pendingCommitCaptures")
228
+ .withIndex("by_status", (q: any) => q.eq("status", "pending"))
229
+ .collect();
230
+
231
+ return {
232
+ total: all.length,
233
+ byStatus,
234
+ byAgent,
235
+ byPhase,
236
+ commitCoverage: { done: done.length, withCommits: withCommits.length },
237
+ pendingCommitCaptures: pendingCaptures.length,
238
+ };
239
+ },
240
+ });
241
+
242
+ // ── Mutations ──────────────────────────────────────────────
243
+
244
+ export const create = mutation({
245
+ args: {
246
+ taskId: v.string(),
247
+ projectId: v.string(),
248
+ agent: v.string(),
249
+ title: v.string(),
250
+ description: v.string(),
251
+ status: v.string(),
252
+ priority: v.string(),
253
+ category: v.optional(v.string()),
254
+ phaseId: v.optional(v.string()),
255
+ tags: v.optional(v.array(v.string())),
256
+ dependencies: v.optional(v.array(v.string())),
257
+ },
258
+ handler: async (ctx, args) => {
259
+ const now = Date.now();
260
+ return await ctx.db.insert("tasks", {
261
+ ...args,
262
+ createdAt: now,
263
+ updatedAt: now,
264
+ statusHistory: [{ status: args.status, timestamp: now, agent: args.agent }],
265
+ commits: [],
266
+ } as any);
267
+ },
268
+ });
269
+
270
+ export const upsert = mutation({
271
+ args: {
272
+ taskId: v.string(),
273
+ projectId: v.string(),
274
+ agent: v.string(),
275
+ title: v.string(),
276
+ description: v.string(),
277
+ status: v.string(),
278
+ priority: v.string(),
279
+ category: v.optional(v.string()),
280
+ phaseId: v.optional(v.string()),
281
+ },
282
+ handler: async (ctx, args) => {
283
+ const existing = await findTaskByTaskId(ctx, args.taskId);
284
+ if (existing) return { skipped: true, _id: existing._id };
285
+
286
+ const now = Date.now();
287
+ const id = await ctx.db.insert("tasks", {
288
+ ...args,
289
+ createdAt: now,
290
+ updatedAt: now,
291
+ statusHistory: [{ status: args.status, timestamp: now, agent: args.agent }],
292
+ commits: [],
293
+ } as any);
294
+ return { skipped: false, _id: id };
295
+ },
296
+ });
297
+
298
+ export const updateStatus = mutation({
299
+ args: {
300
+ id: v.optional(v.id("tasks")),
301
+ taskId: v.optional(v.string()),
302
+ status: v.string(),
303
+ agent: v.optional(v.string()),
304
+ note: v.optional(v.string()),
305
+ commitHash: v.optional(v.string()),
306
+ commitMessage: v.optional(v.string()),
307
+ filesChanged: v.optional(v.array(v.string())),
308
+ },
309
+ handler: async (ctx, args) => {
310
+ const task = await resolveTask(ctx, { id: args.id, taskId: args.taskId });
311
+ const result = await applyStatusTransition(ctx, task, {
312
+ status: args.status,
313
+ agent: args.agent,
314
+ note: args.note,
315
+ commitHash: args.commitHash,
316
+ commitMessage: args.commitMessage,
317
+ filesChanged: args.filesChanged,
318
+ });
319
+ return {
320
+ taskId: task.taskId,
321
+ status: args.status,
322
+ commitHash: result.commitHash,
323
+ pendingCommitCapture: result.pendingCommitCapture,
324
+ };
325
+ },
326
+ });
327
+
328
+ export const markDoneWithCommit = mutation({
329
+ args: {
330
+ taskId: v.string(),
331
+ commitHash: v.string(),
332
+ commitMessage: v.optional(v.string()),
333
+ filesChanged: v.optional(v.array(v.string())),
334
+ agent: v.optional(v.string()),
335
+ note: v.optional(v.string()),
336
+ },
337
+ handler: async (ctx, args) => {
338
+ const task = await resolveTask(ctx, { taskId: args.taskId });
339
+ const result = await applyStatusTransition(ctx, task, {
340
+ status: "done",
341
+ agent: args.agent,
342
+ note: args.note ?? "Marked done with explicit commit hash",
343
+ commitHash: args.commitHash,
344
+ commitMessage: args.commitMessage,
345
+ filesChanged: args.filesChanged,
346
+ });
347
+ return { taskId: args.taskId, status: "done", commitHash: result.commitHash };
348
+ },
349
+ });
350
+
351
+ export const captureCommitFromGitHook = mutation({
352
+ args: {
353
+ taskId: v.string(),
354
+ hash: v.string(),
355
+ message: v.string(),
356
+ filesChanged: v.optional(v.array(v.string())),
357
+ capturedBy: v.optional(v.string()),
358
+ markDoneIfNeeded: v.optional(v.boolean()),
359
+ },
360
+ handler: async (ctx, args) => {
361
+ const task = await findTaskByTaskId(ctx, args.taskId);
362
+ if (!task) throw new Error(`Task ${args.taskId} not found`);
363
+
364
+ const now = Date.now();
365
+ const commits = Array.isArray(task.commits) ? task.commits : [];
366
+ const duplicate = commits.some((commit: any) => commit.hash === args.hash);
367
+
368
+ if (!duplicate) {
369
+ await ctx.db.patch(task._id, {
370
+ updatedAt: now,
371
+ commits: [
372
+ ...commits,
373
+ {
374
+ hash: args.hash,
375
+ message: args.message,
376
+ timestamp: now,
377
+ filesChanged: args.filesChanged,
378
+ agent: args.capturedBy ?? task.agent,
379
+ },
380
+ ],
381
+ });
382
+ }
383
+
384
+ await insertCommitCaptureIfMissing(ctx, {
385
+ taskId: args.taskId,
386
+ hash: args.hash,
387
+ message: args.message,
388
+ source: "hook",
389
+ filesChanged: args.filesChanged,
390
+ capturedBy: args.capturedBy,
391
+ capturedAt: now,
392
+ });
393
+
394
+ await resolvePendingCommitRequests(ctx, args.taskId);
395
+
396
+ if ((args.markDoneIfNeeded ?? true) && task.status !== "done") {
397
+ await applyStatusTransition(ctx, task, {
398
+ status: "done",
399
+ agent: args.capturedBy ?? task.agent,
400
+ note: `Auto-marked done by hook with commit ${args.hash}`,
401
+ commitHash: args.hash,
402
+ commitMessage: args.message,
403
+ filesChanged: args.filesChanged,
404
+ });
405
+ }
406
+
407
+ return { taskId: args.taskId, hash: args.hash, attached: !duplicate };
408
+ },
409
+ });
410
+
411
+ export const addCommit = mutation({
412
+ args: {
413
+ id: v.optional(v.id("tasks")),
414
+ taskId: v.optional(v.string()),
415
+ hash: v.string(),
416
+ message: v.string(),
417
+ filesChanged: v.optional(v.array(v.string())),
418
+ agent: v.optional(v.string()),
419
+ },
420
+ handler: async (ctx, args) => {
421
+ const task = await resolveTask(ctx, { id: args.id, taskId: args.taskId });
422
+ const commits = Array.isArray(task.commits) ? task.commits : [];
423
+ const duplicate = commits.some((commit: any) => commit.hash === args.hash);
424
+
425
+ if (!duplicate) {
426
+ await ctx.db.patch(task._id, {
427
+ updatedAt: Date.now(),
428
+ commits: [
429
+ ...commits,
430
+ {
431
+ hash: args.hash,
432
+ message: args.message,
433
+ timestamp: Date.now(),
434
+ filesChanged: args.filesChanged,
435
+ agent: args.agent,
436
+ },
437
+ ],
438
+ });
439
+ }
440
+
441
+ await insertCommitCaptureIfMissing(ctx, {
442
+ taskId: task.taskId,
443
+ hash: args.hash,
444
+ message: args.message,
445
+ source: "manual",
446
+ filesChanged: args.filesChanged,
447
+ capturedBy: args.agent,
448
+ });
449
+
450
+ await resolvePendingCommitRequests(ctx, task.taskId);
451
+ return { taskId: task.taskId, duplicate };
452
+ },
453
+ });
454
+
455
+ export const addBlocker = mutation({
456
+ args: {
457
+ id: v.id("tasks"),
458
+ reason: v.string(),
459
+ },
460
+ handler: async (ctx, { id, reason }) => {
461
+ const task = await ctx.db.get(id);
462
+ if (!task) throw new Error("Task not found");
463
+ const blockers = task.blockers ?? [];
464
+ await ctx.db.patch(id, {
465
+ updatedAt: Date.now(),
466
+ blockers: [...blockers, { reason, createdAt: Date.now() }],
467
+ });
468
+ },
469
+ });
470
+
471
+ export const resolveBlocker = mutation({
472
+ args: {
473
+ id: v.id("tasks"),
474
+ blockerIndex: v.number(),
475
+ resolvedBy: v.string(),
476
+ },
477
+ handler: async (ctx, { id, blockerIndex, resolvedBy }) => {
478
+ const task = await ctx.db.get(id);
479
+ if (!task) throw new Error("Task not found");
480
+ const blockers = [...(task.blockers ?? [])];
481
+ if (blockerIndex >= 0 && blockerIndex < blockers.length) {
482
+ blockers[blockerIndex] = {
483
+ ...blockers[blockerIndex],
484
+ resolvedAt: Date.now(),
485
+ resolvedBy,
486
+ };
487
+ }
488
+ await ctx.db.patch(id, { updatedAt: Date.now(), blockers });
489
+ },
490
+ });
491
+
492
+ export const updateNotes = mutation({
493
+ args: {
494
+ id: v.id("tasks"),
495
+ notes: v.string(),
496
+ },
497
+ handler: async (ctx, { id, notes }) => {
498
+ await ctx.db.patch(id, { notes, updatedAt: Date.now() });
499
+ },
500
+ });