trekoon 0.3.6 → 0.3.8
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/.agents/skills/trekoon/SKILL.md +198 -73
- package/.agents/skills/trekoon/reference/execution-with-team.md +9 -11
- package/.agents/skills/trekoon/reference/execution.md +26 -9
- package/.agents/skills/trekoon/reference/planning.md +48 -0
- package/README.md +39 -14
- package/docs/quickstart.md +21 -0
- package/package.json +1 -1
- package/src/board/assets/app.js +19 -25
- package/src/board/assets/components/Notice.js +18 -4
- package/src/board/assets/state/actions.js +6 -6
- package/src/board/assets/state/api.js +155 -31
- package/src/board/assets/state/store.js +38 -6
- package/src/board/assets/state/utils.js +123 -30
- package/src/board/routes.ts +397 -54
- package/src/board/server.ts +57 -4
- package/src/board/snapshot.ts +205 -173
- package/src/commands/board.ts +1 -1
- package/src/commands/events.ts +17 -11
- package/src/commands/quickstart.ts +10 -0
- package/src/commands/subtask.ts +2 -2
- package/src/domain/mutation-service.ts +452 -54
- package/src/domain/tracker-domain.ts +185 -7
- package/src/storage/migrations.ts +123 -0
- package/src/storage/path.ts +12 -1
- package/src/storage/schema.ts +18 -1
- package/src/storage/worktree-recovery.ts +12 -6
- package/src/sync/branch-db.ts +12 -1
- package/src/sync/event-writes.ts +47 -7
- package/src/sync/git-context.ts +10 -6
- package/src/sync/service.ts +759 -151
|
@@ -27,10 +27,23 @@ function normalizeArray(value) {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* @param {{ id?: string }} record
|
|
30
|
-
* @returns {string}
|
|
30
|
+
* @returns {string|null}
|
|
31
31
|
*/
|
|
32
32
|
function getId(record) {
|
|
33
|
-
return typeof record?.id === "string" && record.id.length > 0 ? record.id :
|
|
33
|
+
return typeof record?.id === "string" && record.id.length > 0 ? record.id : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeTimestamp(value, fallback) {
|
|
37
|
+
const normalized = Number(value);
|
|
38
|
+
return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeText(value, fallback = "") {
|
|
42
|
+
return String(value ?? fallback).replace(/\\n/g, "\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeOwner(value) {
|
|
46
|
+
return typeof value === "string" ? value : null;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
/**
|
|
@@ -55,19 +68,27 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
55
68
|
const rawTasks = normalizeArray(rawSnapshot?.tasks);
|
|
56
69
|
const rawSubtasks = normalizeArray(rawSnapshot?.subtasks);
|
|
57
70
|
const rawDependencies = normalizeArray(rawSnapshot?.dependencies);
|
|
71
|
+
|
|
58
72
|
const taskIndex = new Map();
|
|
59
73
|
const subtaskIndex = new Map();
|
|
60
74
|
|
|
61
|
-
const tasks = rawTasks.
|
|
75
|
+
const tasks = rawTasks.flatMap((task) => {
|
|
76
|
+
const taskId = getId(task);
|
|
77
|
+
if (!taskId) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const createdAt = normalizeTimestamp(task.createdAt, Date.now());
|
|
62
82
|
const normalizedTask = {
|
|
63
|
-
id:
|
|
83
|
+
id: taskId,
|
|
64
84
|
kind: "task",
|
|
65
85
|
epicId: task.epicId ?? task.epic?.id ?? null,
|
|
66
|
-
title:
|
|
67
|
-
description:
|
|
86
|
+
title: normalizeText(task.title, "Untitled task"),
|
|
87
|
+
description: normalizeText(task.description),
|
|
68
88
|
status: normalizeStatus(task.status),
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
owner: normalizeOwner(task.owner),
|
|
90
|
+
createdAt,
|
|
91
|
+
updatedAt: normalizeTimestamp(task.updatedAt, createdAt),
|
|
71
92
|
blockedBy: [],
|
|
72
93
|
blocks: [],
|
|
73
94
|
dependencyIds: [],
|
|
@@ -77,19 +98,26 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
77
98
|
};
|
|
78
99
|
|
|
79
100
|
taskIndex.set(normalizedTask.id, normalizedTask);
|
|
80
|
-
return normalizedTask;
|
|
101
|
+
return [normalizedTask];
|
|
81
102
|
});
|
|
82
103
|
|
|
83
|
-
const subtasks = rawSubtasks.
|
|
104
|
+
const subtasks = rawSubtasks.flatMap((subtask) => {
|
|
105
|
+
const subtaskId = getId(subtask);
|
|
106
|
+
if (!subtaskId) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const createdAt = normalizeTimestamp(subtask.createdAt, Date.now());
|
|
84
111
|
const normalizedSubtask = {
|
|
85
|
-
id:
|
|
112
|
+
id: subtaskId,
|
|
86
113
|
kind: "subtask",
|
|
87
114
|
taskId: subtask.taskId ?? subtask.task?.id ?? null,
|
|
88
|
-
title:
|
|
89
|
-
description:
|
|
115
|
+
title: normalizeText(subtask.title, "Untitled subtask"),
|
|
116
|
+
description: normalizeText(subtask.description),
|
|
90
117
|
status: normalizeStatus(subtask.status),
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
owner: normalizeOwner(subtask.owner),
|
|
119
|
+
createdAt,
|
|
120
|
+
updatedAt: normalizeTimestamp(subtask.updatedAt, createdAt),
|
|
93
121
|
blockedBy: [],
|
|
94
122
|
blocks: [],
|
|
95
123
|
dependencyIds: [],
|
|
@@ -98,7 +126,7 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
98
126
|
};
|
|
99
127
|
|
|
100
128
|
subtaskIndex.set(normalizedSubtask.id, normalizedSubtask);
|
|
101
|
-
return normalizedSubtask;
|
|
129
|
+
return [normalizedSubtask];
|
|
102
130
|
});
|
|
103
131
|
|
|
104
132
|
for (const subtask of subtasks) {
|
|
@@ -108,13 +136,22 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
|
|
111
|
-
const dependencies = rawDependencies.
|
|
112
|
-
|
|
113
|
-
sourceId
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
const dependencies = rawDependencies.flatMap((dependency) => {
|
|
140
|
+
const dependencyId = getId(dependency);
|
|
141
|
+
const sourceId = typeof dependency?.sourceId === "string" && dependency.sourceId.length > 0 ? dependency.sourceId : null;
|
|
142
|
+
const dependsOnId = typeof dependency?.dependsOnId === "string" && dependency.dependsOnId.length > 0 ? dependency.dependsOnId : null;
|
|
143
|
+
if (!dependencyId || !sourceId || !dependsOnId) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return [{
|
|
148
|
+
id: dependencyId,
|
|
149
|
+
sourceId,
|
|
150
|
+
sourceKind: dependency.sourceKind === "subtask" ? "subtask" : "task",
|
|
151
|
+
dependsOnId,
|
|
152
|
+
dependsOnKind: dependency.dependsOnKind === "subtask" ? "subtask" : "task",
|
|
153
|
+
}];
|
|
154
|
+
});
|
|
118
155
|
|
|
119
156
|
const lookupNode = (kind, id) => {
|
|
120
157
|
if (kind === "subtask") {
|
|
@@ -136,23 +173,28 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
136
173
|
}
|
|
137
174
|
}
|
|
138
175
|
|
|
139
|
-
const epics = rawEpics.
|
|
176
|
+
const epics = rawEpics.flatMap((epic) => {
|
|
140
177
|
const epicId = getId(epic);
|
|
178
|
+
if (!epicId) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
const epicTasks = tasks.filter((task) => task.epicId === epicId);
|
|
183
|
+
const createdAt = normalizeTimestamp(epic.createdAt, Date.now());
|
|
142
184
|
const normalizedEpic = {
|
|
143
185
|
id: epicId,
|
|
144
186
|
title: String(epic.title ?? "Untitled epic"),
|
|
145
|
-
description:
|
|
187
|
+
description: normalizeText(epic.description),
|
|
146
188
|
status: normalizeStatus(String(epic.status ?? "todo")),
|
|
147
|
-
createdAt
|
|
148
|
-
updatedAt:
|
|
189
|
+
createdAt,
|
|
190
|
+
updatedAt: normalizeTimestamp(epic.updatedAt, createdAt),
|
|
149
191
|
taskIds: epicTasks.map((task) => task.id),
|
|
150
192
|
counts: deriveCounts(epicTasks),
|
|
151
193
|
searchText: "",
|
|
152
194
|
};
|
|
153
195
|
|
|
154
196
|
normalizedEpic.searchText = [normalizedEpic.title, normalizedEpic.description, ...epicTasks.map((task) => task.title)].join(" ").toLowerCase();
|
|
155
|
-
return normalizedEpic;
|
|
197
|
+
return [normalizedEpic];
|
|
156
198
|
});
|
|
157
199
|
|
|
158
200
|
for (const subtask of subtasks) {
|
|
@@ -195,6 +237,51 @@ export function normalizeSnapshot(rawSnapshot) {
|
|
|
195
237
|
};
|
|
196
238
|
}
|
|
197
239
|
|
|
240
|
+
function mergeRecordsById(existingRecords, incomingRecords, deletedIds = []) {
|
|
241
|
+
const deletedIdSet = new Set(deletedIds);
|
|
242
|
+
const nextRecords = existingRecords.filter((record) => !deletedIdSet.has(record.id));
|
|
243
|
+
const indexById = new Map(nextRecords.map((record, index) => [record.id, index]));
|
|
244
|
+
|
|
245
|
+
for (const record of incomingRecords) {
|
|
246
|
+
if (typeof record?.id !== "string" || record.id.length === 0) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const existingIndex = indexById.get(record.id);
|
|
251
|
+
if (existingIndex === undefined) {
|
|
252
|
+
indexById.set(record.id, nextRecords.length);
|
|
253
|
+
nextRecords.push(record);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
nextRecords[existingIndex] = record;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return nextRecords;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function applySnapshotDelta(snapshot, delta) {
|
|
264
|
+
const baseSnapshot = snapshot && typeof snapshot === "object"
|
|
265
|
+
? snapshot
|
|
266
|
+
: { generatedAt: null, epics: [], tasks: [], subtasks: [], dependencies: [] };
|
|
267
|
+
|
|
268
|
+
if (!delta || typeof delta !== "object") {
|
|
269
|
+
return baseSnapshot;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
generatedAt: delta.generatedAt ?? baseSnapshot.generatedAt ?? null,
|
|
274
|
+
epics: mergeRecordsById(baseSnapshot.epics ?? [], normalizeArray(delta.epics), normalizeArray(delta.deletedEpicIds)),
|
|
275
|
+
tasks: mergeRecordsById(baseSnapshot.tasks ?? [], normalizeArray(delta.tasks), normalizeArray(delta.deletedTaskIds)),
|
|
276
|
+
subtasks: mergeRecordsById(baseSnapshot.subtasks ?? [], normalizeArray(delta.subtasks), normalizeArray(delta.deletedSubtaskIds)),
|
|
277
|
+
dependencies: mergeRecordsById(
|
|
278
|
+
baseSnapshot.dependencies ?? [],
|
|
279
|
+
normalizeArray(delta.dependencies),
|
|
280
|
+
normalizeArray(delta.deletedDependencyIds),
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
198
285
|
const dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
199
286
|
|
|
200
287
|
/**
|
|
@@ -204,8 +291,14 @@ const dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium",
|
|
|
204
291
|
* @returns {string}
|
|
205
292
|
*/
|
|
206
293
|
export function formatDate(timestamp) {
|
|
207
|
-
|
|
208
|
-
|
|
294
|
+
const normalized = Number(timestamp);
|
|
295
|
+
if (!Number.isFinite(normalized) || normalized <= 0) return "Unknown";
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
return dateFormatter.format(normalized);
|
|
299
|
+
} catch {
|
|
300
|
+
return "Unknown";
|
|
301
|
+
}
|
|
209
302
|
}
|
|
210
303
|
|
|
211
304
|
/**
|