trekoon 0.3.0 → 0.3.2
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 +274 -26
- package/.agents/skills/trekoon/reference/execution-with-team.md +213 -0
- package/.agents/skills/trekoon/reference/execution.md +210 -0
- package/.agents/skills/trekoon/reference/planning.md +244 -0
- package/README.md +24 -10
- package/docs/ai-agents.md +108 -30
- package/docs/commands.md +81 -5
- package/docs/machine-contracts.md +120 -0
- package/docs/plans/r1-unified-skill-rewrite.md +290 -0
- package/docs/plans/r10-suggest-command-skill-integration.md +152 -0
- package/docs/plans/r9-task-done-diff-skill-integration.md +113 -0
- package/docs/quickstart.md +31 -0
- package/package.json +2 -2
- package/src/board/assets/app.js +5 -0
- package/src/board/assets/components/EpicsOverview.js +13 -0
- package/src/board/assets/components/Workspace.js +27 -12
- package/src/board/assets/components/helpers.js +3 -2
- package/src/board/assets/runtime/delegation.js +69 -1
- package/src/board/assets/state/actions.js +27 -1
- package/src/board/assets/state/store.js +37 -8
- package/src/board/assets/state/utils.js +42 -0
- package/src/board/assets/styles/board.css +68 -0
- package/src/board/routes.ts +2 -0
- package/src/commands/epic.ts +74 -3
- package/src/commands/session.ts +7 -75
- package/src/commands/skills.ts +39 -32
- package/src/commands/subtask.ts +7 -5
- package/src/commands/suggest.ts +283 -0
- package/src/commands/sync-helpers.ts +75 -0
- package/src/commands/task-readiness.ts +8 -20
- package/src/commands/task.ts +59 -3
- package/src/domain/mutation-service.ts +69 -42
- package/src/domain/tracker-domain.ts +151 -22
- package/src/domain/types.ts +12 -0
- package/src/index.ts +1 -1
- package/src/io/output.ts +4 -2
- package/src/runtime/cli-shell.ts +26 -3
- package/src/runtime/command-types.ts +1 -1
- package/src/storage/database.ts +43 -1
- package/src/storage/events-retention.ts +57 -8
- package/src/storage/migrations.ts +58 -3
- package/src/sync/service.ts +101 -24
- package/src/sync/types.ts +1 -0
|
@@ -177,9 +177,64 @@ const MIGRATIONS: readonly Migration[] = [
|
|
|
177
177
|
up(db: Database): void {
|
|
178
178
|
migrateWorktreeScopedSyncMetadata(db);
|
|
179
179
|
},
|
|
180
|
-
down(
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
down(_db: Database): void {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"Migration 0004 (worktree_scoped_sync_metadata) is irreversible. " +
|
|
183
|
+
"It adds columns via ALTER TABLE that cannot be removed without " +
|
|
184
|
+
"reconstructing tables and risking data loss. " +
|
|
185
|
+
"Rollback below version 4 is not supported.",
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
version: 5,
|
|
191
|
+
name: "0005_dependency_edge_integrity",
|
|
192
|
+
up(db: Database): void {
|
|
193
|
+
// Clean up orphaned dependency rows where source or target no longer exists.
|
|
194
|
+
db.exec(`
|
|
195
|
+
DELETE FROM dependencies
|
|
196
|
+
WHERE source_id NOT IN (SELECT id FROM tasks UNION ALL SELECT id FROM subtasks)
|
|
197
|
+
OR depends_on_id NOT IN (SELECT id FROM tasks UNION ALL SELECT id FROM subtasks);
|
|
198
|
+
`);
|
|
199
|
+
|
|
200
|
+
// Deduplicate any existing duplicate edges before creating the unique index.
|
|
201
|
+
// Keep one arbitrary row per logical edge (MIN(id) is lexicographic, not chronological, but any survivor is equivalent).
|
|
202
|
+
db.exec(`
|
|
203
|
+
DELETE FROM dependencies
|
|
204
|
+
WHERE id NOT IN (
|
|
205
|
+
SELECT MIN(id) FROM dependencies
|
|
206
|
+
GROUP BY source_id, depends_on_id
|
|
207
|
+
);
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_dependencies_edge ON dependencies (source_id, depends_on_id);");
|
|
211
|
+
},
|
|
212
|
+
down(_db: Database): void {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"Migration 0005 (dependency_edge_integrity) is irreversible. " +
|
|
215
|
+
"It removes orphaned rows and deduplicates dependency edges. " +
|
|
216
|
+
"Rollback below version 5 is not supported.",
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
version: 6,
|
|
222
|
+
name: "0006_add_owner_column",
|
|
223
|
+
up(db: Database): void {
|
|
224
|
+
if (!tableHasColumn(db, "tasks", "owner")) {
|
|
225
|
+
db.exec("ALTER TABLE tasks ADD COLUMN owner TEXT;");
|
|
226
|
+
}
|
|
227
|
+
if (!tableHasColumn(db, "subtasks", "owner")) {
|
|
228
|
+
db.exec("ALTER TABLE subtasks ADD COLUMN owner TEXT;");
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
down(_db: Database): void {
|
|
232
|
+
throw new Error(
|
|
233
|
+
"Migration 0006 (add_owner_column) is irreversible. " +
|
|
234
|
+
"It adds columns via ALTER TABLE that cannot be removed without " +
|
|
235
|
+
"reconstructing tables and risking data loss. " +
|
|
236
|
+
"Rollback below version 6 is not supported.",
|
|
237
|
+
);
|
|
183
238
|
},
|
|
184
239
|
},
|
|
185
240
|
];
|
package/src/sync/service.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { type Database } from "bun:sqlite";
|
|
4
4
|
|
|
5
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
5
|
+
import { openTrekoonDatabase, writeTransaction } from "../storage/database";
|
|
6
6
|
import { countBranchEventsSince, queryBranchEventsSince } from "./branch-db";
|
|
7
7
|
import { persistGitContext, resolveGitContext } from "./git-context";
|
|
8
8
|
import {
|
|
@@ -15,6 +15,49 @@ import {
|
|
|
15
15
|
type SyncStatusSummary,
|
|
16
16
|
} from "./types";
|
|
17
17
|
|
|
18
|
+
function isCursorStale(db: Database, cursorToken: string, sourceBranch: string): boolean {
|
|
19
|
+
if (cursorToken === "0:") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const [createdAtRaw, idRaw] = cursorToken.split(":");
|
|
24
|
+
const createdAt: number = Number.parseInt(createdAtRaw ?? "0", 10);
|
|
25
|
+
const id: string = idRaw ?? "";
|
|
26
|
+
|
|
27
|
+
if (!Number.isFinite(createdAt) || createdAt === 0) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if the event referenced by the cursor still exists.
|
|
32
|
+
// If the cursor references a specific event id, check for it.
|
|
33
|
+
// Otherwise, check if any event at or after the cursor timestamp exists
|
|
34
|
+
// on the source branch.
|
|
35
|
+
if (id.length > 0) {
|
|
36
|
+
const row = db
|
|
37
|
+
.query("SELECT id FROM events WHERE id = ? LIMIT 1;")
|
|
38
|
+
.get(id) as { id: string } | null;
|
|
39
|
+
if (row) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The referenced event is gone. Check if there are any events on the
|
|
45
|
+
// source branch at or after the cursor timestamp — if not, the cursor
|
|
46
|
+
// may simply be at the end of the stream.
|
|
47
|
+
const newerRow = db
|
|
48
|
+
.query(
|
|
49
|
+
`SELECT id FROM events
|
|
50
|
+
WHERE git_branch = ? AND created_at >= ?
|
|
51
|
+
ORDER BY created_at ASC, id ASC
|
|
52
|
+
LIMIT 1;`,
|
|
53
|
+
)
|
|
54
|
+
.get(sourceBranch, createdAt) as { id: string } | null;
|
|
55
|
+
|
|
56
|
+
// If there are newer events but our referenced event is gone,
|
|
57
|
+
// events between the cursor and the oldest remaining event were pruned.
|
|
58
|
+
return newerRow !== null;
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
interface StoredEvent {
|
|
19
62
|
readonly id: string;
|
|
20
63
|
readonly entity_kind: string;
|
|
@@ -289,23 +332,22 @@ function entityFieldConflict(
|
|
|
289
332
|
return null;
|
|
290
333
|
}
|
|
291
334
|
|
|
335
|
+
// Note: loads all matching events into memory. For entities with very large
|
|
336
|
+
// event histories, consider a cursor-based scan. The idx_events_entity index
|
|
337
|
+
// keeps the scan narrow by (entity_kind, entity_id).
|
|
292
338
|
const rows = localDb
|
|
293
339
|
.query(
|
|
294
340
|
`
|
|
295
341
|
SELECT payload, git_branch
|
|
296
342
|
FROM events
|
|
297
|
-
WHERE entity_kind = ? AND entity_id = ?
|
|
343
|
+
WHERE entity_kind = ? AND entity_id = ? AND git_branch != ?
|
|
298
344
|
ORDER BY created_at DESC, id DESC
|
|
299
|
-
LIMIT
|
|
300
|
-
|
|
345
|
+
LIMIT 500;
|
|
346
|
+
`,
|
|
301
347
|
)
|
|
302
|
-
.all(event.entity_kind, event.entity_id) as Array<{ payload: string; git_branch: string | null }>;
|
|
348
|
+
.all(event.entity_kind, event.entity_id, sourceBranch) as Array<{ payload: string; git_branch: string | null }>;
|
|
303
349
|
|
|
304
350
|
for (const row of rows) {
|
|
305
|
-
if (row.git_branch === sourceBranch) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
351
|
const payloadValidation = parsePayload(row.payload);
|
|
310
352
|
if (!payloadValidation.ok) {
|
|
311
353
|
continue;
|
|
@@ -360,6 +402,15 @@ function createConflict(
|
|
|
360
402
|
).run(randomUUID(), event.id, event.entity_kind, event.entity_id, fieldName, oursValue, theirsValue, resolution, now, now);
|
|
361
403
|
}
|
|
362
404
|
|
|
405
|
+
function hasLocalEntityEdits(db: Database, entityKind: string, entityId: string, sourceBranch: string): boolean {
|
|
406
|
+
const row = db
|
|
407
|
+
.query(
|
|
408
|
+
`SELECT 1 FROM events WHERE entity_kind = ? AND entity_id = ? AND git_branch != ? LIMIT 1;`,
|
|
409
|
+
)
|
|
410
|
+
.get(entityKind, entityId, sourceBranch);
|
|
411
|
+
return row !== null;
|
|
412
|
+
}
|
|
413
|
+
|
|
363
414
|
function rowExists(db: Database, tableName: string, id: string): boolean {
|
|
364
415
|
const row = db.query(`SELECT id FROM ${tableName} WHERE id = ? LIMIT 1;`).get(id) as { id: string } | null;
|
|
365
416
|
return row !== null;
|
|
@@ -495,10 +546,9 @@ function applyCreate(db: Database, event: StoredEvent, fields: Record<string, un
|
|
|
495
546
|
updated_at,
|
|
496
547
|
version
|
|
497
548
|
) VALUES (?, ?, ?, ?, ?, ?, ?, 1)
|
|
498
|
-
ON CONFLICT(
|
|
499
|
-
|
|
549
|
+
ON CONFLICT(source_id, depends_on_id) DO UPDATE SET
|
|
550
|
+
id = excluded.id,
|
|
500
551
|
source_kind = excluded.source_kind,
|
|
501
|
-
depends_on_id = excluded.depends_on_id,
|
|
502
552
|
depends_on_kind = excluded.depends_on_kind,
|
|
503
553
|
updated_at = excluded.updated_at,
|
|
504
554
|
version = dependencies.version + 1;
|
|
@@ -689,6 +739,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
689
739
|
persistGitContext(storage.db, git);
|
|
690
740
|
const cursor = loadCursor(storage.db, git.worktreePath, sourceBranch);
|
|
691
741
|
const cursorToken = cursor?.cursor_token ?? "0:";
|
|
742
|
+
const staleCursor: boolean = cursor !== null && isCursorStale(storage.db, cursorToken, sourceBranch);
|
|
692
743
|
const incomingEvents: StoredEvent[] = queryBranchEventsSince(storage.db, sourceBranch, cursorToken) as StoredEvent[];
|
|
693
744
|
|
|
694
745
|
// Same-branch fast path: skip conflict detection when already on sourceBranch.
|
|
@@ -697,7 +748,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
697
748
|
let lastToken: string | null = null;
|
|
698
749
|
let lastEventAt: number | null = cursor?.last_event_at ?? null;
|
|
699
750
|
|
|
700
|
-
storage.db
|
|
751
|
+
writeTransaction(storage.db, (): void => {
|
|
701
752
|
for (const incoming of incomingEvents) {
|
|
702
753
|
storeEvent(storage.db, incoming);
|
|
703
754
|
lastToken = cursorTokenFromEvent(incoming);
|
|
@@ -707,7 +758,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
707
758
|
if (lastToken) {
|
|
708
759
|
saveCursor(storage.db, git.worktreePath, sourceBranch, lastToken, lastEventAt);
|
|
709
760
|
}
|
|
710
|
-
})
|
|
761
|
+
});
|
|
711
762
|
|
|
712
763
|
return {
|
|
713
764
|
sourceBranch,
|
|
@@ -721,7 +772,10 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
721
772
|
applyRejectedEvents: 0,
|
|
722
773
|
quarantinedEvents: 0,
|
|
723
774
|
conflictEvents: 0,
|
|
724
|
-
|
|
775
|
+
staleCursor,
|
|
776
|
+
errorHints: staleCursor
|
|
777
|
+
? ["Stale cursor detected; some events may have been pruned. Consider a full rebuild."]
|
|
778
|
+
: [],
|
|
725
779
|
},
|
|
726
780
|
};
|
|
727
781
|
}
|
|
@@ -735,7 +789,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
735
789
|
let lastToken: string | null = null;
|
|
736
790
|
let lastEventAt: number | null = cursor?.last_event_at ?? null;
|
|
737
791
|
|
|
738
|
-
storage.db
|
|
792
|
+
writeTransaction(storage.db, (): void => {
|
|
739
793
|
for (const incoming of incomingEvents) {
|
|
740
794
|
const payloadValidation = parsePayload(incoming.payload);
|
|
741
795
|
|
|
@@ -758,6 +812,23 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
758
812
|
}
|
|
759
813
|
|
|
760
814
|
const payload: EventPayload = { fields: payloadValidation.fields };
|
|
815
|
+
|
|
816
|
+
const isDeleteWithLocalEdits =
|
|
817
|
+
incoming.operation.endsWith(".deleted") &&
|
|
818
|
+
hasLocalEntityEdits(storage.db, incoming.entity_kind, incoming.entity_id, sourceBranch);
|
|
819
|
+
if (isDeleteWithLocalEdits) {
|
|
820
|
+
// Note: dependency.removed is intentionally excluded — dependencies are
|
|
821
|
+
// edges (not entities with local edit history), so conflict detection
|
|
822
|
+
// does not apply to them.
|
|
823
|
+
createConflict(storage.db, incoming, "__delete__", null, "Entity deleted on source branch");
|
|
824
|
+
createdConflicts += 1;
|
|
825
|
+
conflictEvents += 1;
|
|
826
|
+
storeEvent(storage.db, incoming);
|
|
827
|
+
lastToken = cursorTokenFromEvent(incoming);
|
|
828
|
+
lastEventAt = incoming.created_at;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
761
832
|
const fieldsToApply: Record<string, unknown> = {};
|
|
762
833
|
let withheldConflictCount = 0;
|
|
763
834
|
|
|
@@ -801,7 +872,16 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
801
872
|
if (lastToken) {
|
|
802
873
|
saveCursor(storage.db, git.worktreePath, sourceBranch, lastToken, lastEventAt);
|
|
803
874
|
}
|
|
804
|
-
})
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
const errorHints: string[] = buildSyncErrorHints({
|
|
878
|
+
malformedPayloadEvents,
|
|
879
|
+
applyRejectedEvents,
|
|
880
|
+
conflictEvents,
|
|
881
|
+
});
|
|
882
|
+
if (staleCursor) {
|
|
883
|
+
errorHints.push("Stale cursor detected; some events may have been pruned. Consider a full rebuild.");
|
|
884
|
+
}
|
|
805
885
|
|
|
806
886
|
return {
|
|
807
887
|
sourceBranch,
|
|
@@ -815,11 +895,8 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
815
895
|
applyRejectedEvents,
|
|
816
896
|
quarantinedEvents,
|
|
817
897
|
conflictEvents,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
applyRejectedEvents,
|
|
821
|
-
conflictEvents,
|
|
822
|
-
}),
|
|
898
|
+
staleCursor,
|
|
899
|
+
errorHints,
|
|
823
900
|
},
|
|
824
901
|
};
|
|
825
902
|
} finally {
|
|
@@ -1010,7 +1087,7 @@ export function syncResolve(cwd: string, conflictId: string, resolution: SyncRes
|
|
|
1010
1087
|
throw new Error(`Conflict '${conflictId}' already resolved.`);
|
|
1011
1088
|
}
|
|
1012
1089
|
|
|
1013
|
-
storage.db
|
|
1090
|
+
writeTransaction(storage.db, (): void => {
|
|
1014
1091
|
if (resolution === "theirs") {
|
|
1015
1092
|
updateSingleField(
|
|
1016
1093
|
storage.db,
|
|
@@ -1027,7 +1104,7 @@ export function syncResolve(cwd: string, conflictId: string, resolution: SyncRes
|
|
|
1027
1104
|
.run(resolution, now, conflict.id);
|
|
1028
1105
|
|
|
1029
1106
|
appendResolutionEvent(storage.db, git.branchName, git.headSha, conflict, resolution);
|
|
1030
|
-
})
|
|
1107
|
+
});
|
|
1031
1108
|
|
|
1032
1109
|
return {
|
|
1033
1110
|
conflictId,
|
package/src/sync/types.ts
CHANGED