reygent-code 0.1.0 → 1.0.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.
package/dist/cli.js CHANGED
@@ -584,6 +584,8 @@ var claudeAdapter = {
584
584
  });
585
585
  registerChildProcess(child);
586
586
  let resultText = "";
587
+ let resultErrorMessage;
588
+ let resultApiErrorStatus;
587
589
  let resultUsage;
588
590
  const textChunks = [];
589
591
  const name = options.agentName;
@@ -606,7 +608,13 @@ var claudeAdapter = {
606
608
  if (stdoutEnded && stderrEnded && processExitCode !== null) {
607
609
  clearTimeout(timeout);
608
610
  const stdout = resultText || textChunks.join("\n");
609
- resolve5({ stdout, exitCode: processExitCode, usage: resultUsage });
611
+ resolve5({
612
+ stdout,
613
+ exitCode: processExitCode,
614
+ usage: resultUsage,
615
+ errorMessage: resultErrorMessage,
616
+ apiErrorStatus: resultApiErrorStatus
617
+ });
610
618
  }
611
619
  };
612
620
  const stdoutRL = createInterface({ input: child.stdout });
@@ -641,6 +649,10 @@ var claudeAdapter = {
641
649
  } else if (event.type === "result") {
642
650
  const msg = event;
643
651
  resultText = msg.result;
652
+ if (msg.is_error) {
653
+ resultErrorMessage = msg.result;
654
+ resultApiErrorStatus = msg.api_error_status;
655
+ }
644
656
  const { inputTokens, outputTokens, cachedTokens, cacheWriteTokens } = extractTokenUsage(msg);
645
657
  const hasUsage = msg.total_cost_usd !== void 0 || msg.duration_ms !== void 0 || msg.num_turns !== void 0 || inputTokens !== void 0 || outputTokens !== void 0;
646
658
  if (hasUsage) {
@@ -809,14 +821,47 @@ var geminiAdapter = {
809
821
  let inputTokens;
810
822
  let outputTokens;
811
823
  let cachedTokens;
824
+ let errorMessage;
825
+ let apiErrorStatus;
812
826
  try {
813
827
  const parsed = JSON.parse(stdout);
814
828
  resultText = parsed.response ?? parsed.text ?? stdout;
815
829
  inputTokens = parsed.usage_metadata?.prompt_token_count ?? parsed.input_tokens;
816
830
  outputTokens = parsed.usage_metadata?.candidates_token_count ?? parsed.output_tokens;
817
831
  cachedTokens = parsed.usage_metadata?.cached_content_token_count;
832
+ if (parsed.error) {
833
+ errorMessage = parsed.error.message;
834
+ let statusCode = parsed.error.status;
835
+ if (parsed.error.code) {
836
+ if (typeof parsed.error.code === "number") {
837
+ statusCode = parsed.error.code;
838
+ } else if (typeof parsed.error.code === "string") {
839
+ const code2 = parsed.error.code.toLowerCase();
840
+ if (code2 === "not_found" || code2 === "model_not_found") {
841
+ statusCode = 404;
842
+ } else if (code2 === "permission_denied" || code2 === "unauthenticated") {
843
+ statusCode = 403;
844
+ } else if (code2 === "invalid_api_key" || code2 === "invalid_authentication") {
845
+ statusCode = 401;
846
+ } else if (code2 === "resource_exhausted" || code2 === "rate_limit_exceeded") {
847
+ statusCode = 429;
848
+ } else if (code2 === "internal" || code2 === "server_error") {
849
+ statusCode = 500;
850
+ } else if (code2 === "invalid_argument") {
851
+ statusCode = 400;
852
+ }
853
+ }
854
+ }
855
+ apiErrorStatus = statusCode;
856
+ }
818
857
  } catch {
819
858
  }
859
+ if (code !== 0 && !errorMessage && stderrChunks.length > 0) {
860
+ const stderr = stderrChunks.join("").trim();
861
+ if (stderr) {
862
+ errorMessage = stderr;
863
+ }
864
+ }
820
865
  resolve5({
821
866
  stdout: resultText,
822
867
  exitCode: code ?? 1,
@@ -826,7 +871,9 @@ var geminiAdapter = {
826
871
  outputTokens,
827
872
  cachedTokens,
828
873
  provider: "gemini"
829
- }
874
+ },
875
+ errorMessage,
876
+ apiErrorStatus
830
877
  });
831
878
  });
832
879
  });
@@ -922,8 +969,10 @@ var codexAdapter = {
922
969
  child.stdout.on("data", (chunk) => {
923
970
  stdout += chunk.toString();
924
971
  });
972
+ const stderrChunks = [];
925
973
  child.stderr.on("data", (chunk) => {
926
974
  const text = chunk.toString();
975
+ stderrChunks.push(text);
927
976
  if (options.onActivity) {
928
977
  const line = text.trim();
929
978
  if (line) options.onActivity({ agent: name, detail: line.slice(0, 80) });
@@ -942,14 +991,44 @@ var codexAdapter = {
942
991
  let inputTokens;
943
992
  let outputTokens;
944
993
  let cachedTokens;
994
+ let errorMessage;
995
+ let apiErrorStatus;
945
996
  try {
946
997
  const parsed = JSON.parse(stdout);
947
998
  resultText = parsed.response ?? parsed.text ?? stdout;
948
999
  inputTokens = parsed.usage?.prompt_tokens ?? parsed.input_tokens;
949
1000
  outputTokens = parsed.usage?.completion_tokens ?? parsed.output_tokens;
950
1001
  cachedTokens = parsed.usage?.prompt_tokens_details?.cached_tokens ?? parsed.cached_tokens;
1002
+ if (parsed.error) {
1003
+ errorMessage = parsed.error.message;
1004
+ if (typeof parsed.error.code === "string") {
1005
+ const code2 = parsed.error.code;
1006
+ if (code2 === "model_not_found" || code2 === "invalid_model") {
1007
+ apiErrorStatus = 404;
1008
+ } else if (code2 === "invalid_api_key" || code2 === "invalid_request_error") {
1009
+ apiErrorStatus = 401;
1010
+ } else if (code2 === "rate_limit_exceeded") {
1011
+ apiErrorStatus = 429;
1012
+ } else if (code2 === "insufficient_quota") {
1013
+ apiErrorStatus = 402;
1014
+ } else if (code2 === "server_error") {
1015
+ apiErrorStatus = 500;
1016
+ } else if (code2.includes("not_found")) {
1017
+ apiErrorStatus = 404;
1018
+ } else if (code2.includes("auth") || code2.includes("unauthorized")) {
1019
+ apiErrorStatus = 401;
1020
+ }
1021
+ }
1022
+ apiErrorStatus = apiErrorStatus ?? parsed.error.status;
1023
+ }
951
1024
  } catch {
952
1025
  }
1026
+ if (code !== 0 && !errorMessage && stderrChunks.length > 0) {
1027
+ const stderr = stderrChunks.join("").trim();
1028
+ if (stderr) {
1029
+ errorMessage = stderr;
1030
+ }
1031
+ }
953
1032
  resolve5({
954
1033
  stdout: resultText,
955
1034
  exitCode: code ?? 1,
@@ -960,7 +1039,9 @@ var codexAdapter = {
960
1039
  cachedTokens,
961
1040
  // Note: cacheWriteTokens not extracted — OpenAI doesn't currently expose this field
962
1041
  provider: "codex"
963
- }
1042
+ },
1043
+ errorMessage,
1044
+ apiErrorStatus
964
1045
  });
965
1046
  });
966
1047
  });
@@ -2616,7 +2697,56 @@ function searchKnowledge(query) {
2616
2697
  return results;
2617
2698
  }
2618
2699
 
2700
+ // src/telemetry-helpers.ts
2701
+ function emitErrorTask(message, stage, options) {
2702
+ const chesstrace = getChesstrace();
2703
+ if (chesstrace) {
2704
+ try {
2705
+ chesstrace.emit(Events.ERROR_TASK, {
2706
+ type: "TaskError",
2707
+ message,
2708
+ stage,
2709
+ ...options?.agent && { agent: options.agent },
2710
+ ...options?.errorMessage && { errorMessage: options.errorMessage },
2711
+ ...options?.apiErrorStatus && { apiErrorStatus: options.apiErrorStatus }
2712
+ });
2713
+ } catch (err) {
2714
+ if (isDebug()) {
2715
+ const errMsg = err instanceof Error ? err.message : String(err);
2716
+ console.error(`[DEBUG] Telemetry emit failed (ERROR_TASK): ${errMsg}`);
2717
+ }
2718
+ }
2719
+ }
2720
+ }
2721
+
2619
2722
  // src/spawn.ts
2723
+ function looksLikeMalformedModel(model) {
2724
+ if (!model) return false;
2725
+ const trimmed = model.trim();
2726
+ if (trimmed.length === 0) return true;
2727
+ if (/\s/.test(trimmed)) return true;
2728
+ if (/^[^a-zA-Z0-9]|[^a-zA-Z0-9]$/.test(trimmed)) return true;
2729
+ if (trimmed.length < 3) return true;
2730
+ return false;
2731
+ }
2732
+ function formatExitDetail(result, model) {
2733
+ if (result.errorMessage) {
2734
+ const status = result.apiErrorStatus ? ` (HTTP ${result.apiErrorStatus})` : "";
2735
+ let detail = `
2736
+ ${result.errorMessage}${status}`;
2737
+ if (result.apiErrorStatus === 404 && /not available/i.test(result.errorMessage) && looksLikeMalformedModel(model)) {
2738
+ detail += `
2739
+ Tip: edit .reygent/config.json "model" field, or run \`reygent config\` to pick a supported model.`;
2740
+ }
2741
+ return detail;
2742
+ }
2743
+ const trimmed = result.stdout.trim();
2744
+ if (!trimmed) return "";
2745
+ const truncated = trimmed.slice(0, 500);
2746
+ const suffix = trimmed.length > 500 ? "..." : "";
2747
+ return `
2748
+ ${truncated}${suffix}`;
2749
+ }
2620
2750
  async function spawnAgentStream(name, prompt2, timeoutMs, options) {
2621
2751
  const providerName = options?.provider ?? resolveProvider();
2622
2752
  const adapter = getProvider(providerName);
@@ -2628,13 +2758,12 @@ async function spawnAgentStream(name, prompt2, timeoutMs, options) {
2628
2758
  provider: providerName,
2629
2759
  reason
2630
2760
  });
2631
- chesstrace.emit(Events.ERROR_TASK, {
2632
- type: "TaskError",
2633
- message: `Provider "${providerName}" is not available: ${reason}`,
2634
- stage: options?.stage ?? "spawn",
2635
- agent: name
2636
- });
2637
2761
  }
2762
+ emitErrorTask(
2763
+ `Provider "${providerName}" is not available: ${reason}`,
2764
+ options?.stage ?? "spawn",
2765
+ { agent: name }
2766
+ );
2638
2767
  throw new TaskError(`Provider "${providerName}" is not available: ${reason}`);
2639
2768
  }
2640
2769
  const modelId = options?.model ?? await resolveModel(providerName);
@@ -2818,18 +2947,16 @@ async function runPlanner(spec, previousAnswers, options) {
2818
2947
  const agents = getAgents();
2819
2948
  const plannerAgent = agents.find((a) => a.name === "planner");
2820
2949
  const prompt2 = buildPrompt(spec, previousAnswers, options);
2821
- const { stdout: raw, exitCode, usage } = await spawnAgentStream("planner", prompt2, 3e5, { quiet: true, onActivity: options?.onActivity, provider: plannerAgent?.provider, model: plannerAgent?.model });
2950
+ const spawnResult = await spawnAgentStream("planner", prompt2, 3e5, { quiet: true, onActivity: options?.onActivity, provider: plannerAgent?.provider, model: plannerAgent?.model });
2951
+ const { stdout: raw, exitCode, usage, errorMessage, apiErrorStatus } = spawnResult;
2822
2952
  if (exitCode !== 0) {
2823
- const chesstrace2 = getChesstrace();
2824
- if (chesstrace2) {
2825
- chesstrace2.emit(Events.ERROR_TASK, {
2826
- type: "TaskError",
2827
- message: `Planner: agent exited with code ${exitCode}`,
2828
- stage: "plan",
2829
- agent: "planner"
2830
- });
2831
- }
2832
- throw new TaskError(`Planner: agent exited with code ${exitCode}`);
2953
+ const detail = formatExitDetail(spawnResult, plannerAgent?.model);
2954
+ emitErrorTask(
2955
+ `Planner: agent exited with code ${exitCode}${detail}`,
2956
+ "plan",
2957
+ { agent: "planner", errorMessage, apiErrorStatus }
2958
+ );
2959
+ throw new TaskError(`Planner: agent exited with code ${exitCode}${detail}`);
2833
2960
  }
2834
2961
  let parsed;
2835
2962
  try {
@@ -2857,31 +2984,23 @@ async function runPlanner(spec, previousAnswers, options) {
2857
2984
  }
2858
2985
  }
2859
2986
  const errors = Array.isArray(obj.errors) ? obj.errors.join("\n - ") : "unknown validation error";
2860
- const chesstrace2 = getChesstrace();
2861
- if (chesstrace2) {
2862
- chesstrace2.emit(Events.ERROR_TASK, {
2863
- type: "TaskError",
2864
- message: `Planner: spec validation failed:
2987
+ emitErrorTask(
2988
+ `Planner: spec validation failed:
2865
2989
  - ${errors}`,
2866
- stage: "plan",
2867
- agent: "planner"
2868
- });
2869
- }
2990
+ "plan",
2991
+ { agent: "planner" }
2992
+ );
2870
2993
  throw new TaskError(
2871
2994
  `Planner: spec validation failed:
2872
2995
  - ${errors}`
2873
2996
  );
2874
2997
  }
2875
2998
  if (obj.valid !== true) {
2876
- const chesstrace2 = getChesstrace();
2877
- if (chesstrace2) {
2878
- chesstrace2.emit(Events.ERROR_TASK, {
2879
- type: "TaskError",
2880
- message: "Planner: unexpected response \u2014 missing 'valid' field",
2881
- stage: "plan",
2882
- agent: "planner"
2883
- });
2884
- }
2999
+ emitErrorTask(
3000
+ "Planner: unexpected response \u2014 missing 'valid' field",
3001
+ "plan",
3002
+ { agent: "planner" }
3003
+ );
2885
3004
  throw new TaskError(
2886
3005
  "Planner: unexpected response \u2014 missing 'valid' field"
2887
3006
  );
@@ -2889,49 +3008,37 @@ async function runPlanner(spec, previousAnswers, options) {
2889
3008
  const { goals, tasks, constraints, dod } = obj;
2890
3009
  const chesstrace = getChesstrace();
2891
3010
  if (!isNonEmptyStringArray(goals)) {
2892
- if (chesstrace) {
2893
- chesstrace.emit(Events.ERROR_TASK, {
2894
- type: "TaskError",
2895
- message: "Planner: 'goals' must be a non-empty string array",
2896
- stage: "plan",
2897
- agent: "planner"
2898
- });
2899
- }
3011
+ emitErrorTask(
3012
+ "Planner: 'goals' must be a non-empty string array",
3013
+ "plan",
3014
+ { agent: "planner" }
3015
+ );
2900
3016
  throw new TaskError("Planner: 'goals' must be a non-empty string array");
2901
3017
  }
2902
3018
  if (!isNonEmptyStringArray(tasks)) {
2903
- if (chesstrace) {
2904
- chesstrace.emit(Events.ERROR_TASK, {
2905
- type: "TaskError",
2906
- message: "Planner: 'tasks' must be a non-empty string array",
2907
- stage: "plan",
2908
- agent: "planner"
2909
- });
2910
- }
3019
+ emitErrorTask(
3020
+ "Planner: 'tasks' must be a non-empty string array",
3021
+ "plan",
3022
+ { agent: "planner" }
3023
+ );
2911
3024
  throw new TaskError("Planner: 'tasks' must be a non-empty string array");
2912
3025
  }
2913
3026
  if (!isNonEmptyStringArray(constraints)) {
2914
- if (chesstrace) {
2915
- chesstrace.emit(Events.ERROR_TASK, {
2916
- type: "TaskError",
2917
- message: "Planner: 'constraints' must be a non-empty string array",
2918
- stage: "plan",
2919
- agent: "planner"
2920
- });
2921
- }
3027
+ emitErrorTask(
3028
+ "Planner: 'constraints' must be a non-empty string array",
3029
+ "plan",
3030
+ { agent: "planner" }
3031
+ );
2922
3032
  throw new TaskError(
2923
3033
  "Planner: 'constraints' must be a non-empty string array"
2924
3034
  );
2925
3035
  }
2926
3036
  if (!isNonEmptyStringArray(dod)) {
2927
- if (chesstrace) {
2928
- chesstrace.emit(Events.ERROR_TASK, {
2929
- type: "TaskError",
2930
- message: "Planner: 'dod' must be a non-empty string array",
2931
- stage: "plan",
2932
- agent: "planner"
2933
- });
2934
- }
3037
+ emitErrorTask(
3038
+ "Planner: 'dod' must be a non-empty string array",
3039
+ "plan",
3040
+ { agent: "planner" }
3041
+ );
2935
3042
  throw new TaskError("Planner: 'dod' must be a non-empty string array");
2936
3043
  }
2937
3044
  return { result: { goals, tasks, constraints, dod }, usage };
@@ -3017,10 +3124,19 @@ Be specific and actionable. Expand the description into concrete requirements th
3017
3124
  **Description:** ${description}${answersContext}`;
3018
3125
  }
3019
3126
  async function runClarification(description, previousAnswers, onActivity) {
3127
+ const agents = getAgents();
3128
+ const plannerAgent = agents.find((a) => a.name === "planner");
3020
3129
  const prompt2 = buildClarificationPrompt(description, previousAnswers);
3021
- const { stdout: raw, exitCode } = await spawnAgentStream("generate-spec", prompt2, 12e4, { quiet: true, onActivity });
3130
+ const clarifyResult = await spawnAgentStream("generate-spec", prompt2, 12e4, { quiet: true, onActivity });
3131
+ const { stdout: raw, exitCode, errorMessage, apiErrorStatus } = clarifyResult;
3022
3132
  if (exitCode !== 0) {
3023
- throw new TaskError(`generate-spec: agent exited with code ${exitCode}`);
3133
+ const detail = formatExitDetail(clarifyResult, plannerAgent?.model);
3134
+ emitErrorTask(
3135
+ `generate-spec: agent exited with code ${exitCode}${detail}`,
3136
+ "clarification",
3137
+ { agent: "generate-spec", errorMessage, apiErrorStatus }
3138
+ );
3139
+ throw new TaskError(`generate-spec: agent exited with code ${exitCode}${detail}`);
3024
3140
  }
3025
3141
  let parsed;
3026
3142
  try {
@@ -3052,10 +3168,19 @@ async function runClarification(description, previousAnswers, onActivity) {
3052
3168
  return { ready: true };
3053
3169
  }
3054
3170
  async function generateSpec(description, clarificationAnswers, onActivity) {
3171
+ const agents = getAgents();
3172
+ const plannerAgent = agents.find((a) => a.name === "planner");
3055
3173
  const prompt2 = buildGeneratePrompt(description, clarificationAnswers);
3056
- const { stdout, exitCode } = await spawnAgentStream("generate-spec", prompt2, 12e4, { onActivity });
3174
+ const specResult = await spawnAgentStream("generate-spec", prompt2, 12e4, { onActivity });
3175
+ const { stdout, exitCode, errorMessage, apiErrorStatus } = specResult;
3057
3176
  if (exitCode !== 0) {
3058
- throw new TaskError(`generate-spec: agent exited with code ${exitCode}`);
3177
+ const detail = formatExitDetail(specResult, plannerAgent?.model);
3178
+ emitErrorTask(
3179
+ `generate-spec: agent exited with code ${exitCode}${detail}`,
3180
+ "generate",
3181
+ { agent: "generate-spec", errorMessage, apiErrorStatus }
3182
+ );
3183
+ throw new TaskError(`generate-spec: agent exited with code ${exitCode}${detail}`);
3059
3184
  }
3060
3185
  if (!stdout) {
3061
3186
  throw new TaskError("generate-spec: empty result from agent");
@@ -3861,15 +3986,11 @@ async function runImplement(spec, plan, options, retryOptions) {
3861
3986
  const devAgent = agents.find((a) => a.name === "dev");
3862
3987
  const qeAgent = agents.find((a) => a.name === "qe");
3863
3988
  if (!devAgent || !qeAgent) {
3864
- const chesstrace2 = getChesstrace();
3865
- if (chesstrace2) {
3866
- chesstrace2.emit(Events.ERROR_TASK, {
3867
- type: "TaskError",
3868
- message: "Implement: missing dev or qe agent config",
3869
- stage: options?.stage ?? "implement",
3870
- agent: "implement"
3871
- });
3872
- }
3989
+ emitErrorTask(
3990
+ "Implement: missing dev or qe agent config",
3991
+ options?.stage ?? "implement",
3992
+ { agent: "implement" }
3993
+ );
3873
3994
  throw new TaskError("Implement: missing dev or qe agent config");
3874
3995
  }
3875
3996
  const agentsToRun = retryOptions?.agentsToRun ?? ["dev", "qe"];
@@ -3908,9 +4029,12 @@ async function runImplement(spec, plan, options, retryOptions) {
3908
4029
  if (devResult.value.exitCode === 0) {
3909
4030
  dev = extractDevOutput(devResult.value.stdout);
3910
4031
  } else {
3911
- console.log(chalk13.red("dev agent failed:"), `exit code ${devResult.value.exitCode}`);
3912
- const summary = getFailureSummary(devResult.value.stdout);
3913
- if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4032
+ const detail = formatExitDetail(devResult.value, devAgent.model);
4033
+ console.log(chalk13.red("dev agent failed:"), `exit code ${devResult.value.exitCode}${detail}`);
4034
+ if (!devResult.value.errorMessage || devResult.value.errorMessage.length < 50) {
4035
+ const summary = getFailureSummary(devResult.value.stdout);
4036
+ if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4037
+ }
3914
4038
  }
3915
4039
  } else {
3916
4040
  console.log(chalk13.red("dev agent failed:"), devResult.reason);
@@ -3923,9 +4047,12 @@ async function runImplement(spec, plan, options, retryOptions) {
3923
4047
  if (qeResult.value.exitCode === 0) {
3924
4048
  qe = extractQEOutput(qeResult.value.stdout);
3925
4049
  } else {
3926
- console.log(chalk13.red("qe agent failed:"), `exit code ${qeResult.value.exitCode}`);
3927
- const summary = getFailureSummary(qeResult.value.stdout);
3928
- if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4050
+ const detail = formatExitDetail(qeResult.value, qeAgent.model);
4051
+ console.log(chalk13.red("qe agent failed:"), `exit code ${qeResult.value.exitCode}${detail}`);
4052
+ if (!qeResult.value.errorMessage || qeResult.value.errorMessage.length < 50) {
4053
+ const summary = getFailureSummary(qeResult.value.stdout);
4054
+ if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4055
+ }
3929
4056
  }
3930
4057
  } else {
3931
4058
  console.log(chalk13.red("qe agent failed:"), qeResult.reason);
@@ -3941,9 +4068,12 @@ async function runImplement(spec, plan, options, retryOptions) {
3941
4068
  }
3942
4069
  usages.push({ agent: "dev", usage: devResult.usage });
3943
4070
  if (devResult.exitCode !== 0) {
3944
- console.log(chalk13.red("dev agent failed:"), `exit code ${devResult.exitCode}`);
3945
- const summary = getFailureSummary(devResult.stdout);
3946
- if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4071
+ const detail = formatExitDetail(devResult);
4072
+ console.log(chalk13.red("dev agent failed:"), `exit code ${devResult.exitCode}${detail}`);
4073
+ if (!devResult.errorMessage) {
4074
+ const summary = getFailureSummary(devResult.stdout);
4075
+ if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4076
+ }
3947
4077
  }
3948
4078
  } catch (err) {
3949
4079
  console.log(chalk13.red("dev agent failed:"), err);
@@ -3958,9 +4088,12 @@ async function runImplement(spec, plan, options, retryOptions) {
3958
4088
  }
3959
4089
  usages.push({ agent: "qe", usage: qeResult.usage });
3960
4090
  if (qeResult.exitCode !== 0) {
3961
- console.log(chalk13.red("qe agent failed:"), `exit code ${qeResult.exitCode}`);
3962
- const summary = getFailureSummary(qeResult.stdout);
3963
- if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4091
+ const detail = formatExitDetail(qeResult);
4092
+ console.log(chalk13.red("qe agent failed:"), `exit code ${qeResult.exitCode}${detail}`);
4093
+ if (!qeResult.errorMessage) {
4094
+ const summary = getFailureSummary(qeResult.stdout);
4095
+ if (summary) console.log(chalk13.gray(" \u21B3"), chalk13.gray(summary));
4096
+ }
3964
4097
  }
3965
4098
  } catch (err) {
3966
4099
  console.log(chalk13.red("qe agent failed:"), err);
@@ -3969,36 +4102,27 @@ async function runImplement(spec, plan, options, retryOptions) {
3969
4102
  }
3970
4103
  const chesstrace = getChesstrace();
3971
4104
  if (runDev && dev === null && (runQE && qe === null)) {
3972
- if (chesstrace) {
3973
- chesstrace.emit(Events.ERROR_TASK, {
3974
- type: "TaskError",
3975
- message: "Implement: all requested agents failed",
3976
- stage: options?.stage ?? "implement",
3977
- agent: "implement"
3978
- });
3979
- }
4105
+ emitErrorTask(
4106
+ "Implement: all requested agents failed",
4107
+ options?.stage ?? "implement",
4108
+ { agent: "implement" }
4109
+ );
3980
4110
  throw new TaskError("Implement: all requested agents failed");
3981
4111
  }
3982
4112
  if (runDev && !runQE && dev === null) {
3983
- if (chesstrace) {
3984
- chesstrace.emit(Events.ERROR_TASK, {
3985
- type: "TaskError",
3986
- message: "Implement: dev agent failed",
3987
- stage: options?.stage ?? "implement",
3988
- agent: "dev"
3989
- });
3990
- }
4113
+ emitErrorTask(
4114
+ "Implement: dev agent failed",
4115
+ options?.stage ?? "implement",
4116
+ { agent: "dev" }
4117
+ );
3991
4118
  throw new TaskError("Implement: dev agent failed");
3992
4119
  }
3993
4120
  if (!runDev && runQE && qe === null) {
3994
- if (chesstrace) {
3995
- chesstrace.emit(Events.ERROR_TASK, {
3996
- type: "TaskError",
3997
- message: "Implement: qe agent failed",
3998
- stage: options?.stage ?? "implement",
3999
- agent: "qe"
4000
- });
4001
- }
4121
+ emitErrorTask(
4122
+ "Implement: qe agent failed",
4123
+ options?.stage ?? "implement",
4124
+ { agent: "qe" }
4125
+ );
4002
4126
  throw new TaskError("Implement: qe agent failed");
4003
4127
  }
4004
4128
  return { implement: { dev, qe }, usages };
@@ -8518,21 +8642,15 @@ ${extra.trim()}` : extra.trim();
8518
8642
  console.log();
8519
8643
  const pushSpinner = createLiveStatus("committing and pushing...");
8520
8644
  const trace = getChesstrace();
8645
+ const commitMessage = "fix: address PR review comments";
8646
+ const maxRetries = options.retryCommits ?? 3;
8647
+ let committed = false;
8521
8648
  try {
8522
8649
  await exec4("git", ["add", "-A"]);
8523
- await exec4("git", ["commit", "-m", "fix: address PR review comments"]);
8524
- try {
8525
- trace.emit(Events.GIT_COMMIT, { branch, messageSubject: "fix: address PR review comments" });
8526
- } catch {
8527
- }
8528
- await exec4("git", ["push", "origin", branch]);
8529
- try {
8530
- trace.emit(Events.GIT_PUSH, { branch });
8531
- } catch {
8532
- }
8533
- pushSpinner.succeed(chalk22.green("Changes committed and pushed."));
8534
- } catch (pushErr) {
8535
- const msg = pushErr instanceof Error ? pushErr.message : String(pushErr);
8650
+ await exec4("git", ["commit", "-m", commitMessage]);
8651
+ committed = true;
8652
+ } catch (commitErr) {
8653
+ const msg = commitErr instanceof Error ? commitErr.message : String(commitErr);
8536
8654
  if (msg.includes("nothing to commit")) {
8537
8655
  try {
8538
8656
  const ahead = await exec4("git", [
@@ -8554,11 +8672,47 @@ ${extra.trim()}` : extra.trim();
8554
8672
  pushSpinner.warn(chalk22.yellow("Nothing to commit or push."));
8555
8673
  }
8556
8674
  } else {
8675
+ let retryCount = 0;
8676
+ while (retryCount < maxRetries) {
8677
+ retryCount++;
8678
+ pushSpinner.text = `pre-commit hook failed, re-staging and retrying (${retryCount}/${maxRetries})...`;
8679
+ try {
8680
+ await exec4("git", ["add", "-A"]);
8681
+ await exec4("git", ["commit", "-m", commitMessage]);
8682
+ committed = true;
8683
+ break;
8684
+ } catch (retryErr) {
8685
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
8686
+ if (retryCount >= maxRetries) {
8687
+ try {
8688
+ trace.emit(Events.GIT_ERROR, { operation: "commit", error: retryMsg, retriesExhausted: maxRetries });
8689
+ } catch {
8690
+ }
8691
+ pushSpinner.fail(chalk22.red(`Commit failed after ${maxRetries} retries: ${retryMsg}`));
8692
+ }
8693
+ }
8694
+ }
8695
+ }
8696
+ }
8697
+ if (committed) {
8698
+ try {
8699
+ trace.emit(Events.GIT_COMMIT, { branch, messageSubject: commitMessage });
8700
+ } catch {
8701
+ }
8702
+ try {
8703
+ await exec4("git", ["push", "origin", branch]);
8557
8704
  try {
8558
- trace.emit(Events.GIT_ERROR, { operation: "commit-push", error: msg });
8705
+ trace.emit(Events.GIT_PUSH, { branch });
8559
8706
  } catch {
8560
8707
  }
8561
- pushSpinner.fail(chalk22.red(`Push failed: ${msg}`));
8708
+ pushSpinner.succeed(chalk22.green("Changes committed and pushed."));
8709
+ } catch (pushErr) {
8710
+ const pushMsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
8711
+ try {
8712
+ trace.emit(Events.GIT_ERROR, { operation: "push", error: pushMsg });
8713
+ } catch {
8714
+ }
8715
+ pushSpinner.fail(chalk22.red(`Push failed: ${pushMsg}`));
8562
8716
  }
8563
8717
  }
8564
8718
  console.log();
@@ -10517,7 +10671,13 @@ program.command("run").description("Run the reygent workflow from spec to review
10517
10671
  }
10518
10672
  }).action(runCommand);
10519
10673
  program.command("review-work").description("Review current branch and post summary to PR/MR").option("--spec <source>", "Spec source with provider prefix (jira:KEY, linear:ID, markdown:FILE) \u2014 file paths auto-infer markdown:").option("--insecure", "Skip SSL certificate verification for API calls", false).action(reviewWorkCommand);
10520
- program.command("review-comments").description("Fetch PR/MR review comments and address them with an agent").option("--insecure", "Skip SSL certificate verification for API calls", false).option("--auto-approve", "Auto-approve plan and execute without prompting", false).action(reviewCommentsCommand);
10674
+ program.command("review-comments").description("Fetch PR/MR review comments and address them with an agent").option("--insecure", "Skip SSL certificate verification for API calls", false).option("--auto-approve", "Auto-approve plan and execute without prompting", false).option("--retry-commits <count>", "Max retries for pre-commit hook failures (default: 3)", (val) => {
10675
+ const num = parseInt(val, 10);
10676
+ if (isNaN(num) || num < 0 || num > 10) {
10677
+ throw new Error("--retry-commits must be between 0 and 10");
10678
+ }
10679
+ return num;
10680
+ }).action(reviewCommentsCommand);
10521
10681
  program.command("config").description("Configure default provider, model, and per-agent overrides").action(configCommand);
10522
10682
  registerTelemetryCommand(program);
10523
10683
  registerAnalyzeCommand(program);