@controlvector/cv-agent 1.4.0 → 1.6.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
@@ -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;
@@ -3643,7 +3643,7 @@ async function apiCall(creds, method, path, body) {
3643
3643
  body: body ? JSON.stringify(body) : void 0
3644
3644
  });
3645
3645
  }
3646
- async function registerExecutor(creds, machineName, workingDir, repositoryId) {
3646
+ async function registerExecutor(creds, machineName, workingDir, repositoryId, metadata) {
3647
3647
  const body = {
3648
3648
  name: `cva:${machineName}`,
3649
3649
  machine_name: machineName,
@@ -3657,6 +3657,11 @@ async function registerExecutor(creds, machineName, workingDir, repositoryId) {
3657
3657
  if (repositoryId) {
3658
3658
  body.repository_id = repositoryId;
3659
3659
  }
3660
+ if (metadata?.role) body.role = metadata.role;
3661
+ if (metadata?.dispatch_guard) body.dispatch_guard = metadata.dispatch_guard;
3662
+ if (metadata?.tags) body.tags = metadata.tags;
3663
+ if (metadata?.owner_project) body.owner_project = metadata.owner_project;
3664
+ if (metadata?.integration) body.integration = metadata.integration;
3660
3665
  const res = await apiCall(creds, "POST", "/api/v1/executors", body);
3661
3666
  if (!res.ok) {
3662
3667
  const err = await res.text();
@@ -4072,6 +4077,15 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
4072
4077
  var import_fs2 = require("fs");
4073
4078
  var import_os2 = require("os");
4074
4079
  var import_path2 = require("path");
4080
+ async function readWorkspaceConfig(workspaceRoot) {
4081
+ try {
4082
+ const { readFile } = await import("fs/promises");
4083
+ const content = await readFile((0, import_path2.join)(workspaceRoot, ".cva", "agent.json"), "utf-8");
4084
+ return JSON.parse(content);
4085
+ } catch {
4086
+ return {};
4087
+ }
4088
+ }
4075
4089
  function getConfigPath() {
4076
4090
  return (0, import_path2.join)((0, import_os2.homedir)(), ".config", "cva", "config.json");
4077
4091
  }
@@ -4089,6 +4103,277 @@ async function writeConfig(config) {
4089
4103
  await import_fs2.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
4090
4104
  }
4091
4105
 
4106
+ // src/commands/git-safety.ts
4107
+ var import_node_child_process2 = require("node:child_process");
4108
+ function git(cmd, cwd) {
4109
+ return (0, import_node_child_process2.execSync)(cmd, { cwd, encoding: "utf8", timeout: 3e4 }).trim();
4110
+ }
4111
+ function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
4112
+ const targetBranch = branch || "main";
4113
+ try {
4114
+ let statusOutput;
4115
+ try {
4116
+ statusOutput = git("git status --porcelain", workspaceRoot);
4117
+ } catch {
4118
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false, error: "git status failed" };
4119
+ }
4120
+ const lines = statusOutput.split("\n").filter(Boolean);
4121
+ if (lines.length === 0) {
4122
+ try {
4123
+ const unpushed = git(`git log origin/${targetBranch}..HEAD --oneline 2>/dev/null`, workspaceRoot);
4124
+ if (unpushed) {
4125
+ console.log(` [git-safety] Found unpushed commits \u2014 pushing now`);
4126
+ git(`git push origin ${targetBranch}`, workspaceRoot);
4127
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: true };
4128
+ }
4129
+ } catch {
4130
+ }
4131
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false };
4132
+ }
4133
+ let added = 0, modified = 0, deleted = 0;
4134
+ for (const line of lines) {
4135
+ const code = line.substring(0, 2);
4136
+ if (code.includes("?")) added++;
4137
+ else if (code.includes("D")) deleted++;
4138
+ else if (code.includes("M") || code.includes("A")) modified++;
4139
+ else added++;
4140
+ }
4141
+ console.log(
4142
+ ` [git-safety] ${lines.length} uncommitted changes (${added} new, ${modified} modified, ${deleted} deleted) \u2014 committing now`
4143
+ );
4144
+ git("git add -A", workspaceRoot);
4145
+ const shortId = taskId.substring(0, 8);
4146
+ const commitMsg = `task: ${taskTitle} [${shortId}]
4147
+
4148
+ Auto-committed by cv-agent git safety net.
4149
+ Task ID: ${taskId}
4150
+ Files: ${added} added, ${modified} modified, ${deleted} deleted`;
4151
+ let commitSha;
4152
+ try {
4153
+ const commitOutput = git(`git commit -m ${JSON.stringify(commitMsg)}`, workspaceRoot);
4154
+ const shaMatch = commitOutput.match(/\[[\w/]+ ([a-f0-9]+)\]/);
4155
+ commitSha = shaMatch ? shaMatch[1] : void 0;
4156
+ } catch (e) {
4157
+ if (!e.message?.includes("nothing to commit")) {
4158
+ return {
4159
+ hadChanges: true,
4160
+ filesAdded: added,
4161
+ filesModified: modified,
4162
+ filesDeleted: deleted,
4163
+ pushed: false,
4164
+ error: `Commit failed: ${e.message}`
4165
+ };
4166
+ }
4167
+ }
4168
+ try {
4169
+ git(`git push origin ${targetBranch}`, workspaceRoot);
4170
+ console.log(` [git-safety] Committed and pushed: ${commitSha || "ok"}`);
4171
+ } catch (pushErr) {
4172
+ console.log(` [git-safety] Push failed: ${pushErr.message}`);
4173
+ return {
4174
+ hadChanges: true,
4175
+ filesAdded: added,
4176
+ filesModified: modified,
4177
+ filesDeleted: deleted,
4178
+ commitSha,
4179
+ pushed: false,
4180
+ error: `Push failed: ${pushErr.message}`
4181
+ };
4182
+ }
4183
+ return {
4184
+ hadChanges: true,
4185
+ filesAdded: added,
4186
+ filesModified: modified,
4187
+ filesDeleted: deleted,
4188
+ commitSha,
4189
+ pushed: true
4190
+ };
4191
+ } catch (err) {
4192
+ return {
4193
+ hadChanges: false,
4194
+ filesAdded: 0,
4195
+ filesModified: 0,
4196
+ filesDeleted: 0,
4197
+ pushed: false,
4198
+ error: err.message
4199
+ };
4200
+ }
4201
+ }
4202
+
4203
+ // src/commands/deploy-manifest.ts
4204
+ var import_node_child_process3 = require("node:child_process");
4205
+ var import_node_fs = require("node:fs");
4206
+ var import_node_path = require("node:path");
4207
+ function exec(cmd, cwd, timeoutMs = 3e5, env2) {
4208
+ try {
4209
+ const stdout = (0, import_node_child_process3.execSync)(cmd, {
4210
+ cwd,
4211
+ encoding: "utf8",
4212
+ timeout: timeoutMs,
4213
+ env: env2 ? { ...process.env, ...env2 } : void 0,
4214
+ stdio: ["pipe", "pipe", "pipe"]
4215
+ });
4216
+ return { ok: true, stdout, stderr: "" };
4217
+ } catch (err) {
4218
+ return { ok: false, stdout: err.stdout || "", stderr: err.stderr || err.message || "" };
4219
+ }
4220
+ }
4221
+ async function httpStatus(url, timeoutMs = 1e4) {
4222
+ try {
4223
+ const controller = new AbortController();
4224
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4225
+ const res = await fetch(url, { signal: controller.signal });
4226
+ clearTimeout(timer);
4227
+ return res.status;
4228
+ } catch {
4229
+ return 0;
4230
+ }
4231
+ }
4232
+ async function checkHealth(url, timeoutSeconds) {
4233
+ const deadline = Date.now() + timeoutSeconds * 1e3;
4234
+ while (Date.now() < deadline) {
4235
+ const status = await httpStatus(url);
4236
+ if (status >= 200 && status < 500) return true;
4237
+ await new Promise((r) => setTimeout(r, 2e3));
4238
+ }
4239
+ return false;
4240
+ }
4241
+ function getHeadCommit(cwd) {
4242
+ try {
4243
+ return (0, import_node_child_process3.execSync)("git rev-parse HEAD", { cwd, encoding: "utf8", timeout: 5e3 }).trim();
4244
+ } catch {
4245
+ return null;
4246
+ }
4247
+ }
4248
+ function loadDeployManifest(workspaceRoot) {
4249
+ const manifestPath = (0, import_node_path.join)(workspaceRoot, ".cva", "deploy.json");
4250
+ if (!(0, import_node_fs.existsSync)(manifestPath)) return null;
4251
+ try {
4252
+ const raw = (0, import_node_fs.readFileSync)(manifestPath, "utf8");
4253
+ const manifest = JSON.parse(raw);
4254
+ if (!manifest.version || manifest.version < 1) {
4255
+ console.log(" [deploy] Invalid manifest version");
4256
+ return null;
4257
+ }
4258
+ return manifest;
4259
+ } catch (err) {
4260
+ console.log(` [deploy] Failed to read .cva/deploy.json: ${err.message}`);
4261
+ return null;
4262
+ }
4263
+ }
4264
+ async function postTaskDeploy(workspaceRoot, taskId, log) {
4265
+ const manifest = loadDeployManifest(workspaceRoot);
4266
+ if (!manifest) {
4267
+ return { deployed: false, steps: [{ step: "manifest", status: "skipped", message: "No .cva/deploy.json" }] };
4268
+ }
4269
+ const steps = [];
4270
+ const preDeployCommit = getHeadCommit(workspaceRoot);
4271
+ if (manifest.build?.steps) {
4272
+ for (const buildStep of manifest.build.steps) {
4273
+ const stepName = `build:${buildStep.name}`;
4274
+ console.log(` [deploy] Building: ${buildStep.name}`);
4275
+ log("lifecycle", `Building: ${buildStep.name}`);
4276
+ const start = Date.now();
4277
+ const cwd = (0, import_node_path.join)(workspaceRoot, buildStep.working_dir || ".");
4278
+ const timeoutMs = (buildStep.timeout_seconds || 300) * 1e3;
4279
+ const result = exec(buildStep.command, cwd, timeoutMs);
4280
+ if (!result.ok) {
4281
+ const msg = `Build failed: ${buildStep.name}
4282
+ ${result.stderr.slice(-500)}`;
4283
+ steps.push({ step: stepName, status: "failed", message: msg, durationMs: Date.now() - start });
4284
+ log("error", msg);
4285
+ return { deployed: false, steps, error: msg };
4286
+ }
4287
+ steps.push({ step: stepName, status: "ok", durationMs: Date.now() - start });
4288
+ }
4289
+ }
4290
+ if (manifest.migrate?.command) {
4291
+ console.log(" [deploy] Running migration...");
4292
+ log("lifecycle", "Applying database migration");
4293
+ const start = Date.now();
4294
+ const env2 = manifest.migrate.env || {};
4295
+ const result = exec(manifest.migrate.command, workspaceRoot, 6e4, env2);
4296
+ if (!result.ok) {
4297
+ const msg = `Migration failed:
4298
+ ${result.stderr.slice(-500)}`;
4299
+ steps.push({ step: "migrate", status: "failed", message: msg, durationMs: Date.now() - start });
4300
+ log("error", msg);
4301
+ return { deployed: false, steps, error: msg };
4302
+ }
4303
+ steps.push({ step: "migrate", status: "ok", durationMs: Date.now() - start });
4304
+ }
4305
+ if (manifest.service && manifest.service.type !== "none" && manifest.service.restart_command) {
4306
+ console.log(` [deploy] Restarting service: ${manifest.service.name || "default"}`);
4307
+ log("lifecycle", `Restarting service: ${manifest.service.name || "default"}`);
4308
+ const start = Date.now();
4309
+ const result = exec(manifest.service.restart_command, workspaceRoot, 3e4);
4310
+ if (!result.ok) {
4311
+ const msg = `Restart failed:
4312
+ ${result.stderr.slice(-300)}`;
4313
+ steps.push({ step: "restart", status: "failed", message: msg, durationMs: Date.now() - start });
4314
+ } else {
4315
+ steps.push({ step: "restart", status: "ok", durationMs: Date.now() - start });
4316
+ }
4317
+ const waitMs = (manifest.service.startup_wait_seconds || 5) * 1e3;
4318
+ await new Promise((r) => setTimeout(r, waitMs));
4319
+ }
4320
+ if (manifest.verify) {
4321
+ console.log(" [deploy] Verifying deployment...");
4322
+ log("lifecycle", "Verifying deployment");
4323
+ const timeoutSec = manifest.verify.timeout_seconds || 30;
4324
+ if (manifest.verify.health_url) {
4325
+ const healthy = await checkHealth(manifest.verify.health_url, timeoutSec);
4326
+ if (!healthy) {
4327
+ const msg = `Health check failed: ${manifest.verify.health_url} did not respond within ${timeoutSec}s`;
4328
+ steps.push({ step: "verify:health", 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:health", status: "ok" });
4337
+ }
4338
+ for (const test of manifest.verify.smoke_tests || []) {
4339
+ const status = await httpStatus(test.url);
4340
+ if (!test.expected_status.includes(status)) {
4341
+ const msg = `Smoke test "${test.name}" failed: got ${status}, expected ${test.expected_status.join("|")}`;
4342
+ steps.push({ step: `verify:${test.name}`, status: "failed", message: msg });
4343
+ log("error", msg);
4344
+ if (manifest.rollback?.auto_rollback_on_verify_failure && preDeployCommit) {
4345
+ await rollback(workspaceRoot, preDeployCommit, manifest, log);
4346
+ return { deployed: false, steps, error: msg, rolledBack: true };
4347
+ }
4348
+ return { deployed: false, steps, error: msg };
4349
+ }
4350
+ steps.push({ step: `verify:${test.name}`, status: "ok" });
4351
+ }
4352
+ }
4353
+ console.log(" [deploy] Deployment verified");
4354
+ log("lifecycle", "Deployment verified successfully");
4355
+ return { deployed: true, steps };
4356
+ }
4357
+ async function rollback(workspaceRoot, targetCommit, manifest, log) {
4358
+ console.log(` [deploy] Rolling back to ${targetCommit.substring(0, 8)}`);
4359
+ log("lifecycle", `Rolling back to ${targetCommit.substring(0, 8)}`);
4360
+ try {
4361
+ exec(`git checkout ${targetCommit}`, workspaceRoot, 1e4);
4362
+ } catch {
4363
+ log("error", "Rollback: git checkout failed");
4364
+ return;
4365
+ }
4366
+ if (manifest.build?.steps) {
4367
+ for (const step of manifest.build.steps) {
4368
+ exec(step.command, (0, import_node_path.join)(workspaceRoot, step.working_dir || "."), (step.timeout_seconds || 300) * 1e3);
4369
+ }
4370
+ }
4371
+ if (manifest.service?.restart_command) {
4372
+ exec(manifest.service.restart_command, workspaceRoot, 3e4);
4373
+ }
4374
+ log("lifecycle", "Rollback complete");
4375
+ }
4376
+
4092
4377
  // src/commands/agent.ts
4093
4378
  var AUTH_ERROR_PATTERNS = [
4094
4379
  "Not logged in",
@@ -4110,7 +4395,7 @@ function containsAuthError(text) {
4110
4395
  }
4111
4396
  async function checkClaudeAuth() {
4112
4397
  try {
4113
- const output = (0, import_node_child_process2.execSync)("claude --version 2>&1", {
4398
+ const output = (0, import_node_child_process4.execSync)("claude --version 2>&1", {
4114
4399
  encoding: "utf8",
4115
4400
  timeout: 1e4,
4116
4401
  env: { ...process.env }
@@ -4321,7 +4606,7 @@ async function launchAutoApproveMode(prompt, options) {
4321
4606
  if (sessionId && !isContinue) {
4322
4607
  args.push("--session-id", sessionId);
4323
4608
  }
4324
- const child = (0, import_node_child_process2.spawn)("claude", args, {
4609
+ const child = (0, import_node_child_process4.spawn)("claude", args, {
4325
4610
  cwd: options.cwd,
4326
4611
  stdio: ["inherit", "pipe", "pipe"],
4327
4612
  env: options.spawnEnv || { ...process.env }
@@ -4455,7 +4740,7 @@ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
4455
4740
  }
4456
4741
  async function launchRelayMode(prompt, options) {
4457
4742
  return new Promise((resolve, reject) => {
4458
- const child = (0, import_node_child_process2.spawn)("claude", [], {
4743
+ const child = (0, import_node_child_process4.spawn)("claude", [], {
4459
4744
  cwd: options.cwd,
4460
4745
  stdio: ["pipe", "pipe", "pipe"],
4461
4746
  env: options.spawnEnv || { ...process.env }
@@ -4681,19 +4966,19 @@ async function handleSelfUpdate(task, state, creds) {
4681
4966
  let output = "";
4682
4967
  if (source === "npm" || source.startsWith("npm:")) {
4683
4968
  const pkg = source === "npm" ? "@controlvector/cv-agent@latest" : source.replace("npm:", "");
4684
- output = (0, import_node_child_process2.execSync)(`npm install -g ${pkg} 2>&1`, { encoding: "utf8", timeout: 12e4 });
4969
+ output = (0, import_node_child_process4.execSync)(`npm install -g ${pkg} 2>&1`, { encoding: "utf8", timeout: 12e4 });
4685
4970
  } else if (source.startsWith("git:")) {
4686
4971
  const repoPath = source.replace("git:", "");
4687
- output = (0, import_node_child_process2.execSync)(`cd ${repoPath} && git pull && npm install && npm run build && npm link 2>&1`, {
4972
+ output = (0, import_node_child_process4.execSync)(`cd ${repoPath} && git pull && npm install && npm run build && npm link 2>&1`, {
4688
4973
  encoding: "utf8",
4689
4974
  timeout: 3e5
4690
4975
  });
4691
4976
  } else {
4692
- output = (0, import_node_child_process2.execSync)(`npm install -g @controlvector/cv-agent@latest 2>&1`, { encoding: "utf8", timeout: 12e4 });
4977
+ output = (0, import_node_child_process4.execSync)(`npm install -g @controlvector/cv-agent@latest 2>&1`, { encoding: "utf8", timeout: 12e4 });
4693
4978
  }
4694
4979
  let newVersion = "unknown";
4695
4980
  try {
4696
- newVersion = (0, import_node_child_process2.execSync)("cva --version 2>/dev/null || echo unknown", { encoding: "utf8" }).trim();
4981
+ newVersion = (0, import_node_child_process4.execSync)("cva --version 2>/dev/null || echo unknown", { encoding: "utf8" }).trim();
4697
4982
  } catch {
4698
4983
  }
4699
4984
  postTaskEvent(creds, task.id, {
@@ -4719,7 +5004,7 @@ async function handleSelfUpdate(task, state, creds) {
4719
5004
  if (task.input?.constraints?.includes("restart")) {
4720
5005
  console.log(source_default.yellow("Restarting agent with updated binary..."));
4721
5006
  const args = process.argv.slice(1).join(" ");
4722
- (0, import_node_child_process2.execSync)(`nohup cva ${args} > /tmp/cva-restart.log 2>&1 &`, { stdio: "ignore" });
5007
+ (0, import_node_child_process4.execSync)(`nohup cva ${args} > /tmp/cva-restart.log 2>&1 &`, { stdio: "ignore" });
4723
5008
  process.exit(0);
4724
5009
  }
4725
5010
  } catch (err) {
@@ -4751,7 +5036,7 @@ async function runAgent(options) {
4751
5036
  process.exit(1);
4752
5037
  }
4753
5038
  try {
4754
- (0, import_node_child_process2.execSync)("claude --version", { stdio: "pipe", timeout: 5e3 });
5039
+ (0, import_node_child_process4.execSync)("claude --version", { stdio: "pipe", timeout: 5e3 });
4755
5040
  } catch {
4756
5041
  console.log();
4757
5042
  console.log(source_default.red("Claude Code CLI not found.") + " Install it first:");
@@ -4780,7 +5065,7 @@ async function runAgent(options) {
4780
5065
  currentAuthStatus = "api_key_fallback";
4781
5066
  }
4782
5067
  try {
4783
- (0, import_node_child_process2.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
5068
+ (0, import_node_child_process4.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
4784
5069
  } catch {
4785
5070
  console.log(source_default.yellow("!") + " cv-git CLI not found. Claude Code will fall back to raw git commands.");
4786
5071
  console.log(` Install it: ${source_default.cyan("npm install -g @controlvector/cv-git")}`);
@@ -4800,7 +5085,7 @@ async function runAgent(options) {
4800
5085
  }
4801
5086
  let detectedRepoId;
4802
5087
  try {
4803
- const remoteUrl = (0, import_node_child_process2.execSync)("git remote get-url origin 2>/dev/null", {
5088
+ const remoteUrl = (0, import_node_child_process4.execSync)("git remote get-url origin 2>/dev/null", {
4804
5089
  cwd: workingDir,
4805
5090
  encoding: "utf8",
4806
5091
  timeout: 5e3
@@ -4821,8 +5106,47 @@ async function runAgent(options) {
4821
5106
  }
4822
5107
  } catch {
4823
5108
  }
5109
+ const wsConfig = await readWorkspaceConfig(workingDir);
5110
+ const globalConfig = await readConfig();
5111
+ const role = options.role || wsConfig.role || globalConfig.role || "development";
5112
+ const dispatchGuard = options.dispatchGuard || wsConfig.dispatch_guard || globalConfig.dispatch_guard || "open";
5113
+ const tags = (options.tags?.split(",") || wsConfig.tags || globalConfig.tags)?.map((t) => t.trim()).filter(Boolean);
5114
+ const ownerProject = options.ownerProject || wsConfig.owner_project || globalConfig.owner_project;
5115
+ let integration;
5116
+ if (options.integration || wsConfig.integration) {
5117
+ if (options.integration) {
5118
+ const safeTasks = options.safeTasks?.split(",").map((t) => t.trim()).filter(Boolean);
5119
+ const unsafeTasks = safeTasks ? ["code_change", "deploy", "test", "custom"].filter((t) => !safeTasks.includes(t)) : void 0;
5120
+ integration = {
5121
+ system: options.integration,
5122
+ description: options.integrationDescription || `Integrated into ${options.integration}`,
5123
+ service_port: options.integrationPort ? parseInt(options.integrationPort, 10) : void 0,
5124
+ safe_task_types: safeTasks,
5125
+ unsafe_task_types: unsafeTasks
5126
+ };
5127
+ } else if (wsConfig.integration) {
5128
+ integration = wsConfig.integration;
5129
+ }
5130
+ }
5131
+ const executorMeta = {
5132
+ role,
5133
+ dispatch_guard: dispatchGuard,
5134
+ tags,
5135
+ owner_project: ownerProject,
5136
+ integration
5137
+ };
5138
+ if (role !== "development") {
5139
+ console.log(source_default.yellow(` Role: ${role}`));
5140
+ }
5141
+ if (integration) {
5142
+ console.log(source_default.yellow(` Integration: ${integration.system}${integration.service_port ? ` (port ${integration.service_port})` : ""}`));
5143
+ }
5144
+ if (dispatchGuard !== "open") {
5145
+ const guardIcon = dispatchGuard === "locked" ? "\u{1F512}" : "\u26A0\uFE0F";
5146
+ console.log(source_default.yellow(` Guard: ${guardIcon} ${dispatchGuard}`));
5147
+ }
4824
5148
  const executor = await withRetry(
4825
- () => registerExecutor(creds, machineName, workingDir, detectedRepoId),
5149
+ () => registerExecutor(creds, machineName, workingDir, detectedRepoId, executorMeta),
4826
5150
  "Executor registration"
4827
5151
  );
4828
5152
  const mode = options.autoApprove ? "auto-approve" : "relay";
@@ -4970,6 +5294,33 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4970
5294
  });
4971
5295
  }
4972
5296
  console.log(source_default.gray("\n" + "-".repeat(60)));
5297
+ if (result.exitCode === 0 || result.exitCode === 1) {
5298
+ try {
5299
+ const gitSafety = gitSafetyNet(
5300
+ options.workingDir,
5301
+ task.title,
5302
+ task.id,
5303
+ task.branch
5304
+ );
5305
+ if (gitSafety.hadChanges) {
5306
+ sendTaskLog(
5307
+ creds,
5308
+ state.executorId,
5309
+ task.id,
5310
+ "git",
5311
+ `Safety net: committed ${gitSafety.filesAdded + gitSafety.filesModified} files (${gitSafety.commitSha || "ok"})`,
5312
+ { added: gitSafety.filesAdded, modified: gitSafety.filesModified, deleted: gitSafety.filesDeleted }
5313
+ );
5314
+ } else if (gitSafety.pushed) {
5315
+ sendTaskLog(creds, state.executorId, task.id, "git", "Safety net: pushed unpushed commits");
5316
+ }
5317
+ if (gitSafety.error) {
5318
+ sendTaskLog(creds, state.executorId, task.id, "info", `Git safety warning: ${gitSafety.error}`);
5319
+ }
5320
+ } catch (gitErr) {
5321
+ sendTaskLog(creds, state.executorId, task.id, "info", `Git safety net error: ${gitErr.message}`);
5322
+ }
5323
+ }
4973
5324
  const postGitState = capturePostTaskState(options.workingDir, preGitState);
4974
5325
  const payload = buildCompletionPayload(result.exitCode, preGitState, postGitState, startTime, result.output);
4975
5326
  const elapsed = formatDuration(Date.now() - startTime);
@@ -4978,6 +5329,28 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4978
5329
  ...postGitState.filesModified,
4979
5330
  ...postGitState.filesDeleted
4980
5331
  ];
5332
+ let deployResult;
5333
+ if (result.exitCode === 0) {
5334
+ try {
5335
+ const logFn = (type, msg) => {
5336
+ sendTaskLog(creds, state.executorId, task.id, type, msg);
5337
+ };
5338
+ deployResult = await postTaskDeploy(options.workingDir, task.id, logFn);
5339
+ if (deployResult.deployed) {
5340
+ sendTaskLog(creds, state.executorId, task.id, "lifecycle", "Post-task deploy: verified");
5341
+ } else if (deployResult.error) {
5342
+ sendTaskLog(
5343
+ creds,
5344
+ state.executorId,
5345
+ task.id,
5346
+ "error",
5347
+ `Post-task deploy failed: ${deployResult.error}${deployResult.rolledBack ? " (rolled back)" : ""}`
5348
+ );
5349
+ }
5350
+ } catch (deployErr) {
5351
+ sendTaskLog(creds, state.executorId, task.id, "info", `Deploy manifest error: ${deployErr.message}`);
5352
+ }
5353
+ }
4981
5354
  postTaskEvent(creds, task.id, {
4982
5355
  event_type: "completed",
4983
5356
  content: {
@@ -5121,6 +5494,14 @@ function agentCommand() {
5121
5494
  cmd.option("--poll-interval <seconds>", "How often to check for tasks, minimum 3 (default: 5)", "5");
5122
5495
  cmd.option("--working-dir <path>", "Working directory for Claude Code (default: current directory)", ".");
5123
5496
  cmd.option("--auto-approve", "Pre-approve all tool permissions (uses -p mode)", false);
5497
+ cmd.option("--role <role>", "Executor role: development, production, ci, staging");
5498
+ cmd.option("--integration <system>", "System this executor is integrated into (e.g. nyx-forge)");
5499
+ cmd.option("--integration-description <desc>", "Description of the integration");
5500
+ cmd.option("--integration-port <port>", "Port the integrated service runs on");
5501
+ cmd.option("--safe-tasks <types>", "Comma-separated task types safe for this executor (e.g. review,research)");
5502
+ cmd.option("--dispatch-guard <guard>", "Dispatch guard: open, confirm, locked (default: open)");
5503
+ cmd.option("--tags <tags>", "Comma-separated tags for this executor");
5504
+ cmd.option("--owner-project <project>", "Project this executor belongs to");
5124
5505
  cmd.action(async (opts) => {
5125
5506
  await runAgent(opts);
5126
5507
  });
@@ -5228,7 +5609,7 @@ function authCommand() {
5228
5609
  }
5229
5610
 
5230
5611
  // src/commands/remote.ts
5231
- var import_node_child_process3 = require("node:child_process");
5612
+ var import_node_child_process5 = require("node:child_process");
5232
5613
  function getGitHost(apiUrl) {
5233
5614
  return apiUrl.replace(/^https?:\/\//, "").replace(/^api\./, "git.");
5234
5615
  }
@@ -5243,16 +5624,16 @@ async function remoteAdd(ownerRepo) {
5243
5624
  }
5244
5625
  const remoteUrl = `https://${gitHost}/${owner}/${repo}.git`;
5245
5626
  try {
5246
- const existing = (0, import_node_child_process3.execSync)("git remote get-url cvhub", { encoding: "utf-8", timeout: 5e3 }).trim();
5627
+ const existing = (0, import_node_child_process5.execSync)("git remote get-url cvhub", { encoding: "utf-8", timeout: 5e3 }).trim();
5247
5628
  if (existing === remoteUrl) {
5248
5629
  console.log(source_default.gray(`Remote 'cvhub' already set to ${remoteUrl}`));
5249
5630
  return;
5250
5631
  }
5251
- (0, import_node_child_process3.execSync)(`git remote set-url cvhub ${remoteUrl}`, { timeout: 5e3 });
5632
+ (0, import_node_child_process5.execSync)(`git remote set-url cvhub ${remoteUrl}`, { timeout: 5e3 });
5252
5633
  console.log(source_default.green("Updated") + ` remote 'cvhub' -> ${remoteUrl}`);
5253
5634
  } catch {
5254
5635
  try {
5255
- (0, import_node_child_process3.execSync)(`git remote add cvhub ${remoteUrl}`, { timeout: 5e3 });
5636
+ (0, import_node_child_process5.execSync)(`git remote add cvhub ${remoteUrl}`, { timeout: 5e3 });
5256
5637
  console.log(source_default.green("Added") + ` remote 'cvhub' -> ${remoteUrl}`);
5257
5638
  } catch (err) {
5258
5639
  console.log(source_default.red("Failed to add remote:") + ` ${err.message}`);
@@ -5444,7 +5825,7 @@ function statusCommand() {
5444
5825
 
5445
5826
  // src/index.ts
5446
5827
  var program2 = new Command();
5447
- program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.4.0" : "1.1.0");
5828
+ program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.6.0" : "1.1.0");
5448
5829
  program2.addCommand(agentCommand());
5449
5830
  program2.addCommand(authCommand());
5450
5831
  program2.addCommand(remoteCommand());