baxian 2.0.3 → 2.0.5
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/README.md +2 -2
- package/README.zh-CN.md +3 -3
- package/dist/agent/bootstrap.js +4 -4
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +7 -10
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/detect/manifest.d.ts +2 -1
- package/dist/agent/detect/manifest.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts +2 -0
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +48 -4
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +41 -4
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +755 -105
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts +0 -1
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +1 -5
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +2 -3
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/repo-store.d.ts +9 -15
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +156 -151
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/tmux-probe-poller.js +4 -4
- package/dist/agent/tmux.d.ts +0 -5
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +1 -1
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +2 -7
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.js +7 -7
- package/dist/api/hosts.js +6 -6
- package/dist/api/platform-guard.js +2 -2
- package/dist/api/projects.js +14 -14
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +94 -57
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/config/backup.d.ts +3 -2
- package/dist/config/backup.d.ts.map +1 -1
- package/dist/config/backup.js +78 -15
- package/dist/config/backup.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +7 -6
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.js +29 -29
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +3 -0
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/ws.d.ts.map +1 -1
- package/dist/event/ws.js +3 -3
- package/dist/event/ws.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/restart-sentinel.d.ts.map +1 -1
- package/dist/lifecycle/restart-sentinel.js +2 -1
- package/dist/lifecycle/restart-sentinel.js.map +1 -1
- package/dist/platform/feedback.d.ts +5 -1
- package/dist/platform/feedback.d.ts.map +1 -1
- package/dist/platform/feedback.js +7 -1
- package/dist/platform/feedback.js.map +1 -1
- package/dist/platform/git-driver.d.ts +0 -2
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +2 -2
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/manifest.d.ts +0 -1
- package/dist/platform/manifest.d.ts.map +1 -1
- package/dist/platform/manifest.js +0 -12
- package/dist/platform/manifest.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +7 -2
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +8 -6
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +1 -1
- package/dist/platform/pr-binding.d.ts +2 -1
- package/dist/platform/pr-binding.d.ts.map +1 -1
- package/dist/platform/pr-conversation-cache.d.ts +11 -11
- package/dist/platform/pr-conversation-cache.d.ts.map +1 -1
- package/dist/platform/pr-conversation-cache.js +64 -27
- package/dist/platform/pr-conversation-cache.js.map +1 -1
- package/dist/platform/review-timeline.d.ts +17 -2
- package/dist/platform/review-timeline.d.ts.map +1 -1
- package/dist/platform/review-timeline.js +93 -84
- package/dist/platform/review-timeline.js.map +1 -1
- package/dist/platform/verdict-engine.d.ts +3 -2
- package/dist/platform/verdict-engine.d.ts.map +1 -1
- package/dist/shared/constants.d.ts +1 -3
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +0 -2
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +33 -3
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +14 -0
- package/dist/shared/types.js.map +1 -1
- package/dist/state/agent-store.d.ts.map +1 -1
- package/dist/state/agent-store.js +2 -1
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/error-record-store.d.ts.map +1 -1
- package/dist/state/error-record-store.js +2 -1
- package/dist/state/error-record-store.js.map +1 -1
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +2 -1
- package/dist/state/lock.js.map +1 -1
- package/dist/state/managed-path.d.ts +2 -0
- package/dist/state/managed-path.d.ts.map +1 -0
- package/dist/state/managed-path.js +11 -0
- package/dist/state/managed-path.js.map +1 -0
- package/dist/state/pet-store.d.ts.map +1 -1
- package/dist/state/pet-store.js +2 -1
- package/dist/state/pet-store.js.map +1 -1
- package/dist/state/process-lock.d.ts.map +1 -1
- package/dist/state/process-lock.js +2 -1
- package/dist/state/process-lock.js.map +1 -1
- package/dist/state/snapshot.d.ts +0 -1
- package/dist/state/snapshot.d.ts.map +1 -1
- package/dist/state/snapshot.js +0 -13
- package/dist/state/snapshot.js.map +1 -1
- package/dist/state/task-store.d.ts +0 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +97 -24
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-CzVzThuT.js +15 -0
- package/dist/web/assets/{index-yqxawAPJ.css → index-Rmp4wT_5.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/agent/lineage.d.ts +0 -14
- package/dist/agent/lineage.d.ts.map +0 -1
- package/dist/agent/lineage.js +0 -33
- package/dist/agent/lineage.js.map +0 -1
- package/dist/web/assets/index-CiAnjs1_.js +0 -15
package/dist/agent/manager.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
4
|
import { join, normalize } from 'node:path';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { createSignalToken, PASSIVE_VERDICT_WATCH } from './phase-signal.js';
|
|
6
7
|
import { tmuxInstallHint } from './preflight.js';
|
|
7
|
-
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, effectiveTaskReviewRound, isSpecStagePhase, isTaskOpen, repoIdentityKey, taskGenerationGuard, taskReviewRound, taskMatchesGeneration, } from '../shared/index.js';
|
|
8
|
+
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, effectiveTaskReviewRound, isSpecStagePhase, isTaskOpen, repoIdentityKey, taskAttentionGeneration, taskGenerationGuard, taskReviewRound, taskMatchesGeneration, } from '../shared/index.js';
|
|
8
9
|
import { buildProjectDriver, makeDriverExec } from '../platform/driver-host.js';
|
|
9
10
|
import { DriverOpError, buildDriverRunContext, GitDriver, safeDriverErrorText } from '../platform/git-driver.js';
|
|
11
|
+
import { versionTimeOf } from '../platform/row-schema.js';
|
|
10
12
|
import { checkOpenPrBinding, checkPrBinding } from '../platform/pr-binding.js';
|
|
11
13
|
import { collectPendingFeedback, scanCommentSourcesOnce } from '../platform/feedback.js';
|
|
12
|
-
import {
|
|
14
|
+
import { prReviewCacheRevision } from '../platform/pr-conversation-cache.js';
|
|
15
|
+
import { deadTokens, recheckPassProvenance } from '../platform/verdict-engine.js';
|
|
13
16
|
import { platformBindingMismatch, taskNeedsPlatformBindingAudit, } from '../platform/startup.js';
|
|
14
17
|
import { AGENT_STORE_NOOP } from '../state/agent-store.js';
|
|
15
18
|
import { SkillRegistry } from '../skill/registry.js';
|
|
16
19
|
import { createRunner, LocalRunner, shellQuote, resolveAgentHost, hostGroupKey, workdirHostGroupKey, } from './runner.js';
|
|
17
|
-
import { isTransientNetworkFailure } from './net-exec.js';
|
|
20
|
+
import { execOutcomeUnknown, isTransientNetworkFailure } from './net-exec.js';
|
|
18
21
|
import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js';
|
|
19
22
|
import { TmuxManager, ReplNotReadyError, detectStartupDialog, detectRuntimeMenu, runtimeBusyCheck, hasRuntimeReadyView, hasReplProcTitle, hasOscTitleWorking, hasOscTitleIdle, screenAllowsTitleIdle, PaneGoneError, } from './tmux.js';
|
|
20
23
|
import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch } from './branch.js';
|
|
21
|
-
import { RepoStore, createRepoStoreCache, moveFileIntoPlace,
|
|
24
|
+
import { RepoStore, createRepoStoreCache, moveFileIntoPlace, stageFile, trashBatchDirUnder } from './repo-store.js';
|
|
22
25
|
import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
|
|
23
26
|
import { buildPromptInline, buildGreetingPrompt, PromptSizeError, RequiredSkillsMissingError, MAX_PROMPT_BYTES_ROUTE_LIMIT, } from './prompt.js';
|
|
24
27
|
import { ApiError } from '../errors.js';
|
|
@@ -27,6 +30,7 @@ import { resolveProjectTool } from '../config/validator.js';
|
|
|
27
30
|
import { SHA_HEX_SOURCE } from '../platform/types.js';
|
|
28
31
|
import { bodyDigest } from '../platform/body-digest.js';
|
|
29
32
|
import { validateCommentBody } from '../platform/command-renderer.js';
|
|
33
|
+
import { buildReviewTokenLine, extractReviewTokens, rowTokens } from '../platform/markers.js';
|
|
30
34
|
const BENIGN_DISPATCH_CONFLICT_CODES = new Set(['dispatch-superseded', 'dispatch-in-flight']);
|
|
31
35
|
export function isBenignDispatchConflict(err) {
|
|
32
36
|
return err instanceof ApiError
|
|
@@ -39,6 +43,16 @@ function requireTaskQaAgentId(task, operation) {
|
|
|
39
43
|
throw new Error(`${operation}: task ${task.id} has no QA participant`);
|
|
40
44
|
return task.qaAgentId;
|
|
41
45
|
}
|
|
46
|
+
function samePassProvenance(left, right) {
|
|
47
|
+
return left !== undefined
|
|
48
|
+
&& right !== undefined
|
|
49
|
+
&& left.sourceKey === right.sourceKey
|
|
50
|
+
&& left.id === right.id
|
|
51
|
+
&& left.bodyDigest === right.bodyDigest
|
|
52
|
+
&& left.token === right.token
|
|
53
|
+
&& left.failToken === right.failToken
|
|
54
|
+
&& left.anchorSha === right.anchorSha;
|
|
55
|
+
}
|
|
42
56
|
export class EnsureSessionError extends Error {
|
|
43
57
|
partial;
|
|
44
58
|
constructor(partial, message) {
|
|
@@ -226,6 +240,7 @@ export class AgentManager {
|
|
|
226
240
|
paneStreamerManager;
|
|
227
241
|
pluginRegistry;
|
|
228
242
|
platformDefaultBranchOf;
|
|
243
|
+
prConversationCache;
|
|
229
244
|
platformDriverExec;
|
|
230
245
|
phaseSignalWatcher;
|
|
231
246
|
errorRecordStore;
|
|
@@ -234,6 +249,7 @@ export class AgentManager {
|
|
|
234
249
|
dispatchSettleTimeoutMs;
|
|
235
250
|
dispatchAckResendIntervalMs = 3_000;
|
|
236
251
|
taskMutationQueue = Promise.resolve();
|
|
252
|
+
taskMutationScope = new AsyncLocalStorage();
|
|
237
253
|
agentOperationQueues = new Map();
|
|
238
254
|
agentIndex;
|
|
239
255
|
platformRunner;
|
|
@@ -254,6 +270,8 @@ export class AgentManager {
|
|
|
254
270
|
cancelInterruptGuardWaitMs = DEFAULT_DISPATCH_ACK_TIMEOUT_MS + 5_000;
|
|
255
271
|
markCompleteInFlight = new Set();
|
|
256
272
|
specVerdictInFlight = new Set();
|
|
273
|
+
codeVerdictInFlight = new Set();
|
|
274
|
+
retryTaskInFlight = new Set();
|
|
257
275
|
cancelCleanupInFlight = new Map();
|
|
258
276
|
deletionInFlight = new Set();
|
|
259
277
|
divergedAgents = new Set();
|
|
@@ -273,6 +291,7 @@ export class AgentManager {
|
|
|
273
291
|
this.paneStreamerManager = deps.paneStreamerManager;
|
|
274
292
|
this.pluginRegistry = deps.pluginRegistry;
|
|
275
293
|
this.platformDefaultBranchOf = deps.platformDefaultBranchOf;
|
|
294
|
+
this.prConversationCache = deps.prConversationCache;
|
|
276
295
|
this.errorRecordStore = deps.errorRecordStore;
|
|
277
296
|
this.phaseSignalWatcher = deps.phaseSignalWatcher
|
|
278
297
|
?? (deps.paneStreamerManager
|
|
@@ -299,10 +318,26 @@ export class AgentManager {
|
|
|
299
318
|
};
|
|
300
319
|
}
|
|
301
320
|
withTaskLock(fn) {
|
|
302
|
-
const
|
|
321
|
+
const inherited = this.taskMutationScope.getStore();
|
|
322
|
+
if (inherited?.active)
|
|
323
|
+
return fn();
|
|
324
|
+
const next = this.taskMutationQueue.then(() => {
|
|
325
|
+
const scope = { active: true };
|
|
326
|
+
return this.taskMutationScope.run(scope, async () => {
|
|
327
|
+
try {
|
|
328
|
+
return await fn();
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
scope.active = false;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
303
335
|
this.taskMutationQueue = next.catch(() => undefined);
|
|
304
336
|
return next;
|
|
305
337
|
}
|
|
338
|
+
outsideTaskMutationScope(fn) {
|
|
339
|
+
return this.taskMutationScope.exit(fn);
|
|
340
|
+
}
|
|
306
341
|
withAgentOperationLease(agentId, fn) {
|
|
307
342
|
const previous = this.agentOperationQueues.get(agentId) ?? Promise.resolve();
|
|
308
343
|
const next = previous.then(fn);
|
|
@@ -365,7 +400,7 @@ export class AgentManager {
|
|
|
365
400
|
return true;
|
|
366
401
|
}
|
|
367
402
|
catch (err) {
|
|
368
|
-
console.warn(`[AgentManager] safeEmit ${event.type} failed
|
|
403
|
+
console.warn(`[AgentManager] safeEmit ${event.type} failed; persistence or handler delivery may be incomplete:`, err);
|
|
369
404
|
return false;
|
|
370
405
|
}
|
|
371
406
|
}
|
|
@@ -621,12 +656,14 @@ export class AgentManager {
|
|
|
621
656
|
const subdir = skillSubdirFor(agent.runtime);
|
|
622
657
|
const destRoot = `${workdir}/${subdir}`;
|
|
623
658
|
await this.excludeInjectedSkills(runner, workdir, subdir);
|
|
659
|
+
// Batch inside the swept tree itself: a symlinked skills dir may live on another filesystem, where mv degrades to copy+unlink.
|
|
660
|
+
const trashDir = trashBatchDirUnder(`${destRoot}/.baxian-trash`, 'skills');
|
|
624
661
|
await this.runUnderSkillDirLock(this.skillDirLockKey(agent, workdir), async () => {
|
|
625
662
|
await this.ensureSkillDirSafe(runner, workdir, subdir, [
|
|
626
663
|
...this.skillRegistry.names(),
|
|
627
664
|
...this.skillRegistry.pluginSkillNames(scope),
|
|
628
|
-
]);
|
|
629
|
-
await this.skillRegistry.materialize((path, content) => this.atomicWriteFile(runner,
|
|
665
|
+
], trashDir);
|
|
666
|
+
await this.skillRegistry.materialize((path, content) => this.atomicWriteFile(runner, path, content, trashDir), destRoot, scope);
|
|
630
667
|
});
|
|
631
668
|
}
|
|
632
669
|
sessionLifecycleChain = new Map();
|
|
@@ -714,33 +751,44 @@ export class AgentManager {
|
|
|
714
751
|
});
|
|
715
752
|
return run;
|
|
716
753
|
}
|
|
717
|
-
async atomicWriteFile(runner,
|
|
754
|
+
async atomicWriteFile(runner, finalPath, content, trashDir) {
|
|
718
755
|
const tmp = `${finalPath}.baxian-tmp-${randomBytes(6).toString('hex')}`;
|
|
719
756
|
try {
|
|
720
|
-
await
|
|
721
|
-
await moveFileIntoPlace(runner, tmp, finalPath, {
|
|
757
|
+
await stageFile(runner, tmp, content, { trashDir });
|
|
758
|
+
await moveFileIntoPlace(runner, tmp, finalPath, { trashDir });
|
|
722
759
|
}
|
|
723
760
|
catch (err) {
|
|
724
761
|
throw new Error(`atomic skill write failed (${finalPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
725
762
|
}
|
|
726
763
|
}
|
|
727
|
-
async ensureSkillDirSafe(runner, workdir, subdir, keepNames) {
|
|
728
|
-
const
|
|
764
|
+
async ensureSkillDirSafe(runner, workdir, subdir, keepNames, trashDir) {
|
|
765
|
+
const SKILLS_SWEPT = 'BX_SKILLS_SWEPT';
|
|
729
766
|
const keep = keepNames.map((n) => `! -name ${shellQuote(n)}`).join(' ');
|
|
730
|
-
const
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
767
|
+
const batch = shellQuote(trashDir);
|
|
768
|
+
// Stale directories go first so the later link/file passes only see what stayed behind in kept dirs. -H follows a symlinked skills dir given as the start path; -exec … + propagates a mover failure into find's exit code (\; would swallow it); pruning .baxian-trash keeps the never-emptied trash out of the walk (it only grows, and one unreadable old batch would fail every sweep).
|
|
769
|
+
const prune = `-name .baxian-trash -prune -o`;
|
|
770
|
+
const findStaleDirs = `find -H ${subdir} -maxdepth 1 ${prune} -name 'baxian-*' ${keep}`;
|
|
771
|
+
const findStaleLinks = `find -H ${subdir} ${prune} -path '${subdir}/baxian-*' -type l`;
|
|
772
|
+
const findStaleFiles = `find -H ${subdir} ${prune} -path '${subdir}/baxian-*/*' -type f ! -name 'SKILL.md'`;
|
|
773
|
+
const mover = `dest="$0"; for f in "$@"; do ` +
|
|
774
|
+
`mkdir -p "$dest" || exit 1; ` +
|
|
775
|
+
`base=$(basename -- "$f"); t="$dest/$base"; n=0; ` +
|
|
776
|
+
`while [ -e "$t" ] || [ -L "$t" ]; do n=$((n+1)); t="$dest/$base.$n"; done; ` +
|
|
777
|
+
`mv -- "$f" "$t" || exit 1; ` +
|
|
778
|
+
`done`;
|
|
779
|
+
const moveStale = (find) => `${find} -exec sh -c '${mover}' ${batch} {} +`;
|
|
780
|
+
const inner = `cd ${shellQuote(workdir)} && ` +
|
|
737
781
|
`mkdir -p ${subdir} && ` +
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
782
|
+
`${moveStale(findStaleDirs)} && ` +
|
|
783
|
+
`${moveStale(findStaleLinks)} && ` +
|
|
784
|
+
`${moveStale(findStaleFiles)} && ` +
|
|
785
|
+
`printf '%s' ${SKILLS_SWEPT}`;
|
|
741
786
|
const res = await runner.exec(`sh -c ${shellQuote(inner)}`);
|
|
742
|
-
if (res
|
|
743
|
-
throw new Error(`
|
|
787
|
+
if (execOutcomeUnknown(res)) {
|
|
788
|
+
throw new Error(`skill sweep outcome unknown in ${workdir}/${subdir} (exit ${res.exitCode}): ${res.stderr.trim() || res.stdout.trim() || 'no output'}`);
|
|
789
|
+
}
|
|
790
|
+
if (res.exitCode !== 0 || !res.stdout.includes(SKILLS_SWEPT)) {
|
|
791
|
+
throw new Error(`failed to prepare ${subdir} in ${workdir}: ${res.stderr.trim() || `exit ${res.exitCode}, no completion marker`}`);
|
|
744
792
|
}
|
|
745
793
|
}
|
|
746
794
|
async tagSessionSkillsVersion(tmux, agentId, ref) {
|
|
@@ -757,18 +805,25 @@ export class AgentManager {
|
|
|
757
805
|
return tagged !== this.skillRegistry.contentHash(this.agentSkillScope(agentId));
|
|
758
806
|
}
|
|
759
807
|
async excludeInjectedSkills(runner, workdir, subdir) {
|
|
760
|
-
|
|
761
|
-
|
|
808
|
+
// cd -P first: tracked/exclude/probe all run against the skills dir's PHYSICAL owning repo, so a symlink pointing back inside a worktree keeps the tracked-content protection while one pointing outside degrades to the plain non-git path.
|
|
809
|
+
const inner = `cd ${shellQuote(workdir)} && ` +
|
|
810
|
+
`mkdir -p ${subdir} && cd -P ${subdir} && ` +
|
|
762
811
|
`if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo BX_SKILLS_NON_GIT; exit 0; fi; ` +
|
|
763
|
-
`tracked="$(git ls-files --
|
|
812
|
+
`tracked="$(git ls-files -- 'baxian-*' 2>/dev/null | head -5)"; ` +
|
|
764
813
|
`if [ -n "$tracked" ]; then printf 'BX_SKILLS_TRACKED %s\\n' "$tracked" | head -1; exit 0; fi; ` +
|
|
765
814
|
`p="$(git rev-parse --git-path info/exclude)" || { echo BX_SKILLS_EXCLUDE_WRITE_FAILED; exit 0; }; ` +
|
|
766
|
-
|
|
767
|
-
`
|
|
815
|
+
// show-prefix is a literal path but lands in a gitignore pattern: escape fnmatch metacharacters or a dir like skill[dir] never matches its own rule.
|
|
816
|
+
`pre="$(git rev-parse --show-prefix 2>/dev/null | sed 's/[][\\*?]/\\\\&/g')"; rule="/\${pre}baxian-*"; trashrule="/\${pre}.baxian-trash"; ` +
|
|
817
|
+
`{ mkdir -p "$(dirname "$p")" && { grep -qxF "$rule" "$p" 2>/dev/null || printf '%s\\n' "$rule" >> "$p"; } ` +
|
|
818
|
+
`&& { grep -qxF "$trashrule" "$p" 2>/dev/null || printf '%s\\n' "$trashrule" >> "$p"; }; } ` +
|
|
768
819
|
`|| { echo BX_SKILLS_EXCLUDE_WRITE_FAILED; exit 0; }; ` +
|
|
769
|
-
`if git check-ignore -q --
|
|
820
|
+
`if git check-ignore -q -- 'baxian-__probe__/SKILL.md' ` +
|
|
821
|
+
`&& git check-ignore -q -- '.baxian-trash/__probe__'; then echo BX_SKILLS_OK; ` +
|
|
770
822
|
`else echo BX_SKILLS_EXCLUDE_INEFFECTIVE; fi`;
|
|
771
823
|
const res = await runner.exec(`sh -c ${shellQuote(inner)}`);
|
|
824
|
+
if (execOutcomeUnknown(res)) {
|
|
825
|
+
throw new Error(`skill exclusion probe outcome unknown in ${workdir} (exit ${res.exitCode}): ${res.stderr.trim() || res.stdout.trim() || 'no output'}`);
|
|
826
|
+
}
|
|
772
827
|
if (res.exitCode !== 0) {
|
|
773
828
|
throw new Error(`skill exclusion probe failed in ${workdir}: ${res.stderr.trim() || `exit ${res.exitCode}`}`);
|
|
774
829
|
}
|
|
@@ -911,7 +966,7 @@ export class AgentManager {
|
|
|
911
966
|
}
|
|
912
967
|
}
|
|
913
968
|
await this.markDialogPending(agentId, creationToken);
|
|
914
|
-
void this.slowPollDialogPending(agentId, creationToken).catch((pollErr) => {
|
|
969
|
+
void this.outsideTaskMutationScope(() => this.slowPollDialogPending(agentId, creationToken)).catch((pollErr) => {
|
|
915
970
|
console.warn(`[bootstrap] slowPoll for ${agentId} crashed:`, pollErr);
|
|
916
971
|
});
|
|
917
972
|
return;
|
|
@@ -1184,10 +1239,10 @@ export class AgentManager {
|
|
|
1184
1239
|
err.partial.handled = true;
|
|
1185
1240
|
const snapshotPaneId = state.paneId;
|
|
1186
1241
|
const snapshotTaskId = state.taskId;
|
|
1187
|
-
void this.slowPollDialogPending(agentId, state.creationToken, {
|
|
1242
|
+
void this.outsideTaskMutationScope(() => this.slowPollDialogPending(agentId, state.creationToken, {
|
|
1188
1243
|
...(snapshotPaneId !== undefined ? { expectedPaneId: snapshotPaneId } : {}),
|
|
1189
1244
|
expectedTaskId: snapshotTaskId,
|
|
1190
|
-
}).catch((pollErr) => {
|
|
1245
|
+
})).catch((pollErr) => {
|
|
1191
1246
|
console.warn(`[runtime] slowPoll for ${agentId} crashed:`, pollErr);
|
|
1192
1247
|
});
|
|
1193
1248
|
return true;
|
|
@@ -1255,6 +1310,7 @@ export class AgentManager {
|
|
|
1255
1310
|
return;
|
|
1256
1311
|
const now = new Date().toISOString();
|
|
1257
1312
|
let projectIdForEmit = '';
|
|
1313
|
+
let taskIdForEmit;
|
|
1258
1314
|
let wrote = false;
|
|
1259
1315
|
const isBootstrapPath = creationToken !== undefined;
|
|
1260
1316
|
if (isBootstrapPath && !(await this.runGreetingHandshake(agentId, cfg, pane))) {
|
|
@@ -1270,6 +1326,7 @@ export class AgentManager {
|
|
|
1270
1326
|
return AGENT_STORE_NOOP;
|
|
1271
1327
|
}
|
|
1272
1328
|
projectIdForEmit = fresh.projectId;
|
|
1329
|
+
taskIdForEmit = fresh.taskId;
|
|
1273
1330
|
wrote = true;
|
|
1274
1331
|
if (isBootstrapPath) {
|
|
1275
1332
|
return {
|
|
@@ -1283,11 +1340,15 @@ export class AgentManager {
|
|
|
1283
1340
|
updatedAt: now,
|
|
1284
1341
|
};
|
|
1285
1342
|
}
|
|
1343
|
+
const awaitingReason = fresh.taskId
|
|
1344
|
+
? `Runtime dialog resolved; agent REPL ready. Open task ${fresh.taskId}: cancel it if it is still active, ` +
|
|
1345
|
+
'otherwise Resume can release the binding; Delete the agent if the binding cannot be recovered.'
|
|
1346
|
+
: 'Runtime dialog resolved; agent REPL ready. Click Resume to release the binding and let baxian pick the next task.';
|
|
1286
1347
|
return {
|
|
1287
1348
|
...fresh,
|
|
1288
1349
|
paneId,
|
|
1289
1350
|
awaitingPhase: 'agent_dialog_resolved_runtime',
|
|
1290
|
-
awaitingReason
|
|
1351
|
+
awaitingReason,
|
|
1291
1352
|
updatedAt: now,
|
|
1292
1353
|
};
|
|
1293
1354
|
});
|
|
@@ -1304,9 +1365,13 @@ export class AgentManager {
|
|
|
1304
1365
|
});
|
|
1305
1366
|
}
|
|
1306
1367
|
else {
|
|
1307
|
-
|
|
1368
|
+
const note = taskIdForEmit
|
|
1369
|
+
? `Runtime dialog resolved; agent REPL ready. Open task ${taskIdForEmit}: cancel it if it is still active, ` +
|
|
1370
|
+
'otherwise Resume can release the binding; Delete the agent if the binding cannot be recovered.'
|
|
1371
|
+
: 'Runtime dialog resolved; agent REPL ready. Click Resume to continue.';
|
|
1372
|
+
await this.emitIntervention(projectIdForEmit, agentId, taskIdForEmit, {
|
|
1308
1373
|
phase: 'agent_dialog_resolved_runtime',
|
|
1309
|
-
note
|
|
1374
|
+
note,
|
|
1310
1375
|
});
|
|
1311
1376
|
}
|
|
1312
1377
|
return;
|
|
@@ -1317,7 +1382,7 @@ export class AgentManager {
|
|
|
1317
1382
|
return;
|
|
1318
1383
|
const controller = new AbortController();
|
|
1319
1384
|
this.runtimeMenuWatchers.set(agentId, controller);
|
|
1320
|
-
void this.runtimeMenuWatchLoop(agentId, controller.signal)
|
|
1385
|
+
void this.outsideTaskMutationScope(() => this.runtimeMenuWatchLoop(agentId, controller.signal))
|
|
1321
1386
|
.catch((err) => {
|
|
1322
1387
|
console.warn(`[runtimeMenuWatch] ${agentId} loop crashed:`, err);
|
|
1323
1388
|
})
|
|
@@ -1378,6 +1443,10 @@ export class AgentManager {
|
|
|
1378
1443
|
}
|
|
1379
1444
|
else if (!onMenu && pendingMenu) {
|
|
1380
1445
|
pendingMenu = false;
|
|
1446
|
+
await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
|
|
1447
|
+
phase: 'agent_runtime_menu_resolved',
|
|
1448
|
+
previousPhase: 'agent_runtime_menu_pending',
|
|
1449
|
+
});
|
|
1381
1450
|
}
|
|
1382
1451
|
}
|
|
1383
1452
|
}
|
|
@@ -2272,7 +2341,7 @@ export class AgentManager {
|
|
|
2272
2341
|
if (this.needInputRetryTimer)
|
|
2273
2342
|
return;
|
|
2274
2343
|
this.needInputRetryTimer = setInterval(() => {
|
|
2275
|
-
void this.needInputRetryPass();
|
|
2344
|
+
void this.outsideTaskMutationScope(() => this.needInputRetryPass());
|
|
2276
2345
|
}, this.needInputRetryIntervalMs);
|
|
2277
2346
|
this.needInputRetryTimer.unref?.();
|
|
2278
2347
|
}
|
|
@@ -2464,13 +2533,13 @@ export class AgentManager {
|
|
|
2464
2533
|
if (wm !== null && bumped && mode === 'restore') {
|
|
2465
2534
|
const delta = this.collectPredecessorNeedInputDelta(args.agentId, args.taskId, wm.epoch);
|
|
2466
2535
|
if (delta) {
|
|
2467
|
-
void this.commitNeedInputWatermark({
|
|
2536
|
+
void this.outsideTaskMutationScope(() => this.commitNeedInputWatermark({
|
|
2468
2537
|
agentId: args.agentId,
|
|
2469
2538
|
taskId: args.taskId,
|
|
2470
2539
|
epoch: wm.epoch,
|
|
2471
2540
|
askSeq: Math.max(delta.askSeq, wm.askSeq),
|
|
2472
2541
|
answeredSeq: Math.max(delta.answeredSeq, wm.answeredSeq),
|
|
2473
|
-
});
|
|
2542
|
+
}));
|
|
2474
2543
|
}
|
|
2475
2544
|
}
|
|
2476
2545
|
try {
|
|
@@ -3358,7 +3427,7 @@ export class AgentManager {
|
|
|
3358
3427
|
if (!this.deletionGateOpen(agentId, genAtEntry)) {
|
|
3359
3428
|
throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
|
|
3360
3429
|
}
|
|
3361
|
-
const workdir =
|
|
3430
|
+
const { workdir } = await this.ensureWorkdir(cfg, project, runner);
|
|
3362
3431
|
const paneWorkdir = await tmux.getPaneCurrentPath(pane);
|
|
3363
3432
|
if (!await sameDirOnHost(runner, paneWorkdir, workdir)) {
|
|
3364
3433
|
throw new Error(`restart-repl: pane Workdir ${paneWorkdir} does not match agent Workdir ${workdir}; ` +
|
|
@@ -3762,8 +3831,8 @@ export class AgentManager {
|
|
|
3762
3831
|
const project = this.getProjectConfig(cfg.projectId);
|
|
3763
3832
|
if (!project)
|
|
3764
3833
|
throw new Error(`Unknown project: ${cfg.projectId}`);
|
|
3765
|
-
const
|
|
3766
|
-
return { targets:
|
|
3834
|
+
const team = this.findAgentTeam(agentId);
|
|
3835
|
+
return { targets: team?.map(agent => agent.id) ?? [agentId] };
|
|
3767
3836
|
}
|
|
3768
3837
|
async cleanupRemovedAgentRuntime(targets) {
|
|
3769
3838
|
const failures = [];
|
|
@@ -3862,13 +3931,13 @@ export class AgentManager {
|
|
|
3862
3931
|
const now = new Date().toISOString();
|
|
3863
3932
|
if (cfg.role !== 'dev')
|
|
3864
3933
|
throw new Error(`Agent ${cfg.id} is not a dev agent`);
|
|
3865
|
-
const
|
|
3934
|
+
const team = this.findAgentTeam(cfg.id);
|
|
3866
3935
|
const devAgentId = cfg.id;
|
|
3867
3936
|
const qaAgentId = input.preferredAgentId === ''
|
|
3868
3937
|
? '_unassigned_preview_qa'
|
|
3869
|
-
:
|
|
3938
|
+
: team?.find(agent => agent.role === 'qa')?.id;
|
|
3870
3939
|
if (!qaAgentId)
|
|
3871
|
-
throw new Error(`Agent ${cfg.id} has no qa agent in its
|
|
3940
|
+
throw new Error(`Agent ${cfg.id} has no qa agent in its Agent Team`);
|
|
3872
3941
|
const fakeTask = {
|
|
3873
3942
|
id: 'task-9999999999',
|
|
3874
3943
|
projectId,
|
|
@@ -3910,11 +3979,11 @@ export class AgentManager {
|
|
|
3910
3979
|
return undefined;
|
|
3911
3980
|
return buildProjectDriver(project, this.pluginRegistry, this.platformDriverExec);
|
|
3912
3981
|
}
|
|
3913
|
-
|
|
3982
|
+
findAgentTeam(anchorAgentId) {
|
|
3914
3983
|
for (const project of this.config.project) {
|
|
3915
|
-
for (const
|
|
3916
|
-
if (
|
|
3917
|
-
return
|
|
3984
|
+
for (const team of project.agent) {
|
|
3985
|
+
if (team.some(agent => agent.id === anchorAgentId))
|
|
3986
|
+
return team;
|
|
3918
3987
|
}
|
|
3919
3988
|
}
|
|
3920
3989
|
return undefined;
|
|
@@ -3925,8 +3994,8 @@ export class AgentManager {
|
|
|
3925
3994
|
return undefined;
|
|
3926
3995
|
let longest;
|
|
3927
3996
|
let longestBytes = -1;
|
|
3928
|
-
for (const
|
|
3929
|
-
for (const a of
|
|
3997
|
+
for (const team of project.agent) {
|
|
3998
|
+
for (const a of team) {
|
|
3930
3999
|
if (a.role !== 'dev' || !a.workdir)
|
|
3931
4000
|
continue;
|
|
3932
4001
|
const bytes = Buffer.byteLength(a.workdir, 'utf8');
|
|
@@ -3941,6 +4010,99 @@ export class AgentManager {
|
|
|
3941
4010
|
async getTask(taskId) {
|
|
3942
4011
|
return this.taskStore.get(taskId);
|
|
3943
4012
|
}
|
|
4013
|
+
async recordTaskAttention(event) {
|
|
4014
|
+
if (!event.taskId)
|
|
4015
|
+
return;
|
|
4016
|
+
await this.withTaskLock(async () => {
|
|
4017
|
+
const task = await this.taskStore.get(event.taskId);
|
|
4018
|
+
if (!task)
|
|
4019
|
+
return;
|
|
4020
|
+
const reasonValue = typeof event.data.phase === 'string'
|
|
4021
|
+
? event.data.phase
|
|
4022
|
+
: typeof event.data.reason === 'string'
|
|
4023
|
+
? event.data.reason
|
|
4024
|
+
: 'human-intervention';
|
|
4025
|
+
const reason = reasonValue.trim() || 'human-intervention';
|
|
4026
|
+
const previousPhase = typeof event.data.previousPhase === 'string'
|
|
4027
|
+
? event.data.previousPhase.trim()
|
|
4028
|
+
: '';
|
|
4029
|
+
const clearedReason = reason === 'mr-reopened'
|
|
4030
|
+
? 'mr-closed-unmerged'
|
|
4031
|
+
: reason === 'resumed'
|
|
4032
|
+
|| reason === 'git-review-dispatch-hold-cleared'
|
|
4033
|
+
|| reason === 'agent_runtime_menu_resolved'
|
|
4034
|
+
? previousPhase
|
|
4035
|
+
: '';
|
|
4036
|
+
if (clearedReason) {
|
|
4037
|
+
if (task.attention?.reason !== clearedReason
|
|
4038
|
+
|| Date.parse(event.timestamp) < Date.parse(task.attention.occurredAt))
|
|
4039
|
+
return;
|
|
4040
|
+
const next = { ...task, updatedAt: new Date().toISOString() };
|
|
4041
|
+
delete next.attention;
|
|
4042
|
+
await this.taskStore.set(next);
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
if (reason === 'resumed'
|
|
4046
|
+
|| reason === 'git-review-dispatch-hold-cleared'
|
|
4047
|
+
|| reason === 'agent_runtime_menu_resolved')
|
|
4048
|
+
return;
|
|
4049
|
+
const runbookValue = [
|
|
4050
|
+
event.data.note,
|
|
4051
|
+
event.data.recommendation,
|
|
4052
|
+
event.data.message,
|
|
4053
|
+
event.data.error,
|
|
4054
|
+
...(typeof event.data.phase === 'string' ? [event.data.reason] : []),
|
|
4055
|
+
]
|
|
4056
|
+
.find(value => typeof value === 'string' && value.trim() !== '');
|
|
4057
|
+
const runbook = typeof runbookValue === 'string'
|
|
4058
|
+
? runbookValue.trim()
|
|
4059
|
+
: `Task ${task.id} requires human intervention for ${reason}. Inspect the task and its linked agent before choosing an action.`;
|
|
4060
|
+
const occurredAt = Number.isFinite(Date.parse(event.timestamp))
|
|
4061
|
+
? event.timestamp
|
|
4062
|
+
: new Date().toISOString();
|
|
4063
|
+
const generation = taskAttentionGeneration(task);
|
|
4064
|
+
let recommendedActions;
|
|
4065
|
+
if (task.status === 'max_rounds') {
|
|
4066
|
+
recommendedActions = ['verdict', 'cancel'];
|
|
4067
|
+
}
|
|
4068
|
+
else if (TERMINAL_STATUSES.includes(task.status)) {
|
|
4069
|
+
recommendedActions = task.remoteCleanup !== undefined
|
|
4070
|
+
&& task.remoteCleanup.stage !== 'manual'
|
|
4071
|
+
? ['cancel']
|
|
4072
|
+
: ['retry'];
|
|
4073
|
+
}
|
|
4074
|
+
else if (task.status === 'review') {
|
|
4075
|
+
recommendedActions = isSpecStagePhase(task.phase)
|
|
4076
|
+
? ['advance', 'cancel']
|
|
4077
|
+
: ['advance', 'verdict', 'cancel'];
|
|
4078
|
+
}
|
|
4079
|
+
else if (task.status === 'spec-ready' || task.status === 'merge-ready') {
|
|
4080
|
+
recommendedActions = ['verdict', 'cancel'];
|
|
4081
|
+
}
|
|
4082
|
+
else {
|
|
4083
|
+
recommendedActions = ['advance', 'cancel'];
|
|
4084
|
+
}
|
|
4085
|
+
if (task.attention
|
|
4086
|
+
&& Date.parse(task.attention.occurredAt) > Date.parse(occurredAt))
|
|
4087
|
+
return;
|
|
4088
|
+
if (task.attention
|
|
4089
|
+
&& task.attention.reason === reason
|
|
4090
|
+
&& task.attention.runbook === runbook
|
|
4091
|
+
&& Date.parse(task.attention.occurredAt) >= Date.parse(occurredAt))
|
|
4092
|
+
return;
|
|
4093
|
+
await this.taskStore.set({
|
|
4094
|
+
...task,
|
|
4095
|
+
attention: {
|
|
4096
|
+
reason,
|
|
4097
|
+
runbook,
|
|
4098
|
+
occurredAt,
|
|
4099
|
+
recommendedActions,
|
|
4100
|
+
generation,
|
|
4101
|
+
},
|
|
4102
|
+
updatedAt: new Date().toISOString(),
|
|
4103
|
+
});
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
3944
4106
|
async getAgentState(agentId) {
|
|
3945
4107
|
return this.agentStore.get(agentId);
|
|
3946
4108
|
}
|
|
@@ -4027,15 +4189,30 @@ export class AgentManager {
|
|
|
4027
4189
|
delete next.reviewDispatch;
|
|
4028
4190
|
delete next.reviewRoundPending;
|
|
4029
4191
|
}
|
|
4030
|
-
if (nextStatus !== task.status
|
|
4031
|
-
|
|
4032
|
-
if (outbox.length > 0)
|
|
4033
|
-
next.outbox = outbox;
|
|
4034
|
-
else
|
|
4035
|
-
delete next.outbox;
|
|
4036
|
-
}
|
|
4192
|
+
if (nextStatus !== task.status)
|
|
4193
|
+
this.stripGitVerdictOutbox(next);
|
|
4037
4194
|
return next;
|
|
4038
4195
|
}
|
|
4196
|
+
stripGitVerdictOutbox(task) {
|
|
4197
|
+
if (!task.outbox)
|
|
4198
|
+
return;
|
|
4199
|
+
const outbox = task.outbox.filter(entry => entry.type !== 'git.spec-verdict' && entry.type !== 'git.code-verdict');
|
|
4200
|
+
if (outbox.length > 0)
|
|
4201
|
+
task.outbox = outbox;
|
|
4202
|
+
else
|
|
4203
|
+
delete task.outbox;
|
|
4204
|
+
}
|
|
4205
|
+
stripCodeVerdictOutboxForAnchor(task, anchorSha) {
|
|
4206
|
+
if (!task.outbox)
|
|
4207
|
+
return;
|
|
4208
|
+
const normalizedAnchor = anchorSha.toLowerCase();
|
|
4209
|
+
const outbox = task.outbox.filter(entry => entry.type !== 'git.code-verdict'
|
|
4210
|
+
|| entry.data.anchorSha.toLowerCase() === normalizedAnchor);
|
|
4211
|
+
if (outbox.length > 0)
|
|
4212
|
+
task.outbox = outbox;
|
|
4213
|
+
else
|
|
4214
|
+
delete task.outbox;
|
|
4215
|
+
}
|
|
4039
4216
|
async confirmPostApprovePromptDelivered(taskId, expected) {
|
|
4040
4217
|
await this.withTaskLock(async () => {
|
|
4041
4218
|
const task = await this.taskStore.get(taskId);
|
|
@@ -4229,6 +4406,9 @@ export class AgentManager {
|
|
|
4229
4406
|
specVerdictOutbox(task) {
|
|
4230
4407
|
return task.outbox?.find((entry) => entry.type === 'git.spec-verdict');
|
|
4231
4408
|
}
|
|
4409
|
+
codeVerdictOutbox(task) {
|
|
4410
|
+
return task.outbox?.find((entry) => entry.type === 'git.code-verdict');
|
|
4411
|
+
}
|
|
4232
4412
|
async mutateTaskOutboxEntry(taskId, key, mutate) {
|
|
4233
4413
|
return this.withTaskLock(async () => {
|
|
4234
4414
|
const task = await this.taskStore.get(taskId);
|
|
@@ -4258,6 +4438,10 @@ export class AgentManager {
|
|
|
4258
4438
|
await this.deliverSpecVerdictOutbox(taskId, entry.key);
|
|
4259
4439
|
continue;
|
|
4260
4440
|
}
|
|
4441
|
+
if (entry.type === 'git.code-verdict') {
|
|
4442
|
+
await this.deliverCodeVerdictOutbox(taskId, entry.key);
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4261
4445
|
await this.eventBus.emit({
|
|
4262
4446
|
id: `outbox:${entry.key}`,
|
|
4263
4447
|
type: 'human.intervention',
|
|
@@ -4379,6 +4563,10 @@ export class AgentManager {
|
|
|
4379
4563
|
return null;
|
|
4380
4564
|
if (Object.hasOwn(opts, 'expectPhase') && current.phase !== opts.expectPhase)
|
|
4381
4565
|
return null;
|
|
4566
|
+
if (current.status === 'review'
|
|
4567
|
+
&& current.reviewHeadAnchorSha?.toLowerCase() === opts.headSha.toLowerCase()
|
|
4568
|
+
&& this.codeVerdictOutbox(current) !== undefined)
|
|
4569
|
+
return null;
|
|
4382
4570
|
if (opts.patch?.branch && opts.patch.branch !== current.branch) {
|
|
4383
4571
|
const existing = await this.findTaskByBranch(opts.patch.branch, current.projectId);
|
|
4384
4572
|
if (existing && existing.id !== taskId)
|
|
@@ -4399,6 +4587,7 @@ export class AgentManager {
|
|
|
4399
4587
|
? 'review'
|
|
4400
4588
|
: 'recheck');
|
|
4401
4589
|
const next = this.stripPostApproveEpisodeFields(current);
|
|
4590
|
+
this.stripGitVerdictOutbox(next);
|
|
4402
4591
|
delete next.passProvenance;
|
|
4403
4592
|
Object.assign(next, opts.patch ?? {}, {
|
|
4404
4593
|
status: 'review',
|
|
@@ -4468,6 +4657,7 @@ export class AgentManager {
|
|
|
4468
4657
|
return null;
|
|
4469
4658
|
const now = new Date().toISOString();
|
|
4470
4659
|
const next = this.stripPostApproveEpisodeFields(current);
|
|
4660
|
+
this.stripGitVerdictOutbox(next);
|
|
4471
4661
|
delete next.reviewDispatch;
|
|
4472
4662
|
delete next.reviewRoundPending;
|
|
4473
4663
|
const postApproveGeneration = createSignalToken();
|
|
@@ -4567,6 +4757,7 @@ export class AgentManager {
|
|
|
4567
4757
|
},
|
|
4568
4758
|
updatedAt: now,
|
|
4569
4759
|
};
|
|
4760
|
+
this.stripCodeVerdictOutboxForAnchor(next, verifiedHeadSha);
|
|
4570
4761
|
await this.taskStore.set(next);
|
|
4571
4762
|
if (qaHold !== undefined) {
|
|
4572
4763
|
const cleared = await this.clearAwaitingHuman(qaId, {
|
|
@@ -4612,6 +4803,9 @@ export class AgentManager {
|
|
|
4612
4803
|
}
|
|
4613
4804
|
next.reviewDispatchedAt = now;
|
|
4614
4805
|
delete next.reviewDispatch;
|
|
4806
|
+
if (next.attention?.reason === 'git-review-dispatch-recovery-uncertain') {
|
|
4807
|
+
delete next.attention;
|
|
4808
|
+
}
|
|
4615
4809
|
await this.taskStore.set(next);
|
|
4616
4810
|
return true;
|
|
4617
4811
|
});
|
|
@@ -5100,20 +5294,28 @@ export class AgentManager {
|
|
|
5100
5294
|
return next;
|
|
5101
5295
|
});
|
|
5102
5296
|
}
|
|
5103
|
-
async noteReviewConversationRevision(taskId) {
|
|
5297
|
+
async noteReviewConversationRevision(taskId, conversation) {
|
|
5104
5298
|
await this.withTaskLock(async () => {
|
|
5105
5299
|
const task = await this.taskStore.get(taskId);
|
|
5106
5300
|
if (!task || TERMINAL_STATUSES.includes(task.status))
|
|
5107
5301
|
return;
|
|
5108
5302
|
const now = new Date().toISOString();
|
|
5109
|
-
|
|
5303
|
+
const next = { ...task, reviewConversationUpdatedAt: now, updatedAt: now };
|
|
5304
|
+
const repoKey = next.platformBinding?.repoKey;
|
|
5305
|
+
if (conversation !== undefined
|
|
5306
|
+
&& this.prConversationCache !== undefined
|
|
5307
|
+
&& repoKey !== undefined
|
|
5308
|
+
&& next.prNumber === conversation.prNumber) {
|
|
5309
|
+
this.prConversationCache.put(next.id, prReviewCacheRevision(next, repoKey), conversation.payload, next.reviewConversationUpdatedAt ?? '');
|
|
5310
|
+
}
|
|
5311
|
+
await this.taskStore.set(next);
|
|
5110
5312
|
});
|
|
5111
5313
|
}
|
|
5112
5314
|
assertPlatformBinding(task) {
|
|
5113
5315
|
const mismatch = platformBindingMismatch(this.config, task);
|
|
5114
5316
|
if (mismatch === undefined)
|
|
5115
5317
|
return;
|
|
5116
|
-
void this.emitPlatformBindingIntervention(task, mismatch);
|
|
5318
|
+
void this.outsideTaskMutationScope(() => this.emitPlatformBindingIntervention(task, mismatch));
|
|
5117
5319
|
if (mismatch.reason === 'missing-binding-snapshot') {
|
|
5118
5320
|
throw new Error(`platform binding missing for task ${task.id}; refusing platform operations`);
|
|
5119
5321
|
}
|
|
@@ -5650,8 +5852,12 @@ export class AgentManager {
|
|
|
5650
5852
|
return null;
|
|
5651
5853
|
if (task.prNumber !== undefined && task.prNumber !== observation.prNumber)
|
|
5652
5854
|
return null;
|
|
5855
|
+
const repairsDeliveryConfirmation = confirmation?.source === 'human'
|
|
5856
|
+
&& (task.status === 'review' || task.status === 'fixing')
|
|
5857
|
+
&& Object.hasOwn(confirmation, 'expectedSignalToken');
|
|
5653
5858
|
if (confirmation !== undefined) {
|
|
5654
5859
|
if (task.status !== 'in_progress'
|
|
5860
|
+
&& !repairsDeliveryConfirmation
|
|
5655
5861
|
&& (task.phase !== confirmation.phase
|
|
5656
5862
|
|| task.deliveryConfirmation?.phase !== confirmation.phase))
|
|
5657
5863
|
return null;
|
|
@@ -5678,7 +5884,7 @@ export class AgentManager {
|
|
|
5678
5884
|
...(confirmation !== undefined
|
|
5679
5885
|
? {
|
|
5680
5886
|
phase: confirmation.phase,
|
|
5681
|
-
deliveryConfirmation: task.status === 'in_progress'
|
|
5887
|
+
deliveryConfirmation: task.status === 'in_progress' || repairsDeliveryConfirmation
|
|
5682
5888
|
? { phase: confirmation.phase, source: confirmation.source, at: now }
|
|
5683
5889
|
: task.deliveryConfirmation,
|
|
5684
5890
|
...(confirmation.branch !== undefined ? { branch: confirmation.branch } : {}),
|
|
@@ -5930,13 +6136,13 @@ export class AgentManager {
|
|
|
5930
6136
|
}
|
|
5931
6137
|
const genAtEntry = this.deletionGenerationOf(input.preferredAgentId);
|
|
5932
6138
|
const target = await this.pickAgent(projectId, input.preferredAgentId);
|
|
5933
|
-
const
|
|
5934
|
-
const dev =
|
|
6139
|
+
const team = this.findAgentTeam(input.preferredAgentId);
|
|
6140
|
+
const dev = team?.find(agent => agent.role === 'dev');
|
|
5935
6141
|
if (!dev)
|
|
5936
|
-
throw new ApiError(409, `Agent ${input.preferredAgentId} has no dev agent in its
|
|
5937
|
-
const qa =
|
|
6142
|
+
throw new ApiError(409, `Agent ${input.preferredAgentId} has no dev agent in its Agent Team`);
|
|
6143
|
+
const qa = team?.find(agent => agent.role === 'qa');
|
|
5938
6144
|
if (!qa)
|
|
5939
|
-
throw new ApiError(409, `Agent ${input.preferredAgentId} has no qa agent in its
|
|
6145
|
+
throw new ApiError(409, `Agent ${input.preferredAgentId} has no qa agent in its Agent Team`);
|
|
5940
6146
|
const participantGens = new Map([dev.id, qa.id]
|
|
5941
6147
|
.map(id => [id, this.deletionGenerationOf(id)]));
|
|
5942
6148
|
const participantsFresh = () => this.deletionGenerationOf(input.preferredAgentId) === genAtEntry
|
|
@@ -5951,7 +6157,7 @@ export class AgentManager {
|
|
|
5951
6157
|
};
|
|
5952
6158
|
const persistQueuedOrReject = (reason) => {
|
|
5953
6159
|
if (!participantsFresh()) {
|
|
5954
|
-
throw new ApiError(409, `Agent ${input.preferredAgentId} or
|
|
6160
|
+
throw new ApiError(409, `Agent ${input.preferredAgentId} or an Agent Team member is being deleted or recreated during task creation; please retry`);
|
|
5955
6161
|
}
|
|
5956
6162
|
return this.persistQueuedTask(queued, reason, input.preferredAgentId);
|
|
5957
6163
|
};
|
|
@@ -6061,6 +6267,9 @@ export class AgentManager {
|
|
|
6061
6267
|
}
|
|
6062
6268
|
async createAndStartTask(projectId, input, opts = {}) {
|
|
6063
6269
|
const task = await this.createTask(projectId, input);
|
|
6270
|
+
return this.startCreatedTask(task, opts);
|
|
6271
|
+
}
|
|
6272
|
+
async startCreatedTask(task, opts = {}) {
|
|
6064
6273
|
if (task.status === 'in_progress' && task.agentId) {
|
|
6065
6274
|
const signalToken = createSignalToken();
|
|
6066
6275
|
await this.updateTask(task.id, this.dispatchTokenFields(signalToken));
|
|
@@ -6238,7 +6447,7 @@ export class AgentManager {
|
|
|
6238
6447
|
await tmux.sendKeysLiteral(pane, command);
|
|
6239
6448
|
await tmux.sendEnter(pane);
|
|
6240
6449
|
guardHandedOff = true;
|
|
6241
|
-
void this.waitForReplPromptReady(tmux, pane, cfg.runtime, this.compactIdleWaitMs)
|
|
6450
|
+
void this.outsideTaskMutationScope(() => this.waitForReplPromptReady(tmux, pane, cfg.runtime, this.compactIdleWaitMs))
|
|
6242
6451
|
.catch(err => {
|
|
6243
6452
|
console.warn(`[AgentManager] sendSlashCommand(${agentId}, ${command}) post idle wait failed:`, err);
|
|
6244
6453
|
})
|
|
@@ -6281,6 +6490,16 @@ export class AgentManager {
|
|
|
6281
6490
|
clearPendingDispatchRetry(taskId) {
|
|
6282
6491
|
this.pendingDispatchRetryByTask.delete(taskId);
|
|
6283
6492
|
}
|
|
6493
|
+
resetPendingDispatchRetryBudget(taskId) {
|
|
6494
|
+
const entry = this.pendingDispatchRetryByTask.get(taskId);
|
|
6495
|
+
if (!entry)
|
|
6496
|
+
return;
|
|
6497
|
+
this.pendingDispatchRetryByTask.set(taskId, {
|
|
6498
|
+
...entry,
|
|
6499
|
+
since: Date.now(),
|
|
6500
|
+
budgetAlerted: undefined,
|
|
6501
|
+
});
|
|
6502
|
+
}
|
|
6284
6503
|
clearPendingDispatchRetryIfMatches(taskId, expected) {
|
|
6285
6504
|
const entry = this.pendingDispatchRetryByTask.get(taskId);
|
|
6286
6505
|
if (entry?.agentId === expected.agentId && entry.signalToken === expected.signalToken) {
|
|
@@ -6450,20 +6669,20 @@ export class AgentManager {
|
|
|
6450
6669
|
}
|
|
6451
6670
|
const now = new Date().toISOString();
|
|
6452
6671
|
const initiallyUnassigned = fresh.preferredAgentId === '';
|
|
6453
|
-
const
|
|
6672
|
+
const team = initiallyUnassigned ? this.findAgentTeam(agentId) : undefined;
|
|
6454
6673
|
const devId = initiallyUnassigned
|
|
6455
|
-
?
|
|
6674
|
+
? team?.find(agent => agent.role === 'dev')?.id
|
|
6456
6675
|
: fresh.devAgentId;
|
|
6457
6676
|
if (!devId) {
|
|
6458
6677
|
await this.lockManager.releaseIfOwner(agentId, taskId, lockToken);
|
|
6459
|
-
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no dev agent in its
|
|
6678
|
+
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no dev agent in its Agent Team` };
|
|
6460
6679
|
}
|
|
6461
6680
|
const qaId = initiallyUnassigned
|
|
6462
|
-
?
|
|
6681
|
+
? team?.find(agent => agent.role === 'qa')?.id
|
|
6463
6682
|
: fresh.qaAgentId;
|
|
6464
6683
|
if (!qaId) {
|
|
6465
6684
|
await this.lockManager.releaseIfOwner(agentId, taskId, lockToken);
|
|
6466
|
-
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no qa agent in its
|
|
6685
|
+
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no qa agent in its Agent Team` };
|
|
6467
6686
|
}
|
|
6468
6687
|
const participantGens = new Map([devId, qaId]
|
|
6469
6688
|
.map(pid => [pid, this.deletionGenerationOf(pid)]));
|
|
@@ -7780,10 +7999,10 @@ export class AgentManager {
|
|
|
7780
7999
|
if (await this.rollbackUndeliveredBootstrap(state))
|
|
7781
8000
|
continue;
|
|
7782
8001
|
await this.markDialogPending(state.id, state.creationToken);
|
|
7783
|
-
void this.slowPollDialogPending(state.id, state.creationToken, {
|
|
8002
|
+
void this.outsideTaskMutationScope(() => this.slowPollDialogPending(state.id, state.creationToken, {
|
|
7784
8003
|
...(state.paneId !== undefined ? { expectedPaneId: state.paneId } : {}),
|
|
7785
8004
|
expectedTaskId: state.taskId,
|
|
7786
|
-
}).catch((pollErr) => {
|
|
8005
|
+
})).catch((pollErr) => {
|
|
7787
8006
|
console.warn(`[recover] slowPoll for ${state.id} crashed:`, pollErr);
|
|
7788
8007
|
});
|
|
7789
8008
|
continue;
|
|
@@ -8369,6 +8588,169 @@ export class AgentManager {
|
|
|
8369
8588
|
return dispatched;
|
|
8370
8589
|
}
|
|
8371
8590
|
}
|
|
8591
|
+
async advanceTask(taskId, opts = {}) {
|
|
8592
|
+
let task = await this.taskStore.get(taskId);
|
|
8593
|
+
if (!task)
|
|
8594
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
8595
|
+
const executor = opts.executor ?? (task.status === 'review' ? 'qa' : 'dev');
|
|
8596
|
+
let updated;
|
|
8597
|
+
if (task.status === 'pending') {
|
|
8598
|
+
if (executor !== 'dev')
|
|
8599
|
+
throw new ApiError(409, 'Pending tasks can only advance to Dev');
|
|
8600
|
+
const result = await this.dispatchPendingTask(taskId, opts.agentId);
|
|
8601
|
+
if (result.errorCode !== undefined)
|
|
8602
|
+
throw new ApiError(result.errorCode, result.error ?? 'Dispatch failed');
|
|
8603
|
+
if (!result.task)
|
|
8604
|
+
throw new ApiError(404, `Task ${taskId} disappeared during dispatch`);
|
|
8605
|
+
updated = result.task;
|
|
8606
|
+
}
|
|
8607
|
+
else if (executor === 'qa') {
|
|
8608
|
+
if (task.status !== 'in_progress' && task.status !== 'fixing' && task.status !== 'review') {
|
|
8609
|
+
throw new ApiError(409, `Task ${taskId} cannot advance to QA from ${task.status}`);
|
|
8610
|
+
}
|
|
8611
|
+
updated = await this.dispatchReviewToQa(taskId, {
|
|
8612
|
+
fromStatus: [task.status],
|
|
8613
|
+
confirmUncertainNotDelivered: true,
|
|
8614
|
+
...(opts.stage !== undefined ? { stage: opts.stage } : {}),
|
|
8615
|
+
...(opts.actorId !== undefined ? { actorId: opts.actorId } : {}),
|
|
8616
|
+
...(opts.prNumber !== undefined ? { prNumber: opts.prNumber } : {}),
|
|
8617
|
+
});
|
|
8618
|
+
}
|
|
8619
|
+
else if (task.status === 'review') {
|
|
8620
|
+
throw new ApiError(409, 'Review cannot advance to Dev without a request-changes verdict');
|
|
8621
|
+
}
|
|
8622
|
+
else if (task.status === 'in_progress' || task.status === 'fixing') {
|
|
8623
|
+
if (!task.devAgentId)
|
|
8624
|
+
throw new ApiError(409, `Task ${taskId} has no Dev participant`);
|
|
8625
|
+
const replayed = await this.redispatchTaskPromptAfterReplRestart(task.devAgentId, taskId);
|
|
8626
|
+
if (!replayed)
|
|
8627
|
+
throw new ApiError(409, `Task ${taskId} changed or its Dev prompt could not be replayed`);
|
|
8628
|
+
updated = (await this.taskStore.get(taskId)) ?? task;
|
|
8629
|
+
}
|
|
8630
|
+
else if (task.status === 'approved') {
|
|
8631
|
+
if (!task.devAgentId)
|
|
8632
|
+
throw new ApiError(409, `Task ${taskId} has no Dev participant`);
|
|
8633
|
+
if (task.postApproveRevoked) {
|
|
8634
|
+
if (opts.confirmRevoked !== true) {
|
|
8635
|
+
throw new ApiError(409, `Task ${taskId} post-approve replay is revoked; confirmRevoked=true is required`);
|
|
8636
|
+
}
|
|
8637
|
+
task = await this.restoreRevokedPostApprove(task);
|
|
8638
|
+
}
|
|
8639
|
+
const replayed = await this.redispatchTaskPromptAfterReplRestart(task.devAgentId, taskId);
|
|
8640
|
+
if (!replayed)
|
|
8641
|
+
throw new ApiError(409, `Task ${taskId} changed or its post-approve prompt could not be replayed`);
|
|
8642
|
+
updated = (await this.taskStore.get(taskId)) ?? task;
|
|
8643
|
+
}
|
|
8644
|
+
else {
|
|
8645
|
+
throw new ApiError(409, `Task ${taskId} cannot be advanced from ${task.status}`);
|
|
8646
|
+
}
|
|
8647
|
+
await this.auditHumanTaskOperation(updated, 'advance', executor, opts.note);
|
|
8648
|
+
return updated;
|
|
8649
|
+
}
|
|
8650
|
+
async restoreRevokedPostApprove(task) {
|
|
8651
|
+
if (!task.postApproveRevoked || task.prNumber === undefined) {
|
|
8652
|
+
throw new ApiError(409, `Task ${task.id} has no revoked post-approve episode`);
|
|
8653
|
+
}
|
|
8654
|
+
const [binding, accepted] = await Promise.all([
|
|
8655
|
+
this.platformVerifyPrBinding(task.id, task.prNumber),
|
|
8656
|
+
this.platformVerifyAcceptedPass(task.id),
|
|
8657
|
+
]);
|
|
8658
|
+
if (!binding.ok) {
|
|
8659
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: PR binding ${binding.reason}`);
|
|
8660
|
+
}
|
|
8661
|
+
if (accepted.kind !== 'valid') {
|
|
8662
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: ${accepted.reason}`);
|
|
8663
|
+
}
|
|
8664
|
+
if (accepted.pendingCount > 0) {
|
|
8665
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: the accepted pass has pending feedback`);
|
|
8666
|
+
}
|
|
8667
|
+
const reviewedPass = task.passProvenance;
|
|
8668
|
+
if (!reviewedPass || binding.headSha.toLowerCase() !== reviewedPass.anchorSha.toLowerCase()) {
|
|
8669
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: the current PR head is not the accepted head`);
|
|
8670
|
+
}
|
|
8671
|
+
return this.withTaskLock(async () => {
|
|
8672
|
+
const fresh = await this.taskStore.get(task.id);
|
|
8673
|
+
if (!fresh || fresh.status !== 'approved'
|
|
8674
|
+
|| fresh.postApproveRevoked?.generation !== task.postApproveRevoked.generation
|
|
8675
|
+
|| fresh.prNumber !== task.prNumber
|
|
8676
|
+
|| !samePassProvenance(fresh.passProvenance, reviewedPass)) {
|
|
8677
|
+
throw new ApiError(409, `Task ${task.id} changed while restoring post-approve replay`);
|
|
8678
|
+
}
|
|
8679
|
+
const generation = createSignalToken();
|
|
8680
|
+
const token = createSignalTokenExcluding(generation);
|
|
8681
|
+
const next = {
|
|
8682
|
+
...this.stripPostApproveEpisodeFields(fresh),
|
|
8683
|
+
postApproveGeneration: generation,
|
|
8684
|
+
postApproveHeadSha: binding.headSha,
|
|
8685
|
+
postApproveToken: token,
|
|
8686
|
+
postApprovePhase: 'installed',
|
|
8687
|
+
pendingRedispatch: true,
|
|
8688
|
+
redispatchCount: 0,
|
|
8689
|
+
updatedAt: new Date().toISOString(),
|
|
8690
|
+
};
|
|
8691
|
+
await this.taskStore.set(next);
|
|
8692
|
+
return next;
|
|
8693
|
+
});
|
|
8694
|
+
}
|
|
8695
|
+
async submitTaskVerdict(taskId, action, comments, note) {
|
|
8696
|
+
const task = await this.taskStore.get(taskId);
|
|
8697
|
+
if (!task)
|
|
8698
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
8699
|
+
let updated;
|
|
8700
|
+
if (task.status === 'spec-ready'
|
|
8701
|
+
|| (task.status === 'max_rounds' && isSpecStagePhase(task.phase))) {
|
|
8702
|
+
if (action !== 'approve' && action !== 'request-changes') {
|
|
8703
|
+
throw new ApiError(409, `Task ${taskId} requires a spec approve or request-changes verdict`);
|
|
8704
|
+
}
|
|
8705
|
+
updated = await this.submitSpecVerdict(taskId, action, comments);
|
|
8706
|
+
}
|
|
8707
|
+
else if (task.status === 'review' && !isSpecStagePhase(task.phase)) {
|
|
8708
|
+
if (action !== 'pass' && action !== 'request-changes') {
|
|
8709
|
+
throw new ApiError(409, `Task ${taskId} requires a code pass or request-changes verdict`);
|
|
8710
|
+
}
|
|
8711
|
+
updated = await this.submitCodeVerdict(taskId, action, comments);
|
|
8712
|
+
}
|
|
8713
|
+
else if (task.status === 'max_rounds') {
|
|
8714
|
+
if (action === 'continue')
|
|
8715
|
+
updated = await this.continueDevRound(taskId);
|
|
8716
|
+
else if (action === 'complete')
|
|
8717
|
+
updated = await this.markTaskComplete(taskId);
|
|
8718
|
+
else
|
|
8719
|
+
throw new ApiError(409, `Task ${taskId} requires continue or complete at code max_rounds`);
|
|
8720
|
+
}
|
|
8721
|
+
else if (task.status === 'merge-ready') {
|
|
8722
|
+
if (action !== 'confirm-merge') {
|
|
8723
|
+
throw new ApiError(409, `Task ${taskId} requires confirm-merge`);
|
|
8724
|
+
}
|
|
8725
|
+
updated = await this.markTaskComplete(taskId);
|
|
8726
|
+
}
|
|
8727
|
+
else {
|
|
8728
|
+
throw new ApiError(409, `Task ${taskId} cannot accept a verdict from ${task.status}`);
|
|
8729
|
+
}
|
|
8730
|
+
await this.auditHumanTaskOperation(updated, 'verdict', action, note);
|
|
8731
|
+
return updated;
|
|
8732
|
+
}
|
|
8733
|
+
async auditHumanTaskOperation(task, operation, action, note, details = {}) {
|
|
8734
|
+
const emitted = await this.safeEmit({
|
|
8735
|
+
id: '',
|
|
8736
|
+
type: 'task.updated',
|
|
8737
|
+
timestamp: new Date().toISOString(),
|
|
8738
|
+
projectId: task.projectId,
|
|
8739
|
+
...(task.agentId ? { agentId: task.agentId } : {}),
|
|
8740
|
+
taskId: task.id,
|
|
8741
|
+
data: {
|
|
8742
|
+
...details,
|
|
8743
|
+
operation,
|
|
8744
|
+
action,
|
|
8745
|
+
actor: 'human',
|
|
8746
|
+
provenance: 'human',
|
|
8747
|
+
note: note?.trim() || `${operation}:${action}`,
|
|
8748
|
+
},
|
|
8749
|
+
});
|
|
8750
|
+
if (!emitted) {
|
|
8751
|
+
throw new ApiError(500, `Task ${task.id} ${operation} completed, but its audit event could not be persisted; verify the task state before retrying`);
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8372
8754
|
async dispatchGitFixToDev(taskId) {
|
|
8373
8755
|
const task = await this.taskStore.get(taskId);
|
|
8374
8756
|
if (!task || task.status !== 'fixing')
|
|
@@ -8461,7 +8843,7 @@ export class AgentManager {
|
|
|
8461
8843
|
const devState = await this.agentStore.get(devAgentId);
|
|
8462
8844
|
if (devState?.taskId !== taskId || !devState.workdir) {
|
|
8463
8845
|
throw new ApiError(409, `Dev ${devAgentId} no longer holds task ${taskId}'s Workdir (cannot continue); ` +
|
|
8464
|
-
`
|
|
8846
|
+
`submit a complete verdict to merge the PR as-is, or cancel the task`);
|
|
8465
8847
|
}
|
|
8466
8848
|
const prevReviewRound = task.reviewRound;
|
|
8467
8849
|
const transitioned = await this.transitionTaskStatus(taskId, 'fixing', { fromStatus: ['max_rounds'] }, { reviewRound: prevReviewRound + 1, fixDispatchedAt: new Date().toISOString() });
|
|
@@ -8553,26 +8935,53 @@ export class AgentManager {
|
|
|
8553
8935
|
const t = await this.taskStore.get(taskId);
|
|
8554
8936
|
if (!t)
|
|
8555
8937
|
throw new ApiError(404, 'Task not found');
|
|
8556
|
-
|
|
8557
|
-
|| (t.status === 'max_rounds' && isSpecStagePhase(t.phase));
|
|
8558
|
-
if (!retryable) {
|
|
8938
|
+
if (!TERMINAL_STATUSES.includes(t.status)) {
|
|
8559
8939
|
throw new ApiError(409, `Task ${taskId} cannot be retried in status "${t.status}"; cancel it first or wait for completion`);
|
|
8560
8940
|
}
|
|
8941
|
+
if (t.replacementTaskId !== undefined) {
|
|
8942
|
+
throw new ApiError(409, `Task ${taskId} was already retried as ${t.replacementTaskId}`);
|
|
8943
|
+
}
|
|
8944
|
+
if (this.retryTaskInFlight.has(taskId)) {
|
|
8945
|
+
throw new ApiError(409, `Task ${taskId} retry is already in progress`);
|
|
8946
|
+
}
|
|
8947
|
+
this.retryTaskInFlight.add(taskId);
|
|
8561
8948
|
return t;
|
|
8562
8949
|
});
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8950
|
+
try {
|
|
8951
|
+
const input = {
|
|
8952
|
+
title: old.title,
|
|
8953
|
+
description: old.description,
|
|
8954
|
+
preferredAgentId: this.resolveRetryPreferredAgentId(old),
|
|
8955
|
+
};
|
|
8956
|
+
if (old.images?.length) {
|
|
8957
|
+
input.images = await this.readStagedImages(old.id, old.images);
|
|
8958
|
+
}
|
|
8959
|
+
await this.validateTaskDispatch(old.projectId, input);
|
|
8960
|
+
const replacement = await this.createTask(old.projectId, input);
|
|
8961
|
+
await this.withTaskLock(async () => {
|
|
8962
|
+
const source = await this.taskStore.get(taskId);
|
|
8963
|
+
if (!source)
|
|
8964
|
+
throw new ApiError(404, `Task ${taskId} disappeared while recording its retry`);
|
|
8965
|
+
if (source.replacementTaskId !== undefined && source.replacementTaskId !== replacement.id) {
|
|
8966
|
+
throw new ApiError(409, `Task ${taskId} was already retried as ${source.replacementTaskId}`);
|
|
8967
|
+
}
|
|
8968
|
+
const next = {
|
|
8969
|
+
...source,
|
|
8970
|
+
replacementTaskId: replacement.id,
|
|
8971
|
+
updatedAt: new Date().toISOString(),
|
|
8972
|
+
};
|
|
8973
|
+
if (old.attention !== undefined
|
|
8974
|
+
&& source.attention?.reason === old.attention.reason
|
|
8975
|
+
&& source.attention.occurredAt === old.attention.occurredAt) {
|
|
8976
|
+
delete next.attention;
|
|
8977
|
+
}
|
|
8978
|
+
await this.taskStore.set(next);
|
|
8979
|
+
});
|
|
8980
|
+
return this.startCreatedTask(replacement);
|
|
8570
8981
|
}
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
await this.cancelTask(old.id);
|
|
8982
|
+
finally {
|
|
8983
|
+
this.retryTaskInFlight.delete(taskId);
|
|
8574
8984
|
}
|
|
8575
|
-
return this.createAndStartTask(old.projectId, input);
|
|
8576
8985
|
}
|
|
8577
8986
|
resolveRetryPreferredAgentId(task) {
|
|
8578
8987
|
const configured = this.getAgentConfig(task.preferredAgentId);
|
|
@@ -8582,7 +8991,7 @@ export class AgentManager {
|
|
|
8582
8991
|
const anchors = new Set([task.qaAgentId, task.devAgentId, task.agentId]
|
|
8583
8992
|
.filter((agentId) => typeof agentId === 'string' && agentId !== ''));
|
|
8584
8993
|
for (const anchor of anchors) {
|
|
8585
|
-
const dev = this.
|
|
8994
|
+
const dev = this.findAgentTeam(anchor)?.find(agent => TASK_OWNER_ROLES.has(agent.role));
|
|
8586
8995
|
const current = dev ? this.getAgentConfig(dev.id) : undefined;
|
|
8587
8996
|
if (current?.projectId === task.projectId)
|
|
8588
8997
|
return current.id;
|
|
@@ -8624,13 +9033,13 @@ export class AgentManager {
|
|
|
8624
9033
|
}
|
|
8625
9034
|
if (!TASK_OWNER_ROLES.has(cfg.role))
|
|
8626
9035
|
throw new ApiError(400, 'Agent is not a dev agent');
|
|
8627
|
-
const
|
|
8628
|
-
const dev =
|
|
9036
|
+
const team = this.findAgentTeam(cfg.id);
|
|
9037
|
+
const dev = team?.find(agent => agent.role === 'dev');
|
|
8629
9038
|
if (!dev)
|
|
8630
|
-
throw new ApiError(409, `Agent ${cfg.id} has no dev agent in its
|
|
8631
|
-
const qa =
|
|
9039
|
+
throw new ApiError(409, `Agent ${cfg.id} has no dev agent in its Agent Team`);
|
|
9040
|
+
const qa = team?.find(agent => agent.role === 'qa');
|
|
8632
9041
|
if (!qa)
|
|
8633
|
-
throw new ApiError(409, `Agent ${cfg.id} has no qa agent in its
|
|
9042
|
+
throw new ApiError(409, `Agent ${cfg.id} has no qa agent in its Agent Team`);
|
|
8634
9043
|
task.preferredAgentId = patch.preferredAgentId;
|
|
8635
9044
|
task.devAgentId = dev.id;
|
|
8636
9045
|
task.qaAgentId = qa.id;
|
|
@@ -8668,22 +9077,22 @@ export class AgentManager {
|
|
|
8668
9077
|
throw new ApiError(409, `Task ${taskId} is not at max_rounds (status=${task.status})`);
|
|
8669
9078
|
}
|
|
8670
9079
|
if (isSpecStagePhase(task.phase)) {
|
|
8671
|
-
throw new ApiError(409, `
|
|
9080
|
+
throw new ApiError(409, `Complete verdict is only supported for code-phase tasks`);
|
|
8672
9081
|
}
|
|
8673
9082
|
if (!task.prNumber || !task.branch) {
|
|
8674
|
-
throw new ApiError(400, `Task ${taskId} has no PR/branch; cannot
|
|
9083
|
+
throw new ApiError(400, `Task ${taskId} has no PR/branch; cannot complete`);
|
|
8675
9084
|
}
|
|
8676
9085
|
for (const agentId of [task.agentId, task.qaAgentId]) {
|
|
8677
9086
|
if (!agentId)
|
|
8678
9087
|
continue;
|
|
8679
9088
|
const state = await this.agentStore.get(agentId);
|
|
8680
9089
|
if (state?.status === 'awaiting_human' && state.taskId === taskId) {
|
|
8681
|
-
throw new ApiError(409, `Agent ${agentId} is awaiting human intervention on this task; resume/restart/delete it before
|
|
9090
|
+
throw new ApiError(409, `Agent ${agentId} is awaiting human intervention on this task; resume/restart/delete it before completing`);
|
|
8682
9091
|
}
|
|
8683
9092
|
}
|
|
8684
9093
|
const claimed = await this.transitionTaskStatus(taskId, 'merge-ready', { fromStatus: ['max_rounds'] });
|
|
8685
9094
|
if (!claimed) {
|
|
8686
|
-
throw new ApiError(409, `Task ${taskId} changed status during
|
|
9095
|
+
throw new ApiError(409, `Task ${taskId} changed status during complete verdict; aborted`);
|
|
8687
9096
|
}
|
|
8688
9097
|
try {
|
|
8689
9098
|
await this.mergePr(taskId, { humanOverride: true });
|
|
@@ -9193,6 +9602,247 @@ export class AgentManager {
|
|
|
9193
9602
|
}
|
|
9194
9603
|
return parked;
|
|
9195
9604
|
}
|
|
9605
|
+
async submitCodeVerdict(taskId, verdict, comments) {
|
|
9606
|
+
const trimmed = comments?.trim() ?? '';
|
|
9607
|
+
if (verdict === 'request-changes' && !trimmed) {
|
|
9608
|
+
throw new ApiError(400, 'comments is required for request-changes');
|
|
9609
|
+
}
|
|
9610
|
+
if (extractReviewTokens(trimmed).length > 0) {
|
|
9611
|
+
throw new ApiError(400, 'comments must not contain baxian review markers');
|
|
9612
|
+
}
|
|
9613
|
+
const task = await this.withTaskLock(async () => {
|
|
9614
|
+
const fresh = await this.taskStore.get(taskId);
|
|
9615
|
+
if (!fresh)
|
|
9616
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
9617
|
+
const pending = this.codeVerdictOutbox(fresh);
|
|
9618
|
+
const kind = verdict === 'pass' ? 'pass' : 'fail';
|
|
9619
|
+
if (pending !== undefined
|
|
9620
|
+
&& (pending.data.kind !== kind || pending.data.comments !== trimmed)) {
|
|
9621
|
+
throw new ApiError(409, `Task ${taskId} has a different code verdict already in progress`);
|
|
9622
|
+
}
|
|
9623
|
+
if (pending === undefined && (fresh.status !== 'review' || isSpecStagePhase(fresh.phase))) {
|
|
9624
|
+
throw new ApiError(409, `Task ${taskId} is not awaiting a code review verdict`);
|
|
9625
|
+
}
|
|
9626
|
+
if (this.codeVerdictInFlight.has(taskId)) {
|
|
9627
|
+
throw new ApiError(409, `Task ${taskId} code verdict is already being processed`);
|
|
9628
|
+
}
|
|
9629
|
+
this.codeVerdictInFlight.add(taskId);
|
|
9630
|
+
return fresh;
|
|
9631
|
+
});
|
|
9632
|
+
try {
|
|
9633
|
+
return await this.executeCodeVerdict(task, verdict, trimmed);
|
|
9634
|
+
}
|
|
9635
|
+
finally {
|
|
9636
|
+
this.codeVerdictInFlight.delete(taskId);
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
codeVerdictCommentBody(entry) {
|
|
9640
|
+
const lines = [
|
|
9641
|
+
entry.data.kind === 'pass'
|
|
9642
|
+
? 'Human code verdict: pass'
|
|
9643
|
+
: 'Human code verdict: request changes',
|
|
9644
|
+
];
|
|
9645
|
+
if (entry.data.comments)
|
|
9646
|
+
lines.push('', entry.data.comments);
|
|
9647
|
+
lines.push('', buildReviewTokenLine({
|
|
9648
|
+
kind: entry.data.kind,
|
|
9649
|
+
anchorSha: entry.data.anchorSha,
|
|
9650
|
+
token: entry.data.token,
|
|
9651
|
+
}));
|
|
9652
|
+
const body = lines.join('\n');
|
|
9653
|
+
try {
|
|
9654
|
+
validateCommentBody(body);
|
|
9655
|
+
}
|
|
9656
|
+
catch (err) {
|
|
9657
|
+
throw new ApiError(400, `Invalid code verdict comment: ${err instanceof Error ? err.message : String(err)}`);
|
|
9658
|
+
}
|
|
9659
|
+
return body;
|
|
9660
|
+
}
|
|
9661
|
+
async scanCodeVerdictMarker(driver, entry, oppositeToken) {
|
|
9662
|
+
const scans = await this.platformScanComments(driver, entry.data.prNumber);
|
|
9663
|
+
if (scans.some(scan => !scan.ok))
|
|
9664
|
+
return 'unknown';
|
|
9665
|
+
const dead = deadTokens(scans);
|
|
9666
|
+
if (dead.has(entry.data.token))
|
|
9667
|
+
return 'dead';
|
|
9668
|
+
let present = false;
|
|
9669
|
+
let opposite = false;
|
|
9670
|
+
for (const scan of scans) {
|
|
9671
|
+
for (const row of scan.rows) {
|
|
9672
|
+
if (row.system === true || versionTimeOf(row) === undefined)
|
|
9673
|
+
continue;
|
|
9674
|
+
for (const marker of rowTokens(row)) {
|
|
9675
|
+
if (dead.has(marker.token))
|
|
9676
|
+
continue;
|
|
9677
|
+
if (marker.anchorSha.toLowerCase() !== entry.data.anchorSha.toLowerCase())
|
|
9678
|
+
continue;
|
|
9679
|
+
if (marker.kind === entry.data.kind && marker.token === entry.data.token)
|
|
9680
|
+
present = true;
|
|
9681
|
+
if (marker.kind !== entry.data.kind && marker.token === oppositeToken)
|
|
9682
|
+
opposite = true;
|
|
9683
|
+
}
|
|
9684
|
+
}
|
|
9685
|
+
}
|
|
9686
|
+
if (opposite)
|
|
9687
|
+
return 'opposite';
|
|
9688
|
+
return present ? 'present' : 'absent';
|
|
9689
|
+
}
|
|
9690
|
+
async reconcileCodeVerdictComment(driver, entry) {
|
|
9691
|
+
const expectedDigest = bodyDigest(this.codeVerdictCommentBody(entry));
|
|
9692
|
+
const scans = await this.platformScanComments(driver, entry.data.prNumber);
|
|
9693
|
+
if (scans.some(scan => !scan.ok))
|
|
9694
|
+
return 'unknown';
|
|
9695
|
+
if (deadTokens(scans).has(entry.data.token))
|
|
9696
|
+
return 'dead';
|
|
9697
|
+
if (scans.some(scan => scan.rows.some(row => row.system !== true
|
|
9698
|
+
&& versionTimeOf(row) !== undefined
|
|
9699
|
+
&& row.bodyDigest === expectedDigest)))
|
|
9700
|
+
return 'present';
|
|
9701
|
+
return 'absent';
|
|
9702
|
+
}
|
|
9703
|
+
async verifyCodeVerdictHead(taskId, prNumber, anchorSha) {
|
|
9704
|
+
try {
|
|
9705
|
+
const binding = await this.platformVerifyPrBinding(taskId, prNumber);
|
|
9706
|
+
if (!binding.ok)
|
|
9707
|
+
return { ok: false, reason: `the PR binding is ${binding.reason}` };
|
|
9708
|
+
if (binding.headSha.toLowerCase() !== anchorSha.toLowerCase()) {
|
|
9709
|
+
return { ok: false, reason: 'the PR head changed' };
|
|
9710
|
+
}
|
|
9711
|
+
return { ok: true };
|
|
9712
|
+
}
|
|
9713
|
+
catch (err) {
|
|
9714
|
+
throw new ApiError(503, `Cannot verify code verdict head for task ${taskId}: ${safeDriverErrorText(err)}`);
|
|
9715
|
+
}
|
|
9716
|
+
}
|
|
9717
|
+
async deliverCodeVerdictOutbox(taskId, key) {
|
|
9718
|
+
let task = await this.taskStore.get(taskId);
|
|
9719
|
+
let entry = task ? this.codeVerdictOutbox(task) : undefined;
|
|
9720
|
+
if (!task || !entry || entry.key !== key)
|
|
9721
|
+
return;
|
|
9722
|
+
const driver = this.platformDriverFor(task.projectId);
|
|
9723
|
+
if (!driver)
|
|
9724
|
+
throw new ApiError(503, `Git driver unavailable for task ${task.id}`);
|
|
9725
|
+
const binding = await this.verifyCodeVerdictHead(task.id, entry.data.prNumber, entry.data.anchorSha);
|
|
9726
|
+
if (!binding.ok) {
|
|
9727
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9728
|
+
throw new ApiError(409, `Cannot write code verdict for task ${task.id}: ${binding.reason}`);
|
|
9729
|
+
}
|
|
9730
|
+
if (entry.data.writeAttemptedAt !== undefined) {
|
|
9731
|
+
const reconciled = await this.reconcileCodeVerdictComment(driver, entry);
|
|
9732
|
+
if (reconciled === 'dead') {
|
|
9733
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9734
|
+
throw new ApiError(409, `Code verdict token for task ${task.id} was dismissed; advance QA to create a new review pass`);
|
|
9735
|
+
}
|
|
9736
|
+
if (reconciled === 'present') {
|
|
9737
|
+
return;
|
|
9738
|
+
}
|
|
9739
|
+
if (reconciled === 'unknown'
|
|
9740
|
+
|| Date.now() - Date.parse(entry.data.writeAttemptedAt) < driver.visibilityLagMs) {
|
|
9741
|
+
throw new ApiError(503, `Code verdict comment outcome is still being reconciled for task ${task.id}`);
|
|
9742
|
+
}
|
|
9743
|
+
}
|
|
9744
|
+
const expectedAttempt = entry.data.writeAttemptedAt;
|
|
9745
|
+
const claimed = await this.mutateTaskOutboxEntry(task.id, entry.key, current => current.type === 'git.code-verdict' && current.data.writeAttemptedAt === expectedAttempt
|
|
9746
|
+
? { ...current, data: { ...current.data, writeAttemptedAt: new Date().toISOString() } }
|
|
9747
|
+
: null);
|
|
9748
|
+
const claimedEntry = claimed ? this.codeVerdictOutbox(claimed) : undefined;
|
|
9749
|
+
if (!claimed || claimedEntry?.key !== entry.key) {
|
|
9750
|
+
throw new ApiError(503, `Code verdict comment write is already claimed for task ${task.id}`);
|
|
9751
|
+
}
|
|
9752
|
+
task = claimed;
|
|
9753
|
+
entry = claimedEntry;
|
|
9754
|
+
let writeFailure;
|
|
9755
|
+
try {
|
|
9756
|
+
await driver.runOp('comment', {
|
|
9757
|
+
prNumber: entry.data.prNumber,
|
|
9758
|
+
body: this.codeVerdictCommentBody(entry),
|
|
9759
|
+
});
|
|
9760
|
+
}
|
|
9761
|
+
catch (error) {
|
|
9762
|
+
writeFailure = { error };
|
|
9763
|
+
}
|
|
9764
|
+
const reconciled = await this.reconcileCodeVerdictComment(driver, entry);
|
|
9765
|
+
if (reconciled === 'dead') {
|
|
9766
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9767
|
+
throw new ApiError(409, `Code verdict token for task ${task.id} was dismissed; advance QA to create a new review pass`);
|
|
9768
|
+
}
|
|
9769
|
+
if (writeFailure !== undefined && reconciled !== 'present') {
|
|
9770
|
+
const { error } = writeFailure;
|
|
9771
|
+
const uncertain = !(error instanceof DriverOpError)
|
|
9772
|
+
|| error.info.exitCode === 255
|
|
9773
|
+
|| isTransientNetworkFailure(error.info.stderrTail ?? '')
|
|
9774
|
+
|| isTransientNetworkFailure(error.message);
|
|
9775
|
+
if (reconciled === 'unknown' || uncertain) {
|
|
9776
|
+
throw new ApiError(503, `Code verdict comment outcome is unknown for task ${task.id}; the persisted outbox will reconcile it`);
|
|
9777
|
+
}
|
|
9778
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9779
|
+
throw new ApiError(502, `Code verdict comment was rejected for task ${task.id}: ${safeDriverErrorText(error)}`);
|
|
9780
|
+
}
|
|
9781
|
+
if (reconciled !== 'present') {
|
|
9782
|
+
throw new ApiError(503, `Code verdict comment is not yet visible for task ${task.id}; the persisted outbox will reconcile it`);
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9785
|
+
async executeCodeVerdict(task, verdict, comments) {
|
|
9786
|
+
if (task.status !== 'review' || isSpecStagePhase(task.phase)
|
|
9787
|
+
|| task.prNumber === undefined || task.reviewHeadAnchorSha === undefined
|
|
9788
|
+
|| task.passToken === undefined || task.failToken === undefined) {
|
|
9789
|
+
throw new ApiError(409, `Task ${task.id} has no complete code review generation`);
|
|
9790
|
+
}
|
|
9791
|
+
const kind = verdict === 'pass' ? 'pass' : 'fail';
|
|
9792
|
+
const token = kind === 'pass' ? task.passToken : task.failToken;
|
|
9793
|
+
const existingEntry = this.codeVerdictOutbox(task);
|
|
9794
|
+
const entry = existingEntry ?? {
|
|
9795
|
+
key: token,
|
|
9796
|
+
type: 'git.code-verdict',
|
|
9797
|
+
data: {
|
|
9798
|
+
prNumber: task.prNumber,
|
|
9799
|
+
kind,
|
|
9800
|
+
anchorSha: task.reviewHeadAnchorSha,
|
|
9801
|
+
token,
|
|
9802
|
+
comments,
|
|
9803
|
+
},
|
|
9804
|
+
};
|
|
9805
|
+
this.codeVerdictCommentBody(entry);
|
|
9806
|
+
const driver = this.platformDriverFor(task.projectId);
|
|
9807
|
+
if (!driver)
|
|
9808
|
+
throw new ApiError(503, `Git driver unavailable for task ${task.id}`);
|
|
9809
|
+
const binding = await this.verifyCodeVerdictHead(task.id, task.prNumber, task.reviewHeadAnchorSha);
|
|
9810
|
+
if (!binding.ok) {
|
|
9811
|
+
throw new ApiError(409, `Cannot submit code verdict for task ${task.id}: ${binding.reason}`);
|
|
9812
|
+
}
|
|
9813
|
+
if (existingEntry === undefined) {
|
|
9814
|
+
const marker = await this.scanCodeVerdictMarker(driver, entry, kind === 'pass' ? task.failToken : task.passToken);
|
|
9815
|
+
if (marker === 'present')
|
|
9816
|
+
return task;
|
|
9817
|
+
if (marker === 'opposite') {
|
|
9818
|
+
throw new ApiError(409, `Task ${task.id} already has the opposite verdict on the current review head`);
|
|
9819
|
+
}
|
|
9820
|
+
if (marker === 'dead') {
|
|
9821
|
+
throw new ApiError(409, `Code verdict token for task ${task.id} was dismissed; advance QA to create a new review pass`);
|
|
9822
|
+
}
|
|
9823
|
+
if (marker === 'unknown') {
|
|
9824
|
+
throw new ApiError(503, `Cannot safely check existing code verdict comments for task ${task.id}`);
|
|
9825
|
+
}
|
|
9826
|
+
const created = await this.withTaskLock(async () => {
|
|
9827
|
+
const fresh = await this.taskStore.get(task.id);
|
|
9828
|
+
if (!fresh || !taskMatchesGeneration(fresh, taskGenerationGuard(task))
|
|
9829
|
+
|| this.codeVerdictOutbox(fresh) !== undefined)
|
|
9830
|
+
return null;
|
|
9831
|
+
const next = {
|
|
9832
|
+
...fresh,
|
|
9833
|
+
outbox: [...(fresh.outbox ?? []), entry],
|
|
9834
|
+
updatedAt: new Date().toISOString(),
|
|
9835
|
+
};
|
|
9836
|
+
await this.taskStore.set(next);
|
|
9837
|
+
return next;
|
|
9838
|
+
});
|
|
9839
|
+
if (!created)
|
|
9840
|
+
throw new ApiError(409, `Task ${task.id} changed while recording the code verdict`);
|
|
9841
|
+
task = created;
|
|
9842
|
+
}
|
|
9843
|
+
await this.deliverCodeVerdictOutbox(task.id, entry.key);
|
|
9844
|
+
return (await this.taskStore.get(task.id)) ?? task;
|
|
9845
|
+
}
|
|
9196
9846
|
async submitSpecVerdict(taskId, verdict, comments) {
|
|
9197
9847
|
const trimmed = comments?.trim();
|
|
9198
9848
|
if (verdict === 'request-changes' && !trimmed) {
|
|
@@ -9849,8 +10499,8 @@ export class AgentManager {
|
|
|
9849
10499
|
function buildAgentIndex(config) {
|
|
9850
10500
|
const index = new Map();
|
|
9851
10501
|
for (const project of config.project) {
|
|
9852
|
-
for (const
|
|
9853
|
-
for (const agent of
|
|
10502
|
+
for (const team of project.agent) {
|
|
10503
|
+
for (const agent of team) {
|
|
9854
10504
|
index.set(agent.id, { ...agent, projectId: project.id });
|
|
9855
10505
|
}
|
|
9856
10506
|
}
|