@bevel-software/platform-core-backend 0.3.1 → 0.4.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 (43) hide show
  1. package/dist/modules/mcp/manual-failure-memo.d.ts +16 -1
  2. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  3. package/dist/modules/mcp/manual-failure-memo.js +23 -1
  4. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  5. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  6. package/dist/modules/mcp/mcp.service.js +6 -2
  7. package/dist/modules/mcp/mcp.service.js.map +1 -1
  8. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  9. package/dist/modules/secrets-vault/db-secrets-vault.service.js +5 -0
  10. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  11. package/dist/modules/workflow/git/git.service.d.ts +14 -0
  12. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  13. package/dist/modules/workflow/git/git.service.js +47 -1
  14. package/dist/modules/workflow/git/git.service.js.map +1 -1
  15. package/dist/modules/workflow/pending-commits.service.d.ts +17 -0
  16. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  17. package/dist/modules/workflow/pending-commits.service.js +33 -0
  18. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  19. package/dist/modules/workflow/workflow.errors.d.ts +26 -0
  20. package/dist/modules/workflow/workflow.errors.d.ts.map +1 -1
  21. package/dist/modules/workflow/workflow.errors.js +38 -0
  22. package/dist/modules/workflow/workflow.errors.js.map +1 -1
  23. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  24. package/dist/modules/workflow/workflow.routes.js +5 -2
  25. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  26. package/dist/modules/workflow/workflow.service.d.ts +25 -1
  27. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  28. package/dist/modules/workflow/workflow.service.js +75 -6
  29. package/dist/modules/workflow/workflow.service.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +70 -54
  32. package/src/modules/mcp/manual-failure-memo.ts +89 -67
  33. package/src/modules/mcp/mcp.service.ts +1011 -1007
  34. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +39 -0
  35. package/src/modules/secrets-vault/db-secrets-vault.service.ts +5 -0
  36. package/src/modules/workflow/__tests__/pending-commits.enqueue-if-absent.test.ts +72 -0
  37. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +111 -2
  38. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +74 -0
  39. package/src/modules/workflow/git/git.service.ts +50 -0
  40. package/src/modules/workflow/pending-commits.service.ts +39 -0
  41. package/src/modules/workflow/workflow.errors.ts +41 -0
  42. package/src/modules/workflow/workflow.routes.ts +5 -2
  43. package/src/modules/workflow/workflow.service.ts +83 -5
@@ -60,9 +60,11 @@ import {
60
60
  ChangeRequestConflictsError,
61
61
  DuplicateChangeRequestError,
62
62
  RolesYamlPreservationError,
63
+ PullRebaseConflictError,
63
64
  PushNeedsAgentResolutionError,
64
65
  WorkflowValidationError,
65
66
  } from './workflow.errors.js';
67
+ import { RECOVERY_BOT_EMAIL, RECOVERY_BOT_NAME } from './recovery-bot.js';
66
68
  import { AccessDeniedError } from '../access/access-errors.js';
67
69
 
68
70
  const execFileAsync = promisify(execFile);
@@ -299,8 +301,64 @@ export class WorkflowService implements IWorkflowService {
299
301
  return this.git.fetch(workspaceId);
300
302
  }
301
303
 
302
- updateFromRemote(workspaceId: string): Promise<void> {
303
- return this.git.pull(workspaceId);
304
+ async updateFromRemote(workspaceId: string, user?: AuthUser): Promise<void> {
305
+ try {
306
+ await this.git.pull(workspaceId);
307
+ } catch (err) {
308
+ if (err instanceof PullRebaseConflictError) {
309
+ await this.queuePullConflictRecovery(workspaceId, err, user);
310
+ }
311
+ throw err;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * A pull hit a rebase conflict — the workspace carries local commits that
317
+ * conflict with origin, a state that never resolves on its own (every
318
+ * retry pull re-hits it, and the local commits are saved-but-unshared
319
+ * content violating save=share). Queue ONE pending-commits row for it so
320
+ * the worker's existing retry → recovery-agent → escalate ladder takes
321
+ * over in the background.
322
+ *
323
+ * `enqueueIfAbsent` (not `enqueue`): sync attempts can fire repeatedly
324
+ * (every branch focus / merge), and a fresh `enqueue` would reset the
325
+ * existing row's retry counters each time — starving the ladder so the
326
+ * recovery agent never spawns. It also refuses to resurrect a
327
+ * `needs_attention` row: once the ladder has escalated to a human, more
328
+ * agent runs on the same divergence are just a loop.
329
+ *
330
+ * One row, first conflicted path as the representative: the recovery
331
+ * agent's job is "bring the BRANCH into a pushable state", so one ladder
332
+ * per divergence — not one per conflicted file, which would multiply
333
+ * agent runs for a single underlying conflict.
334
+ *
335
+ * Best-effort by design — the caller is already surfacing the conflict
336
+ * error; a queue hiccup must not mask it.
337
+ */
338
+ private async queuePullConflictRecovery(
339
+ workspaceId: string,
340
+ err: PullRebaseConflictError,
341
+ user?: AuthUser,
342
+ ): Promise<void> {
343
+ try {
344
+ const queued = await this.pendingCommits.enqueueIfAbsent({
345
+ workspaceId,
346
+ branch: err.branch,
347
+ path: err.conflictedPaths[0],
348
+ authorEmail: user?.email ?? RECOVERY_BOT_EMAIL,
349
+ authorName: user?.name ?? RECOVERY_BOT_NAME,
350
+ });
351
+ if (queued) {
352
+ console.warn(
353
+ `[workflow] pull conflict on ws=${workspaceId} branch=${err.branch} (${err.conflictedPaths.join(', ')}) — queued background recovery`,
354
+ );
355
+ }
356
+ } catch (queueErr) {
357
+ console.error(
358
+ `[workflow] failed to queue pull-conflict recovery for ws=${workspaceId}:`,
359
+ queueErr instanceof Error ? queueErr.message : queueErr,
360
+ );
361
+ }
304
362
  }
305
363
 
306
364
  resetToRemote(workspaceId: string, branch: string): Promise<void> {
@@ -680,9 +738,21 @@ export class WorkflowService implements IWorkflowService {
680
738
  ): Promise<void> {
681
739
  const change = await this.git.commitFile(workspaceId, user, targetPath);
682
740
  if (!change) {
683
- // No-op commit (path was already clean — typical for a double-enqueue
684
- // or a save of bytes identical to HEAD). Nothing to push; nothing to
685
- // emit. The worker treats this as success and drops the row.
741
+ // No-op commit — the path was already clean. Usually a double-enqueue
742
+ // or a save of bytes identical to HEAD, BUT a clean tree does NOT
743
+ // prove there's nothing to share: the autosave path
744
+ // (`commitFileWhileLocked`) commits locally with a best-effort push,
745
+ // and when that push fails the commit stays stranded on the local
746
+ // branch with the tree clean. Treating that as success dropped the
747
+ // row without ever starting the retry → recovery-agent ladder —
748
+ // which is exactly how a diverged workspace ends up stuck forever.
749
+ // So: if the branch is ahead of its remote-tracking ref, push (with
750
+ // the same cooperative recovery, so a conflicting divergence throws
751
+ // and the worker's ladder takes over). Nothing to emit either way —
752
+ // there's no new sha.
753
+ if (await this.git.hasUnpushedCommits(workspaceId)) {
754
+ await this.pushWithRecovery(workspaceId, branch, targetPath, user);
755
+ }
686
756
  return;
687
757
  }
688
758
  await this.pushWithRecovery(workspaceId, branch, targetPath, user);
@@ -1352,6 +1422,14 @@ export class WorkflowService implements IWorkflowService {
1352
1422
  const targetWorkspace = await this.workspaceService.getOrCreateForBranch(baseBranch);
1353
1423
  await this.git.pull(targetWorkspace.id);
1354
1424
  } catch (err) {
1425
+ // Still best-effort for the merge response (the merge already landed
1426
+ // on origin) — but a rebase CONFLICT here means the target workspace
1427
+ // is stranded (local commits vs origin, never self-heals), so queue
1428
+ // the background recovery ladder before shrugging.
1429
+ if (err instanceof PullRebaseConflictError) {
1430
+ const targetWorkspaceId = encodeURIComponent(baseBranch);
1431
+ await this.queuePullConflictRecovery(targetWorkspaceId, err, user);
1432
+ }
1355
1433
  console.warn(
1356
1434
  `[merge] post-merge pull of target "${baseBranch}" failed — its workspace may be momentarily behind origin`,
1357
1435
  err,