@h-rig/task-io-plugin 0.0.0-e2e-live.20260630085347

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 ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/task-io-plugin
@@ -0,0 +1,5 @@
1
+ import { type RigPlugin } from "@rig/core/config";
2
+ export declare const TASK_IO_PLUGIN_NAME = "@rig/task-io-plugin";
3
+ export declare const taskIoPlugin: RigPlugin;
4
+ export declare function createTaskIoPlugin(): RigPlugin;
5
+ export default taskIoPlugin;
@@ -0,0 +1,696 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+
18
+ // packages/task-io-plugin/src/service.ts
19
+ var exports_service = {};
20
+ __export(exports_service, {
21
+ createTaskIoService: () => createTaskIoService
22
+ });
23
+ import { defineCapability } from "@rig/core/capability";
24
+ import { requireCapabilityForRoot } from "@rig/core/capability-loaders";
25
+ import { resolvePluginHost } from "@rig/core/project-plugins";
26
+ import {
27
+ REPO_CHANGE_SET,
28
+ TASK_ARTIFACTS,
29
+ TASK_STATE_STORE
30
+ } from "@rig/contracts";
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ function taskText(value) {
35
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
36
+ }
37
+ function taskUrl(record) {
38
+ const metadata = record;
39
+ return taskText(metadata.url) ?? taskText(metadata.html_url) ?? taskText(metadata.webUrl);
40
+ }
41
+ function taskBody(record) {
42
+ const metadata = record;
43
+ return taskText(metadata.body) ?? taskText(metadata.description);
44
+ }
45
+ function taskTitle(record) {
46
+ const metadata = record;
47
+ return taskText(metadata.title) ?? taskText(metadata.name) ?? record.id;
48
+ }
49
+ function taskStringList(value) {
50
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
51
+ }
52
+ function taskDependencies(record) {
53
+ const raw = record;
54
+ const dependencies = taskStringList(raw.dependencies);
55
+ if (dependencies.length > 0)
56
+ return dependencies;
57
+ const deps = taskStringList(raw.deps);
58
+ return deps.length > 0 ? deps : null;
59
+ }
60
+ function taskParentChildDeps(record) {
61
+ const raw = record;
62
+ const parentChildDeps = taskStringList(raw.parentChildDeps);
63
+ if (parentChildDeps.length > 0)
64
+ return parentChildDeps;
65
+ const parents = taskStringList(raw.parents);
66
+ return parents.length > 0 ? parents : null;
67
+ }
68
+ function readTaskStatus(task) {
69
+ return typeof task.status === "string" ? task.status.trim().toLowerCase() : "";
70
+ }
71
+ function currentLogin(env = process.env) {
72
+ for (const key of ["RIG_GITHUB_LOGIN", "GITHUB_ACTOR", "GH_USER", "USER", "USERNAME"]) {
73
+ const value = env[key]?.trim();
74
+ if (value)
75
+ return value;
76
+ }
77
+ return;
78
+ }
79
+ function resolveAssignee(value, env = process.env) {
80
+ const trimmed = value?.trim();
81
+ if (!trimmed)
82
+ return;
83
+ const lowered = trimmed.toLowerCase();
84
+ if (lowered === "me" || lowered === "@me")
85
+ return currentLogin(env) ?? "@me";
86
+ return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
87
+ }
88
+ function taskMatchesState(task, state) {
89
+ if (!state)
90
+ return true;
91
+ const closed = CLOSED_STATUSES[readTaskStatus(task)] === true;
92
+ return state === "closed" ? closed : !closed;
93
+ }
94
+ function taskMatchesSearch(task, search) {
95
+ const needle = search?.trim().toLowerCase();
96
+ if (!needle)
97
+ return true;
98
+ const record = task;
99
+ const title = typeof task.title === "string" ? task.title : "";
100
+ const body = typeof record.body === "string" ? record.body : "";
101
+ const description = typeof record.description === "string" ? record.description : "";
102
+ return `${task.id}
103
+ ${title}
104
+ ${body}
105
+ ${description}`.toLowerCase().includes(needle);
106
+ }
107
+ function collectAssigneeLogins(value) {
108
+ if (typeof value === "string")
109
+ return [value];
110
+ if (Array.isArray(value))
111
+ return value.flatMap((entry) => collectAssigneeLogins(entry));
112
+ if (isRecord(value))
113
+ return [value.login, value.username, value.name, value.id].flatMap((entry) => collectAssigneeLogins(entry));
114
+ return [];
115
+ }
116
+ function taskAssignees(task) {
117
+ const record = task;
118
+ const raw = isRecord(record.raw) ? record.raw : null;
119
+ return [
120
+ record.assignee,
121
+ record.assignees,
122
+ record.assignedTo,
123
+ record.owner,
124
+ raw?.assignee,
125
+ raw?.assignees,
126
+ raw?.assignedTo,
127
+ raw?.owner
128
+ ].flatMap((value) => collectAssigneeLogins(value));
129
+ }
130
+ function taskMatchesAssignee(task, assignee) {
131
+ if (!assignee)
132
+ return true;
133
+ const needle = assignee.startsWith("@") ? assignee.slice(1) : assignee;
134
+ return taskAssignees(task).some((login) => {
135
+ const normalized = login.startsWith("@") ? login.slice(1) : login;
136
+ return normalized.localeCompare(needle, undefined, { sensitivity: "accent" }) === 0;
137
+ });
138
+ }
139
+ function applyTaskIoFilters(tasks, filters = {}) {
140
+ const filtered = tasks.filter((task) => taskMatchesAssignee(task, resolveAssignee(filters.assignee))).filter((task) => taskMatchesState(task, filters.state)).filter((task) => taskMatchesSearch(task, filters.search));
141
+ return filters.limit === undefined ? filtered : filtered.slice(0, filters.limit);
142
+ }
143
+ function taskCreateInput(task) {
144
+ const deps = Array.isArray(task.dependencies) ? task.dependencies.filter((entry) => typeof entry === "string") : Array.isArray(task.deps) ? task.deps.filter((entry) => typeof entry === "string") : [];
145
+ const parents = Array.isArray(task.parents) ? task.parents.filter((entry) => typeof entry === "string") : [];
146
+ return {
147
+ title: taskText(task.title) ?? taskText(task.name) ?? "Untitled task",
148
+ body: taskText(task.body) ?? taskText(task.description) ?? "",
149
+ ...deps.length > 0 ? { deps } : {},
150
+ ...parents.length > 0 ? { parents } : {},
151
+ metadata: { ...task }
152
+ };
153
+ }
154
+ function toRigTask(record, sourceKind) {
155
+ const dependencies = taskDependencies(record);
156
+ const parentChildDeps = taskParentChildDeps(record);
157
+ return {
158
+ ...record,
159
+ id: record.id,
160
+ title: taskTitle(record),
161
+ status: typeof record.status === "string" ? record.status : null,
162
+ source: sourceKind,
163
+ url: taskUrl(record),
164
+ body: taskBody(record),
165
+ ...dependencies ? { dependencies } : {},
166
+ ...parentChildDeps ? { parentChildDeps } : {}
167
+ };
168
+ }
169
+ async function loadTaskSource(projectRoot) {
170
+ const { config, host: pluginHost } = await resolvePluginHost(projectRoot);
171
+ const factory = pluginHost.resolveTaskSourceFactoryByKind(config.taskSource.kind);
172
+ if (!factory) {
173
+ const kinds = pluginHost.listExecutableTaskSources().map((entry) => entry.kind).join(", ") || "none";
174
+ throw new Error(`No task source factory registered for kind ${config.taskSource.kind}. Registered task sources: ${kinds}.`);
175
+ }
176
+ return { kind: config.taskSource.kind, source: factory.factory(config.taskSource, { projectRoot }) };
177
+ }
178
+ function getTaskCreator(source) {
179
+ const record = source;
180
+ if (typeof record.createTask === "function")
181
+ return record.createTask.bind(source);
182
+ if (typeof record.create === "function")
183
+ return (task) => record.create?.(taskCreateInput(task));
184
+ return null;
185
+ }
186
+ async function loadTaskArtifacts(projectRoot) {
187
+ return requireCapabilityForRoot(projectRoot, TaskArtifactsCap, "No task artifacts capability provides task artifact writes for this project root.");
188
+ }
189
+ async function loadTaskStateStore(projectRoot) {
190
+ return requireCapabilityForRoot(projectRoot, TaskStateStoreCap, "No task state store capability provides task fact writes for this project root.");
191
+ }
192
+ async function loadRepoChangeSet(projectRoot) {
193
+ return requireCapabilityForRoot(projectRoot, RepoChangeSetCap, "No repo change-set capability provides task artifact change manifests for this project root.");
194
+ }
195
+ async function repoChangeSetPaths(result) {
196
+ const entries = await Promise.resolve(result);
197
+ return Array.from(new Set(entries.map((entry) => entry.path).filter(Boolean)));
198
+ }
199
+ async function resolveActiveTaskId(projectRoot, taskId) {
200
+ const stateStore = await loadTaskStateStore(projectRoot);
201
+ const activeTask = taskId ?? stateStore.readCurrentTaskId(projectRoot);
202
+ if (!activeTask)
203
+ throw new Error("No active task.");
204
+ return activeTask;
205
+ }
206
+ function createTaskIoService() {
207
+ return {
208
+ async listTasks(projectRoot, filters) {
209
+ const { kind, source } = await loadTaskSource(projectRoot);
210
+ return applyTaskIoFilters((await source.list()).map((task) => toRigTask(task, kind)), filters);
211
+ },
212
+ async getTask(projectRoot, taskId) {
213
+ const { kind, source } = await loadTaskSource(projectRoot);
214
+ const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
215
+ return task ? toRigTask(task, kind) : null;
216
+ },
217
+ async createTask(projectRoot, task) {
218
+ const { kind, source } = await loadTaskSource(projectRoot);
219
+ const creator = getTaskCreator(source);
220
+ if (!creator)
221
+ throw new Error(`The configured ${kind} task source does not expose a task creation API.`);
222
+ const result = await creator(task);
223
+ const taskId = typeof result === "string" ? result : isRecord(result) && typeof result.id === "string" ? result.id : null;
224
+ return { taskId, source: kind, result };
225
+ },
226
+ async recordTaskFact(projectRoot, input) {
227
+ const taskId = await resolveActiveTaskId(projectRoot, input.taskId);
228
+ if (input.type === "decision") {
229
+ const result2 = await (await loadTaskArtifacts(projectRoot)).appendDecision({ projectRoot, taskId, text: input.text });
230
+ return { taskId: result2.taskId, message: result2.message };
231
+ }
232
+ const result = (await loadTaskStateStore(projectRoot)).recordFailedApproach({ projectRoot, taskId, text: input.text });
233
+ return { taskId: result.taskId, message: result.message };
234
+ },
235
+ async prepareTaskArtifacts(projectRoot, taskId) {
236
+ const activeTask = await resolveActiveTaskId(projectRoot, taskId);
237
+ const repoChangeSet = await loadRepoChangeSet(projectRoot);
238
+ const changedFiles = await repoChangeSetPaths(repoChangeSet.changedFiles({ projectRoot, selector: { kind: "task", taskId: activeTask, scoped: true } }));
239
+ return (await loadTaskArtifacts(projectRoot)).prepareCompletionArtifacts({ projectRoot, taskId: activeTask, changedFiles });
240
+ }
241
+ };
242
+ }
243
+ var CLOSED_STATUSES, RepoChangeSetCap, TaskArtifactsCap, TaskStateStoreCap;
244
+ var init_service = __esm(() => {
245
+ CLOSED_STATUSES = {
246
+ closed: true,
247
+ completed: true,
248
+ complete: true,
249
+ done: true,
250
+ cancelled: true,
251
+ canceled: true,
252
+ merged: true,
253
+ resolved: true
254
+ };
255
+ RepoChangeSetCap = defineCapability(REPO_CHANGE_SET);
256
+ TaskArtifactsCap = defineCapability(TASK_ARTIFACTS);
257
+ TaskStateStoreCap = defineCapability(TASK_STATE_STORE);
258
+ });
259
+
260
+ // packages/task-io-plugin/src/task-projection.ts
261
+ var exports_task_projection = {};
262
+ __export(exports_task_projection, {
263
+ createTaskProjectionCapability: () => createTaskProjectionCapability
264
+ });
265
+ function isRecord2(value) {
266
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
267
+ }
268
+ function stringList(value) {
269
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
270
+ }
271
+ function unique(values) {
272
+ return Array.from(new Set(values));
273
+ }
274
+ function taskId(task) {
275
+ const id = task?.id.trim();
276
+ return id && id.length > 0 ? id : null;
277
+ }
278
+ function taskTitle2(task) {
279
+ const title = task?.title?.trim();
280
+ return title && title.length > 0 ? title : taskId(task) ?? "Untitled task";
281
+ }
282
+ function taskStatus(task) {
283
+ const status = task?.status?.trim();
284
+ return status && status.length > 0 ? status : "unknown";
285
+ }
286
+ function taskDescription(task) {
287
+ return [task.source, task.url, task.body].flatMap((part) => {
288
+ const text = part?.trim();
289
+ return text ? [text] : [];
290
+ }).join(" \xB7 ");
291
+ }
292
+ function normalizeTaskStatus(value) {
293
+ const normalized = value?.trim().replace(/\s+/g, "_").replace(/-/g, "_");
294
+ return normalized && TASK_STATUSES.has(normalized) ? normalized : "unknown";
295
+ }
296
+ function normalizeTaskPriority(value) {
297
+ if (typeof value === "number" && Number.isFinite(value))
298
+ return value;
299
+ if (typeof value === "string" && value.trim()) {
300
+ const parsed = Number(value);
301
+ return Number.isFinite(parsed) ? parsed : null;
302
+ }
303
+ return null;
304
+ }
305
+ function taskBadgeProjection(task) {
306
+ const metadata = isRecord2(task.metadata) ? task.metadata : {};
307
+ return {
308
+ id: task.id,
309
+ title: taskTitle2(task),
310
+ status: normalizeTaskStatus(task.status),
311
+ priority: normalizeTaskPriority(task.priority ?? (typeof metadata.priority === "number" || typeof metadata.priority === "string" ? metadata.priority : null)),
312
+ metadata,
313
+ source: task.source,
314
+ url: task.url,
315
+ body: task.body,
316
+ assignedTo: task.assignedTo,
317
+ assignees: task.assignees,
318
+ dependencies: [...stringList(task.dependencies), ...stringList(metadata.dependencies)],
319
+ parentChildDeps: [...stringList(task.parentChildDeps), ...stringList(metadata.parentChildDeps)]
320
+ };
321
+ }
322
+ function taskAssigneeText(task) {
323
+ const assignees = [
324
+ ...stringList(task.assignees),
325
+ ...typeof task.assignedTo === "string" ? [task.assignedTo] : stringList(task.assignedTo),
326
+ ...stringList(task.metadata?.assignees),
327
+ ...stringList(task.metadata?.assignedTo)
328
+ ];
329
+ return assignees.length > 0 ? `assigned ${assignees.join(",")}` : "unassigned";
330
+ }
331
+ function readTaskMetadataStringList(task, key) {
332
+ const taskRecord = task;
333
+ const topLevel = stringList(taskRecord[key]);
334
+ if (topLevel.length > 0)
335
+ return topLevel;
336
+ const metadata = isRecord2(task.metadata) ? task.metadata : null;
337
+ const metadataList = stringList(metadata?.[key]);
338
+ if (metadataList.length > 0)
339
+ return metadataList;
340
+ return key === "dependencies" ? stringList(metadata?.deps) : [];
341
+ }
342
+ function readTaskBlockingDependencyRefs(task) {
343
+ return readTaskMetadataStringList(task, "dependencies");
344
+ }
345
+ function readTaskSourceIssueId(task) {
346
+ if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0)
347
+ return task.sourceIssueId;
348
+ const metadata = isRecord2(task.metadata) ? task.metadata : null;
349
+ if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0)
350
+ return metadata.sourceIssueId;
351
+ const rigMetadata = isRecord2(metadata?._rig) ? metadata._rig : null;
352
+ return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
353
+ }
354
+ function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
355
+ if (tasksById.has(ref))
356
+ return ref;
357
+ return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
358
+ }
359
+ function buildTaskReferenceIndex(tasks) {
360
+ return {
361
+ tasksById: new Map(tasks.map((task) => [task.id, task])),
362
+ taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
363
+ taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
364
+ const sourceIssueId = readTaskSourceIssueId(task);
365
+ return sourceIssueId ? [[sourceIssueId, task.id]] : [];
366
+ }))
367
+ };
368
+ }
369
+ function computeTaskBlockingDepths(tasks) {
370
+ const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
371
+ const memo = new Map;
372
+ const visit = (taskIdValue, stack) => {
373
+ const cached = memo.get(taskIdValue);
374
+ if (cached !== undefined)
375
+ return cached;
376
+ if (stack.has(taskIdValue))
377
+ return 0;
378
+ const task = tasksById.get(taskIdValue);
379
+ if (!task)
380
+ return 0;
381
+ stack.add(taskIdValue);
382
+ const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskIdValue);
383
+ const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
384
+ stack.delete(taskIdValue);
385
+ memo.set(taskIdValue, depth);
386
+ return depth;
387
+ };
388
+ for (const task of tasks)
389
+ visit(task.id, new Set);
390
+ return memo;
391
+ }
392
+ function isTaskTerminalStatus(status) {
393
+ switch (status) {
394
+ case "closed":
395
+ case "completed":
396
+ case "done":
397
+ case "cancelled":
398
+ return true;
399
+ default:
400
+ return false;
401
+ }
402
+ }
403
+ function isTaskBlockedStatus(status) {
404
+ return status === "blocked";
405
+ }
406
+ function isTaskRunnableStatus(status) {
407
+ if (!status)
408
+ return false;
409
+ if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
410
+ return false;
411
+ switch (status) {
412
+ case "ready":
413
+ case "open":
414
+ case "failed":
415
+ return true;
416
+ default:
417
+ return false;
418
+ }
419
+ }
420
+ function computeTaskDependencyBadges(tasks) {
421
+ const index = buildTaskReferenceIndex(tasks);
422
+ const blockingDepths = computeTaskBlockingDepths(tasks);
423
+ const dependencyIdsByTask = new Map;
424
+ const unresolvedRefsByTask = new Map;
425
+ const blocksByTask = new Map;
426
+ for (const task of tasks) {
427
+ const dependencyIds = [];
428
+ const unresolvedRefs = [];
429
+ for (const ref of readTaskBlockingDependencyRefs(task)) {
430
+ const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
431
+ if (dependencyId && dependencyId !== task.id) {
432
+ dependencyIds.push(dependencyId);
433
+ const blocks = blocksByTask.get(dependencyId);
434
+ if (blocks)
435
+ blocks.push(task.id);
436
+ else
437
+ blocksByTask.set(dependencyId, [task.id]);
438
+ } else if (ref !== task.id) {
439
+ unresolvedRefs.push(ref);
440
+ }
441
+ }
442
+ dependencyIdsByTask.set(task.id, unique(dependencyIds));
443
+ unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
444
+ }
445
+ const summaries = new Map;
446
+ for (const task of tasks) {
447
+ const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
448
+ const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
449
+ const blockedBy = dependencyIds.filter((dependencyId) => {
450
+ const dependency = index.tasksById.get(dependencyId);
451
+ return dependency ? !isTaskTerminalStatus(dependency.status) : false;
452
+ });
453
+ const blocks = unique(blocksByTask.get(task.id) ?? []);
454
+ const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
455
+ const ready = isTaskRunnableStatus(task.status) && !blocked;
456
+ const badges = [];
457
+ if (blocked) {
458
+ badges.push({
459
+ kind: "blocked",
460
+ label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
461
+ description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
462
+ ...blockedBy.length > 0 ? { count: blockedBy.length } : {},
463
+ taskIds: blockedBy
464
+ });
465
+ } else if (ready) {
466
+ badges.push({ kind: "ready", label: "ready", description: "No open dependencies block this task." });
467
+ }
468
+ if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
469
+ badges.push({
470
+ kind: "dependency",
471
+ label: `deps ${dependencyIds.length}/${blocks.length}`,
472
+ description: [
473
+ dependencyIds.length > 0 ? `depends on ${dependencyIds.join(", ")}` : null,
474
+ blocks.length > 0 ? `blocks ${blocks.join(", ")}` : null,
475
+ unresolvedDependencyRefs.length > 0 ? `unresolved ${unresolvedDependencyRefs.join(", ")}` : null
476
+ ].filter(Boolean).join(" \xB7 "),
477
+ count: dependencyIds.length + blocks.length + unresolvedDependencyRefs.length,
478
+ taskIds: [...dependencyIds, ...blocks]
479
+ });
480
+ }
481
+ summaries.set(task.id, {
482
+ taskId: task.id,
483
+ blockingDepth: blockingDepths.get(task.id) ?? 0,
484
+ dependencyIds,
485
+ unresolvedDependencyRefs,
486
+ blockedBy,
487
+ blocks,
488
+ blocked,
489
+ ready,
490
+ dependencyCount: dependencyIds.length,
491
+ dependentCount: blocks.length,
492
+ badges
493
+ });
494
+ }
495
+ return summaries;
496
+ }
497
+ function projectTaskStatusForGrouping(status) {
498
+ switch (status) {
499
+ case "failed":
500
+ return "ready";
501
+ case "closed":
502
+ return "completed";
503
+ case "running":
504
+ case "in_progress":
505
+ return "running";
506
+ case "blocked":
507
+ return "blocked";
508
+ case "completed":
509
+ case "cancelled":
510
+ case "draft":
511
+ case "queued":
512
+ case "ready":
513
+ case "open":
514
+ return status;
515
+ default:
516
+ return "unknown";
517
+ }
518
+ }
519
+ function groupTasksByProjectedStatus(tasks) {
520
+ const byStatus = new Map;
521
+ for (const task of tasks) {
522
+ const projectedStatus = projectTaskStatusForGrouping(task.status);
523
+ const group = byStatus.get(projectedStatus);
524
+ if (group)
525
+ group.push(task);
526
+ else
527
+ byStatus.set(projectedStatus, [task]);
528
+ }
529
+ const result = [];
530
+ for (const status of TASK_GROUP_STATUS_PRIORITY) {
531
+ const group = byStatus.get(status);
532
+ if (group && group.length > 0)
533
+ result.push({ status, tasks: group });
534
+ }
535
+ const overflowStatuses = Array.from(byStatus.keys()).filter((status) => !TASK_GROUP_STATUS_PRIORITY.includes(status)).sort();
536
+ for (const status of overflowStatuses) {
537
+ const group = byStatus.get(status);
538
+ if (group && group.length > 0)
539
+ result.push({ status, tasks: group });
540
+ }
541
+ return result;
542
+ }
543
+ function assigneeLoginsFromValue(value) {
544
+ if (!Array.isArray(value))
545
+ return [];
546
+ return value.flatMap((entry) => {
547
+ if (typeof entry === "string" && entry.trim())
548
+ return [entry.trim().replace(/^@/, "").toLowerCase()];
549
+ if (isRecord2(entry) && typeof entry.login === "string" && entry.login.trim())
550
+ return [entry.login.trim().replace(/^@/, "").toLowerCase()];
551
+ return [];
552
+ });
553
+ }
554
+ function normalizeTaskAssigneeFilter(assignee, currentUserLogin) {
555
+ const trimmed = assignee?.trim();
556
+ if (!trimmed)
557
+ return null;
558
+ if (trimmed === "@me" || trimmed.toLowerCase() === "me")
559
+ return currentUserLogin?.trim() ? currentUserLogin.trim().replace(/^@/, "").toLowerCase() : null;
560
+ return trimmed.replace(/^@/, "").toLowerCase();
561
+ }
562
+ function readTaskAssigneeLogins(task) {
563
+ const taskRecord = task;
564
+ const metadata = isRecord2(task.metadata) ? task.metadata : null;
565
+ const raw = isRecord2(metadata?.raw) ? metadata.raw : null;
566
+ return Array.from(new Set([
567
+ ...assigneeLoginsFromValue(taskRecord.assignees),
568
+ ...assigneeLoginsFromValue(metadata?.assignees),
569
+ ...assigneeLoginsFromValue(raw?.assignees)
570
+ ]));
571
+ }
572
+ function taskMatchesAssigneeFilter(task, assignee, options = {}) {
573
+ const normalized = normalizeTaskAssigneeFilter(assignee, options.currentUserLogin);
574
+ if (!normalized)
575
+ return false;
576
+ return readTaskAssigneeLogins(task).includes(normalized);
577
+ }
578
+ function selectTasksAssignedToMe(tasks, currentUserLogin) {
579
+ return tasks.filter((task) => taskMatchesAssigneeFilter(task, "@me", currentUserLogin === undefined ? {} : { currentUserLogin }));
580
+ }
581
+ function listRows(input) {
582
+ const tasks = input.tasks;
583
+ if (!tasks)
584
+ return [];
585
+ const projected = tasks.map((task) => taskBadgeProjection(task));
586
+ const visible = input.assignedToMe ? selectTasksAssignedToMe(projected, input.currentUserLogin ?? null) : projected;
587
+ const sourceById = new Map(tasks.map((task) => [task.id, task]));
588
+ const badgesByTaskId = computeTaskDependencyBadges(visible);
589
+ const groups = groupTasksByProjectedStatus(visible);
590
+ const ordered = groups.flatMap((group) => group.tasks).slice(0, input.limit ?? 20);
591
+ return ordered.flatMap((task) => {
592
+ const sourceTask = sourceById.get(task.id);
593
+ if (!sourceTask)
594
+ return [];
595
+ const summary = badgesByTaskId.get(task.id);
596
+ const badge = summary?.badges.find((entry) => entry.kind === "blocked" || entry.kind === "ready")?.label ?? taskStatus(sourceTask);
597
+ return [{
598
+ id: task.id,
599
+ label: taskTitle2(sourceTask),
600
+ currentValue: badge,
601
+ description: [input.nextReadyTaskId === task.id ? "next-ready" : null, taskAssigneeText(sourceTask), summary?.badges.map((entry) => entry.description).join("; "), taskDescription(sourceTask) || task.id].filter(Boolean).join(" \xB7 ")
602
+ }];
603
+ });
604
+ }
605
+ function boardTasks(input) {
606
+ const sourceById = new Map((input.tasks ?? []).map((task) => [task.id, task]));
607
+ return listRows(input).flatMap((row) => {
608
+ const sourceTask = sourceById.get(row.id);
609
+ if (!sourceTask)
610
+ return [];
611
+ return [{
612
+ id: row.id,
613
+ status: taskStatus(sourceTask),
614
+ title: row.label,
615
+ ...row.currentValue ? { badge: row.currentValue } : {},
616
+ ...row.description ? { description: row.description } : {}
617
+ }];
618
+ });
619
+ }
620
+ function stats(tasks) {
621
+ if (!tasks)
622
+ return null;
623
+ return {
624
+ total: tasks.length,
625
+ ready: tasks.filter((task) => taskStatus(task) === "ready").length,
626
+ running: tasks.filter((task) => taskStatus(task) === "running").length
627
+ };
628
+ }
629
+ function createTaskProjectionCapability() {
630
+ return {
631
+ normalizeStatus: normalizeTaskStatus,
632
+ normalizePriority: normalizeTaskPriority,
633
+ project: taskBadgeProjection,
634
+ assigneeText: taskAssigneeText,
635
+ title: taskTitle2,
636
+ id: taskId,
637
+ status: taskStatus,
638
+ description: taskDescription,
639
+ listRows,
640
+ boardTasks,
641
+ stats
642
+ };
643
+ }
644
+ var TASK_STATUSES, TASK_GROUP_STATUS_PRIORITY;
645
+ var init_task_projection = __esm(() => {
646
+ TASK_STATUSES = new Set([
647
+ "draft",
648
+ "open",
649
+ "ready",
650
+ "queued",
651
+ "running",
652
+ "in_progress",
653
+ "under_review",
654
+ "blocked",
655
+ "unknown",
656
+ "completed",
657
+ "failed",
658
+ "cancelled",
659
+ "closed"
660
+ ]);
661
+ TASK_GROUP_STATUS_PRIORITY = ["running", "blocked", "queued", "ready", "open", "draft", "unknown", "cancelled", "completed"];
662
+ });
663
+
664
+ // packages/task-io-plugin/src/plugin.ts
665
+ import { defineCapability as defineCapability2, defineServiceCapability } from "@rig/core/capability";
666
+ import { definePlugin } from "@rig/core/config";
667
+ import { TASK_IO_SERVICE_CAPABILITY, TASK_PROJECTION_CAPABILITY_ID } from "@rig/contracts";
668
+ var TASK_IO_PLUGIN_NAME = "@rig/task-io-plugin";
669
+ var TaskIoCap = defineCapability2(TASK_IO_SERVICE_CAPABILITY);
670
+ var TaskProjectionCap = defineServiceCapability(TASK_PROJECTION_CAPABILITY_ID);
671
+ var taskIoPlugin = definePlugin({
672
+ name: TASK_IO_PLUGIN_NAME,
673
+ version: "0.0.0-alpha.1",
674
+ contributes: {
675
+ capabilities: [
676
+ TaskIoCap.provide(async () => (await Promise.resolve().then(() => (init_service(), exports_service))).createTaskIoService(), {
677
+ title: "Task IO",
678
+ description: "Normalized task-source list/get/create operations for CLI and cockpit consumers. Target owner for the legacy TASK_DATA compatibility facade once provider factories receive host/resolver context."
679
+ }),
680
+ TaskProjectionCap.provideService(async () => (await Promise.resolve().then(() => (init_task_projection(), exports_task_projection))).createTaskProjectionCapability(), {
681
+ title: "Task projection",
682
+ description: "Task-owned title/status/priority/assignee projection for cockpit and CLI surfaces."
683
+ })
684
+ ]
685
+ }
686
+ });
687
+ function createTaskIoPlugin() {
688
+ return taskIoPlugin;
689
+ }
690
+ var plugin_default = taskIoPlugin;
691
+ export {
692
+ taskIoPlugin,
693
+ plugin_default as default,
694
+ createTaskIoPlugin,
695
+ TASK_IO_PLUGIN_NAME
696
+ };
@@ -0,0 +1,6 @@
1
+ import { type RegisteredTaskSource, type TaskIoService } from "@rig/contracts";
2
+ export type TaskSourceHandle = {
3
+ readonly kind: string;
4
+ readonly source: RegisteredTaskSource;
5
+ };
6
+ export declare function createTaskIoService(): TaskIoService;
@@ -0,0 +1,238 @@
1
+ // @bun
2
+ // packages/task-io-plugin/src/service.ts
3
+ import { defineCapability } from "@rig/core/capability";
4
+ import { requireCapabilityForRoot } from "@rig/core/capability-loaders";
5
+ import { resolvePluginHost } from "@rig/core/project-plugins";
6
+ import {
7
+ REPO_CHANGE_SET,
8
+ TASK_ARTIFACTS,
9
+ TASK_STATE_STORE
10
+ } from "@rig/contracts";
11
+ function isRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function taskText(value) {
15
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
16
+ }
17
+ function taskUrl(record) {
18
+ const metadata = record;
19
+ return taskText(metadata.url) ?? taskText(metadata.html_url) ?? taskText(metadata.webUrl);
20
+ }
21
+ function taskBody(record) {
22
+ const metadata = record;
23
+ return taskText(metadata.body) ?? taskText(metadata.description);
24
+ }
25
+ function taskTitle(record) {
26
+ const metadata = record;
27
+ return taskText(metadata.title) ?? taskText(metadata.name) ?? record.id;
28
+ }
29
+ function taskStringList(value) {
30
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
31
+ }
32
+ function taskDependencies(record) {
33
+ const raw = record;
34
+ const dependencies = taskStringList(raw.dependencies);
35
+ if (dependencies.length > 0)
36
+ return dependencies;
37
+ const deps = taskStringList(raw.deps);
38
+ return deps.length > 0 ? deps : null;
39
+ }
40
+ function taskParentChildDeps(record) {
41
+ const raw = record;
42
+ const parentChildDeps = taskStringList(raw.parentChildDeps);
43
+ if (parentChildDeps.length > 0)
44
+ return parentChildDeps;
45
+ const parents = taskStringList(raw.parents);
46
+ return parents.length > 0 ? parents : null;
47
+ }
48
+ var CLOSED_STATUSES = {
49
+ closed: true,
50
+ completed: true,
51
+ complete: true,
52
+ done: true,
53
+ cancelled: true,
54
+ canceled: true,
55
+ merged: true,
56
+ resolved: true
57
+ };
58
+ function readTaskStatus(task) {
59
+ return typeof task.status === "string" ? task.status.trim().toLowerCase() : "";
60
+ }
61
+ function currentLogin(env = process.env) {
62
+ for (const key of ["RIG_GITHUB_LOGIN", "GITHUB_ACTOR", "GH_USER", "USER", "USERNAME"]) {
63
+ const value = env[key]?.trim();
64
+ if (value)
65
+ return value;
66
+ }
67
+ return;
68
+ }
69
+ function resolveAssignee(value, env = process.env) {
70
+ const trimmed = value?.trim();
71
+ if (!trimmed)
72
+ return;
73
+ const lowered = trimmed.toLowerCase();
74
+ if (lowered === "me" || lowered === "@me")
75
+ return currentLogin(env) ?? "@me";
76
+ return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
77
+ }
78
+ function taskMatchesState(task, state) {
79
+ if (!state)
80
+ return true;
81
+ const closed = CLOSED_STATUSES[readTaskStatus(task)] === true;
82
+ return state === "closed" ? closed : !closed;
83
+ }
84
+ function taskMatchesSearch(task, search) {
85
+ const needle = search?.trim().toLowerCase();
86
+ if (!needle)
87
+ return true;
88
+ const record = task;
89
+ const title = typeof task.title === "string" ? task.title : "";
90
+ const body = typeof record.body === "string" ? record.body : "";
91
+ const description = typeof record.description === "string" ? record.description : "";
92
+ return `${task.id}
93
+ ${title}
94
+ ${body}
95
+ ${description}`.toLowerCase().includes(needle);
96
+ }
97
+ function collectAssigneeLogins(value) {
98
+ if (typeof value === "string")
99
+ return [value];
100
+ if (Array.isArray(value))
101
+ return value.flatMap((entry) => collectAssigneeLogins(entry));
102
+ if (isRecord(value))
103
+ return [value.login, value.username, value.name, value.id].flatMap((entry) => collectAssigneeLogins(entry));
104
+ return [];
105
+ }
106
+ function taskAssignees(task) {
107
+ const record = task;
108
+ const raw = isRecord(record.raw) ? record.raw : null;
109
+ return [
110
+ record.assignee,
111
+ record.assignees,
112
+ record.assignedTo,
113
+ record.owner,
114
+ raw?.assignee,
115
+ raw?.assignees,
116
+ raw?.assignedTo,
117
+ raw?.owner
118
+ ].flatMap((value) => collectAssigneeLogins(value));
119
+ }
120
+ function taskMatchesAssignee(task, assignee) {
121
+ if (!assignee)
122
+ return true;
123
+ const needle = assignee.startsWith("@") ? assignee.slice(1) : assignee;
124
+ return taskAssignees(task).some((login) => {
125
+ const normalized = login.startsWith("@") ? login.slice(1) : login;
126
+ return normalized.localeCompare(needle, undefined, { sensitivity: "accent" }) === 0;
127
+ });
128
+ }
129
+ function applyTaskIoFilters(tasks, filters = {}) {
130
+ const filtered = tasks.filter((task) => taskMatchesAssignee(task, resolveAssignee(filters.assignee))).filter((task) => taskMatchesState(task, filters.state)).filter((task) => taskMatchesSearch(task, filters.search));
131
+ return filters.limit === undefined ? filtered : filtered.slice(0, filters.limit);
132
+ }
133
+ function taskCreateInput(task) {
134
+ const deps = Array.isArray(task.dependencies) ? task.dependencies.filter((entry) => typeof entry === "string") : Array.isArray(task.deps) ? task.deps.filter((entry) => typeof entry === "string") : [];
135
+ const parents = Array.isArray(task.parents) ? task.parents.filter((entry) => typeof entry === "string") : [];
136
+ return {
137
+ title: taskText(task.title) ?? taskText(task.name) ?? "Untitled task",
138
+ body: taskText(task.body) ?? taskText(task.description) ?? "",
139
+ ...deps.length > 0 ? { deps } : {},
140
+ ...parents.length > 0 ? { parents } : {},
141
+ metadata: { ...task }
142
+ };
143
+ }
144
+ function toRigTask(record, sourceKind) {
145
+ const dependencies = taskDependencies(record);
146
+ const parentChildDeps = taskParentChildDeps(record);
147
+ return {
148
+ ...record,
149
+ id: record.id,
150
+ title: taskTitle(record),
151
+ status: typeof record.status === "string" ? record.status : null,
152
+ source: sourceKind,
153
+ url: taskUrl(record),
154
+ body: taskBody(record),
155
+ ...dependencies ? { dependencies } : {},
156
+ ...parentChildDeps ? { parentChildDeps } : {}
157
+ };
158
+ }
159
+ async function loadTaskSource(projectRoot) {
160
+ const { config, host: pluginHost } = await resolvePluginHost(projectRoot);
161
+ const factory = pluginHost.resolveTaskSourceFactoryByKind(config.taskSource.kind);
162
+ if (!factory) {
163
+ const kinds = pluginHost.listExecutableTaskSources().map((entry) => entry.kind).join(", ") || "none";
164
+ throw new Error(`No task source factory registered for kind ${config.taskSource.kind}. Registered task sources: ${kinds}.`);
165
+ }
166
+ return { kind: config.taskSource.kind, source: factory.factory(config.taskSource, { projectRoot }) };
167
+ }
168
+ function getTaskCreator(source) {
169
+ const record = source;
170
+ if (typeof record.createTask === "function")
171
+ return record.createTask.bind(source);
172
+ if (typeof record.create === "function")
173
+ return (task) => record.create?.(taskCreateInput(task));
174
+ return null;
175
+ }
176
+ var RepoChangeSetCap = defineCapability(REPO_CHANGE_SET);
177
+ var TaskArtifactsCap = defineCapability(TASK_ARTIFACTS);
178
+ var TaskStateStoreCap = defineCapability(TASK_STATE_STORE);
179
+ async function loadTaskArtifacts(projectRoot) {
180
+ return requireCapabilityForRoot(projectRoot, TaskArtifactsCap, "No task artifacts capability provides task artifact writes for this project root.");
181
+ }
182
+ async function loadTaskStateStore(projectRoot) {
183
+ return requireCapabilityForRoot(projectRoot, TaskStateStoreCap, "No task state store capability provides task fact writes for this project root.");
184
+ }
185
+ async function loadRepoChangeSet(projectRoot) {
186
+ return requireCapabilityForRoot(projectRoot, RepoChangeSetCap, "No repo change-set capability provides task artifact change manifests for this project root.");
187
+ }
188
+ async function repoChangeSetPaths(result) {
189
+ const entries = await Promise.resolve(result);
190
+ return Array.from(new Set(entries.map((entry) => entry.path).filter(Boolean)));
191
+ }
192
+ async function resolveActiveTaskId(projectRoot, taskId) {
193
+ const stateStore = await loadTaskStateStore(projectRoot);
194
+ const activeTask = taskId ?? stateStore.readCurrentTaskId(projectRoot);
195
+ if (!activeTask)
196
+ throw new Error("No active task.");
197
+ return activeTask;
198
+ }
199
+ function createTaskIoService() {
200
+ return {
201
+ async listTasks(projectRoot, filters) {
202
+ const { kind, source } = await loadTaskSource(projectRoot);
203
+ return applyTaskIoFilters((await source.list()).map((task) => toRigTask(task, kind)), filters);
204
+ },
205
+ async getTask(projectRoot, taskId) {
206
+ const { kind, source } = await loadTaskSource(projectRoot);
207
+ const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
208
+ return task ? toRigTask(task, kind) : null;
209
+ },
210
+ async createTask(projectRoot, task) {
211
+ const { kind, source } = await loadTaskSource(projectRoot);
212
+ const creator = getTaskCreator(source);
213
+ if (!creator)
214
+ throw new Error(`The configured ${kind} task source does not expose a task creation API.`);
215
+ const result = await creator(task);
216
+ const taskId = typeof result === "string" ? result : isRecord(result) && typeof result.id === "string" ? result.id : null;
217
+ return { taskId, source: kind, result };
218
+ },
219
+ async recordTaskFact(projectRoot, input) {
220
+ const taskId = await resolveActiveTaskId(projectRoot, input.taskId);
221
+ if (input.type === "decision") {
222
+ const result2 = await (await loadTaskArtifacts(projectRoot)).appendDecision({ projectRoot, taskId, text: input.text });
223
+ return { taskId: result2.taskId, message: result2.message };
224
+ }
225
+ const result = (await loadTaskStateStore(projectRoot)).recordFailedApproach({ projectRoot, taskId, text: input.text });
226
+ return { taskId: result.taskId, message: result.message };
227
+ },
228
+ async prepareTaskArtifacts(projectRoot, taskId) {
229
+ const activeTask = await resolveActiveTaskId(projectRoot, taskId);
230
+ const repoChangeSet = await loadRepoChangeSet(projectRoot);
231
+ const changedFiles = await repoChangeSetPaths(repoChangeSet.changedFiles({ projectRoot, selector: { kind: "task", taskId: activeTask, scoped: true } }));
232
+ return (await loadTaskArtifacts(projectRoot)).prepareCompletionArtifacts({ projectRoot, taskId: activeTask, changedFiles });
233
+ }
234
+ };
235
+ }
236
+ export {
237
+ createTaskIoService
238
+ };
@@ -0,0 +1,2 @@
1
+ import type { TaskProjectionCapability } from "@rig/contracts";
2
+ export declare function createTaskProjectionCapability(): TaskProjectionCapability;
@@ -0,0 +1,400 @@
1
+ // @bun
2
+ // packages/task-io-plugin/src/task-projection.ts
3
+ function isRecord(value) {
4
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
5
+ }
6
+ function stringList(value) {
7
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
8
+ }
9
+ function unique(values) {
10
+ return Array.from(new Set(values));
11
+ }
12
+ function taskId(task) {
13
+ const id = task?.id.trim();
14
+ return id && id.length > 0 ? id : null;
15
+ }
16
+ function taskTitle(task) {
17
+ const title = task?.title?.trim();
18
+ return title && title.length > 0 ? title : taskId(task) ?? "Untitled task";
19
+ }
20
+ function taskStatus(task) {
21
+ const status = task?.status?.trim();
22
+ return status && status.length > 0 ? status : "unknown";
23
+ }
24
+ function taskDescription(task) {
25
+ return [task.source, task.url, task.body].flatMap((part) => {
26
+ const text = part?.trim();
27
+ return text ? [text] : [];
28
+ }).join(" \xB7 ");
29
+ }
30
+ var TASK_STATUSES = new Set([
31
+ "draft",
32
+ "open",
33
+ "ready",
34
+ "queued",
35
+ "running",
36
+ "in_progress",
37
+ "under_review",
38
+ "blocked",
39
+ "unknown",
40
+ "completed",
41
+ "failed",
42
+ "cancelled",
43
+ "closed"
44
+ ]);
45
+ function normalizeTaskStatus(value) {
46
+ const normalized = value?.trim().replace(/\s+/g, "_").replace(/-/g, "_");
47
+ return normalized && TASK_STATUSES.has(normalized) ? normalized : "unknown";
48
+ }
49
+ function normalizeTaskPriority(value) {
50
+ if (typeof value === "number" && Number.isFinite(value))
51
+ return value;
52
+ if (typeof value === "string" && value.trim()) {
53
+ const parsed = Number(value);
54
+ return Number.isFinite(parsed) ? parsed : null;
55
+ }
56
+ return null;
57
+ }
58
+ function taskBadgeProjection(task) {
59
+ const metadata = isRecord(task.metadata) ? task.metadata : {};
60
+ return {
61
+ id: task.id,
62
+ title: taskTitle(task),
63
+ status: normalizeTaskStatus(task.status),
64
+ priority: normalizeTaskPriority(task.priority ?? (typeof metadata.priority === "number" || typeof metadata.priority === "string" ? metadata.priority : null)),
65
+ metadata,
66
+ source: task.source,
67
+ url: task.url,
68
+ body: task.body,
69
+ assignedTo: task.assignedTo,
70
+ assignees: task.assignees,
71
+ dependencies: [...stringList(task.dependencies), ...stringList(metadata.dependencies)],
72
+ parentChildDeps: [...stringList(task.parentChildDeps), ...stringList(metadata.parentChildDeps)]
73
+ };
74
+ }
75
+ function taskAssigneeText(task) {
76
+ const assignees = [
77
+ ...stringList(task.assignees),
78
+ ...typeof task.assignedTo === "string" ? [task.assignedTo] : stringList(task.assignedTo),
79
+ ...stringList(task.metadata?.assignees),
80
+ ...stringList(task.metadata?.assignedTo)
81
+ ];
82
+ return assignees.length > 0 ? `assigned ${assignees.join(",")}` : "unassigned";
83
+ }
84
+ function readTaskMetadataStringList(task, key) {
85
+ const taskRecord = task;
86
+ const topLevel = stringList(taskRecord[key]);
87
+ if (topLevel.length > 0)
88
+ return topLevel;
89
+ const metadata = isRecord(task.metadata) ? task.metadata : null;
90
+ const metadataList = stringList(metadata?.[key]);
91
+ if (metadataList.length > 0)
92
+ return metadataList;
93
+ return key === "dependencies" ? stringList(metadata?.deps) : [];
94
+ }
95
+ function readTaskBlockingDependencyRefs(task) {
96
+ return readTaskMetadataStringList(task, "dependencies");
97
+ }
98
+ function readTaskSourceIssueId(task) {
99
+ if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0)
100
+ return task.sourceIssueId;
101
+ const metadata = isRecord(task.metadata) ? task.metadata : null;
102
+ if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0)
103
+ return metadata.sourceIssueId;
104
+ const rigMetadata = isRecord(metadata?._rig) ? metadata._rig : null;
105
+ return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
106
+ }
107
+ function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
108
+ if (tasksById.has(ref))
109
+ return ref;
110
+ return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
111
+ }
112
+ function buildTaskReferenceIndex(tasks) {
113
+ return {
114
+ tasksById: new Map(tasks.map((task) => [task.id, task])),
115
+ taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
116
+ taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
117
+ const sourceIssueId = readTaskSourceIssueId(task);
118
+ return sourceIssueId ? [[sourceIssueId, task.id]] : [];
119
+ }))
120
+ };
121
+ }
122
+ function computeTaskBlockingDepths(tasks) {
123
+ const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
124
+ const memo = new Map;
125
+ const visit = (taskIdValue, stack) => {
126
+ const cached = memo.get(taskIdValue);
127
+ if (cached !== undefined)
128
+ return cached;
129
+ if (stack.has(taskIdValue))
130
+ return 0;
131
+ const task = tasksById.get(taskIdValue);
132
+ if (!task)
133
+ return 0;
134
+ stack.add(taskIdValue);
135
+ const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskIdValue);
136
+ const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
137
+ stack.delete(taskIdValue);
138
+ memo.set(taskIdValue, depth);
139
+ return depth;
140
+ };
141
+ for (const task of tasks)
142
+ visit(task.id, new Set);
143
+ return memo;
144
+ }
145
+ function isTaskTerminalStatus(status) {
146
+ switch (status) {
147
+ case "closed":
148
+ case "completed":
149
+ case "done":
150
+ case "cancelled":
151
+ return true;
152
+ default:
153
+ return false;
154
+ }
155
+ }
156
+ function isTaskBlockedStatus(status) {
157
+ return status === "blocked";
158
+ }
159
+ function isTaskRunnableStatus(status) {
160
+ if (!status)
161
+ return false;
162
+ if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
163
+ return false;
164
+ switch (status) {
165
+ case "ready":
166
+ case "open":
167
+ case "failed":
168
+ return true;
169
+ default:
170
+ return false;
171
+ }
172
+ }
173
+ function computeTaskDependencyBadges(tasks) {
174
+ const index = buildTaskReferenceIndex(tasks);
175
+ const blockingDepths = computeTaskBlockingDepths(tasks);
176
+ const dependencyIdsByTask = new Map;
177
+ const unresolvedRefsByTask = new Map;
178
+ const blocksByTask = new Map;
179
+ for (const task of tasks) {
180
+ const dependencyIds = [];
181
+ const unresolvedRefs = [];
182
+ for (const ref of readTaskBlockingDependencyRefs(task)) {
183
+ const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
184
+ if (dependencyId && dependencyId !== task.id) {
185
+ dependencyIds.push(dependencyId);
186
+ const blocks = blocksByTask.get(dependencyId);
187
+ if (blocks)
188
+ blocks.push(task.id);
189
+ else
190
+ blocksByTask.set(dependencyId, [task.id]);
191
+ } else if (ref !== task.id) {
192
+ unresolvedRefs.push(ref);
193
+ }
194
+ }
195
+ dependencyIdsByTask.set(task.id, unique(dependencyIds));
196
+ unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
197
+ }
198
+ const summaries = new Map;
199
+ for (const task of tasks) {
200
+ const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
201
+ const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
202
+ const blockedBy = dependencyIds.filter((dependencyId) => {
203
+ const dependency = index.tasksById.get(dependencyId);
204
+ return dependency ? !isTaskTerminalStatus(dependency.status) : false;
205
+ });
206
+ const blocks = unique(blocksByTask.get(task.id) ?? []);
207
+ const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
208
+ const ready = isTaskRunnableStatus(task.status) && !blocked;
209
+ const badges = [];
210
+ if (blocked) {
211
+ badges.push({
212
+ kind: "blocked",
213
+ label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
214
+ description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
215
+ ...blockedBy.length > 0 ? { count: blockedBy.length } : {},
216
+ taskIds: blockedBy
217
+ });
218
+ } else if (ready) {
219
+ badges.push({ kind: "ready", label: "ready", description: "No open dependencies block this task." });
220
+ }
221
+ if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
222
+ badges.push({
223
+ kind: "dependency",
224
+ label: `deps ${dependencyIds.length}/${blocks.length}`,
225
+ description: [
226
+ dependencyIds.length > 0 ? `depends on ${dependencyIds.join(", ")}` : null,
227
+ blocks.length > 0 ? `blocks ${blocks.join(", ")}` : null,
228
+ unresolvedDependencyRefs.length > 0 ? `unresolved ${unresolvedDependencyRefs.join(", ")}` : null
229
+ ].filter(Boolean).join(" \xB7 "),
230
+ count: dependencyIds.length + blocks.length + unresolvedDependencyRefs.length,
231
+ taskIds: [...dependencyIds, ...blocks]
232
+ });
233
+ }
234
+ summaries.set(task.id, {
235
+ taskId: task.id,
236
+ blockingDepth: blockingDepths.get(task.id) ?? 0,
237
+ dependencyIds,
238
+ unresolvedDependencyRefs,
239
+ blockedBy,
240
+ blocks,
241
+ blocked,
242
+ ready,
243
+ dependencyCount: dependencyIds.length,
244
+ dependentCount: blocks.length,
245
+ badges
246
+ });
247
+ }
248
+ return summaries;
249
+ }
250
+ function projectTaskStatusForGrouping(status) {
251
+ switch (status) {
252
+ case "failed":
253
+ return "ready";
254
+ case "closed":
255
+ return "completed";
256
+ case "running":
257
+ case "in_progress":
258
+ return "running";
259
+ case "blocked":
260
+ return "blocked";
261
+ case "completed":
262
+ case "cancelled":
263
+ case "draft":
264
+ case "queued":
265
+ case "ready":
266
+ case "open":
267
+ return status;
268
+ default:
269
+ return "unknown";
270
+ }
271
+ }
272
+ var TASK_GROUP_STATUS_PRIORITY = ["running", "blocked", "queued", "ready", "open", "draft", "unknown", "cancelled", "completed"];
273
+ function groupTasksByProjectedStatus(tasks) {
274
+ const byStatus = new Map;
275
+ for (const task of tasks) {
276
+ const projectedStatus = projectTaskStatusForGrouping(task.status);
277
+ const group = byStatus.get(projectedStatus);
278
+ if (group)
279
+ group.push(task);
280
+ else
281
+ byStatus.set(projectedStatus, [task]);
282
+ }
283
+ const result = [];
284
+ for (const status of TASK_GROUP_STATUS_PRIORITY) {
285
+ const group = byStatus.get(status);
286
+ if (group && group.length > 0)
287
+ result.push({ status, tasks: group });
288
+ }
289
+ const overflowStatuses = Array.from(byStatus.keys()).filter((status) => !TASK_GROUP_STATUS_PRIORITY.includes(status)).sort();
290
+ for (const status of overflowStatuses) {
291
+ const group = byStatus.get(status);
292
+ if (group && group.length > 0)
293
+ result.push({ status, tasks: group });
294
+ }
295
+ return result;
296
+ }
297
+ function assigneeLoginsFromValue(value) {
298
+ if (!Array.isArray(value))
299
+ return [];
300
+ return value.flatMap((entry) => {
301
+ if (typeof entry === "string" && entry.trim())
302
+ return [entry.trim().replace(/^@/, "").toLowerCase()];
303
+ if (isRecord(entry) && typeof entry.login === "string" && entry.login.trim())
304
+ return [entry.login.trim().replace(/^@/, "").toLowerCase()];
305
+ return [];
306
+ });
307
+ }
308
+ function normalizeTaskAssigneeFilter(assignee, currentUserLogin) {
309
+ const trimmed = assignee?.trim();
310
+ if (!trimmed)
311
+ return null;
312
+ if (trimmed === "@me" || trimmed.toLowerCase() === "me")
313
+ return currentUserLogin?.trim() ? currentUserLogin.trim().replace(/^@/, "").toLowerCase() : null;
314
+ return trimmed.replace(/^@/, "").toLowerCase();
315
+ }
316
+ function readTaskAssigneeLogins(task) {
317
+ const taskRecord = task;
318
+ const metadata = isRecord(task.metadata) ? task.metadata : null;
319
+ const raw = isRecord(metadata?.raw) ? metadata.raw : null;
320
+ return Array.from(new Set([
321
+ ...assigneeLoginsFromValue(taskRecord.assignees),
322
+ ...assigneeLoginsFromValue(metadata?.assignees),
323
+ ...assigneeLoginsFromValue(raw?.assignees)
324
+ ]));
325
+ }
326
+ function taskMatchesAssigneeFilter(task, assignee, options = {}) {
327
+ const normalized = normalizeTaskAssigneeFilter(assignee, options.currentUserLogin);
328
+ if (!normalized)
329
+ return false;
330
+ return readTaskAssigneeLogins(task).includes(normalized);
331
+ }
332
+ function selectTasksAssignedToMe(tasks, currentUserLogin) {
333
+ return tasks.filter((task) => taskMatchesAssigneeFilter(task, "@me", currentUserLogin === undefined ? {} : { currentUserLogin }));
334
+ }
335
+ function listRows(input) {
336
+ const tasks = input.tasks;
337
+ if (!tasks)
338
+ return [];
339
+ const projected = tasks.map((task) => taskBadgeProjection(task));
340
+ const visible = input.assignedToMe ? selectTasksAssignedToMe(projected, input.currentUserLogin ?? null) : projected;
341
+ const sourceById = new Map(tasks.map((task) => [task.id, task]));
342
+ const badgesByTaskId = computeTaskDependencyBadges(visible);
343
+ const groups = groupTasksByProjectedStatus(visible);
344
+ const ordered = groups.flatMap((group) => group.tasks).slice(0, input.limit ?? 20);
345
+ return ordered.flatMap((task) => {
346
+ const sourceTask = sourceById.get(task.id);
347
+ if (!sourceTask)
348
+ return [];
349
+ const summary = badgesByTaskId.get(task.id);
350
+ const badge = summary?.badges.find((entry) => entry.kind === "blocked" || entry.kind === "ready")?.label ?? taskStatus(sourceTask);
351
+ return [{
352
+ id: task.id,
353
+ label: taskTitle(sourceTask),
354
+ currentValue: badge,
355
+ description: [input.nextReadyTaskId === task.id ? "next-ready" : null, taskAssigneeText(sourceTask), summary?.badges.map((entry) => entry.description).join("; "), taskDescription(sourceTask) || task.id].filter(Boolean).join(" \xB7 ")
356
+ }];
357
+ });
358
+ }
359
+ function boardTasks(input) {
360
+ const sourceById = new Map((input.tasks ?? []).map((task) => [task.id, task]));
361
+ return listRows(input).flatMap((row) => {
362
+ const sourceTask = sourceById.get(row.id);
363
+ if (!sourceTask)
364
+ return [];
365
+ return [{
366
+ id: row.id,
367
+ status: taskStatus(sourceTask),
368
+ title: row.label,
369
+ ...row.currentValue ? { badge: row.currentValue } : {},
370
+ ...row.description ? { description: row.description } : {}
371
+ }];
372
+ });
373
+ }
374
+ function stats(tasks) {
375
+ if (!tasks)
376
+ return null;
377
+ return {
378
+ total: tasks.length,
379
+ ready: tasks.filter((task) => taskStatus(task) === "ready").length,
380
+ running: tasks.filter((task) => taskStatus(task) === "running").length
381
+ };
382
+ }
383
+ function createTaskProjectionCapability() {
384
+ return {
385
+ normalizeStatus: normalizeTaskStatus,
386
+ normalizePriority: normalizeTaskPriority,
387
+ project: taskBadgeProjection,
388
+ assigneeText: taskAssigneeText,
389
+ title: taskTitle,
390
+ id: taskId,
391
+ status: taskStatus,
392
+ description: taskDescription,
393
+ listRows,
394
+ boardTasks,
395
+ stats
396
+ };
397
+ }
398
+ export {
399
+ createTaskProjectionCapability
400
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@h-rig/task-io-plugin",
3
+ "version": "0.0.0-e2e-live.20260630085347",
4
+ "type": "module",
5
+ "description": "Rig package",
6
+ "license": "UNLICENSED",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/src/plugin.d.ts",
14
+ "import": "./dist/src/plugin.js"
15
+ },
16
+ "./plugin": {
17
+ "types": "./dist/src/plugin.d.ts",
18
+ "import": "./dist/src/plugin.js"
19
+ }
20
+ },
21
+ "engines": {
22
+ "bun": ">=1.3.11"
23
+ },
24
+ "dependencies": {
25
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.0-e2e-live.20260630085347",
26
+ "@rig/core": "npm:@h-rig/core@0.0.0-e2e-live.20260630085347"
27
+ }
28
+ }