trekoon 0.3.3 → 0.3.4
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 +51 -2
- package/README.md +152 -126
- package/docs/ai-agents.md +105 -104
- package/docs/commands.md +137 -167
- package/docs/machine-contracts.md +67 -68
- package/docs/quickstart.md +73 -148
- package/package.json +1 -1
- package/src/commands/help.ts +239 -252
- package/src/commands/quickstart.ts +67 -77
- package/src/commands/skills.ts +104 -19
- package/src/commands/sync.ts +93 -5
- package/src/domain/tracker-domain.ts +210 -37
- package/src/storage/events-retention.ts +72 -0
- package/src/storage/migrations.ts +28 -0
- package/src/sync/event-writes.ts +8 -6
- package/src/sync/service.ts +80 -52
- package/src/sync/types.ts +12 -0
package/src/sync/service.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { type Database } from "bun:sqlite";
|
|
|
4
4
|
|
|
5
5
|
import { openTrekoonDatabase, writeTransaction } from "../storage/database";
|
|
6
6
|
import { countBranchEventsSince, queryBranchEventsSince } from "./branch-db";
|
|
7
|
+
import { nextEventTimestamp } from "./event-writes";
|
|
7
8
|
import { persistGitContext, resolveGitContext } from "./git-context";
|
|
8
9
|
import {
|
|
9
10
|
type PullSummary,
|
|
11
|
+
type ResolvePreviewSummary,
|
|
10
12
|
type ResolveSummary,
|
|
11
13
|
type SyncConflictDetail,
|
|
12
14
|
type SyncConflictListItem,
|
|
@@ -15,6 +17,13 @@ import {
|
|
|
15
17
|
type SyncStatusSummary,
|
|
16
18
|
} from "./types";
|
|
17
19
|
|
|
20
|
+
const SYNC_ALLOWED_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
21
|
+
epics: ["title", "description", "status"],
|
|
22
|
+
tasks: ["epic_id", "title", "description", "status"],
|
|
23
|
+
subtasks: ["task_id", "title", "description", "status"],
|
|
24
|
+
dependencies: ["source_id", "source_kind", "depends_on_id", "depends_on_kind"],
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
function isCursorStale(db: Database, cursorToken: string, sourceBranch: string): boolean {
|
|
19
28
|
if (cursorToken === "0:") {
|
|
20
29
|
return false;
|
|
@@ -301,14 +310,7 @@ function currentEntityFieldValue(db: Database, entityKind: string, entityId: str
|
|
|
301
310
|
return undefined;
|
|
302
311
|
}
|
|
303
312
|
|
|
304
|
-
const
|
|
305
|
-
epics: ["title", "description", "status"],
|
|
306
|
-
tasks: ["epic_id", "title", "description", "status"],
|
|
307
|
-
subtasks: ["task_id", "title", "description", "status"],
|
|
308
|
-
dependencies: ["source_id", "source_kind", "depends_on_id", "depends_on_kind"],
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const validFields = allowedFields[tableName] ?? [];
|
|
313
|
+
const validFields = SYNC_ALLOWED_FIELDS[tableName] ?? [];
|
|
312
314
|
if (!validFields.includes(fieldName)) {
|
|
313
315
|
return undefined;
|
|
314
316
|
}
|
|
@@ -564,18 +566,11 @@ function applyUpdatePatch(db: Database, event: StoredEvent, fields: Record<strin
|
|
|
564
566
|
return false;
|
|
565
567
|
}
|
|
566
568
|
|
|
567
|
-
const allowedFields: Record<string, readonly string[]> = {
|
|
568
|
-
epics: ["title", "description", "status"],
|
|
569
|
-
tasks: ["epic_id", "title", "description", "status"],
|
|
570
|
-
subtasks: ["task_id", "title", "description", "status"],
|
|
571
|
-
dependencies: ["source_id", "source_kind", "depends_on_id", "depends_on_kind"],
|
|
572
|
-
};
|
|
573
|
-
|
|
574
569
|
if (!rowExists(db, tableName, event.entity_id)) {
|
|
575
570
|
return false;
|
|
576
571
|
}
|
|
577
572
|
|
|
578
|
-
const allowed = new Set(
|
|
573
|
+
const allowed = new Set(SYNC_ALLOWED_FIELDS[tableName] ?? []);
|
|
579
574
|
const entries = Object.entries(fields).filter(([fieldName, value]) => allowed.has(fieldName) && typeof value === "string");
|
|
580
575
|
|
|
581
576
|
if (entries.length === 0) {
|
|
@@ -905,7 +900,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
905
900
|
}
|
|
906
901
|
|
|
907
902
|
function parseConflictValue(value: string | null): unknown {
|
|
908
|
-
if (
|
|
903
|
+
if (value === null || value === undefined) {
|
|
909
904
|
return null;
|
|
910
905
|
}
|
|
911
906
|
|
|
@@ -922,14 +917,7 @@ function updateSingleField(db: Database, entityKind: string, entityId: string, f
|
|
|
922
917
|
return;
|
|
923
918
|
}
|
|
924
919
|
|
|
925
|
-
const
|
|
926
|
-
epics: ["title", "description", "status"],
|
|
927
|
-
tasks: ["epic_id", "title", "description", "status"],
|
|
928
|
-
subtasks: ["task_id", "title", "description", "status"],
|
|
929
|
-
dependencies: ["source_id", "source_kind", "depends_on_id", "depends_on_kind"],
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
const validFields: readonly string[] = allowedFields[tableName] ?? [];
|
|
920
|
+
const validFields: readonly string[] = SYNC_ALLOWED_FIELDS[tableName] ?? [];
|
|
933
921
|
if (!validFields.includes(fieldName)) {
|
|
934
922
|
return;
|
|
935
923
|
}
|
|
@@ -948,8 +936,9 @@ function appendResolutionEvent(
|
|
|
948
936
|
gitHead: string | null,
|
|
949
937
|
conflict: ConflictRow,
|
|
950
938
|
resolution: SyncResolution,
|
|
939
|
+
timestamp?: number,
|
|
951
940
|
): void {
|
|
952
|
-
const now: number =
|
|
941
|
+
const now: number = timestamp ?? nextEventTimestamp(db);
|
|
953
942
|
const resolvedValue: string | null = resolution === "theirs" ? conflict.theirs_value : conflict.ours_value;
|
|
954
943
|
|
|
955
944
|
db.query(
|
|
@@ -1061,6 +1050,29 @@ export function getSyncConflict(cwd: string, conflictId: string): SyncConflictDe
|
|
|
1061
1050
|
}
|
|
1062
1051
|
}
|
|
1063
1052
|
|
|
1053
|
+
function lookupPendingConflict(db: Database, conflictId: string): ConflictRow {
|
|
1054
|
+
const conflict = db
|
|
1055
|
+
.query(
|
|
1056
|
+
`
|
|
1057
|
+
SELECT id, event_id, entity_kind, entity_id, field_name, ours_value, theirs_value, resolution, created_at, updated_at
|
|
1058
|
+
FROM sync_conflicts
|
|
1059
|
+
WHERE id = ?
|
|
1060
|
+
LIMIT 1;
|
|
1061
|
+
`,
|
|
1062
|
+
)
|
|
1063
|
+
.get(conflictId) as ConflictRow | null;
|
|
1064
|
+
|
|
1065
|
+
if (!conflict) {
|
|
1066
|
+
throw new Error(`Conflict '${conflictId}' not found.`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (conflict.resolution !== "pending") {
|
|
1070
|
+
throw new Error(`Conflict '${conflictId}' already resolved.`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return conflict;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1064
1076
|
export function syncResolve(cwd: string, conflictId: string, resolution: SyncResolution): ResolveSummary {
|
|
1065
1077
|
const storage = openTrekoonDatabase(cwd);
|
|
1066
1078
|
const git = resolveGitContext(cwd);
|
|
@@ -1068,42 +1080,31 @@ export function syncResolve(cwd: string, conflictId: string, resolution: SyncRes
|
|
|
1068
1080
|
try {
|
|
1069
1081
|
persistGitContext(storage.db, git);
|
|
1070
1082
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
LIMIT 1;
|
|
1078
|
-
`,
|
|
1079
|
-
)
|
|
1080
|
-
.get(conflictId) as ConflictRow | null;
|
|
1081
|
-
|
|
1082
|
-
if (!conflict) {
|
|
1083
|
-
throw new Error(`Conflict '${conflictId}' not found.`);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (conflict.resolution !== "pending") {
|
|
1087
|
-
throw new Error(`Conflict '${conflictId}' already resolved.`);
|
|
1088
|
-
}
|
|
1083
|
+
// lookupPendingConflict is inside the writeTransaction so that the
|
|
1084
|
+
// "is this still pending?" check and the resolution mutation are
|
|
1085
|
+
// atomic. Without this, two concurrent resolves could both pass
|
|
1086
|
+
// the check and double-resolve the same conflict.
|
|
1087
|
+
const conflict = writeTransaction(storage.db, (): ConflictRow => {
|
|
1088
|
+
const row = lookupPendingConflict(storage.db, conflictId);
|
|
1089
1089
|
|
|
1090
|
-
writeTransaction(storage.db, (): void => {
|
|
1091
1090
|
if (resolution === "theirs") {
|
|
1092
1091
|
updateSingleField(
|
|
1093
1092
|
storage.db,
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
parseConflictValue(
|
|
1093
|
+
row.entity_kind,
|
|
1094
|
+
row.entity_id,
|
|
1095
|
+
row.field_name,
|
|
1096
|
+
parseConflictValue(row.theirs_value),
|
|
1098
1097
|
);
|
|
1099
1098
|
}
|
|
1100
1099
|
|
|
1101
|
-
const now: number =
|
|
1100
|
+
const now: number = nextEventTimestamp(storage.db);
|
|
1102
1101
|
storage.db
|
|
1103
1102
|
.query("UPDATE sync_conflicts SET resolution = ?, updated_at = ?, version = version + 1 WHERE id = ?;")
|
|
1104
|
-
.run(resolution, now,
|
|
1103
|
+
.run(resolution, now, row.id);
|
|
1104
|
+
|
|
1105
|
+
appendResolutionEvent(storage.db, git.branchName, git.headSha, row, resolution, now);
|
|
1105
1106
|
|
|
1106
|
-
|
|
1107
|
+
return row;
|
|
1107
1108
|
});
|
|
1108
1109
|
|
|
1109
1110
|
return {
|
|
@@ -1117,3 +1118,30 @@ export function syncResolve(cwd: string, conflictId: string, resolution: SyncRes
|
|
|
1117
1118
|
storage.close();
|
|
1118
1119
|
}
|
|
1119
1120
|
}
|
|
1121
|
+
|
|
1122
|
+
// Preview is read-only — no git context persistence needed.
|
|
1123
|
+
export function syncResolvePreview(cwd: string, conflictId: string, resolution: SyncResolution): ResolvePreviewSummary {
|
|
1124
|
+
const storage = openTrekoonDatabase(cwd);
|
|
1125
|
+
|
|
1126
|
+
try {
|
|
1127
|
+
const conflict = lookupPendingConflict(storage.db, conflictId);
|
|
1128
|
+
|
|
1129
|
+
const oursValue: unknown = parseConflictValue(conflict.ours_value);
|
|
1130
|
+
const theirsValue: unknown = parseConflictValue(conflict.theirs_value);
|
|
1131
|
+
const wouldWrite: unknown = resolution === "theirs" ? theirsValue : oursValue;
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
conflictId,
|
|
1135
|
+
resolution,
|
|
1136
|
+
entityKind: conflict.entity_kind,
|
|
1137
|
+
entityId: conflict.entity_id,
|
|
1138
|
+
fieldName: conflict.field_name,
|
|
1139
|
+
oursValue,
|
|
1140
|
+
theirsValue,
|
|
1141
|
+
wouldWrite,
|
|
1142
|
+
dryRun: true,
|
|
1143
|
+
};
|
|
1144
|
+
} finally {
|
|
1145
|
+
storage.close();
|
|
1146
|
+
}
|
|
1147
|
+
}
|
package/src/sync/types.ts
CHANGED
|
@@ -43,6 +43,18 @@ export interface ResolveSummary {
|
|
|
43
43
|
readonly fieldName: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface ResolvePreviewSummary {
|
|
47
|
+
readonly conflictId: string;
|
|
48
|
+
readonly resolution: SyncResolution;
|
|
49
|
+
readonly entityKind: string;
|
|
50
|
+
readonly entityId: string;
|
|
51
|
+
readonly fieldName: string;
|
|
52
|
+
readonly oursValue: unknown;
|
|
53
|
+
readonly theirsValue: unknown;
|
|
54
|
+
readonly wouldWrite: unknown;
|
|
55
|
+
readonly dryRun: true;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
export interface SyncConflictListItem {
|
|
47
59
|
readonly id: string;
|
|
48
60
|
readonly event_id: string;
|