@bevel-software/platform-core-backend 0.28.1 → 0.30.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.
Files changed (274) hide show
  1. package/agent-guide/access-control.md +15 -0
  2. package/agent-guide/skills.md +8 -0
  3. package/dist/__tests__/test-repositories.d.ts +3 -0
  4. package/dist/__tests__/test-repositories.d.ts.map +1 -0
  5. package/dist/__tests__/test-repositories.js +18 -0
  6. package/dist/__tests__/test-repositories.js.map +1 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -1
  8. package/dist/core/catalog-revision.js +4 -2
  9. package/dist/core/catalog-revision.js.map +1 -1
  10. package/dist/core/compose-embed.d.ts +35 -0
  11. package/dist/core/compose-embed.d.ts.map +1 -0
  12. package/dist/core/compose-embed.js +14 -0
  13. package/dist/core/compose-embed.js.map +1 -0
  14. package/dist/core/core-ports.d.ts +10 -0
  15. package/dist/core/core-ports.d.ts.map +1 -1
  16. package/dist/core/core-ports.js.map +1 -1
  17. package/dist/core/create-core-server.d.ts.map +1 -1
  18. package/dist/core/create-core-server.js +8 -2
  19. package/dist/core/create-core-server.js.map +1 -1
  20. package/dist/core/create-core-services.d.ts +4 -1
  21. package/dist/core/create-core-services.d.ts.map +1 -1
  22. package/dist/core/create-core-services.js +57 -18
  23. package/dist/core/create-core-services.js.map +1 -1
  24. package/dist/core/lifecycle.d.ts +19 -6
  25. package/dist/core/lifecycle.d.ts.map +1 -1
  26. package/dist/core/lifecycle.js +17 -6
  27. package/dist/core/lifecycle.js.map +1 -1
  28. package/dist/modules/access/access-control.interface.d.ts +20 -0
  29. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  30. package/dist/modules/access/access-control.service.d.ts +68 -15
  31. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  32. package/dist/modules/access/access-control.service.js +282 -58
  33. package/dist/modules/access/access-control.service.js.map +1 -1
  34. package/dist/modules/access/access.routes.d.ts.map +1 -1
  35. package/dist/modules/access/access.routes.js +20 -6
  36. package/dist/modules/access/access.routes.js.map +1 -1
  37. package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
  38. package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
  39. package/dist/modules/auth/index.d.ts +15 -0
  40. package/dist/modules/auth/index.d.ts.map +1 -0
  41. package/dist/modules/auth/index.js +20 -0
  42. package/dist/modules/auth/index.js.map +1 -0
  43. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  44. package/dist/modules/diff/diff.service.js +7 -1
  45. package/dist/modules/diff/diff.service.js.map +1 -1
  46. package/dist/modules/embed/embed.interface.d.ts +22 -0
  47. package/dist/modules/embed/embed.interface.d.ts.map +1 -1
  48. package/dist/modules/embed/embed.service.d.ts +28 -14
  49. package/dist/modules/embed/embed.service.d.ts.map +1 -1
  50. package/dist/modules/embed/embed.service.js +38 -17
  51. package/dist/modules/embed/embed.service.js.map +1 -1
  52. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  53. package/dist/modules/embed/embed.tools.js +10 -2
  54. package/dist/modules/embed/embed.tools.js.map +1 -1
  55. package/dist/modules/kb-fs/git-dir.d.ts +21 -0
  56. package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
  57. package/dist/modules/kb-fs/git-dir.js +58 -0
  58. package/dist/modules/kb-fs/git-dir.js.map +1 -0
  59. package/dist/modules/kb-fs/index.d.ts +9 -0
  60. package/dist/modules/kb-fs/index.d.ts.map +1 -0
  61. package/dist/modules/kb-fs/index.js +3 -0
  62. package/dist/modules/kb-fs/index.js.map +1 -0
  63. package/dist/modules/kb-fs/ref-files.d.ts +63 -0
  64. package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
  65. package/dist/modules/kb-fs/ref-files.js +249 -0
  66. package/dist/modules/kb-fs/ref-files.js.map +1 -0
  67. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
  68. package/dist/modules/plugins/join-request-jobs.service.js +43 -8
  69. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
  70. package/dist/modules/plugins/teams.routes.d.ts +4 -0
  71. package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
  72. package/dist/modules/plugins/teams.routes.js +9 -1
  73. package/dist/modules/plugins/teams.routes.js.map +1 -1
  74. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  75. package/dist/modules/skills/skills.tools.d.ts +8 -1
  76. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  77. package/dist/modules/skills/skills.tools.js +115 -11
  78. package/dist/modules/skills/skills.tools.js.map +1 -1
  79. package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
  80. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
  81. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  82. package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
  83. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  84. package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
  85. package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
  86. package/dist/modules/tool-helpers/default-branch-write.js +63 -0
  87. package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
  88. package/dist/modules/tool-helpers/index.d.ts +1 -0
  89. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  90. package/dist/modules/tool-helpers/index.js +1 -0
  91. package/dist/modules/tool-helpers/index.js.map +1 -1
  92. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
  93. package/dist/modules/tool-helpers/tool.contract.d.ts +2 -2
  94. package/dist/modules/workflow/file-lock.service.d.ts +27 -5
  95. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  96. package/dist/modules/workflow/file-lock.service.js +70 -18
  97. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  98. package/dist/modules/workflow/git/git.service.d.ts +42 -0
  99. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  100. package/dist/modules/workflow/git/git.service.js +201 -24
  101. package/dist/modules/workflow/git/git.service.js.map +1 -1
  102. package/dist/modules/workflow/index.d.ts +8 -0
  103. package/dist/modules/workflow/index.d.ts.map +1 -0
  104. package/dist/modules/workflow/index.js +8 -0
  105. package/dist/modules/workflow/index.js.map +1 -0
  106. package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
  107. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  108. package/dist/modules/workflow/pending-commits.service.js +4 -24
  109. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  110. package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
  111. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  112. package/dist/modules/workflow/workflow.routes.js +20 -1
  113. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  114. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  115. package/dist/modules/workflow/workflow.service.js +5 -3
  116. package/dist/modules/workflow/workflow.service.js.map +1 -1
  117. package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
  118. package/dist/modules/workspace/agent-access.gate.js +2 -2
  119. package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
  120. package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
  121. package/dist/modules/workspace/bounded-matcher.js +261 -0
  122. package/dist/modules/workspace/bounded-matcher.js.map +1 -0
  123. package/dist/modules/workspace/fetch-instant.d.ts +9 -0
  124. package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
  125. package/dist/modules/workspace/fetch-instant.js +13 -0
  126. package/dist/modules/workspace/fetch-instant.js.map +1 -0
  127. package/dist/modules/workspace/index.d.ts +17 -0
  128. package/dist/modules/workspace/index.d.ts.map +1 -0
  129. package/dist/modules/workspace/index.js +16 -0
  130. package/dist/modules/workspace/index.js.map +1 -0
  131. package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
  132. package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
  133. package/dist/modules/workspace/kb-repository.contract.js +19 -0
  134. package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
  135. package/dist/modules/workspace/kb-repository.d.ts +236 -0
  136. package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
  137. package/dist/modules/workspace/kb-repository.js +811 -0
  138. package/dist/modules/workspace/kb-repository.js.map +1 -0
  139. package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
  140. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
  141. package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
  142. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
  143. package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
  144. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  145. package/dist/modules/workspace/routine-write-policy.js +0 -5
  146. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  147. package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
  148. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
  149. package/dist/modules/workspace/set-aside-clone.js +139 -0
  150. package/dist/modules/workspace/set-aside-clone.js.map +1 -1
  151. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
  152. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  153. package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
  154. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  155. package/dist/modules/workspace/workspace.service.d.ts +68 -32
  156. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  157. package/dist/modules/workspace/workspace.service.js +240 -161
  158. package/dist/modules/workspace/workspace.service.js.map +1 -1
  159. package/dist/modules/workspace/workspace.tools.d.ts +13 -4
  160. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  161. package/dist/modules/workspace/workspace.tools.js +268 -356
  162. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  163. package/dist/shared/workspace-id.d.ts +17 -0
  164. package/dist/shared/workspace-id.d.ts.map +1 -1
  165. package/dist/shared/workspace-id.js +24 -0
  166. package/dist/shared/workspace-id.js.map +1 -1
  167. package/dist/test-setup.js +4 -0
  168. package/dist/test-setup.js.map +1 -1
  169. package/mcp-app/page.html +142 -8
  170. package/package.json +3 -3
  171. package/src/__tests__/test-repositories.ts +22 -0
  172. package/src/__tests__/workspace-service.ts +69 -0
  173. package/src/core/__tests__/compose-embed.test.ts +65 -0
  174. package/src/core/__tests__/lifecycle.test.ts +45 -26
  175. package/src/core/catalog-revision.ts +4 -2
  176. package/src/core/compose-embed.ts +50 -0
  177. package/src/core/core-ports.ts +10 -0
  178. package/src/core/create-core-server.ts +8 -2
  179. package/src/core/create-core-services.ts +63 -24
  180. package/src/core/lifecycle.ts +34 -12
  181. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +443 -25
  182. package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
  183. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
  184. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  185. package/src/modules/access/access-control.interface.ts +22 -0
  186. package/src/modules/access/access-control.service.ts +302 -58
  187. package/src/modules/access/access.routes.ts +22 -6
  188. package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
  189. package/src/modules/auth/index.ts +29 -0
  190. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
  191. package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
  192. package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
  193. package/src/modules/diff/diff.service.ts +6 -4
  194. package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
  195. package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
  196. package/src/modules/embed/embed.interface.ts +24 -0
  197. package/src/modules/embed/embed.service.ts +44 -19
  198. package/src/modules/embed/embed.tools.ts +10 -2
  199. package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
  200. package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
  201. package/src/modules/kb-fs/git-dir.ts +53 -0
  202. package/src/modules/kb-fs/index.ts +8 -0
  203. package/src/modules/kb-fs/ref-files.ts +279 -0
  204. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
  205. package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
  206. package/src/modules/plugins/join-request-jobs.service.ts +45 -8
  207. package/src/modules/plugins/teams.routes.ts +14 -1
  208. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
  209. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
  210. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
  211. package/src/modules/skills/skills.contract.ts +4 -0
  212. package/src/modules/skills/skills.tools.ts +120 -10
  213. package/src/modules/tool-auth/internal-token.service.ts +1 -1
  214. package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
  215. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
  216. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
  217. package/src/modules/tool-helpers/default-branch-write.ts +90 -0
  218. package/src/modules/tool-helpers/index.ts +5 -0
  219. package/src/modules/tool-helpers/tool-handler.ts +2 -2
  220. package/src/modules/tool-helpers/tool.contract.ts +2 -2
  221. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +42 -3
  222. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
  223. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
  224. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
  225. package/src/modules/workflow/file-lock.service.ts +79 -18
  226. package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
  227. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
  228. package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
  229. package/src/modules/workflow/git/git.service.ts +207 -22
  230. package/src/modules/workflow/index.ts +7 -0
  231. package/src/modules/workflow/pending-commits.service.ts +4 -23
  232. package/src/modules/workflow/workflow-hooks.ts +1 -1
  233. package/src/modules/workflow/workflow.routes.ts +24 -1
  234. package/src/modules/workflow/workflow.service.ts +3 -2
  235. package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
  236. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
  237. package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
  238. package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
  239. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
  240. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
  241. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
  242. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
  243. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
  244. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
  245. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
  246. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
  247. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
  248. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
  249. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
  250. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
  251. package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
  252. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
  253. package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
  254. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
  255. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
  256. package/src/modules/workspace/__tests__/workspace.tools.test.ts +174 -309
  257. package/src/modules/workspace/agent-access.gate.ts +2 -2
  258. package/src/modules/workspace/bounded-matcher.ts +328 -0
  259. package/src/modules/workspace/fetch-instant.ts +13 -0
  260. package/src/modules/workspace/index.ts +26 -0
  261. package/src/modules/workspace/kb-repository.contract.ts +124 -0
  262. package/src/modules/workspace/kb-repository.ts +844 -0
  263. package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
  264. package/src/modules/workspace/routine-write-policy.ts +0 -15
  265. package/src/modules/workspace/set-aside-clone.ts +140 -0
  266. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
  267. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
  268. package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
  269. package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
  270. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
  271. package/src/modules/workspace/workspace.service.ts +247 -169
  272. package/src/modules/workspace/workspace.tools.ts +299 -385
  273. package/src/shared/workspace-id.ts +24 -0
  274. package/src/test-setup.ts +5 -0
@@ -8,6 +8,7 @@ import { WorkspaceMutex } from '../../kb-fs/mutex.js';
8
8
  import { isAbsence } from '../../../shared/fs.contract.js';
9
9
  import { assertInsideRepo } from '../../kb-fs/repo-path.js';
10
10
  import { cloneTrackingConfigArgs, SAFE_IMPLICIT_FETCH_ARGS } from '../../kb-fs/clone-config.js';
11
+ import { readRefSha } from '../../kb-fs/index.js';
11
12
  import { assertValidBranchName, assertValidRelativePath, isBranchAuthoredBy, isOwnSuggestionsBranch, } from '../../kb-fs/branch-name.js';
12
13
  import { AppliedChangeMismatchError, BranchAuthorshipError, BranchDeleteRefusedError, WorkflowDomainError, WorkflowValidationError, ProtectedBranchError, PullRebaseConflictError, RemoteBranchGoneError, VersionNotOnBranchError, isMissingRemoteBranchFailure, } from '../../../shared/domain-errors.js';
13
14
  import { GitRunError, assertOneSpecPerLine, isGitTimeout, redactGitToken, } from '../../../shared/git.contract.js';
@@ -243,6 +244,24 @@ export class GitService {
243
244
  // concurrent processes worst case — fast for typical (<8) feature-branch
244
245
  // counts, calm under large branch lists.
245
246
  static MAX_AHEAD_BEHIND_CONCURRENCY = 8;
247
+ /**
248
+ * A change request's changed entries, keyed by the two commits its diff
249
+ * was read between. Two commits never change, and neither does the
250
+ * merge-base between them, so the three-dot diff of a pair is a fact: a
251
+ * push moves a tip, the pair changes, the old entry is simply never asked
252
+ * for again. No TTL, no invalidation.
253
+ *
254
+ * This is what a change-request listing was paying git for on every read:
255
+ * one `diff` per open request, polled every minute and refetched on every
256
+ * event, where nothing had moved. Now git is asked once per pair, which is
257
+ * once per push. Keyed by commits alone, not by workspace: every clone of
258
+ * the knowledge base holds the same objects, so the answer is the same in
259
+ * all of them. Bounded FIFO; the bound only caps memory.
260
+ */
261
+ changedEntriesByCommits = new Map();
262
+ /** Diffs being read right now, so concurrent misses on one pair share one `git diff`. */
263
+ changedEntriesInFlight = new Map();
264
+ static CHANGED_ENTRIES_MAX = 4096;
246
265
  constructor(workspaceService,
247
266
  /**
248
267
  * Workflow lifecycle hooks. This service only consults the ADVISORY
@@ -321,7 +340,9 @@ export class GitService {
321
340
  * above rather than kept separately, so it cannot disagree with it.
322
341
  */
323
342
  lastRemoteContact() {
324
- let latest = null;
343
+ // The repository's fetches are every worktree's; the per-workspace record
344
+ // is a full clone's.
345
+ let latest = this.workspaceService.repository?.lastContact() ?? null;
325
346
  for (const [workspaceId, at] of this.lastImplicitFetchAt) {
326
347
  if (latest === null || at > latest.at) {
327
348
  latest = { at, ok: this.lastImplicitFetchOk.get(workspaceId) ?? false };
@@ -419,12 +440,22 @@ export class GitService {
419
440
  // Union local heads with origin's remote-tracking refs: a fresh per-user
420
441
  // clone only has one local head (HEAD's default), so without this we'd hide
421
442
  // target-company-state and every draft pushed by another user.
443
+ //
444
+ // The workspace is a worktree of the one repository, which holds every
445
+ // OTHER worktree's local branch too. Those are not this workspace's: a
446
+ // clone never had them. A local head checked out in another worktree
447
+ // is read as that branch's remote-tracking ref alone (and not at all
448
+ // once origin no longer has it), so a branch deleted on origin is not
449
+ // kept alive by its own worktree's local ref — the orphan sweep reads
450
+ // this list. A branch is checked out in at most one worktree, so "not
451
+ // this one's HEAD" is "another's".
422
452
  const { stdout } = await this.git(cwd, [
423
453
  'for-each-ref',
424
- '--format=%(refname:short)%09%(refname)',
454
+ '--format=%(refname:short)%09%(refname)%09%(worktreepath)',
425
455
  'refs/heads',
426
456
  'refs/remotes/origin',
427
457
  ]);
458
+ const ownBranch = (await this.git(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD']).then((r) => r.stdout, () => '')).trim();
428
459
  const buckets = new Map();
429
460
  for (const line of stdout.split('\n')) {
430
461
  const trimmed = line.trim();
@@ -434,7 +465,7 @@ export class GitService {
434
465
  if (tab < 0)
435
466
  continue;
436
467
  const rawShort = trimmed.slice(0, tab);
437
- const ref = trimmed.slice(tab + 1);
468
+ const [ref = '', worktreePath = ''] = trimmed.slice(tab + 1).split('\t');
438
469
  if (!rawShort || !ref)
439
470
  continue;
440
471
  // Skip symbolic refs up-front. Different git versions report
@@ -447,6 +478,8 @@ export class GitService {
447
478
  const shortName = isLocal ? rawShort : rawShort.replace(/^origin\//, '');
448
479
  if (!shortName)
449
480
  continue;
481
+ if (isLocal && worktreePath !== '' && shortName !== ownBranch)
482
+ continue;
450
483
  // Final guard: the strip above turns `origin/HEAD` into `HEAD`. Even
451
484
  // if a future git version lands us here, don't bucket it as a branch.
452
485
  if (shortName === 'HEAD')
@@ -530,6 +563,20 @@ export class GitService {
530
563
  * and the object store, neither of which checkout/status/commit touch.
531
564
  */
532
565
  async fetchOriginIfStale(cwd, workspaceId, force = false) {
566
+ // A worktree's remote-tracking refs are the repository's: ONE fetch for
567
+ // every workspace, throttled, joined and reserved there. The per-workspace
568
+ // record below is for a full clone from an earlier release, which fetches
569
+ // on its own until the boot phase converts it.
570
+ const repository = (await this.workspaceService.repositoryOfCheckout?.(cwd)) ?? null;
571
+ if (repository) {
572
+ return repository.fetch({
573
+ maxAgeMs: GitService.IMPLICIT_FETCH_TTL_MS,
574
+ // A host that is down is not asked again by every poll inside the
575
+ // window, as the per-workspace record below stamps a failure too.
576
+ failureBackoffMs: GitService.IMPLICIT_FETCH_TTL_MS,
577
+ force,
578
+ });
579
+ }
533
580
  const last = this.lastImplicitFetchAt.get(workspaceId) ?? 0;
534
581
  if (!force && Date.now() - last < GitService.IMPLICIT_FETCH_TTL_MS) {
535
582
  return this.lastImplicitFetchOk.get(workspaceId) ?? true;
@@ -603,8 +650,9 @@ export class GitService {
603
650
  // navigation to the new draft's URL fails with
604
651
  // "Remote branch <name> not found in upstream origin". Roll the local
605
652
  // ref back on failure so a retry isn't blocked by "branch already exists".
653
+ // No `-u`: the local head is dropped right below, and its upstream with it.
606
654
  try {
607
- await this.git(cwd, ['push', '-u', 'origin', name]);
655
+ await this.git(cwd, ['push', 'origin', name]);
608
656
  }
609
657
  catch (err) {
610
658
  await this.git(cwd, ['branch', '-D', name]).catch(() => undefined);
@@ -725,7 +773,15 @@ export class GitService {
725
773
  let localTip = null;
726
774
  if (await this.refExists(cwd, `refs/heads/${name}`)) {
727
775
  localTip = await this.revParseOrNull(cwd, `refs/heads/${name}`);
728
- await this.git(cwd, ['branch', '-D', name]);
776
+ await this.git(cwd, ['branch', '-D', name]).catch((err) => {
777
+ // In a repository of worktrees the branch's local ref is shared, and
778
+ // git keeps it while the branch's own workspace has it checked out.
779
+ // Removing that workspace — which the caller does next — takes the
780
+ // worktree and the ref with it.
781
+ if (/checked out at|used by worktree/i.test(err instanceof Error ? err.message : String(err)))
782
+ return;
783
+ throw err;
784
+ });
729
785
  }
730
786
  return { lastCommit: remoteTip ?? localTip };
731
787
  });
@@ -1221,7 +1277,14 @@ export class GitService {
1221
1277
  const touched = await this.unpushedTouchedPaths(cwd);
1222
1278
  await this.assertCanWriteAtRef(workspaceId, `origin/${branch}`, user.email, touched);
1223
1279
  }
1224
- await this.git(cwd, ['push', '-u', 'origin', branch]);
1280
+ // No `-u`: it writes the upstream into the config during the push, and
1281
+ // a worktree's config is the repository's, shared by every workspace —
1282
+ // git refuses a second writer of it rather than waiting. The upstream is
1283
+ // stamped after the push instead, as one short write made as the
1284
+ // repository's administrator (see `git`).
1285
+ await this.git(cwd, ['push', 'origin', branch]);
1286
+ await this.git(cwd, ['config', '--replace-all', `branch.${branch}.remote`, 'origin']);
1287
+ await this.git(cwd, ['config', '--replace-all', `branch.${branch}.merge`, `refs/heads/${branch}`]);
1225
1288
  }
1226
1289
  }
1227
1290
  // `forcePush` method removed. The workflow layer no longer auto-force-pushes
@@ -2745,9 +2808,8 @@ export class GitService {
2745
2808
  await this.fetchPrRefs(cwd, baseBranch, headBranch);
2746
2809
  }
2747
2810
  return this.mutex.run(workspaceId, async () => {
2748
- const baseRef = pinned ? pinned.base : await this.resolvePublishedBranchRef(cwd, baseBranch);
2749
- const headRef = pinned ? pinned.head : await this.resolvePublishedBranchRef(cwd, headBranch);
2750
- return this.prChangedPathsAt(cwd, baseRef, headRef, opts);
2811
+ const at = pinned ?? (await this.publishedPrEnds(cwd, baseBranch, headBranch));
2812
+ return flattenChangedPaths(await this.prChangedEntriesBetweenCommits(cwd, at.base, at.head), opts);
2751
2813
  });
2752
2814
  }
2753
2815
  /**
@@ -2800,12 +2862,49 @@ export class GitService {
2800
2862
  await this.fetchPrRefs(cwd, baseBranch, headBranch);
2801
2863
  }
2802
2864
  return this.mutex.run(workspaceId, async () => {
2803
- const baseRef = pinned ? pinned.base : await this.resolvePublishedBranchRef(cwd, baseBranch);
2804
- const headRef = pinned ? pinned.head : await this.resolvePublishedBranchRef(cwd, headBranch);
2805
- const entries = await this.prChangedEntriesAt(cwd, baseRef, headRef);
2865
+ const at = pinned ?? (await this.publishedPrEnds(cwd, baseBranch, headBranch));
2866
+ const entries = await this.prChangedEntriesBetweenCommits(cwd, at.base, at.head);
2806
2867
  return { paths: flattenChangedPaths(entries, {}), pairs: changedPathPairs(entries) };
2807
2868
  });
2808
2869
  }
2870
+ /**
2871
+ * {@link prChangedEntriesAt} for a pair of COMMITS, read once per pair
2872
+ * (see `changedEntriesByCommits`). The caller holds the reservation, as for
2873
+ * `prChangedEntriesAt`; a memo hit holds it for nothing, which is cheaper
2874
+ * than a second code path around the lock.
2875
+ */
2876
+ async prChangedEntriesBetweenCommits(cwd, baseSha, headSha) {
2877
+ const key = `${baseSha}\0${headSha}`;
2878
+ const known = this.changedEntriesByCommits.get(key);
2879
+ if (known)
2880
+ return known;
2881
+ let pending = this.changedEntriesInFlight.get(key);
2882
+ if (!pending) {
2883
+ pending = this.prChangedEntriesAt(cwd, baseSha, headSha)
2884
+ .then((entries) => {
2885
+ if (this.changedEntriesByCommits.size >= GitService.CHANGED_ENTRIES_MAX) {
2886
+ const oldest = this.changedEntriesByCommits.keys().next().value;
2887
+ if (oldest !== undefined)
2888
+ this.changedEntriesByCommits.delete(oldest);
2889
+ }
2890
+ this.changedEntriesByCommits.set(key, entries);
2891
+ return entries;
2892
+ })
2893
+ .finally(() => {
2894
+ if (this.changedEntriesInFlight.get(key) === pending)
2895
+ this.changedEntriesInFlight.delete(key);
2896
+ });
2897
+ // Same bound as the memo: evicting an in-flight entry costs at most one
2898
+ // redundant diff for that pair, never a lost answer.
2899
+ if (this.changedEntriesInFlight.size >= GitService.CHANGED_ENTRIES_MAX) {
2900
+ const oldest = this.changedEntriesInFlight.keys().next().value;
2901
+ if (oldest !== undefined)
2902
+ this.changedEntriesInFlight.delete(oldest);
2903
+ }
2904
+ this.changedEntriesInFlight.set(key, pending);
2905
+ }
2906
+ return pending;
2907
+ }
2809
2908
  /**
2810
2909
  * The merge-base commit of a change request's two branches (their origin
2811
2910
  * refs), or null when they share no history. This is the "before" the CR's
@@ -3103,18 +3202,20 @@ export class GitService {
3103
3202
  * broken for some other reason.
3104
3203
  */
3105
3204
  async publishedPrCommits(cwd, baseBranch, headBranch) {
3106
- const shas = [];
3107
- for (const branch of [baseBranch, headBranch]) {
3108
- try {
3109
- shas.push((await this.resolvePublishedBranch(cwd, branch)).sha);
3110
- }
3111
- catch (err) {
3112
- if (err instanceof WorkflowValidationError)
3113
- return null;
3114
- throw err;
3115
- }
3205
+ try {
3206
+ return await this.publishedPrEnds(cwd, baseBranch, headBranch);
3207
+ }
3208
+ catch (err) {
3209
+ if (err instanceof WorkflowValidationError)
3210
+ return null;
3211
+ throw err;
3116
3212
  }
3117
- return { base: shas[0], head: shas[1] };
3213
+ }
3214
+ /** The two commits a change request is read between, each resolved as {@link resolvePublishedBranch} does. */
3215
+ async publishedPrEnds(cwd, baseBranch, headBranch) {
3216
+ const base = (await this.resolvePublishedBranch(cwd, baseBranch)).sha;
3217
+ const head = (await this.resolvePublishedBranch(cwd, headBranch)).sha;
3218
+ return { base, head };
3118
3219
  }
3119
3220
  /**
3120
3221
  * `resolveBranchRef` with the preference reversed: the PUBLISHED ref
@@ -3135,6 +3236,15 @@ export class GitService {
3135
3236
  */
3136
3237
  async resolvePublishedBranch(cwd, branch) {
3137
3238
  for (const ref of [`refs/remotes/origin/${branch}`, `refs/heads/${branch}`]) {
3239
+ // The ref file first (`readRefSha`): no process, same answer, same
3240
+ // freshness. Git is asked only for what the files cannot decide — a
3241
+ // missing ref is decided by the files and the loop moves on; an
3242
+ // unreadable one is git's to answer, as the authority.
3243
+ const fromFiles = await readRefSha(cwd, ref);
3244
+ if (fromFiles === 'missing')
3245
+ continue;
3246
+ if (fromFiles !== 'unreadable')
3247
+ return { ref, sha: fromFiles.sha };
3138
3248
  try {
3139
3249
  const { stdout } = await this.git(cwd, ['rev-parse', '--verify', '--quiet', ref]);
3140
3250
  return { ref, sha: stdout.trim() };
@@ -3482,6 +3592,30 @@ export class GitService {
3482
3592
  const { stdout } = await this.git(cwd, ['ls-tree', '-r', '-z', '--name-only', ref, '--', folder]);
3483
3593
  return stdout.split('\0').filter((line) => line.length > 0);
3484
3594
  }
3595
+ /**
3596
+ * The commit `ref` names right now, or null when it names none (an unborn
3597
+ * `HEAD`, a ref that does not exist). A caller that reads several things
3598
+ * "at HEAD" pins the commit once with this and asks each read at that
3599
+ * commit, so a commit landing between the reads cannot hand it one
3600
+ * snapshot's files with another's rules.
3601
+ *
3602
+ * Null is ONLY "names no commit": `rev-parse --verify --quiet` says that
3603
+ * with exit status 1 and nothing on stderr. Any other failure — no
3604
+ * repository there, git itself failing, a deadline — is thrown, so a
3605
+ * caller cannot read a broken clone as an empty one.
3606
+ */
3607
+ async commitAtRef(workspaceId, ref) {
3608
+ const cwd = await this.repoDir(workspaceId);
3609
+ try {
3610
+ const { stdout } = await this.git(cwd, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
3611
+ return stdout.trim() || null;
3612
+ }
3613
+ catch (err) {
3614
+ if (err instanceof GitRunError && !err.timedOut && err.exitCode === 1 && !(err.stderr ?? '').trim())
3615
+ return null;
3616
+ throw err;
3617
+ }
3618
+ }
3485
3619
  /**
3486
3620
  * A clean `git status` for a path has two honest readings (already
3487
3621
  * committed; a queued re-apply of something that landed) and one dishonest
@@ -3534,7 +3668,7 @@ export class GitService {
3534
3668
  * from a real failure are unaffected.
3535
3669
  */
3536
3670
  git(cwd, args, opts) {
3537
- return this.gitRunner.run(cwd, args, {
3671
+ const run = () => this.gitRunner.run(cwd, args, {
3538
3672
  ...opts,
3539
3673
  // `GIT_LITERAL_PATHSPECS=1` makes git treat every pathspec literally
3540
3674
  // instead of interpreting `[`, `]`, `*`, `?`, `!`, or `:(magic)` as glob
@@ -3549,6 +3683,17 @@ export class GitService {
3549
3683
  // a search for a file literally named that.
3550
3684
  env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
3551
3685
  });
3686
+ // A write to the git config is a write to the ONE file every worktree of
3687
+ // the repository shares, which git refuses a second writer of rather than
3688
+ // waiting: it runs as the repository's administrator. Everything else —
3689
+ // a fetch, a commit, a branch delete — git serializes with its own locks,
3690
+ // as it does for any two worktrees of one repository.
3691
+ return writesSharedConfig(args) ? this.asRepositoryAdministrator(cwd, run) : run();
3692
+ }
3693
+ /** `run` as the repository's administrator when `cwd` is a worktree of it; as itself for a full clone. */
3694
+ async asRepositoryAdministrator(cwd, run) {
3695
+ const repository = (await this.workspaceService.repositoryOfCheckout?.(cwd)) ?? null;
3696
+ return repository ? repository.exclusive(run) : run();
3552
3697
  }
3553
3698
  /**
3554
3699
  * {@link git}, with stdout as raw BYTES.
@@ -3722,4 +3867,36 @@ export function parseNumstatZ(out) {
3722
3867
  }
3723
3868
  return entries;
3724
3869
  }
3870
+ /**
3871
+ * Whether a git command WRITES the config — the one file every worktree of
3872
+ * the knowledge base's repository shares, which git refuses a second writer
3873
+ * of rather than waiting — and so must run as the repository's administrator
3874
+ * (`IKbRepository.exclusive`): a `config` that sets or unsets, a `remote`
3875
+ * change, a branch rename, delete or upstream change (its config section is
3876
+ * written, or removed with the branch), a push that sets the upstream. Reads
3877
+ * of the config are not writes. Nothing else is: the refs, `packed-refs` and
3878
+ * the object store are locked by git itself, and the worktree list is the
3879
+ * repository's own to serialize (`KbRepository`).
3880
+ *
3881
+ * Leading `-c key=value` / `-C dir` options are skipped to find the subcommand.
3882
+ */
3883
+ function writesSharedConfig(args) {
3884
+ let i = 0;
3885
+ while (i < args.length && (args[i] === '-c' || args[i] === '-C'))
3886
+ i += 2;
3887
+ const sub = args[i];
3888
+ const rest = args.slice(i + 1);
3889
+ switch (sub) {
3890
+ case 'remote':
3891
+ return true;
3892
+ case 'config':
3893
+ return !rest.some((a) => /^(--get|--get-all|--get-regexp|--list|-l|--show-origin)$/.test(a));
3894
+ case 'branch':
3895
+ return rest.some((a) => /^(-d|-D|--delete|-m|-M|--move|-u|--set-upstream-to=.*|--unset-upstream)$/.test(a));
3896
+ case 'push':
3897
+ return rest.some((a) => a === '-u' || a === '--set-upstream');
3898
+ default:
3899
+ return false;
3900
+ }
3901
+ }
3725
3902
  //# sourceMappingURL=git.service.js.map