input-kanban 0.0.6 → 0.0.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.
@@ -1,15 +1,38 @@
1
1
  #!/usr/bin/env node
2
+ import fsp from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { spawn } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
4
6
 
5
7
  const VALID_RUNNERS = ['headless', 'tmux'];
8
+ const VALID_SANDBOXES = ['read-only', 'workspace-write', 'danger-full-access'];
9
+ const COMMANDS = new Set(['serve', 'submit', 'status', 'result', 'stop', 'auto']);
10
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
11
+ const STATUS_TEXT = {
12
+ created: '已创建', planning: '拆分中', plan_failed: '拆分失败', plan_empty: '拆分为空', planned: '已拆分',
13
+ running: '执行中', batch_blocked: '批次阻塞', batches_completed: '批次完成', judging: '验收中', judged: '已验收',
14
+ judge_failed: '验收失败', stopped: '已停止'
15
+ };
16
+
17
+ function validateChoice(value, source, choices) {
18
+ if (choices.includes(value)) return value;
19
+ throw new Error(`invalid ${source}: ${value}; expected one of: ${choices.join(', ')}`);
20
+ }
6
21
 
7
22
  function validateRunner(value, source) {
8
- if (VALID_RUNNERS.includes(value)) return value;
9
- throw new Error(`invalid ${source}: ${value}; expected one of: ${VALID_RUNNERS.join(', ')}`);
23
+ return validateChoice(value, source, VALID_RUNNERS);
24
+ }
25
+
26
+ function validateSandbox(value, source) {
27
+ return validateChoice(value, source, VALID_SANDBOXES);
28
+ }
29
+
30
+ function splitCommand(argv) {
31
+ if (argv[0] && COMMANDS.has(argv[0])) return { command: argv[0], rest: argv.slice(1) };
32
+ return { command: 'serve', rest: argv };
10
33
  }
11
34
 
12
- function parseArgs(argv) {
35
+ function parseServeArgs(argv) {
13
36
  const args = { host: '127.0.0.1', port: undefined, repo: undefined, runsDir: undefined, codexBin: undefined, runner: undefined, open: false, help: false };
14
37
  for (let i = 0; i < argv.length; i++) {
15
38
  const arg = argv[i];
@@ -28,22 +51,240 @@ function parseArgs(argv) {
28
51
  return args;
29
52
  }
30
53
 
54
+ function parseStatusArgs(argv) {
55
+ const args = { host: '127.0.0.1', port: 8787, runsDir: undefined, runId: undefined, watch: false, pollMs: 3000, help: false };
56
+ for (let i = 0; i < argv.length; i++) {
57
+ const arg = argv[i];
58
+ const next = () => argv[++i];
59
+ if (arg === '--help' || arg === '-h') args.help = true;
60
+ else if (arg === '--host') args.host = next();
61
+ else if (arg === '--port' || arg === '-p') args.port = Number(next());
62
+ else if (arg === '--runs-dir') args.runsDir = next();
63
+ else if (arg === '--watch') args.watch = true;
64
+ else if (arg === '--poll-ms') args.pollMs = Number(next());
65
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
66
+ else throw new Error(`unknown status argument: ${arg}`);
67
+ }
68
+ return args;
69
+ }
70
+
71
+ function parseResultArgs(argv) {
72
+ const args = { runsDir: undefined, runId: undefined, copy: false, help: false };
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const arg = argv[i];
75
+ const next = () => argv[++i];
76
+ if (arg === '--help' || arg === '-h') args.help = true;
77
+ else if (arg === '--runs-dir') args.runsDir = next();
78
+ else if (arg === '--copy') args.copy = true;
79
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
80
+ else throw new Error(`unknown result argument: ${arg}`);
81
+ }
82
+ return args;
83
+ }
84
+
85
+ function parseStopArgs(argv) {
86
+ const args = { runsDir: undefined, runId: undefined, reason: 'stopped from CLI', help: false };
87
+ for (let i = 0; i < argv.length; i++) {
88
+ const arg = argv[i];
89
+ const next = () => argv[++i];
90
+ if (arg === '--help' || arg === '-h') args.help = true;
91
+ else if (arg === '--runs-dir') args.runsDir = next();
92
+ else if (arg === '--reason') args.reason = next();
93
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
94
+ else throw new Error(`unknown stop argument: ${arg}`);
95
+ }
96
+ return args;
97
+ }
98
+
99
+ function parseAutoArgs(argv) {
100
+ const args = { host: '127.0.0.1', port: 8787, runsDir: undefined, codexBin: undefined, runner: undefined, runId: undefined, pollMs: 3000, help: false };
101
+ for (let i = 0; i < argv.length; i++) {
102
+ const arg = argv[i];
103
+ const next = () => argv[++i];
104
+ if (arg === '--help' || arg === '-h') args.help = true;
105
+ else if (arg === '--host') args.host = next();
106
+ else if (arg === '--port' || arg === '-p') args.port = Number(next());
107
+ else if (arg === '--runs-dir') args.runsDir = next();
108
+ else if (arg === '--codex-bin') args.codexBin = next();
109
+ else if (arg === '--runner') args.runner = validateRunner(next(), '--runner');
110
+ else if (arg === '--poll-ms') args.pollMs = Number(next());
111
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
112
+ else throw new Error(`unknown auto argument: ${arg}`);
113
+ }
114
+ return args;
115
+ }
116
+
117
+ function parseSubmitArgs(argv) {
118
+ const args = {
119
+ host: '127.0.0.1', port: 8787, repo: undefined, runsDir: undefined, codexBin: undefined,
120
+ runner: undefined, label: undefined, taskText: undefined, taskFile: undefined, maxParallel: 3,
121
+ workerSandbox: 'workspace-write', auto: true, detach: false, watch: true, pollMs: 3000, help: false
122
+ };
123
+ for (let i = 0; i < argv.length; i++) {
124
+ const arg = argv[i];
125
+ const next = () => argv[++i];
126
+ if (arg === '--help' || arg === '-h') args.help = true;
127
+ else if (arg === '--host') args.host = next();
128
+ else if (arg === '--port' || arg === '-p') args.port = Number(next());
129
+ else if (arg === '--repo' || arg === '-r') args.repo = next();
130
+ else if (arg === '--runs-dir') args.runsDir = next();
131
+ else if (arg === '--codex-bin') args.codexBin = next();
132
+ else if (arg === '--runner') args.runner = validateRunner(next(), '--runner');
133
+ else if (arg === '--label' || arg === '-l') args.label = next();
134
+ else if (arg === '--task') args.taskText = next();
135
+ else if (arg === '--task-file') args.taskFile = next();
136
+ else if (arg === '--max-parallel') args.maxParallel = Number(next());
137
+ else if (arg === '--worker-sandbox') args.workerSandbox = validateSandbox(next(), '--worker-sandbox');
138
+ else if (arg === '--auto') { args.auto = true; args.watch = true; }
139
+ else if (arg === '--no-auto') { args.auto = false; args.watch = false; }
140
+ else if (arg === '--detach' || arg === '-d') args.detach = true;
141
+ else if (arg === '--watch') args.watch = true;
142
+ else if (arg === '--poll-ms') args.pollMs = Number(next());
143
+ else throw new Error(`unknown submit argument: ${arg}`);
144
+ }
145
+ return args;
146
+ }
147
+
148
+ function applyRuntimeEnv(args) {
149
+ if (args.port) process.env.PORT = String(args.port);
150
+ if (args.host) process.env.HOST = args.host;
151
+ if (args.repo) process.env.KANBAN_DEFAULT_REPO = path.resolve(args.repo);
152
+ else if (!process.env.KANBAN_DEFAULT_REPO) process.env.KANBAN_DEFAULT_REPO = process.cwd();
153
+ if (args.runsDir) process.env.KANBAN_RUNS_DIR = path.resolve(args.runsDir);
154
+ if (args.codexBin) process.env.KANBAN_CODEX_BIN = args.codexBin;
155
+ if (args.runner) process.env.KANBAN_RUNNER = args.runner;
156
+ }
157
+
31
158
  function printHelp() {
32
159
  console.log(`input-kanban
33
160
 
34
161
  Usage:
35
162
  input-kanban [options]
163
+ input-kanban serve [options]
164
+ input-kanban submit [options]
165
+ input-kanban status [runId] [options]
166
+ input-kanban result [runId] [options]
167
+ input-kanban stop <runId> [options]
168
+
169
+ Serve options:
170
+ --host <host> Host to bind, default 127.0.0.1
171
+ -p, --port <port> Port to bind, default 8787
172
+ -r, --repo <path> Default target repository, default current directory
173
+ --runs-dir <path> Runtime runs directory, default ~/.input-kanban/runs
174
+ --codex-bin <path> Codex CLI executable, default codex
175
+ --runner <mode> Runner mode: headless or tmux, default headless
176
+ --open Open browser after starting
177
+ --no-open Do not open browser, default
178
+
179
+ Status options:
180
+ --runs-dir <path> Runtime runs directory shared with the Web UI
181
+ --watch Keep printing status until the run reaches a terminal state
182
+ --poll-ms <ms> Watch poll interval, default 3000
183
+
184
+ Result options:
185
+ --runs-dir <path> Runtime runs directory shared with the Web UI
186
+ --copy Copy final result to clipboard
187
+
188
+ Stop options:
189
+ --runs-dir <path> Runtime runs directory shared with the Web UI
190
+ --reason <text> Stop reason stored in run state
191
+
192
+ Submit options:
193
+ -r, --repo <path> Target Git work tree, default current directory
194
+ -l, --label <label> Task batch name, default generated from task text
195
+ --task <text> Task description text
196
+ --task-file <path> Read task description from file, use - for stdin
197
+ --max-parallel <n> Default max parallel workers, default 3
198
+ --worker-sandbox <mode> read-only, workspace-write, or danger-full-access
199
+ --runner <mode> Runner mode: headless or tmux
200
+ --runs-dir <path> Runtime runs directory shared with the Web UI
201
+ --auto Plan, dispatch all batches, judge, and watch, default for submit
202
+ --no-auto Only create the run and start planning
203
+ -d, --detach Run the default auto loop in a background supervisor
204
+ --watch Watch status after starting the planner
205
+ --poll-ms <ms> Watch poll interval, default 3000
206
+ -h, --help Show help
207
+ `);
208
+ }
209
+
210
+ function printSubmitHelp() {
211
+ console.log(`input-kanban submit
212
+
213
+ Usage:
214
+ input-kanban submit --repo <path> --task-file task.md
215
+ input-kanban submit --repo <path> --task "fix the bug" --label "bugfix"
216
+ input-kanban submit --task-file task.md -d
36
217
 
37
218
  Options:
38
- --host <host> Host to bind, default 127.0.0.1
39
- -p, --port <port> Port to bind, default 8787
40
- -r, --repo <path> Default target repository, default current directory
41
- --runs-dir <path> Runtime runs directory, default ~/.input-kanban/runs
42
- --codex-bin <path> Codex CLI executable, default codex
43
- --runner <mode> Runner mode: headless or tmux, default headless
44
- --open Open browser after starting
45
- --no-open Do not open browser, default
46
- -h, --help Show help
219
+ -r, --repo <path> Target Git work tree, default current directory
220
+ -l, --label <label> Task batch name, default generated from task text
221
+ --task <text> Task description text
222
+ --task-file <path> Read task description from file, use - for stdin
223
+ --max-parallel <n> Default max parallel workers, default 3
224
+ --worker-sandbox <mode> read-only, workspace-write, or danger-full-access
225
+ --runner <mode> Runner mode: headless or tmux
226
+ --runs-dir <path> Runtime runs directory shared with the Web UI
227
+ --auto Plan, dispatch all batches, judge, and watch, default for submit
228
+ --no-auto Only create the run and start planning
229
+ -d, --detach Run the default auto loop in a background supervisor
230
+ --watch Watch status after starting the planner
231
+ --poll-ms <ms> Watch poll interval, default 3000
232
+ `);
233
+ }
234
+
235
+ function printStatusHelp() {
236
+ console.log(`input-kanban status
237
+
238
+ Usage:
239
+ input-kanban status
240
+ input-kanban status <runId>
241
+ input-kanban status --watch
242
+ input-kanban status <runId> --watch
243
+
244
+ Options:
245
+ --runs-dir <path> Runtime runs directory shared with the Web UI
246
+ --watch Keep printing status until the run reaches a terminal state
247
+ --poll-ms <ms> Watch poll interval, default 3000
248
+ `);
249
+ }
250
+
251
+ function printResultHelp() {
252
+ console.log(`input-kanban result
253
+
254
+ Usage:
255
+ input-kanban result
256
+ input-kanban result <runId>
257
+ input-kanban result <runId> --copy
258
+
259
+ Options:
260
+ --runs-dir <path> Runtime runs directory shared with the Web UI
261
+ --copy Copy final result to clipboard
262
+ `);
263
+ }
264
+
265
+ function printStopHelp() {
266
+ console.log(`input-kanban stop
267
+
268
+ Usage:
269
+ input-kanban stop <runId>
270
+
271
+ Options:
272
+ --runs-dir <path> Runtime runs directory shared with the Web UI
273
+ --reason <text> Stop reason stored in run state
274
+ `);
275
+ }
276
+
277
+ function printAutoHelp() {
278
+ console.log(`input-kanban auto
279
+
280
+ Usage:
281
+ input-kanban auto <runId>
282
+
283
+ Options:
284
+ --runs-dir <path> Runtime runs directory shared with the Web UI
285
+ --codex-bin <path> Codex CLI executable, default codex
286
+ --runner <mode> Runner mode: headless or tmux
287
+ --poll-ms <ms> Watch poll interval, default 3000
47
288
  `);
48
289
  }
49
290
 
@@ -54,17 +295,124 @@ function openBrowser(url) {
54
295
  child.unref();
55
296
  }
56
297
 
57
- try {
58
- const args = parseArgs(process.argv.slice(2));
59
- if (args.help) { printHelp(); process.exit(0); }
60
- if (args.port) process.env.PORT = String(args.port);
61
- if (args.host) process.env.HOST = args.host;
62
- if (args.repo) process.env.KANBAN_DEFAULT_REPO = path.resolve(args.repo);
63
- else if (!process.env.KANBAN_DEFAULT_REPO) process.env.KANBAN_DEFAULT_REPO = process.cwd();
64
- if (args.runsDir) process.env.KANBAN_RUNS_DIR = path.resolve(args.runsDir);
65
- if (args.codexBin) process.env.KANBAN_CODEX_BIN = args.codexBin;
66
- if (args.runner) process.env.KANBAN_RUNNER = args.runner;
298
+ async function readStdin() {
299
+ const chunks = [];
300
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
301
+ return Buffer.concat(chunks).toString('utf8');
302
+ }
303
+
304
+ async function readTaskText(args) {
305
+ if (args.taskText !== undefined) return args.taskText;
306
+ if (args.taskFile === '-') return await readStdin();
307
+ if (args.taskFile) return await fsp.readFile(path.resolve(args.taskFile), 'utf8');
308
+ throw new Error('submit requires --task or --task-file');
309
+ }
310
+
311
+ function webUrl(args, runId = '') {
312
+ return `http://${args.host || '127.0.0.1'}:${Number(args.port || 8787)}${runId ? ` (runId: ${runId})` : ''}`;
313
+ }
314
+
315
+ function displayStatus(status) {
316
+ const text = STATUS_TEXT[status] || status || '-';
317
+ return status && text !== status ? `${text}(${status})` : text;
318
+ }
319
+
320
+ function countByStatus(state) {
321
+ const tasks = state.tasks || [];
322
+ return {
323
+ total: tasks.length,
324
+ completed: tasks.filter(task => task.status === 'completed').length,
325
+ running: tasks.filter(task => task.status === 'running').length,
326
+ failed: tasks.filter(task => ['failed', 'unknown'].includes(task.status)).length
327
+ };
328
+ }
329
+
330
+ function currentBatchText(state) {
331
+ const batch = (state.batches || []).find(item => item.status !== 'completed');
332
+ if (!batch) return '-';
333
+ const tasks = batch.tasks || [];
334
+ const completed = tasks.filter(task => task.status === 'completed').length;
335
+ return `${batch.name || batch.id}(${batch.id}) ${displayStatus(batch.status)} ${completed}/${tasks.length}`;
336
+ }
337
+
338
+ function statusLine(state) {
339
+ const counts = countByStatus(state);
340
+ return `${state.label || state.runId}|${state.runId}|状态 ${displayStatus(state.status)}|进度 ${counts.completed}/${counts.total}|执行中 ${counts.running}|失败 ${counts.failed}`;
341
+ }
342
+
343
+ function printRunStatus(state) {
344
+ const counts = countByStatus(state);
345
+ console.log(`任务批次: ${state.label || '-'}`);
346
+ console.log(`Run ID: ${state.runId}`);
347
+ console.log(`状态: ${displayStatus(state.status)}`);
348
+ console.log(`仓库: ${state.repo || '-'}`);
349
+ console.log(`当前批次: ${currentBatchText(state)}`);
350
+ console.log(`进度: ${counts.completed}/${counts.total} |执行中 ${counts.running} |失败 ${counts.failed}`);
351
+ if (state.judge?.status && state.judge.status !== 'pending') console.log(`验收: ${displayStatus(state.judge.status)}`);
352
+ }
353
+
354
+ function isTerminal(state) {
355
+ return ['judged', 'judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(state.status);
356
+ }
357
+
358
+ function isFailureTerminal(state) {
359
+ return ['judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(state.status);
360
+ }
361
+
362
+ function hasRecoverableUnknownTask(state) {
363
+ return (state.tasks || []).some(task => task.status === 'unknown' && (task.exitCode === undefined || task.exitCode === 0));
364
+ }
365
+
366
+ async function confirmFailureTerminal(runId, state, refreshRun, pollMs) {
367
+ let confirmed = state;
368
+ const deadline = Date.now() + 30000;
369
+ while (confirmed?.status === 'batch_blocked' && hasRecoverableUnknownTask(confirmed) && Date.now() < deadline) {
370
+ await delay(Math.max(500, Number(pollMs) || 3000));
371
+ confirmed = await refreshRun(runId);
372
+ if (!confirmed || !isTerminal(confirmed) || confirmed.status !== state.status) return { confirmed: false, state: confirmed };
373
+ }
374
+ return { confirmed: true, state: confirmed };
375
+ }
376
+
377
+ async function watchRun(runId, { auto = false, pollMs = 3000 } = {}) {
378
+ const { dispatchRun, refreshRun, startJudge } = await import('../src/orchestrator.js');
379
+ let lastStatus = '';
380
+ let judgeStarted = false;
381
+ while (true) {
382
+ const state = await refreshRun(runId);
383
+ if (!state) throw new Error(`run not found: ${runId}`);
384
+ const line = statusLine(state);
385
+ if (line !== lastStatus) {
386
+ console.log(`[${new Date().toLocaleTimeString()}] ${line}`);
387
+ lastStatus = line;
388
+ }
389
+
390
+ if (auto && state.status === 'planned') {
391
+ console.log('自动派发任务...');
392
+ await dispatchRun(runId);
393
+ } else if (auto && state.status === 'batches_completed' && state.judge?.status !== 'running' && !judgeStarted) {
394
+ console.log('自动发起最终验收...');
395
+ judgeStarted = true;
396
+ await startJudge(runId);
397
+ }
398
+
399
+ if (isTerminal(state)) {
400
+ if (isFailureTerminal(state)) {
401
+ const result = await confirmFailureTerminal(runId, state, refreshRun, pollMs);
402
+ if (!result.confirmed) {
403
+ lastStatus = '';
404
+ continue;
405
+ }
406
+ return result.state || state;
407
+ }
408
+ return state;
409
+ }
410
+ await delay(Math.max(500, Number(pollMs) || 3000));
411
+ }
412
+ }
67
413
 
414
+ async function serve(args) {
415
+ applyRuntimeEnv(args);
68
416
  const { startServer } = await import('../src/server.js');
69
417
  const instance = await startServer({ host: process.env.HOST, port: Number(process.env.PORT || 8787), log: false });
70
418
  console.log('Input Kanban started');
@@ -76,6 +424,168 @@ try {
76
424
  const shutdown = () => { instance.stop().finally(() => process.exit(0)); };
77
425
  process.on('SIGINT', shutdown);
78
426
  process.on('SIGTERM', shutdown);
427
+ }
428
+
429
+ function detachedAutoArgs(runId, args) {
430
+ const cliPath = fileURLToPath(import.meta.url);
431
+ const values = [cliPath, 'auto', runId, '--host', args.host || '127.0.0.1', '--port', String(args.port || 8787), '--poll-ms', String(args.pollMs || 3000)];
432
+ if (args.runsDir) values.push('--runs-dir', path.resolve(args.runsDir));
433
+ if (args.codexBin) values.push('--codex-bin', args.codexBin);
434
+ if (args.runner) values.push('--runner', args.runner);
435
+ return values;
436
+ }
437
+
438
+ function startDetachedAuto(runId, args) {
439
+ const child = spawn(process.execPath, detachedAutoArgs(runId, args), {
440
+ detached: true,
441
+ stdio: 'ignore',
442
+ env: process.env
443
+ });
444
+ child.unref();
445
+ return child.pid;
446
+ }
447
+
448
+ async function latestRunId() {
449
+ const { listRuns } = await import('../src/orchestrator.js');
450
+ const runs = await listRuns();
451
+ if (!runs.length) throw new Error('没有找到任务批次');
452
+ return runs[0].runId;
453
+ }
454
+
455
+ async function status(args) {
456
+ applyRuntimeEnv(args);
457
+ const runId = args.runId || await latestRunId();
458
+ if (args.watch) {
459
+ await watchRun(runId, { auto: false, pollMs: args.pollMs });
460
+ return;
461
+ }
462
+ const { refreshRun } = await import('../src/orchestrator.js');
463
+ const state = await refreshRun(runId);
464
+ if (!state) throw new Error(`run not found: ${runId}`);
465
+ printRunStatus(state);
466
+ }
467
+
468
+ async function readFinalResult(runId) {
469
+ const { loadRun, readRunFile } = await import('../src/orchestrator.js');
470
+ const state = await loadRun(runId);
471
+ if (!state) throw new Error(`run not found: ${runId}`);
472
+ try { return await readRunFile(runId, 'judge', 'verdict.json'); }
473
+ catch {}
474
+ try { return await readRunFile(runId, 'judge', 'last_message.md'); }
475
+ catch {}
476
+ throw new Error(`最终结果尚未生成:当前状态 ${displayStatus(state.status)}`);
477
+ }
478
+
479
+ function clipboardCommands() {
480
+ if (process.platform === 'darwin') return [['pbcopy', []]];
481
+ if (process.platform === 'win32') return [['clip', []]];
482
+ return [['wl-copy', []], ['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]];
483
+ }
484
+
485
+ async function copyToClipboard(text) {
486
+ let lastError = null;
487
+ for (const [command, args] of clipboardCommands()) {
488
+ try {
489
+ await new Promise((resolve, reject) => {
490
+ const child = spawn(command, args, { stdio: ['pipe', 'ignore', 'ignore'] });
491
+ child.on('error', reject);
492
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`${command} exited with ${code}`)));
493
+ child.stdin.end(text);
494
+ });
495
+ return;
496
+ } catch (error) {
497
+ lastError = error;
498
+ }
499
+ }
500
+ throw new Error(`无法复制到剪贴板:${lastError?.message || '未找到可用剪贴板命令'}`);
501
+ }
502
+
503
+ async function result(args) {
504
+ applyRuntimeEnv(args);
505
+ const runId = args.runId || await latestRunId();
506
+ const text = await readFinalResult(runId);
507
+ if (args.copy) {
508
+ await copyToClipboard(text);
509
+ console.log(`已复制最终结果: ${runId}`);
510
+ return;
511
+ }
512
+ console.log(text);
513
+ }
514
+
515
+ async function stop(args) {
516
+ applyRuntimeEnv(args);
517
+ if (!args.runId) throw new Error('stop requires a runId');
518
+ const { stopRun } = await import('../src/orchestrator.js');
519
+ const state = await stopRun(args.runId, { reason: args.reason });
520
+ console.log(`已停止任务批次: ${state.runId}`);
521
+ }
522
+
523
+ async function autoRun(args) {
524
+ applyRuntimeEnv(args);
525
+ if (!args.runId) throw new Error('auto requires a runId');
526
+ const { loadRun, startPlanner } = await import('../src/orchestrator.js');
527
+ const state = await loadRun(args.runId);
528
+ if (!state) throw new Error(`run not found: ${args.runId}`);
529
+ if (state.status === 'created') await startPlanner(args.runId);
530
+ const finalState = await watchRun(args.runId, { auto: true, pollMs: args.pollMs });
531
+ if (isFailureTerminal(finalState)) process.exitCode = 1;
532
+ }
533
+
534
+ async function submit(args) {
535
+ if (args.detach && !args.auto) throw new Error('--detach requires auto mode; remove --no-auto');
536
+ applyRuntimeEnv(args);
537
+ const taskText = await readTaskText(args);
538
+ const { createRun, startPlanner } = await import('../src/orchestrator.js');
539
+ const state = await createRun({
540
+ label: args.label,
541
+ taskText,
542
+ repo: process.env.KANBAN_DEFAULT_REPO,
543
+ maxParallel: args.maxParallel,
544
+ workerSandbox: args.workerSandbox
545
+ });
546
+ console.log(`已创建任务批次: ${state.runId}`);
547
+ console.log(`看板地址: ${webUrl(args, state.runId)}`);
548
+ console.log(`终端查看: input-kanban status ${state.runId} --watch`);
549
+ if (args.detach) {
550
+ const pid = startDetachedAuto(state.runId, args);
551
+ console.log(`后台执行中: supervisor pid ${pid}`);
552
+ return;
553
+ }
554
+ console.log('发起任务拆分...');
555
+ await startPlanner(state.runId);
556
+ if (!args.watch && !args.auto) return;
557
+ const finalState = await watchRun(state.runId, { auto: args.auto, pollMs: args.pollMs });
558
+ console.log(`最终状态: ${finalState.status}`);
559
+ if (isFailureTerminal(finalState)) process.exitCode = 1;
560
+ }
561
+
562
+ try {
563
+ const { command, rest } = splitCommand(process.argv.slice(2));
564
+ if (command === 'serve') {
565
+ const args = parseServeArgs(rest);
566
+ if (args.help) { printHelp(); process.exit(0); }
567
+ await serve(args);
568
+ } else if (command === 'submit') {
569
+ const args = parseSubmitArgs(rest);
570
+ if (args.help) { printSubmitHelp(); process.exit(0); }
571
+ await submit(args);
572
+ } else if (command === 'status') {
573
+ const args = parseStatusArgs(rest);
574
+ if (args.help) { printStatusHelp(); process.exit(0); }
575
+ await status(args);
576
+ } else if (command === 'result') {
577
+ const args = parseResultArgs(rest);
578
+ if (args.help) { printResultHelp(); process.exit(0); }
579
+ await result(args);
580
+ } else if (command === 'stop') {
581
+ const args = parseStopArgs(rest);
582
+ if (args.help) { printStopHelp(); process.exit(0); }
583
+ await stop(args);
584
+ } else if (command === 'auto') {
585
+ const args = parseAutoArgs(rest);
586
+ if (args.help) { printAutoHelp(); process.exit(0); }
587
+ await autoRun(args);
588
+ }
79
589
  } catch (error) {
80
590
  console.error(error.message || String(error));
81
591
  process.exit(1);
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "input-kanban",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "input-kanban": "bin/input-kanban.js"
7
7
  },
8
8
  "scripts": {
9
9
  "start": "node bin/input-kanban.js",
10
- "check": "node --check bin/input-kanban.js && node --check bin/input-kanban-format-events.js && node --check bin/input-kanban-tmux-overview.js && node --check src/server.js && node --check src/orchestrator.js && node --check src/appServerClient.js && node --check src/utils.js && node --check src/eventFormatter.js && node --check src/runners/index.js && node --check src/runners/headlessRunner.js && node --check src/runners/tmuxRunner.js && node --check src/runners/tmuxUtils.js && node --check src/tmux.js && node --test"
10
+ "check": "node --check bin/input-kanban.js && node --check bin/input-kanban-format-events.js && node --check bin/input-kanban-timestamp-events.js && node --check bin/input-kanban-tmux-overview.js && node --check src/server.js && node --check src/orchestrator.js && node --check src/appServerClient.js && node --check src/utils.js && node --check src/eventFormatter.js && node --check src/runners/index.js && node --check src/runners/headlessRunner.js && node --check src/runners/tmuxRunner.js && node --check src/runners/tmuxUtils.js && node --check src/tmux.js && node --test"
11
11
  },
12
- "dependencies": {},
13
12
  "description": "A local Codex orchestration kanban dashboard",
14
13
  "files": [
15
14
  "bin",
@@ -17,6 +16,7 @@
17
16
  "public",
18
17
  "README.md",
19
18
  "README.en.md",
19
+ "RELEASE_NOTES.md",
20
20
  "PROJECT_GUIDE.md",
21
21
  "ENVIRONMENT.md"
22
22
  ],
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2
+ <path fill="#000" d="M24 8h80c8.8 0 16 7.2 16 16v80c0 8.8-7.2 16-16 16H24c-8.8 0-16-7.2-16-16V24C8 15.2 15.2 8 24 8Zm8 24v64h14V32H32Zm25 0v64h14V32H57Zm25 0v64h14V32H82Z"/>
3
+ </svg>