trekoon 0.2.1 → 0.2.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 +95 -33
- package/README.md +74 -13
- package/package.json +1 -1
- package/src/commands/help.ts +47 -13
- package/src/commands/init.ts +104 -6
- package/src/commands/quickstart.ts +76 -30
- package/src/commands/session.ts +223 -0
- package/src/commands/skills.ts +100 -63
- package/src/commands/sync.ts +62 -21
- package/src/commands/task-readiness.ts +147 -0
- package/src/commands/task.ts +81 -143
- package/src/commands/wipe.ts +15 -5
- package/src/runtime/cli-shell.ts +83 -5
- package/src/storage/database.ts +86 -0
- package/src/storage/migrations.ts +48 -0
- package/src/storage/path.ts +70 -21
- package/src/storage/schema.ts +9 -2
- package/src/storage/worktree-recovery.ts +376 -0
- package/src/sync/branch-db.ts +87 -35
- package/src/sync/git-context.ts +7 -2
- package/src/sync/service.ts +89 -95
- package/src/sync/types.ts +2 -0
package/src/sync/service.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { type Database } from "bun:sqlite";
|
|
4
4
|
|
|
5
5
|
import { openTrekoonDatabase } from "../storage/database";
|
|
6
|
-
import {
|
|
6
|
+
import { countBranchEventsSince, queryBranchEventsSince } from "./branch-db";
|
|
7
7
|
import { persistGitContext, resolveGitContext } from "./git-context";
|
|
8
8
|
import {
|
|
9
9
|
type PullSummary,
|
|
@@ -29,6 +29,8 @@ interface StoredEvent {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
interface CursorRow {
|
|
32
|
+
readonly owner_scope: string;
|
|
33
|
+
readonly owner_worktree_path: string;
|
|
32
34
|
readonly source_branch: string;
|
|
33
35
|
readonly cursor_token: string;
|
|
34
36
|
readonly last_event_at: number | null;
|
|
@@ -116,40 +118,45 @@ function tableForEntityKind(entityKind: string): "epics" | "tasks" | "subtasks"
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
function parseCursorToken(token: string): { createdAt: number; id: string | null } {
|
|
120
|
-
const [createdAtRaw, idRaw] = token.split(":");
|
|
121
|
-
const createdAt: number = Number.parseInt(createdAtRaw ?? "0", 10);
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
createdAt: Number.isFinite(createdAt) ? createdAt : 0,
|
|
125
|
-
id: idRaw && idRaw.length > 0 ? idRaw : null,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
121
|
function cursorTokenFromEvent(event: StoredEvent): string {
|
|
130
122
|
return `${event.created_at}:${event.id}`;
|
|
131
123
|
}
|
|
132
124
|
|
|
133
|
-
function
|
|
125
|
+
function cursorIdForWorktree(worktreePath: string, sourceBranch: string): string {
|
|
126
|
+
return `${worktreePath}::${sourceBranch}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadCursor(db: Database, worktreePath: string, sourceBranch: string): CursorRow | null {
|
|
134
130
|
return db
|
|
135
131
|
.query(
|
|
136
132
|
`
|
|
137
|
-
SELECT source_branch, cursor_token, last_event_at
|
|
133
|
+
SELECT owner_scope, owner_worktree_path, source_branch, cursor_token, last_event_at
|
|
138
134
|
FROM sync_cursors
|
|
139
|
-
WHERE
|
|
135
|
+
WHERE owner_scope = 'worktree'
|
|
136
|
+
AND owner_worktree_path = ?
|
|
137
|
+
AND source_branch = ?
|
|
140
138
|
LIMIT 1;
|
|
141
139
|
`,
|
|
142
140
|
)
|
|
143
|
-
.get(sourceBranch) as CursorRow | null;
|
|
141
|
+
.get(worktreePath, sourceBranch) as CursorRow | null;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
function saveCursor(
|
|
144
|
+
function saveCursor(
|
|
145
|
+
db: Database,
|
|
146
|
+
worktreePath: string,
|
|
147
|
+
sourceBranch: string,
|
|
148
|
+
cursorToken: string,
|
|
149
|
+
lastEventAt: number | null,
|
|
150
|
+
): void {
|
|
147
151
|
const now: number = Date.now();
|
|
152
|
+
const cursorId = cursorIdForWorktree(worktreePath, sourceBranch);
|
|
148
153
|
|
|
149
154
|
db.query(
|
|
150
155
|
`
|
|
151
156
|
INSERT INTO sync_cursors (
|
|
152
157
|
id,
|
|
158
|
+
owner_scope,
|
|
159
|
+
owner_worktree_path,
|
|
153
160
|
source_branch,
|
|
154
161
|
cursor_token,
|
|
155
162
|
last_event_at,
|
|
@@ -157,7 +164,9 @@ function saveCursor(db: Database, sourceBranch: string, cursorToken: string, las
|
|
|
157
164
|
updated_at,
|
|
158
165
|
version
|
|
159
166
|
) VALUES (
|
|
160
|
-
@
|
|
167
|
+
@cursorId,
|
|
168
|
+
'worktree',
|
|
169
|
+
@worktreePath,
|
|
161
170
|
@sourceBranch,
|
|
162
171
|
@cursorToken,
|
|
163
172
|
@lastEventAt,
|
|
@@ -166,12 +175,16 @@ function saveCursor(db: Database, sourceBranch: string, cursorToken: string, las
|
|
|
166
175
|
1
|
|
167
176
|
)
|
|
168
177
|
ON CONFLICT(id) DO UPDATE SET
|
|
178
|
+
owner_scope = excluded.owner_scope,
|
|
179
|
+
owner_worktree_path = excluded.owner_worktree_path,
|
|
169
180
|
cursor_token = excluded.cursor_token,
|
|
170
181
|
last_event_at = excluded.last_event_at,
|
|
171
182
|
updated_at = excluded.updated_at,
|
|
172
183
|
version = sync_cursors.version + 1;
|
|
173
184
|
`,
|
|
174
185
|
).run({
|
|
186
|
+
"@cursorId": cursorId,
|
|
187
|
+
"@worktreePath": worktreePath,
|
|
175
188
|
"@sourceBranch": sourceBranch,
|
|
176
189
|
"@cursorToken": cursorToken,
|
|
177
190
|
"@lastEventAt": lastEventAt,
|
|
@@ -179,25 +192,6 @@ function saveCursor(db: Database, sourceBranch: string, cursorToken: string, las
|
|
|
179
192
|
});
|
|
180
193
|
}
|
|
181
194
|
|
|
182
|
-
function queryNewEvents(remoteDb: Database, cursorToken: string): StoredEvent[] {
|
|
183
|
-
const cursor = parseCursorToken(cursorToken);
|
|
184
|
-
|
|
185
|
-
return remoteDb
|
|
186
|
-
.query(
|
|
187
|
-
`
|
|
188
|
-
SELECT id, entity_kind, entity_id, operation, payload, git_branch, git_head, created_at, updated_at, version
|
|
189
|
-
FROM events
|
|
190
|
-
WHERE created_at > @createdAt
|
|
191
|
-
OR (created_at = @createdAt AND id > @id)
|
|
192
|
-
ORDER BY created_at ASC, id ASC;
|
|
193
|
-
`,
|
|
194
|
-
)
|
|
195
|
-
.all({
|
|
196
|
-
"@createdAt": cursor.createdAt,
|
|
197
|
-
"@id": cursor.id ?? "",
|
|
198
|
-
}) as StoredEvent[];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
195
|
function countPendingConflicts(db: Database): number {
|
|
202
196
|
const row = db
|
|
203
197
|
.query("SELECT COUNT(*) AS count FROM sync_conflicts WHERE resolution = 'pending';")
|
|
@@ -206,54 +200,24 @@ function countPendingConflicts(db: Database): number {
|
|
|
206
200
|
return row?.count ?? 0;
|
|
207
201
|
}
|
|
208
202
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
203
|
+
function countAhead(localDb: Database, currentBranch: string | null, sourceBranch: string): number {
|
|
204
|
+
if (!currentBranch || currentBranch === sourceBranch) {
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const row = localDb
|
|
212
209
|
.query(
|
|
213
210
|
`
|
|
214
211
|
SELECT COUNT(*) AS count
|
|
215
212
|
FROM events
|
|
216
|
-
WHERE
|
|
217
|
-
OR (created_at = @createdAt AND id > @id);
|
|
213
|
+
WHERE git_branch = @branch;
|
|
218
214
|
`,
|
|
219
215
|
)
|
|
220
|
-
.get({
|
|
221
|
-
"@createdAt": cursor.createdAt,
|
|
222
|
-
"@id": cursor.id ?? "",
|
|
223
|
-
}) as { count: number } | null;
|
|
216
|
+
.get({ "@branch": currentBranch }) as { count: number } | null;
|
|
224
217
|
|
|
225
218
|
return row?.count ?? 0;
|
|
226
219
|
}
|
|
227
220
|
|
|
228
|
-
function countAhead(localDb: Database, currentBranch: string | null, remoteDbPath: string): number {
|
|
229
|
-
if (!currentBranch) {
|
|
230
|
-
return 0;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
localDb.query("ATTACH DATABASE ? AS sync_remote;").run(remoteDbPath);
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const row = localDb
|
|
237
|
-
.query(
|
|
238
|
-
`
|
|
239
|
-
SELECT COUNT(*) AS count
|
|
240
|
-
FROM events AS local_events
|
|
241
|
-
WHERE local_events.git_branch = @branch
|
|
242
|
-
AND NOT EXISTS (
|
|
243
|
-
SELECT 1
|
|
244
|
-
FROM sync_remote.events AS remote_events
|
|
245
|
-
WHERE remote_events.id = local_events.id
|
|
246
|
-
);
|
|
247
|
-
`,
|
|
248
|
-
)
|
|
249
|
-
.get({ "@branch": currentBranch }) as { count: number } | null;
|
|
250
|
-
|
|
251
|
-
return row?.count ?? 0;
|
|
252
|
-
} finally {
|
|
253
|
-
localDb.query("DETACH DATABASE sync_remote;").run();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
221
|
function buildSyncErrorHints(diagnostics: {
|
|
258
222
|
malformedPayloadEvents: number;
|
|
259
223
|
applyRejectedEvents: number;
|
|
@@ -700,21 +664,18 @@ export function syncStatus(cwd: string, sourceBranch: string): SyncStatusSummary
|
|
|
700
664
|
try {
|
|
701
665
|
persistGitContext(storage.db, git);
|
|
702
666
|
|
|
703
|
-
const cursor = loadCursor(storage.db, sourceBranch);
|
|
667
|
+
const cursor = loadCursor(storage.db, git.worktreePath, sourceBranch);
|
|
704
668
|
const cursorToken: string = cursor?.cursor_token ?? "0:";
|
|
705
|
-
const
|
|
669
|
+
const onSourceBranch: boolean = git.branchName !== null && git.branchName === sourceBranch;
|
|
706
670
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
} finally {
|
|
716
|
-
remote.close();
|
|
717
|
-
}
|
|
671
|
+
return {
|
|
672
|
+
sourceBranch,
|
|
673
|
+
ahead: countAhead(storage.db, git.branchName, sourceBranch),
|
|
674
|
+
behind: onSourceBranch ? 0 : countBranchEventsSince(storage.db, sourceBranch, cursorToken),
|
|
675
|
+
pendingConflicts: countPendingConflicts(storage.db),
|
|
676
|
+
sameBranch: onSourceBranch,
|
|
677
|
+
git,
|
|
678
|
+
};
|
|
718
679
|
} finally {
|
|
719
680
|
storage.close();
|
|
720
681
|
}
|
|
@@ -722,15 +683,48 @@ export function syncStatus(cwd: string, sourceBranch: string): SyncStatusSummary
|
|
|
722
683
|
|
|
723
684
|
export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
724
685
|
const storage = openTrekoonDatabase(cwd);
|
|
725
|
-
const git = resolveGitContext(cwd);
|
|
726
|
-
persistGitContext(storage.db, git);
|
|
727
|
-
|
|
728
|
-
const remote = openBranchDatabaseSnapshot(sourceBranch, cwd);
|
|
729
686
|
|
|
730
687
|
try {
|
|
731
|
-
const
|
|
688
|
+
const git = resolveGitContext(cwd);
|
|
689
|
+
persistGitContext(storage.db, git);
|
|
690
|
+
const cursor = loadCursor(storage.db, git.worktreePath, sourceBranch);
|
|
732
691
|
const cursorToken = cursor?.cursor_token ?? "0:";
|
|
733
|
-
const incomingEvents: StoredEvent[] =
|
|
692
|
+
const incomingEvents: StoredEvent[] = queryBranchEventsSince(storage.db, sourceBranch, cursorToken) as StoredEvent[];
|
|
693
|
+
|
|
694
|
+
// Same-branch fast path: skip conflict detection when already on sourceBranch.
|
|
695
|
+
// Null branchName (detached HEAD) falls through to the normal path.
|
|
696
|
+
if (git.branchName !== null && git.branchName === sourceBranch) {
|
|
697
|
+
let lastToken: string | null = null;
|
|
698
|
+
let lastEventAt: number | null = cursor?.last_event_at ?? null;
|
|
699
|
+
|
|
700
|
+
storage.db.transaction((): void => {
|
|
701
|
+
for (const incoming of incomingEvents) {
|
|
702
|
+
storeEvent(storage.db, incoming);
|
|
703
|
+
lastToken = cursorTokenFromEvent(incoming);
|
|
704
|
+
lastEventAt = incoming.created_at;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (lastToken) {
|
|
708
|
+
saveCursor(storage.db, git.worktreePath, sourceBranch, lastToken, lastEventAt);
|
|
709
|
+
}
|
|
710
|
+
})();
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
sourceBranch,
|
|
714
|
+
scannedEvents: incomingEvents.length,
|
|
715
|
+
appliedEvents: 0,
|
|
716
|
+
createdConflicts: 0,
|
|
717
|
+
cursorToken: lastToken,
|
|
718
|
+
sameBranch: true,
|
|
719
|
+
diagnostics: {
|
|
720
|
+
malformedPayloadEvents: 0,
|
|
721
|
+
applyRejectedEvents: 0,
|
|
722
|
+
quarantinedEvents: 0,
|
|
723
|
+
conflictEvents: 0,
|
|
724
|
+
errorHints: [],
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
}
|
|
734
728
|
|
|
735
729
|
let appliedEvents = 0;
|
|
736
730
|
let createdConflicts = 0;
|
|
@@ -805,7 +799,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
805
799
|
}
|
|
806
800
|
|
|
807
801
|
if (lastToken) {
|
|
808
|
-
saveCursor(storage.db, sourceBranch, lastToken, lastEventAt);
|
|
802
|
+
saveCursor(storage.db, git.worktreePath, sourceBranch, lastToken, lastEventAt);
|
|
809
803
|
}
|
|
810
804
|
})();
|
|
811
805
|
|
|
@@ -815,6 +809,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
815
809
|
appliedEvents,
|
|
816
810
|
createdConflicts,
|
|
817
811
|
cursorToken: lastToken,
|
|
812
|
+
sameBranch: false,
|
|
818
813
|
diagnostics: {
|
|
819
814
|
malformedPayloadEvents,
|
|
820
815
|
applyRejectedEvents,
|
|
@@ -828,7 +823,6 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
828
823
|
},
|
|
829
824
|
};
|
|
830
825
|
} finally {
|
|
831
|
-
remote.close();
|
|
832
826
|
storage.close();
|
|
833
827
|
}
|
|
834
828
|
}
|
package/src/sync/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface SyncStatusSummary {
|
|
|
12
12
|
readonly ahead: number;
|
|
13
13
|
readonly behind: number;
|
|
14
14
|
readonly pendingConflicts: number;
|
|
15
|
+
readonly sameBranch: boolean;
|
|
15
16
|
readonly git: GitContextSnapshot;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -21,6 +22,7 @@ export interface PullSummary {
|
|
|
21
22
|
readonly appliedEvents: number;
|
|
22
23
|
readonly createdConflicts: number;
|
|
23
24
|
readonly cursorToken: string | null;
|
|
25
|
+
readonly sameBranch: boolean;
|
|
24
26
|
readonly diagnostics: SyncPullDiagnostics;
|
|
25
27
|
}
|
|
26
28
|
|