agent-rev 0.1.2 → 0.2.3

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.
@@ -26,4 +26,4 @@ export declare function runRepl(resumeSession?: Session): Promise<void>;
26
26
  * NO spawnea CLIs externos. Usa la conexión OAuth de qwen directamente.
27
27
  * Prompt estricto: cada rol solo hace lo que debe, sin extras.
28
28
  */
29
- export declare function runRole(role: string, arg: string): Promise<void>;
29
+ export declare function runRole(role: string, arg: string, model?: string): Promise<void>;
@@ -9,8 +9,8 @@ import { CLI_REGISTRY } from '../types.js';
9
9
  import { writeJson, ensureDir, readJson, listDir, fileExists } from '../utils/fs.js';
10
10
  import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig } from '../utils/config.js';
11
11
  import { log } from '../utils/logger.js';
12
- import { AgentEngine } from '../core/engine.js';
13
- import { qwenAuthStatus, QWEN_AGENT_HOME } from '../utils/qwen-auth.js';
12
+ import { AgentEngine, ExitError } from '../core/engine.js';
13
+ import { qwenAuthStatus, QWEN_AGENT_HOME, fetchQwenModels } from '../utils/qwen-auth.js';
14
14
  import { renderWelcomePanel, renderHelpHint, renderSectionBox, renderMultiSectionBox } from '../ui/theme.js';
15
15
  import { FixedInput } from '../ui/input.js';
16
16
  import { newSession, saveSession } from '../utils/sessions.js';
@@ -219,6 +219,25 @@ function detectModels(cliName) {
219
219
  return out.trim().split('\n').filter(Boolean);
220
220
  }
221
221
  case 'aider': return ['default'];
222
+ case 'agent-orch':
223
+ case 'agent-impl':
224
+ case 'agent-rev':
225
+ case 'agent-explorer': {
226
+ try {
227
+ const out = execSync(`${cliName} --list-models 2>/dev/null`, { encoding: 'utf-8' });
228
+ const models = out.trim().split('\n').filter(Boolean);
229
+ if (models.length)
230
+ return models;
231
+ }
232
+ catch { }
233
+ // fallback: list from opencode since that's typically the underlying CLI
234
+ try {
235
+ const out = execSync('opencode models 2>/dev/null', { encoding: 'utf-8' });
236
+ return out.trim().split('\n').filter(Boolean);
237
+ }
238
+ catch { }
239
+ return ['configured'];
240
+ }
222
241
  default: return ['default'];
223
242
  }
224
243
  }
@@ -634,29 +653,51 @@ async function cmdStatus(fi) {
634
653
  fi.println(renderMultiSectionBox(sections));
635
654
  fi.println('');
636
655
  }
637
- async function cmdModels(provider, fi) {
638
- const installed = detectInstalledClis();
639
- if (provider) {
640
- const info = CLI_REGISTRY[provider];
641
- if (!info) {
642
- fi.println(chalk.red(` Unknown: ${provider}`));
643
- return;
644
- }
645
- const models = detectModels(provider);
646
- fi.println(chalk.bold(`\n ${info.name} models:`));
647
- for (const m of models)
648
- fi.println(` ${m}`);
656
+ async function cmdModels(roleArg, fi, rl) {
657
+ const auth = await loadAuth();
658
+ const authedProviders = auth.entries.map(e => e.provider);
659
+ if (authedProviders.length === 0) {
660
+ fi.println(chalk.yellow(' No authenticated providers. Run /login first.'));
661
+ return;
662
+ }
663
+ // Fetch models from the OAuth provider (coordinator's native connection)
664
+ fi.println(chalk.dim(' Fetching models from your OAuth account...'));
665
+ const prov = authedProviders[0];
666
+ let models;
667
+ if (prov === 'qwen') {
668
+ models = await fetchQwenModels();
649
669
  }
650
670
  else {
651
- for (const c of installed) {
652
- const models = detectModels(c.name);
653
- fi.println(chalk.bold(`\n ${c.name} (${c.path}):`));
654
- for (const m of models.slice(0, 10))
655
- fi.println(` ${m}`);
656
- if (models.length > 10)
657
- fi.println(chalk.dim(` ... and ${models.length - 10} more`));
658
- }
671
+ models = detectModels(prov);
672
+ }
673
+ if (!models.length) {
674
+ fi.println(chalk.red(' No models available.'));
675
+ return;
676
+ }
677
+ // Show numbered list
678
+ const resume = fi.suspend();
679
+ const cliConfig = await loadCliConfig();
680
+ const currentModel = cliConfig.coordinatorModel ?? '(auto)';
681
+ console.log(chalk.bold.cyan(`\n Coordinator model [current: ${currentModel}]\n`));
682
+ console.log(chalk.dim(' These are your OAuth account models. CLIs are configured separately with /setup.\n'));
683
+ models.forEach((m, i) => console.log(chalk.dim(` ${String(i + 1).padStart(2)}. ${m}`)));
684
+ const modelInput = await ask(rl, chalk.bold('\n Model # or name (Enter to keep current): '));
685
+ resume();
686
+ if (!modelInput.trim()) {
687
+ fi.println(chalk.dim(' → no change'));
688
+ return;
659
689
  }
690
+ const num = parseInt(modelInput.trim());
691
+ const selectedModel = (!isNaN(num) && num >= 1 && num <= models.length)
692
+ ? models[num - 1]
693
+ : modelInput.trim();
694
+ // Save as coordinator model only
695
+ cliConfig.coordinatorModel = selectedModel;
696
+ await saveCliConfig(cliConfig);
697
+ // Update gCoordinatorCmd in memory
698
+ gCoordinatorCmd = buildCmd(prov, selectedModel);
699
+ fi.println(chalk.green(`\n ✓ Coordinator model → ${selectedModel}`));
700
+ fi.println(chalk.dim(` CMD: ${gCoordinatorCmd}`));
660
701
  fi.println('');
661
702
  }
662
703
  async function cmdAuthStatus(fi) {
@@ -829,7 +870,8 @@ export async function initCoordinator() {
829
870
  }
830
871
  // Build coordinator command using the ACTIVE CLI (not orchestrator)
831
872
  // The coordinator converses naturally using whatever CLI is active (qwen, claude, etc.)
832
- const activeModel = detectModels(activeCli.name)[0] || 'default';
873
+ const cliCfg = await loadCliConfig();
874
+ const activeModel = cliCfg.coordinatorModel || detectModels(activeCli.name)[0] || 'default';
833
875
  gCoordinatorCmd = buildCmd(activeCli.name, activeModel);
834
876
  return { coordinatorCmd: gCoordinatorCmd, activeCli, installed, rl };
835
877
  }
@@ -928,31 +970,133 @@ export async function runRepl(resumeSession) {
928
970
  resume();
929
971
  }
930
972
  };
973
+ // Show coordinator header before the REPL loop (once, at startup)
974
+ try {
975
+ const dir = process.cwd();
976
+ const config = await loadProjectConfig(dir);
977
+ if (config.roles?.orchestrator?.cli) {
978
+ fi.println('');
979
+ fi.println(chalk.bold.cyan('═'.repeat(46)));
980
+ fi.println(chalk.bold.cyan(` COORDINADOR — ${config.project}`));
981
+ fi.println(chalk.bold.cyan('═'.repeat(46)));
982
+ fi.println(chalk.blue(` → Rol: COORDINADOR (solo orquesta, nunca ejecuta)`));
983
+ fi.println(chalk.dim(' ' + '─'.repeat(42)));
984
+ fi.println(chalk.bold(` ── FASE 0 — Clarificacion ──`));
985
+ fi.println(chalk.dim(` → El coordinador va a conversar con vos para entender la tarea.`));
986
+ fi.println(chalk.dim(` → Cuando el coordinador tenga claro el objetivo, te va a pedir confirmación para lanzar el plan.`));
987
+ fi.println(chalk.dim(' ' + '─'.repeat(42)));
988
+ }
989
+ }
990
+ catch { }
991
+ // Command handler — used by the main loop AND by the engine during mid-conversation
992
+ const handleCmd = async (trimmed) => {
993
+ const parts = trimmed.slice(1).split(/\s+/);
994
+ const cmd = parts[0].toLowerCase();
995
+ const args = parts.slice(1);
996
+ switch (cmd) {
997
+ case 'setup': {
998
+ const sub = args[0]?.toLowerCase();
999
+ const roleMap = {
1000
+ orch: 'orchestrator', impl: 'implementor', rev: 'reviewer',
1001
+ explorer: 'explorer', proposer: 'proposer', critic: 'critic',
1002
+ };
1003
+ if (sub && roleMap[sub]) {
1004
+ await withRl((rl) => cmdConfigOneRole(rl, roleMap[sub]));
1005
+ }
1006
+ else {
1007
+ await withRl(cmdSetup);
1008
+ }
1009
+ break;
1010
+ }
1011
+ case 'config-multi':
1012
+ await withRl(cmdConfigMulti);
1013
+ break;
1014
+ case 'status':
1015
+ await cmdStatus(fi);
1016
+ break;
1017
+ case 'run': {
1018
+ const dir = process.cwd();
1019
+ const config = await loadProjectConfig(dir);
1020
+ if (args[0] === 'orch' || args[0] === 'orchestrator') {
1021
+ const task = args.slice(1).join(' ');
1022
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1023
+ const result = await engine.runOrchestrator(task);
1024
+ fi.println(chalk.green(` Task ID: ${result.taskId}`));
1025
+ }
1026
+ else if (args[0] === 'impl' || args[0] === 'implementor') {
1027
+ const taskId = args[1];
1028
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1029
+ const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1030
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1031
+ await engine.runImplementor(taskId, plan);
1032
+ }
1033
+ else if (args[0] === 'rev' || args[0] === 'reviewer') {
1034
+ const taskId = args[1];
1035
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1036
+ const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1037
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1038
+ const progress = await readJson(path.join(taskDir, 'progress.json'));
1039
+ await engine.runReviewer(taskId, plan, progress);
1040
+ }
1041
+ else {
1042
+ const task = args.join(' ');
1043
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1044
+ await engine.runFullCycle(task);
1045
+ }
1046
+ break;
1047
+ }
1048
+ case 'models':
1049
+ case 'model':
1050
+ await withRl((rl) => cmdModels(args[0], fi, rl));
1051
+ break;
1052
+ case 'login':
1053
+ await withRl(async (rl) => { await cmdLogin(rl); });
1054
+ break;
1055
+ case 'logout': {
1056
+ const credsPath = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
1057
+ await fs.unlink(credsPath).catch(() => { });
1058
+ const authStore = await loadAuth();
1059
+ authStore.entries = [];
1060
+ delete authStore.activeProvider;
1061
+ await saveAuth(authStore);
1062
+ fi.println(chalk.dim(' Logged out. Credentials cleared.'));
1063
+ fi.teardown();
1064
+ rl.close();
1065
+ throw new ExitError();
1066
+ }
1067
+ case 'auth-status':
1068
+ await cmdAuthStatus(fi);
1069
+ break;
1070
+ case 'tasks':
1071
+ await cmdTasks(fi);
1072
+ break;
1073
+ case 'clear':
1074
+ fi.teardown();
1075
+ console.clear();
1076
+ fi.setup();
1077
+ break;
1078
+ case 'help':
1079
+ cmdHelp(fi);
1080
+ break;
1081
+ case 'exit':
1082
+ case 'quit':
1083
+ fi.println(chalk.dim(' Bye!'));
1084
+ fi.teardown();
1085
+ rl.close();
1086
+ throw new ExitError();
1087
+ default:
1088
+ fi.println(chalk.red(` Unknown command: /${cmd}`));
1089
+ fi.println(chalk.dim(' Type /help for available commands'));
1090
+ }
1091
+ fi.redrawBox();
1092
+ };
931
1093
  // Main REPL loop
932
- let firstMessage = true;
933
1094
  // eslint-disable-next-line no-constant-condition
934
1095
  while (true) {
935
1096
  const line = await fi.readLine();
936
1097
  const trimmed = line.trim();
937
1098
  if (!trimmed)
938
1099
  continue;
939
- // Show coordinator header before the first user message
940
- if (firstMessage && !trimmed.startsWith('/')) {
941
- firstMessage = false;
942
- try {
943
- const dir = process.cwd();
944
- const config = await loadProjectConfig(dir);
945
- if (config.roles?.orchestrator?.cli) {
946
- fi.println('');
947
- fi.println(chalk.bold.cyan('═'.repeat(46)));
948
- fi.println(chalk.bold.cyan(` COORDINADOR — ${config.project}`));
949
- fi.println(chalk.bold.cyan('═'.repeat(46)));
950
- fi.println(chalk.blue(` → Rol: COORDINADOR (solo orquesta, nunca ejecuta)`));
951
- fi.println(chalk.dim(' ' + '─'.repeat(42)));
952
- }
953
- }
954
- catch { }
955
- }
956
1100
  // Show user message in the scroll area (chat style, right-aligned per line)
957
1101
  const userLines = trimmed.split('\n').filter(l => l.trim());
958
1102
  for (let i = 0; i < userLines.length; i++) {
@@ -972,128 +1116,29 @@ export async function runRepl(resumeSession) {
972
1116
  fi.println(chalk.red(' No roles configured. Run /config-multi first.'));
973
1117
  }
974
1118
  else {
975
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi);
1119
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
976
1120
  await engine.runFullCycle(trimmed);
977
1121
  session.messages.push({ role: 'agent', content: `[task cycle completed for: ${trimmed}]`, ts: new Date().toISOString() });
978
1122
  await saveSession(session);
979
1123
  }
980
1124
  }
981
1125
  catch (err) {
1126
+ if (err instanceof ExitError)
1127
+ return;
982
1128
  fi.println(chalk.red(` Error: ${err.message}`));
983
1129
  }
984
1130
  fi.redrawBox();
985
1131
  continue;
986
1132
  }
987
- const parts = trimmed.slice(1).split(/\s+/);
988
- const cmd = parts[0].toLowerCase();
989
- const args = parts.slice(1);
990
1133
  try {
991
- switch (cmd) {
992
- case 'setup': {
993
- const sub = args[0]?.toLowerCase();
994
- const roleMap = {
995
- orch: 'orchestrator', impl: 'implementor', rev: 'reviewer',
996
- explorer: 'explorer', proposer: 'proposer', critic: 'critic',
997
- };
998
- if (sub && roleMap[sub]) {
999
- await withRl((rl) => cmdConfigOneRole(rl, roleMap[sub]));
1000
- }
1001
- else {
1002
- await withRl(cmdSetup);
1003
- }
1004
- break;
1005
- }
1006
- case 'config-multi':
1007
- await withRl(cmdConfigMulti);
1008
- break;
1009
- case 'status':
1010
- await cmdStatus(fi);
1011
- break;
1012
- case 'run':
1013
- if (args[0] === 'orch' || args[0] === 'orchestrator') {
1014
- const task = args.slice(1).join(' ');
1015
- const dir = process.cwd();
1016
- const config = await loadProjectConfig(dir);
1017
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi);
1018
- const result = await engine.runOrchestrator(task);
1019
- fi.println(chalk.green(` Task ID: ${result.taskId}`));
1020
- }
1021
- else if (args[0] === 'impl' || args[0] === 'implementor') {
1022
- const taskId = args[1];
1023
- const dir = process.cwd();
1024
- const config = await loadProjectConfig(dir);
1025
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi);
1026
- const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1027
- const plan = await readJson(path.join(taskDir, 'plan.json'));
1028
- await engine.runImplementor(taskId, plan);
1029
- }
1030
- else if (args[0] === 'rev' || args[0] === 'reviewer') {
1031
- const taskId = args[1];
1032
- const dir = process.cwd();
1033
- const config = await loadProjectConfig(dir);
1034
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi);
1035
- const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1036
- const plan = await readJson(path.join(taskDir, 'plan.json'));
1037
- const progress = await readJson(path.join(taskDir, 'progress.json'));
1038
- await engine.runReviewer(taskId, plan, progress);
1039
- }
1040
- else {
1041
- const task = args.join(' ');
1042
- const dir = process.cwd();
1043
- const config = await loadProjectConfig(dir);
1044
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi);
1045
- await engine.runFullCycle(task);
1046
- }
1047
- break;
1048
- case 'models':
1049
- await cmdModels(args[0], fi);
1050
- break;
1051
- case 'login':
1052
- await withRl(async (rl) => { await cmdLogin(rl); });
1053
- break;
1054
- case 'logout': {
1055
- // Clear OAuth credentials and auth store
1056
- const credsPath = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
1057
- await fs.unlink(credsPath).catch(() => { });
1058
- const authStore = await loadAuth();
1059
- authStore.entries = [];
1060
- delete authStore.activeProvider;
1061
- await saveAuth(authStore);
1062
- fi.println(chalk.dim(' Logged out. Credentials cleared.'));
1063
- fi.teardown();
1064
- rl.close();
1065
- return;
1066
- }
1067
- case 'auth-status':
1068
- await cmdAuthStatus(fi);
1069
- break;
1070
- case 'tasks':
1071
- await cmdTasks(fi);
1072
- break;
1073
- case 'clear':
1074
- fi.teardown();
1075
- console.clear();
1076
- fi.setup();
1077
- break;
1078
- case 'help':
1079
- cmdHelp(fi);
1080
- break;
1081
- case 'exit':
1082
- case 'quit':
1083
- fi.println(chalk.dim(' Bye!'));
1084
- fi.teardown();
1085
- rl.close();
1086
- return;
1087
- default:
1088
- fi.println(chalk.red(` Unknown command: /${cmd}`));
1089
- fi.println(chalk.dim(' Type /help for available commands'));
1090
- }
1134
+ await handleCmd(trimmed);
1091
1135
  }
1092
1136
  catch (err) {
1137
+ if (err instanceof ExitError)
1138
+ return;
1093
1139
  fi.println(chalk.red(` Error: ${err.message}`));
1140
+ fi.redrawBox();
1094
1141
  }
1095
- // Always redraw box after each command
1096
- fi.redrawBox();
1097
1142
  }
1098
1143
  }
1099
1144
  /**
@@ -1101,7 +1146,7 @@ export async function runRepl(resumeSession) {
1101
1146
  * NO spawnea CLIs externos. Usa la conexión OAuth de qwen directamente.
1102
1147
  * Prompt estricto: cada rol solo hace lo que debe, sin extras.
1103
1148
  */
1104
- export async function runRole(role, arg) {
1149
+ export async function runRole(role, arg, model) {
1105
1150
  const init = await initCoordinator();
1106
1151
  if (!init)
1107
1152
  process.exit(1);
@@ -1123,7 +1168,20 @@ export async function runRole(role, arg) {
1123
1168
  rl.close();
1124
1169
  process.exit(1);
1125
1170
  }
1126
- console.log(chalk.bold.cyan(`\n Agent-mp Rol: ${role.toUpperCase()}\n`));
1171
+ // Override model if passed via --model flag
1172
+ if (model) {
1173
+ const roleKey = role.toLowerCase().startsWith('orch') ? 'orchestrator'
1174
+ : role.toLowerCase().startsWith('impl') ? 'implementor'
1175
+ : role.toLowerCase().startsWith('rev') ? 'reviewer'
1176
+ : role.toLowerCase().startsWith('exp') ? 'explorer'
1177
+ : undefined;
1178
+ if (roleKey && config.roles[roleKey]) {
1179
+ const r = config.roles[roleKey];
1180
+ r.model = model;
1181
+ r.cmd = r.cmd.replace(/(-m|--model)\s+\S+/, `$1 ${model}`);
1182
+ }
1183
+ }
1184
+ console.log(chalk.bold.cyan(`\n Agent-mp — Rol: ${role.toUpperCase()}${model ? ` (${model})` : ''}\n`));
1127
1185
  const engine = new AgentEngine(config, dir, coordinatorCmd, rl);
1128
1186
  try {
1129
1187
  switch (role.toLowerCase()) {
@@ -57,6 +57,24 @@ function detectModels(cliName) {
57
57
  }
58
58
  case 'aider':
59
59
  return ['default'];
60
+ case 'agent-orch':
61
+ case 'agent-impl':
62
+ case 'agent-rev':
63
+ case 'agent-explorer': {
64
+ try {
65
+ const out = execSync(`${cliName} --list-models 2>/dev/null`, { encoding: 'utf-8' });
66
+ const models = out.trim().split('\n').filter(Boolean);
67
+ if (models.length)
68
+ return models;
69
+ }
70
+ catch { }
71
+ try {
72
+ const out = execSync('opencode models 2>/dev/null', { encoding: 'utf-8' });
73
+ return out.trim().split('\n').filter(Boolean);
74
+ }
75
+ catch { }
76
+ return ['configured'];
77
+ }
60
78
  default:
61
79
  return ['default'];
62
80
  }
@@ -1,15 +1,21 @@
1
1
  import * as readline from 'readline';
2
2
  import { AgentConfig, TaskPlan, TaskProgress } from '../types.js';
3
3
  import type { FixedInput } from '../ui/input.js';
4
+ /** Thrown when a slash command inside a conversation requests exit */
5
+ export declare class ExitError extends Error {
6
+ constructor();
7
+ }
8
+ export type SlashHandler = (input: string) => Promise<void>;
4
9
  export declare class AgentEngine {
5
10
  private config;
6
11
  private projectDir;
7
12
  private coordinatorCmd;
8
13
  private rl?;
9
14
  private fi?;
15
+ private slashHandler?;
10
16
  private totalTokens;
11
17
  private phaseTokens;
12
- constructor(config: AgentConfig, projectDir: string, coordinatorCmd?: string, rl?: readline.Interface, fi?: FixedInput);
18
+ constructor(config: AgentConfig, projectDir: string, coordinatorCmd?: string, rl?: readline.Interface, fi?: FixedInput, slashHandler?: SlashHandler);
13
19
  /**
14
20
  * FASE 0 — Clarificacion con el programador.
15
21
  * El coordinador (CLI activo, ej: Qwen) conversa con el usuario
@@ -9,6 +9,10 @@ import chalk from 'chalk';
9
9
  import { QWEN_AGENT_HOME } from '../utils/qwen-auth.js';
10
10
  import { callQwenAPI } from '../utils/qwen-auth.js';
11
11
  import * as fs from 'fs/promises';
12
+ /** Thrown when a slash command inside a conversation requests exit */
13
+ export class ExitError extends Error {
14
+ constructor() { super('exit'); }
15
+ }
12
16
  /** Parse --help output to detect current flags */
13
17
  function detectCliFlags(cliName) {
14
18
  try {
@@ -205,14 +209,16 @@ export class AgentEngine {
205
209
  coordinatorCmd;
206
210
  rl;
207
211
  fi;
212
+ slashHandler;
208
213
  totalTokens = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 };
209
214
  phaseTokens = [];
210
- constructor(config, projectDir, coordinatorCmd, rl, fi) {
215
+ constructor(config, projectDir, coordinatorCmd, rl, fi, slashHandler) {
211
216
  this.config = config;
212
217
  this.projectDir = projectDir;
213
218
  this.coordinatorCmd = coordinatorCmd || '';
214
219
  this.rl = rl;
215
220
  this.fi = fi;
221
+ this.slashHandler = slashHandler;
216
222
  }
217
223
  /**
218
224
  * FASE 0 — Clarificacion con el programador.
@@ -223,14 +229,10 @@ export class AgentEngine {
223
229
  if (!this.coordinatorCmd) {
224
230
  return initialTask;
225
231
  }
226
- log.section('FASE 0 — Clarificacion');
227
- log.info('El coordinador va a conversar con vos para entender la tarea.');
228
- log.info('Cuando el coordinador tenga claro el objetivo, te va a pedir confirmación para lanzar el plan.');
229
- log.divider();
230
232
  const context = await this.buildCoordinatorContext();
231
233
  let conversationHistory = `TAREA INICIAL: ${initialTask}`;
232
- // Loop de clarificacion
233
- while (true) {
234
+ // Helper: call coordinator CLI and return response text
235
+ const callCoordinator = async () => {
234
236
  const prompt = `Sos el COORDINADOR de un equipo multi-agente de desarrollo.
235
237
  Tu trabajo es ENTENDER lo que el programador necesita haciendo PREGUNTAS si es necesario.
236
238
 
@@ -247,12 +249,11 @@ INSTRUCCIONES:
247
249
  - NO uses JSON, habla normalmente.
248
250
  - Sé breve y directo.`;
249
251
  log.info('Coordinador analizando...');
250
- // Usar credenciales corporativas si es qwen
251
252
  const envOverride = {};
252
- const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
253
- const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
253
+ let res;
254
254
  if (this.coordinatorCmd.startsWith('qwen')) {
255
- // Check if corporate credentials exist
255
+ const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
256
+ const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
256
257
  let corporateCredsContent = null;
257
258
  try {
258
259
  corporateCredsContent = await fs.readFile(corporateCreds, 'utf-8');
@@ -260,93 +261,63 @@ INSTRUCCIONES:
260
261
  catch {
261
262
  console.log(chalk.red('\n ✗ No hay credenciales corporativas de Qwen.'));
262
263
  console.log(chalk.yellow(' Ejecutá /login para autenticarte.\n'));
263
- return initialTask;
264
+ return '';
264
265
  }
265
- // Backup personal creds
266
266
  let personalBackup = null;
267
267
  try {
268
268
  personalBackup = await fs.readFile(personalCreds, 'utf-8');
269
269
  }
270
270
  catch { }
271
- // Copy corporate creds to personal location
272
271
  await fs.writeFile(personalCreds, corporateCredsContent);
273
- const res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
274
- // Restore personal creds
272
+ res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
275
273
  if (personalBackup) {
276
274
  await fs.writeFile(personalCreds, personalBackup);
277
275
  }
278
276
  else {
279
- // No personal backup, remove the file
280
277
  await fs.unlink(personalCreds).catch(() => { });
281
278
  }
282
- // Extraer texto legible del output (Qwen CLI devuelve JSON con eventos)
283
- let responseText = res.output.trim();
284
- try {
285
- const json = JSON.parse(res.output);
286
- if (Array.isArray(json)) {
287
- // Buscar el resultado final
288
- for (const item of json) {
289
- if (item.type === 'result' && typeof item.result === 'string') {
290
- responseText = item.result;
291
- break;
292
- }
293
- if (item.type === 'assistant' && item.message?.content?.length > 0) {
294
- const textContent = item.message.content.find((c) => c.type === 'text');
295
- if (textContent?.text) {
296
- responseText = textContent.text;
297
- break;
298
- }
299
- }
300
- }
301
- }
302
- }
303
- catch {
304
- // No es JSON, usar el output directo
305
- }
306
- console.log('');
307
- console.log(chalk.cyan(' Coordinador:'));
308
- console.log(chalk.white(` ${responseText}`));
309
- console.log('');
310
- // Verificar si el coordinador quiere lanzar el orchestrator EXPLICITAMENTE
311
- // Solo si pregunta "confirmás" o similar
312
- const lower = responseText.toLowerCase();
313
- if ((lower.includes('confirm') || lower.includes('procedo') || lower.includes('lanzo el plan')) &&
314
- (lower.includes('orchestrator') || lower.includes('plan'))) {
315
- const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl, this.fi);
316
- if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 's') {
317
- return conversationHistory;
318
- }
319
- const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl, this.fi);
320
- conversationHistory += `\nPROGRAMADOR: ${correction}`;
321
- continue;
322
- }
323
- // Si no, seguir conversando
324
- const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
325
- conversationHistory += `\nPROGRAMADOR: ${answer}`;
326
279
  }
327
280
  else {
328
- // Para otros CLIs, usar normalmente
329
- const res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
330
- // Extraer texto legible del output
331
- let responseText = res.output.trim();
332
- try {
333
- const json = JSON.parse(res.output);
334
- if (Array.isArray(json)) {
335
- for (const item of json) {
336
- if (item.type === 'result' && typeof item.result === 'string') {
337
- responseText = item.result;
281
+ res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
282
+ }
283
+ // Extract readable text (Qwen CLI returns JSON events)
284
+ let responseText = res.output.trim();
285
+ try {
286
+ const json = JSON.parse(res.output);
287
+ if (Array.isArray(json)) {
288
+ for (const item of json) {
289
+ if (item.type === 'result' && typeof item.result === 'string') {
290
+ responseText = item.result;
291
+ break;
292
+ }
293
+ if (item.type === 'assistant' && item.message?.content?.length > 0) {
294
+ const t = item.message.content.find((c) => c.type === 'text');
295
+ if (t?.text) {
296
+ responseText = t.text;
338
297
  break;
339
298
  }
340
299
  }
341
300
  }
342
301
  }
343
- catch { }
302
+ }
303
+ catch { }
304
+ return responseText;
305
+ };
306
+ // Clarification loop — coordinator is only called when there is new user input
307
+ let needsCoordinatorCall = true;
308
+ while (true) {
309
+ if (needsCoordinatorCall) {
310
+ needsCoordinatorCall = false;
311
+ const responseText = await callCoordinator();
312
+ if (!responseText)
313
+ return initialTask; // auth error
344
314
  console.log('');
345
315
  console.log(chalk.cyan(' Coordinador:'));
346
316
  console.log(chalk.white(` ${responseText}`));
347
317
  console.log('');
318
+ // If coordinator is asking for confirmation to launch the plan
348
319
  const lower = responseText.toLowerCase();
349
- if ((lower.includes('confirm') || lower.includes('procedo')) &&
320
+ if ((lower.includes('confirm') || lower.includes('procedo') || lower.includes('lanzo el plan')) &&
350
321
  (lower.includes('orchestrator') || lower.includes('plan'))) {
351
322
  const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl, this.fi);
352
323
  if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 's') {
@@ -354,11 +325,20 @@ INSTRUCCIONES:
354
325
  }
355
326
  const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl, this.fi);
356
327
  conversationHistory += `\nPROGRAMADOR: ${correction}`;
328
+ needsCoordinatorCall = true;
357
329
  continue;
358
330
  }
359
- const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
360
- conversationHistory += `\nPROGRAMADOR: ${answer}`;
361
331
  }
332
+ // Ask user — slash commands are handled here without calling coordinator again
333
+ const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
334
+ const trimmedAnswer = answer.trim();
335
+ if (trimmedAnswer.startsWith('/') && this.slashHandler) {
336
+ await this.slashHandler(trimmedAnswer);
337
+ // needsCoordinatorCall is still false — re-prompt without coordinator call
338
+ continue;
339
+ }
340
+ conversationHistory += `\nPROGRAMADOR: ${answer}`;
341
+ needsCoordinatorCall = true; // got real input, coordinator should respond
362
342
  }
363
343
  }
364
344
  async runWithFallback(roleName, prompt, phaseName) {
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ program
18
18
  .option('--continue', 'Resume the last session in the current directory')
19
19
  .option('--resume [id]', 'Resume any saved session by ID (omit ID to pick from list)')
20
20
  .option('--rol <role>', 'Run a specific role directly: orchestrator | implementor | reviewer | coordinator')
21
+ .option('--models [cli]', 'List available models (optionally filter by CLI name)')
22
+ .option('--model [cli]', 'Alias for --models')
21
23
  .option('--reset-coordinator', 'Clear coordinator selection (re-pick on next run)')
22
24
  .option('--reset-auth', 'Wipe all auth credentials and start fresh')
23
25
  .addHelpText('after', `
@@ -66,13 +68,19 @@ const ROLE_BINS = {
66
68
  };
67
69
  const nativeRole = ROLE_BINS[PKG_NAME];
68
70
  if (nativeRole) {
69
- // Role-specific CLI: accept task as positional arg or prompt for it
70
- const taskArg = process.argv.slice(2).filter(a => !a.startsWith('-')).join(' ').trim();
71
+ // Parse --model flag if provided
72
+ const args = process.argv.slice(2);
73
+ const modelIdx = args.findIndex(a => a === '--model' || a === '-m');
74
+ let model;
75
+ if (modelIdx !== -1 && args[modelIdx + 1]) {
76
+ model = args[modelIdx + 1];
77
+ }
78
+ const taskArg = args.filter((a, i) => !a.startsWith('-') && i !== modelIdx + 1).join(' ').trim();
71
79
  if (!taskArg) {
72
- console.error(chalk.red(` Usage: ${PKG_NAME} "<task description or task-id>"`));
80
+ console.error(chalk.red(` Usage: ${PKG_NAME} [--model <model>] "<task description or task-id>"`));
73
81
  process.exit(1);
74
82
  }
75
- await runRole(nativeRole, taskArg);
83
+ await runRole(nativeRole, taskArg, model);
76
84
  process.exit(0);
77
85
  }
78
86
  // ─────────────────────────────────────────────────────────────────────────────
@@ -98,6 +106,54 @@ program.action(async (task, options) => {
98
106
  }
99
107
  process.exit(0);
100
108
  }
109
+ // --models / --model: list models for authenticated providers only
110
+ if (options.models !== undefined || options.model !== undefined) {
111
+ const { execSync } = await import('child_process');
112
+ const { loadAuth } = await import('./utils/config.js');
113
+ const filter = typeof options.models === 'string' ? options.models
114
+ : typeof options.model === 'string' ? options.model
115
+ : undefined;
116
+ const auth = await loadAuth();
117
+ const authedProviders = auth.entries.map(e => e.provider);
118
+ if (authedProviders.length === 0) {
119
+ console.log(chalk.yellow(' No authenticated providers. Run /login first.'));
120
+ process.exit(0);
121
+ }
122
+ const targets = filter ? [filter] : authedProviders;
123
+ const { fetchQwenModels } = await import('./utils/qwen-auth.js');
124
+ for (const provider of targets) {
125
+ try {
126
+ let models = [];
127
+ if (provider === 'qwen') {
128
+ models = await fetchQwenModels();
129
+ }
130
+ else if (provider === 'opencode') {
131
+ models = execSync('opencode models 2>/dev/null', { encoding: 'utf-8' }).trim().split('\n').filter(Boolean);
132
+ }
133
+ else if (provider === 'claude') {
134
+ const help = execSync('claude --help 2>/dev/null', { encoding: 'utf-8' });
135
+ models = help.match(/'([^']+)'/g)?.map(m => m.replace(/'/g, '')).filter(Boolean) ?? ['opus', 'sonnet', 'haiku'];
136
+ }
137
+ else if (provider === 'gemini') {
138
+ models = execSync('gemini models 2>/dev/null', { encoding: 'utf-8' }).trim().split('\n').filter(Boolean);
139
+ if (!models.length)
140
+ models = ['gemini-2.5-pro', 'gemini-2.5-flash'];
141
+ }
142
+ else {
143
+ models = ['default'];
144
+ }
145
+ const email = auth.entries.find(e => e.provider === provider)?.email;
146
+ const label = email ? `${provider} (${email})` : provider;
147
+ console.log(chalk.bold.cyan(`\n ${label}:`));
148
+ models.forEach(m => console.log(chalk.dim(` ${m}`)));
149
+ }
150
+ catch {
151
+ console.log(chalk.dim(` ${provider}: (unavailable)`));
152
+ }
153
+ }
154
+ console.log('');
155
+ process.exit(0);
156
+ }
101
157
  // --continue: resume last session for current dir
102
158
  if (options.continue) {
103
159
  const session = await getLastSessionForDir(process.cwd());
package/dist/types.js CHANGED
@@ -55,4 +55,36 @@ export const CLI_REGISTRY = {
55
55
  promptFlag: '-p',
56
56
  promptPosition: 'flag',
57
57
  },
58
+ 'agent-orch': {
59
+ name: 'Agent Orch',
60
+ command: 'agent-orch',
61
+ modelFlag: '--model',
62
+ extraFlags: '',
63
+ promptFlag: '',
64
+ promptPosition: 'arg',
65
+ },
66
+ 'agent-impl': {
67
+ name: 'Agent Impl',
68
+ command: 'agent-impl',
69
+ modelFlag: '--model',
70
+ extraFlags: '',
71
+ promptFlag: '',
72
+ promptPosition: 'arg',
73
+ },
74
+ 'agent-rev': {
75
+ name: 'Agent Rev',
76
+ command: 'agent-rev',
77
+ modelFlag: '--model',
78
+ extraFlags: '',
79
+ promptFlag: '',
80
+ promptPosition: 'arg',
81
+ },
82
+ 'agent-explorer': {
83
+ name: 'Agent Explorer',
84
+ command: 'agent-explorer',
85
+ modelFlag: '--model',
86
+ extraFlags: '',
87
+ promptFlag: '',
88
+ promptPosition: 'arg',
89
+ },
58
90
  };
@@ -8,5 +8,6 @@ export declare function qwenAuthStatus(): Promise<{
8
8
  authenticated: boolean;
9
9
  email?: string;
10
10
  }>;
11
+ export declare function fetchQwenModels(): Promise<string[]>;
11
12
  export declare function getQwenAccessToken(): Promise<string | null>;
12
13
  export declare function callQwenAPI(prompt: string, model?: string): Promise<string>;
@@ -216,6 +216,23 @@ export async function qwenAuthStatus() {
216
216
  return { authenticated: false };
217
217
  return { authenticated: true };
218
218
  }
219
+ export async function fetchQwenModels() {
220
+ const token = await loadToken();
221
+ if (!token)
222
+ return [];
223
+ try {
224
+ const res = await fetch('https://chat.qwen.ai/api/models', {
225
+ headers: { Authorization: `Bearer ${token.accessToken}` },
226
+ });
227
+ if (!res.ok)
228
+ return [];
229
+ const data = await res.json();
230
+ return (data.data ?? []).map((m) => m.id).filter(Boolean);
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
219
236
  export async function getQwenAccessToken() {
220
237
  const token = await loadToken();
221
238
  return token?.accessToken || null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-rev",
3
- "version": "0.1.2",
4
- "description": "Reviewer agent — validates and reviews implementation against the plan",
3
+ "version": "0.2.3",
4
+ "description": "Reviewer agent — validates and reviews code written by the implementor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "files": [
@@ -15,14 +15,7 @@
15
15
  "dev": "tsx src/index.ts",
16
16
  "prepublishOnly": "npm run build"
17
17
  },
18
- "keywords": [
19
- "ai",
20
- "agent",
21
- "orchestrator",
22
- "multi-agent",
23
- "cli",
24
- "coding"
25
- ],
18
+ "keywords": ["ai", "agent", "reviewer", "multi-agent", "cli", "coding"],
26
19
  "license": "MIT",
27
20
  "dependencies": {
28
21
  "@anthropic-ai/sdk": "^0.39.0",