trekoon 0.4.1 → 0.4.3
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 +97 -765
- package/.agents/skills/trekoon/reference/execution-with-team.md +91 -141
- package/.agents/skills/trekoon/reference/execution.md +188 -159
- package/.agents/skills/trekoon/reference/harness-primitives.md +77 -0
- package/.agents/skills/trekoon/reference/planning.md +213 -213
- package/.agents/skills/trekoon/reference/status-machine.md +21 -0
- package/.agents/skills/trekoon/reference/sync.md +82 -0
- package/README.md +29 -8
- package/docs/ai-agents.md +65 -6
- package/docs/commands.md +149 -5
- package/docs/machine-contracts.md +123 -0
- package/docs/quickstart.md +55 -3
- package/package.json +1 -1
- package/src/board/assets/app.js +47 -13
- package/src/board/assets/components/Component.js +20 -8
- package/src/board/assets/components/Workspace.js +9 -3
- package/src/board/assets/components/helpers.js +4 -0
- package/src/board/assets/runtime/delegation.js +8 -0
- package/src/board/assets/runtime/focus-trap.js +48 -0
- package/src/board/assets/state/actions.js +45 -4
- package/src/board/assets/state/api.js +304 -17
- package/src/board/assets/state/store.js +82 -11
- package/src/board/assets/state/url.js +10 -0
- package/src/board/assets/state/utils.js +2 -1
- package/src/board/event-bus.ts +81 -0
- package/src/board/routes.ts +430 -40
- package/src/board/server.ts +86 -10
- package/src/board/snapshot.ts +6 -0
- package/src/board/wal-watcher.ts +313 -0
- package/src/commands/board.ts +52 -17
- package/src/commands/epic.ts +7 -9
- package/src/commands/error-utils.ts +54 -1
- package/src/commands/help.ts +75 -10
- package/src/commands/migrate.ts +153 -24
- package/src/commands/quickstart.ts +7 -0
- package/src/commands/skills.ts +17 -5
- package/src/commands/subtask.ts +71 -10
- package/src/commands/suggest.ts +6 -13
- package/src/commands/task.ts +137 -88
- package/src/domain/batch-validation.ts +329 -0
- package/src/domain/cascade-planner.ts +412 -0
- package/src/domain/dependency-rules.ts +15 -0
- package/src/domain/mutation-service.ts +842 -187
- package/src/domain/search.ts +113 -0
- package/src/domain/tracker-domain.ts +167 -693
- package/src/domain/types.ts +56 -2
- package/src/export/render-markdown.ts +1 -2
- package/src/index.ts +37 -0
- package/src/runtime/cli-shell.ts +44 -0
- package/src/runtime/daemon.ts +700 -0
- package/src/storage/backup.ts +166 -0
- package/src/storage/database.ts +268 -4
- package/src/storage/migrations.ts +441 -22
- package/src/storage/path.ts +8 -0
- package/src/storage/schema.ts +5 -1
- package/src/sync/event-writes.ts +38 -11
- package/src/sync/git-context.ts +226 -8
- package/src/sync/service.ts +679 -156
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CompactDependencySpec,
|
|
3
|
+
type CompactEntityRef,
|
|
4
|
+
type CompactSubtaskSpec,
|
|
5
|
+
type DependencyRecord,
|
|
6
|
+
DomainError,
|
|
7
|
+
type SubtaskRecord,
|
|
8
|
+
type TaskRecord,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Shared interfaces
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface ResolvedDependencyBatchSpec {
|
|
16
|
+
readonly index: number;
|
|
17
|
+
readonly sourceId: string;
|
|
18
|
+
readonly sourceKind: "task" | "subtask";
|
|
19
|
+
readonly dependsOnId: string;
|
|
20
|
+
readonly dependsOnKind: "task" | "subtask";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DependencyBatchValidationIssue {
|
|
24
|
+
readonly index: number;
|
|
25
|
+
readonly type: "missing_id" | "duplicate" | "cycle";
|
|
26
|
+
readonly sourceId: string;
|
|
27
|
+
readonly dependsOnId: string;
|
|
28
|
+
readonly details: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DependencyBatchResolution {
|
|
32
|
+
readonly spec?: ResolvedDependencyBatchSpec;
|
|
33
|
+
readonly issues: readonly DependencyBatchValidationIssue[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ResolvedCompactEntity {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly kind: "task" | "subtask";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Reader interface — the only DB-touching surface the callers must supply
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export interface BatchValidationReader {
|
|
46
|
+
getTask(id: string): TaskRecord | null;
|
|
47
|
+
getSubtask(id: string): SubtaskRecord | null;
|
|
48
|
+
getDependencyByEdge(sourceId: string, dependsOnId: string): DependencyRecord | null;
|
|
49
|
+
buildDependencyAdjacency(): Map<string, Set<string>>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Helpers
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function assertNonEmptyLocal(field: string, value: string | undefined | null): string {
|
|
57
|
+
const normalized: string = (value ?? "").trim();
|
|
58
|
+
if (!normalized) {
|
|
59
|
+
throw new DomainError({
|
|
60
|
+
code: "invalid_input",
|
|
61
|
+
message: `${field} must be a non-empty string`,
|
|
62
|
+
details: { field },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveNodeKindLocal(id: string, reader: BatchValidationReader): "task" | "subtask" {
|
|
69
|
+
if (reader.getTask(id)) return "task";
|
|
70
|
+
if (reader.getSubtask(id)) return "subtask";
|
|
71
|
+
throw new DomainError({
|
|
72
|
+
code: "not_found",
|
|
73
|
+
message: `node not found: ${id}`,
|
|
74
|
+
details: { id, expectedKinds: ["task", "subtask"] },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveDependencyBatchId(
|
|
79
|
+
reference: CompactEntityRef,
|
|
80
|
+
field: "source" | "dependsOn",
|
|
81
|
+
index: number,
|
|
82
|
+
reader: BatchValidationReader,
|
|
83
|
+
): { readonly id?: string; readonly issues: readonly DependencyBatchValidationIssue[] } {
|
|
84
|
+
if (reference.kind === "temp_key") {
|
|
85
|
+
return {
|
|
86
|
+
issues: [
|
|
87
|
+
{
|
|
88
|
+
index,
|
|
89
|
+
type: "missing_id",
|
|
90
|
+
sourceId: field === "source" ? `@${reference.tempKey}` : "",
|
|
91
|
+
dependsOnId: field === "dependsOn" ? `@${reference.tempKey}` : "",
|
|
92
|
+
details: {
|
|
93
|
+
field,
|
|
94
|
+
tempKey: reference.tempKey,
|
|
95
|
+
message: `Unresolved temp key @${reference.tempKey}`,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const id = assertNonEmptyLocal(field === "source" ? "sourceId" : "dependsOnId", reference.id);
|
|
103
|
+
const task = reader.getTask(id);
|
|
104
|
+
const subtask = reader.getSubtask(id);
|
|
105
|
+
if (!task && !subtask) {
|
|
106
|
+
return {
|
|
107
|
+
issues: [
|
|
108
|
+
{
|
|
109
|
+
index,
|
|
110
|
+
type: "missing_id",
|
|
111
|
+
sourceId: field === "source" ? id : "",
|
|
112
|
+
dependsOnId: field === "dependsOn" ? id : "",
|
|
113
|
+
details: {
|
|
114
|
+
field,
|
|
115
|
+
id,
|
|
116
|
+
message: `Node not found: ${id}`,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { id, issues: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function wouldCreateCycleInAdjacency(
|
|
127
|
+
adjacency: ReadonlyMap<string, ReadonlySet<string>>,
|
|
128
|
+
sourceId: string,
|
|
129
|
+
dependsOnId: string,
|
|
130
|
+
): boolean {
|
|
131
|
+
const visited = new Set<string>();
|
|
132
|
+
const queue: string[] = [dependsOnId];
|
|
133
|
+
|
|
134
|
+
while (queue.length > 0) {
|
|
135
|
+
const current = queue.shift();
|
|
136
|
+
if (current === undefined || visited.has(current)) continue;
|
|
137
|
+
if (current === sourceId) return true;
|
|
138
|
+
visited.add(current);
|
|
139
|
+
const neighbors = adjacency.get(current);
|
|
140
|
+
if (neighbors === undefined) continue;
|
|
141
|
+
for (const neighbor of neighbors) {
|
|
142
|
+
if (!visited.has(neighbor)) queue.push(neighbor);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveEpicExpandEntityRef(
|
|
150
|
+
reference: CompactEntityRef,
|
|
151
|
+
mappings: readonly { tempKey: string; id: string; kind: "task" | "subtask" }[],
|
|
152
|
+
option: "subtask" | "dep",
|
|
153
|
+
index: number,
|
|
154
|
+
field: "parent" | "source" | "dependsOn",
|
|
155
|
+
reader: BatchValidationReader,
|
|
156
|
+
): ResolvedCompactEntity {
|
|
157
|
+
if (reference.kind === "temp_key") {
|
|
158
|
+
const mapping = mappings.find((candidate) => candidate.tempKey === reference.tempKey);
|
|
159
|
+
if (mapping === undefined) {
|
|
160
|
+
throw new DomainError({
|
|
161
|
+
code: "invalid_input",
|
|
162
|
+
message: `Unknown temp key @${reference.tempKey} in --${option} spec ${index + 1}`,
|
|
163
|
+
details: { index, field, tempKey: reference.tempKey, option },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { id: mapping.id, kind: mapping.kind };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const id = assertNonEmptyLocal(field === "parent" ? "taskId" : `${field}Id`, reference.id);
|
|
170
|
+
return { id, kind: resolveNodeKindLocal(id, reader) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Exported pure functions
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
export function resolveDependencyBatchSpec(
|
|
178
|
+
index: number,
|
|
179
|
+
spec: CompactDependencySpec,
|
|
180
|
+
reader: BatchValidationReader,
|
|
181
|
+
): DependencyBatchResolution {
|
|
182
|
+
const sourceResolution = resolveDependencyBatchId(spec.source, "source", index, reader);
|
|
183
|
+
const dependsOnResolution = resolveDependencyBatchId(spec.dependsOn, "dependsOn", index, reader);
|
|
184
|
+
const issues = [...sourceResolution.issues, ...dependsOnResolution.issues];
|
|
185
|
+
const sourceId = sourceResolution.id;
|
|
186
|
+
const dependsOnId = dependsOnResolution.id;
|
|
187
|
+
|
|
188
|
+
if (sourceId === undefined || dependsOnId === undefined) {
|
|
189
|
+
return { issues };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (sourceId === dependsOnId) {
|
|
193
|
+
return {
|
|
194
|
+
issues: [
|
|
195
|
+
...issues,
|
|
196
|
+
{
|
|
197
|
+
index,
|
|
198
|
+
type: "cycle",
|
|
199
|
+
sourceId,
|
|
200
|
+
dependsOnId,
|
|
201
|
+
details: { sourceId, dependsOnId, reason: "self_reference" },
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
spec: {
|
|
209
|
+
index,
|
|
210
|
+
sourceId,
|
|
211
|
+
sourceKind: resolveNodeKindLocal(sourceId, reader),
|
|
212
|
+
dependsOnId,
|
|
213
|
+
dependsOnKind: resolveNodeKindLocal(dependsOnId, reader),
|
|
214
|
+
},
|
|
215
|
+
issues,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resolveEpicExpandSubtaskSpecs(
|
|
220
|
+
specs: readonly CompactSubtaskSpec[],
|
|
221
|
+
mappings: readonly { tempKey: string; id: string; kind: "task" | "subtask" }[],
|
|
222
|
+
reader: BatchValidationReader,
|
|
223
|
+
): CompactSubtaskSpec[] {
|
|
224
|
+
return specs.map((spec, index) => {
|
|
225
|
+
const parent = resolveEpicExpandEntityRef(spec.parent, mappings, "subtask", index, "parent", reader);
|
|
226
|
+
if (parent.kind !== "task") {
|
|
227
|
+
throw new DomainError({
|
|
228
|
+
code: "invalid_input",
|
|
229
|
+
message: `Subtask parent must resolve to a task in --subtask spec ${index + 1}`,
|
|
230
|
+
details: { index, field: "parent", kind: parent.kind, id: parent.id },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return { ...spec, parent: { kind: "id", id: parent.id } };
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function resolveEpicExpandDependencySpecs(
|
|
238
|
+
specs: readonly CompactDependencySpec[],
|
|
239
|
+
mappings: readonly { tempKey: string; id: string; kind: "task" | "subtask" }[],
|
|
240
|
+
reader: BatchValidationReader,
|
|
241
|
+
): CompactDependencySpec[] {
|
|
242
|
+
return specs.map((spec, index) => ({
|
|
243
|
+
source: {
|
|
244
|
+
kind: "id",
|
|
245
|
+
id: resolveEpicExpandEntityRef(spec.source, mappings, "dep", index, "source", reader).id,
|
|
246
|
+
},
|
|
247
|
+
dependsOn: {
|
|
248
|
+
kind: "id",
|
|
249
|
+
id: resolveEpicExpandEntityRef(spec.dependsOn, mappings, "dep", index, "dependsOn", reader).id,
|
|
250
|
+
},
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function collectDependencyBatchIssues(
|
|
255
|
+
specs: readonly ResolvedDependencyBatchSpec[],
|
|
256
|
+
reader: BatchValidationReader,
|
|
257
|
+
): DependencyBatchValidationIssue[] {
|
|
258
|
+
const issues: DependencyBatchValidationIssue[] = [];
|
|
259
|
+
const seenEdges = new Map<string, number>();
|
|
260
|
+
const adjacency = reader.buildDependencyAdjacency();
|
|
261
|
+
|
|
262
|
+
for (const spec of specs) {
|
|
263
|
+
const edgeKey = `${spec.sourceId}->${spec.dependsOnId}`;
|
|
264
|
+
const existingIndex = seenEdges.get(edgeKey);
|
|
265
|
+
if (existingIndex !== undefined) {
|
|
266
|
+
issues.push({
|
|
267
|
+
index: spec.index,
|
|
268
|
+
type: "duplicate",
|
|
269
|
+
sourceId: spec.sourceId,
|
|
270
|
+
dependsOnId: spec.dependsOnId,
|
|
271
|
+
details: {
|
|
272
|
+
sourceId: spec.sourceId,
|
|
273
|
+
dependsOnId: spec.dependsOnId,
|
|
274
|
+
firstIndex: existingIndex,
|
|
275
|
+
duplicateIndex: spec.index,
|
|
276
|
+
duplicateKind: "batch",
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (reader.getDependencyByEdge(spec.sourceId, spec.dependsOnId) !== null) {
|
|
283
|
+
issues.push({
|
|
284
|
+
index: spec.index,
|
|
285
|
+
type: "duplicate",
|
|
286
|
+
sourceId: spec.sourceId,
|
|
287
|
+
dependsOnId: spec.dependsOnId,
|
|
288
|
+
details: {
|
|
289
|
+
sourceId: spec.sourceId,
|
|
290
|
+
dependsOnId: spec.dependsOnId,
|
|
291
|
+
duplicateKind: "existing",
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if (wouldCreateCycleInAdjacency(adjacency, spec.sourceId, spec.dependsOnId)) {
|
|
300
|
+
issues.push({
|
|
301
|
+
index: spec.index,
|
|
302
|
+
type: "cycle",
|
|
303
|
+
sourceId: spec.sourceId,
|
|
304
|
+
dependsOnId: spec.dependsOnId,
|
|
305
|
+
details: { sourceId: spec.sourceId, dependsOnId: spec.dependsOnId },
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const nextNeighbors = adjacency.get(spec.sourceId) ?? new Set<string>();
|
|
311
|
+
nextNeighbors.add(spec.dependsOnId);
|
|
312
|
+
adjacency.set(spec.sourceId, nextNeighbors);
|
|
313
|
+
seenEdges.set(edgeKey, spec.index);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return issues.sort((left, right) => left.index - right.index || left.type.localeCompare(right.type));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function buildDependencyAdjacency(
|
|
320
|
+
rows: ReadonlyArray<{ source_id: string; depends_on_id: string }>,
|
|
321
|
+
): Map<string, Set<string>> {
|
|
322
|
+
const adjacency = new Map<string, Set<string>>();
|
|
323
|
+
for (const row of rows) {
|
|
324
|
+
const neighbors = adjacency.get(row.source_id) ?? new Set<string>();
|
|
325
|
+
neighbors.add(row.depends_on_id);
|
|
326
|
+
adjacency.set(row.source_id, neighbors);
|
|
327
|
+
}
|
|
328
|
+
return adjacency;
|
|
329
|
+
}
|