@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.
- package/dist/modules/mcp/manual-failure-memo.d.ts +16 -1
- package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
- package/dist/modules/mcp/manual-failure-memo.js +23 -1
- package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -2
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +5 -0
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +14 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +47 -1
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +17 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +33 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow.errors.d.ts +26 -0
- package/dist/modules/workflow/workflow.errors.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.errors.js +38 -0
- package/dist/modules/workflow/workflow.errors.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +5 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +25 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +75 -6
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +70 -54
- package/src/modules/mcp/manual-failure-memo.ts +89 -67
- package/src/modules/mcp/mcp.service.ts +1011 -1007
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +39 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +5 -0
- package/src/modules/workflow/__tests__/pending-commits.enqueue-if-absent.test.ts +72 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +111 -2
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +74 -0
- package/src/modules/workflow/git/git.service.ts +50 -0
- package/src/modules/workflow/pending-commits.service.ts +39 -0
- package/src/modules/workflow/workflow.errors.ts +41 -0
- package/src/modules/workflow/workflow.routes.ts +5 -2
- 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
|
-
|
|
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
|
|
684
|
-
// or a save of bytes identical to HEAD
|
|
685
|
-
//
|
|
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,
|