input-kanban 0.0.8 → 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.
@@ -4,9 +4,10 @@ import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
+ const PACKAGE_VERSION = JSON.parse(await fsp.readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
7
8
  const VALID_RUNNERS = ['headless', 'tmux'];
8
9
  const VALID_SANDBOXES = ['read-only', 'workspace-write', 'danger-full-access'];
9
- const COMMANDS = new Set(['serve', 'submit', 'status', 'result', 'stop', 'auto']);
10
+ const COMMANDS = new Set(['serve', 'submit', 'runs', 'status', 'result', 'retry', 'stop', 'auto']);
10
11
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
11
12
  const STATUS_TEXT = {
12
13
  created: '已创建', planning: '拆分中', plan_failed: '拆分失败', plan_empty: '拆分为空', planned: '已拆分',
@@ -28,16 +29,26 @@ function validateSandbox(value, source) {
28
29
  }
29
30
 
30
31
  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 };
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 };
33
43
  }
34
44
 
35
45
  function parseServeArgs(argv) {
36
- const args = { host: '127.0.0.1', port: undefined, repo: undefined, runsDir: undefined, codexBin: undefined, runner: undefined, open: false, help: false };
46
+ const args = { host: '127.0.0.1', port: undefined, repo: undefined, runsDir: undefined, codexBin: undefined, runner: undefined, open: false, json: false, help: false };
37
47
  for (let i = 0; i < argv.length; i++) {
38
48
  const arg = argv[i];
39
49
  const next = () => argv[++i];
40
50
  if (arg === '--help' || arg === '-h') args.help = true;
51
+ else if (arg === '--json' || arg === '-j') args.json = true;
41
52
  else if (arg === '--open') args.open = true;
42
53
  else if (arg === '--no-open') args.open = false;
43
54
  else if (arg === '--host') args.host = next();
@@ -51,12 +62,29 @@ function parseServeArgs(argv) {
51
62
  return args;
52
63
  }
53
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
+
54
81
  function parseStatusArgs(argv) {
55
- const args = { host: '127.0.0.1', port: 8787, runsDir: undefined, runId: undefined, watch: false, pollMs: 3000, help: false };
82
+ const args = { host: '127.0.0.1', port: 8787, runsDir: undefined, runId: undefined, watch: false, json: false, pollMs: 3000, help: false };
56
83
  for (let i = 0; i < argv.length; i++) {
57
84
  const arg = argv[i];
58
85
  const next = () => argv[++i];
59
86
  if (arg === '--help' || arg === '-h') args.help = true;
87
+ else if (arg === '--json' || arg === '-j') args.json = true;
60
88
  else if (arg === '--host') args.host = next();
61
89
  else if (arg === '--port' || arg === '-p') args.port = Number(next());
62
90
  else if (arg === '--runs-dir') args.runsDir = next();
@@ -69,11 +97,12 @@ function parseStatusArgs(argv) {
69
97
  }
70
98
 
71
99
  function parseResultArgs(argv) {
72
- const args = { runsDir: undefined, runId: undefined, copy: false, help: false };
100
+ const args = { runsDir: undefined, runId: undefined, copy: false, json: false, help: false };
73
101
  for (let i = 0; i < argv.length; i++) {
74
102
  const arg = argv[i];
75
103
  const next = () => argv[++i];
76
104
  if (arg === '--help' || arg === '-h') args.help = true;
105
+ else if (arg === '--json' || arg === '-j') args.json = true;
77
106
  else if (arg === '--runs-dir') args.runsDir = next();
78
107
  else if (arg === '--copy') args.copy = true;
79
108
  else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
@@ -82,12 +111,30 @@ function parseResultArgs(argv) {
82
111
  return args;
83
112
  }
84
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
+
85
131
  function parseStopArgs(argv) {
86
- const args = { runsDir: undefined, runId: undefined, reason: 'stopped from CLI', help: false };
132
+ const args = { runsDir: undefined, runId: undefined, reason: 'stopped from CLI', json: false, help: false };
87
133
  for (let i = 0; i < argv.length; i++) {
88
134
  const arg = argv[i];
89
135
  const next = () => argv[++i];
90
136
  if (arg === '--help' || arg === '-h') args.help = true;
137
+ else if (arg === '--json' || arg === '-j') args.json = true;
91
138
  else if (arg === '--runs-dir') args.runsDir = next();
92
139
  else if (arg === '--reason') args.reason = next();
93
140
  else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
@@ -97,17 +144,19 @@ function parseStopArgs(argv) {
97
144
  }
98
145
 
99
146
  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 };
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 };
101
148
  for (let i = 0; i < argv.length; i++) {
102
149
  const arg = argv[i];
103
150
  const next = () => argv[++i];
104
151
  if (arg === '--help' || arg === '-h') args.help = true;
152
+ else if (arg === '--json' || arg === '-j') args.json = true;
105
153
  else if (arg === '--host') args.host = next();
106
154
  else if (arg === '--port' || arg === '-p') args.port = Number(next());
107
155
  else if (arg === '--runs-dir') args.runsDir = next();
108
156
  else if (arg === '--codex-bin') args.codexBin = next();
109
157
  else if (arg === '--runner') args.runner = validateRunner(next(), '--runner');
110
158
  else if (arg === '--poll-ms') args.pollMs = Number(next());
159
+ else if (arg === '--max-retries') args.maxRetries = Number(next());
111
160
  else if (!arg.startsWith('-') && !args.runId) args.runId = arg;
112
161
  else throw new Error(`unknown auto argument: ${arg}`);
113
162
  }
@@ -118,12 +167,13 @@ function parseSubmitArgs(argv) {
118
167
  const args = {
119
168
  host: '127.0.0.1', port: 8787, repo: undefined, runsDir: undefined, codexBin: undefined,
120
169
  runner: undefined, label: undefined, taskText: undefined, taskFile: undefined, maxParallel: 3,
121
- workerSandbox: 'workspace-write', auto: true, detach: false, watch: true, pollMs: 3000, help: false
170
+ workerSandbox: 'workspace-write', auto: true, detach: false, watch: true, json: false, pollMs: 3000, maxRetries: 1, help: false
122
171
  };
123
172
  for (let i = 0; i < argv.length; i++) {
124
173
  const arg = argv[i];
125
174
  const next = () => argv[++i];
126
175
  if (arg === '--help' || arg === '-h') args.help = true;
176
+ else if (arg === '--json' || arg === '-j') args.json = true;
127
177
  else if (arg === '--host') args.host = next();
128
178
  else if (arg === '--port' || arg === '-p') args.port = Number(next());
129
179
  else if (arg === '--repo' || arg === '-r') args.repo = next();
@@ -140,6 +190,7 @@ function parseSubmitArgs(argv) {
140
190
  else if (arg === '--detach' || arg === '-d') args.detach = true;
141
191
  else if (arg === '--watch') args.watch = true;
142
192
  else if (arg === '--poll-ms') args.pollMs = Number(next());
193
+ else if (arg === '--max-retries') args.maxRetries = Number(next());
143
194
  else throw new Error(`unknown submit argument: ${arg}`);
144
195
  }
145
196
  return args;
@@ -155,15 +206,26 @@ function applyRuntimeEnv(args) {
155
206
  if (args.runner) process.env.KANBAN_RUNNER = args.runner;
156
207
  }
157
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
+
158
217
  function printHelp() {
159
- console.log(`input-kanban
218
+ console.log(`input-kanban v${PACKAGE_VERSION}
160
219
 
161
220
  Usage:
162
221
  input-kanban [options]
163
222
  input-kanban serve [options]
164
223
  input-kanban submit [options]
224
+ input-kanban --version
225
+ input-kanban runs [options]
165
226
  input-kanban status [runId] [options]
166
227
  input-kanban result [runId] [options]
228
+ input-kanban retry <runId> [taskId] [options]
167
229
  input-kanban stop <runId> [options]
168
230
 
169
231
  Serve options:
@@ -173,21 +235,39 @@ Serve options:
173
235
  --runs-dir <path> Runtime runs directory, default ~/.input-kanban/runs
174
236
  --codex-bin <path> Codex CLI executable, default codex
175
237
  --runner <mode> Runner mode: headless or tmux, default headless
238
+ -j, --json Emit JSON startup output
239
+ -v, --version Print version and exit
176
240
  --open Open browser after starting
177
241
  --no-open Do not open browser, default
178
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
+
179
250
  Status options:
180
251
  --runs-dir <path> Runtime runs directory shared with the Web UI
181
252
  --watch Keep printing status until the run reaches a terminal state
182
253
  --poll-ms <ms> Watch poll interval, default 3000
254
+ -j, --json Emit JSON output instead of human text
183
255
 
184
256
  Result options:
185
257
  --runs-dir <path> Runtime runs directory shared with the Web UI
186
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
187
266
 
188
267
  Stop options:
189
268
  --runs-dir <path> Runtime runs directory shared with the Web UI
190
269
  --reason <text> Stop reason stored in run state
270
+ -j, --json Emit JSON output instead of human text
191
271
 
192
272
  Submit options:
193
273
  -r, --repo <path> Target Git work tree, default current directory
@@ -203,6 +283,7 @@ Submit options:
203
283
  -d, --detach Run the default auto loop in a background supervisor
204
284
  --watch Watch status after starting the planner
205
285
  --poll-ms <ms> Watch poll interval, default 3000
286
+ -j, --json Emit JSON output instead of human text
206
287
  -h, --help Show help
207
288
  `);
208
289
  }
@@ -232,6 +313,23 @@ Options:
232
313
  `);
233
314
  }
234
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
323
+
324
+ Options:
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
+
235
333
  function printStatusHelp() {
236
334
  console.log(`input-kanban status
237
335
 
@@ -245,6 +343,7 @@ Options:
245
343
  --runs-dir <path> Runtime runs directory shared with the Web UI
246
344
  --watch Keep printing status until the run reaches a terminal state
247
345
  --poll-ms <ms> Watch poll interval, default 3000
346
+ -j, --json Emit JSON output instead of human text
248
347
  `);
249
348
  }
250
349
 
@@ -259,6 +358,23 @@ Usage:
259
358
  Options:
260
359
  --runs-dir <path> Runtime runs directory shared with the Web UI
261
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
262
378
  `);
263
379
  }
264
380
 
@@ -271,6 +387,7 @@ Usage:
271
387
  Options:
272
388
  --runs-dir <path> Runtime runs directory shared with the Web UI
273
389
  --reason <text> Stop reason stored in run state
390
+ -j, --json Emit JSON output instead of human text
274
391
  `);
275
392
  }
276
393
 
@@ -285,6 +402,7 @@ Options:
285
402
  --codex-bin <path> Codex CLI executable, default codex
286
403
  --runner <mode> Runner mode: headless or tmux
287
404
  --poll-ms <ms> Watch poll interval, default 3000
405
+ -j, --json Emit JSON output instead of human text
288
406
  `);
289
407
  }
290
408
 
@@ -308,8 +426,12 @@ async function readTaskText(args) {
308
426
  throw new Error('submit requires --task or --task-file');
309
427
  }
310
428
 
429
+ function baseUrl(args) {
430
+ return `http://${args.host || '127.0.0.1'}:${Number(args.port || 8787)}`;
431
+ }
432
+
311
433
  function webUrl(args, runId = '') {
312
- return `http://${args.host || '127.0.0.1'}:${Number(args.port || 8787)}${runId ? ` (runId: ${runId})` : ''}`;
434
+ return `${baseUrl(args)}${runId ? ` (runId: ${runId})` : ''}`;
313
435
  }
314
436
 
315
437
  function displayStatus(status) {
@@ -351,6 +473,13 @@ function printRunStatus(state) {
351
473
  if (state.judge?.status && state.judge.status !== 'pending') console.log(`验收: ${displayStatus(state.judge.status)}`);
352
474
  }
353
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
+
354
483
  function isTerminal(state) {
355
484
  return ['judged', 'judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(state.status);
356
485
  }
@@ -359,6 +488,12 @@ function isFailureTerminal(state) {
359
488
  return ['judge_failed', 'batch_blocked', 'plan_failed', 'plan_empty', 'stopped'].includes(state.status);
360
489
  }
361
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
+
362
497
  function hasRecoverableUnknownTask(state) {
363
498
  return (state.tasks || []).some(task => task.status === 'unknown' && (task.exitCode === undefined || task.exitCode === 0));
364
499
  }
@@ -374,8 +509,8 @@ async function confirmFailureTerminal(runId, state, refreshRun, pollMs) {
374
509
  return { confirmed: true, state: confirmed };
375
510
  }
376
511
 
377
- async function watchRun(runId, { auto = false, pollMs = 3000 } = {}) {
378
- const { dispatchRun, refreshRun, startJudge } = await import('../src/orchestrator.js');
512
+ async function watchRun(runId, { auto = false, pollMs = 3000, quiet = false, maxRetries = 1 } = {}) {
513
+ const { dispatchRun, refreshRun, retryRun, startJudge } = await import('../src/orchestrator.js');
379
514
  let lastStatus = '';
380
515
  let judgeStarted = false;
381
516
  while (true) {
@@ -383,15 +518,26 @@ async function watchRun(runId, { auto = false, pollMs = 3000 } = {}) {
383
518
  if (!state) throw new Error(`run not found: ${runId}`);
384
519
  const line = statusLine(state);
385
520
  if (line !== lastStatus) {
386
- console.log(`[${new Date().toLocaleTimeString()}] ${line}`);
521
+ if (!quiet) console.log(`[${new Date().toLocaleTimeString()}] ${line}`);
387
522
  lastStatus = line;
388
523
  }
389
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
+
390
536
  if (auto && state.status === 'planned') {
391
- console.log('自动派发任务...');
537
+ if (!quiet) console.log('自动派发任务...');
392
538
  await dispatchRun(runId);
393
539
  } else if (auto && state.status === 'batches_completed' && state.judge?.status !== 'running' && !judgeStarted) {
394
- console.log('自动发起最终验收...');
540
+ if (!quiet) console.log('自动发起最终验收...');
395
541
  judgeStarted = true;
396
542
  await startJudge(runId);
397
543
  }
@@ -415,11 +561,15 @@ async function serve(args) {
415
561
  applyRuntimeEnv(args);
416
562
  const { startServer } = await import('../src/server.js');
417
563
  const instance = await startServer({ host: process.env.HOST, port: Number(process.env.PORT || 8787), log: false });
418
- console.log('Input Kanban started');
419
- console.log(`URL: ${instance.url}`);
420
- console.log(`Repo: ${instance.defaultRepo}`);
421
- console.log(`Runs: ${instance.runsDir}`);
422
- 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
+ }
423
573
  if (args.open) openBrowser(instance.url);
424
574
  const shutdown = () => { instance.stop().finally(() => process.exit(0)); };
425
575
  process.on('SIGINT', shutdown);
@@ -428,7 +578,7 @@ async function serve(args) {
428
578
 
429
579
  function detachedAutoArgs(runId, args) {
430
580
  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)];
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)];
432
582
  if (args.runsDir) values.push('--runs-dir', path.resolve(args.runsDir));
433
583
  if (args.codexBin) values.push('--codex-bin', args.codexBin);
434
584
  if (args.runner) values.push('--runner', args.runner);
@@ -452,16 +602,32 @@ async function latestRunId() {
452
602
  return runs[0].runId;
453
603
  }
454
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
+
455
618
  async function status(args) {
456
619
  applyRuntimeEnv(args);
457
620
  const runId = args.runId || await latestRunId();
621
+ const { refreshRun, summaryOfRun } = await import('../src/orchestrator.js');
458
622
  if (args.watch) {
459
- await watchRun(runId, { auto: false, pollMs: args.pollMs });
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) });
460
626
  return;
461
627
  }
462
- const { refreshRun } = await import('../src/orchestrator.js');
463
628
  const state = await refreshRun(runId);
464
629
  if (!state) throw new Error(`run not found: ${runId}`);
630
+ if (args.json) { printJson({ ok: true, command: 'status', run: summaryOfRun(state) }); return; }
465
631
  printRunStatus(state);
466
632
  }
467
633
 
@@ -469,9 +635,17 @@ async function readFinalResult(runId) {
469
635
  const { loadRun, readRunFile } = await import('../src/orchestrator.js');
470
636
  const state = await loadRun(runId);
471
637
  if (!state) throw new Error(`run not found: ${runId}`);
472
- try { return await readRunFile(runId, 'judge', 'verdict.json'); }
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
+ }
473
644
  catch {}
474
- try { return await readRunFile(runId, 'judge', 'last_message.md'); }
645
+ try {
646
+ const text = await readRunFile(runId, 'judge', 'last_message.md');
647
+ return { state, source: 'judge/last_message.md', text, parsed: null };
648
+ }
475
649
  catch {}
476
650
  throw new Error(`最终结果尚未生成:当前状态 ${displayStatus(state.status)}`);
477
651
  }
@@ -503,39 +677,56 @@ async function copyToClipboard(text) {
503
677
  async function result(args) {
504
678
  applyRuntimeEnv(args);
505
679
  const runId = args.runId || await latestRunId();
506
- const text = await readFinalResult(runId);
680
+ const { state, source, text, parsed } = await readFinalResult(runId);
681
+ const { summaryOfRun } = await import('../src/orchestrator.js');
507
682
  if (args.copy) {
508
683
  await copyToClipboard(text);
684
+ if (args.json) { printJson({ ok: true, command: 'result', run: summaryOfRun(state), source, copied: true }); return; }
509
685
  console.log(`已复制最终结果: ${runId}`);
510
686
  return;
511
687
  }
688
+ if (args.json) {
689
+ printJson({ ok: true, command: 'result', run: summaryOfRun(state), source, result: parsed, text: parsed ? null : text });
690
+ return;
691
+ }
512
692
  console.log(text);
513
693
  }
514
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
+
515
704
  async function stop(args) {
516
705
  applyRuntimeEnv(args);
517
706
  if (!args.runId) throw new Error('stop requires a runId');
518
- const { stopRun } = await import('../src/orchestrator.js');
707
+ const { stopRun, summaryOfRun } = await import('../src/orchestrator.js');
519
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; }
520
710
  console.log(`已停止任务批次: ${state.runId}`);
521
711
  }
522
712
 
523
713
  async function autoRun(args) {
524
714
  applyRuntimeEnv(args);
525
715
  if (!args.runId) throw new Error('auto requires a runId');
526
- const { loadRun, startPlanner } = await import('../src/orchestrator.js');
716
+ const { loadRun, startPlanner, summaryOfRun } = await import('../src/orchestrator.js');
527
717
  const state = await loadRun(args.runId);
528
718
  if (!state) throw new Error(`run not found: ${args.runId}`);
529
719
  if (state.status === 'created') await startPlanner(args.runId);
530
- const finalState = await watchRun(args.runId, { auto: true, pollMs: args.pollMs });
720
+ const finalState = await watchRun(args.runId, { auto: true, pollMs: args.pollMs, quiet: args.json, maxRetries: args.maxRetries });
531
721
  if (isFailureTerminal(finalState)) process.exitCode = 1;
722
+ if (args.json) { printJson({ ok: true, command: 'auto', run: summaryOfRun(finalState) }); return; }
532
723
  }
533
724
 
534
725
  async function submit(args) {
535
726
  if (args.detach && !args.auto) throw new Error('--detach requires auto mode; remove --no-auto');
536
727
  applyRuntimeEnv(args);
537
728
  const taskText = await readTaskText(args);
538
- const { createRun, startPlanner } = await import('../src/orchestrator.js');
729
+ const { createRun, startPlanner, summaryOfRun } = await import('../src/orchestrator.js');
539
730
  const state = await createRun({
540
731
  label: args.label,
541
732
  taskText,
@@ -543,46 +734,71 @@ async function submit(args) {
543
734
  maxParallel: args.maxParallel,
544
735
  workerSandbox: args.workerSandbox
545
736
  });
546
- console.log(`已创建任务批次: ${state.runId}`);
547
- console.log(`看板地址: ${webUrl(args, state.runId)}`);
548
- console.log(`终端查看: input-kanban status ${state.runId} --watch`);
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
+ }
549
742
  if (args.detach) {
550
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; }
551
745
  console.log(`后台执行中: supervisor pid ${pid}`);
552
746
  return;
553
747
  }
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}`);
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 });
559
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}`);
560
758
  }
561
759
 
562
760
  try {
563
- const { command, rest } = splitCommand(process.argv.slice(2));
761
+ const { command, rest, globals = {} } = splitCommand(process.argv.slice(2));
564
762
  if (command === 'serve') {
565
763
  const args = parseServeArgs(rest);
764
+ args.json = args.json || globals.json;
566
765
  if (args.help) { printHelp(); process.exit(0); }
567
766
  await serve(args);
767
+ } else if (command === 'version') {
768
+ printVersion();
568
769
  } else if (command === 'submit') {
569
770
  const args = parseSubmitArgs(rest);
771
+ args.json = args.json || globals.json;
570
772
  if (args.help) { printSubmitHelp(); process.exit(0); }
571
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);
572
779
  } else if (command === 'status') {
573
780
  const args = parseStatusArgs(rest);
781
+ args.json = args.json || globals.json;
574
782
  if (args.help) { printStatusHelp(); process.exit(0); }
575
783
  await status(args);
576
784
  } else if (command === 'result') {
577
785
  const args = parseResultArgs(rest);
786
+ args.json = args.json || globals.json;
578
787
  if (args.help) { printResultHelp(); process.exit(0); }
579
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);
580
794
  } else if (command === 'stop') {
581
795
  const args = parseStopArgs(rest);
796
+ args.json = args.json || globals.json;
582
797
  if (args.help) { printStopHelp(); process.exit(0); }
583
798
  await stop(args);
584
799
  } else if (command === 'auto') {
585
800
  const args = parseAutoArgs(rest);
801
+ args.json = args.json || globals.json;
586
802
  if (args.help) { printAutoHelp(); process.exit(0); }
587
803
  await autoRun(args);
588
804
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "input-kanban",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "input-kanban": "bin/input-kanban.js"
@@ -10,6 +10,7 @@
10
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
12
  "description": "A local Codex orchestration kanban dashboard",
13
+ "license": "MIT",
13
14
  "files": [
14
15
  "bin",
15
16
  "src",
@@ -18,7 +19,8 @@
18
19
  "README.en.md",
19
20
  "RELEASE_NOTES.md",
20
21
  "PROJECT_GUIDE.md",
21
- "ENVIRONMENT.md"
22
+ "ENVIRONMENT.md",
23
+ "LICENSE"
22
24
  ],
23
25
  "keywords": [
24
26
  "codex",