@tritard/waterbrother 0.5.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,178 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const TASKS_DIR_NAME = "tasks";
6
+ const INDEX_FILE = "index.json";
7
+ const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
8
+
9
+ const VALID_STATES = [
10
+ "decide-required",
11
+ "decision-ready",
12
+ "build-ready",
13
+ "review-ready",
14
+ "accepted",
15
+ "closed"
16
+ ];
17
+
18
+ function tasksDir(cwd) {
19
+ return path.join(cwd, ".waterbrother", TASKS_DIR_NAME);
20
+ }
21
+
22
+ function taskFilePath(cwd, taskId) {
23
+ return path.join(tasksDir(cwd), `${taskId}.json`);
24
+ }
25
+
26
+ function indexFilePath(cwd) {
27
+ return path.join(tasksDir(cwd), INDEX_FILE);
28
+ }
29
+
30
+ function slugify(name) {
31
+ return String(name || "")
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9]+/g, "-")
34
+ .replace(/^-|-$/g, "")
35
+ .slice(0, 60);
36
+ }
37
+
38
+ function makeTaskId(name) {
39
+ const slug = slugify(name);
40
+ const rand = crypto.randomBytes(3).toString("hex");
41
+ return slug ? `task_${slug}-${rand}` : `task_${rand}`;
42
+ }
43
+
44
+ async function ensureTasksDir(cwd) {
45
+ await fs.mkdir(tasksDir(cwd), { recursive: true });
46
+ }
47
+
48
+ async function readJsonSafe(filePath) {
49
+ try {
50
+ const raw = await fs.readFile(filePath, "utf8");
51
+ return JSON.parse(raw);
52
+ } catch (error) {
53
+ if (error?.code === "ENOENT") return null;
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async function readIndex(cwd) {
59
+ const data = await readJsonSafe(indexFilePath(cwd));
60
+ if (!data || typeof data !== "object") return { activeTaskId: null, taskIds: [] };
61
+ return {
62
+ activeTaskId: data.activeTaskId || null,
63
+ taskIds: Array.isArray(data.taskIds) ? data.taskIds : []
64
+ };
65
+ }
66
+
67
+ async function writeIndex(cwd, index) {
68
+ await ensureTasksDir(cwd);
69
+ await fs.writeFile(indexFilePath(cwd), `${JSON.stringify(index, null, 2)}\n`, "utf8");
70
+ }
71
+
72
+ export async function createTask({ cwd, name, sessionId, mode, autonomy }) {
73
+ if (!cwd) throw new Error("cwd is required");
74
+ if (!name) throw new Error("task name is required");
75
+
76
+ const id = makeTaskId(name);
77
+ const now = new Date().toISOString();
78
+ const task = {
79
+ id,
80
+ name: String(name).trim(),
81
+ cwd,
82
+ sessionId: sessionId || null,
83
+ branch: null,
84
+ state: "decide-required",
85
+ goal: "",
86
+ decisionId: null,
87
+ chosenOption: null,
88
+ activeContract: null,
89
+ currentCheckpoint: null,
90
+ latestReceiptId: null,
91
+ lastVerdict: null,
92
+ experienceMode: mode || "standard",
93
+ autonomyMode: autonomy || "scoped",
94
+ accepted: false,
95
+ createdAt: now,
96
+ updatedAt: now
97
+ };
98
+
99
+ await ensureTasksDir(cwd);
100
+ await fs.writeFile(taskFilePath(cwd, id), `${JSON.stringify(task, null, 2)}\n`, "utf8");
101
+
102
+ const index = await readIndex(cwd);
103
+ if (!index.taskIds.includes(id)) {
104
+ index.taskIds.push(id);
105
+ }
106
+ index.activeTaskId = id;
107
+ await writeIndex(cwd, index);
108
+
109
+ return task;
110
+ }
111
+
112
+ export async function loadTask({ cwd, taskId }) {
113
+ if (!cwd || !taskId) return null;
114
+ return readJsonSafe(taskFilePath(cwd, taskId));
115
+ }
116
+
117
+ export async function saveTask({ cwd, task }) {
118
+ if (!cwd || !task?.id) throw new Error("cwd and task.id are required");
119
+ const next = { ...task, updatedAt: new Date().toISOString() };
120
+ await ensureTasksDir(cwd);
121
+ await fs.writeFile(taskFilePath(cwd, next.id), `${JSON.stringify(next, null, 2)}\n`, "utf8");
122
+ return next;
123
+ }
124
+
125
+ export async function listTasks({ cwd, limit = 20 }) {
126
+ const index = await readIndex(cwd);
127
+ const safeLimit = Math.max(1, Math.min(100, Number(limit) || 20));
128
+ const tasks = [];
129
+
130
+ for (const taskId of index.taskIds) {
131
+ const task = await loadTask({ cwd, taskId });
132
+ if (task) tasks.push(task);
133
+ }
134
+
135
+ return tasks
136
+ .sort((a, b) => Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0))
137
+ .slice(0, safeLimit);
138
+ }
139
+
140
+ export async function findTaskByName({ cwd, name }) {
141
+ if (!name) return null;
142
+ const normalized = String(name).trim().toLowerCase();
143
+ const tasks = await listTasks({ cwd, limit: 100 });
144
+ return tasks.find((t) => t.name.toLowerCase() === normalized) || null;
145
+ }
146
+
147
+ export async function setActiveTask({ cwd, taskId }) {
148
+ const index = await readIndex(cwd);
149
+ index.activeTaskId = taskId || null;
150
+ await writeIndex(cwd, index);
151
+ }
152
+
153
+ export async function getActiveTask({ cwd }) {
154
+ const index = await readIndex(cwd);
155
+ if (!index.activeTaskId) return null;
156
+ return loadTask({ cwd, taskId: index.activeTaskId });
157
+ }
158
+
159
+ export async function closeTask({ cwd, taskId }) {
160
+ const task = await loadTask({ cwd, taskId });
161
+ if (!task) return null;
162
+ task.state = "closed";
163
+ task.updatedAt = new Date().toISOString();
164
+ await saveTask({ cwd, task });
165
+
166
+ const index = await readIndex(cwd);
167
+ if (index.activeTaskId === taskId) {
168
+ index.activeTaskId = null;
169
+ await writeIndex(cwd, index);
170
+ }
171
+ return task;
172
+ }
173
+
174
+ export function isValidTaskState(state) {
175
+ return VALID_STATES.includes(state);
176
+ }
177
+
178
+ export { VALID_STATES, slugify };