baxian 1.2.0 → 1.2.1
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 +6 -4
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +92 -53
- 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 +5 -6
- 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.js +1 -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 +8 -9
- 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/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-CC3XRKh1.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)
|
|
@@ -2392,7 +2423,7 @@ export class AgentManager {
|
|
|
2392
2423
|
}
|
|
2393
2424
|
return filenames;
|
|
2394
2425
|
}
|
|
2395
|
-
// Retry reads staged bytes up-front; a missing source is a visible 409, never a silent drop
|
|
2426
|
+
// Retry reads staged bytes up-front; a missing source is a visible 409, never a silent drop.
|
|
2396
2427
|
async readStagedImages(taskId, filenames) {
|
|
2397
2428
|
const dir = join(this.imageStagingRoot, taskId);
|
|
2398
2429
|
const out = [];
|
|
@@ -2410,7 +2441,7 @@ export class AgentManager {
|
|
|
2410
2441
|
return out;
|
|
2411
2442
|
}
|
|
2412
2443
|
// 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
|
|
2444
|
+
// woven into the prompt. Missing staging aborts the dispatch loudly (no silent skip).
|
|
2414
2445
|
async materializeTaskImages(runner, task) {
|
|
2415
2446
|
const filenames = task.images ?? [];
|
|
2416
2447
|
if (filenames.length === 0)
|
|
@@ -2434,7 +2465,7 @@ export class AgentManager {
|
|
|
2434
2465
|
// Dev-facing deliverable phases all carry the task's uploaded images, since the image is a
|
|
2435
2466
|
// persistent task input the dev needs while producing or revising the spec/code — and a fresh
|
|
2436
2467
|
// runtime (restart/recovery) loses the original context. IMAGE_DISPATCH_PHASES 中的后续阶段经
|
|
2437
|
-
// continueSession 触发此方法;QA 阶段和 post-approve
|
|
2468
|
+
// continueSession 触发此方法;QA 阶段和 post-approve 不传图。
|
|
2438
2469
|
async imagePathsForDispatch(runner, task, phase) {
|
|
2439
2470
|
if (!IMAGE_DISPATCH_PHASES.has(phase))
|
|
2440
2471
|
return [];
|
|
@@ -2653,6 +2684,14 @@ export class AgentManager {
|
|
|
2653
2684
|
const hasQaPartner = !!(task.qaAgentId ?? this.findQaPartner(agentId)?.id);
|
|
2654
2685
|
let prompt;
|
|
2655
2686
|
try {
|
|
2687
|
+
// Server review has no platform fallback: a dev with no QA partner would emit a
|
|
2688
|
+
// completion signal nobody consumes and the task strands (server mode is forced
|
|
2689
|
+
// for non-GitHub repos via effectiveReviewMode). Enforce at this single dispatch
|
|
2690
|
+
// chokepoint — the catch below removes the worktree, then the task fails cleanly.
|
|
2691
|
+
if (task.reviewMode === 'server' && !hasQaPartner) {
|
|
2692
|
+
throw new DispatchTerminalError('server_review_needs_qa', `Task ${task.id} runs in server review mode but dev "${agentId}" has no QA partner; ` +
|
|
2693
|
+
`pair a QA agent with this dev (server review has no platform fallback to absorb the verdict).`);
|
|
2694
|
+
}
|
|
2656
2695
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
2657
2696
|
prompt = buildPromptInline({
|
|
2658
2697
|
task,
|
|
@@ -2775,7 +2814,7 @@ export class AgentManager {
|
|
|
2775
2814
|
}
|
|
2776
2815
|
// dedup baseline 记录的是「已 paste 进 idle composer 的 skill 文本」:paste 落入 composer 即进
|
|
2777
2816
|
// REPL 上下文,与 submit-ack 无关。ack 超时(首个 Enter 被吞)下 skill 仍在 composer,跳过落盘会让
|
|
2778
|
-
//
|
|
2817
|
+
// 下一轮整组重注入——即 SKILLS 重复注入。但 composerDelivered=false(paste 时 pane
|
|
2779
2818
|
// 已 busy,文本进了运行中输入流而非 composer)则不能落盘,否则恢复提示会缺必需 skill。freshRuntime
|
|
2780
2819
|
// 负责 REPL 真正重启时作废 baseline。
|
|
2781
2820
|
if (ack.composerDelivered) {
|
|
@@ -2905,7 +2944,7 @@ export class AgentManager {
|
|
|
2905
2944
|
});
|
|
2906
2945
|
// "pane already busy at baseline" means the paste landed on an already-running input stream,
|
|
2907
2946
|
// NOT an idle composer — the skills did NOT become context, so callers must not record them
|
|
2908
|
-
// as injected
|
|
2947
|
+
// as injected. Any other timeout (idle composer / swallowed Enter) did
|
|
2909
2948
|
// deliver the prompt text into the composer.
|
|
2910
2949
|
const composerDelivered = !/pane already busy at baseline/.test(message);
|
|
2911
2950
|
return { acked: false, composerDelivered };
|
|
@@ -3028,7 +3067,7 @@ export class AgentManager {
|
|
|
3028
3067
|
&& opts.postApproveRedispatchCount > 0
|
|
3029
3068
|
&& !ensure.freshRuntime;
|
|
3030
3069
|
// 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
|
|
3070
|
+
// task's uploaded images so a fresh code-phase context still sees their paths.
|
|
3032
3071
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
3033
3072
|
prompt = buildPromptInline({
|
|
3034
3073
|
task,
|
|
@@ -3440,7 +3479,7 @@ export class AgentManager {
|
|
|
3440
3479
|
let qaToRelease;
|
|
3441
3480
|
// Server-mode ready gate may have already published remote artifacts
|
|
3442
3481
|
// (pushed branch / open PR). Capture before flipping to cancelled so the
|
|
3443
|
-
// post-lock cleanup can retire them instead of orphaning
|
|
3482
|
+
// post-lock cleanup can retire them instead of orphaning.
|
|
3444
3483
|
// mayBeInFlight: approved+marker means the publish prompt may STILL be
|
|
3445
3484
|
// running — retirement must wait for the dev interrupt or the in-flight
|
|
3446
3485
|
// push/pr-create would recreate the artifacts right after cleanup.
|
|
@@ -3484,7 +3523,7 @@ export class AgentManager {
|
|
|
3484
3523
|
}
|
|
3485
3524
|
}
|
|
3486
3525
|
else if (task.status === 'merge-ready' && task.prNumber !== undefined && task.branch && task.agentId) {
|
|
3487
|
-
// GitHub-mode gate cancel leaves the same orphaned PR/branch
|
|
3526
|
+
// GitHub-mode gate cancel leaves the same orphaned PR/branch.
|
|
3488
3527
|
publishedCleanup = {
|
|
3489
3528
|
afterDone: 'pr',
|
|
3490
3529
|
branch: task.branch,
|
|
@@ -3570,7 +3609,7 @@ export class AgentManager {
|
|
|
3570
3609
|
const project = this.getProjectConfig(result.projectId);
|
|
3571
3610
|
try {
|
|
3572
3611
|
if (publishedCleanup.afterDone === 'pr' && publishedCleanup.prNumber !== undefined && project) {
|
|
3573
|
-
const close = await this.platformRunner.exec(`gh pr close ${publishedCleanup.prNumber} --repo ${shellQuote(project.repo)} ` +
|
|
3612
|
+
const close = await this.platformRunner.exec(`gh pr close ${publishedCleanup.prNumber} --repo ${shellQuote(repoSlug(project.repo))} ` +
|
|
3574
3613
|
`--comment ${shellQuote('Task cancelled in baxian; closing the published PR.')} --delete-branch`);
|
|
3575
3614
|
if (close.exitCode !== 0)
|
|
3576
3615
|
throw new Error(close.stderr.trim() || close.stdout.trim());
|
|
@@ -3605,7 +3644,7 @@ export class AgentManager {
|
|
|
3605
3644
|
}
|
|
3606
3645
|
return result;
|
|
3607
3646
|
}
|
|
3608
|
-
//
|
|
3647
|
+
// create-time 不再校验 awaiting_human / creating / bound —— 这些都是"忙",
|
|
3609
3648
|
// 允许入队(落 pending);可执行性判断下沉到 dispatchPendingTask(或 createTask 已空闲分支)。
|
|
3610
3649
|
// 仍保留:agent 存在/同 project/role=dev(非空时)+ prompt size 上界。
|
|
3611
3650
|
async validateTaskDispatch(projectId, input) {
|
|
@@ -3868,7 +3907,7 @@ export class AgentManager {
|
|
|
3868
3907
|
throw new ApiError(409, `Continue one round is only supported for code-phase tasks`);
|
|
3869
3908
|
}
|
|
3870
3909
|
// 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
|
|
3910
|
+
// fix protocol from the stored findings — no PR exists at this point.
|
|
3872
3911
|
if (task.reviewMode === 'server') {
|
|
3873
3912
|
if (!task.agentId) {
|
|
3874
3913
|
throw new ApiError(400, `Task ${taskId} has no dev agent; cannot continue`);
|
|
@@ -4070,7 +4109,7 @@ export class AgentManager {
|
|
|
4070
4109
|
return { expectedKinds: ['spec-fixed'], agentId: task.agentId };
|
|
4071
4110
|
}
|
|
4072
4111
|
if (task.phase !== 'spec' && task.status === 'fixing' && task.agentId) {
|
|
4073
|
-
// Code-track fixing: dev emits pr-fixed when the round is done
|
|
4112
|
+
// Code-track fixing: dev emits pr-fixed when the round is done.
|
|
4074
4113
|
return { expectedKinds: ['pr-fixed'], agentId: task.agentId };
|
|
4075
4114
|
}
|
|
4076
4115
|
if (task.phase === undefined && task.status === 'in_progress' && task.agentId) {
|
|
@@ -4156,7 +4195,7 @@ export class AgentManager {
|
|
|
4156
4195
|
preferredAgentId: old.preferredAgentId,
|
|
4157
4196
|
};
|
|
4158
4197
|
// Retry preserves uploaded images: read the old task's staged bytes up-front
|
|
4159
|
-
// (missing → visible 409 before any new task/binding is created).
|
|
4198
|
+
// (missing → visible 409 before any new task/binding is created).
|
|
4160
4199
|
if (old.images?.length) {
|
|
4161
4200
|
input.images = await this.readStagedImages(old.id, old.images);
|
|
4162
4201
|
}
|
|
@@ -4222,7 +4261,7 @@ export class AgentManager {
|
|
|
4222
4261
|
const matchHead = opts.matchHeadSha
|
|
4223
4262
|
? ` --match-head-commit ${shellQuote(opts.matchHeadSha)}`
|
|
4224
4263
|
: '';
|
|
4225
|
-
const result = await this.platformRunner.exec(`gh pr merge ${task.prNumber} --repo ${shellQuote(project.repo)}${matchHead} --squash --delete-branch`);
|
|
4264
|
+
const result = await this.platformRunner.exec(`gh pr merge ${task.prNumber} --repo ${shellQuote(repoSlug(project.repo))}${matchHead} --squash --delete-branch`);
|
|
4226
4265
|
if (result.exitCode !== 0) {
|
|
4227
4266
|
throw new Error(`gh pr merge failed for PR #${task.prNumber}: ${result.stderr || result.stdout}`);
|
|
4228
4267
|
}
|
|
@@ -4247,7 +4286,7 @@ export class AgentManager {
|
|
|
4247
4286
|
const task = await this.claimCompleteGate(taskId, ['max_rounds', 'approved']);
|
|
4248
4287
|
try {
|
|
4249
4288
|
// Server-mode publish retry: a failed afterDone dispatch leaves the task
|
|
4250
|
-
// 'approved' with dev released — mark-complete re-runs the publish
|
|
4289
|
+
// 'approved' with dev released — mark-complete re-runs the publish.
|
|
4251
4290
|
const serverApprovedRetry = task.status === 'approved' && task.reviewMode === 'server';
|
|
4252
4291
|
if (!serverApprovedRetry && task.status !== 'max_rounds') {
|
|
4253
4292
|
throw new ApiError(409, `Task ${taskId} is not at max_rounds (status=${task.status})`);
|
|
@@ -4267,7 +4306,7 @@ export class AgentManager {
|
|
|
4267
4306
|
// publishDispatchedAt persists delivery across restarts: set = the
|
|
4268
4307
|
// prompt reached the pane before the restart (still in flight, 409);
|
|
4269
4308
|
// cleared = the dispatch failed and this approved state is retryable —
|
|
4270
|
-
// stop the recovered watch and let the retry own the dispatch
|
|
4309
|
+
// stop the recovered watch and let the retry own the dispatch.
|
|
4271
4310
|
if (this.phaseSignalWatcher?.expectedKindsFor(taskId).has('code-ready')) {
|
|
4272
4311
|
if (!this.phaseSignalWatcher.isRecovered(taskId) || task.publishDispatchedAt) {
|
|
4273
4312
|
throw new ApiError(409, `Task ${taskId} publish is in flight; retry only after it fails`);
|
|
@@ -4287,13 +4326,13 @@ export class AgentManager {
|
|
|
4287
4326
|
return (await this.taskStore.get(taskId));
|
|
4288
4327
|
}
|
|
4289
4328
|
// 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
|
|
4329
|
+
// afterDone flow (or finish directly) instead of the legacy PR merge.
|
|
4291
4330
|
// Inside the in-flight claim so a concurrent Continue can't act on the same
|
|
4292
4331
|
// max_rounds snapshot and release dev mid-publish.
|
|
4293
4332
|
if (task.reviewMode === 'server') {
|
|
4294
4333
|
// Max_rounds never routed an approve verdict — snapshot afterDone NOW so
|
|
4295
4334
|
// the eventual ready-confirm uses this decision, not future hot config.
|
|
4296
|
-
const afterDone = this.config.review.afterDone
|
|
4335
|
+
const afterDone = this.coerceAfterDone(task.projectId, this.config.review.afterDone);
|
|
4297
4336
|
await this.updateTask(taskId, { afterDone });
|
|
4298
4337
|
if (afterDone === null) {
|
|
4299
4338
|
const done = await this.transitionTaskStatus(taskId, 'done', { fromStatus: ['max_rounds'] });
|
|
@@ -4834,7 +4873,7 @@ export class AgentManager {
|
|
|
4834
4873
|
return null;
|
|
4835
4874
|
const { qaId, devAgentId, projectId, newToken, newRound } = claim;
|
|
4836
4875
|
// continueSession failure after the transition would otherwise strand the
|
|
4837
|
-
// task in 'review' with a fresh token nobody will ever signal
|
|
4876
|
+
// task in 'review' with a fresh token nobody will ever signal.
|
|
4838
4877
|
const rollback = async () => {
|
|
4839
4878
|
await this.transitionTaskStatus(taskId, claim.originalStatus, { fromStatus: ['review'] }, {
|
|
4840
4879
|
signalToken: claim.originalToken,
|
|
@@ -4851,7 +4890,7 @@ export class AgentManager {
|
|
|
4851
4890
|
// The entry signal (code/spec-done|fixed) was already consumed by the
|
|
4852
4891
|
// watcher; a pre-transition failure must re-arm it with the unrotated token
|
|
4853
4892
|
// or the agent's re-emit after the operator fixes availability has no
|
|
4854
|
-
// consumer
|
|
4893
|
+
// consumer.
|
|
4855
4894
|
const rearmEntrySignal = async () => {
|
|
4856
4895
|
const entryKind = claim.originalStatus === 'fixing'
|
|
4857
4896
|
? (opts.phase === 'spec' ? 'spec-fixed' : 'code-fixed')
|
|
@@ -4935,7 +4974,7 @@ export class AgentManager {
|
|
|
4935
4974
|
// A continuation consumed the QA's reviewed signal (not the dev's entry
|
|
4936
4975
|
// signal): rollback restores the prior slice's review/token, so re-arm the
|
|
4937
4976
|
// reviewed watcher — the QA's re-emit replays the stored batch findings and
|
|
4938
|
-
// resumes the next-slice dispatch
|
|
4977
|
+
// resumes the next-slice dispatch.
|
|
4939
4978
|
const rearmConsumedSignal = async () => {
|
|
4940
4979
|
if (opts.continuation) {
|
|
4941
4980
|
await this.setupPhaseSignal(taskId, qaId, expectedKind, { skipSnapshot: true });
|
|
@@ -5005,7 +5044,7 @@ export class AgentManager {
|
|
|
5005
5044
|
taskPhase: (task.phase ?? 'code'),
|
|
5006
5045
|
currentSpecRound: task.specReviewRound,
|
|
5007
5046
|
// Continue-one-round enters from max_rounds — failure must restore THAT,
|
|
5008
|
-
// not silently demote the human's pause decision to 'review'
|
|
5047
|
+
// not silently demote the human's pause decision to 'review'.
|
|
5009
5048
|
originalStatus: task.status,
|
|
5010
5049
|
originalToken: task.signalToken,
|
|
5011
5050
|
};
|
|
@@ -5101,7 +5140,7 @@ export class AgentManager {
|
|
|
5101
5140
|
await this.releaseAgentForTask(devAgentId, taskId, 'idle').catch(() => undefined);
|
|
5102
5141
|
// Rollback restored review/old-token, but the QA's reviewed signal was
|
|
5103
5142
|
// consumed — without a subscriber its re-emit can never retry the fix
|
|
5104
|
-
// dispatch
|
|
5143
|
+
// dispatch.
|
|
5105
5144
|
await rearmReviewedSignal();
|
|
5106
5145
|
}
|
|
5107
5146
|
throw err;
|
|
@@ -5137,7 +5176,7 @@ export class AgentManager {
|
|
|
5137
5176
|
await this.updateTask(taskId, { signalToken: newToken });
|
|
5138
5177
|
// The publish prompt never reached the pane — restore the pre-rotation token
|
|
5139
5178
|
// (so recovery still matches the pre-dispatch arm) and clear the delivery
|
|
5140
|
-
// marker so retry knows this approved state is preemptible
|
|
5179
|
+
// marker so retry knows this approved state is preemptible.
|
|
5141
5180
|
const rollbackToken = async () => {
|
|
5142
5181
|
await this.updateTask(taskId, { signalToken: originalToken, publishDispatchedAt: undefined })
|
|
5143
5182
|
.catch(() => undefined);
|
|
@@ -5160,7 +5199,7 @@ export class AgentManager {
|
|
|
5160
5199
|
// path below clears it. The remaining crash window (marker written, paste
|
|
5161
5200
|
// never ran) fails CLOSED — retry 409s on a publish that never started and
|
|
5162
5201
|
// the operator escapes via Cancel — instead of the old window's fail-open
|
|
5163
|
-
// double publish (paste ran, marker missing, retry re-pastes)
|
|
5202
|
+
// double publish (paste ran, marker missing, retry re-pastes).
|
|
5164
5203
|
await this.updateTask(taskId, { publishDispatchedAt: new Date().toISOString() });
|
|
5165
5204
|
let resumed = false;
|
|
5166
5205
|
try {
|
|
@@ -5268,7 +5307,7 @@ export class AgentManager {
|
|
|
5268
5307
|
const project = this.getProjectConfig(task.projectId);
|
|
5269
5308
|
const mergeAuto = project?.merge === 'auto';
|
|
5270
5309
|
// Snapshot from verdict time — a hot config flip between publish and
|
|
5271
|
-
// confirm must not reroute an already-published artifact
|
|
5310
|
+
// confirm must not reroute an already-published artifact.
|
|
5272
5311
|
const afterDone = this.resolveAfterDone(task);
|
|
5273
5312
|
if (task.status === 'merge-ready') {
|
|
5274
5313
|
if (mergeAuto && task.prNumber) {
|
|
@@ -5347,7 +5386,7 @@ export class AgentManager {
|
|
|
5347
5386
|
}
|
|
5348
5387
|
// Merge failures keep the gate: transient gh/network errors retry via another
|
|
5349
5388
|
// Confirm, a stale head resolves via Cancel or an external decision — terminal
|
|
5350
|
-
// 'failed' would orphan the published PR/branch outside the task flow
|
|
5389
|
+
// 'failed' would orphan the published PR/branch outside the task flow.
|
|
5351
5390
|
async executeConfirmMerge(task, merge) {
|
|
5352
5391
|
try {
|
|
5353
5392
|
await merge();
|
|
@@ -5418,7 +5457,7 @@ export class AgentManager {
|
|
|
5418
5457
|
const defaultBranch = db.stdout.trim().replace(/^origin\//, '');
|
|
5419
5458
|
if (db.exitCode !== 0 || defaultBranch === '') {
|
|
5420
5459
|
// A silent 'main' fallback would push the reviewed branch onto the wrong
|
|
5421
|
-
// ref for repos whose default branch differs
|
|
5460
|
+
// ref for repos whose default branch differs.
|
|
5422
5461
|
throw new Error(`ffMergeBranch: cannot resolve default branch: ${db.stderr.trim() || 'empty origin/HEAD'}`);
|
|
5423
5462
|
}
|
|
5424
5463
|
const fetch = await runner.exec(`${cd}git fetch origin`);
|
|
@@ -5426,7 +5465,7 @@ export class AgentManager {
|
|
|
5426
5465
|
throw new Error(`ffMergeBranch [git fetch] failed: ${fetch.stderr.trim()}`);
|
|
5427
5466
|
}
|
|
5428
5467
|
// Reviewed-head guard (branch path): refuse if origin/<branch> moved after
|
|
5429
|
-
// the gate — symmetric with the pr path's --match-head-commit
|
|
5468
|
+
// the gate — symmetric with the pr path's --match-head-commit.
|
|
5430
5469
|
if (task.latestHeadSha) {
|
|
5431
5470
|
const remoteHead = await runner.exec(`${cd}git rev-parse ${shellQuote(`origin/${branch}`)}`);
|
|
5432
5471
|
if (remoteHead.exitCode !== 0 || remoteHead.stdout.trim() !== task.latestHeadSha) {
|
|
@@ -5442,7 +5481,7 @@ export class AgentManager {
|
|
|
5442
5481
|
throw new Error(`ffMergeBranch [push] failed: ${push.stderr.trim() || push.stdout.trim()}`);
|
|
5443
5482
|
}
|
|
5444
5483
|
// The merge has landed; branch deletion is cleanup — a transient failure
|
|
5445
|
-
// here must not flip an already-merged task to failed
|
|
5484
|
+
// here must not flip an already-merged task to failed.
|
|
5446
5485
|
const del = await runner.exec(`${cd}git push origin --delete ${shellQuote(branch)}`);
|
|
5447
5486
|
if (del.exitCode !== 0) {
|
|
5448
5487
|
console.warn(`[AgentManager] ffMergeBranch: post-merge branch delete failed for ${branch}: ${del.stderr.trim() || del.stdout.trim()}`);
|