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.
Files changed (70) hide show
  1. package/dist/agent/bootstrap.d.ts +1 -1
  2. package/dist/agent/bootstrap.d.ts.map +1 -1
  3. package/dist/agent/bootstrap.js +33 -0
  4. package/dist/agent/bootstrap.js.map +1 -1
  5. package/dist/agent/manager.d.ts +6 -4
  6. package/dist/agent/manager.d.ts.map +1 -1
  7. package/dist/agent/manager.js +92 -53
  8. package/dist/agent/manager.js.map +1 -1
  9. package/dist/agent/phase-signal.d.ts.map +1 -1
  10. package/dist/agent/phase-signal.js.map +1 -1
  11. package/dist/agent/preflight.d.ts.map +1 -1
  12. package/dist/agent/preflight.js +49 -22
  13. package/dist/agent/preflight.js.map +1 -1
  14. package/dist/agent/prompt.d.ts +1 -1
  15. package/dist/agent/prompt.d.ts.map +1 -1
  16. package/dist/agent/prompt.js +5 -6
  17. package/dist/agent/prompt.js.map +1 -1
  18. package/dist/agent/repo-store.d.ts +5 -3
  19. package/dist/agent/repo-store.d.ts.map +1 -1
  20. package/dist/agent/repo-store.js +53 -24
  21. package/dist/agent/repo-store.js.map +1 -1
  22. package/dist/agent/review-transport.js +2 -2
  23. package/dist/agent/review-transport.js.map +1 -1
  24. package/dist/agent/tmux-probe-poller.js +4 -4
  25. package/dist/agent/tmux-probe-poller.js.map +1 -1
  26. package/dist/agent/tmux.js +9 -9
  27. package/dist/agent/tmux.js.map +1 -1
  28. package/dist/api/agents.js +1 -1
  29. package/dist/api/agents.js.map +1 -1
  30. package/dist/api/config.js +1 -1
  31. package/dist/api/config.js.map +1 -1
  32. package/dist/api/hosts.js +2 -2
  33. package/dist/api/hosts.js.map +1 -1
  34. package/dist/api/tasks.js +1 -1
  35. package/dist/api/tasks.js.map +1 -1
  36. package/dist/config/loader.js +5 -1
  37. package/dist/config/loader.js.map +1 -1
  38. package/dist/config/validator.d.ts.map +1 -1
  39. package/dist/config/validator.js +37 -3
  40. package/dist/config/validator.js.map +1 -1
  41. package/dist/event/handlers.d.ts.map +1 -1
  42. package/dist/event/handlers.js +8 -9
  43. package/dist/event/handlers.js.map +1 -1
  44. package/dist/event/server-handlers.js +10 -10
  45. package/dist/event/server-handlers.js.map +1 -1
  46. package/dist/github/poller.d.ts.map +1 -1
  47. package/dist/github/poller.js +13 -3
  48. package/dist/github/poller.js.map +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +7 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/shared/constants.js +1 -1
  53. package/dist/shared/constants.js.map +1 -1
  54. package/dist/shared/git-url.d.ts +14 -0
  55. package/dist/shared/git-url.d.ts.map +1 -0
  56. package/dist/shared/git-url.js +76 -0
  57. package/dist/shared/git-url.js.map +1 -0
  58. package/dist/shared/index.d.ts +1 -0
  59. package/dist/shared/index.d.ts.map +1 -1
  60. package/dist/shared/index.js +1 -0
  61. package/dist/shared/index.js.map +1 -1
  62. package/dist/shared/types.d.ts +1 -1
  63. package/dist/shared/types.d.ts.map +1 -1
  64. package/dist/terminal/attach.d.ts.map +1 -1
  65. package/dist/terminal/attach.js +15 -5
  66. package/dist/terminal/attach.js.map +1 -1
  67. package/dist/web/assets/index-CC3XRKh1.js +4 -0
  68. package/dist/web/index.html +1 -1
  69. package/package.json +1 -1
  70. package/dist/web/assets/index-OtgjyQI1.js +0 -4
@@ -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 (task-055).
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
- // (task-071) without risking a double-submit (a real submit goes busy well within this window).
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 (PR #288).
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 ?? null;
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
- // 撤销 updatedAt guard(round-13 codex review):updatedAt 太宽,正常 background updates
587
+ // 不用 updatedAt guardupdatedAt 太宽,正常 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 死锁,见 owner round-15 评审)。
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 (PR #288).
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 (PR #288).
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/${project.repo}/pulls/${task.prNumber}/comments`, '.[] | select(.in_reply_to_id != null) | .created_at'),
1850
- this.ghCreatedAt(`repos/${project.repo}/issues/${task.prNumber}/comments`, '.[].created_at'),
1851
- this.ghCreatedAt(`repos/${project.repo}/pulls/${task.prNumber}/reviews`, '.[] | select(.submitted_at != null) | .submitted_at'),
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 (task-055).
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.config.review.mode ?? 'github',
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.config.review.mode ?? 'github',
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.config.review.mode ?? 'github',
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.config.review.mode ?? 'github',
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 (task-055).
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
- /** task-055 entry A: write an uploaded image to the running agent's host, paste its path (no Enter). */
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 (task-055).
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, task-055).
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 不传图(task-055)。
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
- // 下一轮整组重注入——正是 task-071 的 SKILLS 重复注入。但 composerDelivered=false(paste 时 pane
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 (codex 3345468647). Any other timeout (idle composer / swallowed Enter) did
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 (task-055).
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 (PR #288).
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 (PR #288).
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
- // task-044 重构:create-time 不再校验 awaiting_human / creating / bound —— 这些都是"忙",
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 (PR #288).
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 (task-060).
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). task-055.
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 (PR #288).
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 (PR #288).
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 (PR #288).
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 ?? null;
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 (PR #288).
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 (PR #288).
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 (PR #288).
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' (PR #288).
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 (PR #288).
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 (PR #288).
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) (PR #288).
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 (PR #288).
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 (PR #288).
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 (PR #288).
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 (PR #288).
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 (PR #288).
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()}`);