@ifrankwang/openspec-agents 0.132.1 → 0.133.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/.mcp-server/cli.mjs +6950 -2070
- package/README.md +10 -0
- package/assets/agents/openspec-reviewer.md +1 -1
- package/assets/skills/orchestrator/SKILL.md +9 -0
- package/assets/workflows/review-simple.yaml +86 -0
- package/assets/workflows/review.yaml +124 -0
- package/dsh/cordis.patch.yml +1 -1
- package/package.json +1 -1
- package/src/adapters/plugin-common/index.ts +1 -1
- package/src/core/git.ts +124 -22
- package/src/core/state.ts +3 -0
- package/src/core/tools/lifecycle.ts +416 -31
- package/src/core/tools/schemas.ts +38 -5
- package/src/core/tools/types.ts +12 -2
- package/src/core/types.ts +26 -0
- package/src/core/views.ts +14 -1
- package/src/core/workflow/engine.ts +41 -9
- package/src/core/workflow/loader.ts +12 -3
- package/src/core/workflow/status.ts +76 -5
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "path"
|
|
2
2
|
import { rmdir } from "node:fs/promises"
|
|
3
|
-
import type { OrchestrateState, TaskItem, TaskStatus, WorkflowMode } from "../types.ts"
|
|
4
|
-
import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS, SIMPLE_REVIEW_STEPS } from "../types.ts"
|
|
3
|
+
import type { OrchestrateState, TaskItem, TaskStatus, WorkflowMode, ReviewScope } from "../types.ts"
|
|
4
|
+
import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS, SIMPLE_REVIEW_STEPS, REVIEW_TASK_GROUP_ID } from "../types.ts"
|
|
5
5
|
import { agentToReviewLayer } from "../constants.ts"
|
|
6
|
-
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, removeTaskGroupWorktree, type DetectChangesResult } from "../git.ts"
|
|
6
|
+
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, removeTaskGroupWorktree, isLocalBranch, listLocalBranches, type DetectChangesResult } from "../git.ts"
|
|
7
7
|
import { readStateByWorktree, readStateByChangeId, writeState, writeContextToWorktree } from "../state.ts"
|
|
8
8
|
import { generateIsolationNamespace } from "../namespace.ts"
|
|
9
9
|
import { readExemptions } from "../exemptions.ts"
|
|
@@ -12,7 +12,7 @@ import type { ParsedTask } from "../tasks-md.ts"
|
|
|
12
12
|
import { assertOrchestrator, findTaskGroup } from "../derive.ts"
|
|
13
13
|
import { assertPathWithin } from "../paths.ts"
|
|
14
14
|
import { loadWorkflowFile, resolveWorkflowPath, type LoadedWorkflow } from "../workflow/loader.ts"
|
|
15
|
-
import { createInitialWorkItem, isBlockingSeverity, isTerminalPhase, recommendForItem, resetInternalRetryCount, adjudicateStep, clearStepTags } from "../workflow/engine.ts"
|
|
15
|
+
import { createInitialWorkItem, isBlockingSeverity, isTerminalPhase, recommendForItem, resetInternalRetryCount, adjudicateStep, clearStepTags, REVIEW_FIX_POLICY_KEY } from "../workflow/engine.ts"
|
|
16
16
|
import { renderWorkflowStatusView } from "../workflow/status.ts"
|
|
17
17
|
import { taskChildrenOf } from "../task-children.ts"
|
|
18
18
|
import type { WorkItem, WorkItemPhase } from "../workflow/types.ts"
|
|
@@ -143,7 +143,7 @@ function applyRecoveryState(
|
|
|
143
143
|
item: WorkItem,
|
|
144
144
|
recovery: InitParams["recovery"],
|
|
145
145
|
parsedTasks: ParsedTask[],
|
|
146
|
-
mode
|
|
146
|
+
state: Pick<OrchestrateState, "mode" | "kind" | "reviewScope">,
|
|
147
147
|
): void {
|
|
148
148
|
// 恢复重建为已知状态后清除残留推进阻塞原因,避免 orchestrator 视图展示过期信息
|
|
149
149
|
delete item.metadata["_advance_block_reason"]
|
|
@@ -151,6 +151,41 @@ function applyRecoveryState(
|
|
|
151
151
|
resetInternalRetryCount(item)
|
|
152
152
|
// 清除检查点标记残留:恢复重建为已知状态后 _checkpoint 已无意义(checkpoint 态属于中断中的 step)。
|
|
153
153
|
delete item.metadata["_checkpoint"]
|
|
154
|
+
if (state.kind === "review") {
|
|
155
|
+
// 独立审查会话恢复:无 analyze 前置、无 task children;phase 值域入口已收敛为 review / dev_impl。
|
|
156
|
+
const phase = recovery?.phase
|
|
157
|
+
if (!phase || phase === "dev_impl") {
|
|
158
|
+
// 恢复进 implement:tags 整体重置(不残留 implement passed,否则 dev 不会被重派)
|
|
159
|
+
item.phase = "in_progress"
|
|
160
|
+
item.currentStep = "implement"
|
|
161
|
+
item.tags = {}
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
// review 分支:恢复进 review 时 implement 必然已通过
|
|
165
|
+
item.phase = "review"
|
|
166
|
+
item.tags["implement:openspec-developer"] = "passed"
|
|
167
|
+
// failed 审查标记删除回 pending(passed 保留——已审查通过无需重跑),对 review 两种颗粒度的
|
|
168
|
+
// step id(verify_* / quality_review)统一处理
|
|
169
|
+
for (const key of Object.keys(item.tags)) {
|
|
170
|
+
if (
|
|
171
|
+
key.startsWith("verify_tool:") || key.startsWith("verify_task:") ||
|
|
172
|
+
key.startsWith("verify_quality:") || key.startsWith("quality_review:")
|
|
173
|
+
) {
|
|
174
|
+
if (item.tags[key] !== "passed") delete item.tags[key]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// reset_steps:清空指定审查 step 全部 tags(passed 也清,强制重审),值域已按形态校验
|
|
178
|
+
for (const stepId of recovery?.reset_steps ?? []) {
|
|
179
|
+
clearStepTags(item, stepId)
|
|
180
|
+
}
|
|
181
|
+
// currentStep 前移到第一个未全 passed 的审查 step(review workflow 生效,按颗粒度选文件)
|
|
182
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state))
|
|
183
|
+
item.currentStep = firstUnpassedReviewStep(item, workflow)
|
|
184
|
+
// 全 passed 收口 done:review 会话无 task children,终态检查天然通过
|
|
185
|
+
if (item.currentStep === null) item.phase = "done"
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
const mode = state.mode
|
|
154
189
|
if (mode === "simple") {
|
|
155
190
|
const phase = recovery?.phase
|
|
156
191
|
if (!phase || phase === "task_analysis" || phase === "dev_impl") {
|
|
@@ -281,20 +316,39 @@ function assertValidRecovery(recovery: InitParams["recovery"]): void {
|
|
|
281
316
|
}
|
|
282
317
|
|
|
283
318
|
/**
|
|
284
|
-
* reset_steps
|
|
285
|
-
* -
|
|
286
|
-
* - simple 模式仅接受 quality_review
|
|
287
|
-
*
|
|
288
|
-
* (错误早于任何状态变更且不落盘),值域校验依赖 state
|
|
319
|
+
* reset_steps 值域的形态感知校验(state 读取/固化后调用):
|
|
320
|
+
* - 独立审查会话按颗粒度:thorough 仅接受 verify_tool/verify_task/verify_quality,simple 仅接受 quality_review;
|
|
321
|
+
* - change 会话 full 模式仅接受 verify_tool/verify_task/verify_quality,simple 模式仅接受 quality_review。
|
|
322
|
+
* 跨形态值抛错并列出当前形态的合法值。phase/review_layer/组合校验在 assertValidRecovery 前置
|
|
323
|
+
* (错误早于任何状态变更且不落盘),值域校验依赖 state 故后置到 state 读取之后——
|
|
289
324
|
* state 读取不是状态变更,该原则不破坏。
|
|
290
325
|
*/
|
|
291
|
-
function assertValidResetStepValues(
|
|
326
|
+
function assertValidResetStepValues(
|
|
327
|
+
recovery: InitParams["recovery"],
|
|
328
|
+
state: Pick<OrchestrateState, "mode" | "kind" | "reviewScope">,
|
|
329
|
+
): void {
|
|
292
330
|
if (!recovery?.reset_steps?.length) return
|
|
293
|
-
|
|
331
|
+
let valid: readonly string[]
|
|
332
|
+
let formLabel: string
|
|
333
|
+
if (state.kind === "review") {
|
|
334
|
+
if (state.reviewScope?.granularity === "simple") {
|
|
335
|
+
valid = SIMPLE_REVIEW_STEPS
|
|
336
|
+
formLabel = "独立审查会话(simple 颗粒度)"
|
|
337
|
+
} else {
|
|
338
|
+
valid = REVIEW_VERIFY_STEPS
|
|
339
|
+
formLabel = "独立审查会话(thorough 颗粒度)"
|
|
340
|
+
}
|
|
341
|
+
} else if ((state.mode ?? "full") === "simple") {
|
|
342
|
+
valid = SIMPLE_REVIEW_STEPS
|
|
343
|
+
formLabel = "simple 模式"
|
|
344
|
+
} else {
|
|
345
|
+
valid = REVIEW_VERIFY_STEPS
|
|
346
|
+
formLabel = "full 模式"
|
|
347
|
+
}
|
|
294
348
|
for (const stepId of recovery.reset_steps) {
|
|
295
|
-
if (!
|
|
349
|
+
if (!valid.includes(stepId)) {
|
|
296
350
|
throw new Error(
|
|
297
|
-
`reset_steps 中的 step "${stepId}"
|
|
351
|
+
`reset_steps 中的 step "${stepId}" 不属于当前会话形态(${formLabel})的审查 step,` +
|
|
298
352
|
`合法值:${valid.join("、")}。传入值:"${stepId}"。`
|
|
299
353
|
)
|
|
300
354
|
}
|
|
@@ -320,6 +374,235 @@ function otherTaskGroupsSettled(state: OrchestrateState, targetGroupId: string):
|
|
|
320
374
|
})
|
|
321
375
|
}
|
|
322
376
|
|
|
377
|
+
// ─── 独立审查会话(kind=review,不绑定 OpenSpec change)───
|
|
378
|
+
|
|
379
|
+
/** 分支名等非法字符归一为 -(会话 id 推导用,保持确定性)。 */
|
|
380
|
+
function normalizeSessionToken(s: string): string {
|
|
381
|
+
return s.replace(/[^A-Za-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "") || "head"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 独立审查会话 id 确定性推导:pr → review-pr-<base>-<head>;full → review-full-<当前分支>-<yyyymmdd>。 */
|
|
385
|
+
function deriveReviewSessionId(scope: ReviewScope, currentBranch: string): string {
|
|
386
|
+
if (scope.scopeType === "pr") {
|
|
387
|
+
return `review-pr-${normalizeSessionToken(scope.baseRef ?? "")}-${normalizeSessionToken(scope.headRef ?? "")}`
|
|
388
|
+
}
|
|
389
|
+
const day = new Date().toISOString().slice(0, 10).replace(/-/g, "")
|
|
390
|
+
return `review-full-${normalizeSessionToken(currentBranch)}-${day}`
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 审查范围等价判定(幂等复用判定:scopeType/推导后的 baseRef/headRef/granularity/fix 全等)。 */
|
|
394
|
+
function reviewScopeEquals(a: ReviewScope, b: ReviewScope): boolean {
|
|
395
|
+
return (
|
|
396
|
+
a.scopeType === b.scopeType && a.baseRef === b.baseRef && a.headRef === b.headRef &&
|
|
397
|
+
a.granularity === b.granularity && a.fix === b.fix
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** 校验独立审查入口的分支必须为存在的本地分支(收尾合并以 update-ref 推进本地分支引用,
|
|
402
|
+
* origin/* 等远端 ref 会静默创建错误的本地分支,故显式拒绝)。 */
|
|
403
|
+
async function assertLocalReviewBranch(worktree: string, branch: string, role: string): Promise<void> {
|
|
404
|
+
if (typeof branch !== "string" || branch.trim() === "" || /\s/.test(branch)) {
|
|
405
|
+
throw new Error(`独立审查会话的 ${role} 分支名不合法:"${String(branch)}"。`)
|
|
406
|
+
}
|
|
407
|
+
if (branch.startsWith("origin/") || branch.startsWith("refs/remotes/")) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`独立审查会话的 ${role} 分支 "${branch}" 是远端引用:收尾合并只推进本地分支引用,远端引用会静默创建错误的本地分支。` +
|
|
410
|
+
`请改用对应的本地分支名(如需审查远端内容,先在本地建分支跟踪后再传入)。`
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
if (!(await isLocalBranch(worktree, branch))) {
|
|
414
|
+
const branches = await listLocalBranches(worktree)
|
|
415
|
+
throw new Error(
|
|
416
|
+
`独立审查会话的 ${role} 分支 "${branch}" 不是本地分支。当前本地分支:${branches.join("、") || "(无)"}。`
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** 解析并校验 review_scope 参数(枚举值域 + 分支本地性 + base 推导),返回规范化 ReviewScope 与当前分支。 */
|
|
422
|
+
async function resolveReviewScope(
|
|
423
|
+
worktree: string,
|
|
424
|
+
raw: NonNullable<InitParams["review_scope"]>,
|
|
425
|
+
): Promise<{ scope: ReviewScope; currentBranch: string }> {
|
|
426
|
+
const currentBranch = await getCurrentBranch(worktree)
|
|
427
|
+
if (raw.scope_type !== "pr" && raw.scope_type !== "full") {
|
|
428
|
+
throw new Error(`review_scope.scope_type 不合法,合法值:pr、full。传入值:"${String(raw.scope_type)}"。`)
|
|
429
|
+
}
|
|
430
|
+
if (raw.granularity !== "simple" && raw.granularity !== "thorough") {
|
|
431
|
+
throw new Error(`review_scope.granularity 不合法,合法值:simple、thorough。传入值:"${String(raw.granularity)}"。`)
|
|
432
|
+
}
|
|
433
|
+
if (raw.fix !== "none" && raw.fix !== "fix") {
|
|
434
|
+
throw new Error(`review_scope.fix 不合法,合法值:none、fix。传入值:"${String(raw.fix)}"。`)
|
|
435
|
+
}
|
|
436
|
+
if (raw.scope_type === "full") {
|
|
437
|
+
if (raw.base_ref || raw.head_ref) {
|
|
438
|
+
throw new Error("review_scope.scope_type=full(全量审查)不接受 base_ref/head_ref——全量形态不界定分支区间。")
|
|
439
|
+
}
|
|
440
|
+
return { scope: { scopeType: "full", granularity: raw.granularity, fix: raw.fix }, currentBranch }
|
|
441
|
+
}
|
|
442
|
+
// pr 形态:head 必传;base 缺省推导(优先 main、其次 master,都不存在报错列出本地分支)
|
|
443
|
+
const headRef = raw.head_ref
|
|
444
|
+
if (!headRef) throw new Error("review_scope.scope_type=pr 必须提供 head_ref(审查目标分支,须为本地分支)。")
|
|
445
|
+
let baseRef = raw.base_ref
|
|
446
|
+
if (!baseRef) {
|
|
447
|
+
if (await isLocalBranch(worktree, "main")) baseRef = "main"
|
|
448
|
+
else if (await isLocalBranch(worktree, "master")) baseRef = "master"
|
|
449
|
+
else {
|
|
450
|
+
const branches = await listLocalBranches(worktree)
|
|
451
|
+
throw new Error(
|
|
452
|
+
`review_scope 未传 base_ref 且无法自动推导(main 与 master 均不是本地分支)。请显式传入 base_ref。当前本地分支:${branches.join("、") || "(无)"}。`
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
await assertLocalReviewBranch(worktree, baseRef, "base_ref")
|
|
457
|
+
await assertLocalReviewBranch(worktree, headRef, "head_ref")
|
|
458
|
+
return { scope: { scopeType: "pr", baseRef, headRef, granularity: raw.granularity, fix: raw.fix }, currentBranch }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 构造独立审查会话的虚拟组 WorkItem(无 task children,初始即落 review 阶段首个审查 step)。 */
|
|
462
|
+
function createReviewWorkItem(scope: ReviewScope, sessionId: string): WorkItem {
|
|
463
|
+
const item = createInitialWorkItem({
|
|
464
|
+
id: `task:${REVIEW_TASK_GROUP_ID}`,
|
|
465
|
+
source: "review",
|
|
466
|
+
externalId: REVIEW_TASK_GROUP_ID,
|
|
467
|
+
type: "task",
|
|
468
|
+
title: "独立代码审查",
|
|
469
|
+
description: "独立代码审查(不绑定 OpenSpec change)",
|
|
470
|
+
labels: ["standalone-review"],
|
|
471
|
+
})
|
|
472
|
+
item.phase = "review"
|
|
473
|
+
item.currentStep = scope.granularity === "thorough" ? "verify_tool" : "quality_review"
|
|
474
|
+
item.metadata["name"] = "独立代码审查"
|
|
475
|
+
item.metadata["task_count"] = 0
|
|
476
|
+
item.metadata["source"] = "review"
|
|
477
|
+
item.metadata["review_session_id"] = sessionId
|
|
478
|
+
// 引擎无 state 访问:item.metadata.review_fix_policy 为引擎消费的单一事实源(fail→done 短路依据),
|
|
479
|
+
// 由 init 从 state.reviewScope.fix 写入,二者恒一致(state 为配置事实源,metadata 为引擎读侧投影)。
|
|
480
|
+
item.metadata[REVIEW_FIX_POLICY_KEY] = scope.fix
|
|
481
|
+
return item
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 独立审查会话 init(review_scope 入口):
|
|
486
|
+
* - 同参数重复 init 幂等(复用既有会话);已有 review 会话传不同参数报错防误覆盖;
|
|
487
|
+
* - 会话 id 确定性推导并在返回体回传,后续所有工具以 change_id 形式传入该会话 id;
|
|
488
|
+
* - 不走 tasks.md 解析 / mode 校验 / mode 字段写入;recovery 仅接受 phase=review。
|
|
489
|
+
*/
|
|
490
|
+
async function initReviewSession(params: InitParams, ctx: ToolContext): Promise<string> {
|
|
491
|
+
const args = params
|
|
492
|
+
if (args.change_id || args.task_group_id || args.base_branch || args.mode) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
"review_scope 与 change_id/task_group_id 是两种互斥入口(同传报错、都不传报错,二选一);" +
|
|
495
|
+
"base_branch 与 mode 仅 change 会话入口有效,独立审查会话不使用。"
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
if (args.recovery) {
|
|
499
|
+
if (args.recovery.phase !== "review") {
|
|
500
|
+
throw new Error(`独立审查会话的 recovery 仅支持 phase="review"(无分析阶段),传入值:"${args.recovery.phase}"。`)
|
|
501
|
+
}
|
|
502
|
+
if (args.recovery.review_layer) {
|
|
503
|
+
throw new Error("独立审查会话不支持 review_layer(按颗粒度整体审查),如需重置指定层请用 reset_steps。")
|
|
504
|
+
}
|
|
505
|
+
if (args.recovery.reopenIssues) {
|
|
506
|
+
throw new Error("独立审查会话不支持 reopenIssues。")
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const { scope, currentBranch } = await resolveReviewScope(ctx.worktree, args.review_scope!)
|
|
510
|
+
const sessionId = deriveReviewSessionId(scope, currentBranch)
|
|
511
|
+
let state = await readStateByChangeId(ctx.worktree, sessionId)
|
|
512
|
+
if (state) {
|
|
513
|
+
if (state.kind !== "review" || !state.reviewScope) {
|
|
514
|
+
throw new Error(`会话 id "${sessionId}" 已被非独立审查会话占用,请核对后重试(防止误覆盖既有进度)。`)
|
|
515
|
+
}
|
|
516
|
+
if (!reviewScopeEquals(state.reviewScope, scope)) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`独立审查会话 "${sessionId}" 已存在且审查范围不同(已固化:${JSON.stringify(state.reviewScope)},传入:${JSON.stringify(scope)})。` +
|
|
519
|
+
`会话 id 由审查范围推导,重复 init 仅支持同参数幂等复用;需不同范围请调整参数(id 随之变化)。`
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
if (args.recovery) {
|
|
523
|
+
const item = state.workItems.find((w) => w.id === `task:${REVIEW_TASK_GROUP_ID}`)
|
|
524
|
+
if (!item) throw new Error(`工作项 "task:${REVIEW_TASK_GROUP_ID}" 缺失,会话状态异常,请核对 state 文件。`)
|
|
525
|
+
assertValidResetStepValues(args.recovery, state)
|
|
526
|
+
applyRecoveryState(item, args.recovery, [], state)
|
|
527
|
+
await writeState(ctx.worktree, state)
|
|
528
|
+
return `独立审查会话 "${sessionId}" 已恢复到 review 阶段(当前 step:${item.currentStep ?? "(已收口)"})。后续工具以 change_id="${sessionId}" 传入。`
|
|
529
|
+
}
|
|
530
|
+
return [
|
|
531
|
+
"独立审查会话已初始化(复用既有会话)。",
|
|
532
|
+
"",
|
|
533
|
+
`- **会话 ID**: \`${sessionId}\``,
|
|
534
|
+
`- **说明**: 后续所有 opx_* 工具以 \`change_id="${sessionId}"\` 传入。`,
|
|
535
|
+
].join("\n")
|
|
536
|
+
}
|
|
537
|
+
if (args.recovery) {
|
|
538
|
+
throw new Error(`独立审查会话 "${sessionId}" 不存在,无法按 recovery 恢复。请先不带 recovery 初始化。`)
|
|
539
|
+
}
|
|
540
|
+
// baseBranch 双语义:worktree 分支 fork 源与快进合并源(pr=head_ref、full=当前分支),
|
|
541
|
+
// 同时是 fix 模式收尾合回目标(merge_target);fix=none 收尾不合并不使用。
|
|
542
|
+
const baseBranch = scope.scopeType === "pr" ? scope.headRef! : currentBranch
|
|
543
|
+
state = {
|
|
544
|
+
changeId: sessionId,
|
|
545
|
+
isolationNamespace: generateIsolationNamespace(sessionId),
|
|
546
|
+
taskGroupId: REVIEW_TASK_GROUP_ID,
|
|
547
|
+
baseBranch,
|
|
548
|
+
workItems: [createReviewWorkItem(scope, sessionId)],
|
|
549
|
+
createdAt: new Date().toISOString(),
|
|
550
|
+
updatedAt: new Date().toISOString(),
|
|
551
|
+
kind: "review",
|
|
552
|
+
reviewScope: scope,
|
|
553
|
+
}
|
|
554
|
+
await writeState(ctx.worktree, state)
|
|
555
|
+
const granularityLabel = scope.granularity === "thorough" ? "三层审查(工具检查 → 回归验证 → 五维度质量审查)" : "单层合并审查"
|
|
556
|
+
const fixLabel = scope.fix === "fix" ? "审并修(失败回退修复后重审)" : "只审不修(issue 报告即交付物)"
|
|
557
|
+
const scopeLabel = scope.scopeType === "pr" ? `PR 区间 ${scope.baseRef}..${scope.headRef}` : "全量代码库"
|
|
558
|
+
return [
|
|
559
|
+
"独立审查会话已初始化。",
|
|
560
|
+
"",
|
|
561
|
+
`- **会话 ID**: \`${sessionId}\``,
|
|
562
|
+
`- **审查范围**: ${scopeLabel}`,
|
|
563
|
+
`- **颗粒度**: ${granularityLabel}`,
|
|
564
|
+
`- **修复策略**: ${fixLabel}`,
|
|
565
|
+
"",
|
|
566
|
+
`- **说明**: 后续所有 opx_* 工具以 \`change_id="${sessionId}"\` 传入(含 opx_orch_set_worktree / opx_status / opx_agent_submit / 收尾)。`,
|
|
567
|
+
].join("\n")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** 既有独立审查会话按 change_id 入口的恢复(task_group_id 可缺省,虚拟组 review)。 */
|
|
571
|
+
async function initReviewRecovery(params: InitParams, state: OrchestrateState, ctx: ToolContext): Promise<string> {
|
|
572
|
+
const args = params
|
|
573
|
+
if (args.mode || args.base_branch) {
|
|
574
|
+
throw new Error("独立审查会话不使用 mode / base_branch 参数(审查范围在首次 init 经 review_scope 固化)。")
|
|
575
|
+
}
|
|
576
|
+
if (args.task_group_id && args.task_group_id !== REVIEW_TASK_GROUP_ID) {
|
|
577
|
+
throw new Error(`独立审查会话的虚拟任务组固定为 "${REVIEW_TASK_GROUP_ID}",传入值:"${args.task_group_id}"。`)
|
|
578
|
+
}
|
|
579
|
+
if (args.recovery) {
|
|
580
|
+
if (args.recovery.phase === "task_analysis") {
|
|
581
|
+
throw new Error('独立审查会话无分析阶段,recovery.phase="task_analysis" 不适用(仅支持 review / dev_impl)。')
|
|
582
|
+
}
|
|
583
|
+
if (args.recovery.review_layer) {
|
|
584
|
+
throw new Error("独立审查会话不支持 review_layer(按颗粒度整体审查),如需重置指定层请用 reset_steps。")
|
|
585
|
+
}
|
|
586
|
+
if (args.recovery.reopenIssues) {
|
|
587
|
+
throw new Error("独立审查会话不支持 reopenIssues。")
|
|
588
|
+
}
|
|
589
|
+
assertValidResetStepValues(args.recovery, state)
|
|
590
|
+
}
|
|
591
|
+
const item = state.workItems.find((w) => w.id === `task:${REVIEW_TASK_GROUP_ID}`)
|
|
592
|
+
if (!item) throw new Error(`工作项 "task:${REVIEW_TASK_GROUP_ID}" 缺失,会话状态异常,请核对 state 文件。`)
|
|
593
|
+
if (args.recovery) {
|
|
594
|
+
applyRecoveryState(item, args.recovery, [], state)
|
|
595
|
+
await writeState(ctx.worktree, state)
|
|
596
|
+
return `独立审查会话 "${state.changeId}" 已恢复到 ${args.recovery.phase} 阶段(当前 step:${item.currentStep ?? "(已收口)"})。`
|
|
597
|
+
}
|
|
598
|
+
return [
|
|
599
|
+
"独立审查会话已就绪(复用既有会话)。",
|
|
600
|
+
"",
|
|
601
|
+
`- **会话 ID**: \`${state.changeId}\``,
|
|
602
|
+
`- **说明**: 后续所有 opx_* 工具以 \`change_id="${state.changeId}"\` 传入。`,
|
|
603
|
+
].join("\n")
|
|
604
|
+
}
|
|
605
|
+
|
|
323
606
|
export async function initExecute(params: InitParams, ctx: ToolContext): Promise<string> {
|
|
324
607
|
assertOrchestrator(ctx, "opx_orch_init")
|
|
325
608
|
|
|
@@ -335,14 +618,33 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
335
618
|
(args as any).recovery = parsed
|
|
336
619
|
}
|
|
337
620
|
assertValidRecovery(args.recovery)
|
|
621
|
+
|
|
622
|
+
// 入口分流:review_scope(独立审查会话)与 change_id/task_group_id 互斥——同传报错、都不传报错
|
|
623
|
+
if (args.review_scope !== undefined) {
|
|
624
|
+
return initReviewSession(args, ctx)
|
|
625
|
+
}
|
|
626
|
+
if (!args.change_id || !args.task_group_id) {
|
|
627
|
+
if (args.change_id) {
|
|
628
|
+
// 已存在的独立审查会话按 change_id(会话 id)恢复:虚拟组 review,task_group_id 可缺省
|
|
629
|
+
const existing = await readStateByChangeId(ctx.worktree, args.change_id)
|
|
630
|
+
if (existing?.kind === "review") {
|
|
631
|
+
return initReviewRecovery(args, existing, ctx)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
throw new Error(
|
|
635
|
+
"初始化需要两种入口之一:change 会话传 change_id + task_group_id;独立审查会话传 review_scope(会话 id 由审查范围推导并在返回体回传)。两种入口互斥,不可同传或都不传。"
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
const changeId = args.change_id
|
|
639
|
+
|
|
338
640
|
// mode 值域校验:值域外一律拒绝(无论 state 是否已存在),错误早于任何状态变更且不落盘
|
|
339
641
|
if (args.mode !== undefined && args.mode !== "full" && args.mode !== "simple") {
|
|
340
642
|
throw new Error(`mode 参数不合法,合法值:full、simple。传入值:"${String(args.mode)}"。`)
|
|
341
643
|
}
|
|
342
644
|
|
|
343
|
-
const parsedGroups = await parseAllTaskGroupsFromMd(ctx.worktree,
|
|
645
|
+
const parsedGroups = await parseAllTaskGroupsFromMd(ctx.worktree, changeId)
|
|
344
646
|
if (parsedGroups.length === 0) {
|
|
345
|
-
throw new Error(`无法从 tasks.md 解析出任务组,请检查文件 openspec/changes/${
|
|
647
|
+
throw new Error(`无法从 tasks.md 解析出任务组,请检查文件 openspec/changes/${changeId}/tasks.md。`)
|
|
346
648
|
}
|
|
347
649
|
const targetGroup = parsedGroups.find((g) => g.id === args.task_group_id)
|
|
348
650
|
if (!targetGroup) {
|
|
@@ -355,7 +657,7 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
355
657
|
// 逐任务组解析 tasks.md 子任务(构造各 task WorkItem 的 task children 用)
|
|
356
658
|
const tasksByGroup = new Map<string, ParsedTask[]>()
|
|
357
659
|
for (const g of parsedGroups) {
|
|
358
|
-
tasksByGroup.set(g.id, await parseTasksMdForGroup(ctx.worktree,
|
|
660
|
+
tasksByGroup.set(g.id, await parseTasksMdForGroup(ctx.worktree, changeId, g.id))
|
|
359
661
|
}
|
|
360
662
|
|
|
361
663
|
// base_branch 是 ref 而非严格 branch:只做非空 + 无空白字符等基本检查(完整分支名校验由 git check-ref-format 承担)
|
|
@@ -365,7 +667,7 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
365
667
|
}
|
|
366
668
|
}
|
|
367
669
|
const baseBranch = args.base_branch || await getCurrentBranch(ctx.worktree)
|
|
368
|
-
let state = await readStateByChangeId(ctx.worktree,
|
|
670
|
+
let state = await readStateByChangeId(ctx.worktree, changeId)
|
|
369
671
|
const wasCurrentGroup = state?.taskGroupId === args.task_group_id
|
|
370
672
|
|
|
371
673
|
// state 已存在时的 mode 变更窗口校验/更新(位于 for 循环前——循环内非活跃组新建 item 与
|
|
@@ -395,8 +697,8 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
395
697
|
|
|
396
698
|
if (!state) {
|
|
397
699
|
state = {
|
|
398
|
-
changeId
|
|
399
|
-
isolationNamespace: generateIsolationNamespace(
|
|
700
|
+
changeId,
|
|
701
|
+
isolationNamespace: generateIsolationNamespace(changeId),
|
|
400
702
|
taskGroupId: args.task_group_id,
|
|
401
703
|
baseBranch,
|
|
402
704
|
workItems: [],
|
|
@@ -413,7 +715,7 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
413
715
|
|
|
414
716
|
// reset_steps 值域的模式感知校验:位于 state 读取/固化之后(有效模式 = state.mode ?? "full",
|
|
415
717
|
// 与 applyRecoveryState 消费的生效模式一致)、一切状态变更与落盘之前,跨模式值报错不落盘
|
|
416
|
-
assertValidResetStepValues(args.recovery, state
|
|
718
|
+
assertValidResetStepValues(args.recovery, state)
|
|
417
719
|
|
|
418
720
|
// 按 tasks.md 构造全部任务组的 task WorkItem(单轨:workItems 为唯一事实源)
|
|
419
721
|
for (const group of parsedGroups) {
|
|
@@ -506,7 +808,7 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
506
808
|
item.metadata["base_ref"] = null
|
|
507
809
|
}
|
|
508
810
|
|
|
509
|
-
applyRecoveryState(item, args.recovery, groupTasks, state
|
|
811
|
+
applyRecoveryState(item, args.recovery, groupTasks, state)
|
|
510
812
|
refreshMeta(item)
|
|
511
813
|
if (!existing) state.workItems.push(item)
|
|
512
814
|
}
|
|
@@ -525,6 +827,23 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
525
827
|
return parts.join("")
|
|
526
828
|
}
|
|
527
829
|
|
|
830
|
+
/** 独立审查会话的 worktree 引用绑定(双基准拆分):
|
|
831
|
+
* - merge_target(合回目标)独立存 metadata:fix 模式收尾合回(pr=head_ref、full=当前分支,即 baseBranch);fix=none 收尾销毁不合并;
|
|
832
|
+
* - base_ref(审查锚点)仅 pr 形态设置 = merge-base(base_ref, head_ref);full 形态不设(全量语义,视图渲染「全量代码库」锚点)。 */
|
|
833
|
+
async function bindReviewWorktreeRefs(item: WorkItem, worktreePath: string, branch: string, state: OrchestrateState): Promise<void> {
|
|
834
|
+
item.metadata["worktree_path"] = worktreePath
|
|
835
|
+
item.metadata["branch_name"] = branch
|
|
836
|
+
item.metadata["merge_target"] = state.baseBranch
|
|
837
|
+
if (state.reviewScope?.scopeType === "pr" && state.reviewScope.baseRef) {
|
|
838
|
+
const baseRef = await getMergeBase(worktreePath, state.reviewScope.baseRef)
|
|
839
|
+
if (baseRef) {
|
|
840
|
+
item.metadata["base_ref"] = baseRef
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
delete item.metadata["base_ref"]
|
|
845
|
+
}
|
|
846
|
+
|
|
528
847
|
async function bindWorktreeRefs(
|
|
529
848
|
item: WorkItem,
|
|
530
849
|
worktreePath: string,
|
|
@@ -562,12 +881,17 @@ export async function setWorktreeExecute(params: SetWorktreeParams, ctx: ToolCon
|
|
|
562
881
|
throw new Error(`分支名 "${rawBranch}" 不合法,请修正后重试。`)
|
|
563
882
|
}
|
|
564
883
|
}
|
|
565
|
-
|
|
884
|
+
// 独立审查会话:分支命名 review/<sessionId>、worktree 布局 .worktree/<sessionId>/review
|
|
885
|
+
// (discoverDiskWorktrees 按 review/ 前缀识别为可恢复磁盘痕迹)
|
|
886
|
+
const isReview = state.kind === "review"
|
|
887
|
+
const branch = rawBranch || (isReview ? `review/${state.changeId}` : `task-group/${state.changeId}/${state.taskGroupId}`)
|
|
566
888
|
let wtPath: string
|
|
567
889
|
if (params.worktree_path) {
|
|
568
890
|
wtPath = assertPathWithin(repoRoot, params.worktree_path, "worktree_path")
|
|
569
891
|
} else {
|
|
570
|
-
wtPath =
|
|
892
|
+
wtPath = isReview
|
|
893
|
+
? path.join(repoRoot, ".worktree", state.changeId, "review")
|
|
894
|
+
: path.join(repoRoot, ".worktree", state.changeId, `task-group-${state.taskGroupId}`)
|
|
571
895
|
}
|
|
572
896
|
|
|
573
897
|
const changeStatus = await runGit(repoRoot, ["status", "--porcelain", `openspec/changes/${state.changeId}/`])
|
|
@@ -590,7 +914,8 @@ export async function setWorktreeExecute(params: SetWorktreeParams, ctx: ToolCon
|
|
|
590
914
|
const baseHead = await runGit(repoRoot, ["rev-parse", state.baseBranch])
|
|
591
915
|
const mergeResult = await runGitChecked(existingPath, ["merge", "--ff-only", baseHead])
|
|
592
916
|
if (mergeResult.success) {
|
|
593
|
-
await
|
|
917
|
+
if (isReview) await bindReviewWorktreeRefs(item, existingPath, branch, state)
|
|
918
|
+
else await bindWorktreeRefs(item, existingPath, branch, state.baseBranch)
|
|
594
919
|
reused = true
|
|
595
920
|
} else {
|
|
596
921
|
const clean = await isWorktreeClean(existingPath)
|
|
@@ -605,7 +930,8 @@ export async function setWorktreeExecute(params: SetWorktreeParams, ctx: ToolCon
|
|
|
605
930
|
10
|
|
606
931
|
)
|
|
607
932
|
if (localCommitCount > 0 || Number.isNaN(localCommitCount)) {
|
|
608
|
-
await
|
|
933
|
+
if (isReview) await bindReviewWorktreeRefs(item, existingPath, branch, state)
|
|
934
|
+
else await bindWorktreeRefs(item, existingPath, branch, state.baseBranch)
|
|
609
935
|
reused = true
|
|
610
936
|
} else {
|
|
611
937
|
// 复用失败的清理走共享清理函数:目录侧(git remove + fs 兜底 + prune)与分支删除统一语义
|
|
@@ -621,9 +947,18 @@ export async function setWorktreeExecute(params: SetWorktreeParams, ctx: ToolCon
|
|
|
621
947
|
}
|
|
622
948
|
|
|
623
949
|
if (!reused) {
|
|
950
|
+
// 建分支 fork 源:pr 形态为 head_ref、full 形态为当前分支(两者均为 state.baseBranch)
|
|
624
951
|
const forkBranch = state.baseBranch
|
|
625
952
|
await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch])
|
|
626
|
-
|
|
953
|
+
if (isReview) {
|
|
954
|
+
await bindReviewWorktreeRefs(item, wtPath, branch, state)
|
|
955
|
+
// pr 形态审查锚点(merge-base)必须可得,缺失即建引用失败(对齐 change 路径的 requireBaseRef)
|
|
956
|
+
if (state.reviewScope?.scopeType === "pr" && !item.metadata["base_ref"]) {
|
|
957
|
+
throw new Error(`worktree 创建成功但无法获取 ${state.reviewScope.baseRef} 与 ${state.baseBranch} 的 merge-base:${wtPath}`)
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
await bindWorktreeRefs(item, wtPath, branch, forkBranch, { requireBaseRef: true })
|
|
961
|
+
}
|
|
627
962
|
}
|
|
628
963
|
|
|
629
964
|
await writeState(ctx.worktree, state)
|
|
@@ -689,7 +1024,9 @@ export async function statusExecute(params: StatusParams, ctx: ToolContext): Pro
|
|
|
689
1024
|
const checkpoint =
|
|
690
1025
|
typeof item.metadata["_tool_review_checkpoint"] === "string" ? item.metadata["_tool_review_checkpoint"] : undefined
|
|
691
1026
|
const baseRef = typeof item.metadata["base_ref"] === "string" ? item.metadata["base_ref"] : undefined
|
|
692
|
-
|
|
1027
|
+
// 独立审查 full 形态:审查锚点为全量代码库,返回全量哨兵(视图渲染「全量代码库」而非「未检出变更」)
|
|
1028
|
+
const scopeFull = state.kind === "review" && state.reviewScope?.scopeType === "full"
|
|
1029
|
+
toolChanges = await detectChanges(wtPath, { checkpoint, baseRef, scopeFull })
|
|
693
1030
|
}
|
|
694
1031
|
}
|
|
695
1032
|
// 统计本 change 命中项目级跨 change 豁免清单的存量问题数(工具层降级时写入 exempted_hit 标记)
|
|
@@ -715,6 +1052,37 @@ async function sweepEmptyWorktreeParents(repoRoot: string, removedDir: string, c
|
|
|
715
1052
|
try { await rmdir(worktreeLevelDir) } catch {}
|
|
716
1053
|
}
|
|
717
1054
|
|
|
1055
|
+
/** 独立审查会话收尾的审查结果摘要:issue 按严重级别与维度归并计数(markdown)。 */
|
|
1056
|
+
function renderReviewIssueSummary(item: WorkItem): string {
|
|
1057
|
+
const issues = item.children.filter((c) => c.type === "issue")
|
|
1058
|
+
const lines = ["## 审查结果摘要", ""]
|
|
1059
|
+
if (issues.length === 0) {
|
|
1060
|
+
lines.push("- 未报出任何 issue。", "")
|
|
1061
|
+
return lines.join("\n")
|
|
1062
|
+
}
|
|
1063
|
+
const bySeverity = new Map<string, number>()
|
|
1064
|
+
const byDimension = new Map<string, number>()
|
|
1065
|
+
for (const c of issues) {
|
|
1066
|
+
const sev = c.severity ?? "Info"
|
|
1067
|
+
const dim = String(c.metadata["dimension"] ?? "style")
|
|
1068
|
+
bySeverity.set(sev, (bySeverity.get(sev) ?? 0) + 1)
|
|
1069
|
+
byDimension.set(dim, (byDimension.get(dim) ?? 0) + 1)
|
|
1070
|
+
}
|
|
1071
|
+
lines.push(`- **issue 总数**: ${issues.length}`)
|
|
1072
|
+
lines.push(`- **按严重级别**: ${[...bySeverity.entries()].map(([k, v]) => `${k}=${v}`).join("、")}`)
|
|
1073
|
+
lines.push(`- **按维度**: ${[...byDimension.entries()].map(([k, v]) => `${k}=${v}`).join("、")}`)
|
|
1074
|
+
lines.push("")
|
|
1075
|
+
lines.push("| severity | dimension | 文件位置 | 描述 |", "|----------|-----------|----------|------|")
|
|
1076
|
+
for (const c of issues) {
|
|
1077
|
+
const file = typeof c.metadata["file"] === "string" ? c.metadata["file"] : ""
|
|
1078
|
+
const line = typeof c.metadata["line"] === "number" ? c.metadata["line"] : 0
|
|
1079
|
+
const loc = file ? `${file}${line > 0 ? `:${line}` : ""}` : "(无)"
|
|
1080
|
+
lines.push(`| ${c.severity ?? "Info"} | ${String(c.metadata["dimension"] ?? "style")} | \`${loc}\` | ${c.description.replace(/\|/g, "\\|")} |`)
|
|
1081
|
+
}
|
|
1082
|
+
lines.push("")
|
|
1083
|
+
return lines.join("\n")
|
|
1084
|
+
}
|
|
1085
|
+
|
|
718
1086
|
export async function completeTaskGroupExecute(params: { change_id: string }, ctx: ToolContext): Promise<string> {
|
|
719
1087
|
assertOrchestrator(ctx, "opx_orch_complete_task_group")
|
|
720
1088
|
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
@@ -738,8 +1106,12 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
|
|
|
738
1106
|
if (!clean) throw new Error(`worktree "${worktreePath}" 存在未 commit 内容,请先 commit 再完成任务组。`)
|
|
739
1107
|
}
|
|
740
1108
|
|
|
1109
|
+
// 独立审查会话双维度放宽(其余门禁不动):
|
|
1110
|
+
// - fix=none(只审):issue 报告即交付物,openIssues 门禁放宽(issue 停留 todo 态为合法收口形态);
|
|
1111
|
+
// - 审并修(fix,两种颗粒度):门禁保留(阻塞 issue 未终态拒绝收尾)。
|
|
1112
|
+
const isReviewNone = state.kind === "review" && state.reviewScope?.fix === "none"
|
|
741
1113
|
const openIssues = item.children.filter((c) => isBlockingSeverity(c.severity) && !isTerminalPhase(c.phase))
|
|
742
|
-
if (openIssues.length > 0) {
|
|
1114
|
+
if (!isReviewNone && openIssues.length > 0) {
|
|
743
1115
|
throw new Error(`存在 ${openIssues.length} 个 Low 及以上的未解决 issue 未处理,请先修复或申请豁免。`)
|
|
744
1116
|
}
|
|
745
1117
|
|
|
@@ -767,9 +1139,14 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
|
|
|
767
1139
|
}
|
|
768
1140
|
}
|
|
769
1141
|
|
|
1142
|
+
// 合并按修复策略:fix 模式合回 merge_target(pr=head_ref、full=当前分支,即 baseBranch);
|
|
1143
|
+
// fix=none 不合并(issue 报告即交付物),直接销毁 worktree 与分支。
|
|
770
1144
|
const mergeTarget = state.baseBranch
|
|
771
|
-
if (branchName) {
|
|
1145
|
+
if (branchName && !isReviewNone) {
|
|
772
1146
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget)
|
|
1147
|
+
if (mergeResult.blockedMessage) {
|
|
1148
|
+
return [`- **status**: blocked`, mergeResult.blockedMessage].join("\n")
|
|
1149
|
+
}
|
|
773
1150
|
if (!mergeResult.success) {
|
|
774
1151
|
return [
|
|
775
1152
|
`- **status**: blocked`,
|
|
@@ -810,8 +1187,16 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
|
|
|
810
1187
|
}
|
|
811
1188
|
item.metadata["completed_at"] = new Date().toISOString()
|
|
812
1189
|
await writeState(ctx.worktree, state)
|
|
813
|
-
const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`
|
|
814
1190
|
const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean)
|
|
1191
|
+
if (state.kind === "review") {
|
|
1192
|
+
// 独立审查会话收尾返回审查结果摘要(issue 计数按严重级别/维度归并,只审模式的交付物形态)
|
|
1193
|
+
const summary = renderReviewIssueSummary(item)
|
|
1194
|
+
const doneMessage = isReviewNone
|
|
1195
|
+
? `独立审查会话已完成(只审模式,未合并,worktree 与分支已销毁)。`
|
|
1196
|
+
: `独立审查会话已完成并合并到 "${mergeTarget}"。`
|
|
1197
|
+
return notes.length > 0 ? `${doneMessage}\n${notes.join("\n")}\n${summary}` : `${doneMessage}\n${summary}`
|
|
1198
|
+
}
|
|
1199
|
+
const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`
|
|
815
1200
|
return notes.length > 0 ? `${doneMessage}\n${notes.join("\n")}` : doneMessage
|
|
816
1201
|
}
|
|
817
1202
|
|