@the-open-engine/zeroshot 6.31.3 → 6.32.1

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.
Files changed (47) hide show
  1. package/README.md +66 -98
  2. package/cli/index.js +251 -252
  3. package/cli/lib/setup-provider-readiness.js +86 -0
  4. package/cli/lib/setup-scanner-worker.js +120 -0
  5. package/cli/lib/setup-scanner.js +185 -0
  6. package/cli/lib/setup-wizard-input.js +146 -0
  7. package/cli/lib/setup-wizard-model.js +205 -0
  8. package/cli/lib/setup-wizard-plan-view.js +157 -0
  9. package/cli/lib/setup-wizard-scan-view.js +144 -0
  10. package/cli/lib/setup-wizard-terminal.js +237 -0
  11. package/cli/lib/setup-wizard-view.js +180 -0
  12. package/cli/lib/setup-wizard.js +281 -0
  13. package/cli/message-formatters-normal.js +14 -18
  14. package/cli/message-formatters-watch.js +53 -141
  15. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/adapters/codex.js +8 -2
  17. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  18. package/lib/agent-cli-provider/provider-registry.d.ts +4 -2
  19. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  20. package/lib/agent-cli-provider/provider-registry.js +13 -3
  21. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  22. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  23. package/lib/agent-cli-provider/single-agent-runtime.js +7 -4
  24. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  25. package/lib/agent-cli-provider/types.d.ts +2 -0
  26. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  27. package/lib/agent-cli-provider/types.js.map +1 -1
  28. package/lib/completion.js +102 -153
  29. package/lib/settings.js +10 -2
  30. package/lib/setup-apply.js +62 -55
  31. package/lib/setup-plan.js +32 -52
  32. package/lib/start-cluster.js +65 -25
  33. package/npm-shrinkwrap.json +2 -2
  34. package/package.json +3 -3
  35. package/scripts/postinstall.js +54 -0
  36. package/src/agent/agent-lifecycle.js +11 -1
  37. package/src/agent/agent-task-executor.js +25 -7
  38. package/src/agent/structured-output-error.js +42 -0
  39. package/src/agent-cli-provider/adapters/codex.ts +8 -12
  40. package/src/agent-cli-provider/provider-registry.ts +15 -3
  41. package/src/agent-cli-provider/single-agent-runtime.ts +11 -11
  42. package/src/agent-cli-provider/types.ts +2 -0
  43. package/src/preflight.js +27 -1
  44. package/src/status-footer.js +19 -12
  45. package/task-lib/commands/list.js +90 -78
  46. package/task-lib/commands/status.js +97 -40
  47. package/task-lib/effective-status.js +52 -0
@@ -52,6 +52,10 @@ const {
52
52
  validateCompletedResumeIdentity,
53
53
  } = require('./provider-session');
54
54
  const { extractClaudeVertexModelError } = require('./output-extraction');
55
+ const {
56
+ createStructuredOutputInvalidError,
57
+ isStructuredOutputInvalidError,
58
+ } = require('./structured-output-error');
55
59
  const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
56
60
  function runCommandWithTimeout(command, args, options = {}, callback = null) {
57
61
  const timeout = options.timeout ?? 30000;
@@ -1412,6 +1416,7 @@ async function evaluateStructuredSuccess({ agent, taskId, state, success, allowR
1412
1416
  return { success: true, error: null };
1413
1417
  } catch (error) {
1414
1418
  if (
1419
+ isStructuredOutputInvalidError(error) ||
1415
1420
  isNestedLifecycleError(error) ||
1416
1421
  error?.permanent === true ||
1417
1422
  error?.recoveryAbort === true
@@ -2858,7 +2863,10 @@ async function parseResultOutput(agent, output, { allowRecovery = true } = {}) {
2858
2863
  const schema = agent.config.jsonSchema;
2859
2864
  if (!schema) {
2860
2865
  if (parsed) return parsed;
2861
- throw new Error(`Agent ${agent.id} output missing required JSON block`);
2866
+ throw createStructuredOutputInvalidError(
2867
+ `Agent ${agent.id} output missing required JSON block`,
2868
+ 'missing_json'
2869
+ );
2862
2870
  }
2863
2871
 
2864
2872
  const { createStructuredOutputValidator, reformatOutput } = require('./output-reformatter');
@@ -2911,7 +2919,12 @@ async function parseResultOutput(agent, output, { allowRecovery = true } = {}) {
2911
2919
  throw new Error('Task execution failed - no output');
2912
2920
  }
2913
2921
  const recoveryDetail = recovery?.lastError ? ` Recovery exhausted: ${recovery.lastError}.` : '';
2914
- throw new Error(`Agent ${agent.id} output missing required JSON block.${recoveryDetail}`);
2922
+ throw createStructuredOutputInvalidError(
2923
+ `Agent ${agent.id} output missing required JSON block.${recoveryDetail}`,
2924
+ 'missing_json',
2925
+ directValidation,
2926
+ recovery
2927
+ );
2915
2928
  }
2916
2929
 
2917
2930
  if (!allowRecovery) {
@@ -2921,13 +2934,18 @@ async function parseResultOutput(agent, output, { allowRecovery = true } = {}) {
2921
2934
 
2922
2935
  const errorDetail = directValidation?.error || 'unknown schema error';
2923
2936
  const message = `Agent ${agent.id} output failed JSON schema validation: ${errorDetail}`;
2924
- if (agent.role === 'validator') {
2925
- throw new Error(
2926
- recovery?.status === 'exhausted'
2927
- ? `${message}. Recovery exhausted after ${recovery.attempts} attempts: ${recovery.lastError}`
2928
- : message
2937
+ const terminalStructuredRole = ['planner', 'conductor', 'validator'].includes(agent.role);
2938
+ if (recovery?.status === 'exhausted' && terminalStructuredRole) {
2939
+ throw createStructuredOutputInvalidError(
2940
+ `${message}. Recovery exhausted after ${recovery.attempts} attempts: ${recovery.lastError}`,
2941
+ 'schema_validation',
2942
+ directValidation,
2943
+ recovery
2929
2944
  );
2930
2945
  }
2946
+ if (agent.role === 'validator') {
2947
+ throw new Error(message);
2948
+ }
2931
2949
 
2932
2950
  console.warn(`⚠️ ${message}`);
2933
2951
  agent._publish({
@@ -0,0 +1,42 @@
1
+ const STRUCTURED_OUTPUT_INVALID_CODE = 'STRUCTURED_OUTPUT_INVALID';
2
+
3
+ function createStructuredOutputInvalidError(message, kind, validation = null, recovery = null) {
4
+ const error = new Error(message);
5
+ error.code = STRUCTURED_OUTPUT_INVALID_CODE;
6
+ error.details = {
7
+ kind,
8
+ validationError: validation?.error ?? null,
9
+ recoveryAttempts: recovery?.status === 'exhausted' ? recovery.attempts : 0,
10
+ recoveryError: recovery?.status === 'exhausted' ? recovery.lastError : null,
11
+ };
12
+ return error;
13
+ }
14
+
15
+ function isStructuredOutputInvalidError(error) {
16
+ return error?.code === STRUCTURED_OUTPUT_INVALID_CODE;
17
+ }
18
+
19
+ function buildStructuredOutputClusterFailure(agent, error) {
20
+ return {
21
+ topic: 'CLUSTER_FAILED',
22
+ receiver: 'broadcast',
23
+ content: {
24
+ text: `Cluster failed: structured output is invalid for ${agent.id} - ${error.message}`,
25
+ data: {
26
+ reason: 'structured_output_invalid',
27
+ agentId: agent.id,
28
+ role: agent.role,
29
+ code: error.code,
30
+ details: error.details ?? null,
31
+ error: error.message,
32
+ },
33
+ },
34
+ };
35
+ }
36
+
37
+ module.exports = {
38
+ STRUCTURED_OUTPUT_INVALID_CODE,
39
+ createStructuredOutputInvalidError,
40
+ isStructuredOutputInvalidError,
41
+ buildStructuredOutputClusterFailure,
42
+ };
@@ -255,7 +255,9 @@ function assertStructuredOutputRecoveryFeatures(options: BuildProviderCommandOpt
255
255
  const missing = [
256
256
  ['--sandbox', features.supportsSandbox],
257
257
  ['--ephemeral', features.supportsEphemeral],
258
- ['--ignore-user-config', features.supportsIgnoreUserConfig],
258
+ ...(options.trustIsolatedCodexProfile === true
259
+ ? []
260
+ : [['--ignore-user-config', features.supportsIgnoreUserConfig] as const]),
259
261
  ['--ignore-rules', features.supportsIgnoreRules],
260
262
  ['--strict-config', features.supportsStrictConfig],
261
263
  ['--config', features.supportsConfigOverride],
@@ -284,17 +286,11 @@ function buildStructuredOutputRecoveryCommand(
284
286
  const args = [...spec.args];
285
287
  const prompt = args.pop();
286
288
  if (prompt === undefined) throw new Error('Codex recovery command is missing its prompt');
287
- args.push(
288
- '--sandbox',
289
- 'read-only',
290
- '--ephemeral',
291
- '--ignore-user-config',
292
- '--ignore-rules',
293
- '--strict-config',
294
- '--config',
295
- 'web_search="disabled"',
296
- prompt
297
- );
289
+ args.push('--sandbox', 'read-only', '--ephemeral');
290
+ if (options.trustIsolatedCodexProfile !== true) {
291
+ args.push('--ignore-user-config');
292
+ }
293
+ args.push('--ignore-rules', '--strict-config', '--config', 'web_search="disabled"', prompt);
298
294
  return { ...spec, args };
299
295
  }
300
296
 
@@ -276,9 +276,9 @@ export const providerRegistry = [
276
276
  authInstructions: 'codex login',
277
277
  credentialPaths: ['~/.config/codex', '~/.codex'],
278
278
  credentialEnvKeys: codexAdapter.credentialEnvKeys,
279
- settingsFields: ['webSearch'],
280
- settingsDefaults: { webSearch: false },
281
- settingsValidator: (settings): string | null => validateWebSearchSettings('codex', settings),
279
+ settingsFields: ['webSearch', 'trustIsolatedRecoveryProfile'],
280
+ settingsDefaults: { webSearch: false, trustIsolatedRecoveryProfile: false },
281
+ settingsValidator: validateCodexSettings,
282
282
  capabilities: {
283
283
  ...STANDARD_CAPABILITIES,
284
284
  jsonSchema: true,
@@ -662,6 +662,18 @@ export const providerRegistry = [
662
662
  },
663
663
  ] as const satisfies readonly ProviderRegistryEntry[];
664
664
 
665
+ function validateCodexSettings(settings: Record<string, unknown>): string | null {
666
+ const webSearchError = validateWebSearchSettings('codex', settings);
667
+ if (webSearchError) return webSearchError;
668
+ if (
669
+ settings.trustIsolatedRecoveryProfile === undefined ||
670
+ typeof settings.trustIsolatedRecoveryProfile === 'boolean'
671
+ ) {
672
+ return null;
673
+ }
674
+ return 'providerSettings.codex.trustIsolatedRecoveryProfile must be a boolean';
675
+ }
676
+
665
677
  function validateWebSearchSettings(
666
678
  provider: 'codex' | 'opencode',
667
679
  settings: Record<string, unknown>
@@ -54,16 +54,12 @@ import type {
54
54
 
55
55
  type UnknownFunction = (...args: readonly unknown[]) => unknown;
56
56
 
57
- interface CommandParts {
58
- readonly command: string;
59
- readonly args: readonly string[];
60
- }
61
-
62
57
  interface RuntimeProviderSettings {
63
58
  readonly defaultLevel?: ModelLevel;
64
59
  readonly levelOverrides: LevelOverrides;
65
60
  readonly gateway?: GatewayBuildOptions;
66
61
  readonly webSearch?: boolean;
62
+ readonly trustIsolatedRecoveryProfile?: boolean;
67
63
  }
68
64
 
69
65
  interface RuntimeCommandContext {
@@ -428,7 +424,6 @@ function ompExecutionContext(
428
424
  throw new Error('options.executionContext must be "host", "detached", "docker", or "benchmark".');
429
425
  }
430
426
 
431
-
432
427
  function ompSdkOutputContract(
433
428
  options: BuildProviderCommandOptions
434
429
  ):
@@ -664,7 +659,7 @@ export function probeRuntimeProviderCli(
664
659
  const requested = registryEntry.settingsFields.includes('webSearch')
665
660
  ? runtimeProviderSettings(settings, adapter.id, process.cwd()).webSearch === true
666
661
  : false;
667
- const helpCommand = runtimeHelpCommand(adapter.id);
662
+ const helpCommand = resolveProviderCommand(adapter.id);
668
663
  const commandAvailable =
669
664
  evidence === undefined
670
665
  ? booleanResult(commandExistsFn(helpCommand.command))
@@ -735,6 +730,10 @@ function buildRuntimeOptions(
735
730
  cliFeatures: runtime.cliFeatures,
736
731
  };
737
732
  const resolved = { ...baseResolved };
733
+ delete resolved.trustIsolatedCodexProfile;
734
+ if (baseOptions.structuredOutputRecovery && adapter.id === 'codex') {
735
+ resolved.trustIsolatedCodexProfile = providerSettings.trustIsolatedRecoveryProfile === true;
736
+ }
738
737
  if (baseOptions.structuredOutputRecovery) {
739
738
  delete resolved.resumeSessionId;
740
739
  delete resolved.continueSession;
@@ -870,6 +869,10 @@ function runtimeProviderSettings(
870
869
  providerSettings.webSearch,
871
870
  `settings.providerSettings.${provider}.webSearch`
872
871
  );
872
+ const trustIsolatedRecoveryProfile = optionalBoolean(
873
+ providerSettings.trustIsolatedRecoveryProfile,
874
+ `settings.providerSettings.${provider}.trustIsolatedRecoveryProfile`
875
+ );
873
876
  const gateway =
874
877
  provider === 'gateway'
875
878
  ? normalizeGatewayBuildOptions(providerSettings, 'settings.providerSettings.gateway', cwd)
@@ -878,14 +881,11 @@ function runtimeProviderSettings(
878
881
  levelOverrides,
879
882
  ...(gateway === undefined ? {} : { gateway }),
880
883
  ...(webSearch === undefined ? {} : { webSearch }),
884
+ ...(trustIsolatedRecoveryProfile === undefined ? {} : { trustIsolatedRecoveryProfile }),
881
885
  };
882
886
  return defaultLevel === undefined ? base : { ...base, defaultLevel };
883
887
  }
884
888
 
885
- function runtimeHelpCommand(provider: ProviderId): CommandParts {
886
- return resolveProviderCommand(provider);
887
- }
888
-
889
889
  function probeGatewayProvider(
890
890
  adapter: ProviderAdapter,
891
891
  runtimeSettings?: Record<string, unknown>
@@ -468,6 +468,8 @@ export interface BuildProviderCommandOptions {
468
468
  readonly mcpConfig?: readonly string[];
469
469
  /** Internal profile for provider-neutral structured-output correction turns. */
470
470
  readonly structuredOutputRecovery?: boolean;
471
+ /** Preserve a caller-isolated CODEX_HOME profile during Codex recovery. */
472
+ readonly trustIsolatedCodexProfile?: boolean;
471
473
  }
472
474
 
473
475
  export interface TextEvent {
package/src/preflight.js CHANGED
@@ -468,6 +468,31 @@ function validateProviderIsolationCapabilities(providerName, options) {
468
468
  return errors;
469
469
  }
470
470
 
471
+ function validateProviderExecutionSettings(providerName, settings, options) {
472
+ const metadata = getProviderMetadata(providerName);
473
+ if (!metadata.settingsValidator) return [];
474
+
475
+ const providerSettings = settings.providerSettings?.[providerName];
476
+ if (
477
+ !providerSettings ||
478
+ typeof providerSettings !== 'object' ||
479
+ Array.isArray(providerSettings)
480
+ ) {
481
+ return [];
482
+ }
483
+
484
+ const executionContext = options.requireDocker ? 'docker' : 'detached';
485
+ const error = metadata.settingsValidator(providerSettings, { executionContext });
486
+ if (!error) return [];
487
+
488
+ return [
489
+ formatError(`${metadata.displayName} configuration cannot run cluster agents`, error, [
490
+ metadata.authInstructions,
491
+ 'Or select a different provider with: zeroshot providers set-default <provider>',
492
+ ]),
493
+ ];
494
+ }
495
+
471
496
  function validateProvider(providerName, options) {
472
497
  let metadata;
473
498
  try {
@@ -623,7 +648,7 @@ async function runPreflight(options = {}) {
623
648
  const errors = [];
624
649
  const warnings = [];
625
650
 
626
- const settings = loadSettings();
651
+ const settings = options.settings || loadSettings();
627
652
 
628
653
  if (process.platform === 'win32') {
629
654
  return {
@@ -648,6 +673,7 @@ async function runPreflight(options = {}) {
648
673
 
649
674
  const providerResult = validateProvider(providerName, options);
650
675
  errors.push(...providerResult.errors);
676
+ errors.push(...validateProviderExecutionSettings(providerName, settings, options));
651
677
  warnings.push(...providerResult.warnings);
652
678
 
653
679
  // 4. Check issue provider CLI (if required)
@@ -151,21 +151,29 @@ class StatusFooter {
151
151
  }
152
152
 
153
153
  /**
154
- * Print text to stdout, coordinating with the render cycle.
155
- * When a render is in progress, queues output to prevent cursor corruption.
156
- * When no render is active, writes immediately.
157
- *
158
- * MUST be used instead of console.log() when status footer is active.
159
- * @param {string} text - Text to print (newline will be added)
154
+ * Print one logical line while coordinating with footer rendering.
155
+ * The caller owns line normalization; this method appends one newline.
156
+ * @param {string} text
160
157
  */
161
158
  print(text) {
159
+ this._queueOrWrite(`${text}\n`);
160
+ }
161
+
162
+ /**
163
+ * Write an exact streaming chunk while coordinating with footer rendering.
164
+ * @param {string} text
165
+ */
166
+ write(text) {
167
+ this._queueOrWrite(String(text));
168
+ }
169
+
170
+ /** @private */
171
+ _queueOrWrite(text) {
162
172
  if (this.isRendering) {
163
- // Queue for later - render() will flush after restoring cursor
164
173
  this.printQueue.push(text);
165
- } else {
166
- // Write immediately - no render in progress
167
- process.stdout.write(text + '\n');
174
+ return;
168
175
  }
176
+ process.stdout.write(text);
169
177
  }
170
178
 
171
179
  /**
@@ -176,8 +184,7 @@ class StatusFooter {
176
184
  _flushPrintQueue() {
177
185
  if (this.printQueue.length === 0) return;
178
186
 
179
- // Write all queued output
180
- const output = this.printQueue.map((text) => text + '\n').join('');
187
+ const output = this.printQueue.join('');
181
188
  this.printQueue = [];
182
189
  process.stdout.write(output);
183
190
  }
@@ -1,97 +1,109 @@
1
1
  import chalk from 'chalk';
2
+ import { resolveEffectiveTaskStatus } from '../effective-status.js';
2
3
  import { loadTasks } from '../store.js';
3
- import { isProcessRunning } from '../runner.js';
4
4
 
5
- export function listTasks(options = {}) {
6
- const tasks = loadTasks();
7
- const taskList = Object.values(tasks);
5
+ const DEFAULT_LIMIT = 20;
6
+
7
+ function selectTasks(options = {}, deps = {}) {
8
+ const readTasks = deps.loadTasks || loadTasks;
9
+ const resolveStatus = deps.resolveEffectiveTaskStatus || resolveEffectiveTaskStatus;
10
+ const allTasks = Object.values(readTasks());
11
+ const selected = allTasks
12
+ .map((task) => ({ task, effectiveStatus: resolveStatus(task) }))
13
+ .sort((left, right) => new Date(left.task.createdAt) - new Date(right.task.createdAt))
14
+ .filter(({ effectiveStatus }) => !options.status || effectiveStatus.status === options.status)
15
+ .slice(0, options.limit || DEFAULT_LIMIT);
16
+ return { total: allTasks.length, selected };
17
+ }
18
+
19
+ function projectTask({ task, effectiveStatus }) {
20
+ return {
21
+ id: task.id,
22
+ status: effectiveStatus.status,
23
+ statusReason: effectiveStatus.reason,
24
+ cwd: task.cwd,
25
+ provider: task.provider || null,
26
+ model: task.model || null,
27
+ createdAt: task.createdAt,
28
+ updatedAt: task.updatedAt,
29
+ exitCode: task.exitCode ?? null,
30
+ error: task.error || null,
31
+ attachable: task.attachable === true,
32
+ };
33
+ }
34
+
35
+ export function getTasksData(options = {}, deps = {}) {
36
+ return selectTasks(options, deps).selected.map(projectTask);
37
+ }
8
38
 
9
- if (taskList.length === 0) {
39
+ export function listTasks(options = {}, deps = {}) {
40
+ const { selected, total } = selectTasks(options, deps);
41
+
42
+ if (total === 0) {
10
43
  console.log(chalk.dim('No tasks found.'));
11
44
  return;
12
45
  }
13
46
 
14
- // Sort by creation date, oldest first (chronological)
15
- taskList.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
16
-
17
- // Filter by status if specified
18
- let filtered = taskList;
19
- if (options.status) {
20
- filtered = taskList.filter((t) => t.status === options.status);
47
+ if (options.verbose) {
48
+ printVerboseTasks(selected, total);
49
+ } else {
50
+ printTaskTable(selected, total);
21
51
  }
52
+ }
22
53
 
23
- // Limit results
24
- const limit = options.limit || 20;
25
- filtered = filtered.slice(0, limit);
26
-
27
- // Table format (default) or verbose format
28
- if (options.verbose) {
29
- // Verbose format (old behavior)
30
- console.log(chalk.bold(`\nTasks (${filtered.length}/${taskList.length})\n`));
31
-
32
- for (const task of filtered) {
33
- // Verify running status
34
- let status = task.status;
35
- if (status === 'running' && !isProcessRunning(task.pid)) {
36
- status = 'stale';
37
- }
38
-
39
- const statusColor =
40
- {
41
- running: chalk.green,
42
- completed: chalk.green,
43
- failed: chalk.red,
44
- stale: chalk.yellow,
45
- }[status] || chalk.dim;
46
-
47
- const age = getAge(task.createdAt);
48
- const timestamp = new Date(task.createdAt).toLocaleString();
49
-
50
- console.log(
51
- `${statusColor('●')} ${chalk.cyan(task.id)} ${statusColor(`[${status}]`)} ${chalk.dim(age + ' • ' + timestamp)}`
52
- );
53
- console.log(` ${chalk.dim('CWD:')} ${task.cwd}`);
54
- console.log(` ${chalk.dim('Prompt:')} ${task.prompt}`);
55
- if (task.pid && status === 'running') {
56
- console.log(` ${chalk.dim('PID:')} ${task.pid}`);
57
- }
58
- if (task.error) {
59
- console.log(` ${chalk.red('Error:')} ${task.error}`);
60
- }
61
- console.log();
54
+ function printVerboseTasks(selected, total) {
55
+ console.log(chalk.bold(`\nTasks (${selected.length}/${total})\n`));
56
+
57
+ for (const { task, effectiveStatus } of selected) {
58
+ const statusColor = colorForStatus(effectiveStatus.status);
59
+ const age = getAge(task.createdAt);
60
+ const timestamp = new Date(task.createdAt).toLocaleString();
61
+
62
+ const heading = `${statusColor('●')} ${chalk.cyan(task.id)}`;
63
+ const status = statusColor(`[${effectiveStatus.status}]`);
64
+ const timing = chalk.dim(age + ' • ' + timestamp);
65
+ console.log(`${heading} ${status} ${timing}`);
66
+ console.log(` ${chalk.dim('CWD:')} ${task.cwd}`);
67
+ console.log(` ${chalk.dim('Prompt:')} ${task.prompt}`);
68
+ if (task.pid && effectiveStatus.status === 'running') {
69
+ console.log(` ${chalk.dim('PID:')} ${task.pid}`);
62
70
  }
63
- } else {
64
- // Table format (clean, default)
65
- console.log(chalk.bold(`\n=== Tasks (${filtered.length}/${taskList.length}) ===`));
66
- console.log(`${'ID'.padEnd(25)} ${'Status'.padEnd(12)} ${'Age'.padEnd(10)} CWD`);
67
- console.log('-'.repeat(100));
68
-
69
- for (const task of filtered) {
70
- // Verify running status
71
- let status = task.status;
72
- if (status === 'running' && !isProcessRunning(task.pid)) {
73
- status = 'stale';
74
- }
75
-
76
- const statusColor =
77
- {
78
- running: chalk.green,
79
- completed: chalk.green,
80
- failed: chalk.red,
81
- stale: chalk.yellow,
82
- }[status] || chalk.dim;
83
-
84
- const age = getAge(task.createdAt);
85
- const cwd = task.cwd.replace(process.env.HOME, '~');
86
-
87
- console.log(
88
- `${chalk.cyan(task.id.padEnd(25))} ${statusColor(status.padEnd(12))} ${chalk.dim(age.padEnd(10))} ${chalk.dim(cwd)}`
89
- );
71
+ if (task.error) {
72
+ console.log(` ${chalk.red('Error:')} ${task.error}`);
90
73
  }
91
74
  console.log();
92
75
  }
93
76
  }
94
77
 
78
+ function printTaskTable(selected, total) {
79
+ console.log(chalk.bold(`\n=== Tasks (${selected.length}/${total}) ===`));
80
+ console.log(`${'ID'.padEnd(25)} ${'Status'.padEnd(12)} ${'Age'.padEnd(10)} CWD`);
81
+ console.log('-'.repeat(100));
82
+
83
+ for (const { task, effectiveStatus } of selected) {
84
+ const statusColor = colorForStatus(effectiveStatus.status);
85
+ const age = getAge(task.createdAt);
86
+ const cwd = process.env.HOME ? task.cwd.replace(process.env.HOME, '~') : task.cwd;
87
+
88
+ const id = chalk.cyan(task.id.padEnd(25));
89
+ const status = statusColor(effectiveStatus.status.padEnd(12));
90
+ const timing = chalk.dim(age.padEnd(10));
91
+ console.log(`${id} ${status} ${timing} ${chalk.dim(cwd)}`);
92
+ }
93
+ console.log();
94
+ }
95
+
96
+ function colorForStatus(status) {
97
+ return (
98
+ {
99
+ running: chalk.green,
100
+ completed: chalk.green,
101
+ failed: chalk.red,
102
+ stale: chalk.yellow,
103
+ }[status] || chalk.dim
104
+ );
105
+ }
106
+
95
107
  function getAge(dateStr) {
96
108
  const diff = Date.now() - new Date(dateStr).getTime();
97
109
  const mins = Math.floor(diff / 60000);