trekoon 0.1.6 → 0.1.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 +39 -15
- package/README.md +124 -10
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +13 -0
- package/src/commands/dep.ts +20 -1
- package/src/commands/epic.ts +72 -7
- package/src/commands/help.ts +255 -17
- package/src/commands/quickstart.ts +88 -24
- package/src/commands/skills.ts +177 -14
- package/src/commands/subtask.ts +76 -6
- package/src/commands/sync.ts +4 -0
- package/src/commands/task.ts +299 -7
- package/src/domain/tracker-domain.ts +113 -7
- package/src/domain/types.ts +7 -0
- package/src/runtime/cli-shell.ts +1 -2
- package/src/runtime/version.ts +20 -0
- package/src/storage/migrations.ts +48 -1
- package/src/sync/service.ts +67 -9
- package/src/sync/types.ts +9 -0
|
@@ -278,11 +278,58 @@ function recordMigration(db: Database, migration: Migration): void {
|
|
|
278
278
|
);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
function isSchemaCurrentFastPath(db: Database, latestVersion: number): boolean {
|
|
282
|
+
if (latestVersion === 0 || !migrationTableExists(db) || !hasMigrationVersionColumn(db)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const row = db
|
|
287
|
+
.query(
|
|
288
|
+
`
|
|
289
|
+
SELECT
|
|
290
|
+
COALESCE(MIN(version), 0) AS min_version,
|
|
291
|
+
COALESCE(MAX(version), 0) AS max_version,
|
|
292
|
+
COUNT(DISTINCT version) AS distinct_versions,
|
|
293
|
+
SUM(CASE WHEN version IS NULL THEN 1 ELSE 0 END) AS null_versions
|
|
294
|
+
FROM schema_migrations;
|
|
295
|
+
`,
|
|
296
|
+
)
|
|
297
|
+
.get() as
|
|
298
|
+
| {
|
|
299
|
+
min_version: number;
|
|
300
|
+
max_version: number;
|
|
301
|
+
distinct_versions: number;
|
|
302
|
+
null_versions: number;
|
|
303
|
+
}
|
|
304
|
+
| null;
|
|
305
|
+
|
|
306
|
+
if (!row) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
row.null_versions === 0 &&
|
|
312
|
+
row.min_version === 1 &&
|
|
313
|
+
row.max_version === latestVersion &&
|
|
314
|
+
row.distinct_versions === latestVersion
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
281
318
|
export function migrateDatabase(db: Database): void {
|
|
319
|
+
validateMigrationPlan();
|
|
320
|
+
|
|
321
|
+
const latestVersion: number = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
322
|
+
|
|
323
|
+
// Fast path: avoid BEGIN EXCLUSIVE when schema is already current.
|
|
324
|
+
// This reduces startup lock contention while keeping the explicit
|
|
325
|
+
// transactional migration path for non-current/legacy schemas.
|
|
326
|
+
if (isSchemaCurrentFastPath(db, latestVersion)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
282
330
|
runExclusive(db, (): void => {
|
|
283
331
|
ensureMigrationTable(db);
|
|
284
332
|
ensureMigrationVersionColumn(db);
|
|
285
|
-
validateMigrationPlan();
|
|
286
333
|
|
|
287
334
|
const version: number = currentVersion(db);
|
|
288
335
|
|
package/src/sync/service.ts
CHANGED
|
@@ -225,17 +225,55 @@ function countBehind(remoteDb: Database, cursorToken: string): number {
|
|
|
225
225
|
return row?.count ?? 0;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
}
|
|
231
255
|
}
|
|
232
256
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
257
|
+
function buildSyncErrorHints(diagnostics: {
|
|
258
|
+
malformedPayloadEvents: number;
|
|
259
|
+
applyRejectedEvents: number;
|
|
260
|
+
conflictEvents: number;
|
|
261
|
+
}): string[] {
|
|
262
|
+
const hints: string[] = [];
|
|
263
|
+
|
|
264
|
+
if (diagnostics.malformedPayloadEvents > 0) {
|
|
265
|
+
hints.push("Malformed event payloads were quarantined; inspect sync conflicts with field '__payload__'.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (diagnostics.applyRejectedEvents > 0) {
|
|
269
|
+
hints.push("Some events were quarantined as invalid; inspect sync conflicts with field '__apply__'.");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (diagnostics.conflictEvents > 0) {
|
|
273
|
+
hints.push("Field-level conflicts detected; run 'trekoon sync conflicts list' and resolve pending entries.");
|
|
274
|
+
}
|
|
237
275
|
|
|
238
|
-
return
|
|
276
|
+
return hints;
|
|
239
277
|
}
|
|
240
278
|
|
|
241
279
|
function readFieldValue(payload: EventPayload, field: string): unknown {
|
|
@@ -631,7 +669,7 @@ export function syncStatus(cwd: string, sourceBranch: string): SyncStatusSummary
|
|
|
631
669
|
try {
|
|
632
670
|
return {
|
|
633
671
|
sourceBranch,
|
|
634
|
-
ahead: countAhead(storage.db, git.branchName,
|
|
672
|
+
ahead: countAhead(storage.db, git.branchName, remote.path),
|
|
635
673
|
behind: countBehind(remote.db, cursorToken),
|
|
636
674
|
pendingConflicts: countPendingConflicts(storage.db),
|
|
637
675
|
git,
|
|
@@ -658,6 +696,10 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
658
696
|
|
|
659
697
|
let appliedEvents = 0;
|
|
660
698
|
let createdConflicts = 0;
|
|
699
|
+
let malformedPayloadEvents = 0;
|
|
700
|
+
let applyRejectedEvents = 0;
|
|
701
|
+
let quarantinedEvents = 0;
|
|
702
|
+
let conflictEvents = 0;
|
|
661
703
|
let lastToken: string | null = null;
|
|
662
704
|
let lastEventAt: number | null = cursor?.last_event_at ?? null;
|
|
663
705
|
|
|
@@ -666,6 +708,8 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
666
708
|
const payloadValidation = parsePayload(incoming.payload);
|
|
667
709
|
|
|
668
710
|
if (!payloadValidation.ok) {
|
|
711
|
+
malformedPayloadEvents += 1;
|
|
712
|
+
quarantinedEvents += 1;
|
|
669
713
|
createConflict(
|
|
670
714
|
storage.db,
|
|
671
715
|
incoming,
|
|
@@ -688,6 +732,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
688
732
|
const conflict = entityFieldConflict(storage.db, sourceBranch, incoming, fieldName, value);
|
|
689
733
|
|
|
690
734
|
if (conflict) {
|
|
735
|
+
conflictEvents += 1;
|
|
691
736
|
createConflict(storage.db, incoming, fieldName, conflict.oursValue, conflict.theirsValue);
|
|
692
737
|
createdConflicts += 1;
|
|
693
738
|
continue;
|
|
@@ -699,6 +744,8 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
699
744
|
if (applyEntityFields(storage.db, incoming, fieldsToApply)) {
|
|
700
745
|
appliedEvents += 1;
|
|
701
746
|
} else if (incoming.operation !== "resolve_conflict") {
|
|
747
|
+
applyRejectedEvents += 1;
|
|
748
|
+
quarantinedEvents += 1;
|
|
702
749
|
createConflict(
|
|
703
750
|
storage.db,
|
|
704
751
|
incoming,
|
|
@@ -726,6 +773,17 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
|
|
|
726
773
|
appliedEvents,
|
|
727
774
|
createdConflicts,
|
|
728
775
|
cursorToken: lastToken,
|
|
776
|
+
diagnostics: {
|
|
777
|
+
malformedPayloadEvents,
|
|
778
|
+
applyRejectedEvents,
|
|
779
|
+
quarantinedEvents,
|
|
780
|
+
conflictEvents,
|
|
781
|
+
errorHints: buildSyncErrorHints({
|
|
782
|
+
malformedPayloadEvents,
|
|
783
|
+
applyRejectedEvents,
|
|
784
|
+
conflictEvents,
|
|
785
|
+
}),
|
|
786
|
+
},
|
|
729
787
|
};
|
|
730
788
|
} finally {
|
|
731
789
|
remote.close();
|
package/src/sync/types.ts
CHANGED
|
@@ -21,6 +21,15 @@ export interface PullSummary {
|
|
|
21
21
|
readonly appliedEvents: number;
|
|
22
22
|
readonly createdConflicts: number;
|
|
23
23
|
readonly cursorToken: string | null;
|
|
24
|
+
readonly diagnostics: SyncPullDiagnostics;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SyncPullDiagnostics {
|
|
28
|
+
readonly malformedPayloadEvents: number;
|
|
29
|
+
readonly applyRejectedEvents: number;
|
|
30
|
+
readonly quarantinedEvents: number;
|
|
31
|
+
readonly conflictEvents: number;
|
|
32
|
+
readonly errorHints: readonly string[];
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export interface ResolveSummary {
|