@jamesaphoenix/tx 0.1.0

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.
@@ -0,0 +1,244 @@
1
+ import { Context, Effect, Layer, Schema } from "effect";
2
+ import { writeFileSync, renameSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { DatabaseError, ValidationError } from "../errors.js";
5
+ import { TaskService } from "./task-service.js";
6
+ import { TaskRepository } from "../repo/task-repo.js";
7
+ import { DependencyRepository } from "../repo/dep-repo.js";
8
+ import { SyncOperation as SyncOperationSchema } from "../schemas/sync.js";
9
+ /**
10
+ * SyncService provides JSONL-based export/import for git-tracked task syncing.
11
+ * See DD-009 for full specification.
12
+ */
13
+ export class SyncService extends Context.Tag("SyncService")() {
14
+ }
15
+ const DEFAULT_JSONL_PATH = ".tx/tasks.jsonl";
16
+ /**
17
+ * Convert a Task to a TaskUpsertOp for JSONL export.
18
+ */
19
+ const taskToUpsertOp = (task) => ({
20
+ v: 1,
21
+ op: "upsert",
22
+ ts: task.updatedAt.toISOString(),
23
+ id: task.id,
24
+ data: {
25
+ title: task.title,
26
+ description: task.description,
27
+ status: task.status,
28
+ score: task.score,
29
+ parentId: task.parentId,
30
+ metadata: task.metadata
31
+ }
32
+ });
33
+ /**
34
+ * Convert a TaskDependency to a DepAddOp for JSONL export.
35
+ */
36
+ const depToAddOp = (dep) => ({
37
+ v: 1,
38
+ op: "dep_add",
39
+ ts: dep.createdAt.toISOString(),
40
+ blockerId: dep.blockerId,
41
+ blockedId: dep.blockedId
42
+ });
43
+ /**
44
+ * Write content to file atomically using temp file + rename.
45
+ */
46
+ const atomicWrite = (filePath, content) => {
47
+ const dir = dirname(filePath);
48
+ if (!existsSync(dir)) {
49
+ mkdirSync(dir, { recursive: true });
50
+ }
51
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
52
+ writeFileSync(tempPath, content, "utf-8");
53
+ renameSync(tempPath, filePath);
54
+ };
55
+ export const SyncServiceLive = Layer.effect(SyncService, Effect.gen(function* () {
56
+ const taskService = yield* TaskService;
57
+ const taskRepo = yield* TaskRepository;
58
+ const depRepo = yield* DependencyRepository;
59
+ return {
60
+ export: (path) => Effect.gen(function* () {
61
+ const filePath = resolve(path ?? DEFAULT_JSONL_PATH);
62
+ // Get all tasks and dependencies
63
+ const tasks = yield* taskService.list();
64
+ const deps = yield* depRepo.getAll();
65
+ // Convert to sync operations
66
+ const taskOps = tasks.map(taskToUpsertOp);
67
+ const depOps = deps.map(depToAddOp);
68
+ // Combine and sort by timestamp
69
+ const allOps = [...taskOps, ...depOps].sort((a, b) => a.ts.localeCompare(b.ts));
70
+ // Convert to JSONL format (one JSON object per line)
71
+ const jsonl = allOps.map(op => JSON.stringify(op)).join("\n");
72
+ // Write atomically
73
+ yield* Effect.try({
74
+ try: () => atomicWrite(filePath, jsonl + (jsonl.length > 0 ? "\n" : "")),
75
+ catch: (cause) => new DatabaseError({ cause })
76
+ });
77
+ return {
78
+ opCount: allOps.length,
79
+ path: filePath
80
+ };
81
+ }),
82
+ import: (path) => Effect.gen(function* () {
83
+ const filePath = resolve(path ?? DEFAULT_JSONL_PATH);
84
+ // Check if file exists
85
+ if (!existsSync(filePath)) {
86
+ return { imported: 0, skipped: 0, conflicts: 0 };
87
+ }
88
+ // Read and parse JSONL file
89
+ const content = yield* Effect.try({
90
+ try: () => readFileSync(filePath, "utf-8"),
91
+ catch: (cause) => new DatabaseError({ cause })
92
+ });
93
+ const lines = content.trim().split("\n").filter(Boolean);
94
+ if (lines.length === 0) {
95
+ return { imported: 0, skipped: 0, conflicts: 0 };
96
+ }
97
+ // Parse all operations with Schema validation
98
+ const ops = [];
99
+ for (const line of lines) {
100
+ const parsed = yield* Effect.try({
101
+ try: () => JSON.parse(line),
102
+ catch: (cause) => new ValidationError({ reason: `Invalid JSON: ${cause}` })
103
+ });
104
+ const op = yield* Schema.decodeUnknown(SyncOperationSchema)(parsed).pipe(Effect.mapError((cause) => new ValidationError({ reason: `Schema validation failed: ${cause}` })));
105
+ ops.push(op);
106
+ }
107
+ // Group by entity and find latest state per entity (timestamp wins)
108
+ const taskStates = new Map();
109
+ const depStates = new Map();
110
+ for (const op of ops) {
111
+ if (op.op === "upsert" || op.op === "delete") {
112
+ const existing = taskStates.get(op.id);
113
+ if (!existing || op.ts > existing.ts) {
114
+ taskStates.set(op.id, { op: op, ts: op.ts });
115
+ }
116
+ }
117
+ else if (op.op === "dep_add" || op.op === "dep_remove") {
118
+ const key = `${op.blockerId}:${op.blockedId}`;
119
+ const existing = depStates.get(key);
120
+ if (!existing || op.ts > existing.ts) {
121
+ depStates.set(key, { op: op, ts: op.ts });
122
+ }
123
+ }
124
+ }
125
+ let imported = 0;
126
+ let skipped = 0;
127
+ let conflicts = 0;
128
+ // Apply task operations
129
+ for (const [id, { op }] of taskStates) {
130
+ if (op.op === "upsert") {
131
+ const existing = yield* taskRepo.findById(id);
132
+ if (!existing) {
133
+ // Create new task with the specified ID
134
+ const now = new Date();
135
+ const task = {
136
+ id: id,
137
+ title: op.data.title,
138
+ description: op.data.description,
139
+ status: op.data.status,
140
+ parentId: op.data.parentId,
141
+ score: op.data.score,
142
+ createdAt: new Date(op.ts),
143
+ updatedAt: new Date(op.ts),
144
+ completedAt: op.data.status === "done" ? now : null,
145
+ metadata: op.data.metadata
146
+ };
147
+ yield* taskRepo.insert(task);
148
+ imported++;
149
+ }
150
+ else {
151
+ // Update if JSONL timestamp is newer than existing
152
+ const existingTs = existing.updatedAt.toISOString();
153
+ if (op.ts > existingTs) {
154
+ const updated = {
155
+ ...existing,
156
+ title: op.data.title,
157
+ description: op.data.description,
158
+ status: op.data.status,
159
+ parentId: op.data.parentId,
160
+ score: op.data.score,
161
+ updatedAt: new Date(op.ts),
162
+ completedAt: op.data.status === "done" ? (existing.completedAt ?? new Date()) : null,
163
+ metadata: op.data.metadata
164
+ };
165
+ yield* taskRepo.update(updated);
166
+ imported++;
167
+ }
168
+ else if (op.ts === existingTs) {
169
+ // Same timestamp - skip
170
+ skipped++;
171
+ }
172
+ else {
173
+ // Local is newer - conflict
174
+ conflicts++;
175
+ }
176
+ }
177
+ }
178
+ else if (op.op === "delete") {
179
+ const existing = yield* taskRepo.findById(id);
180
+ if (existing) {
181
+ yield* taskRepo.remove(id);
182
+ imported++;
183
+ }
184
+ }
185
+ }
186
+ // Apply dependency operations
187
+ for (const [_key, { op }] of depStates) {
188
+ if (op.op === "dep_add") {
189
+ // Add dependency, ignore if already exists
190
+ yield* depRepo.insert(op.blockerId, op.blockedId).pipe(Effect.catchAll(() => Effect.void));
191
+ }
192
+ else if (op.op === "dep_remove") {
193
+ // Remove dependency, ignore if doesn't exist
194
+ yield* depRepo.remove(op.blockerId, op.blockedId).pipe(Effect.catchAll(() => Effect.void));
195
+ }
196
+ }
197
+ return { imported, skipped, conflicts };
198
+ }),
199
+ status: () => Effect.gen(function* () {
200
+ const filePath = resolve(DEFAULT_JSONL_PATH);
201
+ // Count tasks in database
202
+ const tasks = yield* taskService.list();
203
+ const dbTaskCount = tasks.length;
204
+ // Count operations in JSONL file and get file info
205
+ let jsonlOpCount = 0;
206
+ let lastExport = null;
207
+ if (existsSync(filePath)) {
208
+ // Get file modification time as lastExport
209
+ const stats = yield* Effect.try({
210
+ try: () => statSync(filePath),
211
+ catch: (cause) => new DatabaseError({ cause })
212
+ });
213
+ lastExport = stats.mtime;
214
+ // Count non-empty lines (each line is one operation)
215
+ const content = yield* Effect.try({
216
+ try: () => readFileSync(filePath, "utf-8"),
217
+ catch: (cause) => new DatabaseError({ cause })
218
+ });
219
+ const lines = content.trim().split("\n").filter(Boolean);
220
+ jsonlOpCount = lines.length;
221
+ }
222
+ // Determine if dirty: DB has changes not in JSONL
223
+ // Dirty if:
224
+ // 1. JSONL doesn't exist and DB has tasks, OR
225
+ // 2. Any task's updatedAt is newer than JSONL file mtime
226
+ let isDirty = false;
227
+ if (dbTaskCount > 0 && !existsSync(filePath)) {
228
+ isDirty = true;
229
+ }
230
+ else if (lastExport !== null && tasks.length > 0) {
231
+ // Check if any task was updated after the last export
232
+ isDirty = tasks.some(task => task.updatedAt > lastExport);
233
+ }
234
+ return {
235
+ dbTaskCount,
236
+ jsonlOpCount,
237
+ lastExport,
238
+ lastImport: null, // Would require additional state tracking
239
+ isDirty
240
+ };
241
+ })
242
+ };
243
+ }));
244
+ //# sourceMappingURL=sync-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-service.js","sourceRoot":"","sources":["../../src/services/sync-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,201 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { TaskRepository } from "../repo/task-repo.js";
3
+ import { DependencyRepository } from "../repo/dep-repo.js";
4
+ import { TaskNotFoundError, ValidationError } from "../errors.js";
5
+ import { generateTaskId } from "../id.js";
6
+ import { isValidTransition, isValidStatus } from "../schema.js";
7
+ export class TaskService extends Context.Tag("TaskService")() {
8
+ }
9
+ export const TaskServiceLive = Layer.effect(TaskService, Effect.gen(function* () {
10
+ const taskRepo = yield* TaskRepository;
11
+ const depRepo = yield* DependencyRepository;
12
+ const enrichWithDeps = (task) => Effect.gen(function* () {
13
+ const blockerIds = yield* depRepo.getBlockerIds(task.id);
14
+ const blockingIds = yield* depRepo.getBlockingIds(task.id);
15
+ const childIds = yield* taskRepo.getChildIds(task.id);
16
+ let isReady = ["backlog", "ready", "planning"].includes(task.status);
17
+ if (isReady && blockerIds.length > 0) {
18
+ const blockers = yield* taskRepo.findByIds(blockerIds);
19
+ isReady = blockers.every(b => b.status === "done");
20
+ }
21
+ return {
22
+ ...task,
23
+ blockedBy: blockerIds,
24
+ blocks: blockingIds,
25
+ children: childIds,
26
+ isReady
27
+ };
28
+ });
29
+ // Batch version of enrichWithDeps - avoids N+1 queries
30
+ const enrichWithDepsBatch = (tasks) => Effect.gen(function* () {
31
+ if (tasks.length === 0)
32
+ return [];
33
+ const taskIds = tasks.map(t => t.id);
34
+ // Batch fetch all dependency info (3 queries total instead of 3N)
35
+ const blockerIdsMap = yield* depRepo.getBlockerIdsForMany(taskIds);
36
+ const blockingIdsMap = yield* depRepo.getBlockingIdsForMany(taskIds);
37
+ const childIdsMap = yield* taskRepo.getChildIdsForMany(taskIds);
38
+ // Collect all unique blocker IDs to fetch their status
39
+ const allBlockerIds = new Set();
40
+ for (const blockerIds of blockerIdsMap.values()) {
41
+ for (const id of blockerIds) {
42
+ allBlockerIds.add(id);
43
+ }
44
+ }
45
+ // Fetch all blocker tasks to check their status (1 query instead of N)
46
+ const blockerTasks = allBlockerIds.size > 0
47
+ ? yield* taskRepo.findByIds([...allBlockerIds])
48
+ : [];
49
+ const blockerStatusMap = new Map();
50
+ for (const t of blockerTasks) {
51
+ blockerStatusMap.set(t.id, t.status);
52
+ }
53
+ // Build TaskWithDeps for each task
54
+ const results = [];
55
+ for (const task of tasks) {
56
+ const blockerIds = blockerIdsMap.get(task.id) ?? [];
57
+ const blockingIds = blockingIdsMap.get(task.id) ?? [];
58
+ const childIds = childIdsMap.get(task.id) ?? [];
59
+ // Compute isReady
60
+ let isReady = ["backlog", "ready", "planning"].includes(task.status);
61
+ if (isReady && blockerIds.length > 0) {
62
+ isReady = blockerIds.every(bid => blockerStatusMap.get(bid) === "done");
63
+ }
64
+ results.push({
65
+ ...task,
66
+ blockedBy: blockerIds,
67
+ blocks: blockingIds,
68
+ children: childIds,
69
+ isReady
70
+ });
71
+ }
72
+ return results;
73
+ });
74
+ // Auto-complete parent task when all children are done (recursive)
75
+ const autoCompleteParent = (parentId, now) => Effect.gen(function* () {
76
+ const parent = yield* taskRepo.findById(parentId);
77
+ if (!parent || parent.status === "done")
78
+ return;
79
+ const childIds = yield* taskRepo.getChildIds(parentId);
80
+ if (childIds.length === 0)
81
+ return;
82
+ const children = yield* taskRepo.findByIds(childIds);
83
+ const allChildrenDone = children.every(c => c.status === "done");
84
+ if (allChildrenDone) {
85
+ const updatedParent = {
86
+ ...parent,
87
+ status: "done",
88
+ updatedAt: now,
89
+ completedAt: now
90
+ };
91
+ yield* taskRepo.update(updatedParent);
92
+ // Recursively check grandparent
93
+ if (parent.parentId) {
94
+ yield* autoCompleteParent(parent.parentId, now);
95
+ }
96
+ }
97
+ });
98
+ return {
99
+ create: (input) => Effect.gen(function* () {
100
+ if (!input.title || input.title.trim().length === 0) {
101
+ return yield* Effect.fail(new ValidationError({ reason: "Title is required" }));
102
+ }
103
+ if (input.parentId) {
104
+ const parent = yield* taskRepo.findById(input.parentId);
105
+ if (!parent) {
106
+ return yield* Effect.fail(new ValidationError({ reason: `Parent ${input.parentId} not found` }));
107
+ }
108
+ }
109
+ const id = yield* generateTaskId();
110
+ const now = new Date();
111
+ const task = {
112
+ id: id,
113
+ title: input.title.trim(),
114
+ description: input.description ?? "",
115
+ status: "backlog",
116
+ parentId: input.parentId ?? null,
117
+ score: input.score ?? 0,
118
+ createdAt: now,
119
+ updatedAt: now,
120
+ completedAt: null,
121
+ metadata: input.metadata ?? {}
122
+ };
123
+ yield* taskRepo.insert(task);
124
+ return task;
125
+ }),
126
+ get: (id) => Effect.gen(function* () {
127
+ const task = yield* taskRepo.findById(id);
128
+ if (!task) {
129
+ return yield* Effect.fail(new TaskNotFoundError({ id }));
130
+ }
131
+ return task;
132
+ }),
133
+ getWithDeps: (id) => Effect.gen(function* () {
134
+ const task = yield* taskRepo.findById(id);
135
+ if (!task) {
136
+ return yield* Effect.fail(new TaskNotFoundError({ id }));
137
+ }
138
+ return yield* enrichWithDeps(task);
139
+ }),
140
+ getWithDepsBatch: (ids) => Effect.gen(function* () {
141
+ if (ids.length === 0)
142
+ return [];
143
+ const tasks = yield* taskRepo.findByIds(ids);
144
+ return yield* enrichWithDepsBatch(tasks);
145
+ }),
146
+ update: (id, input) => Effect.gen(function* () {
147
+ const existing = yield* taskRepo.findById(id);
148
+ if (!existing) {
149
+ return yield* Effect.fail(new TaskNotFoundError({ id }));
150
+ }
151
+ if (input.status && !isValidStatus(input.status)) {
152
+ return yield* Effect.fail(new ValidationError({ reason: `Invalid status: ${input.status}` }));
153
+ }
154
+ if (input.status && input.status !== existing.status) {
155
+ if (!isValidTransition(existing.status, input.status)) {
156
+ return yield* Effect.fail(new ValidationError({
157
+ reason: `Invalid transition: ${existing.status} -> ${input.status}`
158
+ }));
159
+ }
160
+ }
161
+ if (input.parentId) {
162
+ const parent = yield* taskRepo.findById(input.parentId);
163
+ if (!parent) {
164
+ return yield* Effect.fail(new ValidationError({ reason: `Parent ${input.parentId} not found` }));
165
+ }
166
+ }
167
+ const now = new Date();
168
+ const isDone = input.status === "done" && existing.status !== "done";
169
+ const updated = {
170
+ ...existing,
171
+ title: input.title ?? existing.title,
172
+ description: input.description ?? existing.description,
173
+ status: input.status ?? existing.status,
174
+ parentId: input.parentId !== undefined ? input.parentId : existing.parentId,
175
+ score: input.score ?? existing.score,
176
+ updatedAt: now,
177
+ completedAt: isDone ? now : existing.completedAt,
178
+ metadata: input.metadata ? { ...existing.metadata, ...input.metadata } : existing.metadata
179
+ };
180
+ yield* taskRepo.update(updated);
181
+ // Auto-complete parent if all children are done
182
+ if (isDone && updated.parentId) {
183
+ yield* autoCompleteParent(updated.parentId, now);
184
+ }
185
+ return updated;
186
+ }),
187
+ remove: (id) => Effect.gen(function* () {
188
+ const task = yield* taskRepo.findById(id);
189
+ if (!task) {
190
+ return yield* Effect.fail(new TaskNotFoundError({ id }));
191
+ }
192
+ yield* taskRepo.remove(id);
193
+ }),
194
+ list: (filter) => taskRepo.findAll(filter),
195
+ listWithDeps: (filter) => Effect.gen(function* () {
196
+ const tasks = yield* taskRepo.findAll(filter);
197
+ return yield* enrichWithDepsBatch(tasks);
198
+ })
199
+ };
200
+ }));
201
+ //# sourceMappingURL=task-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-service.js","sourceRoot":"","sources":["../../src/services/task-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ -- Version: 001
2
+ -- Migration: initial
3
+
4
+ -- Core tasks table
5
+ CREATE TABLE IF NOT EXISTS tasks (
6
+ id TEXT PRIMARY KEY,
7
+ title TEXT NOT NULL,
8
+ description TEXT DEFAULT '',
9
+ status TEXT NOT NULL DEFAULT 'backlog'
10
+ CHECK (status IN (
11
+ 'backlog', 'ready', 'planning', 'active',
12
+ 'blocked', 'review', 'human_needs_to_review', 'done'
13
+ )),
14
+ parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
15
+ score INTEGER NOT NULL DEFAULT 0,
16
+ created_at TEXT NOT NULL,
17
+ updated_at TEXT NOT NULL,
18
+ completed_at TEXT,
19
+ metadata TEXT DEFAULT '{}'
20
+ );
21
+
22
+ -- Dependency relationships
23
+ CREATE TABLE IF NOT EXISTS task_dependencies (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ blocker_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
26
+ blocked_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
27
+ created_at TEXT NOT NULL,
28
+ UNIQUE(blocker_id, blocked_id),
29
+ CHECK (blocker_id != blocked_id)
30
+ );
31
+
32
+ -- Compaction history
33
+ CREATE TABLE IF NOT EXISTS compaction_log (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ compacted_at TEXT NOT NULL,
36
+ task_count INTEGER NOT NULL,
37
+ summary TEXT NOT NULL,
38
+ task_ids TEXT NOT NULL,
39
+ learnings_exported_to TEXT
40
+ );
41
+
42
+ -- Schema version tracking
43
+ CREATE TABLE IF NOT EXISTS schema_version (
44
+ version INTEGER PRIMARY KEY,
45
+ applied_at TEXT NOT NULL
46
+ );
47
+
48
+ -- Indexes
49
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
50
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
51
+ CREATE INDEX IF NOT EXISTS idx_tasks_score ON tasks(score DESC);
52
+ CREATE INDEX IF NOT EXISTS idx_tasks_created ON tasks(created_at);
53
+ CREATE INDEX IF NOT EXISTS idx_deps_blocker ON task_dependencies(blocker_id);
54
+ CREATE INDEX IF NOT EXISTS idx_deps_blocked ON task_dependencies(blocked_id);
55
+
56
+ -- Record this migration
57
+ INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (1, datetime('now'));
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@jamesaphoenix/tx",
3
+ "version": "0.1.0",
4
+ "description": "A lean task management system for AI agents and humans",
5
+ "type": "module",
6
+ "bin": {
7
+ "tx": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "migrations"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/jamesaphoenix/tx.git"
17
+ },
18
+ "keywords": [
19
+ "task-management",
20
+ "ai-agents",
21
+ "cli",
22
+ "effect-ts",
23
+ "mcp"
24
+ ],
25
+ "author": "James Phoenix",
26
+ "license": "MIT",
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "dev": "tsx src/cli.ts",
30
+ "test": "vitest --run",
31
+ "test:watch": "vitest",
32
+ "validate": "npm run build && npm test && npm link"
33
+ },
34
+ "dependencies": {
35
+ "@effect/cli": "^0.73.1",
36
+ "@effect/platform": "^0.94.2",
37
+ "@effect/platform-node": "^0.104.1",
38
+ "@modelcontextprotocol/sdk": "^1.25.3",
39
+ "better-sqlite3": "^11.7.0",
40
+ "effect": "^3.19.15"
41
+ },
42
+ "devDependencies": {
43
+ "@types/better-sqlite3": "^7.6.12",
44
+ "@types/node": "^22.10.0",
45
+ "tsx": "^4.19.0",
46
+ "typescript": "^5.7.0",
47
+ "vitest": "^3.0.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ }
52
+ }