@ai-development-environment/control-agent 0.0.142 → 0.0.143

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 +277 -116
  2. package/package.json +1 -1
@@ -7617,22 +7617,31 @@ var AgentGraphQLClient = class {
7617
7617
  health() {
7618
7618
  return this.request(`query Health { health }`);
7619
7619
  }
7620
+ async requestDefaultBranchConfiguration(query) {
7621
+ try {
7622
+ return await this.request(query);
7623
+ } catch (error) {
7624
+ if (!(error instanceof Error) || !/Cannot query field "defaultBranchPatterns"/.test(error.message))
7625
+ throw error;
7626
+ return this.request(query.replace(/\sdefaultBranchPatterns\b/g, ""));
7627
+ }
7628
+ }
7620
7629
  async agentCodebases() {
7621
- const data = await this.request(`query AgentCodebases {
7630
+ const data = await this.requestDefaultBranchConfiguration(`query AgentCodebases {
7622
7631
  agentCodebases {
7623
- id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate lastFetchedAt lastFetchAttemptAt
7632
+ id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate defaultBranchPatterns lastFetchedAt lastFetchAttemptAt
7624
7633
  worktrees { id folder branch gitDirectory baseBranchOverride }
7625
7634
  }
7626
7635
  }`);
7627
7636
  return data.agentCodebases;
7628
7637
  }
7629
7638
  async agentCodebaseConfiguration() {
7630
- const data = await this.request(`query AgentCodebaseConfiguration {
7639
+ const data = await this.requestDefaultBranchConfiguration(`query AgentCodebaseConfiguration {
7631
7640
  agentCodebaseConfiguration {
7632
7641
  refreshIntervalSeconds
7633
7642
  fetchIntervalSeconds
7634
7643
  codebases {
7635
- id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate lastFetchedAt lastFetchAttemptAt
7644
+ id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate defaultBranchPatterns lastFetchedAt lastFetchAttemptAt
7636
7645
  worktrees { id folder branch gitDirectory baseBranchOverride }
7637
7646
  }
7638
7647
  }
@@ -8134,6 +8143,50 @@ var CODEBASE_GIT_INSPECT_JOB_KIND = "codebase.git.inspect";
8134
8143
  var CODEBASE_GIT_OPERATION_JOB_KIND = "codebase.git.operation";
8135
8144
  var CODEBASE_BRANCHES_DELETE_JOB_KIND = "codebase.branches.delete";
8136
8145
  var CODEBASE_RECONCILE_EVENT_CAPABILITY = "codebase.reconcile.requested";
8146
+ var CODEBASE_DEFAULT_BRANCHES_CAPABILITY = "codebase.default-branches.v1";
8147
+ function normalizeDefaultBranchPatterns(value) {
8148
+ if (!Array.isArray(value) || value.length > 100)
8149
+ throw new Error(
8150
+ "Default branch patterns must be a list of at most 100 entries"
8151
+ );
8152
+ const patterns = value.map((entry) => {
8153
+ if (typeof entry !== "string" || entry.trim().length > 255)
8154
+ throw new Error(
8155
+ "Default branch patterns must be strings of at most 255 characters"
8156
+ );
8157
+ const pattern = entry.trim();
8158
+ if (pattern && /[\s\x00-\x1f\x7f]/.test(pattern))
8159
+ throw new Error(
8160
+ "Default branch patterns cannot contain whitespace or control characters"
8161
+ );
8162
+ return pattern;
8163
+ });
8164
+ return [...new Set(patterns.filter(Boolean))];
8165
+ }
8166
+ function isDefaultBranch(branch, remoteDefaultBranch, patterns = []) {
8167
+ if (!branch) return false;
8168
+ if (branch === remoteDefaultBranch) return true;
8169
+ return patterns.some((pattern) => {
8170
+ let position = 0;
8171
+ let patternPosition = 0;
8172
+ let star = -1;
8173
+ let resume = 0;
8174
+ while (position < branch.length) {
8175
+ if (pattern[patternPosition] === "*") {
8176
+ star = patternPosition++;
8177
+ resume = position;
8178
+ } else if (pattern[patternPosition] === branch[position]) {
8179
+ patternPosition++;
8180
+ position++;
8181
+ } else if (star >= 0) {
8182
+ patternPosition = star + 1;
8183
+ position = ++resume;
8184
+ } else return false;
8185
+ }
8186
+ while (pattern[patternPosition] === "*") patternPosition++;
8187
+ return patternPosition === pattern.length;
8188
+ });
8189
+ }
8137
8190
  var MAX_CODEBASE_GIT_BRANCHES = 1e3;
8138
8191
  var MAX_CODEBASE_STASHES = 200;
8139
8192
  var MAX_CODEBASE_STASH_PATCH_BYTES = 256 * 1024;
@@ -8439,7 +8492,8 @@ function codebaseJobPayload(value) {
8439
8492
  "codebaseId",
8440
8493
  "expectedOrigin",
8441
8494
  "baseBranch",
8442
- "keepBaseBranchUpToDate"
8495
+ "keepBaseBranchUpToDate",
8496
+ "defaultBranchPatterns"
8443
8497
  ]);
8444
8498
  const unexpected = Object.keys(payload).find((key) => !allowed.has(key));
8445
8499
  if (unexpected)
@@ -8461,6 +8515,11 @@ function codebaseJobPayload(value) {
8461
8515
  payload.keepBaseBranchUpToDate,
8462
8516
  "payload.keepBaseBranchUpToDate"
8463
8517
  )
8518
+ },
8519
+ ...payload.defaultBranchPatterns === void 0 ? {} : {
8520
+ defaultBranchPatterns: normalizeDefaultBranchPatterns(
8521
+ payload.defaultBranchPatterns
8522
+ )
8464
8523
  }
8465
8524
  };
8466
8525
  }
@@ -11683,7 +11742,7 @@ function tailscaleServeRemovePayload(value) {
11683
11742
  // package.json
11684
11743
  var package_default = {
11685
11744
  name: "@ai-development-environment/control-agent",
11686
- version: "0.0.142",
11745
+ version: "0.0.143",
11687
11746
  description: "Control agent for the AI Development Environment",
11688
11747
  license: "MIT",
11689
11748
  repository: {
@@ -11753,6 +11812,7 @@ var PORTABLE_AGENT_CAPABILITIES = [
11753
11812
  CCUSAGE_REPORT_JOB_KIND,
11754
11813
  ...CODEBASE_JOB_KINDS,
11755
11814
  CODEBASE_RECONCILE_EVENT_CAPABILITY,
11815
+ CODEBASE_DEFAULT_BRANCHES_CAPABILITY,
11756
11816
  ...WORKTREE_JOB_KINDS,
11757
11817
  WORKTREE_REVIEWED_SYNC_CAPABILITY,
11758
11818
  ...SKILL_JOB_KINDS,
@@ -45725,76 +45785,165 @@ var refreshCodebase = async (payload, timeoutMs, signal2) => {
45725
45785
  )
45726
45786
  };
45727
45787
  };
45728
- async function updateBaseBranchAfterFetch(folder, baseBranch, timeoutMs, signal2) {
45729
- const localRef = `refs/heads/${baseBranch}`;
45730
- const remoteRef = `refs/remotes/origin/${baseBranch}`;
45788
+ async function updateBaseBranchAfterFetch(folder, baseBranch, timeoutMs, signal2, expectedOrigin, expectedGitDirectory) {
45731
45789
  try {
45732
- const validRef = await git2(
45733
- folder,
45734
- ["check-ref-format", localRef],
45735
- timeoutMs,
45736
- signal2
45737
- );
45738
- if (validRef.exitCode !== 0) return false;
45739
- const staged = await git2(
45740
- folder,
45741
- ["diff", "--cached", "--quiet"],
45742
- timeoutMs,
45743
- signal2
45744
- );
45745
- if (staged.exitCode !== 0) return false;
45746
- const remoteExists = await git2(
45790
+ return await withRepositoryMetadata(
45747
45791
  folder,
45748
- ["show-ref", "--verify", "--quiet", remoteRef],
45749
- timeoutMs,
45750
- signal2
45751
- );
45752
- if (remoteExists.exitCode !== 0) return false;
45753
- const remoteTip = requireSuccess(
45754
- await git2(folder, ["rev-parse", remoteRef], timeoutMs, signal2),
45755
- "Could not pin the fetched base branch"
45756
- ).stdout.trim();
45757
- const currentBranch2 = await git2(
45758
- folder,
45759
- ["symbolic-ref", "--short", "-q", "HEAD"],
45760
- timeoutMs,
45792
+ async () => {
45793
+ if (expectedGitDirectory) {
45794
+ const directory = await git2(
45795
+ folder,
45796
+ ["rev-parse", "--path-format=absolute", "--git-dir"],
45797
+ timeoutMs,
45798
+ signal2
45799
+ );
45800
+ if (directory.exitCode !== 0 || await realpath3(directory.stdout.trim()) !== expectedGitDirectory)
45801
+ return false;
45802
+ }
45803
+ const current = await git2(
45804
+ folder,
45805
+ ["symbolic-ref", "--short", "-q", "HEAD"],
45806
+ timeoutMs,
45807
+ signal2
45808
+ );
45809
+ if (current.exitCode !== 0 || current.stdout.trim() !== baseBranch)
45810
+ return false;
45811
+ if (expectedOrigin) {
45812
+ const remote = await git2(
45813
+ folder,
45814
+ ["remote", "get-url", "origin"],
45815
+ timeoutMs,
45816
+ signal2
45817
+ );
45818
+ if (remote.exitCode !== 0 || normalizeGitOrigin(remote.stdout.trim()).canonicalOrigin !== expectedOrigin)
45819
+ return false;
45820
+ }
45821
+ const status2 = await git2(
45822
+ folder,
45823
+ [
45824
+ "status",
45825
+ "--porcelain=v1",
45826
+ "-z",
45827
+ "--untracked-files=all",
45828
+ "--ignore-submodules=none"
45829
+ ],
45830
+ timeoutMs,
45831
+ signal2
45832
+ );
45833
+ if (status2.exitCode !== 0 || status2.outputTruncated || status2.stdout)
45834
+ return false;
45835
+ for (const name of [
45836
+ "MERGE_HEAD",
45837
+ "rebase-merge",
45838
+ "rebase-apply",
45839
+ "CHERRY_PICK_HEAD",
45840
+ "REVERT_HEAD"
45841
+ ]) {
45842
+ const path = await git2(
45843
+ folder,
45844
+ ["rev-parse", "--path-format=absolute", "--git-path", name],
45845
+ timeoutMs,
45846
+ signal2
45847
+ );
45848
+ if (path.exitCode !== 0) return false;
45849
+ try {
45850
+ await stat4(path.stdout.trim());
45851
+ return false;
45852
+ } catch (error) {
45853
+ if (error.code !== "ENOENT") throw error;
45854
+ }
45855
+ }
45856
+ const remoteRef = `refs/remotes/origin/${baseBranch}`;
45857
+ const valid = await git2(
45858
+ folder,
45859
+ ["check-ref-format", remoteRef],
45860
+ timeoutMs,
45861
+ signal2
45862
+ );
45863
+ if (valid.exitCode !== 0) return false;
45864
+ const tip = await git2(
45865
+ folder,
45866
+ ["rev-parse", "--verify", remoteRef],
45867
+ timeoutMs,
45868
+ signal2
45869
+ );
45870
+ if (tip.exitCode !== 0) return false;
45871
+ const ancestor = await git2(
45872
+ folder,
45873
+ ["merge-base", "--is-ancestor", "HEAD", tip.stdout.trim()],
45874
+ timeoutMs,
45875
+ signal2
45876
+ );
45877
+ if (ancestor.exitCode !== 0) return false;
45878
+ const branch = await git2(
45879
+ folder,
45880
+ ["symbolic-ref", "--short", "-q", "HEAD"],
45881
+ timeoutMs,
45882
+ signal2
45883
+ );
45884
+ if (branch.exitCode !== 0 || branch.stdout.trim() !== baseBranch)
45885
+ return false;
45886
+ return (await git2(
45887
+ folder,
45888
+ ["merge", "--ff-only", tip.stdout.trim()],
45889
+ timeoutMs,
45890
+ signal2
45891
+ )).exitCode === 0;
45892
+ },
45761
45893
  signal2
45762
45894
  );
45763
- if (currentBranch2.exitCode === 0 && currentBranch2.stdout.trim() === baseBranch) {
45764
- const merged = await git2(
45765
- folder,
45766
- ["merge", "--ff-only", remoteTip],
45767
- timeoutMs,
45768
- signal2
45769
- );
45770
- return merged.exitCode === 0;
45771
- }
45772
- const localExists = await git2(
45895
+ } catch (error) {
45896
+ if (signal2.aborted) throw error;
45897
+ return false;
45898
+ }
45899
+ }
45900
+ async function updateDefaultBranchesAfterFetch(folder, defaultBranch, patterns, timeoutMs, signal2, withCheckout, expectedOrigin) {
45901
+ const listed = requireSuccess(
45902
+ await git2(
45773
45903
  folder,
45774
- ["show-ref", "--verify", "--quiet", localRef],
45904
+ ["worktree", "list", "--porcelain", "-z"],
45775
45905
  timeoutMs,
45776
45906
  signal2
45777
- );
45778
- if (localExists.exitCode === 0) {
45779
- const fastForward = await git2(
45780
- folder,
45781
- ["merge-base", "--is-ancestor", localRef, remoteTip],
45782
- timeoutMs,
45783
- signal2
45907
+ ),
45908
+ "Could not list worktrees"
45909
+ );
45910
+ if (listed.outputTruncated) return;
45911
+ for (const block of listed.stdout.split("\0\0")) {
45912
+ signal2.throwIfAborted();
45913
+ const fields = block.split("\0");
45914
+ if (fields.some((field) => field.startsWith("prunable")) || fields.includes("bare"))
45915
+ continue;
45916
+ const checkout = fields.find((field) => field.startsWith("worktree "))?.slice(9);
45917
+ const branch = fields.find((field) => field.startsWith("branch refs/heads/"))?.slice(18);
45918
+ if (!checkout || !isDefaultBranch(branch, defaultBranch, patterns))
45919
+ continue;
45920
+ try {
45921
+ const path = await realpath3(checkout);
45922
+ const directory = requireSuccess(
45923
+ await git2(
45924
+ path,
45925
+ ["rev-parse", "--path-format=absolute", "--git-dir"],
45926
+ timeoutMs,
45927
+ signal2
45928
+ ),
45929
+ "Could not resolve worktree"
45930
+ ).stdout.trim();
45931
+ const expectedGitDirectory = await realpath3(directory);
45932
+ await withCheckout(
45933
+ path,
45934
+ expectedGitDirectory,
45935
+ () => updateBaseBranchAfterFetch(
45936
+ path,
45937
+ branch,
45938
+ timeoutMs,
45939
+ signal2,
45940
+ expectedOrigin,
45941
+ expectedGitDirectory
45942
+ )
45784
45943
  );
45785
- if (fastForward.exitCode !== 0) return false;
45786
- } else if (localExists.exitCode !== 1) {
45787
- return false;
45944
+ } catch (error) {
45945
+ if (signal2.aborted) throw error;
45788
45946
  }
45789
- const updated = await git2(
45790
- folder,
45791
- ["branch", "--force", baseBranch, remoteTip],
45792
- timeoutMs,
45793
- signal2
45794
- );
45795
- return updated.exitCode === 0;
45796
- } catch {
45797
- return false;
45798
45947
  }
45799
45948
  }
45800
45949
  var fetchCodebase = async (payload, timeoutMs, signal2, onLog, context) => {
@@ -45833,46 +45982,37 @@ var fetchCodebase = async (payload, timeoutMs, signal2, onLog, context) => {
45833
45982
  if (error instanceof InterruptedGitInspection) return error.result;
45834
45983
  throw error;
45835
45984
  }
45836
- if (result.exitCode === 0 && input.keepBaseBranchUpToDate && input.baseBranch) {
45837
- await log2("Updating the base branch where it can be fast-forwarded");
45838
- const update = () => updateBaseBranchAfterFetch(
45985
+ if (result.exitCode === 0 && input.keepBaseBranchUpToDate) {
45986
+ await log2(
45987
+ "Updating clean default branch worktrees from their matching origin branches"
45988
+ );
45989
+ await updateDefaultBranchesAfterFetch(
45839
45990
  input.folder,
45840
- input.baseBranch,
45991
+ input.baseBranch ?? null,
45992
+ input.defaultBranchPatterns ?? [],
45841
45993
  timeoutMs,
45842
- signal2
45843
- );
45844
- const gitDirectory2 = requireSuccess(
45845
- await git2(
45846
- input.folder,
45847
- ["rev-parse", "--path-format=absolute", "--git-dir"],
45848
- timeoutMs,
45849
- signal2
45850
- ),
45851
- "Could not resolve base worktree"
45852
- ).stdout.trim();
45853
- const grant = context?.tryWithOperationResources ? await context.tryWithOperationResources(
45854
- [
45855
- {
45856
- kind: "WORKTREE",
45857
- key: `${context.agentId}:worktree:${await realpath3(gitDirectory2)}`,
45858
- agentId: context.agentId,
45859
- codebaseId: input.codebaseId,
45860
- mode: "WRITE"
45861
- },
45862
- {
45863
- kind: "PATH",
45864
- key: input.folder,
45865
- path: input.folder,
45866
- agentId: context.agentId,
45867
- codebaseId: input.codebaseId,
45868
- mode: "WRITE"
45869
- }
45870
- ],
45871
- update
45872
- ) : { ran: true, result: await update() };
45873
- const updated = grant.ran && grant.result;
45874
- await log2(
45875
- updated ? "Base branch updated" : "Base branch left unchanged because it could not be safely fast-forwarded"
45994
+ signal2,
45995
+ (folder, gitDirectory2, update) => context?.tryWithOperationResources ? context.tryWithOperationResources(
45996
+ [
45997
+ {
45998
+ kind: "WORKTREE",
45999
+ key: `${context.agentId}:worktree:${gitDirectory2}`,
46000
+ agentId: context.agentId,
46001
+ codebaseId: input.codebaseId,
46002
+ mode: "WRITE"
46003
+ },
46004
+ {
46005
+ kind: "PATH",
46006
+ key: folder,
46007
+ path: folder,
46008
+ agentId: context.agentId,
46009
+ codebaseId: input.codebaseId,
46010
+ mode: "WRITE"
46011
+ }
46012
+ ],
46013
+ update
46014
+ ) : update(),
46015
+ input.expectedOrigin
45876
46016
  );
45877
46017
  }
45878
46018
  const afterResult = await inspectCodebaseProcess(
@@ -57465,22 +57605,43 @@ var CodebaseMonitor = class {
57465
57605
  signal2
57466
57606
  );
57467
57607
  if (fetchResult.exitCode === 0) {
57468
- if (codebase.keepBaseBranchUpToDate && codebase.defaultBranch) {
57469
- const baseBranch = codebase.defaultBranch;
57470
- await runExclusive(
57471
- () => updateBaseBranchAfterFetch(
57472
- codebase.folder,
57473
- baseBranch,
57474
- INSPECTION_TIMEOUT_MS,
57475
- signal2
57476
- )
57477
- );
57478
- }
57479
57608
  snapshot = await this.inspectRoot(codebase, signal2);
57480
57609
  } else {
57481
57610
  fetchError = (fetchResult.stderr || "Git fetch failed").replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1").slice(0, 2e3);
57482
57611
  }
57483
57612
  }
57613
+ if (snapshot.availability === "AVAILABLE" && codebase.keepBaseBranchUpToDate && !signal2.aborted) {
57614
+ await updateDefaultBranchesAfterFetch(
57615
+ codebase.folder,
57616
+ codebase.defaultBranch,
57617
+ codebase.defaultBranchPatterns ?? [],
57618
+ INSPECTION_TIMEOUT_MS,
57619
+ signal2,
57620
+ (folder, gitDirectory2, update) => this.agentId ? this.repositoryCoordinator.tryWithResources(
57621
+ [
57622
+ {
57623
+ kind: "WORKTREE",
57624
+ key: `${this.agentId}:worktree:${gitDirectory2}`,
57625
+ agentId: this.agentId,
57626
+ codebaseId: codebase.id,
57627
+ mode: "WRITE"
57628
+ },
57629
+ {
57630
+ kind: "PATH",
57631
+ key: folder,
57632
+ path: folder,
57633
+ agentId: this.agentId,
57634
+ codebaseId: codebase.id,
57635
+ mode: "WRITE"
57636
+ }
57637
+ ],
57638
+ update,
57639
+ signal2
57640
+ ) : runExclusive(update),
57641
+ codebase.canonicalOrigin
57642
+ );
57643
+ snapshot = await this.inspectRoot(codebase, signal2);
57644
+ }
57484
57645
  let inventory;
57485
57646
  try {
57486
57647
  inventory = await discoverWorktrees(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.142",
3
+ "version": "0.0.143",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {