input-kanban 0.0.8 → 0.0.10

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