harness-async 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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/dist/dashboard/assets/index-TGNGdtwt.js +246 -0
  4. package/dist/dashboard/assets/index-f4TpA4iP.css +1 -0
  5. package/dist/dashboard/index.html +13 -0
  6. package/dist/src/adapters/claude-adapter.js +52 -0
  7. package/dist/src/adapters/codex-adapter.js +55 -0
  8. package/dist/src/adapters/index.js +14 -0
  9. package/dist/src/adapters/shared.js +74 -0
  10. package/dist/src/cli/commands/daemon.js +116 -0
  11. package/dist/src/cli/commands/doctor.js +50 -0
  12. package/dist/src/cli/commands/hook.js +188 -0
  13. package/dist/src/cli/commands/init.js +22 -0
  14. package/dist/src/cli/commands/run.js +129 -0
  15. package/dist/src/cli/commands/schedule.js +105 -0
  16. package/dist/src/cli/commands/task.js +188 -0
  17. package/dist/src/cli/index.js +23 -0
  18. package/dist/src/cli/utils/notify.js +32 -0
  19. package/dist/src/cli/utils/output.js +94 -0
  20. package/dist/src/core/daemon.js +375 -0
  21. package/dist/src/core/dag.js +80 -0
  22. package/dist/src/core/event-log.js +34 -0
  23. package/dist/src/core/lock.js +25 -0
  24. package/dist/src/core/run-manager.js +265 -0
  25. package/dist/src/core/run-orchestrator.js +193 -0
  26. package/dist/src/core/scheduler.js +106 -0
  27. package/dist/src/core/sessions.js +48 -0
  28. package/dist/src/core/store.js +225 -0
  29. package/dist/src/core/task-manager.js +375 -0
  30. package/dist/src/core/tmux.js +51 -0
  31. package/dist/src/daemon.js +35 -0
  32. package/dist/src/dashboard/routes.js +107 -0
  33. package/dist/src/dashboard/server.js +142 -0
  34. package/dist/src/dashboard/ws.js +75 -0
  35. package/dist/src/types/adapter.js +30 -0
  36. package/dist/src/types/index.js +87 -0
  37. package/package.json +65 -0
@@ -0,0 +1,225 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { detectCycle } from './dag.js';
5
+ import { readEvents } from './event-log.js';
6
+ import { withLock } from './lock.js';
7
+ import { configSchema, indexFileSchema, taskFrontmatterSchema, taskSchema, } from '../types/index.js';
8
+ const INDEX_FILE = 'index.json';
9
+ function slugifyTitle(title) {
10
+ return title
11
+ .trim()
12
+ .toLowerCase()
13
+ .replace(/[^\p{Letter}\p{Number}]+/gu, '-')
14
+ .replace(/^-+|-+$/g, '')
15
+ .slice(0, 80);
16
+ }
17
+ function formatTaskFilename(task) {
18
+ const slug = slugifyTitle(task.title) || `task-${task.id}`;
19
+ return `${String(task.id).padStart(3, '0')}-${slug}.md`;
20
+ }
21
+ function serializeFrontmatter(task) {
22
+ return {
23
+ id: task.id,
24
+ title: task.title,
25
+ level: task.level,
26
+ status: task.status,
27
+ deps: task.deps,
28
+ blocks: task.blocks,
29
+ assignee: task.assignee,
30
+ scope: task.scope,
31
+ project: task.project ?? null,
32
+ created: task.created,
33
+ updated: task.updated,
34
+ tags: task.tags,
35
+ checkpoint: task.checkpoint,
36
+ estimated_effort: task.estimatedEffort,
37
+ };
38
+ }
39
+ function parseFrontmatter(raw) {
40
+ const parsed = matter(raw);
41
+ const frontmatter = taskFrontmatterSchema.parse({
42
+ ...parsed.data,
43
+ estimatedEffort: parsed.data.estimated_effort,
44
+ });
45
+ return taskSchema.parse({
46
+ ...frontmatter,
47
+ project: frontmatter.project ?? null,
48
+ body: parsed.content.trimStart(),
49
+ });
50
+ }
51
+ export function defaultConfig() {
52
+ return configSchema.parse({
53
+ defaultAgent: 'claude',
54
+ notify: {
55
+ enabled: true,
56
+ },
57
+ dashboardPort: 3777,
58
+ });
59
+ }
60
+ export function defaultIndexFile(now) {
61
+ return indexFileSchema.parse({
62
+ version: 1,
63
+ nextId: 1,
64
+ tasks: {},
65
+ dag: {
66
+ edges: [],
67
+ },
68
+ lastUpdated: now,
69
+ });
70
+ }
71
+ export async function createTaskFile(tasksDir, task) {
72
+ await mkdir(tasksDir, { recursive: true });
73
+ const filePath = join(tasksDir, formatTaskFilename(task));
74
+ const raw = matter.stringify(task.body, serializeFrontmatter(task));
75
+ await writeFile(filePath, raw, 'utf8');
76
+ return filePath;
77
+ }
78
+ export async function readTaskFile(filePath) {
79
+ const raw = await readFile(filePath, 'utf8');
80
+ return parseFrontmatter(raw);
81
+ }
82
+ export async function updateTaskFile(filePath, updates) {
83
+ const current = await readTaskFile(filePath);
84
+ const updated = taskSchema.parse({
85
+ ...current,
86
+ ...updates,
87
+ updated: updates.updated ?? new Date().toISOString(),
88
+ body: updates.body ?? current.body,
89
+ });
90
+ const raw = matter.stringify(updated.body, serializeFrontmatter(updated));
91
+ await writeFile(filePath, raw, 'utf8');
92
+ }
93
+ export async function deleteTaskFile(filePath) {
94
+ await rm(filePath, { force: true });
95
+ }
96
+ export async function readIndex(storeDir) {
97
+ const raw = await readFile(join(storeDir, INDEX_FILE), 'utf8');
98
+ return indexFileSchema.parse(JSON.parse(raw));
99
+ }
100
+ export async function readConfig(storeDir) {
101
+ const raw = await readFile(join(storeDir, 'config.json'), 'utf8');
102
+ return configSchema.parse(JSON.parse(raw));
103
+ }
104
+ export async function writeIndex(storeDir, index) {
105
+ const filePath = join(storeDir, INDEX_FILE);
106
+ const tempPath = `${filePath}.tmp`;
107
+ await mkdir(storeDir, { recursive: true });
108
+ await writeFile(tempPath, `${JSON.stringify(indexFileSchema.parse(index), null, 2)}\n`, 'utf8');
109
+ await rename(tempPath, filePath);
110
+ }
111
+ function indexRecordFromTask(task, file) {
112
+ return {
113
+ level: task.level,
114
+ status: task.status,
115
+ deps: [...task.deps],
116
+ file,
117
+ scope: task.scope,
118
+ };
119
+ }
120
+ export function addToIndex(index, task, file) {
121
+ index.tasks[String(task.id)] = indexRecordFromTask(task, file);
122
+ index.nextId = Math.max(index.nextId, task.id + 1);
123
+ index.lastUpdated = task.updated;
124
+ }
125
+ export function updateInIndex(index, taskId, updates) {
126
+ const key = String(taskId);
127
+ const current = index.tasks[key];
128
+ if (!current) {
129
+ throw new Error(`Task ${taskId} not found in index`);
130
+ }
131
+ index.tasks[key] = {
132
+ ...current,
133
+ ...updates,
134
+ };
135
+ }
136
+ export function getFromIndex(index, taskId) {
137
+ return index.tasks[String(taskId)];
138
+ }
139
+ export function addDependency(index, taskId, depId) {
140
+ const task = getFromIndex(index, taskId);
141
+ if (!task) {
142
+ throw new Error(`Task ${taskId} not found in index`);
143
+ }
144
+ if (!task.deps.includes(depId)) {
145
+ task.deps.push(depId);
146
+ }
147
+ const edgeExists = index.dag.edges.some((edge) => edge.from === String(depId) && edge.to === String(taskId));
148
+ if (!edgeExists) {
149
+ index.dag.edges.push({
150
+ from: String(depId),
151
+ to: String(taskId),
152
+ type: 'depends-on',
153
+ });
154
+ }
155
+ }
156
+ export function removeDependency(index, taskId, depId) {
157
+ const task = getFromIndex(index, taskId);
158
+ if (!task) {
159
+ throw new Error(`Task ${taskId} not found in index`);
160
+ }
161
+ task.deps = task.deps.filter((id) => id !== depId);
162
+ index.dag.edges = index.dag.edges.filter((edge) => !(edge.from === String(depId) && edge.to === String(taskId)));
163
+ }
164
+ export function getDependents(index, taskId) {
165
+ return index.dag.edges
166
+ .filter((edge) => edge.from === String(taskId))
167
+ .map((edge) => Number(edge.to));
168
+ }
169
+ export function getDependencies(index, taskId) {
170
+ return getFromIndex(index, taskId)?.deps ?? [];
171
+ }
172
+ function resolveHomeDir(homeDir) {
173
+ return homeDir ?? process.env.HA_HOME ?? process.env.HOME ?? '';
174
+ }
175
+ export function resolveStoreDir(options) {
176
+ if (options.scope === 'global') {
177
+ return join(resolveHomeDir(options.homeDir), '.ha');
178
+ }
179
+ return join(options.cwd, '.ha');
180
+ }
181
+ export async function initializeStore(options) {
182
+ const now = options.now ?? new Date().toISOString();
183
+ const storeDir = resolveStoreDir(options);
184
+ const tasksDir = join(storeDir, 'tasks');
185
+ let created = false;
186
+ await mkdir(tasksDir, { recursive: true });
187
+ async function writeJsonIfMissing(filePath, value) {
188
+ try {
189
+ await readFile(filePath, 'utf8');
190
+ }
191
+ catch (error) {
192
+ if (error.code !== 'ENOENT') {
193
+ throw error;
194
+ }
195
+ created = true;
196
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
197
+ }
198
+ }
199
+ async function writeTextIfMissing(filePath, value = '') {
200
+ try {
201
+ await readFile(filePath, 'utf8');
202
+ }
203
+ catch (error) {
204
+ if (error.code !== 'ENOENT') {
205
+ throw error;
206
+ }
207
+ created = true;
208
+ await writeFile(filePath, value, 'utf8');
209
+ }
210
+ }
211
+ await writeJsonIfMissing(join(storeDir, 'config.json'), defaultConfig());
212
+ await writeJsonIfMissing(join(storeDir, INDEX_FILE), defaultIndexFile(now));
213
+ await writeTextIfMissing(join(storeDir, 'events.ndjson'));
214
+ if (options.scope === 'global') {
215
+ await writeJsonIfMissing(join(storeDir, 'schedule.json'), []);
216
+ await writeJsonIfMissing(join(storeDir, 'daemon.json'), {});
217
+ await writeJsonIfMissing(join(storeDir, 'projects.json'), []);
218
+ }
219
+ return {
220
+ created,
221
+ scope: options.scope,
222
+ storeDir,
223
+ };
224
+ }
225
+ export { detectCycle, readEvents, withLock };
@@ -0,0 +1,375 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { addDependency, addToIndex, createTaskFile, getDependents, getDependencies, readIndex, readTaskFile, updateTaskFile, updateInIndex, resolveStoreDir, withLock, writeIndex, } from './store.js';
4
+ import { appendEvent } from './event-log.js';
5
+ import { readEvents } from './event-log.js';
6
+ import { getExecutionOrder } from './dag.js';
7
+ import { taskLevelSchema, taskStatusSchema } from '../types/index.js';
8
+ export async function createTask(input) {
9
+ const storeDir = resolveStoreDir({
10
+ cwd: input.cwd,
11
+ homeDir: input.homeDir,
12
+ scope: input.scope,
13
+ });
14
+ const indexPath = join(storeDir, 'index.json');
15
+ return withLock(indexPath, async () => {
16
+ const index = await readIndex(storeDir);
17
+ const now = new Date().toISOString();
18
+ const deps = input.deps ?? [];
19
+ for (const depId of deps) {
20
+ if (!index.tasks[String(depId)]) {
21
+ throw new Error(`Dependency task ${depId} not found`);
22
+ }
23
+ }
24
+ const importedBody = input.file ? await readFile(input.file, 'utf8') : '';
25
+ const title = input.title ?? (input.file ? basename(input.file) : undefined);
26
+ if (!title) {
27
+ throw new Error('Task title is required');
28
+ }
29
+ const task = {
30
+ id: index.nextId,
31
+ title,
32
+ level: taskLevelSchema.parse(input.level),
33
+ status: 'pending',
34
+ deps,
35
+ blocks: [],
36
+ assignee: input.assignee ?? 'auto',
37
+ scope: input.scope,
38
+ project: input.scope === 'project' ? input.cwd : null,
39
+ created: now,
40
+ updated: now,
41
+ tags: [],
42
+ checkpoint: null,
43
+ estimatedEffort: 'medium',
44
+ body: importedBody,
45
+ };
46
+ const filePath = await createTaskFile(join(storeDir, 'tasks'), task);
47
+ addToIndex(index, task, basename(filePath));
48
+ for (const depId of deps) {
49
+ addDependency(index, task.id, depId);
50
+ }
51
+ await writeIndex(storeDir, index);
52
+ await appendEvent(storeDir, {
53
+ ts: now,
54
+ type: 'task.created',
55
+ taskId: task.id,
56
+ actor: 'system',
57
+ detail: {
58
+ title: task.title,
59
+ level: task.level,
60
+ },
61
+ });
62
+ return { task, filePath };
63
+ });
64
+ }
65
+ export async function listTasks(input) {
66
+ const stores = await resolveTaskStores(input);
67
+ const tasks = (await Promise.all(stores.map(async (storeDir) => readTasksFromStore(storeDir)))).flat();
68
+ return tasks.filter((task) => {
69
+ if (input.level && task.level !== input.level) {
70
+ return false;
71
+ }
72
+ if (input.status && task.status !== input.status) {
73
+ return false;
74
+ }
75
+ return true;
76
+ });
77
+ }
78
+ export async function showTask(input) {
79
+ const stores = await resolveTaskStores({
80
+ cwd: input.cwd,
81
+ homeDir: input.homeDir,
82
+ scope: input.scope ?? 'project',
83
+ });
84
+ for (const storeDir of stores) {
85
+ try {
86
+ const index = await readIndex(storeDir);
87
+ const record = index.tasks[String(input.id)];
88
+ if (!record) {
89
+ continue;
90
+ }
91
+ return readFile(join(storeDir, 'tasks', record.file), 'utf8');
92
+ }
93
+ catch (error) {
94
+ if (error.code === 'ENOENT') {
95
+ continue;
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+ throw new Error(`Task ${input.id} not found`);
101
+ }
102
+ export async function getTask(input) {
103
+ const location = await findTaskLocation({
104
+ cwd: input.cwd,
105
+ homeDir: input.homeDir,
106
+ id: input.id,
107
+ scope: input.scope ?? 'project',
108
+ });
109
+ if (!location) {
110
+ throw new Error(`Task ${input.id} not found`);
111
+ }
112
+ return readTaskFile(location.filePath);
113
+ }
114
+ export async function getTaskEvents(input) {
115
+ const location = await findTaskLocation({
116
+ cwd: input.cwd,
117
+ homeDir: input.homeDir,
118
+ id: input.id,
119
+ scope: input.scope ?? 'project',
120
+ });
121
+ if (!location) {
122
+ throw new Error(`Task ${input.id} not found`);
123
+ }
124
+ return readEvents(location.storeDir, {
125
+ taskId: input.id,
126
+ });
127
+ }
128
+ export function transitionTask(task, newStatus, depsSatisfied) {
129
+ const currentStatus = task.status;
130
+ const allowed = (currentStatus === 'pending' && newStatus === 'running' && depsSatisfied) ||
131
+ (currentStatus === 'running' && newStatus === 'waiting-review' && task.level === 'L2') ||
132
+ (currentStatus === 'running' && (newStatus === 'completed' || newStatus === 'failed' || newStatus === 'paused')) ||
133
+ (currentStatus === 'waiting-review' && newStatus === 'completed') ||
134
+ (currentStatus === 'failed' && newStatus === 'running' && depsSatisfied) ||
135
+ (currentStatus === 'paused' && newStatus === 'running' && depsSatisfied);
136
+ if (!allowed) {
137
+ throw new Error(`Invalid status transition: ${currentStatus} -> ${newStatus}`);
138
+ }
139
+ return {
140
+ ...task,
141
+ status: newStatus,
142
+ updated: new Date().toISOString(),
143
+ };
144
+ }
145
+ export async function updateTaskStatus(input) {
146
+ const location = await findTaskLocation(input);
147
+ if (!location) {
148
+ throw new Error(`Task ${input.id} not found`);
149
+ }
150
+ return withLock(join(location.storeDir, 'index.json'), async () => {
151
+ const index = await readIndex(location.storeDir);
152
+ const task = await readTaskFile(location.filePath);
153
+ const depsSatisfied = getDependencies(index, task.id).every((depId) => index.tasks[String(depId)]?.status === 'completed');
154
+ const nextTask = transitionTask(task, taskStatusSchema.parse(input.status), depsSatisfied);
155
+ const nextBody = input.reason && nextTask.status === 'failed'
156
+ ? appendFailureReason(task.body, input.reason)
157
+ : task.body;
158
+ await updateTaskFile(location.filePath, {
159
+ ...nextTask,
160
+ body: nextBody,
161
+ });
162
+ updateInIndex(index, task.id, {
163
+ status: nextTask.status,
164
+ });
165
+ index.lastUpdated = nextTask.updated;
166
+ await writeIndex(location.storeDir, index);
167
+ await appendEvent(location.storeDir, {
168
+ ts: nextTask.updated,
169
+ type: 'task.status_changed',
170
+ taskId: task.id,
171
+ actor: input.actor,
172
+ from: task.status,
173
+ to: nextTask.status,
174
+ detail: input.reason ? { reason: input.reason } : {},
175
+ });
176
+ if (nextTask.status === 'completed') {
177
+ for (const dependentId of getDependents(index, task.id)) {
178
+ const unlocked = getDependencies(index, dependentId).every((depId) => index.tasks[String(depId)]?.status === 'completed');
179
+ if (unlocked) {
180
+ await appendEvent(location.storeDir, {
181
+ ts: nextTask.updated,
182
+ type: 'task.dependency_unlocked',
183
+ taskId: dependentId,
184
+ actor: input.actor,
185
+ detail: {
186
+ unlockedBy: task.id,
187
+ },
188
+ });
189
+ }
190
+ }
191
+ }
192
+ return {
193
+ ...nextTask,
194
+ body: nextBody,
195
+ };
196
+ });
197
+ }
198
+ export async function failTask(input) {
199
+ return updateTaskStatus({
200
+ ...input,
201
+ status: 'failed',
202
+ });
203
+ }
204
+ export async function getTaskGraph(input) {
205
+ const stores = await resolveTaskStores(input);
206
+ const tasks = (await Promise.all(stores.map(async (storeDir) => readTasksFromStore(storeDir)))).flat();
207
+ const edges = (await Promise.all(stores.map(async (storeDir) => {
208
+ try {
209
+ const index = await readIndex(storeDir);
210
+ return index.dag.edges.map((edge) => ({
211
+ from: Number(edge.from),
212
+ to: Number(edge.to),
213
+ }));
214
+ }
215
+ catch (error) {
216
+ if (error.code === 'ENOENT') {
217
+ return [];
218
+ }
219
+ throw error;
220
+ }
221
+ }))).flat();
222
+ const byId = new Map(tasks.map((task) => [task.id, task]));
223
+ const orderedTasks = getOrderedTasks(tasks, edges, byId);
224
+ return {
225
+ tasks: orderedTasks,
226
+ edges,
227
+ };
228
+ }
229
+ export async function findTaskLocation(input) {
230
+ return resolveTaskLocation(input);
231
+ }
232
+ export async function listRunnableTasks(input) {
233
+ const stores = await resolveTaskStores(input);
234
+ const tasksByStore = await Promise.all(stores.map(async (storeDir) => {
235
+ try {
236
+ const index = await readIndex(storeDir);
237
+ const tasks = await readTasksFromStore(storeDir);
238
+ return tasks.filter((task) => {
239
+ if (task.status !== 'pending') {
240
+ return false;
241
+ }
242
+ return getDependencies(index, task.id).every((depId) => index.tasks[String(depId)]?.status === 'completed');
243
+ });
244
+ }
245
+ catch (error) {
246
+ if (error.code === 'ENOENT') {
247
+ return [];
248
+ }
249
+ throw error;
250
+ }
251
+ }));
252
+ return tasksByStore.flat().filter((task) => {
253
+ if (input.level && task.level !== input.level) {
254
+ return false;
255
+ }
256
+ if (input.status && task.status !== input.status) {
257
+ return false;
258
+ }
259
+ return true;
260
+ });
261
+ }
262
+ async function resolveTaskStores(input) {
263
+ if (input.scope === 'project') {
264
+ return [resolveStoreDir({ cwd: input.cwd, homeDir: input.homeDir, scope: 'project' })];
265
+ }
266
+ if (input.scope === 'global') {
267
+ return [resolveStoreDir({ cwd: input.cwd, homeDir: input.homeDir, scope: 'global' })];
268
+ }
269
+ const globalStore = resolveStoreDir({
270
+ cwd: input.cwd,
271
+ homeDir: input.homeDir,
272
+ scope: 'global',
273
+ });
274
+ const projectStore = resolveStoreDir({
275
+ cwd: input.cwd,
276
+ homeDir: input.homeDir,
277
+ scope: 'project',
278
+ });
279
+ const stores = new Set([projectStore, globalStore]);
280
+ try {
281
+ const projects = JSON.parse(await readFile(join(globalStore, 'projects.json'), 'utf8'));
282
+ for (const projectDir of projects) {
283
+ stores.add(join(projectDir, '.ha'));
284
+ }
285
+ }
286
+ catch (error) {
287
+ if (error.code !== 'ENOENT') {
288
+ throw error;
289
+ }
290
+ }
291
+ return [...stores];
292
+ }
293
+ async function readTasksFromStore(storeDir) {
294
+ try {
295
+ const index = await readIndex(storeDir);
296
+ const records = Object.entries(index.tasks);
297
+ return Promise.all(records.map(async ([taskId, record]) => {
298
+ const task = await readTaskFile(join(storeDir, 'tasks', record.file));
299
+ return {
300
+ ...task,
301
+ id: Number(taskId),
302
+ };
303
+ }));
304
+ }
305
+ catch (error) {
306
+ if (error.code === 'ENOENT') {
307
+ return [];
308
+ }
309
+ throw error;
310
+ }
311
+ }
312
+ async function resolveTaskLocation(input) {
313
+ const stores = await resolveTaskStores({
314
+ cwd: input.cwd,
315
+ homeDir: input.homeDir,
316
+ scope: input.scope ?? 'project',
317
+ });
318
+ for (const storeDir of stores) {
319
+ try {
320
+ const index = await readIndex(storeDir);
321
+ const record = index.tasks[String(input.id)];
322
+ if (!record) {
323
+ continue;
324
+ }
325
+ return {
326
+ storeDir,
327
+ filePath: join(storeDir, 'tasks', record.file),
328
+ };
329
+ }
330
+ catch (error) {
331
+ if (error.code === 'ENOENT') {
332
+ continue;
333
+ }
334
+ throw error;
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+ function appendFailureReason(body, reason) {
340
+ const trimmedBody = body.trimEnd();
341
+ const prefix = trimmedBody.length > 0 ? `${trimmedBody}\n\n` : '';
342
+ return `${prefix}## Failure reason\n${reason}\n`;
343
+ }
344
+ function getOrderedTasks(tasks, edges, byId) {
345
+ const ids = tasks.map((task) => String(task.id));
346
+ const index = {
347
+ version: 1,
348
+ nextId: 1,
349
+ tasks: Object.fromEntries(tasks.map((task) => [
350
+ String(task.id),
351
+ {
352
+ level: task.level,
353
+ status: task.status,
354
+ deps: task.deps,
355
+ file: '',
356
+ scope: task.scope,
357
+ },
358
+ ])),
359
+ dag: {
360
+ edges: edges.map((edge) => ({
361
+ from: String(edge.from),
362
+ to: String(edge.to),
363
+ type: 'depends-on',
364
+ })),
365
+ },
366
+ lastUpdated: new Date().toISOString(),
367
+ };
368
+ const order = getExecutionOrder(index).filter((id) => ids.includes(String(id)));
369
+ if (order.length === 0) {
370
+ return [...tasks].sort((left, right) => left.id - right.id);
371
+ }
372
+ return order
373
+ .map((id) => byId.get(id))
374
+ .filter((task) => Boolean(task));
375
+ }
@@ -0,0 +1,51 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ export function createTmuxRunner(tmuxBin = 'tmux') {
5
+ return async (args, options = {}) => {
6
+ const result = await execFileAsync(tmuxBin, args, {
7
+ cwd: options.cwd,
8
+ });
9
+ return {
10
+ stdout: result.stdout,
11
+ stderr: result.stderr,
12
+ };
13
+ };
14
+ }
15
+ export async function createTmuxSession(input) {
16
+ const runTmux = input.runTmux ?? createTmuxRunner();
17
+ const args = ['new-session', '-d', '-s', input.sessionName];
18
+ if (input.cwd) {
19
+ args.push('-c', input.cwd);
20
+ }
21
+ args.push(input.command);
22
+ await runTmux(args, { cwd: input.cwd });
23
+ if (input.logFile) {
24
+ await runTmux(['pipe-pane', '-o', '-t', input.sessionName, `cat >> ${quoteShell(input.logFile)}`], { cwd: input.cwd });
25
+ }
26
+ }
27
+ export function attachTmuxSession(sessionName) {
28
+ return `tmux attach -t ${sessionName}`;
29
+ }
30
+ export async function killTmuxSession(sessionName, options = {}) {
31
+ const runTmux = options.runTmux ?? createTmuxRunner();
32
+ await runTmux(['kill-session', '-t', sessionName], { cwd: options.cwd });
33
+ }
34
+ export async function isTmuxSessionAlive(sessionName, options = {}) {
35
+ const runTmux = options.runTmux ?? createTmuxRunner();
36
+ try {
37
+ await runTmux(['has-session', '-t', sessionName], { cwd: options.cwd });
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ export async function getTmuxSessionOutput(sessionName, options = {}) {
45
+ const runTmux = options.runTmux ?? createTmuxRunner();
46
+ const result = await runTmux(['capture-pane', '-t', sessionName, '-p'], { cwd: options.cwd });
47
+ return result.stdout;
48
+ }
49
+ function quoteShell(value) {
50
+ return `'${value.replace(/'/g, `'\\''`)}'`;
51
+ }
@@ -0,0 +1,35 @@
1
+ import { join } from 'node:path';
2
+ import { initializeStore } from './core/store.js';
3
+ import { startDaemonRuntime } from './core/daemon.js';
4
+ import { startDashboardServer } from './dashboard/server.js';
5
+ const cwd = process.env.HA_PROJECT_ROOT ?? process.cwd();
6
+ const homeDir = process.env.HA_HOME;
7
+ await initializeStore({
8
+ cwd,
9
+ homeDir,
10
+ scope: 'global',
11
+ });
12
+ const runtime = await startDaemonRuntime({
13
+ cwd,
14
+ homeDir,
15
+ logger: (message) => {
16
+ console.error(message);
17
+ },
18
+ });
19
+ const dashboard = await startDashboardServer({
20
+ cwd,
21
+ homeDir,
22
+ port: Number(process.env.HA_DASHBOARD_PORT ?? '3777'),
23
+ staticDir: process.env.HA_DASHBOARD_STATIC_DIR ?? join(cwd, 'dist', 'dashboard'),
24
+ });
25
+ const shutdown = async () => {
26
+ await dashboard.stop();
27
+ await runtime.stop();
28
+ process.exit(0);
29
+ };
30
+ process.on('SIGINT', () => {
31
+ void shutdown();
32
+ });
33
+ process.on('SIGTERM', () => {
34
+ void shutdown();
35
+ });