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.
- package/ENVIRONMENT.md +7 -4
- package/LICENSE +21 -0
- package/PROJECT_GUIDE.md +124 -12
- package/README.en.md +27 -17
- package/README.md +27 -17
- package/RELEASE_NOTES.md +38 -1
- package/bin/input-kanban.js +277 -58
- package/package.json +5 -3
- package/public/index.html +101 -20
- package/src/orchestrator.js +523 -201
- package/src/scheduler.js +40 -0
- package/src/server.js +13 -6
- package/src/utils.js +7 -1
package/bin/input-kanban.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 --
|
|
215
|
-
input-kanban submit --
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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)
|
|
557
|
-
|
|
558
|
-
|
|
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
|
}
|