@ifrankwang/openspec-agents 0.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/assets/agents/openspec-architect.md +61 -0
- package/assets/agents/openspec-developer.md +21 -0
- package/assets/agents/openspec-main.md +35 -0
- package/assets/agents/openspec-reviewer-architecture.md +60 -0
- package/assets/agents/openspec-reviewer-maintainability.md +59 -0
- package/assets/agents/openspec-reviewer-performance.md +49 -0
- package/assets/agents/openspec-reviewer-security.md +51 -0
- package/assets/agents/openspec-reviewer-style.md +50 -0
- package/assets/agents/openspec-reviewer-task.md +40 -0
- package/assets/agents/openspec-reviewer-tool.md +30 -0
- package/assets/dashboard/index.html +295 -0
- package/assets/skills/api-conventions/SKILL.md +60 -0
- package/assets/skills/api-test/SKILL.md +175 -0
- package/assets/skills/architecture-evolution/SKILL.md +60 -0
- package/assets/skills/db-conventions/SKILL.md +62 -0
- package/assets/skills/ddd-architecture/SKILL.md +261 -0
- package/assets/skills/environment-cleanup/SKILL.md +44 -0
- package/assets/skills/java-api-conventions/SKILL.md +30 -0
- package/assets/skills/java-code-maintainability/SKILL.md +62 -0
- package/assets/skills/java-code-performance/SKILL.md +49 -0
- package/assets/skills/java-code-style/SKILL.md +54 -0
- package/assets/skills/java-db-conventions/SKILL.md +44 -0
- package/assets/skills/java-ddd-architecture/SKILL.md +533 -0
- package/assets/skills/java-dev-practices/SKILL.md +110 -0
- package/assets/skills/java-quality-gate/SKILL.md +455 -0
- package/assets/skills/java-quality-gate/reference/pmd-rules.md +16 -0
- package/assets/skills/java-quality-tool-improve/SKILL.md +142 -0
- package/assets/skills/java-security/SKILL.md +59 -0
- package/assets/skills/java-technical-debt/SKILL.md +36 -0
- package/assets/skills/orchestrator/SKILL.md +65 -0
- package/assets/skills/quality-gate/SKILL.md +84 -0
- package/assets/skills/security-baseline/SKILL.md +101 -0
- package/assets/skills/technical-debt/SKILL.md +78 -0
- package/assets/workflows/task.yaml +177 -0
- package/package.json +79 -0
- package/src/adapters/agent-md.ts +37 -0
- package/src/adapters/claude-code/index.ts +35 -0
- package/src/adapters/codex/index.ts +161 -0
- package/src/adapters/mcp-common/cli.ts +64 -0
- package/src/adapters/mcp-common/index.ts +137 -0
- package/src/adapters/mcp-common/json-schema.ts +53 -0
- package/src/adapters/mcp-common/server.ts +157 -0
- package/src/adapters/opencode/agents.ts +80 -0
- package/src/adapters/opencode/dashboard.ts +64 -0
- package/src/adapters/opencode/index.ts +34 -0
- package/src/adapters/opencode/json-schema.ts +60 -0
- package/src/adapters/opencode/skills.ts +19 -0
- package/src/adapters/opencode/tools.ts +78 -0
- package/src/adapters/plugin-common/index.ts +173 -0
- package/src/adapters/zcode/index.ts +32 -0
- package/src/core/constants.ts +46 -0
- package/src/core/dashboard.ts +101 -0
- package/src/core/derive.ts +146 -0
- package/src/core/exemptions.ts +168 -0
- package/src/core/git.ts +373 -0
- package/src/core/index.ts +76 -0
- package/src/core/namespace.ts +12 -0
- package/src/core/paths.ts +23 -0
- package/src/core/provider.ts +74 -0
- package/src/core/state.ts +357 -0
- package/src/core/task-children.ts +59 -0
- package/src/core/tasks-md.ts +89 -0
- package/src/core/tools/gate.ts +165 -0
- package/src/core/tools/lifecycle.ts +645 -0
- package/src/core/tools/schemas.ts +363 -0
- package/src/core/tools/submit.ts +863 -0
- package/src/core/tools/types.ts +129 -0
- package/src/core/types.ts +135 -0
- package/src/core/views.ts +195 -0
- package/src/core/workflow/collector.ts +150 -0
- package/src/core/workflow/engine.ts +576 -0
- package/src/core/workflow/index.ts +56 -0
- package/src/core/workflow/loader.ts +277 -0
- package/src/core/workflow/poller.ts +159 -0
- package/src/core/workflow/reset.ts +93 -0
- package/src/core/workflow/status.ts +1010 -0
- package/src/core/workflow/submit.ts +331 -0
- package/src/core/workflow/types.ts +100 -0
- package/src/core/workflow/writeback.ts +48 -0
- package/src/dashboard/page.ts +19 -0
- package/src/index.ts +2 -0
- package/src/skills/resolve.ts +95 -0
- package/src/skills/scan.ts +29 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import type { TaskItem, TaskStatus } from "../types.ts"
|
|
3
|
+
import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS } from "../types.ts"
|
|
4
|
+
import { agentToReviewLayer } from "../constants.ts"
|
|
5
|
+
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, type DetectChangesResult } from "../git.ts"
|
|
6
|
+
import { readStateByWorktree, readStateByChangeId, writeState, writeContextToWorktree } from "../state.ts"
|
|
7
|
+
import { generateIsolationNamespace } from "../namespace.ts"
|
|
8
|
+
import { readExemptions } from "../exemptions.ts"
|
|
9
|
+
import { parseAllTaskGroupsFromMd, parseTasksMdForGroup, extractRelevantSpecsFromTasks } from "../tasks-md.ts"
|
|
10
|
+
import type { ParsedTask } from "../tasks-md.ts"
|
|
11
|
+
import { assertOrchestrator, findTaskGroup } from "../derive.ts"
|
|
12
|
+
import { assertPathWithin } from "../paths.ts"
|
|
13
|
+
import { loadWorkflowFile, TASK_WORKFLOW_PATH, type LoadedWorkflow } from "../workflow/loader.ts"
|
|
14
|
+
import { createInitialWorkItem, isBlockingSeverity, isTerminalPhase, recommendForItem, resetInternalRetryCount, adjudicateStep, clearStepTags } from "../workflow/engine.ts"
|
|
15
|
+
import { renderWorkflowStatusView } from "../workflow/status.ts"
|
|
16
|
+
import { taskChildrenOf } from "../task-children.ts"
|
|
17
|
+
import type { WorkItem, WorkItemPhase } from "../workflow/types.ts"
|
|
18
|
+
import type { InitParams, SetWorktreeParams, UnattendedParams, ToolContext, StatusParams } from "./types.ts"
|
|
19
|
+
|
|
20
|
+
/** 由 tasks.md 解析结果构造 task child WorkItem(初始 todo;externalId 存 taskNumber,id 存数字索引)。 */
|
|
21
|
+
function taskChildFromParsed(p: ParsedTask, index: number): WorkItem {
|
|
22
|
+
const child = createInitialWorkItem({
|
|
23
|
+
id: String(index + 1),
|
|
24
|
+
source: "openspec",
|
|
25
|
+
externalId: p.taskNumber,
|
|
26
|
+
type: "task",
|
|
27
|
+
title: p.title,
|
|
28
|
+
description: p.title,
|
|
29
|
+
})
|
|
30
|
+
child.metadata["specTrace"] = p.specTrace
|
|
31
|
+
child.metadata["taskNumber"] = p.taskNumber
|
|
32
|
+
return child
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 按 tasks.md 同步 task children(整体重建):
|
|
37
|
+
* - 先自愈:现存 task children 重复 id 时合并去重——保留第一出现条目(含进度),丢弃重复条目,
|
|
38
|
+
* 不得因重复抛错(否则封死 recovery=task_analysis 等逃生路径);
|
|
39
|
+
* - 以 parsed 为基准整体重建 task 部分:forceOpen=false 时复用同 id 旧 child(保留 phase/tags/metadata
|
|
40
|
+
* 进度),forceOpen=true 或旧 child 不存在则新建,新建时按 opts.defaultStatus 赋初始 phase;
|
|
41
|
+
* - 移除 tasks.md 已删除的 task child,issue children 不受影响。
|
|
42
|
+
*/
|
|
43
|
+
function syncTaskChildren(item: WorkItem, parsed: ParsedTask[], opts: { forceOpen?: boolean; defaultStatus?: WorkItemPhase }): void {
|
|
44
|
+
// 自愈:现存 task children 重复 id 时合并去重(保留第一出现条目含进度),杜绝"保留旧 + 追加新"产生的重复。
|
|
45
|
+
const seen = new Set<string>()
|
|
46
|
+
const deduped: WorkItem[] = []
|
|
47
|
+
for (const c of item.children) {
|
|
48
|
+
if (c.type === "task") {
|
|
49
|
+
if (seen.has(c.id)) continue
|
|
50
|
+
seen.add(c.id)
|
|
51
|
+
}
|
|
52
|
+
deduped.push(c)
|
|
53
|
+
}
|
|
54
|
+
item.children = deduped
|
|
55
|
+
|
|
56
|
+
const prev = new Map(taskChildrenOf(item).map((c) => [c.id, c]))
|
|
57
|
+
const built = parsed.map((p, i) => {
|
|
58
|
+
const id = String(i + 1)
|
|
59
|
+
const old = prev.get(id)
|
|
60
|
+
if (old && !opts.forceOpen) {
|
|
61
|
+
// 复用旧 child 保留进度(phase/tags/metadata),按 parsed 刷新标题/编号等展示字段
|
|
62
|
+
old.title = p.title
|
|
63
|
+
old.description = p.title
|
|
64
|
+
old.externalId = p.taskNumber
|
|
65
|
+
old.metadata["specTrace"] = p.specTrace
|
|
66
|
+
old.metadata["taskNumber"] = p.taskNumber
|
|
67
|
+
return old
|
|
68
|
+
}
|
|
69
|
+
const child = taskChildFromParsed(p, i)
|
|
70
|
+
if (!opts.forceOpen && opts.defaultStatus) child.phase = opts.defaultStatus
|
|
71
|
+
return child
|
|
72
|
+
})
|
|
73
|
+
// 整体替换 task 部分:旧 task child 全部移除,以 parsed 为基准重建;issue children 不受影响。
|
|
74
|
+
item.children = [...item.children.filter((c) => c.type !== "task"), ...built]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** TaskStatus → task child phase 反查(旧 state metadata.tasks 迁移用)。 */
|
|
78
|
+
function taskStatusToPhase(status: TaskStatus): WorkItemPhase {
|
|
79
|
+
switch (status) {
|
|
80
|
+
case "submitted": return "review"
|
|
81
|
+
case "verified": return "done"
|
|
82
|
+
case "rejected": return "todo"
|
|
83
|
+
default: return "todo"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 旧 state 迁移:metadata.tasks(TaskItem[])→ task children。
|
|
89
|
+
* 无 task child 时按 status 反查 phase 重建并挂入 children;随后删除 metadata.tasks(已废弃)。
|
|
90
|
+
*/
|
|
91
|
+
function migrateLegacyTasks(item: WorkItem): void {
|
|
92
|
+
const raw = item.metadata["tasks"]
|
|
93
|
+
if (!Array.isArray(raw)) return
|
|
94
|
+
if (taskChildrenOf(item).length === 0) {
|
|
95
|
+
for (const t of raw as TaskItem[]) {
|
|
96
|
+
const child = createInitialWorkItem({
|
|
97
|
+
id: String(t.id),
|
|
98
|
+
source: "openspec",
|
|
99
|
+
externalId: t.taskNumber || undefined,
|
|
100
|
+
type: "task",
|
|
101
|
+
title: t.title,
|
|
102
|
+
description: t.title,
|
|
103
|
+
})
|
|
104
|
+
child.metadata["specTrace"] = t.specTrace
|
|
105
|
+
if (t.taskNumber) child.metadata["taskNumber"] = t.taskNumber
|
|
106
|
+
if (t.rejectReason) child.metadata["reject_reason"] = t.rejectReason
|
|
107
|
+
child.phase = taskStatusToPhase(t.status)
|
|
108
|
+
item.children.push(child)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
delete item.metadata["tasks"]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 扫描 review 阶段第一个未全 passed 的 step(按 workflow 声明顺序 verify_tool → verify_task → verify_quality)。
|
|
116
|
+
* 一个 step 全 passed 由 adjudicateStep 判定(该 step 所有 agent 的 tag 均为 passed);全部通过返回 null。
|
|
117
|
+
*/
|
|
118
|
+
function firstUnpassedReviewStep(item: WorkItem, workflow: LoadedWorkflow): string | null {
|
|
119
|
+
const review = workflow.phases.find((p) => p.name === "review")
|
|
120
|
+
if (!review) return null
|
|
121
|
+
for (const step of review.steps) {
|
|
122
|
+
if (adjudicateStep(item, step) !== "passed") return step.id
|
|
123
|
+
}
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 按 recovery 合成活跃组 WorkItem 的 phase/currentStep/tags/task children。
|
|
129
|
+
* - task_analysis:todo/analyze、tags 清空、task children 全 todo(全新开始)
|
|
130
|
+
* - dev_impl:in_progress/implement、analyze 已 passed、task children 保留既有进度(无则 todo)
|
|
131
|
+
* - review:review/verify_*(增量合并——已 passed 的审查标记保留、failed 重置为 pending,currentStep
|
|
132
|
+
* 前移到第一个未全 passed 的子层;review_layer 决定强制前置哪些子层)、analyze+implement 已 passed、
|
|
133
|
+
* task children 保留既有进度(无则 done——review 恢复时子任务应视为已验证,否则 G21 remaining 会让 implement 无法提交)
|
|
134
|
+
*/
|
|
135
|
+
function applyRecoveryState(
|
|
136
|
+
item: WorkItem,
|
|
137
|
+
recovery: InitParams["recovery"],
|
|
138
|
+
parsedTasks: ParsedTask[],
|
|
139
|
+
): void {
|
|
140
|
+
// 恢复重建为已知状态后清除残留推进阻塞原因,避免 orchestrator 视图展示过期信息
|
|
141
|
+
delete item.metadata["_advance_block_reason"]
|
|
142
|
+
// 清除内部重试计数:recovery 恢复后残留 _retryCount 会在下次回退时立即再次触发检查点(死锁)。
|
|
143
|
+
resetInternalRetryCount(item)
|
|
144
|
+
// 清除检查点标记残留:恢复重建为已知状态后 _checkpoint 已无意义(checkpoint 态属于中断中的 step)。
|
|
145
|
+
delete item.metadata["_checkpoint"]
|
|
146
|
+
const phase = recovery?.phase
|
|
147
|
+
if (!phase || phase === "task_analysis") {
|
|
148
|
+
item.phase = "todo"
|
|
149
|
+
item.currentStep = "analyze"
|
|
150
|
+
item.tags = {}
|
|
151
|
+
syncTaskChildren(item, parsedTasks, { forceOpen: true })
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (phase === "dev_impl") {
|
|
155
|
+
item.phase = "in_progress"
|
|
156
|
+
item.currentStep = "implement"
|
|
157
|
+
item.tags = { "analyze:openspec-architect": "passed" }
|
|
158
|
+
syncTaskChildren(item, parsedTasks, { defaultStatus: "todo" })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
item.phase = "review"
|
|
162
|
+
// 前置层保证:恢复进 review 时 analyze/implement 必然已通过
|
|
163
|
+
item.tags["analyze:openspec-architect"] = "passed"
|
|
164
|
+
item.tags["implement:openspec-developer"] = "passed"
|
|
165
|
+
// 增量合并 review 审查标记:值为 passed 的保留(已审查通过层无需重跑),failed 删除回到 pending
|
|
166
|
+
for (const key of Object.keys(item.tags)) {
|
|
167
|
+
if (key.startsWith("verify_tool:") || key.startsWith("verify_task:") || key.startsWith("verify_quality:")) {
|
|
168
|
+
if (item.tags[key] !== "passed") delete item.tags[key]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// reset_steps:把指定 verify step 的全部 tags 重置为 pending(删除 tag 键),使 currentStep 落在
|
|
172
|
+
// 第一个未全部通过的 verify step,可能早于被重置的 step——用于已 passed 但被本层遗漏复核/裁定
|
|
173
|
+
// 阻塞的 review step 强制重新审查(与 resetReviewTagsOnFix 删除 tag 的既有语义一致)。
|
|
174
|
+
for (const stepId of recovery?.reset_steps ?? []) {
|
|
175
|
+
clearStepTags(item, stepId)
|
|
176
|
+
}
|
|
177
|
+
// review_layer 强制前置:tool→task 时 verify_tool 强制 passed;quality 时 verify_tool+verify_task 强制 passed
|
|
178
|
+
if (recovery?.review_layer === "task" || recovery?.review_layer === "quality") {
|
|
179
|
+
item.tags["verify_tool:openspec-reviewer-tool"] = "passed"
|
|
180
|
+
}
|
|
181
|
+
if (recovery?.review_layer === "quality") {
|
|
182
|
+
item.tags["verify_task:openspec-reviewer-task"] = "passed"
|
|
183
|
+
}
|
|
184
|
+
// currentStep 前移:显式指向第一个未全 passed 的 review step(已全 passed 的子层跳过)
|
|
185
|
+
const workflow = loadWorkflowFile(TASK_WORKFLOW_PATH)
|
|
186
|
+
item.currentStep = firstUnpassedReviewStep(item, workflow)
|
|
187
|
+
// 全 passed 收口 done:三个 review step 全 passed 时,仅当 task children 全部终态才收口;
|
|
188
|
+
// 存在未终态 task child 则停在 verify_quality(recommendForItem 会返回 blocked 而非 terminal,安全)。
|
|
189
|
+
if (item.currentStep === null) {
|
|
190
|
+
const unfinishedTasks = item.children.filter((child) => child.type === "task" && !isTerminalPhase(child.phase))
|
|
191
|
+
if (unfinishedTasks.length === 0) {
|
|
192
|
+
item.phase = "done"
|
|
193
|
+
} else {
|
|
194
|
+
item.currentStep = "verify_quality"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
syncTaskChildren(item, parsedTasks, { defaultStatus: "done" })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* recovery 参数值域校验(入口显式拒绝,早于任何状态变更):
|
|
202
|
+
* - phase 必须为合法恢复阶段(task_analysis/dev_impl/review),缺失/非法即抛错并列出合法值;
|
|
203
|
+
* - review_layer 必须为合法子层(tool/task/quality),非法即抛错;
|
|
204
|
+
* - review_layer 仅当 phase=review 时允许存在,其余 phase 组合复用既有组合错误消息;
|
|
205
|
+
* - reset_steps 必须为合法 verify step(verify_tool/verify_task/verify_quality),非空数组,
|
|
206
|
+
* 仅当 phase=review 时允许存在,且与 review_layer 互斥(二者都操纵哪些 review step 通过)。
|
|
207
|
+
*/
|
|
208
|
+
function assertValidRecovery(recovery: InitParams["recovery"]): void {
|
|
209
|
+
if (recovery === undefined) return
|
|
210
|
+
if (!(BUILD_PHASE_TARGETS as readonly string[]).includes(recovery.phase)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`recovery.phase 不合法,合法值:${BUILD_PHASE_TARGETS.join("、")}。传入值:"${String(recovery.phase)}"。`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
if (recovery.review_layer !== undefined && !(REVIEW_LAYERS as readonly string[]).includes(recovery.review_layer)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`recovery.review_layer 不合法,合法值:${REVIEW_LAYERS.join("、")}。传入值:"${String(recovery.review_layer)}"。`
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
if (recovery.review_layer && recovery.phase !== "review") {
|
|
221
|
+
throw new Error(`review_layer 参数仅当 recovery.phase 为 review 时有效,当前 phase 为 "${recovery.phase}"。`)
|
|
222
|
+
}
|
|
223
|
+
if (recovery.reset_steps !== undefined) {
|
|
224
|
+
if (recovery.phase !== "review") {
|
|
225
|
+
throw new Error(`reset_steps 参数仅当 recovery.phase 为 review 时有效,当前 phase 为 "${recovery.phase}"。`)
|
|
226
|
+
}
|
|
227
|
+
if (recovery.review_layer) {
|
|
228
|
+
throw new Error("reset_steps 与 review_layer 互斥,不可同时使用。")
|
|
229
|
+
}
|
|
230
|
+
if (recovery.reset_steps.length === 0) {
|
|
231
|
+
throw new Error("reset_steps 不能为空数组,请至少指定一个 verify step。")
|
|
232
|
+
}
|
|
233
|
+
for (const stepId of recovery.reset_steps) {
|
|
234
|
+
if (!(REVIEW_VERIFY_STEPS as readonly string[]).includes(stepId)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`reset_steps 中的 step "${stepId}" 不合法,合法值:${REVIEW_VERIFY_STEPS.join("、")}。传入值:"${stepId}"。`
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function initExecute(params: InitParams, ctx: ToolContext): Promise<string> {
|
|
244
|
+
assertOrchestrator(ctx, "opx_orch_init")
|
|
245
|
+
|
|
246
|
+
const args = { ...params }
|
|
247
|
+
if (typeof (args as any).recovery === "string") {
|
|
248
|
+
let parsed: unknown
|
|
249
|
+
try { parsed = JSON.parse((args as any).recovery) } catch {
|
|
250
|
+
throw new Error(`recovery 参数解析失败:传入的字符串无法解析为对象。传入值:${(args as any).recovery}`)
|
|
251
|
+
}
|
|
252
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
253
|
+
throw new Error(`recovery 参数解析失败:传入的字符串解析结果不是对象。传入值:${(args as any).recovery}`)
|
|
254
|
+
}
|
|
255
|
+
(args as any).recovery = parsed
|
|
256
|
+
}
|
|
257
|
+
assertValidRecovery(args.recovery)
|
|
258
|
+
|
|
259
|
+
const parsedGroups = await parseAllTaskGroupsFromMd(ctx.worktree, args.change_id)
|
|
260
|
+
if (parsedGroups.length === 0) {
|
|
261
|
+
throw new Error(`无法从 tasks.md 解析出任务组,请检查文件 openspec/changes/${args.change_id}/tasks.md。`)
|
|
262
|
+
}
|
|
263
|
+
const targetGroup = parsedGroups.find((g) => g.id === args.task_group_id)
|
|
264
|
+
if (!targetGroup) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`task_group_id "${args.task_group_id}" 不在 tasks.md 中。\n可用 ID:\n` +
|
|
267
|
+
`- ${parsedGroups.map((g) => g.id).join("\n- ")}`
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 逐任务组解析 tasks.md 子任务(构造各 task WorkItem 的 task children 用)
|
|
272
|
+
const tasksByGroup = new Map<string, ParsedTask[]>()
|
|
273
|
+
for (const g of parsedGroups) {
|
|
274
|
+
tasksByGroup.set(g.id, await parseTasksMdForGroup(ctx.worktree, args.change_id, g.id))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// base_branch 是 ref 而非严格 branch:只做非空 + 无空白字符等基本检查(完整分支名校验由 git check-ref-format 承担)
|
|
278
|
+
if (args.base_branch) {
|
|
279
|
+
if (!args.base_branch.trim() || /\s/.test(args.base_branch)) {
|
|
280
|
+
throw new Error(`base_branch 不合法:"${args.base_branch}"。基准分支名不能为空或包含空白字符。`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const baseBranch = args.base_branch || await getCurrentBranch(ctx.worktree)
|
|
284
|
+
let state = await readStateByChangeId(ctx.worktree, args.change_id)
|
|
285
|
+
const wasCurrentGroup = state?.taskGroupId === args.task_group_id
|
|
286
|
+
|
|
287
|
+
if (!state) {
|
|
288
|
+
state = {
|
|
289
|
+
changeId: args.change_id,
|
|
290
|
+
isolationNamespace: generateIsolationNamespace(args.change_id),
|
|
291
|
+
taskGroupId: args.task_group_id,
|
|
292
|
+
baseBranch,
|
|
293
|
+
workItems: [],
|
|
294
|
+
createdAt: new Date().toISOString(),
|
|
295
|
+
updatedAt: new Date().toISOString(),
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
state.baseBranch = state.baseBranch || baseBranch
|
|
299
|
+
state.isolationNamespace = state.isolationNamespace || generateIsolationNamespace(state.changeId)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 按 tasks.md 构造全部任务组的 task WorkItem(单轨:workItems 为唯一事实源)
|
|
303
|
+
for (const group of parsedGroups) {
|
|
304
|
+
const isCurrent = group.id === args.task_group_id
|
|
305
|
+
const groupTasks = tasksByGroup.get(group.id) ?? []
|
|
306
|
+
const existing = state.workItems.find((w) => w.id === `task:${group.id}`)
|
|
307
|
+
const refreshMeta = (item: WorkItem): void => {
|
|
308
|
+
item.metadata["name"] = group.name
|
|
309
|
+
item.metadata["task_count"] = group.taskCount
|
|
310
|
+
item.metadata["source"] = "openspec"
|
|
311
|
+
item.metadata["relevant_specs"] = extractRelevantSpecsFromTasks(groupTasks)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 旧 state 迁移:metadata.tasks(TaskItem[])→ task children(挂入 children 后删除 metadata.tasks)
|
|
315
|
+
if (existing) migrateLegacyTasks(existing)
|
|
316
|
+
|
|
317
|
+
if (!isCurrent) {
|
|
318
|
+
// 非活跃组:已有则保留进度(仅刷新名称/计数),否则新建
|
|
319
|
+
if (existing) {
|
|
320
|
+
refreshMeta(existing)
|
|
321
|
+
} else {
|
|
322
|
+
const item = createInitialWorkItem({
|
|
323
|
+
id: `task:${group.id}`,
|
|
324
|
+
source: "openspec",
|
|
325
|
+
externalId: group.id,
|
|
326
|
+
type: "task",
|
|
327
|
+
title: group.name,
|
|
328
|
+
description: group.name,
|
|
329
|
+
labels: ["openspec-change"],
|
|
330
|
+
})
|
|
331
|
+
item.currentStep = "analyze"
|
|
332
|
+
syncTaskChildren(item, groupTasks, { defaultStatus: "todo" })
|
|
333
|
+
refreshMeta(item)
|
|
334
|
+
state.workItems.push(item)
|
|
335
|
+
}
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 活跃组:无 recovery 重复初始化当前组 → 保留进度并刷新 task children
|
|
340
|
+
// (按 parsed 数量/标题做一致性重建,复用既有 children 进度,顺带自愈重复 id 的已损坏 state)
|
|
341
|
+
if (existing && !args.recovery && wasCurrentGroup) {
|
|
342
|
+
refreshMeta(existing)
|
|
343
|
+
syncTaskChildren(existing, groupTasks, {})
|
|
344
|
+
// 与 recovery 路径一致:重建为已知状态后清除残留推进阻塞原因,避免视图展示过期信息
|
|
345
|
+
delete existing.metadata["_advance_block_reason"]
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const item = existing ?? createInitialWorkItem({
|
|
350
|
+
id: `task:${group.id}`,
|
|
351
|
+
source: "openspec",
|
|
352
|
+
externalId: group.id,
|
|
353
|
+
type: "task",
|
|
354
|
+
title: group.name,
|
|
355
|
+
description: group.name,
|
|
356
|
+
labels: ["openspec-change"],
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// reopenIssues:已完成组继续修 issue(children 未终态置 todo + reject_reason、清 verify_* tags)
|
|
360
|
+
if (args.recovery?.reopenIssues) {
|
|
361
|
+
const closed = item.metadata["completed_at"] !== undefined || item.phase === "done"
|
|
362
|
+
if (!closed) {
|
|
363
|
+
throw new Error(`reopenIssues 仅支持已完成(completed)的任务组,当前 item.phase="${item.phase}"。`)
|
|
364
|
+
}
|
|
365
|
+
if (args.recovery.phase !== "dev_impl") {
|
|
366
|
+
throw new Error("reopenIssues 仅支持恢复到 dev_impl 阶段。")
|
|
367
|
+
}
|
|
368
|
+
if (args.recovery.review_layer) {
|
|
369
|
+
throw new Error("reopenIssues 与 review_layer 互斥,不可同时使用。")
|
|
370
|
+
}
|
|
371
|
+
for (const child of item.children) {
|
|
372
|
+
if (isTerminalPhase(child.phase)) continue
|
|
373
|
+
child.phase = "todo"
|
|
374
|
+
child.metadata["reject_reason"] = child.metadata["reject_reason"] ?? "通过 reopenIssues 自动驳回"
|
|
375
|
+
}
|
|
376
|
+
for (const key of Object.keys(item.tags)) {
|
|
377
|
+
if (key.startsWith("verify_")) delete item.tags[key]
|
|
378
|
+
}
|
|
379
|
+
delete item.metadata["completed_at"]
|
|
380
|
+
item.metadata["worktree_path"] = null
|
|
381
|
+
item.metadata["branch_name"] = null
|
|
382
|
+
item.metadata["base_ref"] = null
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
applyRecoveryState(item, args.recovery, groupTasks)
|
|
386
|
+
refreshMeta(item)
|
|
387
|
+
if (!existing) state.workItems.push(item)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
state.taskGroupId = args.task_group_id
|
|
391
|
+
await writeState(ctx.worktree, state)
|
|
392
|
+
|
|
393
|
+
return args.recovery
|
|
394
|
+
? `编排会话已初始化。已恢复到 ${args.recovery.phase} 阶段。`
|
|
395
|
+
: "编排会话已初始化。"
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function bindWorktreeRefs(
|
|
399
|
+
item: WorkItem,
|
|
400
|
+
worktreePath: string,
|
|
401
|
+
branch: string,
|
|
402
|
+
baseBranch: string,
|
|
403
|
+
opts: { requireBaseRef?: boolean } = {},
|
|
404
|
+
): Promise<void> {
|
|
405
|
+
item.metadata["worktree_path"] = worktreePath
|
|
406
|
+
item.metadata["branch_name"] = branch
|
|
407
|
+
const baseRef = await getMergeBase(worktreePath, baseBranch)
|
|
408
|
+
if (!baseRef) {
|
|
409
|
+
if (opts.requireBaseRef) {
|
|
410
|
+
throw new Error(`worktree 创建成功但无法获取与 ${baseBranch} 的 merge-base:${worktreePath}`)
|
|
411
|
+
}
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
item.metadata["base_ref"] = baseRef
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function setWorktreeExecute(params: SetWorktreeParams, ctx: ToolContext): Promise<string> {
|
|
418
|
+
assertOrchestrator(ctx, "opx_orch_set_worktree")
|
|
419
|
+
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
420
|
+
if (!state) throw new Error("编排会话未初始化。请先调用 opx_orch_init。")
|
|
421
|
+
const item = state.workItems.find((w) => w.id === `task:${state.taskGroupId}`)
|
|
422
|
+
if (!item) throw new Error(`工作项 "task:${state.taskGroupId}" 缺失,请重新调用 opx_orch_init。`)
|
|
423
|
+
|
|
424
|
+
const repoRoot = ctx.worktree
|
|
425
|
+
// branch_name 可为空(缺省自动生成分支名),仅显式传入时用 git check-ref-format 严格校验。
|
|
426
|
+
// 用 --branch 形态(而非 refs/heads/<name>):前者拒绝前导 `-` 等 git branch 创建亦拒绝的非法分支名,
|
|
427
|
+
// 后者仅检查 refname 合法性,会放行前导 dash 的 plain ref。
|
|
428
|
+
const rawBranch = params.branch_name ?? ""
|
|
429
|
+
if (rawBranch !== "") {
|
|
430
|
+
const check = await runGitChecked(repoRoot, ["check-ref-format", "--branch", rawBranch])
|
|
431
|
+
if (!check.success) {
|
|
432
|
+
throw new Error(`分支名 "${rawBranch}" 不合法,请修正后重试。`)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const branch = rawBranch || `task-group/${state.changeId}/${state.taskGroupId}`
|
|
436
|
+
let wtPath: string
|
|
437
|
+
if (params.worktree_path) {
|
|
438
|
+
wtPath = assertPathWithin(repoRoot, params.worktree_path, "worktree_path")
|
|
439
|
+
} else {
|
|
440
|
+
wtPath = path.join(repoRoot, ".worktree", state.changeId, `task-group-${state.taskGroupId}`)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const changeStatus = await runGit(repoRoot, ["status", "--porcelain", `openspec/changes/${state.changeId}/`])
|
|
444
|
+
if (changeStatus.trim().length > 0) {
|
|
445
|
+
const addResult = await runGitChecked(repoRoot, ["add", `openspec/changes/${state.changeId}/`])
|
|
446
|
+
if (!addResult.success) throw new Error(`change 目录 git add 失败:${addResult.stderr}`)
|
|
447
|
+
const commitResult = await runGitChecked(repoRoot, ["commit", "-m", "docs(openspec): auto-commit before worktree setup"])
|
|
448
|
+
if (!commitResult.success) throw new Error(`change 目录 git commit 失败:${commitResult.stderr}`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const wtList = await runGit(repoRoot, ["worktree", "list"])
|
|
452
|
+
const existingLine = wtList.split("\n").find((l) => {
|
|
453
|
+
const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/)
|
|
454
|
+
return m && m[2].trim() === branch
|
|
455
|
+
})
|
|
456
|
+
const existingPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined
|
|
457
|
+
|
|
458
|
+
let reused = false
|
|
459
|
+
if (existingPath) {
|
|
460
|
+
const baseHead = await runGit(repoRoot, ["rev-parse", state.baseBranch])
|
|
461
|
+
const mergeResult = await runGitChecked(existingPath, ["merge", "--ff-only", baseHead])
|
|
462
|
+
if (mergeResult.success) {
|
|
463
|
+
await bindWorktreeRefs(item, existingPath, branch, state.baseBranch)
|
|
464
|
+
reused = true
|
|
465
|
+
} else {
|
|
466
|
+
const clean = await isWorktreeClean(existingPath)
|
|
467
|
+
if (!clean) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`已有 worktree "${existingPath}" 与 ${state.baseBranch} 分叉且有未提交变更,无法自动 fast-forward。\n` +
|
|
470
|
+
`请手动处理后重试。`
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
const localCommitCount = parseInt(
|
|
474
|
+
await runGit(existingPath, ["rev-list", "--count", `${state.baseBranch}..HEAD`]),
|
|
475
|
+
10
|
|
476
|
+
)
|
|
477
|
+
if (localCommitCount > 0 || Number.isNaN(localCommitCount)) {
|
|
478
|
+
await bindWorktreeRefs(item, existingPath, branch, state.baseBranch)
|
|
479
|
+
reused = true
|
|
480
|
+
} else {
|
|
481
|
+
const rmResult = await runGitChecked(repoRoot, ["worktree", "remove", existingPath, "--force"])
|
|
482
|
+
if (!rmResult.success) {
|
|
483
|
+
throw new Error(`无法清理已有 worktree "${existingPath}":${rmResult.stderr}`)
|
|
484
|
+
}
|
|
485
|
+
const branchRmResult = await runGitChecked(repoRoot, ["branch", "-D", branch])
|
|
486
|
+
if (!branchRmResult.success) {
|
|
487
|
+
throw new Error(`无法清理已有分支 "${branch}":${branchRmResult.stderr}`)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (!reused) {
|
|
494
|
+
const forkBranch = state.baseBranch
|
|
495
|
+
await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch])
|
|
496
|
+
await bindWorktreeRefs(item, wtPath, branch, forkBranch, { requireBaseRef: true })
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await writeState(ctx.worktree, state)
|
|
500
|
+
|
|
501
|
+
// 在 worktree 中写入上下文指针,供 worktree 内 session 读取 state
|
|
502
|
+
const storedPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : null
|
|
503
|
+
if (storedPath) {
|
|
504
|
+
await writeContextToWorktree(storedPath, state.changeId, state.taskGroupId)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return [
|
|
508
|
+
`- **状态**: ${reused ? "复用已有 worktree" : "已创建 worktree"}`,
|
|
509
|
+
`- **路径**: \`${item.metadata["worktree_path"]}\``,
|
|
510
|
+
`- **分支**: \`${branch}\``,
|
|
511
|
+
].join("\n")
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function statusExecute(params: StatusParams, ctx: ToolContext): Promise<string> {
|
|
515
|
+
const agent = ctx.agent
|
|
516
|
+
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
517
|
+
|
|
518
|
+
if (!state) {
|
|
519
|
+
if (ctx.orchestrator) {
|
|
520
|
+
const diskWts = await discoverDiskWorktrees(ctx.worktree)
|
|
521
|
+
if (diskWts.length > 0) {
|
|
522
|
+
const lines = ["# 编排进度", "", "**状态文件**: 未初始化", "", "## 磁盘 Worktree(可恢复进度)", ""]
|
|
523
|
+
lines.push("| 分支 | 路径 |")
|
|
524
|
+
lines.push("|------|------|")
|
|
525
|
+
for (const w of diskWts) lines.push(`| ${w.branch} | \`${w.path}\` |`)
|
|
526
|
+
lines.push("")
|
|
527
|
+
lines.push("请用 question 工具询问用户确认恢复目标,然后调用 opx_orch_init(recovery=...)。")
|
|
528
|
+
return lines.join("\n")
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return "编排会话尚未初始化。请先调用 opx_orch_init。"
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const item = state.workItems.find((w) => w.id === `task:${state.taskGroupId}`)
|
|
535
|
+
if (!item) {
|
|
536
|
+
return "编排会话未就绪:找不到活跃任务组的工作项,请重新调用 opx_orch_init。"
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 单轨:一律由工作流引擎推荐(recommendForItem)渲染动态视图,按调用者角色分流
|
|
540
|
+
const workflow = loadWorkflowFile(TASK_WORKFLOW_PATH)
|
|
541
|
+
const rec = recommendForItem(item, workflow)
|
|
542
|
+
const tg = findTaskGroup(state, state.taskGroupId)
|
|
543
|
+
// 主仓库 openspec 污染诊断(56ddfe9 意图):编排者分派视图展示主仓库污染,供编排者人工核对
|
|
544
|
+
const mainPollution = ctx.orchestrator ? await detectMainRepoPollution(ctx.worktree) : null
|
|
545
|
+
// tool review 检查点增量检测(A4):verify_tool 的 reviewer-tool 工作视图按「检查点 → 当前 HEAD」区间
|
|
546
|
+
// 变更分流(直提 / 仅处理待复核项 / 全量)。渲染层为同步函数,此处预计算后经 WorkflowStatusViewOptions 传入。
|
|
547
|
+
// 仅在推荐分派该 agent 时计算,其余角色/step 不产生额外 git 调用。
|
|
548
|
+
let toolChanges: DetectChangesResult | undefined
|
|
549
|
+
if (
|
|
550
|
+
agentToReviewLayer(agent) === "tool" &&
|
|
551
|
+
rec.status === "recommend" &&
|
|
552
|
+
rec.stepId === "verify_tool" &&
|
|
553
|
+
rec.agents.includes(agent)
|
|
554
|
+
) {
|
|
555
|
+
const wtPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : undefined
|
|
556
|
+
if (wtPath) {
|
|
557
|
+
const checkpoint =
|
|
558
|
+
typeof item.metadata["_tool_review_checkpoint"] === "string" ? item.metadata["_tool_review_checkpoint"] : undefined
|
|
559
|
+
const baseRef = typeof item.metadata["base_ref"] === "string" ? item.metadata["base_ref"] : undefined
|
|
560
|
+
toolChanges = await detectChanges(wtPath, { checkpoint, baseRef })
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// 统计本 change 命中项目级跨 change 豁免清单的存量问题数(工具层降级时写入 exempted_hit 标记)
|
|
564
|
+
const exemptedHits = item.children.filter((c) => c.type === "issue" && c.metadata["exempted_hit"] !== undefined).length
|
|
565
|
+
// 渲染期豁免清单提示数据源:渲染前异步读取一次(renderChildIssue 为同步函数,清单条目以参数传入,不在循环内重复读)
|
|
566
|
+
const exemptionItems = (await readExemptions(ctx.worktree)).items
|
|
567
|
+
return renderWorkflowStatusView(item, workflow, rec, { agent, orchestrator: ctx.orchestrator, identityDeclared: ctx.identityDeclared }, { state, tg, mainPollution, toolChanges, exemptedHits, exemptionItems })
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export async function completeTaskGroupExecute(params: { change_id: string }, ctx: ToolContext): Promise<string> {
|
|
571
|
+
assertOrchestrator(ctx, "opx_orch_complete_task_group")
|
|
572
|
+
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
573
|
+
if (!state) throw new Error("编排会话未初始化。请先调用 opx_orch_init。")
|
|
574
|
+
const item = state.workItems.find((w) => w.id === `task:${state.taskGroupId}`)
|
|
575
|
+
if (!item) throw new Error(`工作项 "task:${state.taskGroupId}" 缺失,请重新调用 opx_orch_init。`)
|
|
576
|
+
|
|
577
|
+
if (item.phase !== "done" || item.metadata["completed_at"] !== undefined) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`阶段顺序错误:opx_orch_complete_task_group 需在 review 完成后调用。\n` +
|
|
580
|
+
`- item.phase=${item.phase}\n` +
|
|
581
|
+
`- completed_at=${item.metadata["completed_at"] ?? "(未设置)"}`
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const worktreePath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : null
|
|
586
|
+
const branchName = typeof item.metadata["branch_name"] === "string" ? item.metadata["branch_name"] : null
|
|
587
|
+
|
|
588
|
+
if (worktreePath) {
|
|
589
|
+
const clean = await isWorktreeClean(worktreePath)
|
|
590
|
+
if (!clean) throw new Error(`worktree "${worktreePath}" 存在未 commit 内容,请先 commit 再完成任务组。`)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const openIssues = item.children.filter((c) => isBlockingSeverity(c.severity) && !isTerminalPhase(c.phase))
|
|
594
|
+
if (openIssues.length > 0) {
|
|
595
|
+
throw new Error(`存在 ${openIssues.length} 个 Low 及以上的未解决 issue 未处理,请先修复或申请豁免。`)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// task children 须全部终态(done/cancelled)才能收尾
|
|
599
|
+
const openTasks = taskChildrenOf(item).filter((c) => !isTerminalPhase(c.phase))
|
|
600
|
+
if (openTasks.length > 0) {
|
|
601
|
+
throw new Error(`存在 ${openTasks.length} 个未完成 task。`)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const blockers = Array.isArray(item.metadata["blockers"])
|
|
605
|
+
? (item.metadata["blockers"] as { status: string }[])
|
|
606
|
+
: []
|
|
607
|
+
const unresolvedBlockers = blockers.filter((blocker) => blocker.status !== "resolved")
|
|
608
|
+
if (unresolvedBlockers.length > 0) {
|
|
609
|
+
throw new Error(`存在 ${unresolvedBlockers.length} 个未解决 blocker,无法完成任务组。`)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const mergeTarget = state.baseBranch
|
|
613
|
+
if (branchName) {
|
|
614
|
+
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget)
|
|
615
|
+
if (!mergeResult.success) {
|
|
616
|
+
return [
|
|
617
|
+
`- **status**: blocked`,
|
|
618
|
+
`- **merge_conflict**: true`,
|
|
619
|
+
`- **说明**: 合并到 "${mergeTarget}" 时发生冲突,已中止合并。`,
|
|
620
|
+
`- **处理**: 请手动在目标分支解决冲突后完成合并 (git merge ${branchName}),完成后重新调 opx_orch_complete_task_group 完成收尾。worktree 与分支已保留。`,
|
|
621
|
+
].join("\n")
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (worktreePath && branchName) {
|
|
625
|
+
try {
|
|
626
|
+
await runGit(ctx.worktree, ["worktree", "remove", worktreePath, "--force"])
|
|
627
|
+
await runGit(ctx.worktree, ["branch", "-D", branchName])
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
item.metadata["completed_at"] = new Date().toISOString()
|
|
632
|
+
await writeState(ctx.worktree, state)
|
|
633
|
+
return `任务组已完成并合并到 "${mergeTarget}"。`
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export async function setUnattendedExecute(params: UnattendedParams, ctx: ToolContext): Promise<string> {
|
|
637
|
+
assertOrchestrator(ctx, "opx_orch_set_unattended")
|
|
638
|
+
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
639
|
+
if (!state) throw new Error("编排会话未初始化。请先调用 opx_orch_init。")
|
|
640
|
+
// enabled 缺省按 schema 声明 default=true 兜底(跨形态一致:opencode 直载透传 input 不应用 zod default)
|
|
641
|
+
state.unattended = params.enabled ?? true
|
|
642
|
+
await writeState(ctx.worktree, state)
|
|
643
|
+
const status = state.unattended ? "开启" : "关闭"
|
|
644
|
+
return `无人值守模式已 **${status}**。启用后系统将自动处理决策点,不再 question 用户。`
|
|
645
|
+
}
|