@ifrankwang/openspec-agents 0.120.1 → 0.121.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 +121 -22
- package/README.md +30 -6
- package/assets/agents/openspec-developer.md +52 -2
- package/assets/agents/openspec-reviewer.md +191 -0
- package/assets/skills/ddd-architecture/SKILL.md +9 -0
- package/assets/skills/java-ddd-architecture/SKILL.md +2 -1
- package/assets/skills/orchestrator/SKILL.md +3 -3
- package/assets/workflows/task-simple.yaml +85 -0
- package/assets/workflows/task.yaml +0 -1
- package/dsh/cordis.patch.yml +141 -438
- package/package.json +1 -1
- package/src/adapters/codex/index.ts +3 -2
- package/src/adapters/mcp-common/server.ts +1 -1
- package/src/adapters/opencode/agents.ts +0 -19
- package/src/adapters/opencode/index.ts +27 -20
- package/src/adapters/opencode/tools.ts +13 -72
- package/src/core/constants.ts +3 -1
- package/src/core/state.ts +3 -0
- package/src/core/tools/lifecycle.ts +59 -8
- package/src/core/tools/submit.ts +30 -5
- package/src/core/tools/types.ts +1 -1
- package/src/core/types.ts +6 -0
- package/src/core/workflow/engine.ts +4 -1
- package/src/core/workflow/loader.ts +52 -5
- package/src/core/workflow/status.ts +43 -10
- package/assets/agents/openspec-architect.md +0 -61
- package/assets/agents/openspec-reviewer-architecture.md +0 -60
- package/assets/agents/openspec-reviewer-maintainability.md +0 -59
- package/assets/agents/openspec-reviewer-performance.md +0 -49
- package/assets/agents/openspec-reviewer-security.md +0 -51
- package/assets/agents/openspec-reviewer-style.md +0 -50
- package/assets/agents/openspec-reviewer-task.md +0 -40
- package/assets/agents/openspec-reviewer-tool.md +0 -30
- package/src/adapters/opencode/json-schema.ts +0 -60
package/package.json
CHANGED
|
@@ -13,10 +13,11 @@ import { join, dirname } from "node:path"
|
|
|
13
13
|
import { parseAgentMd, resolve, EXCLUDED_AGENTS } from "../agent-md.ts"
|
|
14
14
|
import { buildPluginPackage, PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_AUTHOR, type PluginPackageResult } from "../plugin-common/index.ts"
|
|
15
15
|
|
|
16
|
-
/** codex agent
|
|
16
|
+
/** codex agent 工具白名单(按职责收敛,其余身份统一走 default 白名单)。 */
|
|
17
17
|
const AGENT_TOOLS: Record<string, string[]> = {
|
|
18
|
-
"openspec-architect": ["read", "grep", "glob", "ls", "bash", "web"],
|
|
19
18
|
"openspec-developer": ["read", "grep", "glob", "ls", "bash", "apply_patch", "web"],
|
|
19
|
+
// 审查者仅可直改文档/注释,需要 apply_patch;逻辑类修改一律只报(openspec-reviewer.md 角色约束)
|
|
20
|
+
"openspec-reviewer": ["read", "grep", "glob", "ls", "bash", "apply_patch", "web"],
|
|
20
21
|
default: ["read", "grep", "glob", "ls", "bash"],
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -21,22 +21,7 @@ function mapPermission(
|
|
|
21
21
|
|
|
22
22
|
const AGENTS_ROOT = resolve("assets", "agents")
|
|
23
23
|
|
|
24
|
-
/** 已知主代理名集合:注入的 openspec-main + 用户既有 primary agent + opencode 默认主代理 "primary"。 */
|
|
25
|
-
const primaryAgents = new Set<string>(["primary"])
|
|
26
|
-
|
|
27
|
-
/** 编排视角角色判定:调用者是否为主代理(承担编排者职责),替代 agent 名硬编码。 */
|
|
28
|
-
export function isPrimaryAgent(agent: string): boolean {
|
|
29
|
-
return primaryAgents.has(agent)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
24
|
export function injectAgents(config: Record<string, unknown>): void {
|
|
33
|
-
// 探测主代理名:config.agent 中 mode=primary 的条目(用户自定义 + 注入的主代理模板)
|
|
34
|
-
const existingAgents = (config.agent as Record<string, unknown>) ?? {}
|
|
35
|
-
for (const [name, cfg] of Object.entries(existingAgents)) {
|
|
36
|
-
if (typeof cfg === "object" && cfg !== null && (cfg as Record<string, unknown>).mode === "primary") {
|
|
37
|
-
primaryAgents.add(name)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
25
|
if (existsSync(AGENTS_ROOT)) {
|
|
41
26
|
const files = readdirSync(AGENTS_ROOT).filter((f) => f.endsWith(".md"))
|
|
42
27
|
for (const file of files) {
|
|
@@ -60,10 +45,6 @@ export function injectAgents(config: Record<string, unknown>): void {
|
|
|
60
45
|
const perm = mapPermission(frontmatter)
|
|
61
46
|
if (perm) agentConfig.permission = perm
|
|
62
47
|
|
|
63
|
-
if (frontmatter.mode === "primary") {
|
|
64
|
-
primaryAgents.add(name)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
48
|
const existingAgentsMap = (config.agent as Record<string, unknown>) ?? {}
|
|
68
49
|
const existingAgent = existingAgentsMap[name] as Record<string, unknown> | undefined
|
|
69
50
|
config.agent = {
|
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
import { type Plugin } from "@opencode-ai/plugin"
|
|
2
2
|
import { injectSkills } from "./skills.ts"
|
|
3
3
|
import { injectAgents } from "./agents.ts"
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
init,
|
|
7
|
-
set_worktree,
|
|
8
|
-
status,
|
|
9
|
-
complete_task_group,
|
|
10
|
-
set_unattended,
|
|
11
|
-
agent_submit,
|
|
12
|
-
} from "./tools.ts"
|
|
4
|
+
import { resolve } from "../agent-md.ts"
|
|
13
5
|
|
|
14
6
|
/**
|
|
15
|
-
* OpenCode
|
|
16
|
-
*
|
|
17
|
-
*
|
|
7
|
+
* OpenCode 插件壳(MCP 形态):配置注入(agent/skill)+ MCP server 配置注入(config.mcp)。
|
|
8
|
+
* 直载工具注册已移除(BREAKING,change agent-merge-and-mode-config 组 5.1):
|
|
9
|
+
* - opx_* 工具统一经 MCP server 暴露(stdio bundle,--worktree 指向当前项目根),
|
|
10
|
+
* 与 claude-code / zcode / codex 插件的 .mcp.json 形态一致;
|
|
11
|
+
* - 身份不再由会话运行时推导,统一走 mcp-common 的 `_agent` 参数解析(缺省视为编排视角)。
|
|
12
|
+
* dashboard/collector/poller 副作用已迁移至 MCP server 进程(src/adapters/mcp-common/)。
|
|
18
13
|
*/
|
|
19
14
|
export const OpenspecOrchestratePlugin: Plugin = async (input) => {
|
|
20
15
|
return {
|
|
21
16
|
config: async (config) => {
|
|
22
17
|
injectAgents(config)
|
|
23
18
|
injectSkills(config)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
// MCP server 声明:opencode config 的 mcp 段(官方形态
|
|
20
|
+
// { mcp: { <server>: { type: "local", command: [...] } } },见 opencode docs/mcp-servers)。
|
|
21
|
+
// 入口为包内 .mcp-server/cli.mjs(prepack 构建的 MCP server 自包含 bundle),
|
|
22
|
+
// --worktree 指向当前项目根(input.worktree 即 opencode 打开的 git worktree 路径)。
|
|
23
|
+
config.mcp = {
|
|
24
|
+
...(config.mcp ?? {}),
|
|
25
|
+
opx: {
|
|
26
|
+
type: "local",
|
|
27
|
+
command: [
|
|
28
|
+
"node",
|
|
29
|
+
resolve(".mcp-server", "cli.mjs"),
|
|
30
|
+
"--transport",
|
|
31
|
+
"stdio",
|
|
32
|
+
"--worktree",
|
|
33
|
+
input.worktree,
|
|
34
|
+
"--unattended",
|
|
35
|
+
"--strip-opx-prefix",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
}
|
|
32
39
|
},
|
|
33
40
|
}
|
|
34
41
|
}
|
|
@@ -1,78 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 直载工具注册已移除(BREAKING,change agent-merge-and-mode-config 组 5.1):
|
|
3
|
+
* - 插件壳不再直载 opx_* 工具,统一经 MCP server 配置(config.mcp,stdio bundle)暴露;
|
|
4
|
+
* - 身份不再由会话运行时推导(makeCtx 与 isPrimaryAgent 判定已删),统一走 mcp-common 的 `_agent` 参数解析。
|
|
5
|
+
* 本文件仅保留测试直调核心 execute 的无状态透传壳(不注册 OpenCode 工具、无身份推导),
|
|
6
|
+
* 调用方(tests/ 既有用例)显式构造 ToolContext 传入。
|
|
7
|
+
*/
|
|
3
8
|
import {
|
|
4
9
|
initExecute, setWorktreeExecute, statusExecute,
|
|
5
10
|
completeTaskGroupExecute, setUnattendedExecute,
|
|
6
11
|
} from "../../core/tools/lifecycle.ts"
|
|
7
12
|
import { agentSubmitExecute } from "../../core/tools/submit.ts"
|
|
8
|
-
import {
|
|
9
|
-
orchInitSchema, setWorktreeSchema, statusSchema,
|
|
10
|
-
completeTaskGroupSchema, setUnattendedSchema, agentSubmitSchema,
|
|
11
|
-
} from "../../core/tools/schemas.ts"
|
|
12
|
-
import { jsonSchemaToZod } from "./json-schema.ts"
|
|
13
|
-
import { isPrimaryAgent } from "./agents.ts"
|
|
14
|
-
import type { ToolContext } from "../../core/tools/types.ts"
|
|
15
|
-
|
|
16
|
-
/** 直载形态调用上下文:agent 身份由会话运行时推导(恒显式声明),编排视角按主代理名判定。 */
|
|
17
|
-
function makeCtx(context: { worktree: string; agent: string }): ToolContext {
|
|
18
|
-
return {
|
|
19
|
-
worktree: context.worktree,
|
|
20
|
-
agent: context.agent,
|
|
21
|
-
orchestrator: isPrimaryAgent(context.agent),
|
|
22
|
-
identityDeclared: true,
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const init = tool({
|
|
27
|
-
description:
|
|
28
|
-
"初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery 重复初始化当前任务组时保留其阶段和进度;切换到其它任务组时初始化该组。",
|
|
29
|
-
args: jsonSchemaToZod(orchInitSchema).shape as any,
|
|
30
|
-
async execute(args, context) {
|
|
31
|
-
return initExecute(args as any, makeCtx(context))
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
export const set_worktree = tool({
|
|
36
|
-
description:
|
|
37
|
-
"确保目标组的 git worktree 就绪。若已存在则复用,否则按规范自动创建(分支 task-group/{changeId}/{taskGroupId},路径 .worktree/{changeId}/task-group-{taskGroupId})。只补齐资源,不改变阶段。",
|
|
38
|
-
args: jsonSchemaToZod(setWorktreeSchema).shape as any,
|
|
39
|
-
async execute(args, context) {
|
|
40
|
-
return setWorktreeExecute(args as any, makeCtx(context))
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
export const status = tool({
|
|
45
|
-
description:
|
|
46
|
-
"统一状态/上下文查询(只读为主)。按调用者角色路由:编排视角→统计+worktree;architect→spec/blocker;developer→worktree/boundary/task/issue;reviewer-tool→tool 层控件 issue;reviewer-task→task 验证状态;quality reviewer→自维度既有 issue。",
|
|
47
|
-
args: jsonSchemaToZod(statusSchema).shape as any,
|
|
48
|
-
async execute(args, context) {
|
|
49
|
-
return statusExecute({ change_id: args.change_id as string }, makeCtx(context))
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
export const complete_task_group = tool({
|
|
54
|
-
description:
|
|
55
|
-
"完成任务组收尾:合并 task-group 分支到 baseBranch → 清理 worktree 与分支。须在收尾验证(verify_cleanup)通过后调用。合并冲突时中止并返回 blocked(保留 worktree/分支)。",
|
|
56
|
-
args: jsonSchemaToZod(completeTaskGroupSchema).shape as any,
|
|
57
|
-
async execute(args, context) {
|
|
58
|
-
return completeTaskGroupExecute({ change_id: args.change_id as string }, makeCtx(context))
|
|
59
|
-
},
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
export const set_unattended = tool({
|
|
63
|
-
description:
|
|
64
|
-
"开启/关闭无人值守模式。开启后编排流程不再向用户提问:analyze 确认模式由架构师自行裁决(不确认用户);重试检查点、状态异常、blocker 处理等需拍板事项由主代理按编排行为准则自行决策并提交——检查点决策是 opx_agent_submit 工具调用,主代理可自行执行,不因无人值守而抑制。",
|
|
65
|
-
args: jsonSchemaToZod(setUnattendedSchema).shape as any,
|
|
66
|
-
async execute(args, context) {
|
|
67
|
-
return setUnattendedExecute(args as any, makeCtx(context))
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
13
|
|
|
71
|
-
export const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
})
|
|
14
|
+
export const init = { execute: initExecute }
|
|
15
|
+
export const set_worktree = { execute: setWorktreeExecute }
|
|
16
|
+
export const status = { execute: statusExecute }
|
|
17
|
+
export const complete_task_group = { execute: completeTaskGroupExecute }
|
|
18
|
+
export const set_unattended = { execute: setUnattendedExecute }
|
|
19
|
+
export const agent_submit = { execute: agentSubmitExecute }
|
package/src/core/constants.ts
CHANGED
|
@@ -22,11 +22,13 @@ export function agentToReviewDimension(agent: string): ReviewDimension | undefin
|
|
|
22
22
|
return (Object.keys(DIMENSION_AGENT_MAP) as ReviewDimension[]).find((d) => DIMENSION_AGENT_MAP[d] === agent)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** agent → issue 报源层(tool/task/quality):tool/task reviewer 直接映射,quality reviewer 经维度反查命中即 quality
|
|
25
|
+
/** agent → issue 报源层(tool/task/quality):tool/task reviewer 直接映射,quality reviewer 经维度反查命中即 quality。
|
|
26
|
+
* openspec-reviewer(simple 模式合并审查者)映射到 quality 层;full 模式不会分派该身份,该映射只对 simple 生效。其余 agent 返回 undefined。 */
|
|
26
27
|
export function agentToReviewLayer(agent: string | undefined): ReviewLayer | undefined {
|
|
27
28
|
if (!agent) return undefined
|
|
28
29
|
if (agent === "openspec-reviewer-tool") return "tool"
|
|
29
30
|
if (agent === "openspec-reviewer-task") return "task"
|
|
31
|
+
if (agent === "openspec-reviewer") return "quality"
|
|
30
32
|
if (agentToReviewDimension(agent)) return "quality"
|
|
31
33
|
return undefined
|
|
32
34
|
}
|
package/src/core/state.ts
CHANGED
|
@@ -271,6 +271,9 @@ export async function readStateByChangeId(worktree: string, changeId: string): P
|
|
|
271
271
|
createdAt: legacy.createdAt,
|
|
272
272
|
updatedAt: legacy.updatedAt,
|
|
273
273
|
unattended: legacy.unattended,
|
|
274
|
+
// 透传固化模式:state 已存在(重复初始化/切换任务组/recovery)时沿用既有 mode,
|
|
275
|
+
// 缺 mode 的旧变更保持缺省(消费端读时兜底 full,不在此写回)
|
|
276
|
+
mode: legacy.mode,
|
|
274
277
|
}
|
|
275
278
|
if (needsUpgrade) {
|
|
276
279
|
// 迁移产物一次性落盘固定单轨形态,避免每次读取都重建(首次写新目录前幂等迁移旧数据)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "path"
|
|
2
|
-
import type { TaskItem, TaskStatus } from "../types.ts"
|
|
2
|
+
import type { TaskItem, TaskStatus, WorkflowMode } from "../types.ts"
|
|
3
3
|
import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS } from "../types.ts"
|
|
4
4
|
import { agentToReviewLayer } from "../constants.ts"
|
|
5
5
|
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, type DetectChangesResult } from "../git.ts"
|
|
@@ -10,7 +10,7 @@ import { parseAllTaskGroupsFromMd, parseTasksMdForGroup, extractRelevantSpecsFro
|
|
|
10
10
|
import type { ParsedTask } from "../tasks-md.ts"
|
|
11
11
|
import { assertOrchestrator, findTaskGroup } from "../derive.ts"
|
|
12
12
|
import { assertPathWithin } from "../paths.ts"
|
|
13
|
-
import { loadWorkflowFile,
|
|
13
|
+
import { loadWorkflowFile, resolveWorkflowPath, readWorkflowModeConfig, type LoadedWorkflow } from "../workflow/loader.ts"
|
|
14
14
|
import { createInitialWorkItem, isBlockingSeverity, isTerminalPhase, recommendForItem, resetInternalRetryCount, adjudicateStep, clearStepTags } from "../workflow/engine.ts"
|
|
15
15
|
import { renderWorkflowStatusView } from "../workflow/status.ts"
|
|
16
16
|
import { taskChildrenOf } from "../task-children.ts"
|
|
@@ -131,11 +131,17 @@ function firstUnpassedReviewStep(item: WorkItem, workflow: LoadedWorkflow): stri
|
|
|
131
131
|
* - review:review/verify_*(增量合并——已 passed 的审查标记保留、failed 重置为 pending,currentStep
|
|
132
132
|
* 前移到第一个未全 passed 的子层;review_layer 决定强制前置哪些子层)、analyze+implement 已 passed、
|
|
133
133
|
* task children 保留既有进度(无则 done——review 恢复时子任务应视为已验证,否则 G21 remaining 会让 implement 无法提交)
|
|
134
|
+
* simple 分支(mode === "simple",3.2):无 analyze / verify_* step,task_analysis / dev_impl 均落
|
|
135
|
+
* in_progress/implement(task_analysis 重置 task children 全 todo,dev_impl 保留既有进度);review 落
|
|
136
|
+
* review/quality_review——implement 置 passed、quality_review 的 failed tag 删除回 pending(passed 保留)、
|
|
137
|
+
* task children 缺省 done;reset_steps / review_layer 参数在 simple 下接受但空操作(值域校验不变,
|
|
138
|
+
* simple 无对应 verify step 可操作,文档标注无效)。
|
|
134
139
|
*/
|
|
135
140
|
function applyRecoveryState(
|
|
136
141
|
item: WorkItem,
|
|
137
142
|
recovery: InitParams["recovery"],
|
|
138
143
|
parsedTasks: ParsedTask[],
|
|
144
|
+
mode?: WorkflowMode,
|
|
139
145
|
): void {
|
|
140
146
|
// 恢复重建为已知状态后清除残留推进阻塞原因,避免 orchestrator 视图展示过期信息
|
|
141
147
|
delete item.metadata["_advance_block_reason"]
|
|
@@ -143,6 +149,37 @@ function applyRecoveryState(
|
|
|
143
149
|
resetInternalRetryCount(item)
|
|
144
150
|
// 清除检查点标记残留:恢复重建为已知状态后 _checkpoint 已无意义(checkpoint 态属于中断中的 step)。
|
|
145
151
|
delete item.metadata["_checkpoint"]
|
|
152
|
+
if (mode === "simple") {
|
|
153
|
+
const phase = recovery?.phase
|
|
154
|
+
if (!phase || phase === "task_analysis" || phase === "dev_impl") {
|
|
155
|
+
// simple 无 analyze:无 recovery(全新初始化)/ task_analysis / dev_impl 均落 in_progress/implement。
|
|
156
|
+
// tags 整体重置——不残留 implement:openspec-developer passed(否则 dev 不会重派,恢复失去意义)。
|
|
157
|
+
item.phase = "in_progress"
|
|
158
|
+
item.currentStep = "implement"
|
|
159
|
+
item.tags = {}
|
|
160
|
+
if (!phase || phase === "task_analysis") {
|
|
161
|
+
// 重置 task children 全 todo(全新开始)
|
|
162
|
+
syncTaskChildren(item, parsedTasks, { forceOpen: true })
|
|
163
|
+
} else {
|
|
164
|
+
// dev_impl 保留既有进度(无则 todo)
|
|
165
|
+
syncTaskChildren(item, parsedTasks, { defaultStatus: "todo" })
|
|
166
|
+
}
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
// review 分支:恢复进 review 时 implement 必然已通过
|
|
170
|
+
item.phase = "review"
|
|
171
|
+
item.tags["implement:openspec-developer"] = "passed"
|
|
172
|
+
// quality_review 的 failed tag 删除回 pending(passed 保留——已审查通过无需重跑)
|
|
173
|
+
for (const key of Object.keys(item.tags)) {
|
|
174
|
+
if (key.startsWith("quality_review:")) {
|
|
175
|
+
if (item.tags[key] !== "passed") delete item.tags[key]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// reset_steps / review_layer 在 simple 下接受但空操作(值域校验在 assertValidRecovery 不变)
|
|
179
|
+
item.currentStep = "quality_review"
|
|
180
|
+
syncTaskChildren(item, parsedTasks, { defaultStatus: "done" })
|
|
181
|
+
return
|
|
182
|
+
}
|
|
146
183
|
const phase = recovery?.phase
|
|
147
184
|
if (!phase || phase === "task_analysis") {
|
|
148
185
|
item.phase = "todo"
|
|
@@ -181,8 +218,9 @@ function applyRecoveryState(
|
|
|
181
218
|
if (recovery?.review_layer === "quality") {
|
|
182
219
|
item.tags["verify_task:openspec-reviewer-task"] = "passed"
|
|
183
220
|
}
|
|
184
|
-
// currentStep 前移:显式指向第一个未全 passed 的 review step(已全 passed
|
|
185
|
-
|
|
221
|
+
// currentStep 前移:显式指向第一个未全 passed 的 review step(已全 passed 的子层跳过)。
|
|
222
|
+
// 按 mode 选择 workflow 文件:simple 模式 review 阶段仅 quality_review 一个 step。
|
|
223
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath({ mode }))
|
|
186
224
|
item.currentStep = firstUnpassedReviewStep(item, workflow)
|
|
187
225
|
// 全 passed 收口 done:三个 review step 全 passed 时,仅当 task children 全部终态才收口;
|
|
188
226
|
// 存在未终态 task child 则停在 verify_quality(recommendForItem 会返回 blocked 而非 terminal,安全)。
|
|
@@ -293,6 +331,10 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
293
331
|
workItems: [],
|
|
294
332
|
createdAt: new Date().toISOString(),
|
|
295
333
|
updatedAt: new Date().toISOString(),
|
|
334
|
+
// 新建 state 时固化模式:读取 <worktree>/openspec/workflow.yaml(缺失视为 full,
|
|
335
|
+
// 值域外 / YAML 解析失败抛错);state 已存在(recovery/重复初始化/切换任务组)不读配置,
|
|
336
|
+
// 沿用 state 既有 mode;旧 state 缺 mode 由消费端读时兜底 full,不写回。
|
|
337
|
+
mode: readWorkflowModeConfig(ctx.worktree),
|
|
296
338
|
}
|
|
297
339
|
} else {
|
|
298
340
|
state.baseBranch = state.baseBranch || baseBranch
|
|
@@ -328,7 +370,15 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
328
370
|
description: group.name,
|
|
329
371
|
labels: ["openspec-change"],
|
|
330
372
|
})
|
|
331
|
-
item.
|
|
373
|
+
// 新建 item 初始 step 模式感知(3.1):simple 无 analyze step,初始 phase=in_progress、
|
|
374
|
+
// currentStep=implement(执行边界默认整个 worktree),否则 phaseStepMismatch 会拒绝执行;
|
|
375
|
+
// full 保持 todo/analyze 既有语义。
|
|
376
|
+
if (state.mode === "simple") {
|
|
377
|
+
item.phase = "in_progress"
|
|
378
|
+
item.currentStep = "implement"
|
|
379
|
+
} else {
|
|
380
|
+
item.currentStep = "analyze"
|
|
381
|
+
}
|
|
332
382
|
syncTaskChildren(item, groupTasks, { defaultStatus: "todo" })
|
|
333
383
|
refreshMeta(item)
|
|
334
384
|
state.workItems.push(item)
|
|
@@ -382,7 +432,7 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
382
432
|
item.metadata["base_ref"] = null
|
|
383
433
|
}
|
|
384
434
|
|
|
385
|
-
applyRecoveryState(item, args.recovery, groupTasks)
|
|
435
|
+
applyRecoveryState(item, args.recovery, groupTasks, state.mode)
|
|
386
436
|
refreshMeta(item)
|
|
387
437
|
if (!existing) state.workItems.push(item)
|
|
388
438
|
}
|
|
@@ -537,7 +587,8 @@ export async function statusExecute(params: StatusParams, ctx: ToolContext): Pro
|
|
|
537
587
|
}
|
|
538
588
|
|
|
539
589
|
// 单轨:一律由工作流引擎推荐(recommendForItem)渲染动态视图,按调用者角色分流
|
|
540
|
-
|
|
590
|
+
// 按 state.mode 选择 workflow 文件(simple → task-simple.yaml;旧 state 缺 mode 兜底 full)
|
|
591
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state))
|
|
541
592
|
const rec = recommendForItem(item, workflow)
|
|
542
593
|
const tg = findTaskGroup(state, state.taskGroupId)
|
|
543
594
|
// 主仓库 openspec 污染诊断(56ddfe9 意图):编排者分派视图展示主仓库污染,供编排者人工核对
|
|
@@ -637,7 +688,7 @@ export async function setUnattendedExecute(params: UnattendedParams, ctx: ToolCo
|
|
|
637
688
|
assertOrchestrator(ctx, "opx_orch_set_unattended")
|
|
638
689
|
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
639
690
|
if (!state) throw new Error("编排会话未初始化。请先调用 opx_orch_init。")
|
|
640
|
-
// enabled 缺省按 schema 声明 default=true
|
|
691
|
+
// enabled 缺省按 schema 声明 default=true 兜底(透传 input 不应用 zod default)
|
|
641
692
|
state.unattended = params.enabled ?? true
|
|
642
693
|
await writeState(ctx.worktree, state)
|
|
643
694
|
const status = state.unattended ? "开启" : "关闭"
|
package/src/core/tools/submit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OrchestrateState } from "../types.ts"
|
|
2
2
|
import type { ToolContext, AgentSubmitParams } from "./types.ts"
|
|
3
3
|
import type { WorkItem, Severity, StepConfig } from "../workflow/types.ts"
|
|
4
|
-
import { loadWorkflowFile,
|
|
4
|
+
import { loadWorkflowFile, resolveWorkflowPath, type LoadedWorkflow } from "../workflow/loader.ts"
|
|
5
5
|
import {
|
|
6
6
|
submitForStep, adjudicateExempt, adjudicateRecheck, assertSubmitRouting, chainPassAdvance,
|
|
7
7
|
} from "../workflow/submit.ts"
|
|
@@ -15,7 +15,7 @@ import { agentToReviewDimension, agentToReviewLayer, readIssueSource } from "../
|
|
|
15
15
|
import { REVIEW_DIMENSIONS } from "../types.ts"
|
|
16
16
|
import type { Dimension } from "../types.ts"
|
|
17
17
|
import { taskChildrenOf, taskChildById, normalizeTaskChildIds, taskListOf, issueChildrenOf } from "../task-children.ts"
|
|
18
|
-
import { markTaskGroupCheckboxesComplete, reconcileMainPollution, getCurrentHead } from "../git.ts"
|
|
18
|
+
import { markTaskGroupCheckboxesComplete, reconcileMainPollution, getCurrentHead, isWorktreeClean } from "../git.ts"
|
|
19
19
|
import { assertIssueFilesWithin } from "../paths.ts"
|
|
20
20
|
import {
|
|
21
21
|
readStateByWorktree, writeState, getLockPath, acquireLock, releaseLock,
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
assertStructuredSkipReasons, uncoveredMustDo, isValidSkipData, SKIP_REASON_FORMAT,
|
|
26
26
|
} from "./gate.ts"
|
|
27
27
|
|
|
28
|
-
/** 读取
|
|
29
|
-
const loadTaskWorkflow = () => loadWorkflowFile(
|
|
28
|
+
/** 按 state.mode 读取 workflow 配置(simple → task-simple.yaml,否则 task.yaml),进程内按路径缓存。 */
|
|
29
|
+
const loadTaskWorkflow = (state: OrchestrateState) => loadWorkflowFile(resolveWorkflowPath(state))
|
|
30
30
|
|
|
31
31
|
/** issue id 归一化:去 # 前缀(review 工具按 tg.issue 序号引用时可能带 #)。 */
|
|
32
32
|
function normalizeIssueId(id: string): string {
|
|
@@ -336,6 +336,9 @@ function mergeBoundaryInto(item: WorkItem, expansion: NonNullable<AgentSubmitPar
|
|
|
336
336
|
* - verify_tool:本次 new_children 含 Low+,或存在未终态的 Low+ tool 报源层阻塞 child
|
|
337
337
|
* - verify_quality:本次 new_children 含 Low+ 且维度属于当前提交 agent,或存在未终态的 Low+ quality 报源层
|
|
338
338
|
* 阻塞 child 且 dimension 属于当前提交 agent 维度(新报与遗留理由均按维度过滤,F3;报源层由 source 反推)
|
|
339
|
+
* - quality_review(simple 合并审查):本次 new_children 含 Low+,或存在未终态的 Low+ quality 报源层阻塞
|
|
340
|
+
* child(不按维度过滤——openspec-reviewer 无固定维度,对全部质量层负责;按 agentToReviewDimension 过滤
|
|
341
|
+
* 会对该身份恒为 undefined 导致 failed 提交死锁)
|
|
339
342
|
* - verify_cleanup:本次 new_children 含 Low+,或存在未终态的 Low+ 阻塞 child(不按维度/报源层过滤——
|
|
340
343
|
* 收尾层须对全部残留阻塞负责,任何层的遗留阻塞都构成不通过理由)
|
|
341
344
|
* 理由判定在 dedupeNewChildren 之后调用(F4):传入的 newChildren 为已去重的 accepted,重复新报不构成理由。
|
|
@@ -364,6 +367,16 @@ function assertFailedHasReason(
|
|
|
364
367
|
} else if (stepId === "verify_cleanup") {
|
|
365
368
|
layerName = "收尾层"
|
|
366
369
|
hasReason = hasNewBlocking || existingBlocking.length > 0
|
|
370
|
+
} else if (stepId === "quality_review") {
|
|
371
|
+
// simple 合并审查 step:审查者(openspec-reviewer)不归属固定质量维度,理由判定不按
|
|
372
|
+
// agentToReviewDimension 过滤(该值对 openspec-reviewer 为 undefined,按维度过滤会导致任何
|
|
373
|
+
// failed 提交都报「不存在未解决的阻塞 issue」死锁)。本次新报含 Low+ issue,或存在 quality
|
|
374
|
+
// 报源(sourcePhase === "quality")未终态阻塞 issue,均构成不通过理由——simple 审查者对
|
|
375
|
+
// 全部质量层负责,与 full 模式 verify_quality 各维度分支互不影响。
|
|
376
|
+
layerName = "AI 审查层"
|
|
377
|
+
hasReason =
|
|
378
|
+
hasNewBlocking ||
|
|
379
|
+
existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "quality")
|
|
367
380
|
} else {
|
|
368
381
|
const dimension = agentToReviewDimension(agent)
|
|
369
382
|
layerName = dimension ? `AI 审查层(${dimension})` : "AI 审查层"
|
|
@@ -488,7 +501,7 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
488
501
|
try {
|
|
489
502
|
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
490
503
|
if (!state) throw new Error("编排会话未初始化。请先调用 opx_orch_init。")
|
|
491
|
-
const workflow = loadTaskWorkflow()
|
|
504
|
+
const workflow = loadTaskWorkflow(state)
|
|
492
505
|
const item = resolveTaskWorkItem(state)
|
|
493
506
|
|
|
494
507
|
if (params.checkpoint_decision) {
|
|
@@ -673,6 +686,18 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
673
686
|
} else if (stepPhase === "todo") {
|
|
674
687
|
handleAnalyzeParams(item, params)
|
|
675
688
|
} else if (stepPhase === "in_progress") {
|
|
689
|
+
// simple 模式 implement 提交强检查(2.2):工作区必须无未提交内容,不干净直接拒绝并提示先 commit。
|
|
690
|
+
// simple 无 analyze 环节、执行边界默认整个 worktree,dev 提交即收尾合并的载体,未提交内容会
|
|
691
|
+
// 让收尾裸合并携带脏状态;检查在一切参数处理与状态写入之前,拒绝时零状态变更(含内存态)。
|
|
692
|
+
// 收尾门禁(completeTaskGroupExecute 的 worktree 干净检查)仍原样保留作最终兜底。
|
|
693
|
+
if (state.mode === "simple" && params.step_id === "implement") {
|
|
694
|
+
const clean = await isWorktreeClean(wtPath)
|
|
695
|
+
if (!clean) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
"simple 模式 implement 提交被拒绝:工作区存在未提交内容。\n请先 git commit 提交全部变更后再提交 implement。"
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
676
701
|
handleImplementParams(item, params)
|
|
677
702
|
}
|
|
678
703
|
|
package/src/core/tools/types.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface ToolContext {
|
|
|
5
5
|
agent: string
|
|
6
6
|
/** 编排视角角色判定:true 表示调用者承担编排者职责(各 agent 主代理),替代 agent 名硬编码 openspec-orchestrator。 */
|
|
7
7
|
orchestrator?: boolean
|
|
8
|
-
/** 调用者 agent 身份是否显式声明:MCP 形态下为是否携带 `_agent`
|
|
8
|
+
/** 调用者 agent 身份是否显式声明:MCP 形态下为是否携带 `_agent` 参数。
|
|
9
9
|
* false 且落入编排视角时,opx_status 视图渲染补传 `_agent` 的身份提示(MCP 子代理首查死锁兜底)。 */
|
|
10
10
|
identityDeclared?: boolean
|
|
11
11
|
remote?: boolean
|
package/src/core/types.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type OrchestrateStatus = "not_started" | "in_progress" | "completed"
|
|
|
27
27
|
export type DimensionVerdict = "pending" | "passed" | "failed"
|
|
28
28
|
export type QualityLayerProgress = Record<ReviewDimension, DimensionVerdict>
|
|
29
29
|
|
|
30
|
+
/** 流程模式:full(默认,analyze → implement → 三重审查+收尾验证)或 simple(implement → quality_review → done)。 */
|
|
31
|
+
export type WorkflowMode = "full" | "simple"
|
|
32
|
+
|
|
30
33
|
export interface ExecutionBoundary {
|
|
31
34
|
allowed_directories: string[]
|
|
32
35
|
allowed_packages: string[]
|
|
@@ -132,4 +135,7 @@ export interface OrchestrateState {
|
|
|
132
135
|
createdAt: string
|
|
133
136
|
updatedAt: string
|
|
134
137
|
unattended?: boolean
|
|
138
|
+
/** 变更开始时固化的流程模式(opx_orch_init 从 <repo>/openspec/workflow.yaml 读取后写入)。
|
|
139
|
+
* 缺失即旧变更,一律按 full 处理(读时兜底,不写回);已存在的 state 不再被配置改动覆盖。 */
|
|
140
|
+
mode?: WorkflowMode
|
|
135
141
|
}
|
|
@@ -12,11 +12,14 @@ import { reviewLayerFromMetadata, readIssueSource } from "../constants.ts"
|
|
|
12
12
|
|
|
13
13
|
const TERMINAL_PHASES: WorkItemPhase[] = ["done", "cancelled"]
|
|
14
14
|
|
|
15
|
-
/** review
|
|
15
|
+
/** review 验证 step → 其可裁定的 issue 报源层(层感知门禁归因依据)。
|
|
16
|
+
* quality_review(simple 合并审查 step)按 quality 层口径——由 simple 审查者(openspec-reviewer)上报的
|
|
17
|
+
* issue 归入 quality 层,blocking issue 终态才可 pass(与 full 模式 verify_quality 语义一致)。 */
|
|
16
18
|
export const REVIEW_STEP_TO_LAYER: Record<string, "tool" | "task" | "quality"> = {
|
|
17
19
|
verify_tool: "tool",
|
|
18
20
|
verify_task: "task",
|
|
19
21
|
verify_quality: "quality",
|
|
22
|
+
quality_review: "quality",
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** 读取 issue child 的报源层(source 反推,source_phase 仅作历史兜底,缺省 tool)。 */
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from "./types.ts"
|
|
14
14
|
import { WORK_ITEM_PHASES, stepAgentIds } from "./types.ts"
|
|
15
15
|
import { DIMENSION_AGENT_MAP } from "../constants.ts"
|
|
16
|
+
import type { OrchestrateState, WorkflowMode } from "../types.ts"
|
|
16
17
|
|
|
17
18
|
const SPECIAL_TRANSITIONS = ["done", "halt"] as const
|
|
18
19
|
const PHASE_NAMES = new Set<string>(WORK_ITEM_PHASES)
|
|
@@ -23,30 +24,76 @@ export interface LoadedWorkflow extends WorkflowConfig {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* 定位
|
|
27
|
+
* 定位 assets/workflows/<fileName>:源码(src/core/workflow/ 上溯 3 级=仓库根)、插件包 dist 形态
|
|
27
28
|
* (dist/zcode-plugin/.mcp-server/ 上溯 1 级=插件根)与 ZCode 缓存嵌套形态
|
|
28
29
|
* (cache/<marketplace>/<plugin>/<version>/.mcp-server/ 上溯 1 级=版本目录)部署深度不同,
|
|
29
|
-
*
|
|
30
|
+
* 故从模块所在目录逐级上溯探测,首个命中即采用(首中即用)。
|
|
30
31
|
* bundle 单文件合并后 import.meta.url 指向部署位置(cli.mjs),源码形态指向本文件自身,两者自然成立。
|
|
31
32
|
*/
|
|
32
|
-
|
|
33
|
+
function resolveWorkflowFilePath(moduleUrl: string, fileName: string): string {
|
|
33
34
|
const startDir = dirname(fileURLToPath(moduleUrl))
|
|
34
35
|
let dir = startDir
|
|
35
36
|
let probed = 0
|
|
36
37
|
for (;;) {
|
|
37
|
-
const candidate = pathResolve(dir, "assets", "workflows",
|
|
38
|
+
const candidate = pathResolve(dir, "assets", "workflows", fileName)
|
|
38
39
|
if (existsSync(candidate)) return candidate
|
|
39
40
|
probed++
|
|
40
41
|
const parent = dirname(dir)
|
|
41
42
|
if (parent === dir) break // 到达文件系统根
|
|
42
43
|
dir = parent
|
|
43
44
|
}
|
|
44
|
-
throw new Error(`workflow 文件缺失:从 ${startDir} 上溯 ${probed} 级未找到 assets/workflows
|
|
45
|
+
throw new Error(`workflow 文件缺失:从 ${startDir} 上溯 ${probed} 级未找到 assets/workflows/${fileName}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveTaskWorkflowPath(moduleUrl: string): string {
|
|
49
|
+
return resolveWorkflowFilePath(moduleUrl, "task.yaml")
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
// 模块加载期求值,workflow 缺失即启动抛错(fail-fast):opx_* 工具全部依赖 workflow,缺失时
|
|
48
53
|
// server 无可用性,启动即抛比首次调用才报错更早且可读(可读错误替代原生 ENOENT)。
|
|
49
54
|
export const TASK_WORKFLOW_PATH = resolveTaskWorkflowPath(import.meta.url)
|
|
55
|
+
/** simple 流程文件(implement → quality_review → done),与 task.yaml 同目录随插件 bundle 分发。 */
|
|
56
|
+
export const SIMPLE_WORKFLOW_PATH = resolveWorkflowFilePath(import.meta.url, "task-simple.yaml")
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 按 state.mode 选择 workflow 文件:simple → task-simple.yaml,其余(full 或旧 state 缺 mode)→ task.yaml。
|
|
60
|
+
* 旧 state 无 mode 字段一律按 full 处理(读时兜底,不写回),与 D2 固化语义一致。
|
|
61
|
+
*/
|
|
62
|
+
export function resolveWorkflowPath(state: Pick<OrchestrateState, "mode">): string {
|
|
63
|
+
return state.mode === "simple" ? SIMPLE_WORKFLOW_PATH : TASK_WORKFLOW_PATH
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 项目级模式配置文件路径(<worktree>/openspec/workflow.yaml,与 openspec/config.yaml 同级)。 */
|
|
67
|
+
const WORKFLOW_MODE_CONFIG_FILE = "openspec/workflow.yaml"
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 读取项目级流程模式配置(<worktree>/openspec/workflow.yaml):
|
|
71
|
+
* - 文件缺失 → "full"(缺省)
|
|
72
|
+
* - YAML 解析失败 → 抛错
|
|
73
|
+
* - 顶层非 YAML 映射对象 → 抛错
|
|
74
|
+
* - mode 字段缺失 → "full"(缺省)
|
|
75
|
+
* - mode 值域外(非 full/simple)→ 抛错
|
|
76
|
+
* 仅由 opx_orch_init 新建 state 时调用;已存在的 state 不读配置,沿用既有 mode。
|
|
77
|
+
*/
|
|
78
|
+
export function readWorkflowModeConfig(worktree: string): WorkflowMode {
|
|
79
|
+
const configPath = pathResolve(worktree, WORKFLOW_MODE_CONFIG_FILE)
|
|
80
|
+
if (!existsSync(configPath)) return "full"
|
|
81
|
+
let raw: unknown
|
|
82
|
+
try {
|
|
83
|
+
raw = yaml.load(readFileSync(configPath, "utf-8"))
|
|
84
|
+
} catch (err) {
|
|
85
|
+
fail(`${WORKFLOW_MODE_CONFIG_FILE} 解析失败:${(err as Error).message}`)
|
|
86
|
+
}
|
|
87
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
88
|
+
fail(`${WORKFLOW_MODE_CONFIG_FILE} 配置非法:顶层必须是 YAML 映射对象`)
|
|
89
|
+
}
|
|
90
|
+
const mode = (raw as { mode?: unknown }).mode
|
|
91
|
+
if (mode === undefined) return "full"
|
|
92
|
+
if (mode !== "full" && mode !== "simple") {
|
|
93
|
+
fail(`${WORKFLOW_MODE_CONFIG_FILE} 配置非法:mode 值域为 full/simple,收到 ${JSON.stringify(mode)}`)
|
|
94
|
+
}
|
|
95
|
+
return mode
|
|
96
|
+
}
|
|
50
97
|
|
|
51
98
|
const workflowFileCache = new Map<string, LoadedWorkflow>()
|
|
52
99
|
|