@ai-development-environment/control-agent 0.0.114 → 0.0.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/control-agent.js +379 -43
  2. package/package.json +1 -1
@@ -5265,6 +5265,14 @@ var AgentGraphQLClient = class {
5265
5265
  { jobId, logs: [log2] }
5266
5266
  );
5267
5267
  }
5268
+ reportBranchDeletionResult(jobId, result) {
5269
+ return this.request(
5270
+ `mutation ReportBranchDeletionResult($jobId: ID!, $result: BranchDeletionResultInput!) {
5271
+ reportBranchDeletionResult(jobId: $jobId, result: $result) { id }
5272
+ }`,
5273
+ { jobId, result }
5274
+ );
5275
+ }
5268
5276
  appendCommandOutput(jobId, attemptId, chunks) {
5269
5277
  return this.request(
5270
5278
  `mutation AppendCommandRunOutput($jobId: ID!, $attemptId: ID!, $chunks: [CommandRunOutputInput!]!) {
@@ -6175,6 +6183,7 @@ var CODEBASE_CLONE_JOB_KIND = "codebase.clone";
6175
6183
  var CODEBASE_CLONE_INSPECT_JOB_KIND = "codebase.clone.inspect";
6176
6184
  var CODEBASE_GIT_INSPECT_JOB_KIND = "codebase.git.inspect";
6177
6185
  var CODEBASE_GIT_OPERATION_JOB_KIND = "codebase.git.operation";
6186
+ var CODEBASE_BRANCHES_DELETE_JOB_KIND = "codebase.branches.delete";
6178
6187
  var CODEBASE_RECONCILE_EVENT_CAPABILITY = "codebase.reconcile.requested";
6179
6188
  var MAX_CODEBASE_GIT_BRANCHES = 1e3;
6180
6189
  var MAX_CODEBASE_STASHES = 200;
@@ -6198,8 +6207,99 @@ var CODEBASE_JOB_KINDS = [
6198
6207
  CODEBASE_CLONE_JOB_KIND,
6199
6208
  CODEBASE_CLONE_INSPECT_JOB_KIND,
6200
6209
  CODEBASE_GIT_INSPECT_JOB_KIND,
6201
- CODEBASE_GIT_OPERATION_JOB_KIND
6210
+ CODEBASE_GIT_OPERATION_JOB_KIND,
6211
+ CODEBASE_BRANCHES_DELETE_JOB_KIND
6202
6212
  ];
6213
+ function branchHeadSha(value) {
6214
+ if (typeof value !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(value)) {
6215
+ throw new Error("Invalid branch tip SHA");
6216
+ }
6217
+ return value.toLowerCase();
6218
+ }
6219
+ function parseCodebaseBranchTarget(value) {
6220
+ const target2 = objectValue3(value, "branch target");
6221
+ const branch = stringValue3(target2.branch, "branch target.branch");
6222
+ if (!validCodebaseGitBranchName(branch))
6223
+ throw new Error("Invalid Git branch name");
6224
+ return {
6225
+ codebaseId: stringValue3(target2.codebaseId, "branch target.codebaseId"),
6226
+ branch,
6227
+ expectedHeadSha: branchHeadSha(target2.expectedHeadSha)
6228
+ };
6229
+ }
6230
+ function parseLocalBranchInventory(value) {
6231
+ const inventory = objectValue3(value, "local branch inventory");
6232
+ const scannedAt = stringValue3(inventory.scannedAt, "inventory.scannedAt");
6233
+ if (!Number.isFinite(Date.parse(scannedAt)) || !Array.isArray(inventory.branches)) {
6234
+ throw new Error("Invalid local branch inventory");
6235
+ }
6236
+ const names = /* @__PURE__ */ new Set();
6237
+ return {
6238
+ scannedAt: new Date(scannedAt).toISOString(),
6239
+ branches: inventory.branches.map((value2) => {
6240
+ const branch = objectValue3(value2, "local branch");
6241
+ const name = stringValue3(branch.name, "local branch.name");
6242
+ if (!validCodebaseGitBranchName(name) || names.has(name))
6243
+ throw new Error("Invalid or duplicate local branch");
6244
+ names.add(name);
6245
+ const lastCommitAt = nullableString2(
6246
+ branch.lastCommitAt,
6247
+ "local branch.lastCommitAt"
6248
+ );
6249
+ if (lastCommitAt && !Number.isFinite(Date.parse(lastCommitAt)))
6250
+ throw new Error("Invalid local commit date");
6251
+ return {
6252
+ name,
6253
+ headSha: branchHeadSha(branch.headSha),
6254
+ lastCommitAt,
6255
+ lastCommitMessage: nullableString2(
6256
+ branch.lastCommitMessage,
6257
+ "local branch.lastCommitMessage"
6258
+ ),
6259
+ current: booleanValue(branch.current, "local branch.current"),
6260
+ checkedOutPath: nullableString2(
6261
+ branch.checkedOutPath,
6262
+ "local branch.checkedOutPath"
6263
+ )
6264
+ };
6265
+ })
6266
+ };
6267
+ }
6268
+ function codebaseBranchesDeletePayload(value) {
6269
+ const payload = objectValue3(value, "local branch deletion payload");
6270
+ const allowed = /* @__PURE__ */ new Set([
6271
+ "codebaseId",
6272
+ "folder",
6273
+ "expectedOrigin",
6274
+ "defaultBranch",
6275
+ "force",
6276
+ "targets"
6277
+ ]);
6278
+ if (Object.keys(payload).some((key) => !allowed.has(key)))
6279
+ throw new Error("Unexpected local branch deletion field");
6280
+ const codebaseId = stringValue3(payload.codebaseId, "payload.codebaseId");
6281
+ if (!Array.isArray(payload.targets) || !payload.targets.length)
6282
+ throw new Error("Branch targets are required");
6283
+ const targets = payload.targets.map(parseCodebaseBranchTarget);
6284
+ if (targets.some((target2) => target2.codebaseId !== codebaseId))
6285
+ throw new Error("Branch target belongs to another codebase");
6286
+ if (new Set(targets.map((target2) => target2.branch)).size !== targets.length)
6287
+ throw new Error("Duplicate branch target");
6288
+ return {
6289
+ codebaseId,
6290
+ folder: stringValue3(payload.folder, "payload.folder"),
6291
+ expectedOrigin: stringValue3(
6292
+ payload.expectedOrigin,
6293
+ "payload.expectedOrigin"
6294
+ ),
6295
+ defaultBranch: nullableString2(
6296
+ payload.defaultBranch,
6297
+ "payload.defaultBranch"
6298
+ ),
6299
+ force: booleanValue(payload.force, "payload.force"),
6300
+ targets
6301
+ };
6302
+ }
6203
6303
  function objectValue3(value, name) {
6204
6304
  if (!value || typeof value !== "object" || Array.isArray(value)) {
6205
6305
  throw new Error(`${name} must be an object`);
@@ -9964,46 +10064,82 @@ var runCommand = async (rawPayload, _timeoutMs, signal, _onLog, context) => {
9964
10064
  }
9965
10065
  };
9966
10066
 
9967
- // src/handlers/codebase-clone.ts
9968
- import {
9969
- lstat,
9970
- mkdir as mkdir2,
9971
- mkdtemp as mkdtemp2,
9972
- realpath as realpath3,
9973
- rename as rename2,
9974
- rm as rm3,
9975
- rmdir
9976
- } from "node:fs/promises";
9977
- import {
9978
- dirname as dirname3,
9979
- isAbsolute as isAbsolute2,
9980
- join as join4,
9981
- parse,
9982
- relative as relative2,
9983
- resolve as resolve3,
9984
- sep as sep2
9985
- } from "node:path";
9986
-
9987
- // src/repository-coordinator.ts
9988
- var RepositoryCoordinator = class {
9989
- tails = /* @__PURE__ */ new Map();
9990
- async run(key, task) {
9991
- const previous = this.tails.get(key) ?? Promise.resolve();
9992
- let release2;
9993
- const current = new Promise((resolve11) => {
9994
- release2 = resolve11;
9995
- });
9996
- const tail = previous.then(() => current);
9997
- this.tails.set(key, tail);
9998
- await previous;
9999
- try {
10000
- return await task();
10001
- } finally {
10002
- release2();
10003
- if (this.tails.get(key) === tail) this.tails.delete(key);
10004
- }
10067
+ // src/handlers/local-branches.ts
10068
+ var branchError = (error) => (error instanceof Error ? error.message : String(error)).replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1").slice(0, 2e3);
10069
+ async function branchGit(folder, args, timeoutMs, signal) {
10070
+ const result = await captureCommand({
10071
+ command: "git",
10072
+ args: ["-C", folder, ...args],
10073
+ timeoutMs,
10074
+ signal,
10075
+ maxOutputBytes: 16 * 1024 * 1024,
10076
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
10077
+ });
10078
+ if (result.cancelled || result.timedOut)
10079
+ throw new Error(
10080
+ result.cancelled ? "Branch operation cancelled" : "Branch operation timed out"
10081
+ );
10082
+ if (result.outputTruncated)
10083
+ throw new Error(
10084
+ "Local branch inventory exceeded the output limit; the previous inventory was retained"
10085
+ );
10086
+ return result;
10087
+ }
10088
+ async function scanLocalBranches(folder, timeoutMs, signal, branch) {
10089
+ const [current, refs] = await Promise.all([
10090
+ branchGit(
10091
+ folder,
10092
+ ["symbolic-ref", "--short", "-q", "HEAD"],
10093
+ timeoutMs,
10094
+ signal
10095
+ ),
10096
+ branchGit(
10097
+ folder,
10098
+ [
10099
+ "for-each-ref",
10100
+ "--format=%(refname:strip=2)%00%(objectname)%00%(committerdate:iso-strict)%00%(contents:subject)%00%(worktreepath)",
10101
+ branch ? `refs/heads/${branch}` : "refs/heads"
10102
+ ],
10103
+ timeoutMs,
10104
+ signal
10105
+ )
10106
+ ]);
10107
+ if (refs.exitCode !== 0)
10108
+ throw new Error(
10109
+ branchError(refs.stderr || "Could not inspect local branches")
10110
+ );
10111
+ return parseLocalBranchInventory({
10112
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
10113
+ branches: refs.stdout.split("\n").filter(Boolean).map((line) => {
10114
+ const [name, headSha, date, subject, path] = line.split("\0");
10115
+ return {
10116
+ name,
10117
+ headSha,
10118
+ lastCommitAt: date && Number.isFinite(Date.parse(date)) ? new Date(date).toISOString() : null,
10119
+ lastCommitMessage: subject?.slice(0, 2e3) || null,
10120
+ current: current.exitCode === 0 && name === current.stdout.trim(),
10121
+ checkedOutPath: path || null
10122
+ };
10123
+ }).filter((item) => !branch || item.name === branch)
10124
+ });
10125
+ }
10126
+ async function localBranchReport(folder, timeoutMs, signal, unavailable) {
10127
+ const localBranchInventoryAttemptedAt = (/* @__PURE__ */ new Date()).toISOString();
10128
+ try {
10129
+ if (unavailable) throw new Error(unavailable);
10130
+ return {
10131
+ localBranchInventoryAttemptedAt,
10132
+ localBranchInventory: await scanLocalBranches(folder, timeoutMs, signal),
10133
+ localBranchInventoryError: null
10134
+ };
10135
+ } catch (error) {
10136
+ return {
10137
+ localBranchInventoryAttemptedAt,
10138
+ localBranchInventory: null,
10139
+ localBranchInventoryError: branchError(error)
10140
+ };
10005
10141
  }
10006
- };
10142
+ }
10007
10143
 
10008
10144
  // src/handlers/codebases.ts
10009
10145
  import { readdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
@@ -10305,7 +10441,26 @@ var inspectCodebaseFolder = async (payload, timeoutMs, signal) => {
10305
10441
  const input = codebaseJobPayload(payload);
10306
10442
  return inspectCodebaseProcess(input.folder, timeoutMs, signal);
10307
10443
  };
10308
- var refreshCodebase = inspectCodebaseFolder;
10444
+ var refreshCodebase = async (payload, timeoutMs, signal) => {
10445
+ const input = codebaseJobPayload(payload);
10446
+ const result = await inspectCodebaseProcess(
10447
+ input.folder,
10448
+ timeoutMs,
10449
+ signal,
10450
+ input.expectedOrigin
10451
+ );
10452
+ if (!("snapshot" in result)) return result;
10453
+ const snapshot = result.snapshot;
10454
+ return {
10455
+ ...result,
10456
+ ...await localBranchReport(
10457
+ snapshot.folder,
10458
+ Math.min(timeoutMs, 3e4),
10459
+ signal,
10460
+ snapshot.availability === "AVAILABLE" ? void 0 : snapshot.error || "Codebase is unavailable"
10461
+ )
10462
+ };
10463
+ };
10309
10464
  async function updateBaseBranchAfterFetch(folder, baseBranch, timeoutMs, signal) {
10310
10465
  const localRef = `refs/heads/${baseBranch}`;
10311
10466
  const remoteRef = `refs/remotes/origin/${baseBranch}`;
@@ -10462,6 +10617,12 @@ var fetchCodebase = async (payload, timeoutMs, signal, onLog, context) => {
10462
10617
  cancelled: result.cancelled || signal.aborted,
10463
10618
  snapshot,
10464
10619
  ...worktreesRefreshedAt ? { worktreesRefreshedAt } : {},
10620
+ ...await localBranchReport(
10621
+ snapshot.folder,
10622
+ Math.min(timeoutMs, 3e4),
10623
+ signal,
10624
+ snapshot.availability === "AVAILABLE" ? void 0 : snapshot.error || "Codebase is unavailable"
10625
+ ),
10465
10626
  ...worktreeRefreshError ? { worktreeRefreshError } : {}
10466
10627
  };
10467
10628
  };
@@ -10914,10 +11075,162 @@ var operateCodebaseGit = async (payload, timeoutMs, signal) => {
10914
11075
  folder,
10915
11076
  Math.min(timeoutMs, 3e4),
10916
11077
  signal
10917
- )
11078
+ ),
11079
+ ...await localBranchReport(folder, Math.min(timeoutMs, 3e4), signal)
11080
+ };
11081
+ };
11082
+
11083
+ // src/handlers/branches.ts
11084
+ var deleteLocalBranches = async (payload, timeoutMs, signal, _onLog, context) => {
11085
+ const input = codebaseBranchesDeletePayload(payload);
11086
+ const deadline = Date.now() + timeoutMs;
11087
+ const remaining = () => Math.max(0, Math.min(3e4, deadline - Date.now()));
11088
+ const branchDeletionResults = [];
11089
+ for (const target2 of input.targets) {
11090
+ const outcome = {
11091
+ codebaseId: input.codebaseId,
11092
+ branch: target2.branch,
11093
+ outcome: "SKIPPED",
11094
+ reason: null
11095
+ };
11096
+ try {
11097
+ if (signal.aborted || remaining() <= 0)
11098
+ throw new Error(
11099
+ "Branch operation interrupted; refresh before retrying"
11100
+ );
11101
+ const snapshot2 = await inspectCodebase(
11102
+ input.folder,
11103
+ remaining(),
11104
+ signal,
11105
+ input.expectedOrigin
11106
+ );
11107
+ if (snapshot2.availability !== "AVAILABLE")
11108
+ throw new Error(snapshot2.error || "Codebase is unavailable");
11109
+ const inventory = await scanLocalBranches(
11110
+ snapshot2.folder,
11111
+ remaining(),
11112
+ signal,
11113
+ target2.branch
11114
+ );
11115
+ const selected = inventory.branches.find(
11116
+ (branch) => branch.name === target2.branch
11117
+ );
11118
+ const remoteHead = await branchGit(
11119
+ snapshot2.folder,
11120
+ ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
11121
+ remaining(),
11122
+ signal
11123
+ );
11124
+ const localDefault = remoteHead.exitCode === 0 ? remoteHead.stdout.trim().replace(/^origin\//, "") : null;
11125
+ if (!selected) outcome.reason = "Local branch no longer exists";
11126
+ else if (selected.headSha !== target2.expectedHeadSha)
11127
+ outcome.reason = "Branch tip changed; refresh and select it again";
11128
+ else if (selected.current)
11129
+ outcome.reason = "The current branch cannot be deleted";
11130
+ else if (selected.name === input.defaultBranch || selected.name === localDefault)
11131
+ outcome.reason = "The default branch cannot be deleted";
11132
+ else if (selected.checkedOutPath)
11133
+ outcome.reason = `Branch is checked out at ${selected.checkedOutPath}`;
11134
+ else if (!input.defaultBranch && !localDefault)
11135
+ outcome.reason = "The default branch is unknown; configure it before cleanup";
11136
+ else {
11137
+ const deleted = await branchGit(
11138
+ snapshot2.folder,
11139
+ ["branch", input.force ? "-D" : "--delete", "--", selected.name],
11140
+ remaining(),
11141
+ signal
11142
+ );
11143
+ outcome.outcome = deleted.exitCode === 0 ? "DELETED" : "FAILED";
11144
+ outcome.reason = deleted.exitCode === 0 ? null : branchError(
11145
+ deleted.stderr || "Could not safely delete local branch"
11146
+ );
11147
+ }
11148
+ } catch (error) {
11149
+ outcome.outcome = "FAILED";
11150
+ outcome.reason = branchError(error);
11151
+ }
11152
+ branchDeletionResults.push(outcome);
11153
+ try {
11154
+ if (!signal.aborted && remaining() > 0)
11155
+ await context?.reportBranchDeletionResult?.(outcome);
11156
+ } catch {
11157
+ }
11158
+ }
11159
+ const localBranchInventoryAttemptedAt = (/* @__PURE__ */ new Date()).toISOString();
11160
+ let localBranchInventory = null;
11161
+ let localBranchInventoryError = null;
11162
+ let snapshot;
11163
+ try {
11164
+ snapshot = await inspectCodebase(
11165
+ input.folder,
11166
+ remaining(),
11167
+ signal,
11168
+ input.expectedOrigin
11169
+ );
11170
+ if (snapshot.availability !== "AVAILABLE")
11171
+ throw new Error(snapshot.error || "Codebase is unavailable");
11172
+ localBranchInventory = await scanLocalBranches(
11173
+ snapshot.folder,
11174
+ remaining(),
11175
+ signal
11176
+ );
11177
+ } catch (error) {
11178
+ localBranchInventoryError = branchError(error);
11179
+ }
11180
+ return {
11181
+ exitCode: 0,
11182
+ signal: null,
11183
+ cancelled: signal.aborted,
11184
+ timedOut: !signal.aborted && Date.now() >= deadline,
11185
+ ...snapshot ? { snapshot } : {},
11186
+ branchDeletionResults,
11187
+ localBranchInventory,
11188
+ localBranchInventoryError,
11189
+ localBranchInventoryAttemptedAt
10918
11190
  };
10919
11191
  };
10920
11192
 
11193
+ // src/handlers/codebase-clone.ts
11194
+ import {
11195
+ lstat,
11196
+ mkdir as mkdir2,
11197
+ mkdtemp as mkdtemp2,
11198
+ realpath as realpath3,
11199
+ rename as rename2,
11200
+ rm as rm3,
11201
+ rmdir
11202
+ } from "node:fs/promises";
11203
+ import {
11204
+ dirname as dirname3,
11205
+ isAbsolute as isAbsolute2,
11206
+ join as join4,
11207
+ parse,
11208
+ relative as relative2,
11209
+ resolve as resolve3,
11210
+ sep as sep2
11211
+ } from "node:path";
11212
+
11213
+ // src/repository-coordinator.ts
11214
+ var RepositoryCoordinator = class {
11215
+ tails = /* @__PURE__ */ new Map();
11216
+ async run(key, task) {
11217
+ const previous = this.tails.get(key) ?? Promise.resolve();
11218
+ let release2;
11219
+ const current = new Promise((resolve11) => {
11220
+ release2 = resolve11;
11221
+ });
11222
+ const tail = previous.then(() => current);
11223
+ this.tails.set(key, tail);
11224
+ await previous;
11225
+ try {
11226
+ return await task();
11227
+ } finally {
11228
+ release2();
11229
+ if (this.tails.get(key) === tail) this.tails.delete(key);
11230
+ }
11231
+ }
11232
+ };
11233
+
10921
11234
  // src/handlers/codebase-clone.ts
10922
11235
  var destinations = new RepositoryCoordinator();
10923
11236
  var success = {
@@ -20432,6 +20745,7 @@ var handlers = {
20432
20745
  [CODEBASE_FETCH_JOB_KIND]: fetchCodebase,
20433
20746
  [CODEBASE_GIT_INSPECT_JOB_KIND]: inspectCodebaseGit,
20434
20747
  [CODEBASE_GIT_OPERATION_JOB_KIND]: operateCodebaseGit,
20748
+ [CODEBASE_BRANCHES_DELETE_JOB_KIND]: deleteLocalBranches,
20435
20749
  [WORKTREE_INSPECT_JOB_KIND]: inspectWorktree,
20436
20750
  [WORKTREE_AUTO_SYNC_JOB_KIND]: autoSyncWorktree,
20437
20751
  [WORKTREE_BRANCH_JOB_KIND]: branchWorktree,
@@ -20579,6 +20893,7 @@ var JobExecutor = class {
20579
20893
  (log2) => this.client.appendLog(claimed.id, log2).then(() => void 0),
20580
20894
  {
20581
20895
  agentId: claimed.agentId,
20896
+ reportBranchDeletionResult: (result2) => this.client.reportBranchDeletionResult(claimed.id, result2),
20582
20897
  reportWorktreeActivity: (input) => this.client.reportWorktreeActivity(input),
20583
20898
  refreshFetchedCodebase: (input) => refreshFetchedCodebase(
20584
20899
  this.client,
@@ -20727,8 +21042,29 @@ var CodebaseMonitor = class {
20727
21042
  };
20728
21043
  fetchError ??= error instanceof Error ? error.message.slice(0, 2e3) : String(error);
20729
21044
  }
21045
+ const localBranchInventoryAttemptedAt = (/* @__PURE__ */ new Date()).toISOString();
21046
+ let localBranchInventory = null;
21047
+ let localBranchInventoryError = null;
21048
+ try {
21049
+ if (snapshot.availability !== "AVAILABLE")
21050
+ throw new Error(snapshot.error || "Codebase is unavailable");
21051
+ localBranchInventory = await scanLocalBranches(
21052
+ codebase.folder,
21053
+ INSPECTION_TIMEOUT_MS,
21054
+ signal
21055
+ );
21056
+ } catch (error) {
21057
+ localBranchInventoryError = error instanceof Error ? error.message.slice(0, 2e3) : String(error).slice(0, 2e3);
21058
+ }
21059
+ if (signal.aborted) return;
20730
21060
  await this.client.reportCodebaseStatuses([
20731
- { codebaseId: codebase.id, snapshot }
21061
+ {
21062
+ codebaseId: codebase.id,
21063
+ snapshot,
21064
+ localBranchInventory,
21065
+ localBranchInventoryError,
21066
+ localBranchInventoryAttemptedAt
21067
+ }
20732
21068
  ]);
20733
21069
  await this.client.reportWorktrees([
20734
21070
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.114",
3
+ "version": "0.0.115",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {