@controlvector/cv-agent 1.3.0 → 1.5.0

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/bundle.cjs CHANGED
@@ -960,7 +960,7 @@ var require_command = __commonJS({
960
960
  var EventEmitter = require("node:events").EventEmitter;
961
961
  var childProcess = require("node:child_process");
962
962
  var path = require("node:path");
963
- var fs2 = require("node:fs");
963
+ var fs3 = require("node:fs");
964
964
  var process3 = require("node:process");
965
965
  var { Argument: Argument2, humanReadableArgName } = require_argument();
966
966
  var { CommanderError: CommanderError2 } = require_error();
@@ -1893,10 +1893,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1893
1893
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1894
1894
  function findFile(baseDir, baseName) {
1895
1895
  const localBin = path.resolve(baseDir, baseName);
1896
- if (fs2.existsSync(localBin)) return localBin;
1896
+ if (fs3.existsSync(localBin)) return localBin;
1897
1897
  if (sourceExt.includes(path.extname(baseName))) return void 0;
1898
1898
  const foundExt = sourceExt.find(
1899
- (ext) => fs2.existsSync(`${localBin}${ext}`)
1899
+ (ext) => fs3.existsSync(`${localBin}${ext}`)
1900
1900
  );
1901
1901
  if (foundExt) return `${localBin}${foundExt}`;
1902
1902
  return void 0;
@@ -1908,7 +1908,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1908
1908
  if (this._scriptPath) {
1909
1909
  let resolvedScriptPath;
1910
1910
  try {
1911
- resolvedScriptPath = fs2.realpathSync(this._scriptPath);
1911
+ resolvedScriptPath = fs3.realpathSync(this._scriptPath);
1912
1912
  } catch (err) {
1913
1913
  resolvedScriptPath = this._scriptPath;
1914
1914
  }
@@ -3037,7 +3037,7 @@ var {
3037
3037
  } = import_index.default;
3038
3038
 
3039
3039
  // src/commands/agent.ts
3040
- var import_node_child_process2 = require("node:child_process");
3040
+ var import_node_child_process4 = require("node:child_process");
3041
3041
 
3042
3042
  // node_modules/chalk/source/vendor/ansi-styles/index.js
3043
3043
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -3697,12 +3697,13 @@ async function failTask(creds, executorId, taskId, error) {
3697
3697
  const res = await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/fail`, { error });
3698
3698
  if (!res.ok) throw new Error(`Fail failed: ${res.status}`);
3699
3699
  }
3700
- async function sendHeartbeat(creds, executorId, taskId, message) {
3701
- await apiCall(creds, "POST", `/api/v1/executors/${executorId}/heartbeat`).catch(() => {
3700
+ async function sendHeartbeat(creds, executorId, taskId, message, authStatus2) {
3701
+ const body = authStatus2 ? { auth_status: authStatus2 } : void 0;
3702
+ await apiCall(creds, "POST", `/api/v1/executors/${executorId}/heartbeat`, body).catch(() => {
3702
3703
  });
3703
3704
  if (taskId) {
3704
- const body = message ? { message, log_type: "heartbeat" } : void 0;
3705
- await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/heartbeat`, body).catch(() => {
3705
+ const taskBody = message ? { message, log_type: "heartbeat" } : void 0;
3706
+ await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/heartbeat`, taskBody).catch(() => {
3706
3707
  });
3707
3708
  }
3708
3709
  }
@@ -4067,7 +4068,366 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
4067
4068
  throw new Error("unreachable");
4068
4069
  }
4069
4070
 
4071
+ // src/utils/config.ts
4072
+ var import_fs2 = require("fs");
4073
+ var import_os2 = require("os");
4074
+ var import_path2 = require("path");
4075
+ function getConfigPath() {
4076
+ return (0, import_path2.join)((0, import_os2.homedir)(), ".config", "cva", "config.json");
4077
+ }
4078
+ async function readConfig() {
4079
+ try {
4080
+ const content = await import_fs2.promises.readFile(getConfigPath(), "utf-8");
4081
+ return JSON.parse(content);
4082
+ } catch {
4083
+ return {};
4084
+ }
4085
+ }
4086
+ async function writeConfig(config) {
4087
+ const configPath = getConfigPath();
4088
+ await import_fs2.promises.mkdir((0, import_path2.dirname)(configPath), { recursive: true });
4089
+ await import_fs2.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
4090
+ }
4091
+
4092
+ // src/commands/git-safety.ts
4093
+ var import_node_child_process2 = require("node:child_process");
4094
+ function git(cmd, cwd) {
4095
+ return (0, import_node_child_process2.execSync)(cmd, { cwd, encoding: "utf8", timeout: 3e4 }).trim();
4096
+ }
4097
+ function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
4098
+ const targetBranch = branch || "main";
4099
+ try {
4100
+ let statusOutput;
4101
+ try {
4102
+ statusOutput = git("git status --porcelain", workspaceRoot);
4103
+ } catch {
4104
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false, error: "git status failed" };
4105
+ }
4106
+ const lines = statusOutput.split("\n").filter(Boolean);
4107
+ if (lines.length === 0) {
4108
+ try {
4109
+ const unpushed = git(`git log origin/${targetBranch}..HEAD --oneline 2>/dev/null`, workspaceRoot);
4110
+ if (unpushed) {
4111
+ console.log(` [git-safety] Found unpushed commits \u2014 pushing now`);
4112
+ git(`git push origin ${targetBranch}`, workspaceRoot);
4113
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: true };
4114
+ }
4115
+ } catch {
4116
+ }
4117
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false };
4118
+ }
4119
+ let added = 0, modified = 0, deleted = 0;
4120
+ for (const line of lines) {
4121
+ const code = line.substring(0, 2);
4122
+ if (code.includes("?")) added++;
4123
+ else if (code.includes("D")) deleted++;
4124
+ else if (code.includes("M") || code.includes("A")) modified++;
4125
+ else added++;
4126
+ }
4127
+ console.log(
4128
+ ` [git-safety] ${lines.length} uncommitted changes (${added} new, ${modified} modified, ${deleted} deleted) \u2014 committing now`
4129
+ );
4130
+ git("git add -A", workspaceRoot);
4131
+ const shortId = taskId.substring(0, 8);
4132
+ const commitMsg = `task: ${taskTitle} [${shortId}]
4133
+
4134
+ Auto-committed by cv-agent git safety net.
4135
+ Task ID: ${taskId}
4136
+ Files: ${added} added, ${modified} modified, ${deleted} deleted`;
4137
+ let commitSha;
4138
+ try {
4139
+ const commitOutput = git(`git commit -m ${JSON.stringify(commitMsg)}`, workspaceRoot);
4140
+ const shaMatch = commitOutput.match(/\[[\w/]+ ([a-f0-9]+)\]/);
4141
+ commitSha = shaMatch ? shaMatch[1] : void 0;
4142
+ } catch (e) {
4143
+ if (!e.message?.includes("nothing to commit")) {
4144
+ return {
4145
+ hadChanges: true,
4146
+ filesAdded: added,
4147
+ filesModified: modified,
4148
+ filesDeleted: deleted,
4149
+ pushed: false,
4150
+ error: `Commit failed: ${e.message}`
4151
+ };
4152
+ }
4153
+ }
4154
+ try {
4155
+ git(`git push origin ${targetBranch}`, workspaceRoot);
4156
+ console.log(` [git-safety] Committed and pushed: ${commitSha || "ok"}`);
4157
+ } catch (pushErr) {
4158
+ console.log(` [git-safety] Push failed: ${pushErr.message}`);
4159
+ return {
4160
+ hadChanges: true,
4161
+ filesAdded: added,
4162
+ filesModified: modified,
4163
+ filesDeleted: deleted,
4164
+ commitSha,
4165
+ pushed: false,
4166
+ error: `Push failed: ${pushErr.message}`
4167
+ };
4168
+ }
4169
+ return {
4170
+ hadChanges: true,
4171
+ filesAdded: added,
4172
+ filesModified: modified,
4173
+ filesDeleted: deleted,
4174
+ commitSha,
4175
+ pushed: true
4176
+ };
4177
+ } catch (err) {
4178
+ return {
4179
+ hadChanges: false,
4180
+ filesAdded: 0,
4181
+ filesModified: 0,
4182
+ filesDeleted: 0,
4183
+ pushed: false,
4184
+ error: err.message
4185
+ };
4186
+ }
4187
+ }
4188
+
4189
+ // src/commands/deploy-manifest.ts
4190
+ var import_node_child_process3 = require("node:child_process");
4191
+ var import_node_fs = require("node:fs");
4192
+ var import_node_path = require("node:path");
4193
+ function exec(cmd, cwd, timeoutMs = 3e5, env2) {
4194
+ try {
4195
+ const stdout = (0, import_node_child_process3.execSync)(cmd, {
4196
+ cwd,
4197
+ encoding: "utf8",
4198
+ timeout: timeoutMs,
4199
+ env: env2 ? { ...process.env, ...env2 } : void 0,
4200
+ stdio: ["pipe", "pipe", "pipe"]
4201
+ });
4202
+ return { ok: true, stdout, stderr: "" };
4203
+ } catch (err) {
4204
+ return { ok: false, stdout: err.stdout || "", stderr: err.stderr || err.message || "" };
4205
+ }
4206
+ }
4207
+ async function httpStatus(url, timeoutMs = 1e4) {
4208
+ try {
4209
+ const controller = new AbortController();
4210
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4211
+ const res = await fetch(url, { signal: controller.signal });
4212
+ clearTimeout(timer);
4213
+ return res.status;
4214
+ } catch {
4215
+ return 0;
4216
+ }
4217
+ }
4218
+ async function checkHealth(url, timeoutSeconds) {
4219
+ const deadline = Date.now() + timeoutSeconds * 1e3;
4220
+ while (Date.now() < deadline) {
4221
+ const status = await httpStatus(url);
4222
+ if (status >= 200 && status < 500) return true;
4223
+ await new Promise((r) => setTimeout(r, 2e3));
4224
+ }
4225
+ return false;
4226
+ }
4227
+ function getHeadCommit(cwd) {
4228
+ try {
4229
+ return (0, import_node_child_process3.execSync)("git rev-parse HEAD", { cwd, encoding: "utf8", timeout: 5e3 }).trim();
4230
+ } catch {
4231
+ return null;
4232
+ }
4233
+ }
4234
+ function loadDeployManifest(workspaceRoot) {
4235
+ const manifestPath = (0, import_node_path.join)(workspaceRoot, ".cva", "deploy.json");
4236
+ if (!(0, import_node_fs.existsSync)(manifestPath)) return null;
4237
+ try {
4238
+ const raw = (0, import_node_fs.readFileSync)(manifestPath, "utf8");
4239
+ const manifest = JSON.parse(raw);
4240
+ if (!manifest.version || manifest.version < 1) {
4241
+ console.log(" [deploy] Invalid manifest version");
4242
+ return null;
4243
+ }
4244
+ return manifest;
4245
+ } catch (err) {
4246
+ console.log(` [deploy] Failed to read .cva/deploy.json: ${err.message}`);
4247
+ return null;
4248
+ }
4249
+ }
4250
+ async function postTaskDeploy(workspaceRoot, taskId, log) {
4251
+ const manifest = loadDeployManifest(workspaceRoot);
4252
+ if (!manifest) {
4253
+ return { deployed: false, steps: [{ step: "manifest", status: "skipped", message: "No .cva/deploy.json" }] };
4254
+ }
4255
+ const steps = [];
4256
+ const preDeployCommit = getHeadCommit(workspaceRoot);
4257
+ if (manifest.build?.steps) {
4258
+ for (const buildStep of manifest.build.steps) {
4259
+ const stepName = `build:${buildStep.name}`;
4260
+ console.log(` [deploy] Building: ${buildStep.name}`);
4261
+ log("lifecycle", `Building: ${buildStep.name}`);
4262
+ const start = Date.now();
4263
+ const cwd = (0, import_node_path.join)(workspaceRoot, buildStep.working_dir || ".");
4264
+ const timeoutMs = (buildStep.timeout_seconds || 300) * 1e3;
4265
+ const result = exec(buildStep.command, cwd, timeoutMs);
4266
+ if (!result.ok) {
4267
+ const msg = `Build failed: ${buildStep.name}
4268
+ ${result.stderr.slice(-500)}`;
4269
+ steps.push({ step: stepName, status: "failed", message: msg, durationMs: Date.now() - start });
4270
+ log("error", msg);
4271
+ return { deployed: false, steps, error: msg };
4272
+ }
4273
+ steps.push({ step: stepName, status: "ok", durationMs: Date.now() - start });
4274
+ }
4275
+ }
4276
+ if (manifest.migrate?.command) {
4277
+ console.log(" [deploy] Running migration...");
4278
+ log("lifecycle", "Applying database migration");
4279
+ const start = Date.now();
4280
+ const env2 = manifest.migrate.env || {};
4281
+ const result = exec(manifest.migrate.command, workspaceRoot, 6e4, env2);
4282
+ if (!result.ok) {
4283
+ const msg = `Migration failed:
4284
+ ${result.stderr.slice(-500)}`;
4285
+ steps.push({ step: "migrate", status: "failed", message: msg, durationMs: Date.now() - start });
4286
+ log("error", msg);
4287
+ return { deployed: false, steps, error: msg };
4288
+ }
4289
+ steps.push({ step: "migrate", status: "ok", durationMs: Date.now() - start });
4290
+ }
4291
+ if (manifest.service && manifest.service.type !== "none" && manifest.service.restart_command) {
4292
+ console.log(` [deploy] Restarting service: ${manifest.service.name || "default"}`);
4293
+ log("lifecycle", `Restarting service: ${manifest.service.name || "default"}`);
4294
+ const start = Date.now();
4295
+ const result = exec(manifest.service.restart_command, workspaceRoot, 3e4);
4296
+ if (!result.ok) {
4297
+ const msg = `Restart failed:
4298
+ ${result.stderr.slice(-300)}`;
4299
+ steps.push({ step: "restart", status: "failed", message: msg, durationMs: Date.now() - start });
4300
+ } else {
4301
+ steps.push({ step: "restart", status: "ok", durationMs: Date.now() - start });
4302
+ }
4303
+ const waitMs = (manifest.service.startup_wait_seconds || 5) * 1e3;
4304
+ await new Promise((r) => setTimeout(r, waitMs));
4305
+ }
4306
+ if (manifest.verify) {
4307
+ console.log(" [deploy] Verifying deployment...");
4308
+ log("lifecycle", "Verifying deployment");
4309
+ const timeoutSec = manifest.verify.timeout_seconds || 30;
4310
+ if (manifest.verify.health_url) {
4311
+ const healthy = await checkHealth(manifest.verify.health_url, timeoutSec);
4312
+ if (!healthy) {
4313
+ const msg = `Health check failed: ${manifest.verify.health_url} did not respond within ${timeoutSec}s`;
4314
+ steps.push({ step: "verify:health", status: "failed", message: msg });
4315
+ log("error", msg);
4316
+ if (manifest.rollback?.auto_rollback_on_verify_failure && preDeployCommit) {
4317
+ await rollback(workspaceRoot, preDeployCommit, manifest, log);
4318
+ return { deployed: false, steps, error: msg, rolledBack: true };
4319
+ }
4320
+ return { deployed: false, steps, error: msg };
4321
+ }
4322
+ steps.push({ step: "verify:health", status: "ok" });
4323
+ }
4324
+ for (const test of manifest.verify.smoke_tests || []) {
4325
+ const status = await httpStatus(test.url);
4326
+ if (!test.expected_status.includes(status)) {
4327
+ const msg = `Smoke test "${test.name}" failed: got ${status}, expected ${test.expected_status.join("|")}`;
4328
+ steps.push({ step: `verify:${test.name}`, status: "failed", message: msg });
4329
+ log("error", msg);
4330
+ if (manifest.rollback?.auto_rollback_on_verify_failure && preDeployCommit) {
4331
+ await rollback(workspaceRoot, preDeployCommit, manifest, log);
4332
+ return { deployed: false, steps, error: msg, rolledBack: true };
4333
+ }
4334
+ return { deployed: false, steps, error: msg };
4335
+ }
4336
+ steps.push({ step: `verify:${test.name}`, status: "ok" });
4337
+ }
4338
+ }
4339
+ console.log(" [deploy] Deployment verified");
4340
+ log("lifecycle", "Deployment verified successfully");
4341
+ return { deployed: true, steps };
4342
+ }
4343
+ async function rollback(workspaceRoot, targetCommit, manifest, log) {
4344
+ console.log(` [deploy] Rolling back to ${targetCommit.substring(0, 8)}`);
4345
+ log("lifecycle", `Rolling back to ${targetCommit.substring(0, 8)}`);
4346
+ try {
4347
+ exec(`git checkout ${targetCommit}`, workspaceRoot, 1e4);
4348
+ } catch {
4349
+ log("error", "Rollback: git checkout failed");
4350
+ return;
4351
+ }
4352
+ if (manifest.build?.steps) {
4353
+ for (const step of manifest.build.steps) {
4354
+ exec(step.command, (0, import_node_path.join)(workspaceRoot, step.working_dir || "."), (step.timeout_seconds || 300) * 1e3);
4355
+ }
4356
+ }
4357
+ if (manifest.service?.restart_command) {
4358
+ exec(manifest.service.restart_command, workspaceRoot, 3e4);
4359
+ }
4360
+ log("lifecycle", "Rollback complete");
4361
+ }
4362
+
4070
4363
  // src/commands/agent.ts
4364
+ var AUTH_ERROR_PATTERNS = [
4365
+ "Not logged in",
4366
+ "Please run /login",
4367
+ "authentication required",
4368
+ "unauthorized",
4369
+ "expired token",
4370
+ "not authenticated",
4371
+ "login required"
4372
+ ];
4373
+ function containsAuthError(text) {
4374
+ const lower = text.toLowerCase();
4375
+ for (const pattern of AUTH_ERROR_PATTERNS) {
4376
+ if (lower.includes(pattern.toLowerCase())) {
4377
+ return pattern;
4378
+ }
4379
+ }
4380
+ return null;
4381
+ }
4382
+ async function checkClaudeAuth() {
4383
+ try {
4384
+ const output = (0, import_node_child_process4.execSync)("claude --version 2>&1", {
4385
+ encoding: "utf8",
4386
+ timeout: 1e4,
4387
+ env: { ...process.env }
4388
+ });
4389
+ const authError = containsAuthError(output);
4390
+ if (authError) {
4391
+ return { status: "expired", error: authError };
4392
+ }
4393
+ return { status: "authenticated" };
4394
+ } catch (err) {
4395
+ const output = (err.stdout || "") + (err.stderr || "") + (err.message || "");
4396
+ const authError = containsAuthError(output);
4397
+ if (authError) {
4398
+ return { status: "expired", error: authError };
4399
+ }
4400
+ return { status: "not_configured", error: output.slice(0, 500) };
4401
+ }
4402
+ }
4403
+ async function getClaudeEnv() {
4404
+ const env2 = { ...process.env };
4405
+ let usingApiKey = false;
4406
+ if (!env2.ANTHROPIC_API_KEY) {
4407
+ try {
4408
+ const config = await readConfig();
4409
+ if (config.anthropic_api_key) {
4410
+ env2.ANTHROPIC_API_KEY = config.anthropic_api_key;
4411
+ usingApiKey = true;
4412
+ }
4413
+ } catch {
4414
+ }
4415
+ }
4416
+ return { env: env2, usingApiKey };
4417
+ }
4418
+ function buildAuthFailureMessage(errorString, machineName, hasApiKeyFallback) {
4419
+ let msg = `CLAUDE_AUTH_REQUIRED: ${errorString}
4420
+ `;
4421
+ msg += `Machine: ${machineName}
4422
+ `;
4423
+ msg += `Fix: SSH into ${machineName} and run: claude /login
4424
+ `;
4425
+ if (!hasApiKeyFallback) {
4426
+ msg += `Alternative: Set an API key fallback with: cva auth set-api-key sk-ant-...
4427
+ `;
4428
+ }
4429
+ return msg;
4430
+ }
4071
4431
  function buildClaudePrompt(task) {
4072
4432
  let prompt = "";
4073
4433
  prompt += `You are executing a task dispatched via CV-Hub.
@@ -4232,14 +4592,16 @@ async function launchAutoApproveMode(prompt, options) {
4232
4592
  if (sessionId && !isContinue) {
4233
4593
  args.push("--session-id", sessionId);
4234
4594
  }
4235
- const child = (0, import_node_child_process2.spawn)("claude", args, {
4595
+ const child = (0, import_node_child_process4.spawn)("claude", args, {
4236
4596
  cwd: options.cwd,
4237
4597
  stdio: ["inherit", "pipe", "pipe"],
4238
- env: { ...process.env }
4598
+ env: options.spawnEnv || { ...process.env }
4239
4599
  });
4240
4600
  _activeChild = child;
4241
4601
  let stderr = "";
4242
4602
  let lineBuffer = "";
4603
+ let authFailure = false;
4604
+ const spawnTime = Date.now();
4243
4605
  child.stdout?.on("data", (data) => {
4244
4606
  const text = data.toString();
4245
4607
  process.stdout.write(data);
@@ -4247,6 +4609,23 @@ async function launchAutoApproveMode(prompt, options) {
4247
4609
  if (fullOutput.length > MAX_OUTPUT_BYTES) {
4248
4610
  fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
4249
4611
  }
4612
+ if (Date.now() - spawnTime < 1e4) {
4613
+ const authError = containsAuthError(fullOutput + stderr);
4614
+ if (authError) {
4615
+ authFailure = true;
4616
+ console.log(`
4617
+ ${source_default.red("!")} Claude Code auth failure detected: "${authError}"`);
4618
+ console.log(source_default.yellow(` Killing process \u2014 it won't recover without re-authentication.`));
4619
+ if (options.machineName) {
4620
+ console.log(source_default.cyan(` Fix: SSH into ${options.machineName} and run: claude /login`));
4621
+ }
4622
+ try {
4623
+ child.kill("SIGTERM");
4624
+ } catch {
4625
+ }
4626
+ return;
4627
+ }
4628
+ }
4250
4629
  if (options.creds && options.taskId) {
4251
4630
  lineBuffer += text;
4252
4631
  const lines = lineBuffer.split("\n");
@@ -4289,12 +4668,30 @@ async function launchAutoApproveMode(prompt, options) {
4289
4668
  }
4290
4669
  });
4291
4670
  child.stderr?.on("data", (data) => {
4292
- stderr += data.toString();
4671
+ const text = data.toString();
4672
+ stderr += text;
4293
4673
  process.stderr.write(data);
4674
+ if (Date.now() - spawnTime < 1e4 && !authFailure) {
4675
+ const authError = containsAuthError(text);
4676
+ if (authError) {
4677
+ authFailure = true;
4678
+ console.log(`
4679
+ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
4680
+ try {
4681
+ child.kill("SIGTERM");
4682
+ } catch {
4683
+ }
4684
+ }
4685
+ }
4294
4686
  });
4295
4687
  child.on("close", (code, signal) => {
4296
4688
  _activeChild = null;
4297
- resolve({ exitCode: signal === "SIGKILL" ? 137 : code ?? 1, stderr, output: fullOutput });
4689
+ resolve({
4690
+ exitCode: signal === "SIGKILL" ? 137 : code ?? 1,
4691
+ stderr,
4692
+ output: fullOutput,
4693
+ authFailure
4694
+ });
4298
4695
  });
4299
4696
  child.on("error", (err) => {
4300
4697
  _activeChild = null;
@@ -4329,16 +4726,18 @@ async function launchAutoApproveMode(prompt, options) {
4329
4726
  }
4330
4727
  async function launchRelayMode(prompt, options) {
4331
4728
  return new Promise((resolve, reject) => {
4332
- const child = (0, import_node_child_process2.spawn)("claude", [], {
4729
+ const child = (0, import_node_child_process4.spawn)("claude", [], {
4333
4730
  cwd: options.cwd,
4334
4731
  stdio: ["pipe", "pipe", "pipe"],
4335
- env: { ...process.env }
4732
+ env: options.spawnEnv || { ...process.env }
4336
4733
  });
4337
4734
  _activeChild = child;
4338
4735
  let stderr = "";
4339
4736
  let stdoutBuffer = "";
4340
4737
  let fullOutput = "";
4341
4738
  let lastProgressBytes = 0;
4739
+ let authFailure = false;
4740
+ const spawnTime = Date.now();
4342
4741
  child.stdin?.write(prompt + "\n");
4343
4742
  let lastRedirectCheck = Date.now();
4344
4743
  let lineBuffer = "";
@@ -4350,6 +4749,19 @@ async function launchRelayMode(prompt, options) {
4350
4749
  if (fullOutput.length > MAX_OUTPUT_BYTES) {
4351
4750
  fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
4352
4751
  }
4752
+ if (Date.now() - spawnTime < 1e4 && !authFailure) {
4753
+ const authError = containsAuthError(fullOutput + stderr);
4754
+ if (authError) {
4755
+ authFailure = true;
4756
+ console.log(`
4757
+ ${source_default.red("!")} Claude Code auth failure detected: "${authError}"`);
4758
+ try {
4759
+ child.kill("SIGTERM");
4760
+ } catch {
4761
+ }
4762
+ return;
4763
+ }
4764
+ }
4353
4765
  lineBuffer += text;
4354
4766
  const lines = lineBuffer.split("\n");
4355
4767
  lineBuffer = lines.pop() ?? "";
@@ -4490,13 +4902,25 @@ async function launchRelayMode(prompt, options) {
4490
4902
  const text = data.toString();
4491
4903
  stderr += text;
4492
4904
  process.stderr.write(data);
4905
+ if (Date.now() - spawnTime < 1e4 && !authFailure) {
4906
+ const authError = containsAuthError(text);
4907
+ if (authError) {
4908
+ authFailure = true;
4909
+ console.log(`
4910
+ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
4911
+ try {
4912
+ child.kill("SIGTERM");
4913
+ } catch {
4914
+ }
4915
+ }
4916
+ }
4493
4917
  });
4494
4918
  child.on("close", (code, signal) => {
4495
4919
  _activeChild = null;
4496
4920
  if (signal === "SIGKILL") {
4497
- resolve({ exitCode: 137, stderr, output: fullOutput });
4921
+ resolve({ exitCode: 137, stderr, output: fullOutput, authFailure });
4498
4922
  } else {
4499
- resolve({ exitCode: code ?? 1, stderr, output: fullOutput });
4923
+ resolve({ exitCode: code ?? 1, stderr, output: fullOutput, authFailure });
4500
4924
  }
4501
4925
  });
4502
4926
  child.on("error", (err) => {
@@ -4528,19 +4952,19 @@ async function handleSelfUpdate(task, state, creds) {
4528
4952
  let output = "";
4529
4953
  if (source === "npm" || source.startsWith("npm:")) {
4530
4954
  const pkg = source === "npm" ? "@controlvector/cv-agent@latest" : source.replace("npm:", "");
4531
- output = (0, import_node_child_process2.execSync)(`npm install -g ${pkg} 2>&1`, { encoding: "utf8", timeout: 12e4 });
4955
+ output = (0, import_node_child_process4.execSync)(`npm install -g ${pkg} 2>&1`, { encoding: "utf8", timeout: 12e4 });
4532
4956
  } else if (source.startsWith("git:")) {
4533
4957
  const repoPath = source.replace("git:", "");
4534
- output = (0, import_node_child_process2.execSync)(`cd ${repoPath} && git pull && npm install && npm run build && npm link 2>&1`, {
4958
+ output = (0, import_node_child_process4.execSync)(`cd ${repoPath} && git pull && npm install && npm run build && npm link 2>&1`, {
4535
4959
  encoding: "utf8",
4536
4960
  timeout: 3e5
4537
4961
  });
4538
4962
  } else {
4539
- output = (0, import_node_child_process2.execSync)(`npm install -g @controlvector/cv-agent@latest 2>&1`, { encoding: "utf8", timeout: 12e4 });
4963
+ output = (0, import_node_child_process4.execSync)(`npm install -g @controlvector/cv-agent@latest 2>&1`, { encoding: "utf8", timeout: 12e4 });
4540
4964
  }
4541
4965
  let newVersion = "unknown";
4542
4966
  try {
4543
- newVersion = (0, import_node_child_process2.execSync)("cva --version 2>/dev/null || echo unknown", { encoding: "utf8" }).trim();
4967
+ newVersion = (0, import_node_child_process4.execSync)("cva --version 2>/dev/null || echo unknown", { encoding: "utf8" }).trim();
4544
4968
  } catch {
4545
4969
  }
4546
4970
  postTaskEvent(creds, task.id, {
@@ -4566,7 +4990,7 @@ async function handleSelfUpdate(task, state, creds) {
4566
4990
  if (task.input?.constraints?.includes("restart")) {
4567
4991
  console.log(source_default.yellow("Restarting agent with updated binary..."));
4568
4992
  const args = process.argv.slice(1).join(" ");
4569
- (0, import_node_child_process2.execSync)(`nohup cva ${args} > /tmp/cva-restart.log 2>&1 &`, { stdio: "ignore" });
4993
+ (0, import_node_child_process4.execSync)(`nohup cva ${args} > /tmp/cva-restart.log 2>&1 &`, { stdio: "ignore" });
4570
4994
  process.exit(0);
4571
4995
  }
4572
4996
  } catch (err) {
@@ -4598,7 +5022,7 @@ async function runAgent(options) {
4598
5022
  process.exit(1);
4599
5023
  }
4600
5024
  try {
4601
- (0, import_node_child_process2.execSync)("claude --version", { stdio: "pipe", timeout: 5e3 });
5025
+ (0, import_node_child_process4.execSync)("claude --version", { stdio: "pipe", timeout: 5e3 });
4602
5026
  } catch {
4603
5027
  console.log();
4604
5028
  console.log(source_default.red("Claude Code CLI not found.") + " Install it first:");
@@ -4606,8 +5030,28 @@ async function runAgent(options) {
4606
5030
  console.log();
4607
5031
  process.exit(1);
4608
5032
  }
5033
+ const { env: claudeEnv, usingApiKey } = await getClaudeEnv();
5034
+ const authCheck = await checkClaudeAuth();
5035
+ let currentAuthStatus = authCheck.status;
5036
+ if (authCheck.status === "expired") {
5037
+ if (usingApiKey) {
5038
+ console.log(source_default.yellow("!") + " Claude Code OAuth expired, but API key fallback is configured.");
5039
+ currentAuthStatus = "api_key_fallback";
5040
+ } else {
5041
+ console.log();
5042
+ console.log(source_default.red("Claude Code auth expired: ") + source_default.yellow(authCheck.error || "unknown"));
5043
+ console.log(` Fix: Run ${source_default.cyan("claude /login")} to re-authenticate.`);
5044
+ console.log(` Alternative: ${source_default.cyan("cva auth set-api-key sk-ant-...")} for API key fallback.`);
5045
+ console.log();
5046
+ console.log(source_default.gray("Agent will start but pause task claims until auth is resolved."));
5047
+ console.log();
5048
+ }
5049
+ } else if (usingApiKey) {
5050
+ console.log(source_default.gray(" Using Anthropic API key from config as fallback."));
5051
+ currentAuthStatus = "api_key_fallback";
5052
+ }
4609
5053
  try {
4610
- (0, import_node_child_process2.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
5054
+ (0, import_node_child_process4.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
4611
5055
  } catch {
4612
5056
  console.log(source_default.yellow("!") + " cv-git CLI not found. Claude Code will fall back to raw git commands.");
4613
5057
  console.log(` Install it: ${source_default.cyan("npm install -g @controlvector/cv-git")}`);
@@ -4627,7 +5071,7 @@ async function runAgent(options) {
4627
5071
  }
4628
5072
  let detectedRepoId;
4629
5073
  try {
4630
- const remoteUrl = (0, import_node_child_process2.execSync)("git remote get-url origin 2>/dev/null", {
5074
+ const remoteUrl = (0, import_node_child_process4.execSync)("git remote get-url origin 2>/dev/null", {
4631
5075
  cwd: workingDir,
4632
5076
  encoding: "utf8",
4633
5077
  timeout: 5e3
@@ -4672,7 +5116,9 @@ async function runAgent(options) {
4672
5116
  failedCount: 0,
4673
5117
  lastPoll: Date.now(),
4674
5118
  lastTaskEnd: Date.now(),
4675
- running: true
5119
+ running: true,
5120
+ authStatus: currentAuthStatus,
5121
+ machineName
4676
5122
  };
4677
5123
  installSignalHandlers(
4678
5124
  () => state,
@@ -4683,13 +5129,26 @@ async function runAgent(options) {
4683
5129
  );
4684
5130
  while (state.running) {
4685
5131
  try {
5132
+ if (state.authStatus === "expired") {
5133
+ const recheck = await checkClaudeAuth();
5134
+ if (recheck.status === "authenticated") {
5135
+ state.authStatus = "authenticated";
5136
+ console.log(`
5137
+ ${source_default.green("\u2713")} Claude Code auth restored. Resuming task claims.`);
5138
+ } else {
5139
+ sendHeartbeat(creds, state.executorId, void 0, void 0, state.authStatus).catch(() => {
5140
+ });
5141
+ await new Promise((r) => setTimeout(r, pollInterval));
5142
+ continue;
5143
+ }
5144
+ }
4686
5145
  const task = await withRetry(
4687
5146
  () => pollForTask(creds, state.executorId),
4688
5147
  "Task poll"
4689
5148
  );
4690
5149
  state.lastPoll = Date.now();
4691
5150
  if (task) {
4692
- await executeTask(task, state, creds, options);
5151
+ await executeTask(task, state, creds, options, claudeEnv);
4693
5152
  } else {
4694
5153
  updateStatusLine(
4695
5154
  formatDuration(Date.now() - state.lastTaskEnd),
@@ -4705,7 +5164,7 @@ ${source_default.red("!")} Error: ${err.message}`);
4705
5164
  await new Promise((r) => setTimeout(r, pollInterval));
4706
5165
  }
4707
5166
  }
4708
- async function executeTask(task, state, creds, options) {
5167
+ async function executeTask(task, state, creds, options, claudeEnv) {
4709
5168
  const startTime = Date.now();
4710
5169
  state.currentTaskId = task.id;
4711
5170
  if (task.task_type === "_system_update") {
@@ -4733,7 +5192,7 @@ async function executeTask(task, state, creds, options) {
4733
5192
  try {
4734
5193
  const elapsed = formatDuration(Date.now() - startTime);
4735
5194
  setTerminalTitle(`cva: ${task.title} (${elapsed})`);
4736
- await sendHeartbeat(creds, state.executorId, task.id, `Claude Code running (${elapsed} elapsed)`);
5195
+ await sendHeartbeat(creds, state.executorId, task.id, `Claude Code running (${elapsed} elapsed)`, state.authStatus);
4737
5196
  } catch {
4738
5197
  }
4739
5198
  }, 3e4);
@@ -4767,17 +5226,48 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4767
5226
  cwd: options.workingDir,
4768
5227
  creds,
4769
5228
  taskId: task.id,
4770
- executorId: state.executorId
5229
+ executorId: state.executorId,
5230
+ spawnEnv: claudeEnv,
5231
+ machineName: state.machineName
4771
5232
  });
4772
5233
  } else {
4773
5234
  result = await launchRelayMode(prompt, {
4774
5235
  cwd: options.workingDir,
4775
5236
  creds,
4776
5237
  executorId: state.executorId,
4777
- taskId: task.id
5238
+ taskId: task.id,
5239
+ spawnEnv: claudeEnv,
5240
+ machineName: state.machineName
4778
5241
  });
4779
5242
  }
4780
5243
  console.log(source_default.gray("\n" + "-".repeat(60)));
5244
+ if (result.exitCode === 0 || result.exitCode === 1) {
5245
+ try {
5246
+ const gitSafety = gitSafetyNet(
5247
+ options.workingDir,
5248
+ task.title,
5249
+ task.id,
5250
+ task.branch
5251
+ );
5252
+ if (gitSafety.hadChanges) {
5253
+ sendTaskLog(
5254
+ creds,
5255
+ state.executorId,
5256
+ task.id,
5257
+ "git",
5258
+ `Safety net: committed ${gitSafety.filesAdded + gitSafety.filesModified} files (${gitSafety.commitSha || "ok"})`,
5259
+ { added: gitSafety.filesAdded, modified: gitSafety.filesModified, deleted: gitSafety.filesDeleted }
5260
+ );
5261
+ } else if (gitSafety.pushed) {
5262
+ sendTaskLog(creds, state.executorId, task.id, "git", "Safety net: pushed unpushed commits");
5263
+ }
5264
+ if (gitSafety.error) {
5265
+ sendTaskLog(creds, state.executorId, task.id, "info", `Git safety warning: ${gitSafety.error}`);
5266
+ }
5267
+ } catch (gitErr) {
5268
+ sendTaskLog(creds, state.executorId, task.id, "info", `Git safety net error: ${gitErr.message}`);
5269
+ }
5270
+ }
4781
5271
  const postGitState = capturePostTaskState(options.workingDir, preGitState);
4782
5272
  const payload = buildCompletionPayload(result.exitCode, preGitState, postGitState, startTime, result.output);
4783
5273
  const elapsed = formatDuration(Date.now() - startTime);
@@ -4786,6 +5276,28 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4786
5276
  ...postGitState.filesModified,
4787
5277
  ...postGitState.filesDeleted
4788
5278
  ];
5279
+ let deployResult;
5280
+ if (result.exitCode === 0) {
5281
+ try {
5282
+ const logFn = (type, msg) => {
5283
+ sendTaskLog(creds, state.executorId, task.id, type, msg);
5284
+ };
5285
+ deployResult = await postTaskDeploy(options.workingDir, task.id, logFn);
5286
+ if (deployResult.deployed) {
5287
+ sendTaskLog(creds, state.executorId, task.id, "lifecycle", "Post-task deploy: verified");
5288
+ } else if (deployResult.error) {
5289
+ sendTaskLog(
5290
+ creds,
5291
+ state.executorId,
5292
+ task.id,
5293
+ "error",
5294
+ `Post-task deploy failed: ${deployResult.error}${deployResult.rolledBack ? " (rolled back)" : ""}`
5295
+ );
5296
+ }
5297
+ } catch (deployErr) {
5298
+ sendTaskLog(creds, state.executorId, task.id, "info", `Deploy manifest error: ${deployErr.message}`);
5299
+ }
5300
+ }
4789
5301
  postTaskEvent(creds, task.id, {
4790
5302
  event_type: "completed",
4791
5303
  content: {
@@ -4849,6 +5361,31 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4849
5361
  } catch {
4850
5362
  }
4851
5363
  state.failedCount++;
5364
+ } else if (result.authFailure) {
5365
+ const authErrorStr = containsAuthError(result.output + result.stderr) || "auth failure";
5366
+ const hasApiKey = !!claudeEnv?.ANTHROPIC_API_KEY;
5367
+ const authMsg = buildAuthFailureMessage(authErrorStr, state.machineName, hasApiKey);
5368
+ sendTaskLog(creds, state.executorId, task.id, "error", authMsg);
5369
+ console.log();
5370
+ console.log(`\u{1F511} ${source_default.bold.red("AUTH FAILED")} \u2014 Claude Code is not authenticated`);
5371
+ console.log(source_default.yellow(` ${authMsg.replace(/\n/g, "\n ")}`));
5372
+ postTaskEvent(creds, task.id, {
5373
+ event_type: "auth_failure",
5374
+ content: {
5375
+ error: authErrorStr,
5376
+ machine: state.machineName,
5377
+ fix_command: `claude /login`,
5378
+ api_key_configured: hasApiKey
5379
+ }
5380
+ }).catch(() => {
5381
+ });
5382
+ await withRetry(
5383
+ () => failTask(creds, state.executorId, task.id, authMsg),
5384
+ "Report auth failure"
5385
+ );
5386
+ state.failedCount++;
5387
+ state.authStatus = "expired";
5388
+ console.log(source_default.yellow(" Pausing task claims until auth is restored."));
4852
5389
  } else {
4853
5390
  const stderrTail = result.stderr.trim().slice(-500);
4854
5391
  sendTaskLog(
@@ -4978,16 +5515,40 @@ async function authStatus() {
4978
5515
  console.log(`Status: ${source_default.red("unreachable")} (${err.message})`);
4979
5516
  }
4980
5517
  }
5518
+ async function authSetApiKey(key) {
5519
+ if (!key.startsWith("sk-ant-")) {
5520
+ console.log(source_default.red("Invalid API key.") + " Anthropic API keys start with sk-ant-");
5521
+ process.exit(1);
5522
+ }
5523
+ const config = await readConfig();
5524
+ config.anthropic_api_key = key;
5525
+ await writeConfig(config);
5526
+ const masked = key.substring(0, 10) + "..." + key.slice(-4);
5527
+ console.log(source_default.green("API key saved") + ` (${masked})`);
5528
+ console.log(source_default.gray("This key will be used as fallback when Claude Code OAuth expires."));
5529
+ }
5530
+ async function authRemoveApiKey() {
5531
+ const config = await readConfig();
5532
+ if (!config.anthropic_api_key) {
5533
+ console.log(source_default.yellow("No API key configured."));
5534
+ return;
5535
+ }
5536
+ delete config.anthropic_api_key;
5537
+ await writeConfig(config);
5538
+ console.log(source_default.green("API key removed."));
5539
+ }
4981
5540
  function authCommand() {
4982
5541
  const cmd = new Command("auth");
4983
5542
  cmd.description("Manage CV-Hub authentication");
4984
5543
  cmd.command("login").description("Authenticate with CV-Hub using a PAT token").option("--token <token>", "PAT token (or enter interactively)").option("--api-url <url>", "CV-Hub API URL", "https://api.hub.controlvector.io").action(authLogin);
4985
5544
  cmd.command("status").description("Show current authentication status").action(authStatus);
5545
+ cmd.command("set-api-key").description("Set Anthropic API key as fallback for Claude Code OAuth").argument("<key>", "Anthropic API key (sk-ant-...)").action(authSetApiKey);
5546
+ cmd.command("remove-api-key").description("Remove stored Anthropic API key").action(authRemoveApiKey);
4986
5547
  return cmd;
4987
5548
  }
4988
5549
 
4989
5550
  // src/commands/remote.ts
4990
- var import_node_child_process3 = require("node:child_process");
5551
+ var import_node_child_process5 = require("node:child_process");
4991
5552
  function getGitHost(apiUrl) {
4992
5553
  return apiUrl.replace(/^https?:\/\//, "").replace(/^api\./, "git.");
4993
5554
  }
@@ -5002,16 +5563,16 @@ async function remoteAdd(ownerRepo) {
5002
5563
  }
5003
5564
  const remoteUrl = `https://${gitHost}/${owner}/${repo}.git`;
5004
5565
  try {
5005
- const existing = (0, import_node_child_process3.execSync)("git remote get-url cvhub", { encoding: "utf-8", timeout: 5e3 }).trim();
5566
+ const existing = (0, import_node_child_process5.execSync)("git remote get-url cvhub", { encoding: "utf-8", timeout: 5e3 }).trim();
5006
5567
  if (existing === remoteUrl) {
5007
5568
  console.log(source_default.gray(`Remote 'cvhub' already set to ${remoteUrl}`));
5008
5569
  return;
5009
5570
  }
5010
- (0, import_node_child_process3.execSync)(`git remote set-url cvhub ${remoteUrl}`, { timeout: 5e3 });
5571
+ (0, import_node_child_process5.execSync)(`git remote set-url cvhub ${remoteUrl}`, { timeout: 5e3 });
5011
5572
  console.log(source_default.green("Updated") + ` remote 'cvhub' -> ${remoteUrl}`);
5012
5573
  } catch {
5013
5574
  try {
5014
- (0, import_node_child_process3.execSync)(`git remote add cvhub ${remoteUrl}`, { timeout: 5e3 });
5575
+ (0, import_node_child_process5.execSync)(`git remote add cvhub ${remoteUrl}`, { timeout: 5e3 });
5015
5576
  console.log(source_default.green("Added") + ` remote 'cvhub' -> ${remoteUrl}`);
5016
5577
  } catch (err) {
5017
5578
  console.log(source_default.red("Failed to add remote:") + ` ${err.message}`);
@@ -5203,7 +5764,7 @@ function statusCommand() {
5203
5764
 
5204
5765
  // src/index.ts
5205
5766
  var program2 = new Command();
5206
- program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.3.0" : "1.1.0");
5767
+ program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.5.0" : "1.1.0");
5207
5768
  program2.addCommand(agentCommand());
5208
5769
  program2.addCommand(authCommand());
5209
5770
  program2.addCommand(remoteCommand());