baxian 1.2.0 → 1.2.2
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/agent/bootstrap.d.ts +1 -1
- package/dist/agent/bootstrap.d.ts.map +1 -1
- package/dist/agent/bootstrap.js +33 -0
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/manager.d.ts +8 -4
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +120 -73
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +49 -22
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +1 -1
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +8 -7
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +5 -3
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +53 -24
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.js +2 -2
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/agent/tmux-probe-poller.js +4 -4
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.js +9 -9
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +5 -1
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.js +2 -2
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/tasks.js +1 -1
- package/dist/api/tasks.js.map +1 -1
- package/dist/config/loader.js +5 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +37 -3
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +10 -11
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.js +10 -10
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/github/poller.d.ts.map +1 -1
- package/dist/github/poller.js +13 -3
- package/dist/github/poller.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/shared/constants.js +1 -1
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/git-url.d.ts +14 -0
- package/dist/shared/git-url.d.ts.map +1 -0
- package/dist/shared/git-url.js +76 -0
- package/dist/shared/git-url.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/types.d.ts +1 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/state/snapshot.js +1 -1
- package/dist/state/snapshot.js.map +1 -1
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +15 -5
- package/dist/terminal/attach.js.map +1 -1
- package/dist/web/assets/index-COIOmeco.js +4 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-OtgjyQI1.js +0 -4
package/dist/agent/manager.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { createSignalToken } from './phase-signal.js';
|
|
5
|
-
import { BRANCH_PREFIX, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, } from '../shared/index.js';
|
|
5
|
+
import { BRANCH_PREFIX, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, isGitHubRepo, repoSlug, } from '../shared/index.js';
|
|
6
6
|
import { AGENT_STORE_NOOP } from '../state/agent-store.js';
|
|
7
7
|
import { PostApproveStore } from '../state/post-approve-store.js';
|
|
8
8
|
import { SkillRegistry } from '../skill/registry.js';
|
|
@@ -73,7 +73,7 @@ function agentRuntimeKindFor(agent) {
|
|
|
73
73
|
}
|
|
74
74
|
const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 30_000;
|
|
75
75
|
const DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS = 3_000;
|
|
76
|
-
// Dev-facing deliverable phases that weave the task's uploaded image paths into the prompt
|
|
76
|
+
// Dev-facing deliverable phases that weave the task's uploaded image paths into the prompt.
|
|
77
77
|
const IMAGE_DISPATCH_PHASES = new Set(['develop', 'code', 'fix', 'server-feedback']);
|
|
78
78
|
export function canDispatchWithBinding(binding) {
|
|
79
79
|
return !binding?.taskId && !binding?.creationToken && binding?.status !== 'awaiting_human';
|
|
@@ -121,7 +121,7 @@ export class AgentManager {
|
|
|
121
121
|
dispatchAckTimeoutMs;
|
|
122
122
|
dispatchSettleTimeoutMs;
|
|
123
123
|
// Re-send Enter after this long of continuous post-paste idle — recovers a swallowed first Enter
|
|
124
|
-
//
|
|
124
|
+
// without risking a double-submit (a real submit goes busy well within this window).
|
|
125
125
|
dispatchAckResendIntervalMs = 3_000;
|
|
126
126
|
taskMutationQueue = Promise.resolve();
|
|
127
127
|
agentIndex;
|
|
@@ -185,12 +185,32 @@ export class AgentManager {
|
|
|
185
185
|
getReviewStore() {
|
|
186
186
|
return this.reviewStore;
|
|
187
187
|
}
|
|
188
|
+
// Non-GitHub repos are forced to server review mode — there is no platform PR/review
|
|
189
|
+
// to map onto. GitHub honors the global config. Snapshotted per-task at creation.
|
|
190
|
+
effectiveReviewMode(projectId) {
|
|
191
|
+
const project = this.getProjectConfig(projectId);
|
|
192
|
+
if (project && !isGitHubRepo(project.repo))
|
|
193
|
+
return 'server';
|
|
194
|
+
return this.config.review.mode ?? 'github';
|
|
195
|
+
}
|
|
188
196
|
// Snapshot-aware afterDone read: an EXPLICIT null snapshot must win over hot
|
|
189
|
-
// config — `??` would swallow it and reroute an already-decided task
|
|
197
|
+
// config — `??` would swallow it and reroute an already-decided task.
|
|
190
198
|
resolveAfterDone(task) {
|
|
191
199
|
if (task.afterDone !== undefined)
|
|
192
200
|
return task.afterDone;
|
|
193
|
-
return this.config.review.afterDone
|
|
201
|
+
return this.coerceAfterDone(task.projectId, this.config.review.afterDone);
|
|
202
|
+
}
|
|
203
|
+
// Non-GitHub repos have no PR platform: 'pr' degrades to 'branch' (push + optional
|
|
204
|
+
// ff-merge). An unset afterDone defaults to 'branch' so reviewed work actually reaches
|
|
205
|
+
// the remote; an explicit null still means "don't publish". GitHub is unchanged.
|
|
206
|
+
coerceAfterDone(projectId, configured) {
|
|
207
|
+
const project = this.getProjectConfig(projectId);
|
|
208
|
+
if (project && !isGitHubRepo(project.repo)) {
|
|
209
|
+
if (configured === 'pr' || configured === undefined)
|
|
210
|
+
return 'branch';
|
|
211
|
+
return configured;
|
|
212
|
+
}
|
|
213
|
+
return configured ?? null;
|
|
194
214
|
}
|
|
195
215
|
getReviewTransport() {
|
|
196
216
|
this.reviewTransportInstance ??= new ReviewTransport({
|
|
@@ -564,7 +584,7 @@ export class AgentManager {
|
|
|
564
584
|
if (!discoveredPaneId)
|
|
565
585
|
return false;
|
|
566
586
|
const probeNow = new Date().toISOString();
|
|
567
|
-
//
|
|
587
|
+
// 不用 updatedAt guard:updatedAt 太宽,正常 background updates
|
|
568
588
|
// (repoPath refresh / poller bump 等) 也会触发假阳性让合法 retry dialog 路径误拒。
|
|
569
589
|
// race ("DELETE+recreate 后旧回调写新 agent") 在持锁路径下是 theoretical (retry endpoint 持锁
|
|
570
590
|
// 全程到 handleDialogPendingFromRuntime 返回;startSession/continueSession 由 acquireAgentForTask
|
|
@@ -601,7 +621,7 @@ export class AgentManager {
|
|
|
601
621
|
// fromStatus 来自 caller 显式计算:startSession/continueSession 用 opts.dialogFailFromStatuses ??
|
|
602
622
|
// PHASE_EXPECTED_STATUS[phase],dispatchReviewToQa 走 bypassTaskStatusGate 时显式传 [taskStatusAtClaim]
|
|
603
623
|
// (manual review 入口可能是 approved/fixing/in_progress,但 phase='review' 的 default 只接受 'review' →
|
|
604
|
-
// 不传就 skip → task 卡 active
|
|
624
|
+
// 不传就 skip → task 卡 active 死锁)。
|
|
605
625
|
const expectedFromStatuses = opts.expectedFromStatuses ?? [...ACTIVE_TASK_STATUSES];
|
|
606
626
|
const transitioned = await this.transitionTaskStatus(state.taskId, 'failed', { fromStatus: expectedFromStatuses });
|
|
607
627
|
if (transitioned) {
|
|
@@ -1220,7 +1240,7 @@ export class AgentManager {
|
|
|
1220
1240
|
// code-dispatch-failed: the code-phase prompt never reached the pane (spec
|
|
1221
1241
|
// approval already transitioned the task). Resume = clear the hold AND
|
|
1222
1242
|
// redispatch the code prompt (outside this lock) — without the redispatch
|
|
1223
|
-
// the task would stay in_progress with nothing running
|
|
1243
|
+
// the task would stay in_progress with nothing running.
|
|
1224
1244
|
if (state.awaitingPhase === 'code-dispatch-failed'
|
|
1225
1245
|
&& boundTask && ACTIVE_TASK_STATUSES.has(boundTask.status)
|
|
1226
1246
|
&& state.taskId) {
|
|
@@ -1413,7 +1433,7 @@ export class AgentManager {
|
|
|
1413
1433
|
const bound = t.agentId === agentId || t.qaAgentId === agentId;
|
|
1414
1434
|
// Human gates are decision states, not running work: an absent agent
|
|
1415
1435
|
// session must not terminally fail a task whose published PR/branch
|
|
1416
|
-
// would then be orphaned — Confirm/Cancel remain the only exits
|
|
1436
|
+
// would then be orphaned — Confirm/Cancel remain the only exits.
|
|
1417
1437
|
if (t.status === 'ready' || t.status === 'merge-ready')
|
|
1418
1438
|
continue;
|
|
1419
1439
|
if (ACTIVE_TASK_STATUSES.has(t.status) && bound) {
|
|
@@ -1655,7 +1675,7 @@ export class AgentManager {
|
|
|
1655
1675
|
return this.config.project.find(p => p.id === projectId);
|
|
1656
1676
|
}
|
|
1657
1677
|
getProjectByRepo(repo) {
|
|
1658
|
-
return this.config.project.find(p => p.repo === repo);
|
|
1678
|
+
return this.config.project.find(p => repoSlug(p.repo) === repo);
|
|
1659
1679
|
}
|
|
1660
1680
|
findQaPartner(devAgentId) {
|
|
1661
1681
|
for (const project of this.config.project) {
|
|
@@ -1845,10 +1865,11 @@ export class AgentManager {
|
|
|
1845
1865
|
// Three independent endpoints — fetch concurrently, not back-to-back. Reviews
|
|
1846
1866
|
// cover the same-identity `gh pr review --comment` reply path (a PR review with
|
|
1847
1867
|
// a body, surfaced via submitted_at, not an inline/issue comment).
|
|
1868
|
+
const repo = repoSlug(project.repo);
|
|
1848
1869
|
const [inlineReplies, issueComments, reviews] = await Promise.all([
|
|
1849
|
-
this.ghCreatedAt(`repos/${
|
|
1850
|
-
this.ghCreatedAt(`repos/${
|
|
1851
|
-
this.ghCreatedAt(`repos/${
|
|
1870
|
+
this.ghCreatedAt(`repos/${repo}/pulls/${task.prNumber}/comments`, '.[] | select(.in_reply_to_id != null) | .created_at'),
|
|
1871
|
+
this.ghCreatedAt(`repos/${repo}/issues/${task.prNumber}/comments`, '.[].created_at'),
|
|
1872
|
+
this.ghCreatedAt(`repos/${repo}/pulls/${task.prNumber}/reviews`, '.[] | select(.submitted_at != null) | .submitted_at'),
|
|
1852
1873
|
]);
|
|
1853
1874
|
const stamps = [...inlineReplies, ...issueComments, ...reviews];
|
|
1854
1875
|
return stamps.some(ts => {
|
|
@@ -1876,7 +1897,7 @@ export class AgentManager {
|
|
|
1876
1897
|
if (!project) {
|
|
1877
1898
|
throw new Error(`fetchPrHeadSha: unknown project ${task.projectId}`);
|
|
1878
1899
|
}
|
|
1879
|
-
const result = await this.platformRunner.exec(`gh pr view ${task.prNumber} --repo ${shellQuote(project.repo)} --json headRefOid --jq .headRefOid`);
|
|
1900
|
+
const result = await this.platformRunner.exec(`gh pr view ${task.prNumber} --repo ${shellQuote(repoSlug(project.repo))} --json headRefOid --jq .headRefOid`);
|
|
1880
1901
|
if (result.exitCode !== 0) {
|
|
1881
1902
|
throw new Error(`gh pr view failed for PR #${task.prNumber}: ${result.stderr || result.stdout}`);
|
|
1882
1903
|
}
|
|
@@ -1900,7 +1921,7 @@ export class AgentManager {
|
|
|
1900
1921
|
const project = this.getProjectConfig(task.projectId);
|
|
1901
1922
|
if (!project)
|
|
1902
1923
|
return undefined;
|
|
1903
|
-
const result = await this.platformRunner.exec(`gh pr view ${prNumber} --repo ${shellQuote(project.repo)} --json headRefName,headRefOid --jq '.headRefName + "\\t" + .headRefOid'`);
|
|
1924
|
+
const result = await this.platformRunner.exec(`gh pr view ${prNumber} --repo ${shellQuote(repoSlug(project.repo))} --json headRefName,headRefOid --jq '.headRefName + "\\t" + .headRefOid'`);
|
|
1904
1925
|
if (result.exitCode !== 0)
|
|
1905
1926
|
return undefined;
|
|
1906
1927
|
const [headRefName, headSha] = result.stdout.trim().split('\t');
|
|
@@ -1979,7 +2000,7 @@ export class AgentManager {
|
|
|
1979
2000
|
createRepoStore(agent, project, runner) {
|
|
1980
2001
|
const host = resolveAgentHost(this.config.host, agent.host);
|
|
1981
2002
|
if (this.repoStoreFactory) {
|
|
1982
|
-
return this.repoStoreFactory(runner, project.repo, agent.mode, host, this.repoCache);
|
|
2003
|
+
return this.repoStoreFactory(runner, repoSlug(project.repo), agent.mode, host, this.repoCache);
|
|
1983
2004
|
}
|
|
1984
2005
|
return new RepoStore(runner, project.repo, agent.mode, host, this.repoCache);
|
|
1985
2006
|
}
|
|
@@ -2059,9 +2080,19 @@ export class AgentManager {
|
|
|
2059
2080
|
return this.withTaskLock(async () => {
|
|
2060
2081
|
const taskId = await this.taskStore.nextId();
|
|
2061
2082
|
const now = new Date().toISOString();
|
|
2083
|
+
// Server review has no platform fallback: a dev with no QA partner would strand (server mode
|
|
2084
|
+
// is forced for non-GitHub repos). Fail fast BEFORE staging images — a rejected create must not
|
|
2085
|
+
// orphan state/task-images/<taskId> with no TaskState to reclaim it. The dispatch path also
|
|
2086
|
+
// guards (unassigned-claim entry). Unassigned skips this: empty preferredAgentId → no dev config.
|
|
2087
|
+
if (this.getAgentConfig(input.preferredAgentId)?.role === 'dev'
|
|
2088
|
+
&& this.effectiveReviewMode(projectId) === 'server'
|
|
2089
|
+
&& !this.findQaPartner(input.preferredAgentId)) {
|
|
2090
|
+
throw new ApiError(400, `Project "${projectId}" uses server review mode (forced for non-GitHub repos); ` +
|
|
2091
|
+
`dev "${input.preferredAgentId}" needs a QA partner — add a QA agent paired with this dev before creating tasks.`);
|
|
2092
|
+
}
|
|
2062
2093
|
// Stage images first so the task is written + emitted (task.created) WITH its images already
|
|
2063
2094
|
// on disk — a pending task is never observable, or crash-recoverable, without them. A persist
|
|
2064
|
-
// failure here throws before any store write / lock, so nothing half-created survives
|
|
2095
|
+
// failure here throws before any store write / lock, so nothing half-created survives.
|
|
2065
2096
|
const imageFilenames = input.images?.length
|
|
2066
2097
|
? await this.persistTaskImages(taskId, input.images)
|
|
2067
2098
|
: undefined;
|
|
@@ -2077,7 +2108,7 @@ export class AgentManager {
|
|
|
2077
2108
|
reviewRound: 0,
|
|
2078
2109
|
status: 'pending',
|
|
2079
2110
|
branch: BRANCH_PREFIX + taskId,
|
|
2080
|
-
reviewMode: this.
|
|
2111
|
+
reviewMode: this.effectiveReviewMode(projectId),
|
|
2081
2112
|
createdAt: now,
|
|
2082
2113
|
updatedAt: now,
|
|
2083
2114
|
...(imageFilenames ? { images: imageFilenames } : {}),
|
|
@@ -2106,7 +2137,7 @@ export class AgentManager {
|
|
|
2106
2137
|
reviewRound: 0,
|
|
2107
2138
|
status: 'pending',
|
|
2108
2139
|
branch: BRANCH_PREFIX + taskId,
|
|
2109
|
-
reviewMode: this.
|
|
2140
|
+
reviewMode: this.effectiveReviewMode(projectId),
|
|
2110
2141
|
createdAt: now,
|
|
2111
2142
|
updatedAt: now,
|
|
2112
2143
|
...(qa ? { qaAgentId: qa.id } : {}),
|
|
@@ -2139,7 +2170,7 @@ export class AgentManager {
|
|
|
2139
2170
|
reviewRound: 0,
|
|
2140
2171
|
status: 'pending',
|
|
2141
2172
|
branch: BRANCH_PREFIX + taskId,
|
|
2142
|
-
reviewMode: this.
|
|
2173
|
+
reviewMode: this.effectiveReviewMode(projectId),
|
|
2143
2174
|
createdAt: now,
|
|
2144
2175
|
updatedAt: now,
|
|
2145
2176
|
...(qa ? { qaAgentId: qa.id } : {}),
|
|
@@ -2171,7 +2202,7 @@ export class AgentManager {
|
|
|
2171
2202
|
reviewRound: 0,
|
|
2172
2203
|
status: 'in_progress',
|
|
2173
2204
|
branch: BRANCH_PREFIX + taskId,
|
|
2174
|
-
reviewMode: this.
|
|
2205
|
+
reviewMode: this.effectiveReviewMode(projectId),
|
|
2175
2206
|
createdAt: now,
|
|
2176
2207
|
updatedAt: now,
|
|
2177
2208
|
...(imageFilenames ? { images: imageFilenames } : {}),
|
|
@@ -2203,7 +2234,7 @@ export class AgentManager {
|
|
|
2203
2234
|
}
|
|
2204
2235
|
async createAndStartTask(projectId, input, opts = {}) {
|
|
2205
2236
|
// createTask stages images atomically before the task is visible (store + task.created),
|
|
2206
|
-
// so a pending task can never be observed — or crash-recovered — without its images
|
|
2237
|
+
// so a pending task can never be observed — or crash-recovered — without its images.
|
|
2207
2238
|
const task = await this.createTask(projectId, input);
|
|
2208
2239
|
if (task.status === 'in_progress' && task.agentId) {
|
|
2209
2240
|
// Persist token first — prompt build 和 watcher 验证共用 task.signalToken。
|
|
@@ -2294,7 +2325,7 @@ export class AgentManager {
|
|
|
2294
2325
|
}
|
|
2295
2326
|
return (await this.taskStore.get(taskId)) ?? null;
|
|
2296
2327
|
}
|
|
2297
|
-
/**
|
|
2328
|
+
/** Write an uploaded image to the running agent's host, paste its path (no Enter). */
|
|
2298
2329
|
async attachImageToRunningAgent(agentId, bytes, ext) {
|
|
2299
2330
|
const cfg = this.getAgentConfig(agentId);
|
|
2300
2331
|
if (!cfg)
|
|
@@ -2320,8 +2351,7 @@ export class AgentManager {
|
|
|
2320
2351
|
this.compactInFlight.delete(agentId);
|
|
2321
2352
|
}
|
|
2322
2353
|
}
|
|
2323
|
-
|
|
2324
|
-
async compactAgent(agentId) {
|
|
2354
|
+
async sendSlashCommand(agentId, command) {
|
|
2325
2355
|
const cfg = this.getAgentConfig(agentId);
|
|
2326
2356
|
if (!cfg)
|
|
2327
2357
|
throw new ApiError(404, `Unknown agent: ${agentId}`);
|
|
@@ -2344,7 +2374,7 @@ export class AgentManager {
|
|
|
2344
2374
|
|| now.paneId !== paneId
|
|
2345
2375
|
|| now.taskId !== taskIdAtStart
|
|
2346
2376
|
|| now.updatedAt !== updatedAtAtStart) {
|
|
2347
|
-
throw new ApiError(409, `Agent ${agentId} session changed while waiting;
|
|
2377
|
+
throw new ApiError(409, `Agent ${agentId} session changed while waiting; ${command} aborted`);
|
|
2348
2378
|
}
|
|
2349
2379
|
};
|
|
2350
2380
|
const tmux = new TmuxManager(this.createRunnerFor(cfg));
|
|
@@ -2359,18 +2389,22 @@ export class AgentManager {
|
|
|
2359
2389
|
};
|
|
2360
2390
|
await waitReady();
|
|
2361
2391
|
await assertSessionUnchanged();
|
|
2362
|
-
// 残留草稿会被「草稿/compact」连带提交;C-c 清线后再发。
|
|
2363
2392
|
await tmux.sendKeysToPane(paneId, 'C-c');
|
|
2364
2393
|
await waitReady();
|
|
2365
2394
|
await assertSessionUnchanged();
|
|
2366
|
-
await tmux.sendKeysLiteral(paneId,
|
|
2395
|
+
await tmux.sendKeysLiteral(paneId, command);
|
|
2367
2396
|
await tmux.sendEnter(paneId);
|
|
2368
|
-
|
|
2369
|
-
|
|
2397
|
+
if (command === '/clear') {
|
|
2398
|
+
await this.agentStore.update(agentId, (s) => {
|
|
2399
|
+
if (!s)
|
|
2400
|
+
return AGENT_STORE_NOOP;
|
|
2401
|
+
return { ...s, injectedSkills: undefined };
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2370
2404
|
guardHandedOff = true;
|
|
2371
2405
|
void this.waitForReplPromptReady(tmux, paneId, cfg.runtime, this.compactIdleWaitMs)
|
|
2372
2406
|
.catch(err => {
|
|
2373
|
-
console.warn(`[AgentManager]
|
|
2407
|
+
console.warn(`[AgentManager] sendSlashCommand(${agentId}, ${command}) post idle wait failed:`, err);
|
|
2374
2408
|
})
|
|
2375
2409
|
.finally(() => {
|
|
2376
2410
|
this.compactInFlight.delete(agentId);
|
|
@@ -2381,6 +2415,12 @@ export class AgentManager {
|
|
|
2381
2415
|
this.compactInFlight.delete(agentId);
|
|
2382
2416
|
}
|
|
2383
2417
|
}
|
|
2418
|
+
async compactAgent(agentId) {
|
|
2419
|
+
return this.sendSlashCommand(agentId, '/compact');
|
|
2420
|
+
}
|
|
2421
|
+
async clearAgent(agentId) {
|
|
2422
|
+
return this.sendSlashCommand(agentId, '/clear');
|
|
2423
|
+
}
|
|
2384
2424
|
async persistTaskImages(taskId, images) {
|
|
2385
2425
|
const dir = join(this.imageStagingRoot, taskId);
|
|
2386
2426
|
await mkdir(dir, { recursive: true });
|
|
@@ -2392,7 +2432,7 @@ export class AgentManager {
|
|
|
2392
2432
|
}
|
|
2393
2433
|
return filenames;
|
|
2394
2434
|
}
|
|
2395
|
-
// Retry reads staged bytes up-front; a missing source is a visible 409, never a silent drop
|
|
2435
|
+
// Retry reads staged bytes up-front; a missing source is a visible 409, never a silent drop.
|
|
2396
2436
|
async readStagedImages(taskId, filenames) {
|
|
2397
2437
|
const dir = join(this.imageStagingRoot, taskId);
|
|
2398
2438
|
const out = [];
|
|
@@ -2410,7 +2450,7 @@ export class AgentManager {
|
|
|
2410
2450
|
return out;
|
|
2411
2451
|
}
|
|
2412
2452
|
// Materialize staged task images onto the agent host at dispatch; absolute host paths get
|
|
2413
|
-
// woven into the prompt. Missing staging aborts the dispatch loudly (no silent skip
|
|
2453
|
+
// woven into the prompt. Missing staging aborts the dispatch loudly (no silent skip).
|
|
2414
2454
|
async materializeTaskImages(runner, task) {
|
|
2415
2455
|
const filenames = task.images ?? [];
|
|
2416
2456
|
if (filenames.length === 0)
|
|
@@ -2434,7 +2474,7 @@ export class AgentManager {
|
|
|
2434
2474
|
// Dev-facing deliverable phases all carry the task's uploaded images, since the image is a
|
|
2435
2475
|
// persistent task input the dev needs while producing or revising the spec/code — and a fresh
|
|
2436
2476
|
// runtime (restart/recovery) loses the original context. IMAGE_DISPATCH_PHASES 中的后续阶段经
|
|
2437
|
-
// continueSession 触发此方法;QA 阶段和 post-approve
|
|
2477
|
+
// continueSession 触发此方法;QA 阶段和 post-approve 不传图。
|
|
2438
2478
|
async imagePathsForDispatch(runner, task, phase) {
|
|
2439
2479
|
if (!IMAGE_DISPATCH_PHASES.has(phase))
|
|
2440
2480
|
return [];
|
|
@@ -2653,6 +2693,14 @@ export class AgentManager {
|
|
|
2653
2693
|
const hasQaPartner = !!(task.qaAgentId ?? this.findQaPartner(agentId)?.id);
|
|
2654
2694
|
let prompt;
|
|
2655
2695
|
try {
|
|
2696
|
+
// Server review has no platform fallback: a dev with no QA partner would emit a
|
|
2697
|
+
// completion signal nobody consumes and the task strands (server mode is forced
|
|
2698
|
+
// for non-GitHub repos via effectiveReviewMode). Enforce at this single dispatch
|
|
2699
|
+
// chokepoint — the catch below removes the worktree, then the task fails cleanly.
|
|
2700
|
+
if (task.reviewMode === 'server' && !hasQaPartner) {
|
|
2701
|
+
throw new DispatchTerminalError('server_review_needs_qa', `Task ${task.id} runs in server review mode but dev "${agentId}" has no QA partner; ` +
|
|
2702
|
+
`pair a QA agent with this dev (server review has no platform fallback to absorb the verdict).`);
|
|
2703
|
+
}
|
|
2656
2704
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
2657
2705
|
prompt = buildPromptInline({
|
|
2658
2706
|
task,
|
|
@@ -2775,7 +2823,7 @@ export class AgentManager {
|
|
|
2775
2823
|
}
|
|
2776
2824
|
// dedup baseline 记录的是「已 paste 进 idle composer 的 skill 文本」:paste 落入 composer 即进
|
|
2777
2825
|
// REPL 上下文,与 submit-ack 无关。ack 超时(首个 Enter 被吞)下 skill 仍在 composer,跳过落盘会让
|
|
2778
|
-
//
|
|
2826
|
+
// 下一轮整组重注入——即 SKILLS 重复注入。但 composerDelivered=false(paste 时 pane
|
|
2779
2827
|
// 已 busy,文本进了运行中输入流而非 composer)则不能落盘,否则恢复提示会缺必需 skill。freshRuntime
|
|
2780
2828
|
// 负责 REPL 真正重启时作废 baseline。
|
|
2781
2829
|
if (ack.composerDelivered) {
|
|
@@ -2905,7 +2953,7 @@ export class AgentManager {
|
|
|
2905
2953
|
});
|
|
2906
2954
|
// "pane already busy at baseline" means the paste landed on an already-running input stream,
|
|
2907
2955
|
// NOT an idle composer — the skills did NOT become context, so callers must not record them
|
|
2908
|
-
// as injected
|
|
2956
|
+
// as injected. Any other timeout (idle composer / swallowed Enter) did
|
|
2909
2957
|
// deliver the prompt text into the composer.
|
|
2910
2958
|
const composerDelivered = !/pane already busy at baseline/.test(message);
|
|
2911
2959
|
return { acked: false, composerDelivered };
|
|
@@ -3028,7 +3076,7 @@ export class AgentManager {
|
|
|
3028
3076
|
&& opts.postApproveRedispatchCount > 0
|
|
3029
3077
|
&& !ensure.freshRuntime;
|
|
3030
3078
|
// code phase (post spec-approval) flows through here, not startSession — materialize the
|
|
3031
|
-
// task's uploaded images so a fresh code-phase context still sees their paths
|
|
3079
|
+
// task's uploaded images so a fresh code-phase context still sees their paths.
|
|
3032
3080
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
3033
3081
|
prompt = buildPromptInline({
|
|
3034
3082
|
task,
|
|
@@ -3440,7 +3488,7 @@ export class AgentManager {
|
|
|
3440
3488
|
let qaToRelease;
|
|
3441
3489
|
// Server-mode ready gate may have already published remote artifacts
|
|
3442
3490
|
// (pushed branch / open PR). Capture before flipping to cancelled so the
|
|
3443
|
-
// post-lock cleanup can retire them instead of orphaning
|
|
3491
|
+
// post-lock cleanup can retire them instead of orphaning.
|
|
3444
3492
|
// mayBeInFlight: approved+marker means the publish prompt may STILL be
|
|
3445
3493
|
// running — retirement must wait for the dev interrupt or the in-flight
|
|
3446
3494
|
// push/pr-create would recreate the artifacts right after cleanup.
|
|
@@ -3484,7 +3532,7 @@ export class AgentManager {
|
|
|
3484
3532
|
}
|
|
3485
3533
|
}
|
|
3486
3534
|
else if (task.status === 'merge-ready' && task.prNumber !== undefined && task.branch && task.agentId) {
|
|
3487
|
-
// GitHub-mode gate cancel leaves the same orphaned PR/branch
|
|
3535
|
+
// GitHub-mode gate cancel leaves the same orphaned PR/branch.
|
|
3488
3536
|
publishedCleanup = {
|
|
3489
3537
|
afterDone: 'pr',
|
|
3490
3538
|
branch: task.branch,
|
|
@@ -3570,7 +3618,7 @@ export class AgentManager {
|
|
|
3570
3618
|
const project = this.getProjectConfig(result.projectId);
|
|
3571
3619
|
try {
|
|
3572
3620
|
if (publishedCleanup.afterDone === 'pr' && publishedCleanup.prNumber !== undefined && project) {
|
|
3573
|
-
const close = await this.platformRunner.exec(`gh pr close ${publishedCleanup.prNumber} --repo ${shellQuote(project.repo)} ` +
|
|
3621
|
+
const close = await this.platformRunner.exec(`gh pr close ${publishedCleanup.prNumber} --repo ${shellQuote(repoSlug(project.repo))} ` +
|
|
3574
3622
|
`--comment ${shellQuote('Task cancelled in baxian; closing the published PR.')} --delete-branch`);
|
|
3575
3623
|
if (close.exitCode !== 0)
|
|
3576
3624
|
throw new Error(close.stderr.trim() || close.stdout.trim());
|
|
@@ -3605,7 +3653,7 @@ export class AgentManager {
|
|
|
3605
3653
|
}
|
|
3606
3654
|
return result;
|
|
3607
3655
|
}
|
|
3608
|
-
//
|
|
3656
|
+
// create-time 不再校验 awaiting_human / creating / bound —— 这些都是"忙",
|
|
3609
3657
|
// 允许入队(落 pending);可执行性判断下沉到 dispatchPendingTask(或 createTask 已空闲分支)。
|
|
3610
3658
|
// 仍保留:agent 存在/同 project/role=dev(非空时)+ prompt size 上界。
|
|
3611
3659
|
async validateTaskDispatch(projectId, input) {
|
|
@@ -3868,7 +3916,7 @@ export class AgentManager {
|
|
|
3868
3916
|
throw new ApiError(409, `Continue one round is only supported for code-phase tasks`);
|
|
3869
3917
|
}
|
|
3870
3918
|
// Server-mode continue: grant one round past the cap, then re-run the server
|
|
3871
|
-
// fix protocol from the stored findings — no PR exists at this point
|
|
3919
|
+
// fix protocol from the stored findings — no PR exists at this point.
|
|
3872
3920
|
if (task.reviewMode === 'server') {
|
|
3873
3921
|
if (!task.agentId) {
|
|
3874
3922
|
throw new ApiError(400, `Task ${taskId} has no dev agent; cannot continue`);
|
|
@@ -4070,7 +4118,7 @@ export class AgentManager {
|
|
|
4070
4118
|
return { expectedKinds: ['spec-fixed'], agentId: task.agentId };
|
|
4071
4119
|
}
|
|
4072
4120
|
if (task.phase !== 'spec' && task.status === 'fixing' && task.agentId) {
|
|
4073
|
-
// Code-track fixing: dev emits pr-fixed when the round is done
|
|
4121
|
+
// Code-track fixing: dev emits pr-fixed when the round is done.
|
|
4074
4122
|
return { expectedKinds: ['pr-fixed'], agentId: task.agentId };
|
|
4075
4123
|
}
|
|
4076
4124
|
if (task.phase === undefined && task.status === 'in_progress' && task.agentId) {
|
|
@@ -4156,7 +4204,7 @@ export class AgentManager {
|
|
|
4156
4204
|
preferredAgentId: old.preferredAgentId,
|
|
4157
4205
|
};
|
|
4158
4206
|
// Retry preserves uploaded images: read the old task's staged bytes up-front
|
|
4159
|
-
// (missing → visible 409 before any new task/binding is created).
|
|
4207
|
+
// (missing → visible 409 before any new task/binding is created).
|
|
4160
4208
|
if (old.images?.length) {
|
|
4161
4209
|
input.images = await this.readStagedImages(old.id, old.images);
|
|
4162
4210
|
}
|
|
@@ -4222,14 +4270,14 @@ export class AgentManager {
|
|
|
4222
4270
|
const matchHead = opts.matchHeadSha
|
|
4223
4271
|
? ` --match-head-commit ${shellQuote(opts.matchHeadSha)}`
|
|
4224
4272
|
: '';
|
|
4225
|
-
const result = await this.platformRunner.exec(`gh pr merge ${task.prNumber} --repo ${shellQuote(project.repo)}${matchHead} --squash --delete-branch`);
|
|
4273
|
+
const result = await this.platformRunner.exec(`gh pr merge ${task.prNumber} --repo ${shellQuote(repoSlug(project.repo))}${matchHead} --squash --delete-branch`);
|
|
4226
4274
|
if (result.exitCode !== 0) {
|
|
4227
4275
|
throw new Error(`gh pr merge failed for PR #${task.prNumber}: ${result.stderr || result.stdout}`);
|
|
4228
4276
|
}
|
|
4229
4277
|
}
|
|
4230
4278
|
// Manually finish a max_rounds task: merge its PR, then reuse the normal merged
|
|
4231
4279
|
// cleanup chain (pr.merged handler → transition merged + post-merge worktree/branch
|
|
4232
|
-
// cleanup + /
|
|
4280
|
+
// cleanup + /clear + release). Same path the poller drives when it detects the merge.
|
|
4233
4281
|
async markTaskComplete(taskId) {
|
|
4234
4282
|
const peek = await this.taskStore.get(taskId);
|
|
4235
4283
|
if (!peek)
|
|
@@ -4247,7 +4295,7 @@ export class AgentManager {
|
|
|
4247
4295
|
const task = await this.claimCompleteGate(taskId, ['max_rounds', 'approved']);
|
|
4248
4296
|
try {
|
|
4249
4297
|
// Server-mode publish retry: a failed afterDone dispatch leaves the task
|
|
4250
|
-
// 'approved' with dev released — mark-complete re-runs the publish
|
|
4298
|
+
// 'approved' with dev released — mark-complete re-runs the publish.
|
|
4251
4299
|
const serverApprovedRetry = task.status === 'approved' && task.reviewMode === 'server';
|
|
4252
4300
|
if (!serverApprovedRetry && task.status !== 'max_rounds') {
|
|
4253
4301
|
throw new ApiError(409, `Task ${taskId} is not at max_rounds (status=${task.status})`);
|
|
@@ -4267,7 +4315,7 @@ export class AgentManager {
|
|
|
4267
4315
|
// publishDispatchedAt persists delivery across restarts: set = the
|
|
4268
4316
|
// prompt reached the pane before the restart (still in flight, 409);
|
|
4269
4317
|
// cleared = the dispatch failed and this approved state is retryable —
|
|
4270
|
-
// stop the recovered watch and let the retry own the dispatch
|
|
4318
|
+
// stop the recovered watch and let the retry own the dispatch.
|
|
4271
4319
|
if (this.phaseSignalWatcher?.expectedKindsFor(taskId).has('code-ready')) {
|
|
4272
4320
|
if (!this.phaseSignalWatcher.isRecovered(taskId) || task.publishDispatchedAt) {
|
|
4273
4321
|
throw new ApiError(409, `Task ${taskId} publish is in flight; retry only after it fails`);
|
|
@@ -4287,13 +4335,13 @@ export class AgentManager {
|
|
|
4287
4335
|
return (await this.taskStore.get(taskId));
|
|
4288
4336
|
}
|
|
4289
4337
|
// Server-mode capped task, human accepts as-is: no PR exists yet — run the
|
|
4290
|
-
// afterDone flow (or finish directly) instead of the legacy PR merge
|
|
4338
|
+
// afterDone flow (or finish directly) instead of the legacy PR merge.
|
|
4291
4339
|
// Inside the in-flight claim so a concurrent Continue can't act on the same
|
|
4292
4340
|
// max_rounds snapshot and release dev mid-publish.
|
|
4293
4341
|
if (task.reviewMode === 'server') {
|
|
4294
4342
|
// Max_rounds never routed an approve verdict — snapshot afterDone NOW so
|
|
4295
4343
|
// the eventual ready-confirm uses this decision, not future hot config.
|
|
4296
|
-
const afterDone = this.config.review.afterDone
|
|
4344
|
+
const afterDone = this.coerceAfterDone(task.projectId, this.config.review.afterDone);
|
|
4297
4345
|
await this.updateTask(taskId, { afterDone });
|
|
4298
4346
|
if (afterDone === null) {
|
|
4299
4347
|
const done = await this.transitionTaskStatus(taskId, 'done', { fromStatus: ['max_rounds'] });
|
|
@@ -4364,11 +4412,9 @@ export class AgentManager {
|
|
|
4364
4412
|
if (!dev)
|
|
4365
4413
|
return;
|
|
4366
4414
|
this.phaseSignalWatcher?.stop(taskId);
|
|
4367
|
-
// Keep the agent BOUND (non-dispatchable) until branch cleanup +
|
|
4415
|
+
// Keep the agent BOUND (non-dispatchable) until branch cleanup + context reset finish, then
|
|
4368
4416
|
// release. dispatchPostMergeCleanup owns the whole lifecycle: worktree removal → branch
|
|
4369
|
-
// delete →
|
|
4370
|
-
// dispatchPendingTask) from giving this agent a new task before it has cleaned up and
|
|
4371
|
-
// compacted its context.
|
|
4417
|
+
// delete → /clear (or /compact if cleanup failed) → release.
|
|
4372
4418
|
if (task.prNumber && task.branch) {
|
|
4373
4419
|
const ctx = {
|
|
4374
4420
|
prNumber: task.prNumber,
|
|
@@ -4425,7 +4471,8 @@ export class AgentManager {
|
|
|
4425
4471
|
const tmux = new TmuxManager(runner);
|
|
4426
4472
|
const prompt = buildPostMergeCleanupPrompt(ctx, cleanupResult);
|
|
4427
4473
|
const runtime = agentRuntimeKindFor(agent);
|
|
4428
|
-
|
|
4474
|
+
const cleanSlate = cleanupResult.outcome === 'deleted' || cleanupResult.outcome === 'absent';
|
|
4475
|
+
void this.runPostMergeCompaction(tmux, state.paneId, agentId, ctx.taskId, runtime, prompt, cleanSlate).catch(err => console.warn(`[AgentManager] runPostMergeCompaction(${agentId}) failed:`, err));
|
|
4429
4476
|
}
|
|
4430
4477
|
// Release the post-merge binding (clears taskId + frees the lock we held → agent dispatchable
|
|
4431
4478
|
// again). Shared success tail. Skips when the binding has already moved to another task, so it
|
|
@@ -4442,7 +4489,7 @@ export class AgentManager {
|
|
|
4442
4489
|
}
|
|
4443
4490
|
}
|
|
4444
4491
|
// Removes the merged worktree but KEEPS taskId on the binding, so the agent remains
|
|
4445
|
-
// non-dispatchable while branch delete + /
|
|
4492
|
+
// non-dispatchable while branch delete + /clear run. Only worktreePath is dropped.
|
|
4446
4493
|
async removeMergedWorktree(cfg, agentId, expectedTaskId) {
|
|
4447
4494
|
await this.withTaskLock(async () => {
|
|
4448
4495
|
const state = await this.agentStore.get(agentId);
|
|
@@ -4507,12 +4554,12 @@ export class AgentManager {
|
|
|
4507
4554
|
return { outcome: 'failed', detail };
|
|
4508
4555
|
}
|
|
4509
4556
|
}
|
|
4510
|
-
async runPostMergeCompaction(tmux, paneId, agentId, originalTaskId, runtime, prompt) {
|
|
4557
|
+
async runPostMergeCompaction(tmux, paneId, agentId, originalTaskId, runtime, prompt, cleanSlate) {
|
|
4511
4558
|
// 等待获取(而非无条件 add):手动 compact 持锁时直接进入会并发注入,
|
|
4512
4559
|
// 且 finally 会误删对方的 guard 放穿后续请求。
|
|
4513
4560
|
await this.acquireCompactGuard(agentId);
|
|
4514
4561
|
try {
|
|
4515
|
-
await this.runPostMergeCompactionSteps(tmux, paneId, agentId, originalTaskId, runtime, prompt);
|
|
4562
|
+
await this.runPostMergeCompactionSteps(tmux, paneId, agentId, originalTaskId, runtime, prompt, cleanSlate);
|
|
4516
4563
|
}
|
|
4517
4564
|
finally {
|
|
4518
4565
|
this.compactInFlight.delete(agentId);
|
|
@@ -4529,7 +4576,7 @@ export class AgentManager {
|
|
|
4529
4576
|
this.compactInFlight.add(agentId);
|
|
4530
4577
|
return true;
|
|
4531
4578
|
}
|
|
4532
|
-
async runPostMergeCompactionSteps(tmux, paneId, agentId, originalTaskId, runtime, prompt) {
|
|
4579
|
+
async runPostMergeCompactionSteps(tmux, paneId, agentId, originalTaskId, runtime, prompt, cleanSlate) {
|
|
4533
4580
|
const bindingStillOurs = async () => {
|
|
4534
4581
|
const s = await this.agentStore.get(agentId);
|
|
4535
4582
|
return !!s && s.taskId === originalTaskId && s.paneId === paneId;
|
|
@@ -4553,7 +4600,7 @@ export class AgentManager {
|
|
|
4553
4600
|
await this.waitForReplPromptReady(tmux, paneId, runtime, this.compactIdleWaitMs);
|
|
4554
4601
|
if (!await bindingStillOurs())
|
|
4555
4602
|
return;
|
|
4556
|
-
await tmux.sendKeysLiteral(paneId, '/compact');
|
|
4603
|
+
await tmux.sendKeysLiteral(paneId, cleanSlate ? '/clear' : '/compact');
|
|
4557
4604
|
await tmux.sendEnter(paneId);
|
|
4558
4605
|
await new Promise(r => setTimeout(r, this.compactIdlePollMs));
|
|
4559
4606
|
await this.waitForReplPromptReady(tmux, paneId, runtime, this.compactIdleWaitMs);
|
|
@@ -4834,7 +4881,7 @@ export class AgentManager {
|
|
|
4834
4881
|
return null;
|
|
4835
4882
|
const { qaId, devAgentId, projectId, newToken, newRound } = claim;
|
|
4836
4883
|
// continueSession failure after the transition would otherwise strand the
|
|
4837
|
-
// task in 'review' with a fresh token nobody will ever signal
|
|
4884
|
+
// task in 'review' with a fresh token nobody will ever signal.
|
|
4838
4885
|
const rollback = async () => {
|
|
4839
4886
|
await this.transitionTaskStatus(taskId, claim.originalStatus, { fromStatus: ['review'] }, {
|
|
4840
4887
|
signalToken: claim.originalToken,
|
|
@@ -4851,7 +4898,7 @@ export class AgentManager {
|
|
|
4851
4898
|
// The entry signal (code/spec-done|fixed) was already consumed by the
|
|
4852
4899
|
// watcher; a pre-transition failure must re-arm it with the unrotated token
|
|
4853
4900
|
// or the agent's re-emit after the operator fixes availability has no
|
|
4854
|
-
// consumer
|
|
4901
|
+
// consumer.
|
|
4855
4902
|
const rearmEntrySignal = async () => {
|
|
4856
4903
|
const entryKind = claim.originalStatus === 'fixing'
|
|
4857
4904
|
? (opts.phase === 'spec' ? 'spec-fixed' : 'code-fixed')
|
|
@@ -4935,7 +4982,7 @@ export class AgentManager {
|
|
|
4935
4982
|
// A continuation consumed the QA's reviewed signal (not the dev's entry
|
|
4936
4983
|
// signal): rollback restores the prior slice's review/token, so re-arm the
|
|
4937
4984
|
// reviewed watcher — the QA's re-emit replays the stored batch findings and
|
|
4938
|
-
// resumes the next-slice dispatch
|
|
4985
|
+
// resumes the next-slice dispatch.
|
|
4939
4986
|
const rearmConsumedSignal = async () => {
|
|
4940
4987
|
if (opts.continuation) {
|
|
4941
4988
|
await this.setupPhaseSignal(taskId, qaId, expectedKind, { skipSnapshot: true });
|
|
@@ -5005,7 +5052,7 @@ export class AgentManager {
|
|
|
5005
5052
|
taskPhase: (task.phase ?? 'code'),
|
|
5006
5053
|
currentSpecRound: task.specReviewRound,
|
|
5007
5054
|
// Continue-one-round enters from max_rounds — failure must restore THAT,
|
|
5008
|
-
// not silently demote the human's pause decision to 'review'
|
|
5055
|
+
// not silently demote the human's pause decision to 'review'.
|
|
5009
5056
|
originalStatus: task.status,
|
|
5010
5057
|
originalToken: task.signalToken,
|
|
5011
5058
|
};
|
|
@@ -5101,7 +5148,7 @@ export class AgentManager {
|
|
|
5101
5148
|
await this.releaseAgentForTask(devAgentId, taskId, 'idle').catch(() => undefined);
|
|
5102
5149
|
// Rollback restored review/old-token, but the QA's reviewed signal was
|
|
5103
5150
|
// consumed — without a subscriber its re-emit can never retry the fix
|
|
5104
|
-
// dispatch
|
|
5151
|
+
// dispatch.
|
|
5105
5152
|
await rearmReviewedSignal();
|
|
5106
5153
|
}
|
|
5107
5154
|
throw err;
|
|
@@ -5137,7 +5184,7 @@ export class AgentManager {
|
|
|
5137
5184
|
await this.updateTask(taskId, { signalToken: newToken });
|
|
5138
5185
|
// The publish prompt never reached the pane — restore the pre-rotation token
|
|
5139
5186
|
// (so recovery still matches the pre-dispatch arm) and clear the delivery
|
|
5140
|
-
// marker so retry knows this approved state is preemptible
|
|
5187
|
+
// marker so retry knows this approved state is preemptible.
|
|
5141
5188
|
const rollbackToken = async () => {
|
|
5142
5189
|
await this.updateTask(taskId, { signalToken: originalToken, publishDispatchedAt: undefined })
|
|
5143
5190
|
.catch(() => undefined);
|
|
@@ -5160,7 +5207,7 @@ export class AgentManager {
|
|
|
5160
5207
|
// path below clears it. The remaining crash window (marker written, paste
|
|
5161
5208
|
// never ran) fails CLOSED — retry 409s on a publish that never started and
|
|
5162
5209
|
// the operator escapes via Cancel — instead of the old window's fail-open
|
|
5163
|
-
// double publish (paste ran, marker missing, retry re-pastes)
|
|
5210
|
+
// double publish (paste ran, marker missing, retry re-pastes).
|
|
5164
5211
|
await this.updateTask(taskId, { publishDispatchedAt: new Date().toISOString() });
|
|
5165
5212
|
let resumed = false;
|
|
5166
5213
|
try {
|
|
@@ -5268,7 +5315,7 @@ export class AgentManager {
|
|
|
5268
5315
|
const project = this.getProjectConfig(task.projectId);
|
|
5269
5316
|
const mergeAuto = project?.merge === 'auto';
|
|
5270
5317
|
// Snapshot from verdict time — a hot config flip between publish and
|
|
5271
|
-
// confirm must not reroute an already-published artifact
|
|
5318
|
+
// confirm must not reroute an already-published artifact.
|
|
5272
5319
|
const afterDone = this.resolveAfterDone(task);
|
|
5273
5320
|
if (task.status === 'merge-ready') {
|
|
5274
5321
|
if (mergeAuto && task.prNumber) {
|
|
@@ -5303,7 +5350,7 @@ export class AgentManager {
|
|
|
5303
5350
|
matchHeadSha: task.latestHeadSha,
|
|
5304
5351
|
}));
|
|
5305
5352
|
// pr.merged's fromStatus now includes 'ready' — let the handler own the
|
|
5306
|
-
// merged transition + full cleanup chain (branch delete, /
|
|
5353
|
+
// merged transition + full cleanup chain (branch delete, /clear, release).
|
|
5307
5354
|
await this.eventBus.emit({
|
|
5308
5355
|
id: '',
|
|
5309
5356
|
type: 'pr.merged',
|
|
@@ -5347,7 +5394,7 @@ export class AgentManager {
|
|
|
5347
5394
|
}
|
|
5348
5395
|
// Merge failures keep the gate: transient gh/network errors retry via another
|
|
5349
5396
|
// Confirm, a stale head resolves via Cancel or an external decision — terminal
|
|
5350
|
-
// 'failed' would orphan the published PR/branch outside the task flow
|
|
5397
|
+
// 'failed' would orphan the published PR/branch outside the task flow.
|
|
5351
5398
|
async executeConfirmMerge(task, merge) {
|
|
5352
5399
|
try {
|
|
5353
5400
|
await merge();
|
|
@@ -5418,7 +5465,7 @@ export class AgentManager {
|
|
|
5418
5465
|
const defaultBranch = db.stdout.trim().replace(/^origin\//, '');
|
|
5419
5466
|
if (db.exitCode !== 0 || defaultBranch === '') {
|
|
5420
5467
|
// A silent 'main' fallback would push the reviewed branch onto the wrong
|
|
5421
|
-
// ref for repos whose default branch differs
|
|
5468
|
+
// ref for repos whose default branch differs.
|
|
5422
5469
|
throw new Error(`ffMergeBranch: cannot resolve default branch: ${db.stderr.trim() || 'empty origin/HEAD'}`);
|
|
5423
5470
|
}
|
|
5424
5471
|
const fetch = await runner.exec(`${cd}git fetch origin`);
|
|
@@ -5426,7 +5473,7 @@ export class AgentManager {
|
|
|
5426
5473
|
throw new Error(`ffMergeBranch [git fetch] failed: ${fetch.stderr.trim()}`);
|
|
5427
5474
|
}
|
|
5428
5475
|
// Reviewed-head guard (branch path): refuse if origin/<branch> moved after
|
|
5429
|
-
// the gate — symmetric with the pr path's --match-head-commit
|
|
5476
|
+
// the gate — symmetric with the pr path's --match-head-commit.
|
|
5430
5477
|
if (task.latestHeadSha) {
|
|
5431
5478
|
const remoteHead = await runner.exec(`${cd}git rev-parse ${shellQuote(`origin/${branch}`)}`);
|
|
5432
5479
|
if (remoteHead.exitCode !== 0 || remoteHead.stdout.trim() !== task.latestHeadSha) {
|
|
@@ -5442,7 +5489,7 @@ export class AgentManager {
|
|
|
5442
5489
|
throw new Error(`ffMergeBranch [push] failed: ${push.stderr.trim() || push.stdout.trim()}`);
|
|
5443
5490
|
}
|
|
5444
5491
|
// The merge has landed; branch deletion is cleanup — a transient failure
|
|
5445
|
-
// here must not flip an already-merged task to failed
|
|
5492
|
+
// here must not flip an already-merged task to failed.
|
|
5446
5493
|
const del = await runner.exec(`${cd}git push origin --delete ${shellQuote(branch)}`);
|
|
5447
5494
|
if (del.exitCode !== 0) {
|
|
5448
5495
|
console.warn(`[AgentManager] ffMergeBranch: post-merge branch delete failed for ${branch}: ${del.stderr.trim() || del.stdout.trim()}`);
|