input-kanban 0.0.7 → 0.0.9

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,20 +1,54 @@
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
 
7
+ const PACKAGE_VERSION = JSON.parse(await fsp.readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
5
8
  const VALID_RUNNERS = ['headless', 'tmux'];
9
+ const VALID_SANDBOXES = ['read-only', 'workspace-write', 'danger-full-access'];
10
+ const COMMANDS = new Set(['serve', 'submit', 'runs', 'status', 'result', 'retry', 'stop', 'auto']);
11
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
12
+ const STATUS_TEXT = {
13
+ created: '已创建', planning: '拆分中', plan_failed: '拆分失败', plan_empty: '拆分为空', planned: '已拆分',
14
+ running: '执行中', batch_blocked: '批次阻塞', batches_completed: '批次完成', judging: '验收中', judged: '已验收',
15
+ judge_failed: '验收失败', stopped: '已停止'
16
+ };
17
+
18
+ function validateChoice(value, source, choices) {
19
+ if (choices.includes(value)) return value;
20
+ throw new Error(`invalid ${source}: ${value}; expected one of: ${choices.join(', ')}`);
21
+ }
6
22
 
7
23
  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(', ')}`);
24
+ return validateChoice(value, source, VALID_RUNNERS);
25
+ }
26
+
27
+ function validateSandbox(value, source) {
28
+ return validateChoice(value, source, VALID_SANDBOXES);
10
29
  }
11
30
 
12
- function parseArgs(argv) {
13
- const args = { host: '127.0.0.1', port: undefined, repo: undefined, runsDir: undefined, codexBin: undefined, runner: undefined, open: false, help: false };
31
+ function splitCommand(argv) {
32
+ let index = 0;
33
+ const globals = { json: false };
34
+ while (index < argv.length) {
35
+ const arg = argv[index];
36
+ if (arg === '--json' || arg === '-j') { globals.json = true; index++; continue; }
37
+ break;
38
+ }
39
+ const rest = argv.slice(index);
40
+ if (rest[0] === '--version' || rest[0] === '-v' || rest[0] === 'version') return { command: 'version', rest: rest.slice(1), globals };
41
+ if (rest[0] && COMMANDS.has(rest[0])) return { command: rest[0], rest: rest.slice(1), globals };
42
+ return { command: 'serve', rest, globals };
43
+ }
44
+
45
+ function parseServeArgs(argv) {
46
+ const args = { host: '127.0.0.1', port: undefined, repo: undefined, runsDir: undefined, codexBin: undefined, runner: undefined, open: false, json: false, help: false };
14
47
  for (let i = 0; i < argv.length; i++) {
15
48
  const arg = argv[i];
16
49
  const next = () => argv[++i];
17
50
  if (arg === '--help' || arg === '-h') args.help = true;
51
+ else if (arg === '--json' || arg === '-j') args.json = true;
18
52
  else if (arg === '--open') args.open = true;
19
53
  else if (arg === '--no-open') args.open = false;
20
54
  else if (arg === '--host') args.host = next();
@@ -28,22 +62,347 @@ function parseArgs(argv) {
28
62
  return args;
29
63
  }
30
64
 
65
+ function parseRunsArgs(argv) {
66
+ const args = { runsDir: undefined, active: false, includeArchived: false, limit: 20, json: false, help: false };
67
+ for (let i = 0; i < argv.length; i++) {
68
+ const arg = argv[i];
69
+ const next = () => argv[++i];
70
+ if (arg === '--help' || arg === '-h') args.help = true;
71
+ else if (arg === '--json' || arg === '-j') args.json = true;
72
+ else if (arg === '--runs-dir') args.runsDir = next();
73
+ else if (arg === '--active') args.active = true;
74
+ else if (arg === '--include-archived') args.includeArchived = true;
75
+ else if (arg === '--limit') args.limit = Number(next());
76
+ else throw new Error(`unknown runs argument: ${arg}`);
77
+ }
78
+ return args;
79
+ }
80
+
81
+ function parseStatusArgs(argv) {
82
+ const args = { host: '127.0.0.1', port: 8787, runsDir: undefined, runId: undefined, watch: false, json: false, pollMs: 3000, help: false };
83
+ for (let i = 0; i < argv.length; i++) {
84
+ const arg = argv[i];
85
+ const next = () => argv[++i];
86
+ if (arg === '--help' || arg === '-h') args.help = true;
87
+ else if (arg === '--json' || arg === '-j') args.json = true;
88
+ else if (arg === '--host') args.host = next();
89
+ else if (arg === '--port' || arg === '-p') args.port = Number(next());
90
+ else if (arg === '--runs-dir') args.runsDir = next();
91
+ else if (arg === '--watch') args.watch = true;
92
+ else if (arg === '--poll-ms') args.pollMs = Number(next());
93
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
94
+ else throw new Error(`unknown status argument: ${arg}`);
95
+ }
96
+ return args;
97
+ }
98
+
99
+ function parseResultArgs(argv) {
100
+ const args = { runsDir: undefined, runId: undefined, copy: false, json: false, 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 === '--json' || arg === '-j') args.json = true;
106
+ else if (arg === '--runs-dir') args.runsDir = next();
107
+ else if (arg === '--copy') args.copy = true;
108
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
109
+ else throw new Error(`unknown result argument: ${arg}`);
110
+ }
111
+ return args;
112
+ }
113
+
114
+ function parseRetryArgs(argv) {
115
+ const args = { runsDir: undefined, runId: undefined, taskId: undefined, reason: 'manual retry from CLI', maxRetries: 1, json: false, help: false };
116
+ for (let i = 0; i < argv.length; i++) {
117
+ const arg = argv[i];
118
+ const next = () => argv[++i];
119
+ if (arg === '--help' || arg === '-h') args.help = true;
120
+ else if (arg === '--json' || arg === '-j') args.json = true;
121
+ else if (arg === '--runs-dir') args.runsDir = next();
122
+ else if (arg === '--reason') args.reason = next();
123
+ else if (arg === '--max-retries') args.maxRetries = Number(next());
124
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
125
+ else if (!arg.startsWith('-') && !args.taskId) args.taskId = arg;
126
+ else throw new Error(`unknown retry argument: ${arg}`);
127
+ }
128
+ return args;
129
+ }
130
+
131
+ function parseStopArgs(argv) {
132
+ const args = { runsDir: undefined, runId: undefined, reason: 'stopped from CLI', json: false, help: false };
133
+ for (let i = 0; i < argv.length; i++) {
134
+ const arg = argv[i];
135
+ const next = () => argv[++i];
136
+ if (arg === '--help' || arg === '-h') args.help = true;
137
+ else if (arg === '--json' || arg === '-j') args.json = true;
138
+ else if (arg === '--runs-dir') args.runsDir = next();
139
+ else if (arg === '--reason') args.reason = next();
140
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
141
+ else throw new Error(`unknown stop argument: ${arg}`);
142
+ }
143
+ return args;
144
+ }
145
+
146
+ function parseAutoArgs(argv) {
147
+ const args = { host: '127.0.0.1', port: 8787, runsDir: undefined, codexBin: undefined, runner: undefined, runId: undefined, json: false, pollMs: 3000, maxRetries: 1, help: false };
148
+ for (let i = 0; i < argv.length; i++) {
149
+ const arg = argv[i];
150
+ const next = () => argv[++i];
151
+ if (arg === '--help' || arg === '-h') args.help = true;
152
+ else if (arg === '--json' || arg === '-j') args.json = true;
153
+ else if (arg === '--host') args.host = next();
154
+ else if (arg === '--port' || arg === '-p') args.port = Number(next());
155
+ else if (arg === '--runs-dir') args.runsDir = next();
156
+ else if (arg === '--codex-bin') args.codexBin = next();
157
+ else if (arg === '--runner') args.runner = validateRunner(next(), '--runner');
158
+ else if (arg === '--poll-ms') args.pollMs = Number(next());
159
+ else if (arg === '--max-retries') args.maxRetries = Number(next());
160
+ else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
161
+ else throw new Error(`unknown auto argument: ${arg}`);
162
+ }
163
+ return args;
164
+ }
165
+
166
+ function parseSubmitArgs(argv) {
167
+ const args = {
168
+ host: '127.0.0.1', port: 8787, repo: undefined, runsDir: undefined, codexBin: undefined,
169
+ runner: undefined, label: undefined, taskText: undefined, taskFile: undefined, maxParallel: 3,
170
+ workerSandbox: 'workspace-write', auto: true, detach: false, watch: true, json: false, pollMs: 3000, maxRetries: 1, help: false
171
+ };
172
+ for (let i = 0; i < argv.length; i++) {
173
+ const arg = argv[i];
174
+ const next = () => argv[++i];
175
+ if (arg === '--help' || arg === '-h') args.help = true;
176
+ else if (arg === '--json' || arg === '-j') args.json = true;
177
+ else if (arg === '--host') args.host = next();
178
+ else if (arg === '--port' || arg === '-p') args.port = Number(next());
179
+ else if (arg === '--repo' || arg === '-r') args.repo = next();
180
+ else if (arg === '--runs-dir') args.runsDir = next();
181
+ else if (arg === '--codex-bin') args.codexBin = next();
182
+ else if (arg === '--runner') args.runner = validateRunner(next(), '--runner');
183
+ else if (arg === '--label' || arg === '-l') args.label = next();
184
+ else if (arg === '--task') args.taskText = next();
185
+ else if (arg === '--task-file') args.taskFile = next();
186
+ else if (arg === '--max-parallel') args.maxParallel = Number(next());
187
+ else if (arg === '--worker-sandbox') args.workerSandbox = validateSandbox(next(), '--worker-sandbox');
188
+ else if (arg === '--auto') { args.auto = true; args.watch = true; }
189
+ else if (arg === '--no-auto') { args.auto = false; args.watch = false; }
190
+ else if (arg === '--detach' || arg === '-d') args.detach = true;
191
+ else if (arg === '--watch') args.watch = true;
192
+ else if (arg === '--poll-ms') args.pollMs = Number(next());
193
+ else if (arg === '--max-retries') args.maxRetries = Number(next());
194
+ else throw new Error(`unknown submit argument: ${arg}`);
195
+ }
196
+ return args;
197
+ }
198
+
199
+ function applyRuntimeEnv(args) {
200
+ if (args.port) process.env.PORT = String(args.port);
201
+ if (args.host) process.env.HOST = args.host;
202
+ if (args.repo) process.env.KANBAN_DEFAULT_REPO = path.resolve(args.repo);
203
+ else if (!process.env.KANBAN_DEFAULT_REPO) process.env.KANBAN_DEFAULT_REPO = process.cwd();
204
+ if (args.runsDir) process.env.KANBAN_RUNS_DIR = path.resolve(args.runsDir);
205
+ if (args.codexBin) process.env.KANBAN_CODEX_BIN = args.codexBin;
206
+ if (args.runner) process.env.KANBAN_RUNNER = args.runner;
207
+ }
208
+
209
+ function printJson(value) {
210
+ console.log(JSON.stringify(value, null, 2));
211
+ }
212
+
213
+ function printVersion() {
214
+ console.log(`input-kanban v${PACKAGE_VERSION}`);
215
+ }
216
+
31
217
  function printHelp() {
32
- console.log(`input-kanban
218
+ console.log(`input-kanban v${PACKAGE_VERSION}
33
219
 
34
220
  Usage:
35
221
  input-kanban [options]
222
+ input-kanban serve [options]
223
+ input-kanban submit [options]
224
+ input-kanban --version
225
+ input-kanban runs [options]
226
+ input-kanban status [runId] [options]
227
+ input-kanban result [runId] [options]
228
+ input-kanban retry <runId> [taskId] [options]
229
+ input-kanban stop <runId> [options]
230
+
231
+ Serve options:
232
+ --host <host> Host to bind, default 127.0.0.1
233
+ -p, --port <port> Port to bind, default 8787
234
+ -r, --repo <path> Default target repository, default current directory
235
+ --runs-dir <path> Runtime runs directory, default ~/.input-kanban/runs
236
+ --codex-bin <path> Codex CLI executable, default codex
237
+ --runner <mode> Runner mode: headless or tmux, default headless
238
+ -j, --json Emit JSON startup output
239
+ -v, --version Print version and exit
240
+ --open Open browser after starting
241
+ --no-open Do not open browser, default
242
+
243
+ Runs options:
244
+ --runs-dir <path> Runtime runs directory shared with the Web UI
245
+ --active Show only active or pending-action runs
246
+ --include-archived Include archived runs
247
+ --limit <n> Maximum rows to print, default 20
248
+ -j, --json Emit JSON output instead of human text
249
+
250
+ Status options:
251
+ --runs-dir <path> Runtime runs directory shared with the Web UI
252
+ --watch Keep printing status until the run reaches a terminal state
253
+ --poll-ms <ms> Watch poll interval, default 3000
254
+ -j, --json Emit JSON output instead of human text
255
+
256
+ Result options:
257
+ --runs-dir <path> Runtime runs directory shared with the Web UI
258
+ --copy Copy final result to clipboard
259
+ -j, --json Emit JSON output instead of human text
260
+
261
+ Retry options:
262
+ --runs-dir <path> Runtime runs directory shared with the Web UI
263
+ --reason <text> Retry reason stored in task retry history
264
+ --max-retries <n> Retry limit for automatic retry policy, default 1
265
+ -j, --json Emit JSON output instead of human text
266
+
267
+ Stop options:
268
+ --runs-dir <path> Runtime runs directory shared with the Web UI
269
+ --reason <text> Stop reason stored in run state
270
+ -j, --json Emit JSON output instead of human text
271
+
272
+ Submit options:
273
+ -r, --repo <path> Target Git work tree, default current directory
274
+ -l, --label <label> Task batch name, default generated from task text
275
+ --task <text> Task description text
276
+ --task-file <path> Read task description from file, use - for stdin
277
+ --max-parallel <n> Default max parallel workers, default 3
278
+ --worker-sandbox <mode> read-only, workspace-write, or danger-full-access
279
+ --runner <mode> Runner mode: headless or tmux
280
+ --runs-dir <path> Runtime runs directory shared with the Web UI
281
+ --auto Plan, dispatch all batches, judge, and watch, default for submit
282
+ --no-auto Only create the run and start planning
283
+ -d, --detach Run the default auto loop in a background supervisor
284
+ --watch Watch status after starting the planner
285
+ --poll-ms <ms> Watch poll interval, default 3000
286
+ -j, --json Emit JSON output instead of human text
287
+ -h, --help Show help
288
+ `);
289
+ }
290
+
291
+ function printSubmitHelp() {
292
+ console.log(`input-kanban submit
293
+
294
+ Usage:
295
+ input-kanban submit --repo <path> --task-file task.md
296
+ input-kanban submit --repo <path> --task "fix the bug" --label "bugfix"
297
+ input-kanban submit --task-file task.md -d
298
+
299
+ Options:
300
+ -r, --repo <path> Target Git work tree, default current directory
301
+ -l, --label <label> Task batch name, default generated from task text
302
+ --task <text> Task description text
303
+ --task-file <path> Read task description from file, use - for stdin
304
+ --max-parallel <n> Default max parallel workers, default 3
305
+ --worker-sandbox <mode> read-only, workspace-write, or danger-full-access
306
+ --runner <mode> Runner mode: headless or tmux
307
+ --runs-dir <path> Runtime runs directory shared with the Web UI
308
+ --auto Plan, dispatch all batches, judge, and watch, default for submit
309
+ --no-auto Only create the run and start planning
310
+ -d, --detach Run the default auto loop in a background supervisor
311
+ --watch Watch status after starting the planner
312
+ --poll-ms <ms> Watch poll interval, default 3000
313
+ `);
314
+ }
315
+
316
+ function printRunsHelp() {
317
+ console.log(`input-kanban runs
318
+
319
+ Usage:
320
+ input-kanban runs
321
+ input-kanban runs --active
322
+ input-kanban --json runs --active
36
323
 
37
324
  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
325
+ --runs-dir <path> Runtime runs directory shared with the Web UI
326
+ --active Show only active or pending-action runs
327
+ --include-archived Include archived runs
328
+ --limit <n> Maximum rows to print, default 20
329
+ -j, --json Emit JSON output instead of human text
330
+ `);
331
+ }
332
+
333
+ function printStatusHelp() {
334
+ console.log(`input-kanban status
335
+
336
+ Usage:
337
+ input-kanban status
338
+ input-kanban status <runId>
339
+ input-kanban status --watch
340
+ input-kanban status <runId> --watch
341
+
342
+ Options:
343
+ --runs-dir <path> Runtime runs directory shared with the Web UI
344
+ --watch Keep printing status until the run reaches a terminal state
345
+ --poll-ms <ms> Watch poll interval, default 3000
346
+ -j, --json Emit JSON output instead of human text
347
+ `);
348
+ }
349
+
350
+ function printResultHelp() {
351
+ console.log(`input-kanban result
352
+
353
+ Usage:
354
+ input-kanban result
355
+ input-kanban result <runId>
356
+ input-kanban result <runId> --copy
357
+
358
+ Options:
359
+ --runs-dir <path> Runtime runs directory shared with the Web UI
360
+ --copy Copy final result to clipboard
361
+ -j, --json Emit JSON output instead of human text
362
+ `);
363
+ }
364
+
365
+ function printRetryHelp() {
366
+ console.log(`input-kanban retry
367
+
368
+ Usage:
369
+ input-kanban retry <runId>
370
+ input-kanban retry <runId> <taskId>
371
+ input-kanban --json retry <runId> <taskId>
372
+
373
+ Options:
374
+ --runs-dir <path> Runtime runs directory shared with the Web UI
375
+ --reason <text> Retry reason stored in task retry history
376
+ --max-retries <n> Retry limit for automatic retry policy, default 1
377
+ -j, --json Emit JSON output instead of human text
378
+ `);
379
+ }
380
+
381
+ function printStopHelp() {
382
+ console.log(`input-kanban stop
383
+
384
+ Usage:
385
+ input-kanban stop <runId>
386
+
387
+ Options:
388
+ --runs-dir <path> Runtime runs directory shared with the Web UI
389
+ --reason <text> Stop reason stored in run state
390
+ -j, --json Emit JSON output instead of human text
391
+ `);
392
+ }
393
+
394
+ function printAutoHelp() {
395
+ console.log(`input-kanban auto
396
+
397
+ Usage:
398
+ input-kanban auto <runId>
399
+
400
+ Options:
401
+ --runs-dir <path> Runtime runs directory shared with the Web UI
402
+ --codex-bin <path> Codex CLI executable, default codex
403
+ --runner <mode> Runner mode: headless or tmux
404
+ --poll-ms <ms> Watch poll interval, default 3000
405
+ -j, --json Emit JSON output instead of human text
47
406
  `);
48
407
  }
49
408
 
@@ -54,28 +413,395 @@ function openBrowser(url) {
54
413
  child.unref();
55
414
  }
56
415
 
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;
416
+ async function readStdin() {
417
+ const chunks = [];
418
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
419
+ return Buffer.concat(chunks).toString('utf8');
420
+ }
421
+
422
+ async function readTaskText(args) {
423
+ if (args.taskText !== undefined) return args.taskText;
424
+ if (args.taskFile === '-') return await readStdin();
425
+ if (args.taskFile) return await fsp.readFile(path.resolve(args.taskFile), 'utf8');
426
+ throw new Error('submit requires --task or --task-file');
427
+ }
428
+
429
+ function baseUrl(args) {
430
+ return `http://${args.host || '127.0.0.1'}:${Number(args.port || 8787)}`;
431
+ }
432
+
433
+ function webUrl(args, runId = '') {
434
+ return `${baseUrl(args)}${runId ? ` (runId: ${runId})` : ''}`;
435
+ }
67
436
 
437
+ function displayStatus(status) {
438
+ const text = STATUS_TEXT[status] || status || '-';
439
+ return status && text !== status ? `${text}(${status})` : text;
440
+ }
441
+
442
+ function countByStatus(state) {
443
+ const tasks = state.tasks || [];
444
+ return {
445
+ total: tasks.length,
446
+ completed: tasks.filter(task => task.status === 'completed').length,
447
+ running: tasks.filter(task => task.status === 'running').length,
448
+ failed: tasks.filter(task => ['failed', 'unknown'].includes(task.status)).length
449
+ };
450
+ }
451
+
452
+ function currentBatchText(state) {
453
+ const batch = (state.batches || []).find(item => item.status !== 'completed');
454
+ if (!batch) return '-';
455
+ const tasks = batch.tasks || [];
456
+ const completed = tasks.filter(task => task.status === 'completed').length;
457
+ return `${batch.name || batch.id}(${batch.id}) ${displayStatus(batch.status)} ${completed}/${tasks.length}`;
458
+ }
459
+
460
+ function statusLine(state) {
461
+ const counts = countByStatus(state);
462
+ return `${state.label || state.runId}|${state.runId}|状态 ${displayStatus(state.status)}|进度 ${counts.completed}/${counts.total}|执行中 ${counts.running}|失败 ${counts.failed}`;
463
+ }
464
+
465
+ function printRunStatus(state) {
466
+ const counts = countByStatus(state);
467
+ console.log(`任务批次: ${state.label || '-'}`);
468
+ console.log(`Run ID: ${state.runId}`);
469
+ console.log(`状态: ${displayStatus(state.status)}`);
470
+ console.log(`仓库: ${state.repo || '-'}`);
471
+ console.log(`当前批次: ${currentBatchText(state)}`);
472
+ console.log(`进度: ${counts.completed}/${counts.total} |执行中 ${counts.running} |失败 ${counts.failed}`);
473
+ if (state.judge?.status && state.judge.status !== 'pending') console.log(`验收: ${displayStatus(state.judge.status)}`);
474
+ }
475
+
476
+ function printRunsTable(runs) {
477
+ if (!runs.length) { console.log('没有找到任务批次'); return; }
478
+ for (const run of runs) {
479
+ console.log(`${run.runId}|${run.label || '-'}|${displayStatus(run.status)}|进度 ${run.completed}/${run.total}|执行中 ${run.running}|失败 ${run.failed}|runner ${run.runner || '-'}|沙箱 ${run.workerSandbox || '-'}|仓库 ${run.repo || '-'}`);
480
+ }
481
+ }
482
+
483
+ function isTerminal(state) {
484
+ return ['judged', 'judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(state.status);
485
+ }
486
+
487
+ function isFailureTerminal(state) {
488
+ return ['judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(state.status);
489
+ }
490
+
491
+ function isActiveRunSummary(run) {
492
+ if (!run) return false;
493
+ if (Number(run.running) > 0) return true;
494
+ return !['judged', 'judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(run.status);
495
+ }
496
+
497
+ function hasRecoverableUnknownTask(state) {
498
+ return (state.tasks || []).some(task => task.status === 'unknown' && (task.exitCode === undefined || task.exitCode === 0));
499
+ }
500
+
501
+ async function confirmFailureTerminal(runId, state, refreshRun, pollMs) {
502
+ let confirmed = state;
503
+ const deadline = Date.now() + 30000;
504
+ while (confirmed?.status === 'batch_blocked' && hasRecoverableUnknownTask(confirmed) && Date.now() < deadline) {
505
+ await delay(Math.max(500, Number(pollMs) || 3000));
506
+ confirmed = await refreshRun(runId);
507
+ if (!confirmed || !isTerminal(confirmed) || confirmed.status !== state.status) return { confirmed: false, state: confirmed };
508
+ }
509
+ return { confirmed: true, state: confirmed };
510
+ }
511
+
512
+ async function watchRun(runId, { auto = false, pollMs = 3000, quiet = false, maxRetries = 1 } = {}) {
513
+ const { dispatchRun, refreshRun, retryRun, startJudge } = await import('../src/orchestrator.js');
514
+ let lastStatus = '';
515
+ let judgeStarted = false;
516
+ while (true) {
517
+ const state = await refreshRun(runId);
518
+ if (!state) throw new Error(`run not found: ${runId}`);
519
+ const line = statusLine(state);
520
+ if (line !== lastStatus) {
521
+ if (!quiet) console.log(`[${new Date().toLocaleTimeString()}] ${line}`);
522
+ lastStatus = line;
523
+ }
524
+
525
+ if (auto && state.status === 'batch_blocked') {
526
+ try {
527
+ const retryState = await retryRun(runId, { reason: 'auto retry from CLI', maxRetries, auto: true });
528
+ if (!quiet) console.log(`自动重试任务: ${retryState.retriedTaskIds?.join(', ') || '-'}`);
529
+ lastStatus = '';
530
+ continue;
531
+ } catch (error) {
532
+ if (!quiet) console.log(`自动重试跳过: ${error.message || String(error)}`);
533
+ }
534
+ }
535
+
536
+ if (auto && state.status === 'planned') {
537
+ if (!quiet) console.log('自动派发任务...');
538
+ await dispatchRun(runId);
539
+ } else if (auto && state.status === 'batches_completed' && state.judge?.status !== 'running' && !judgeStarted) {
540
+ if (!quiet) console.log('自动发起最终验收...');
541
+ judgeStarted = true;
542
+ await startJudge(runId);
543
+ }
544
+
545
+ if (isTerminal(state)) {
546
+ if (isFailureTerminal(state)) {
547
+ const result = await confirmFailureTerminal(runId, state, refreshRun, pollMs);
548
+ if (!result.confirmed) {
549
+ lastStatus = '';
550
+ continue;
551
+ }
552
+ return result.state || state;
553
+ }
554
+ return state;
555
+ }
556
+ await delay(Math.max(500, Number(pollMs) || 3000));
557
+ }
558
+ }
559
+
560
+ async function serve(args) {
561
+ applyRuntimeEnv(args);
68
562
  const { startServer } = await import('../src/server.js');
69
563
  const instance = await startServer({ host: process.env.HOST, port: Number(process.env.PORT || 8787), log: false });
70
- console.log('Input Kanban started');
71
- console.log(`URL: ${instance.url}`);
72
- console.log(`Repo: ${instance.defaultRepo}`);
73
- console.log(`Runs: ${instance.runsDir}`);
74
- console.log(`Runner: ${instance.runner}`);
564
+ if (args.json) {
565
+ printJson({ ok: true, command: 'serve', version: instance.version, url: instance.url, repo: instance.defaultRepo, runsDir: instance.runsDir, runner: instance.runner });
566
+ } else {
567
+ console.log(`Input Kanban v${PACKAGE_VERSION} started`);
568
+ console.log(`URL: ${instance.url}`);
569
+ console.log(`Repo: ${instance.defaultRepo}`);
570
+ console.log(`Runs: ${instance.runsDir}`);
571
+ console.log(`Runner: ${instance.runner}`);
572
+ }
75
573
  if (args.open) openBrowser(instance.url);
76
574
  const shutdown = () => { instance.stop().finally(() => process.exit(0)); };
77
575
  process.on('SIGINT', shutdown);
78
576
  process.on('SIGTERM', shutdown);
577
+ }
578
+
579
+ function detachedAutoArgs(runId, args) {
580
+ const cliPath = fileURLToPath(import.meta.url);
581
+ const values = [cliPath, 'auto', runId, '--host', args.host || '127.0.0.1', '--port', String(args.port || 8787), '--poll-ms', String(args.pollMs || 3000), '--max-retries', String(args.maxRetries ?? 1)];
582
+ if (args.runsDir) values.push('--runs-dir', path.resolve(args.runsDir));
583
+ if (args.codexBin) values.push('--codex-bin', args.codexBin);
584
+ if (args.runner) values.push('--runner', args.runner);
585
+ return values;
586
+ }
587
+
588
+ function startDetachedAuto(runId, args) {
589
+ const child = spawn(process.execPath, detachedAutoArgs(runId, args), {
590
+ detached: true,
591
+ stdio: 'ignore',
592
+ env: process.env
593
+ });
594
+ child.unref();
595
+ return child.pid;
596
+ }
597
+
598
+ async function latestRunId() {
599
+ const { listRuns } = await import('../src/orchestrator.js');
600
+ const runs = await listRuns();
601
+ if (!runs.length) throw new Error('没有找到任务批次');
602
+ return runs[0].runId;
603
+ }
604
+
605
+ async function runs(args) {
606
+ applyRuntimeEnv(args);
607
+ const { listRuns } = await import('../src/orchestrator.js');
608
+ const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0 ? Number(args.limit) : 20;
609
+ const allRuns = await listRuns({ includeArchived: !!args.includeArchived });
610
+ const filtered = (args.active ? allRuns.filter(isActiveRunSummary) : allRuns).slice(0, limit);
611
+ if (args.json) {
612
+ printJson({ ok: true, command: 'runs', active: !!args.active, includeArchived: !!args.includeArchived, limit, count: filtered.length, runs: filtered });
613
+ return;
614
+ }
615
+ printRunsTable(filtered);
616
+ }
617
+
618
+ async function status(args) {
619
+ applyRuntimeEnv(args);
620
+ const runId = args.runId || await latestRunId();
621
+ const { refreshRun, summaryOfRun } = await import('../src/orchestrator.js');
622
+ if (args.watch) {
623
+ const finalState = await watchRun(runId, { auto: false, pollMs: args.pollMs, quiet: args.json });
624
+ if (isFailureTerminal(finalState)) process.exitCode = 1;
625
+ if (args.json) printJson({ ok: true, command: 'status', run: summaryOfRun(finalState) });
626
+ return;
627
+ }
628
+ const state = await refreshRun(runId);
629
+ if (!state) throw new Error(`run not found: ${runId}`);
630
+ if (args.json) { printJson({ ok: true, command: 'status', run: summaryOfRun(state) }); return; }
631
+ printRunStatus(state);
632
+ }
633
+
634
+ async function readFinalResult(runId) {
635
+ const { loadRun, readRunFile } = await import('../src/orchestrator.js');
636
+ const state = await loadRun(runId);
637
+ if (!state) throw new Error(`run not found: ${runId}`);
638
+ try {
639
+ const text = await readRunFile(runId, 'judge', 'verdict.json');
640
+ let parsed = null;
641
+ try { parsed = JSON.parse(text); } catch {}
642
+ return { state, source: 'judge/verdict.json', text, parsed };
643
+ }
644
+ catch {}
645
+ try {
646
+ const text = await readRunFile(runId, 'judge', 'last_message.md');
647
+ return { state, source: 'judge/last_message.md', text, parsed: null };
648
+ }
649
+ catch {}
650
+ throw new Error(`最终结果尚未生成:当前状态 ${displayStatus(state.status)}`);
651
+ }
652
+
653
+ function clipboardCommands() {
654
+ if (process.platform === 'darwin') return [['pbcopy', []]];
655
+ if (process.platform === 'win32') return [['clip', []]];
656
+ return [['wl-copy', []], ['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]];
657
+ }
658
+
659
+ async function copyToClipboard(text) {
660
+ let lastError = null;
661
+ for (const [command, args] of clipboardCommands()) {
662
+ try {
663
+ await new Promise((resolve, reject) => {
664
+ const child = spawn(command, args, { stdio: ['pipe', 'ignore', 'ignore'] });
665
+ child.on('error', reject);
666
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`${command} exited with ${code}`)));
667
+ child.stdin.end(text);
668
+ });
669
+ return;
670
+ } catch (error) {
671
+ lastError = error;
672
+ }
673
+ }
674
+ throw new Error(`无法复制到剪贴板:${lastError?.message || '未找到可用剪贴板命令'}`);
675
+ }
676
+
677
+ async function result(args) {
678
+ applyRuntimeEnv(args);
679
+ const runId = args.runId || await latestRunId();
680
+ const { state, source, text, parsed } = await readFinalResult(runId);
681
+ const { summaryOfRun } = await import('../src/orchestrator.js');
682
+ if (args.copy) {
683
+ await copyToClipboard(text);
684
+ if (args.json) { printJson({ ok: true, command: 'result', run: summaryOfRun(state), source, copied: true }); return; }
685
+ console.log(`已复制最终结果: ${runId}`);
686
+ return;
687
+ }
688
+ if (args.json) {
689
+ printJson({ ok: true, command: 'result', run: summaryOfRun(state), source, result: parsed, text: parsed ? null : text });
690
+ return;
691
+ }
692
+ console.log(text);
693
+ }
694
+
695
+ async function retry(args) {
696
+ applyRuntimeEnv(args);
697
+ if (!args.runId) throw new Error('retry requires a runId');
698
+ const { retryRun, summaryOfRun } = await import('../src/orchestrator.js');
699
+ const state = await retryRun(args.runId, { taskId: args.taskId, reason: args.reason, maxRetries: args.maxRetries });
700
+ if (args.json) { printJson({ ok: true, command: 'retry', run: summaryOfRun(state), retriedTaskIds: state.retriedTaskIds || [] }); return; }
701
+ console.log(`已重试任务: ${(state.retriedTaskIds || []).join(', ') || '-'}`);
702
+ }
703
+
704
+ async function stop(args) {
705
+ applyRuntimeEnv(args);
706
+ if (!args.runId) throw new Error('stop requires a runId');
707
+ const { stopRun, summaryOfRun } = await import('../src/orchestrator.js');
708
+ const state = await stopRun(args.runId, { reason: args.reason });
709
+ if (args.json) { printJson({ ok: true, command: 'stop', run: summaryOfRun(state), reason: args.reason }); return; }
710
+ console.log(`已停止任务批次: ${state.runId}`);
711
+ }
712
+
713
+ async function autoRun(args) {
714
+ applyRuntimeEnv(args);
715
+ if (!args.runId) throw new Error('auto requires a runId');
716
+ const { loadRun, startPlanner, summaryOfRun } = await import('../src/orchestrator.js');
717
+ const state = await loadRun(args.runId);
718
+ if (!state) throw new Error(`run not found: ${args.runId}`);
719
+ if (state.status === 'created') await startPlanner(args.runId);
720
+ const finalState = await watchRun(args.runId, { auto: true, pollMs: args.pollMs, quiet: args.json, maxRetries: args.maxRetries });
721
+ if (isFailureTerminal(finalState)) process.exitCode = 1;
722
+ if (args.json) { printJson({ ok: true, command: 'auto', run: summaryOfRun(finalState) }); return; }
723
+ }
724
+
725
+ async function submit(args) {
726
+ if (args.detach && !args.auto) throw new Error('--detach requires auto mode; remove --no-auto');
727
+ applyRuntimeEnv(args);
728
+ const taskText = await readTaskText(args);
729
+ const { createRun, startPlanner, summaryOfRun } = await import('../src/orchestrator.js');
730
+ const state = await createRun({
731
+ label: args.label,
732
+ taskText,
733
+ repo: process.env.KANBAN_DEFAULT_REPO,
734
+ maxParallel: args.maxParallel,
735
+ workerSandbox: args.workerSandbox
736
+ });
737
+ if (!args.json) {
738
+ console.log(`已创建任务批次: ${state.runId}`);
739
+ console.log(`看板地址: ${webUrl(args, state.runId)}`);
740
+ console.log(`终端查看: input-kanban status ${state.runId} --watch`);
741
+ }
742
+ if (args.detach) {
743
+ const pid = startDetachedAuto(state.runId, args);
744
+ if (args.json) { printJson({ ok: true, command: 'submit', phase: 'detached', url: baseUrl(args), supervisorPid: pid, run: summaryOfRun(state) }); return; }
745
+ console.log(`后台执行中: supervisor pid ${pid}`);
746
+ return;
747
+ }
748
+ if (!args.json) console.log('发起任务拆分...');
749
+ const plannedState = await startPlanner(state.runId);
750
+ if (!args.watch && !args.auto) {
751
+ if (args.json) { printJson({ ok: true, command: 'submit', phase: 'planned', url: baseUrl(args), auto: args.auto, watch: args.watch, run: summaryOfRun(plannedState || state) }); }
752
+ return;
753
+ }
754
+ const finalState = await watchRun(state.runId, { auto: args.auto, pollMs: args.pollMs, quiet: args.json, maxRetries: args.maxRetries });
755
+ if (isFailureTerminal(finalState)) process.exitCode = 1;
756
+ if (args.json) { printJson({ ok: true, command: 'submit', phase: 'final', url: baseUrl(args), auto: args.auto, watch: args.watch, run: summaryOfRun(finalState) }); return; }
757
+ console.log(`最终状态: ${finalState.status}`);
758
+ }
759
+
760
+ try {
761
+ const { command, rest, globals = {} } = splitCommand(process.argv.slice(2));
762
+ if (command === 'serve') {
763
+ const args = parseServeArgs(rest);
764
+ args.json = args.json || globals.json;
765
+ if (args.help) { printHelp(); process.exit(0); }
766
+ await serve(args);
767
+ } else if (command === 'version') {
768
+ printVersion();
769
+ } else if (command === 'submit') {
770
+ const args = parseSubmitArgs(rest);
771
+ args.json = args.json || globals.json;
772
+ if (args.help) { printSubmitHelp(); process.exit(0); }
773
+ await submit(args);
774
+ } else if (command === 'runs') {
775
+ const args = parseRunsArgs(rest);
776
+ args.json = args.json || globals.json;
777
+ if (args.help) { printRunsHelp(); process.exit(0); }
778
+ await runs(args);
779
+ } else if (command === 'status') {
780
+ const args = parseStatusArgs(rest);
781
+ args.json = args.json || globals.json;
782
+ if (args.help) { printStatusHelp(); process.exit(0); }
783
+ await status(args);
784
+ } else if (command === 'result') {
785
+ const args = parseResultArgs(rest);
786
+ args.json = args.json || globals.json;
787
+ if (args.help) { printResultHelp(); process.exit(0); }
788
+ await result(args);
789
+ } else if (command === 'retry') {
790
+ const args = parseRetryArgs(rest);
791
+ args.json = args.json || globals.json;
792
+ if (args.help) { printRetryHelp(); process.exit(0); }
793
+ await retry(args);
794
+ } else if (command === 'stop') {
795
+ const args = parseStopArgs(rest);
796
+ args.json = args.json || globals.json;
797
+ if (args.help) { printStopHelp(); process.exit(0); }
798
+ await stop(args);
799
+ } else if (command === 'auto') {
800
+ const args = parseAutoArgs(rest);
801
+ args.json = args.json || globals.json;
802
+ if (args.help) { printAutoHelp(); process.exit(0); }
803
+ await autoRun(args);
804
+ }
79
805
  } catch (error) {
80
806
  console.error(error.message || String(error));
81
807
  process.exit(1);