@crewx/cli 0.9.0-rc.94 → 0.9.0-rc.95

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.
@@ -8,90 +8,35 @@
8
8
  * --provider <cli/xxx> Provider override
9
9
  * --model <name> Model override (e.g. claude-sonnet-5)
10
10
  * --metadata <json> Extra metadata JSON (double-quoted object). Propagated to events/hooks/tracing.
11
- * e.g. --metadata='{"workflow_id":"wf-1"}'
12
11
  * --verbose Debug output mode (default: raw agent response only)
13
12
  * --config/-c <path> Config file path override
14
13
  * --output-format <fmt> Output format (json|text|stream-json)
15
14
  * --effort <level> Model effort (high|medium|low)
16
- * -f/--prompt-file <path> Read task body from file (bypasses cmd.exe argv truncation)
17
- * --detach Re-spawn as a detached runner; print task-id and exit 0 immediately.
18
- * Ignored when CREWX_TRACE_ID is already set (recursive-spawn guard) or
19
- * on win32 (unsupported — exits with an error).
15
+ * -f/--prompt-file <path> Read task body from file
16
+ * --detach/--async Persist a receipt and launch a detached runner
17
+ * --wait Force synchronous execution
20
18
  *
21
19
  * Stdin support:
22
20
  * Pipe or redirect content into crewx x to supply the task body via stdin.
23
- * e.g. cat task.md | crewx x "@agent label"
24
21
  * Stdin is ignored when running in an interactive TTY.
25
22
  */
26
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.runDetachedMiddle = void 0;
27
25
  exports.handleExecute = handleExecute;
28
- const child_process_1 = require("child_process");
29
- const fs_1 = require("fs");
30
- const path_1 = require("path");
31
- const os_1 = require("os");
32
26
  const sdk_1 = require("@crewx/sdk");
27
+ const repository_1 = require("@crewx/sdk/repository");
33
28
  const parse_agent_message_1 = require("./parse-agent-message");
34
29
  const parse_common_flags_1 = require("./parse-common-flags");
35
30
  const resolve_prompt_1 = require("./resolve-prompt");
36
31
  const crewx_cli_1 = require("../bootstrap/crewx-cli");
37
32
  const inherited_trace_1 = require("../utils/inherited-trace");
38
33
  const write_output_1 = require("./write-output");
34
+ const detached_1 = require("./detached");
39
35
  const emit_trailer_1 = require("./emit-trailer");
40
- /**
41
- * Split `--detach` out of argv, respecting the `--` literal-args sentinel
42
- * (a `--detach` appearing after `--` is message text, not the flag).
43
- */
44
- function extractDetachFlag(args) {
45
- const rest = [];
46
- let detach = false;
47
- let escapeMode = false;
48
- for (const arg of args) {
49
- if (!escapeMode && arg === '--') {
50
- escapeMode = true;
51
- rest.push(arg);
52
- continue;
53
- }
54
- if (!escapeMode && arg === '--detach') {
55
- detach = true;
56
- continue;
57
- }
58
- rest.push(arg);
59
- }
60
- return { detach, rest };
61
- }
62
- /**
63
- * Double-detach: re-spawn this same CLI entry (minus --detach) as a detached
64
- * process so it survives the parent's exit. The task-id is pre-generated here
65
- * and injected as CREWX_TRACE_ID so the runner's own task row is created under
66
- * this id (see handleExecute's `selfTaskId` derivation below) — it doubles as
67
- * both this task's row id and the root of any further delegation it spawns.
68
- *
69
- * stdout contract (script-parseable): task-id only, on the first line.
70
- * Everything else goes to stderr.
71
- */
72
- function runDetached(filteredArgs) {
73
- const taskId = (0, sdk_1.generateId)('tsk');
74
- const logDir = (0, path_1.join)((0, os_1.homedir)(), '.crewx', 'logs');
75
- if (!(0, fs_1.existsSync)(logDir))
76
- (0, fs_1.mkdirSync)(logDir, { recursive: true });
77
- const logPath = (0, path_1.join)(logDir, `${taskId}.log`);
78
- const logFd = (0, fs_1.openSync)(logPath, 'a');
79
- const entry = process.argv[1];
80
- const child = (0, child_process_1.spawn)(process.execPath, [entry, 'x', ...filteredArgs], {
81
- detached: true,
82
- stdio: ['ignore', logFd, logFd],
83
- env: { ...process.env, CREWX_TRACE_ID: taskId },
84
- });
85
- (0, fs_1.closeSync)(logFd);
86
- child.on('error', (err) => {
87
- process.stderr.write(`Failed to spawn detached runner: ${err.message}\n`);
88
- });
89
- child.unref();
90
- console.log(taskId);
91
- process.stderr.write(`Detached task ${taskId} started (log: ${logPath}).\n`);
92
- process.stderr.write(`Use \`crewx result ${taskId} --wait=N\` to wait for completion.\n`);
93
- process.exit(0);
94
- }
36
+ // Exported for platform-focused tests; the middle hop remains an internal CLI
37
+ // contract and is not routed through the public command registry.
38
+ var detached_2 = require("./detached");
39
+ Object.defineProperty(exports, "runDetachedMiddle", { enumerable: true, get: function () { return detached_2.runDetachedMiddle; } });
95
40
  /**
96
41
  * Handle `crewx execute <agentRef> <message>` command.
97
42
  *
@@ -99,29 +44,27 @@ function runDetached(filteredArgs) {
99
44
  * --verbose: debug info written to stderr, response to stdout.
100
45
  */
101
46
  async function handleExecute(args, command = 'execute', emitState = (0, emit_trailer_1.createDelegationEmitState)()) {
102
- const { detach, rest: detachFilteredArgs } = extractDetachFlag(args);
103
- if (detach) {
104
- // Recursive-spawn guard: a CREWX_TRACE_ID already present means this
105
- // process is itself running inside a traced context (either the
106
- // respawned runner, or a delegated sub-call) — never chain a second
107
- // detach off of it. Silently fall through to normal (synchronous) execution.
108
- if (process.env['CREWX_TRACE_ID']) {
109
- process.stderr.write('Note: --detach ignored (already running inside a traced context; CREWX_TRACE_ID is set).\n');
110
- }
111
- else if (process.platform === 'win32') {
112
- console.error('Error: --detach is not supported on win32.');
113
- (0, emit_trailer_1.emitDelegationResult)({ command, isError: true, state: emitState });
114
- process.exit(1);
115
- return;
116
- }
117
- else {
118
- runDetached(detachFilteredArgs);
119
- return;
120
- }
47
+ // The private middle-hop marker is consumed before public flag parsing. An
48
+ // invalid direct use remains an unknown option and is rejected normally.
49
+ const middleHop = (0, detached_1.readDetachedMiddleHop)(args);
50
+ if (middleHop) {
51
+ await (0, detached_1.runDetachedMiddle)(command, middleHop);
52
+ return;
53
+ }
54
+ const receiptFlags = (0, detached_1.extractReceiptFlags)(args);
55
+ const receiptMode = (0, detached_1.resolveReceiptMode)({
56
+ detach: receiptFlags.detach,
57
+ wait: receiptFlags.wait,
58
+ detachedRunner: (0, inherited_trace_1.isDetachedRunner)(),
59
+ agentContext: Boolean(process.env['CREWX_AGENT_ID']),
60
+ });
61
+ if (receiptMode.receipt) {
62
+ await (0, detached_1.runDetached)(command, receiptFlags.rest, receiptMode.reason);
63
+ return;
121
64
  }
122
65
  let parsedFlags;
123
66
  try {
124
- parsedFlags = (0, parse_common_flags_1.parseCommonFlags)(detachFilteredArgs);
67
+ parsedFlags = (0, parse_common_flags_1.parseCommonFlags)(receiptFlags.rest);
125
68
  }
126
69
  catch (err) {
127
70
  (0, emit_trailer_1.emitDelegationResult)({ command, isError: true, state: emitState });
@@ -187,6 +130,7 @@ async function handleExecute(args, command = 'execute', emitState = (0, emit_tra
187
130
  }
188
131
  // file:// remote agent delegation is handled transparently inside Crewx.query/execute.
189
132
  if (verbose) {
133
+ process.stderr.write(`mode=sync reason=${receiptMode.reason}\n`);
190
134
  process.stderr.write(`📋 Task: ${finalMessage}\n`);
191
135
  process.stderr.write(`🤖 Agent: ${agentRef}\n`);
192
136
  if (thread)
@@ -200,7 +144,7 @@ async function handleExecute(args, command = 'execute', emitState = (0, emit_tra
200
144
  if (effort)
201
145
  process.stderr.write(`⚡ Effort: ${effort}\n`);
202
146
  if (overdrive)
203
- process.stderr.write(`🚀 Overdrive: ON\n`);
147
+ process.stderr.write('🚀 Overdrive: ON\n');
204
148
  process.stderr.write('─'.repeat(60) + '\n');
205
149
  }
206
150
  let parsedMetadata = {};
@@ -214,14 +158,21 @@ async function handleExecute(args, command = 'execute', emitState = (0, emit_tra
214
158
  process.exit(2);
215
159
  return;
216
160
  }
217
- // A trace with a rootTraceId but no parentTaskId means the id was pre-assigned
218
- // to *this* task itself (the detach runner's parent injects only CREWX_TRACE_ID,
219
- // never CREWX_PARENT_TASK_ID — see execute.ts's runDetached), not inherited from
220
- // an ancestor task in a delegation chain (which always carries both). It doubles
221
- // as this task's own row id so `crewx result <task-id>` can find it.
222
- const selfTaskId = inheritedTrace && !inheritedTrace.parentTaskId
223
- ? (inheritedTrace.rootTraceId || undefined)
224
- : undefined;
161
+ // Only the detached runner may adopt a pre-issued task row. Trace variables
162
+ // describe the caller chain and are never used as this task's identity.
163
+ const detachedRunner = (0, inherited_trace_1.isDetachedRunner)();
164
+ const assignedTaskId = (0, inherited_trace_1.readRunnerAssignment)();
165
+ let selfTaskId;
166
+ if (assignedTaskId !== undefined) {
167
+ const adoption = (0, detached_1.adoptAssignedTask)(new repository_1.TaskRepository(), assignedTaskId, detachedRunner);
168
+ if (adoption.kind === 'failed') {
169
+ (0, emit_trailer_1.emitDelegationResult)({ command, trace: inheritedTrace, isError: true, state: emitState });
170
+ process.exit(1);
171
+ return;
172
+ }
173
+ if (adoption.kind === 'adopted')
174
+ selfTaskId = adoption.taskId;
175
+ }
225
176
  let exitCode = 0;
226
177
  let result;
227
178
  try {
@@ -253,9 +204,8 @@ async function handleExecute(args, command = 'execute', emitState = (0, emit_tra
253
204
  process.stderr.write('─'.repeat(40) + '\n');
254
205
  }
255
206
  (0, write_output_1.writeResult)(out, result.data);
256
- if (verbose) {
207
+ if (verbose)
257
208
  process.stderr.write('\n✅ Execute completed successfully\n');
258
- }
259
209
  }
260
210
  }
261
211
  catch (err) {
@@ -1,9 +1,3 @@
1
- /**
2
- * crewx kill handler.
3
- * Kills a running task by ID or kills all running tasks.
4
- *
5
- * Usage:
6
- * crewx kill <task-id> Kill a specific running task
7
- * crewx kill --all Kill all running tasks
8
- */
1
+ /** Compatibility entry point for `crewx kill`; the implementation lives in stop.ts. */
2
+ export { handleStop } from './stop';
9
3
  export declare function handleKill(args: string[]): Promise<void>;
@@ -1,52 +1,11 @@
1
1
  "use strict";
2
- /**
3
- * crewx kill handler.
4
- * Kills a running task by ID or kills all running tasks.
5
- *
6
- * Usage:
7
- * crewx kill <task-id> Kill a specific running task
8
- * crewx kill --all Kill all running tasks
9
- */
10
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleStop = void 0;
11
4
  exports.handleKill = handleKill;
12
- const repository_1 = require("@crewx/sdk/repository");
5
+ /** Compatibility entry point for `crewx kill`; the implementation lives in stop.ts. */
6
+ var stop_1 = require("./stop");
7
+ Object.defineProperty(exports, "handleStop", { enumerable: true, get: function () { return stop_1.handleStop; } });
8
+ const stop_2 = require("./stop");
13
9
  async function handleKill(args) {
14
- const killAll = args.includes('--all');
15
- const taskId = args.find(a => !a.startsWith('--'));
16
- if (!taskId && !killAll) {
17
- console.error('Error: Please provide a task ID or use --all to kill all running tasks.');
18
- console.error('');
19
- console.error('Usage:');
20
- console.error(' crewx kill <task-id> Kill a running task by ID');
21
- console.error(' crewx kill --all Kill all running tasks');
22
- process.exit(1);
23
- return;
24
- }
25
- const repo = new repository_1.TaskRepository();
26
- if (killAll) {
27
- const running = repo.getRunningTasks();
28
- if (running.length === 0) {
29
- console.log('No running tasks found.');
30
- return;
31
- }
32
- console.log(`Found ${running.length} running task(s). Killing...`);
33
- for (const task of running) {
34
- const r = repo.killTask(task.id);
35
- if (r.killed) {
36
- console.log(`Killed task ${task.id}${r.pid ? ` (PID: ${r.pid})` : ''}`);
37
- }
38
- else {
39
- console.log(`Task ${task.id} could not be killed (may have already finished)`);
40
- }
41
- }
42
- return;
43
- }
44
- const r = repo.killTask(taskId);
45
- if (r.killed) {
46
- console.log(`Killed task ${taskId}${r.pid ? ` (PID: ${r.pid})` : ''}`);
47
- }
48
- else {
49
- console.error(`Error: Task not found or not running: ${taskId}`);
50
- process.exit(1);
51
- }
10
+ return (0, stop_2.handleStop)(args, 'kill');
52
11
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * crewx ps handler.
3
- * Lists currently running tasks in table format.
3
+ * Lists active tasks (pending or running) in table format.
4
4
  *
5
5
  * Usage:
6
- * crewx ps List all running tasks
6
+ * crewx ps List all pending and running tasks
7
7
  * crewx ps --json Output as JSON array
8
8
  */
9
9
  export declare function handlePs(args: string[]): Promise<void>;
@@ -1,15 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
3
  * crewx ps handler.
4
- * Lists currently running tasks in table format.
4
+ * Lists active tasks (pending or running) in table format.
5
5
  *
6
6
  * Usage:
7
- * crewx ps List all running tasks
7
+ * crewx ps List all pending and running tasks
8
8
  * crewx ps --json Output as JSON array
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.handlePs = handlePs;
12
12
  const repository_1 = require("@crewx/sdk/repository");
13
+ const cal_1 = require("./cal");
13
14
  function formatElapsed(ms) {
14
15
  const s = Math.floor(ms / 1000);
15
16
  if (s < 60)
@@ -65,6 +66,7 @@ function taskToRow(task, taskRepo) {
65
66
  return [
66
67
  task.id,
67
68
  task.agent_id ?? '—',
69
+ task.status,
68
70
  task.pid !== null && task.pid !== undefined ? String(task.pid) : '—',
69
71
  elapsed,
70
72
  task.mode ?? '—',
@@ -83,12 +85,11 @@ function renderTable(headers, rows) {
83
85
  }
84
86
  async function handlePs(args) {
85
87
  const repo = new repository_1.TaskRepository();
88
+ const reaped = repo.reapOrphanedTasks();
89
+ if (reaped > 0)
90
+ console.error(`Reaped ${reaped} orphaned task(s)`);
86
91
  repo.reapRunningWorkflowTasks();
87
- const tasks = repo.getRunningTasks();
88
- if (tasks.length === 0) {
89
- console.log('No running tasks.');
90
- return;
91
- }
92
+ const tasks = repo.getActiveTasks();
92
93
  if (args.includes('--json')) {
93
94
  const withLastActive = tasks.map((task) => {
94
95
  const lastActive = getLastActive(task, repo);
@@ -101,8 +102,16 @@ async function handlePs(args) {
101
102
  console.log(JSON.stringify(withLastActive, null, 2));
102
103
  return;
103
104
  }
104
- const headers = ['TASK ID', 'AGENT', 'PID', 'ELAPSED', 'MODE', 'LAST ACTIVE'];
105
+ if (tasks.length === 0) {
106
+ console.log('No active tasks.');
107
+ return;
108
+ }
109
+ const headers = ['TASK ID', 'AGENT', 'STATUS', 'PID', 'ELAPSED', 'MODE', 'LAST ACTIVE'];
105
110
  const rows = tasks.map((task) => taskToRow(task, repo));
106
111
  renderTable(headers, rows);
107
- console.log(`\n ${tasks.length} running task(s)`);
112
+ console.log(`\n ${tasks.length} active task(s)`);
113
+ (0, cal_1.writeCalFooter)({
114
+ commandId: 'crewx.ps',
115
+ params: { taskId: tasks[0].id },
116
+ });
108
117
  }
@@ -13,6 +13,8 @@
13
13
  * --output-format <fmt> Output format (json|text|stream-json)
14
14
  * --effort <level> Model effort (high|medium|low)
15
15
  * -f/--prompt-file <path> Read prompt body from file (bypasses cmd.exe argv truncation)
16
+ * --detach/--async Persist a receipt and launch a detached runner
17
+ * --wait Force synchronous execution
16
18
  *
17
19
  * Stdin support:
18
20
  * Pipe or redirect content into crewx q to supply the prompt body via stdin.
@@ -14,6 +14,8 @@
14
14
  * --output-format <fmt> Output format (json|text|stream-json)
15
15
  * --effort <level> Model effort (high|medium|low)
16
16
  * -f/--prompt-file <path> Read prompt body from file (bypasses cmd.exe argv truncation)
17
+ * --detach/--async Persist a receipt and launch a detached runner
18
+ * --wait Force synchronous execution
17
19
  *
18
20
  * Stdin support:
19
21
  * Pipe or redirect content into crewx q to supply the prompt body via stdin.
@@ -23,12 +25,14 @@
23
25
  Object.defineProperty(exports, "__esModule", { value: true });
24
26
  exports.handleQuery = handleQuery;
25
27
  const sdk_1 = require("@crewx/sdk");
28
+ const repository_1 = require("@crewx/sdk/repository");
26
29
  const parse_agent_message_1 = require("./parse-agent-message");
27
30
  const parse_common_flags_1 = require("./parse-common-flags");
28
31
  const resolve_prompt_1 = require("./resolve-prompt");
29
32
  const crewx_cli_1 = require("../bootstrap/crewx-cli");
30
33
  const inherited_trace_1 = require("../utils/inherited-trace");
31
34
  const write_output_1 = require("./write-output");
35
+ const detached_1 = require("./detached");
32
36
  const emit_trailer_1 = require("./emit-trailer");
33
37
  /**
34
38
  * Handle `crewx query <agentRef> <message>` command.
@@ -37,9 +41,27 @@ const emit_trailer_1 = require("./emit-trailer");
37
41
  * --verbose: debug info written to stderr, response to stdout.
38
42
  */
39
43
  async function handleQuery(args, command = 'query', emitState = (0, emit_trailer_1.createDelegationEmitState)()) {
44
+ // The private middle-hop marker is consumed before public flag parsing. An
45
+ // invalid direct use remains an unknown option and is rejected normally.
46
+ const middleHop = (0, detached_1.readDetachedMiddleHop)(args);
47
+ if (middleHop) {
48
+ await (0, detached_1.runDetachedMiddle)(command, middleHop);
49
+ return;
50
+ }
51
+ const receiptFlags = (0, detached_1.extractReceiptFlags)(args);
52
+ const receiptMode = (0, detached_1.resolveReceiptMode)({
53
+ detach: receiptFlags.detach,
54
+ wait: receiptFlags.wait,
55
+ detachedRunner: (0, inherited_trace_1.isDetachedRunner)(),
56
+ agentContext: Boolean(process.env['CREWX_AGENT_ID']),
57
+ });
58
+ if (receiptMode.receipt) {
59
+ await (0, detached_1.runDetached)(command, receiptFlags.rest, receiptMode.reason);
60
+ return;
61
+ }
40
62
  let parsedFlags;
41
63
  try {
42
- parsedFlags = (0, parse_common_flags_1.parseCommonFlags)(args);
64
+ parsedFlags = (0, parse_common_flags_1.parseCommonFlags)(receiptFlags.rest);
43
65
  }
44
66
  catch (err) {
45
67
  (0, emit_trailer_1.emitDelegationResult)({ command, isError: true, state: emitState });
@@ -105,6 +127,7 @@ async function handleQuery(args, command = 'query', emitState = (0, emit_trailer
105
127
  }
106
128
  // file:// remote agent delegation is handled transparently inside Crewx.query/execute.
107
129
  if (verbose) {
130
+ process.stderr.write(`mode=sync reason=${receiptMode.reason}\n`);
108
131
  process.stderr.write(`📋 Task: ${finalMessage}\n`);
109
132
  process.stderr.write(`🤖 Agent: ${agentRef}\n`);
110
133
  if (thread)
@@ -132,12 +155,21 @@ async function handleQuery(args, command = 'query', emitState = (0, emit_trailer
132
155
  process.exit(2);
133
156
  return;
134
157
  }
135
- // A trace with a rootTraceId but no parentTaskId means the id was pre-assigned
136
- // to *this* task itself. Keep query and execute on the same contract so the
137
- // reserved trace id remains the single source of truth for this task row.
138
- const selfTaskId = inheritedTrace && !inheritedTrace.parentTaskId
139
- ? (inheritedTrace.rootTraceId || undefined)
140
- : undefined;
158
+ // Only the detached runner may adopt a pre-issued task row. Trace variables
159
+ // describe the caller chain and are never used as this task's identity.
160
+ const detachedRunner = (0, inherited_trace_1.isDetachedRunner)();
161
+ const assignedTaskId = (0, inherited_trace_1.readRunnerAssignment)();
162
+ let selfTaskId;
163
+ if (assignedTaskId !== undefined) {
164
+ const adoption = (0, detached_1.adoptAssignedTask)(new repository_1.TaskRepository(), assignedTaskId, detachedRunner);
165
+ if (adoption.kind === 'failed') {
166
+ (0, emit_trailer_1.emitDelegationResult)({ command, trace: inheritedTrace, isError: true, state: emitState });
167
+ process.exit(1);
168
+ return;
169
+ }
170
+ if (adoption.kind === 'adopted')
171
+ selfTaskId = adoption.taskId;
172
+ }
141
173
  let exitCode = 0;
142
174
  let result;
143
175
  try {
@@ -13,7 +13,7 @@ exports.KNOWN_COMMANDS = new Set([
13
13
  'q', 'query',
14
14
  'x', 'execute',
15
15
  'agent',
16
- 'ps', 'kill', 'result', 'log', 'restart',
16
+ 'ps', 'stop', 'kill', 'result', 'log', 'restart',
17
17
  'doctor', 'init',
18
18
  'db',
19
19
  'help',
@@ -6,7 +6,7 @@
6
6
  * crewx result <task-id> Print raw result
7
7
  * crewx result <task-id> --json Print full task record as JSON
8
8
  * crewx result <task-id> --wait=N Poll (1s interval) up to N seconds for
9
- * the task to leave 'running'. Exit 124 on
9
+ * the task to leave 'pending'/'running'. Exit 124 on
10
10
  * timeout. --wait=0 behaves like no --wait
11
11
  * (single immediate check).
12
12
  * crewx result List recent tasks (latest 10)
@@ -7,7 +7,7 @@
7
7
  * crewx result <task-id> Print raw result
8
8
  * crewx result <task-id> --json Print full task record as JSON
9
9
  * crewx result <task-id> --wait=N Poll (1s interval) up to N seconds for
10
- * the task to leave 'running'. Exit 124 on
10
+ * the task to leave 'pending'/'running'. Exit 124 on
11
11
  * timeout. --wait=0 behaves like no --wait
12
12
  * (single immediate check).
13
13
  * crewx result List recent tasks (latest 10)
@@ -16,18 +16,46 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.handleResult = handleResult;
17
17
  const sdk_1 = require("@crewx/sdk");
18
18
  const repository_1 = require("@crewx/sdk/repository");
19
+ const cal_1 = require("./cal");
19
20
  const POLL_INTERVAL_MS = 1000;
21
+ const REAP_INTERVAL_MS = 10_000;
20
22
  function statusIcon(status) {
21
23
  switch (status) {
24
+ case 'pending': return '🕓';
22
25
  case 'running': return '⏳';
23
26
  case 'success': return '✅';
24
27
  case 'failed': return '❌';
28
+ case 'stopped': return '⏹';
29
+ case 'runner_lost': return '⚠️';
25
30
  default: return '❓';
26
31
  }
27
32
  }
33
+ function isActiveStatus(status) {
34
+ return status === 'pending' || status === 'running';
35
+ }
28
36
  function sleep(ms) {
29
37
  return new Promise((resolve) => setTimeout(resolve, ms));
30
38
  }
39
+ function resultCommandId(status) {
40
+ switch (status) {
41
+ case 'pending': return 'crewx.result-pending';
42
+ case 'running': return 'crewx.result-running';
43
+ case 'success': return 'crewx.result-done';
44
+ case 'failed':
45
+ case 'stopped':
46
+ case 'runner_lost':
47
+ return 'crewx.result-terminal-abnormal';
48
+ default: return 'crewx.result';
49
+ }
50
+ }
51
+ function writeResultCalFooter(task) {
52
+ const params = { taskId: task.id };
53
+ if (task.agent_id)
54
+ params.agent = task.agent_id;
55
+ if (task.thread_id)
56
+ params.thread = task.thread_id;
57
+ (0, cal_1.writeCalFooter)({ commandId: resultCommandId(task.status), params });
58
+ }
31
59
  /** Parses `--wait=N` (seconds). Returns undefined when the flag is absent. */
32
60
  function parseWaitSeconds(args) {
33
61
  const arg = args.find(a => a.startsWith('--wait='));
@@ -41,6 +69,9 @@ async function handleResult(args) {
41
69
  const waitSeconds = parseWaitSeconds(args);
42
70
  const taskId = args.find(a => !a.startsWith('--'));
43
71
  const repo = new repository_1.TaskRepository();
72
+ const initialReaped = repo.reapOrphanedTasks({ tickIntervalMs: REAP_INTERVAL_MS });
73
+ if (initialReaped > 0)
74
+ console.error(`Reaped ${initialReaped} orphaned task(s)`);
44
75
  if (!taskId) {
45
76
  const all = repo.getAllTasks();
46
77
  const recent = all.slice(0, 10);
@@ -61,6 +92,8 @@ async function handleResult(args) {
61
92
  console.log('');
62
93
  });
63
94
  console.log('Tip: Run `crewx result <task-id>` to see full output.');
95
+ if (!jsonMode)
96
+ writeResultCalFooter(recent[0]);
64
97
  return;
65
98
  }
66
99
  let task = repo.getTask(taskId);
@@ -69,15 +102,24 @@ async function handleResult(args) {
69
102
  process.exit(1);
70
103
  return;
71
104
  }
72
- if (waitSeconds !== undefined && waitSeconds > 0 && task.status === 'running') {
105
+ if (waitSeconds !== undefined && waitSeconds > 0 && isActiveStatus(task.status)) {
73
106
  const deadline = Date.now() + waitSeconds * 1000;
74
- while (task && task.status === 'running') {
107
+ let lastReapAt = Date.now();
108
+ while (task && isActiveStatus(task.status)) {
75
109
  if (Date.now() >= deadline) {
76
- console.error(`Task ${taskId} did not complete within --wait=${waitSeconds}s (status: running).`);
110
+ console.error(`Task ${taskId} is still ${task.status} (waited ${waitSeconds}s; status: ${task.status}). ` +
111
+ 'Run the same command again to keep waiting.');
112
+ writeResultCalFooter(task);
77
113
  process.exit(124);
78
114
  return;
79
115
  }
80
116
  await sleep(Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
117
+ if (Date.now() - lastReapAt >= REAP_INTERVAL_MS) {
118
+ const reaped = repo.reapOrphanedTasks({ tickIntervalMs: REAP_INTERVAL_MS });
119
+ if (reaped > 0)
120
+ console.error(`Reaped ${reaped} orphaned task(s)`);
121
+ lastReapAt = Date.now();
122
+ }
81
123
  task = repo.getTask(taskId);
82
124
  if (!task) {
83
125
  console.error(`Error: Task not found: ${taskId}`);
@@ -90,16 +132,41 @@ async function handleResult(args) {
90
132
  console.log(JSON.stringify(task, null, 2));
91
133
  return;
92
134
  }
135
+ if (task.status === 'pending') {
136
+ console.error(`🕓 Task ${taskId} is pending (waiting to start — runner has not adopted the task yet).`);
137
+ console.error(`Use \`crewx result ${taskId} --wait=N\` to wait for completion.`);
138
+ writeResultCalFooter(task);
139
+ process.exit(1);
140
+ return;
141
+ }
93
142
  if (task.status === 'running') {
94
143
  console.error(`Task ${taskId} is still running.`);
95
- console.error(`Use \`crewx kill ${taskId}\` to stop it.`);
144
+ console.error(`Use \`crewx result ${taskId} --wait=N\` to keep waiting or \`crewx log ${taskId}\` to inspect progress.`);
145
+ writeResultCalFooter(task);
146
+ process.exit(1);
147
+ return;
148
+ }
149
+ if (task.status === 'stopped') {
150
+ if (task.result !== null && task.result !== undefined) {
151
+ console.log(task.result);
152
+ }
153
+ console.error(`⏹ Task ${taskId} was stopped by user: ${task.error ?? 'no further details'}.`);
154
+ writeResultCalFooter(task);
155
+ process.exit(1);
156
+ return;
157
+ }
158
+ if (task.status === 'runner_lost') {
159
+ console.error(`⚠️ Task ${taskId} runner lost before completion: ${task.error ?? 'no further details'}.`);
160
+ writeResultCalFooter(task);
96
161
  process.exit(1);
97
162
  return;
98
163
  }
99
164
  if (task.status === 'failed') {
100
165
  console.error(`Task ${taskId} failed: ${task.error ?? 'unknown error'}`);
166
+ writeResultCalFooter(task);
101
167
  process.exit(1);
102
168
  return;
103
169
  }
104
170
  console.log(task.result ?? '');
171
+ writeResultCalFooter(task);
105
172
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Stop a running task through the SDK's cooperative-stop pipeline.
3
+ *
4
+ * `kill` is kept as a compatibility alias; both entry points use this same
5
+ * handler so server, CLI, and future adapters share one stop contract.
6
+ */
7
+ /** Handle `crewx stop` and the compatibility `crewx kill` alias. */
8
+ export declare function handleStop(args: string[], invokedAs?: 'stop' | 'kill'): Promise<void>;