input-kanban 0.0.9 → 0.0.12

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.
@@ -4,7 +4,7 @@ import fsp from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import {
7
- DEFAULT_REPO, RUNS_DIR, ensureDir, nowIso, makeRunId, readJson,
7
+ DEFAULT_WORKSPACE, DEFAULT_REPO, RUNS_DIR, ensureDir, nowIso, makeRunId, readJson,
8
8
  writeJsonAtomic, fileInfo, readTextMaybe, extractFirstJsonObject, listRunDirs,
9
9
  pathForRun, roleDir, safeIdPart, RUNNER
10
10
  } from './utils.js';
@@ -31,6 +31,61 @@ function normalizeSandbox(value, fallback = 'workspace-write') {
31
31
  function statePath(runDir) { return path.join(runDir, 'run_state.json'); }
32
32
  function planPath(runDir) { return path.join(runDir, 'plan.json'); }
33
33
  function lockPath(runDir) { return path.join(runDir, RUN_STATE_LOCK_NAME); }
34
+ function workspacePathOf(state) { return path.resolve(state?.workspacePath || state?.repo || DEFAULT_WORKSPACE || DEFAULT_REPO); }
35
+ function workspaceNameOf(state) { return state?.workspaceName || path.basename(workspacePathOf(state)) || workspacePathOf(state); }
36
+ async function detectWorkspaceMetadata(workspacePath) {
37
+ const resolvedWorkspace = path.resolve(workspacePath || DEFAULT_WORKSPACE || DEFAULT_REPO || process.cwd());
38
+ const metadata = {
39
+ path: resolvedWorkspace,
40
+ name: path.basename(resolvedWorkspace) || resolvedWorkspace,
41
+ isGit: false
42
+ };
43
+ try {
44
+ const { stdout: rootStdout } = await execFileAsync('git', ['-C', resolvedWorkspace, 'rev-parse', '--show-toplevel'], { timeout: 5000 });
45
+ const gitRoot = rootStdout.trim();
46
+ if (gitRoot) {
47
+ metadata.isGit = true;
48
+ metadata.gitRoot = gitRoot;
49
+ try {
50
+ const { stdout: branchStdout } = await execFileAsync('git', ['-C', resolvedWorkspace, 'branch', '--show-current'], { timeout: 5000 });
51
+ metadata.branch = branchStdout.trim() || 'detached';
52
+ } catch {
53
+ metadata.branch = 'unknown';
54
+ }
55
+ try {
56
+ const { stdout: dirtyStdout } = await execFileAsync('git', ['-C', resolvedWorkspace, 'status', '--porcelain'], { timeout: 5000 });
57
+ metadata.dirty = dirtyStdout.trim().length > 0;
58
+ } catch {
59
+ metadata.dirty = null;
60
+ }
61
+ }
62
+ } catch {}
63
+ return metadata;
64
+ }
65
+ async function assertWorkspacePath(workspace) {
66
+ const resolvedWorkspace = path.resolve(workspace || DEFAULT_WORKSPACE || DEFAULT_REPO || process.cwd());
67
+ let stat;
68
+ try { stat = await fsp.stat(resolvedWorkspace); }
69
+ catch { throw userInputError(`workspace does not exist: ${resolvedWorkspace}`); }
70
+ if (!stat.isDirectory()) throw userInputError(`workspace is not a directory: ${resolvedWorkspace}`);
71
+ return resolvedWorkspace;
72
+ }
73
+ function normalizeWorkspaceState(state) {
74
+ const workspacePath = path.resolve(state?.workspacePath || state?.repo || DEFAULT_WORKSPACE || DEFAULT_REPO);
75
+ const workspaceName = state.workspaceName || path.basename(workspacePath) || workspacePath;
76
+ const git = state.git || state.workspace?.git || null;
77
+ state.workspacePath = workspacePath;
78
+ state.workspaceName = workspaceName;
79
+ state.repo = state.repo || workspacePath;
80
+ state.git = git;
81
+ state.workspace = state.workspace || {
82
+ path: workspacePath,
83
+ name: workspaceName,
84
+ git
85
+ };
86
+ if (!state.workspace.git && git) state.workspace.git = git;
87
+ return state;
88
+ }
34
89
 
35
90
  async function isStaleRunLock(lockFile) {
36
91
  const info = await fileInfo(lockFile);
@@ -165,28 +220,29 @@ function deriveRunLabel(label, taskText) {
165
220
  return truncateDisplayWidth(cleaned, MAX_DERIVED_LABEL_DISPLAY_WIDTH) || 'task';
166
221
  }
167
222
 
168
- async function assertGitWorkTree(repo) {
169
- const resolvedRepo = path.resolve(repo || DEFAULT_REPO);
170
- let stat;
171
- try { stat = await fsp.stat(resolvedRepo); }
172
- catch { throw userInputError(`target repository does not exist: ${resolvedRepo}`); }
173
- if (!stat.isDirectory()) throw userInputError(`target repository is not a directory: ${resolvedRepo}`);
174
- try {
175
- const { stdout } = await execFileAsync('git', ['-C', resolvedRepo, 'rev-parse', '--is-inside-work-tree'], { timeout: 5000 });
176
- if (stdout.trim() === 'true') return resolvedRepo;
177
- } catch {}
178
- throw userInputError(`target repository is not a git work tree: ${resolvedRepo}`);
179
- }
180
223
 
181
- export async function createRun({ label = '', taskText = '', repo = DEFAULT_REPO, maxParallel = 3, workerSandbox = 'workspace-write' } = {}) {
182
- const resolvedRepo = await assertGitWorkTree(repo);
224
+ export async function createRun({ label = '', taskText = '', workspace = '', repo = DEFAULT_REPO, maxParallel = 3, workerSandbox = 'workspace-write' } = {}) {
225
+ const resolvedWorkspace = await assertWorkspacePath(workspace || repo || DEFAULT_WORKSPACE);
226
+ const workspaceMeta = await detectWorkspaceMetadata(resolvedWorkspace);
183
227
  const runLabel = deriveRunLabel(label, taskText);
184
228
  const runId = makeRunId(runLabel);
185
229
  const runDir = pathForRun(runId);
186
230
  await ensureDir(runDir);
187
231
  await fsp.writeFile(path.join(runDir, 'task.md'), taskText || '');
188
232
  const state = {
189
- runId, label: runLabel, repo: resolvedRepo, maxParallel: Number(maxParallel) || 3, workerSandbox: normalizeSandbox(workerSandbox),
233
+ runId,
234
+ label: runLabel,
235
+ workspacePath: resolvedWorkspace,
236
+ workspaceName: workspaceMeta.name,
237
+ workspace: {
238
+ path: resolvedWorkspace,
239
+ name: workspaceMeta.name,
240
+ git: workspaceMeta
241
+ },
242
+ git: workspaceMeta,
243
+ repo: resolvedWorkspace,
244
+ maxParallel: Number(maxParallel) || 3,
245
+ workerSandbox: normalizeSandbox(workerSandbox),
190
246
  runner: RUNNER,
191
247
  status: 'created', createdAt: nowIso(), updatedAt: nowIso(),
192
248
  planner: { status: 'pending' }, batches: [], tasks: [], judge: { status: 'pending' }
@@ -195,23 +251,57 @@ export async function createRun({ label = '', taskText = '', repo = DEFAULT_REPO
195
251
  return state;
196
252
  }
197
253
 
198
- export async function listRuns({ includeArchived = false } = {}) {
254
+ function normalizeWorkspaceFilter(workspace) {
255
+ const value = String(workspace || '').trim();
256
+ if (!value || value === 'all') return '';
257
+ return path.resolve(value);
258
+ }
259
+
260
+ function runMatchesWorkspace(run, workspaceFilter) {
261
+ if (!workspaceFilter) return true;
262
+ const runWorkspace = path.resolve(run?.workspacePath || run?.repo || '');
263
+ return runWorkspace === workspaceFilter;
264
+ }
265
+
266
+ export function isTerminalRunStatus(status) {
267
+ return ['judged', 'judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(status);
268
+ }
269
+
270
+ export function isFailureRunStatus(status) {
271
+ return ['judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(status);
272
+ }
273
+
274
+ export async function listRuns({ includeArchived = false, workspace = '' } = {}) {
275
+ const workspaceFilter = normalizeWorkspaceFilter(workspace);
199
276
  const dirs = await listRunDirs();
200
277
  const rows = [];
201
278
  for (const dir of dirs) {
202
- const s = await loadAndRefreshRun(path.basename(dir), null, { light: true });
203
- if (s && (includeArchived || !s.archived)) rows.push(summaryOfRun(s));
279
+ const s = await loadRun(path.basename(dir));
280
+ if (!s || (!includeArchived && s.archived)) continue;
281
+ const summary = summaryOfRun(s);
282
+ if (!runMatchesWorkspace(summary, workspaceFilter)) continue;
283
+ rows.push(summary);
204
284
  }
205
285
  return rows;
206
286
  }
207
287
 
208
288
  export async function loadRun(runId) {
209
289
  const state = await readJson(statePath(pathForRun(runId)), null);
210
- if (state) ensureBatchShape(state);
290
+ if (state) {
291
+ normalizeWorkspaceState(state);
292
+ if (!state.git || typeof state.git.isGit !== 'boolean') {
293
+ const workspaceMeta = await detectWorkspaceMetadata(workspacePathOf(state));
294
+ state.git = workspaceMeta;
295
+ state.workspace = state.workspace || { path: workspacePathOf(state), name: state.workspaceName, git: workspaceMeta };
296
+ state.workspace.git = workspaceMeta;
297
+ }
298
+ ensureBatchShape(state);
299
+ }
211
300
  return state;
212
301
  }
213
302
 
214
303
  async function saveRun(state) {
304
+ normalizeWorkspaceState(state);
215
305
  ensureBatchShape(state);
216
306
  state.updatedAt = nowIso();
217
307
  await writeJsonAtomic(statePath(pathForRun(state.runId)), state);
@@ -319,7 +409,7 @@ export async function startPlanner(runId) {
319
409
  await fsp.rm(planPath(runDir), { force: true });
320
410
  const taskText = await fsp.readFile(path.join(runDir, 'task.md'), 'utf8');
321
411
  const prompt = defaultPlannerPrompt(state, taskText);
322
- const child = await runner.startCodexTask({ runId: state.runId, taskId: 'planner', batchId: 'planner', runStatePath: statePath(runDir), prompt, sandbox: 'read-only', cwd: state.repo, outDir });
412
+ const child = await runner.startCodexTask({ runId: state.runId, taskId: 'planner', batchId: 'planner', runStatePath: statePath(runDir), prompt, sandbox: 'read-only', cwd: workspacePathOf(state), outDir });
323
413
  state.status = 'planning';
324
414
  state.planner = { status: 'running', pid: child.pid, startedAt: nowIso(), dir: outDir, attempt: (state.plannerAttempts?.length || 0) + 1 };
325
415
  await saveRun(state);
@@ -499,7 +589,7 @@ export async function dispatchRun(runId) {
499
589
  }
500
590
 
501
591
  function artifactPathForState(state, rel) {
502
- return path.isAbsolute(rel) ? rel : path.join(state.repo, rel);
592
+ return path.isAbsolute(rel) ? rel : path.join(workspacePathOf(state), rel);
503
593
  }
504
594
 
505
595
  function workerArtifactInstructions(state, task) {
@@ -529,7 +619,7 @@ ORCHESTRATOR_BATCH_ID: ${task.batchId || 'batch-1'}
529
619
 
530
620
  ${task.prompt}${workerArtifactInstructions(state, task)}${upstreamArtifactInstructions(state, task)}
531
621
  `;
532
- const child = await runner.startCodexTask({ runId: state.runId, taskId: task.id, batchId: task.batchId || 'batch-1', runStatePath: statePath(runDir), prompt: fullPrompt, sandbox: task.sandbox || state.workerSandbox || 'workspace-write', cwd: state.repo, outDir });
622
+ const child = await runner.startCodexTask({ runId: state.runId, taskId: task.id, batchId: task.batchId || 'batch-1', runStatePath: statePath(runDir), prompt: fullPrompt, sandbox: task.sandbox || state.workerSandbox || 'workspace-write', cwd: workspacePathOf(state), outDir });
533
623
  Object.assign(task, { status: 'running', pid: child.pid, startedAt: nowIso(), dir: outDir });
534
624
  }
535
625
 
@@ -688,7 +778,7 @@ export async function startJudge(runId) {
688
778
  const judgeInput = await buildJudgeInput(state);
689
779
  await writeJsonAtomic(judgeInputPath, judgeInput);
690
780
  const prompt = defaultJudgePrompt(state, judgeInputPath);
691
- const child = await runner.startCodexTask({ runId: state.runId, taskId: 'judge', batchId: 'judge', runStatePath: statePath(pathForRun(runId)), prompt, sandbox: 'read-only', cwd: state.repo, outDir });
781
+ const child = await runner.startCodexTask({ runId: state.runId, taskId: 'judge', batchId: 'judge', runStatePath: statePath(pathForRun(runId)), prompt, sandbox: 'read-only', cwd: workspacePathOf(state), outDir });
692
782
  state.judge = { status: 'running', pid: child.pid, startedAt: nowIso(), dir: outDir };
693
783
  state.status = 'judging';
694
784
  await saveRun(state);
@@ -711,6 +801,50 @@ export async function refreshRun(runId, appClient = null) {
711
801
  return await loadAndRefreshRun(runId, appClient, { light: false });
712
802
  }
713
803
 
804
+ export async function autoAdvanceRun(runId, { appClient = null, startCreated = false, maxRetries = 1, retryReason = 'auto retry from scheduler' } = {}) {
805
+ let state = await refreshRun(runId, appClient);
806
+ if (!state || state.archived || state.status === 'stopped') return state;
807
+ if (startCreated && state.status === 'created') {
808
+ try { state = await startPlanner(runId); }
809
+ catch (error) { if (!/planner already running/i.test(error.message || '')) throw error; }
810
+ state = await refreshRun(runId, appClient);
811
+ }
812
+ if (!state || state.archived || state.status === 'stopped') return state;
813
+ if (state.status === 'batch_blocked') {
814
+ try { state = await retryRun(runId, { reason: retryReason, maxRetries, auto: true }); }
815
+ catch (error) { state.autoAdvanceError = error.message || String(error); }
816
+ return await refreshRun(runId, appClient) || state;
817
+ }
818
+ if (state.status === 'planned') {
819
+ try { state = await dispatchRun(runId); }
820
+ catch (error) { if (!/all batches completed|current batch is blocked/i.test(error.message || '')) throw error; }
821
+ return await refreshRun(runId, appClient) || state;
822
+ }
823
+ if (state.status === 'batches_completed' && state.judge?.status !== 'running' && state.judge?.status !== 'completed') {
824
+ try { state = await startJudge(runId); }
825
+ catch (error) { if (!/final judge is allowed only after all batches completed/i.test(error.message || '')) throw error; }
826
+ return await refreshRun(runId, appClient) || state;
827
+ }
828
+ return state;
829
+ }
830
+
831
+ export async function autoAdvanceActiveRuns({ appClient = null, startCreated = false, maxRetries = 1, retryReason = 'auto retry from scheduler' } = {}) {
832
+ const dirs = await listRunDirs();
833
+ const results = [];
834
+ for (const dir of dirs) {
835
+ const runId = path.basename(dir);
836
+ try {
837
+ const initial = await loadRun(runId);
838
+ if (!initial || initial.archived || ['judged', 'judge_failed', 'plan_failed', 'plan_empty', 'stopped'].includes(initial.status)) continue;
839
+ const state = await autoAdvanceRun(runId, { appClient, startCreated, maxRetries, retryReason });
840
+ if (state) results.push({ runId, status: state.status, ok: true });
841
+ } catch (error) {
842
+ results.push({ runId, ok: false, error: error.message || String(error) });
843
+ }
844
+ }
845
+ return results;
846
+ }
847
+
714
848
  async function loadAndRefreshRun(runId, appClient = null, { light = false } = {}) {
715
849
  return await withRunStateLock(runId, async () => {
716
850
  const state = await loadRun(runId);
@@ -796,7 +930,7 @@ async function refreshTask(state, task) {
796
930
  await attachTmuxMetadata(task, dir);
797
931
  delete task.attentionHint;
798
932
  task.artifacts = [];
799
- for (const rel of task.expectedArtifacts || []) task.artifacts.push({ path: rel, ...(await fileInfo(path.isAbsolute(rel) ? rel : path.join(state.repo, rel))) });
933
+ for (const rel of task.expectedArtifacts || []) task.artifacts.push({ path: rel, ...(await fileInfo(path.isAbsolute(rel) ? rel : path.join(workspacePathOf(state), rel))) });
800
934
  const batch = (state.batches || []).find(b => b.id === task.batchId);
801
935
  if (batch) {
802
936
  const bt = batch.tasks.find(t => t.id === task.id);
@@ -1021,7 +1155,10 @@ async function buildJudgeInput(state) {
1021
1155
  run: {
1022
1156
  runId: state.runId,
1023
1157
  label: state.label,
1024
- repo: state.repo,
1158
+ repo: workspacePathOf(state),
1159
+ workspacePath: workspacePathOf(state),
1160
+ workspaceName: state.workspaceName || path.basename(workspacePathOf(state)) || workspacePathOf(state),
1161
+ git: state.git || state.workspace?.git || null,
1025
1162
  status: state.status,
1026
1163
  runner: state.runner || RUNNER,
1027
1164
  createdAt: state.createdAt,
@@ -1065,7 +1202,7 @@ function ensureBatchShape(state) {
1065
1202
  }
1066
1203
 
1067
1204
  async function enrichFromAppServer(state, appClient) {
1068
- const res = await appClient.listThreads({ cwd: state.repo, limit: 100 });
1205
+ const res = await appClient.listThreads({ cwd: workspacePathOf(state), limit: 100 });
1069
1206
  const threads = res?.data || [];
1070
1207
  const all = [{ id: 'planner', target: state.planner }, ...(state.tasks || []).map(t => ({ id: t.id, target: t })), { id: 'judge', target: state.judge }];
1071
1208
  for (const item of all) {
@@ -1089,7 +1226,9 @@ function runDurationEndOfState(s) {
1089
1226
 
1090
1227
  export function summaryOfRun(s) {
1091
1228
  const tasks = s.tasks || [];
1092
- return { runId: s.runId, label: s.label, repo: s.repo, status: s.status, runner: s.runner || RUNNER, workerSandbox: s.workerSandbox || 'workspace-write', archived: !!s.archived, createdAt: s.createdAt, updatedAt: s.updatedAt, durationEnd: runDurationEndOfState(s), total: tasks.length, completed: tasks.filter(t => t.status === 'completed').length, failed: tasks.filter(t => ['failed','unknown'].includes(t.status)).length, running: tasks.filter(t => t.status === 'running').length, batches: (s.batches || []).map(b => ({ id: b.id, name: b.name, status: b.status, total: b.tasks?.length || 0, completed: (b.tasks || []).filter(t => t.status === 'completed').length })) };
1229
+ const workspacePath = s.workspacePath || s.repo || '';
1230
+ const git = s.git || s.workspace?.git || null;
1231
+ return { runId: s.runId, label: s.label, repo: s.repo || workspacePath, workspacePath, workspaceName: s.workspaceName || path.basename(workspacePath || ''), git, status: s.status, runner: s.runner || RUNNER, workerSandbox: s.workerSandbox || 'workspace-write', archived: !!s.archived, createdAt: s.createdAt, updatedAt: s.updatedAt, durationEnd: runDurationEndOfState(s), total: tasks.length, completed: tasks.filter(t => t.status === 'completed').length, failed: tasks.filter(t => ['failed','unknown'].includes(t.status)).length, running: tasks.filter(t => t.status === 'running').length, batches: (s.batches || []).map(b => ({ id: b.id, name: b.name, status: b.status, total: b.tasks?.length || 0, completed: (b.tasks || []).filter(t => t.status === 'completed').length })) };
1093
1232
  }
1094
1233
 
1095
1234
  export async function readRunTaskText(runId) {
@@ -0,0 +1,40 @@
1
+ import { autoAdvanceActiveRuns } from './orchestrator.js';
2
+
3
+ export function startAutoScheduler({ appClient = null, pollMs = Number(process.env.KANBAN_AUTO_POLL_MS || 3000), maxRetries = Number(process.env.KANBAN_AUTO_MAX_RETRIES || 1), startCreated = false, log = false } = {}) {
4
+ const intervalMs = Math.max(500, Number(pollMs) || 3000);
5
+ let stopped = false;
6
+ let running = false;
7
+ let timer = null;
8
+
9
+ const tick = async () => {
10
+ if (stopped || running) return;
11
+ running = true;
12
+ try {
13
+ const results = await autoAdvanceActiveRuns({ appClient, startCreated, maxRetries, retryReason: 'auto retry from server scheduler' });
14
+ if (log) {
15
+ for (const result of results) {
16
+ if (result.ok === false) console.warn(`[scheduler] ${result.runId}: ${result.error}`);
17
+ }
18
+ }
19
+ } catch (error) {
20
+ if (log) console.warn(`[scheduler] ${error.message || String(error)}`);
21
+ } finally {
22
+ running = false;
23
+ }
24
+ };
25
+
26
+ timer = setInterval(() => { tick(); }, intervalMs);
27
+ timer.unref?.();
28
+ tick();
29
+
30
+ return {
31
+ get running() { return running; },
32
+ get stopped() { return stopped; },
33
+ async tick() { await tick(); },
34
+ stop() {
35
+ stopped = true;
36
+ if (timer) clearInterval(timer);
37
+ timer = null;
38
+ }
39
+ };
40
+ }
package/src/server.js CHANGED
@@ -3,8 +3,9 @@ import fsp from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { CodexAppServerClient } from './appServerClient.js';
6
- import { APP_ROOT, DEFAULT_REPO, PACKAGE_VERSION, RUNNER, RUNS_DIR } from './utils.js';
6
+ import { APP_ROOT, DEFAULT_WORKSPACE, DEFAULT_REPO, PACKAGE_VERSION, RUNNER, RUNS_DIR } from './utils.js';
7
7
  import { createRun, listRuns, startPlanner, dispatchRun, startJudge, refreshRun, readRunFile, readRunTaskText, markTaskCompleted, stopRun, archiveRun, renameRun, retryRun } from './orchestrator.js';
8
+ import { startAutoScheduler } from './scheduler.js';
8
9
 
9
10
  const PUBLIC_DIR = path.join(APP_ROOT, 'public');
10
11
 
@@ -42,10 +43,10 @@ async function handleApi(req, res, url, appClient) {
42
43
  const parts = url.pathname.split('/').filter(Boolean);
43
44
  try {
44
45
  if (req.method === 'GET' && url.pathname === '/api/health') {
45
- return send(res, 200, { ok: true, version: PACKAGE_VERSION, appRoot: APP_ROOT, runsDir: RUNS_DIR, defaultRepo: DEFAULT_REPO, runner: RUNNER });
46
+ return send(res, 200, { ok: true, version: PACKAGE_VERSION, appRoot: APP_ROOT, runsDir: RUNS_DIR, defaultWorkspace: DEFAULT_WORKSPACE, defaultRepo: DEFAULT_REPO, runner: RUNNER });
46
47
  }
47
48
  if (parts[1] === 'runs' && parts.length === 2) {
48
- if (req.method === 'GET') return send(res, 200, { runs: await listRuns({ includeArchived: url.searchParams.get('includeArchived') === '1' }) });
49
+ if (req.method === 'GET') return send(res, 200, { runs: await listRuns({ includeArchived: url.searchParams.get('includeArchived') === '1', workspace: url.searchParams.get('workspace') || '' }) });
49
50
  if (req.method === 'POST') {
50
51
  const body = await readBody(req);
51
52
  return send(res, 201, await createRun(body));
@@ -98,17 +99,19 @@ export function createHttpServer({ appClient = new CodexAppServerClient() } = {}
98
99
  });
99
100
  }
100
101
 
101
- export async function startServer({ host = process.env.HOST || '127.0.0.1', port = Number(process.env.PORT || 8787), log = true } = {}) {
102
+ export async function startServer({ host = process.env.HOST || '127.0.0.1', port = Number(process.env.PORT || 8787), log = true, scheduler = true } = {}) {
102
103
  const appClient = new CodexAppServerClient();
103
104
  const server = createHttpServer({ appClient });
105
+ const autoScheduler = scheduler ? startAutoScheduler({ appClient, log }) : null;
104
106
  await new Promise(resolve => server.listen(port, host, resolve));
105
107
  const url = `http://${host}:${port}`;
106
108
  if (log) console.log(`input-kanban listening on ${url}`);
107
109
  const stop = async () => {
110
+ autoScheduler?.stop();
108
111
  appClient.stop();
109
112
  await new Promise(resolve => server.close(resolve));
110
113
  };
111
- return { server, appClient, host, port, url, version: PACKAGE_VERSION, defaultRepo: DEFAULT_REPO, runsDir: RUNS_DIR, runner: RUNNER, stop };
114
+ return { server, appClient, autoScheduler, host, port, url, version: PACKAGE_VERSION, defaultWorkspace: DEFAULT_WORKSPACE, defaultRepo: DEFAULT_REPO, runsDir: RUNS_DIR, runner: RUNNER, scheduler: !!autoScheduler, stop };
112
115
  }
113
116
 
114
117
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
package/src/utils.js CHANGED
@@ -3,13 +3,15 @@ import fsp from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import crypto from 'node:crypto';
5
5
  import { createRequire } from 'node:module';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  const require = createRequire(import.meta.url);
8
9
  const { version: PACKAGE_VERSION } = require('../package.json');
9
10
 
10
- export const APP_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
11
+ export const APP_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
12
  export { PACKAGE_VERSION };
12
- export const DEFAULT_REPO = path.resolve(process.env.KANBAN_DEFAULT_REPO || process.cwd());
13
+ export const DEFAULT_WORKSPACE = path.resolve(process.env.KANBAN_DEFAULT_WORKSPACE || process.env.KANBAN_DEFAULT_REPO || process.cwd());
14
+ export const DEFAULT_REPO = DEFAULT_WORKSPACE;
13
15
  export const RUNS_DIR = path.resolve(process.env.KANBAN_RUNS_DIR || path.join(process.env.HOME || APP_ROOT, '.input-kanban', 'runs'));
14
16
  export const CODEX_BIN = process.env.KANBAN_CODEX_BIN || 'codex';
15
17
  export const VALID_RUNNERS = ['headless', 'tmux'];