@ifrankwang/openspec-agents 0.140.0 → 0.142.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/.mcp-server/cli.mjs +36 -6
- package/README.md +1 -1
- package/assets/skills/openspec-standard/SKILL.md +2 -2
- package/assets/skills/openspec-standard/templates/openspec/config.yaml +2 -1
- package/assets/skills/openspec-standard/templates/schemas/spec-driven-standard/README.md +14 -8
- package/assets/skills/openspec-standard/templates/schemas/spec-driven-standard/schema.yaml +76 -18
- package/assets/skills/openspec-standard/templates/schemas/spec-driven-standard/templates/clarify.md +11 -3
- package/assets/skills/openspec-standard/templates/schemas/spec-driven-standard/templates/proposal.md +12 -2
- package/package.json +1 -1
- package/src/adapters/mcp-common/server.ts +1 -1
- package/src/core/git.ts +24 -1
- package/src/core/tools/lifecycle.ts +36 -10
package/.mcp-server/cli.mjs
CHANGED
|
@@ -21677,6 +21677,19 @@ async function listBranchDriftFiles(worktree, sourceBranch, targetBranch) {
|
|
|
21677
21677
|
return null;
|
|
21678
21678
|
return parseDiffNameOnly(r.stdout);
|
|
21679
21679
|
}
|
|
21680
|
+
async function isVersionOnlyDrift(worktree, sourceBranch, targetBranch, filePath) {
|
|
21681
|
+
const r = await runGitChecked(worktree, ["diff", "--no-renames", `${sourceBranch}...${targetBranch}`, "--", filePath]);
|
|
21682
|
+
if (!r.success)
|
|
21683
|
+
return null;
|
|
21684
|
+
for (const line of r.stdout.split(`
|
|
21685
|
+
`)) {
|
|
21686
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
21687
|
+
continue;
|
|
21688
|
+
if ((line.startsWith("+") || line.startsWith("-")) && !/^[+-]\s*"version"\s*:/.test(line))
|
|
21689
|
+
return false;
|
|
21690
|
+
}
|
|
21691
|
+
return true;
|
|
21692
|
+
}
|
|
21680
21693
|
async function isLocalBranch(worktree, branch) {
|
|
21681
21694
|
const r = await runGitChecked(worktree, ["rev-parse", "--verify", `refs/heads/${branch}`]);
|
|
21682
21695
|
return r.success;
|
|
@@ -29093,10 +29106,27 @@ function renderReviewIssueSummary(item) {
|
|
|
29093
29106
|
`);
|
|
29094
29107
|
}
|
|
29095
29108
|
var DOCUMENTATION_EXTENSIONS = [".md", ".mdx", ".markdown", ".rst", ".adoc"];
|
|
29096
|
-
|
|
29109
|
+
var DRIFT_NEUTRAL_PATH_PREFIXES = ["openspec/"];
|
|
29110
|
+
async function isHarmlessDrift(worktree, sourceBranch, targetBranch, files) {
|
|
29097
29111
|
if (files === null)
|
|
29098
29112
|
return false;
|
|
29099
|
-
|
|
29113
|
+
const versionCandidates = [];
|
|
29114
|
+
for (const f of files) {
|
|
29115
|
+
if (DOCUMENTATION_EXTENSIONS.some((ext) => f.endsWith(ext)))
|
|
29116
|
+
continue;
|
|
29117
|
+
if (DRIFT_NEUTRAL_PATH_PREFIXES.some((p) => f.startsWith(p)))
|
|
29118
|
+
continue;
|
|
29119
|
+
if (f === "package.json" || f.endsWith("/package.json")) {
|
|
29120
|
+
versionCandidates.push(f);
|
|
29121
|
+
continue;
|
|
29122
|
+
}
|
|
29123
|
+
return false;
|
|
29124
|
+
}
|
|
29125
|
+
for (const f of versionCandidates) {
|
|
29126
|
+
if (await isVersionOnlyDrift(worktree, sourceBranch, targetBranch, f) !== true)
|
|
29127
|
+
return false;
|
|
29128
|
+
}
|
|
29129
|
+
return true;
|
|
29100
29130
|
}
|
|
29101
29131
|
function rollbackToCleanupStep(item, workflow) {
|
|
29102
29132
|
if (!workflow.stepMap.has("verify_cleanup"))
|
|
@@ -29197,7 +29227,7 @@ ${checkboxWarning}` : doneMessage;
|
|
|
29197
29227
|
await ensureChangeBranch(ctx.worktree, branchName, mergeTarget);
|
|
29198
29228
|
if (!await isAncestor(ctx.worktree, mergeTarget, branchName)) {
|
|
29199
29229
|
const driftFiles = await listBranchDriftFiles(ctx.worktree, branchName, mergeTarget);
|
|
29200
|
-
if (!
|
|
29230
|
+
if (!await isHarmlessDrift(ctx.worktree, branchName, mergeTarget, driftFiles)) {
|
|
29201
29231
|
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
29202
29232
|
const rolledBack = rollbackToCleanupStep(item, workflow);
|
|
29203
29233
|
if (rolledBack)
|
|
@@ -29210,7 +29240,7 @@ ${checkboxWarning}` : doneMessage;
|
|
|
29210
29240
|
}
|
|
29211
29241
|
const files = driftFiles ?? [];
|
|
29212
29242
|
const driftList = files.length > 10 ? `${files.slice(0, 10).join(", ")} 等 ${files.length} 个` : files.join(", ");
|
|
29213
|
-
docDriftNote = `-
|
|
29243
|
+
docDriftNote = `- **基准漂移(无代码语义)**: 基准分支 \`${mergeTarget}\` 相对变更分支切出点净变化 ${files.length} 个文件` + (files.length > 0 ? `(${driftList})` : "") + ",均为文档、openspec 规划路径或仅版本号变更,不影响代码语义,未回退收尾验证直接合并。";
|
|
29214
29244
|
}
|
|
29215
29245
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
29216
29246
|
if (mergeResult.blockedMessage) {
|
|
@@ -30384,7 +30414,7 @@ var TOOL_SPECS = {
|
|
|
30384
30414
|
execute: (args, ctx) => statusExecute({ change_id: args.change_id }, ctx)
|
|
30385
30415
|
},
|
|
30386
30416
|
opx_orch_complete_task_group: {
|
|
30387
|
-
description: "完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch
|
|
30417
|
+
description: "完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch(漂移无害——文档、openspec 规划路径或仅版本号变更的 package.json——时直接合并收口;漂移含任一其他文件或文本冲突时回退到收尾验证 verify_cleanup 并返回 blocked),成功后销毁 worktree 并删分支。须在收尾验证(verify_cleanup)通过后调用。主仓库本地改动文件与合并写入文件重合、或存在部分暂存文件时中止并返回 blocked(保留 worktree/分支);主仓库无关脏文件不阻塞合并。",
|
|
30388
30418
|
schema: completeTaskGroupSchema,
|
|
30389
30419
|
execute: (args, ctx) => completeTaskGroupExecute(args, ctx)
|
|
30390
30420
|
},
|
|
@@ -30429,7 +30459,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
30429
30459
|
}
|
|
30430
30460
|
} catch {}
|
|
30431
30461
|
}
|
|
30432
|
-
var PKG_VERSION = "0.
|
|
30462
|
+
var PKG_VERSION = "0.142.0";
|
|
30433
30463
|
function buildMcpServer(worktree, opts = {}) {
|
|
30434
30464
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
30435
30465
|
for (const [name, spec] of Object.entries(TOOL_SPECS)) {
|
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ npm install -D @ifrankwang/openspec-agents
|
|
|
92
92
|
1. **输入**:项目中的 OpenSpec 变更规范(变更说明、设计、任务清单、验收规格)。
|
|
93
93
|
2. **分派**:主智能体把任务交给专职智能体——实现者负责写代码,审查者负责检查,质量把关者负责多维验证。
|
|
94
94
|
3. **门禁**:每一步的结论通过检查后才进入下一步;不通过的结论自动回到实现环节修复。
|
|
95
|
-
4. **收尾合并**:任务组完成只做门禁校验并标记变更范围,不合并、不销毁 worktree;最后一个任务组收口时把 change 分支一次性合并回基准分支,随后销毁 worktree 并删除 change
|
|
95
|
+
4. **收尾合并**:任务组完成只做门禁校验并标记变更范围,不合并、不销毁 worktree;最后一个任务组收口时把 change 分支一次性合并回基准分支,随后销毁 worktree 并删除 change 分支。合并前若基准分支已推进,先做基线漂移检查并比对基准侧净变化内容:无害漂移不影响代码语义,直接合并收口——无害指三类之一:文档后缀变化(md / mdx / markdown / rst / adoc)、openspec/ 规划路径下的文档(提案/设计/任务/spec,不限后缀)、package.json 且变更内容仅版本号行;含任一其他文件或文本冲突才把该任务组回退到收尾验证环节(合并基准分支最新代码并回归复验)后再收口——验收仅在实质漂移时动态发生,正常路径零额外环节。
|
|
96
96
|
5. **部署注意事项**:change 的全部任务组完成后,主智能体按状态视图指引汇总部署注意事项(规划文档提取 + 开发者实施补充点,含依赖清单/构建配置变更等合并后需在主仓库重装依赖、重新构建的事项)向用户汇报。全程留痕可审计。
|
|
97
97
|
|
|
98
98
|
## 独立审查会话
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: openspec-standard
|
|
3
|
-
description: OpenSpec 提案标准初始化与升级——把标准版提案规则(openspec/config.yaml + spec-driven-standard schema 全套)安装到当前项目,统一后续提 change
|
|
3
|
+
description: OpenSpec 提案标准初始化与升级——把标准版提案规则(openspec/config.yaml + spec-driven-standard schema 全套)安装到当前项目,统一后续提 change 的质量门槛(关联场景清点、契约变更详列、人话实施逻辑、结构化方案摘要、动笔前访谈与用户确认)。当用户要求初始化、升级或标准化 openspec 提案标准/提案规范时使用。
|
|
4
4
|
capabilities: ["openspec-standard"]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 职责
|
|
8
8
|
|
|
9
|
-
把本 skill 目录 `templates/` 下的标准文件安装到当前项目的 `openspec/` 目录,使该项目此后用 openspec 提 change 时自动遵循统一标准:关联场景清点(共享处理逻辑的变更须与其全部消费入口同一 change
|
|
9
|
+
把本 skill 目录 `templates/` 下的标准文件安装到当前项目的 `openspec/` 目录,使该项目此后用 openspec 提 change 时自动遵循统一标准:关联场景清点(共享处理逻辑的变更须与其全部消费入口同一 change 统一适配,同一数据多条落库路径的校验一致性为访谈必问分支)、契约变更详列、实施逻辑人话分步(proposal 单列实施逻辑小节,禁代码级名词)、结构化方案摘要(访谈穷尽后才给出、以普通消息确认)、动笔前访谈门与用户确认门、任务组实质变更主体等。幂等:重跑即把标准升级到插件当前版本,项目自定义内容按下方合并策略保留。
|
|
10
10
|
|
|
11
11
|
## 前置检查
|
|
12
12
|
|
|
@@ -14,7 +14,8 @@ rules:
|
|
|
14
14
|
proposal:
|
|
15
15
|
- "关联场景清点(强制):proposal 动笔前必须完成「关联场景清点」并以代码检索为证据记入 clarify.md 的「关联场景清点(Related-Surface Survey)」小节:枚举本变更触及能力的全部消费方与关联面——功能入口(API 端点、导入/导出入口、定时任务、消息消费、前端调用等)、共享处理逻辑(被多个入口复用的处理链路)、数据模型与数据流下游(表/字段/事件/导出产物的读写双方),每条附代码位置证据(文件/类/方法)。凡共享处理逻辑发生变更,其全部消费入口必须在同一 change 内统一适配,禁止按入口分别提 change 导致共享逻辑两边割裂维护;确需拆分时必须在 proposal.md 单列「拆分边界」小节论证(每个 change 覆盖哪些入口、共享逻辑变更归属哪个 change、入口间契约),并经用户确认。proposal.md 必须单列「关联场景(Related Surface)」小节逐条列出清点结论与划界结论;泛写「无关联影响」而不附检索证据视为 proposal 未完成。"
|
|
16
16
|
- "契约变更详列(强制):凡变更涉及 DDL(建表/加列/索引/约束)或接口契约(新增/修改端点、请求响应结构、错误码),proposal.md 必须单列「契约变更点」小节逐条列出——DDL 逐表逐列给全表名、列名、类型、可空性、默认值与用途;API 逐端点给全方法+路径、请求/响应结构变化、新增/修改标记与兼容性影响。泛写「有契约变更」不逐条展开视为 proposal 未完成。"
|
|
17
|
-
- "动笔前访谈门(强制):proposal/specs/design/tasks 任一文档动笔前,必须已按 spec-driven-standard schema clarify
|
|
17
|
+
- "动笔前访谈门(强制):proposal/specs/design/tasks 任一文档动笔前,必须已按 spec-driven-standard schema clarify 指令的决策树访谈方法论完成对用户的访谈——把方案建模为决策树逐轮穷尽疑点:每轮将前提已落定、当前可回答的问题一次性编号抛出并逐题附推荐答案,用户答复后重算下一轮前沿,直到决策树所有分支走完、无未决疑点、无静默假设、关联场景清点每个条目均经用户划界确认(纳入/排除+理由);访谈问答全程记入 clarify.md(Open Questions / User Answers / 关联场景清点)。事实类问题必须自行查代码与 spec 求证,不抛给用户;疑点未穷尽时禁止写任何文档,不得以「需求简单」等理由跳过。访谈未穷尽(含关联场景清点尚有未经用户划界确认的条目)时同样禁止呈现方案或方案摘要请求确认——轮次中每题附的推荐答案不视为方案,不得把方案确认合并进任何一轮提问;用户中途索要方案时说明访谈未完并继续提问,不得提前收口。"
|
|
18
|
+
- "实施逻辑与方案摘要结构(强制):proposal.md 必须单列「实施逻辑(Implementation Outline)」小节,按步骤用业务语言(人话)讲清具体怎么做——数据从哪来、经过什么校验与处理、落到哪、关联场景小节划入的每条链路怎么适配;禁止出现类名/方法名/内部函数名等代码级名词(用户明确要求技术深度时除外;契约变更点小节的表名/列名/端点路径等契约事实不受此限、保持精确)。方案摘要必须按 spec-driven-standard schema clarify 指令 Plan confirmation gate 的要素清单结构化逐项展开(含人话分步实施逻辑与关联链路划界结论),两三句话的摘要视为未完成。"
|
|
18
19
|
- "动笔前用户确认门(强制):proposal/specs/design/tasks 任一文档动笔前,clarify.md 的 Architecture Direction 必须已记录用户对方案摘要的明确确认;确认必须以普通消息直接列出方案摘要要素点(要素清单见 spec-driven-standard schema clarify 指令的 Plan confirmation gate)并停下等待用户直接文字回复,禁止通过提问工具发起确认,提问工具的选项点击不视为确认;未记录时禁止写任何文档——主代理亲自写时先停下向用户补确认,生成子代理则立即终止并向编排方报告,不得以「需求简单」等理由跳过或代用户确认。"
|
|
19
20
|
specs:
|
|
20
21
|
- "数据行为需求(如\"系统 SHALL 持久化 X 含审计字段\")放 spec;物理 DDL 放 design.md。"
|
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
通用 spec-driven 工作流模板:clarify → proposal → specs → design → tasks,
|
|
4
4
|
增强三点:
|
|
5
5
|
1. **clarify 前置门槛 + 关联场景清点**:每次变更先做需求澄清与关联场景清点
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
(首轮提问前枚举变更触及能力的全部消费方——功能入口 / 共享处理逻辑 /
|
|
7
|
+
数据流下游,附代码证据;每个清点条目在访谈中经用户划界确认——纳入本
|
|
8
|
+
change 统一适配或排除附理由;同一数据存在多条落库路径时各路径校验与
|
|
9
|
+
处理逻辑的一致性为必问分支;共享处理逻辑变更须与其全部消费入口同一
|
|
10
|
+
change 统一适配);访谈穷尽后、写任何文档之前,主代理必须先向用户汇报
|
|
11
|
+
结构化方案摘要(按要素清单逐项编号展开:一句话做什么 + 是否动契约 +
|
|
12
|
+
选定方案与备选取舍 + 人话分步实施逻辑 + 关联链路划界结论 + 影响的能力
|
|
13
|
+
面)并取得用户明确确认(HARD-GATE:摘要须以普通消息呈现,禁止用提问
|
|
14
|
+
工具发起确认,访谈中的问答不视为确认;访谈未穷尽禁止呈现方案求确认,
|
|
15
|
+
轮次中每题附的推荐答案不视为方案),确认记录落在 clarify.md,后续文档
|
|
16
|
+
不得偏离。proposal.md 单列「实施逻辑」小节,以人话分步讲清怎么做(禁
|
|
17
|
+
代码级名词,契约事实除外),代码级细节归 design.md。
|
|
12
18
|
2. **主代理访谈直录 + 分级撰写 + 独立复核循环**:主代理亲自访谈,问答
|
|
13
19
|
直接记入 clarify.md(访谈阶段唯一落笔的文档,不设独立访谈文件);
|
|
14
20
|
架构方向经用户确认后,按复杂度分级产出其余文档(proposal → specs →
|
|
@@ -67,8 +73,8 @@ openspec/schemas/<schema-name>/
|
|
|
67
73
|
|
|
68
74
|
| 位置 | 承载内容 | 例子 |
|
|
69
75
|
|---|---|---|
|
|
70
|
-
| `schema.yaml` instruction | 生成方法论与文档格式要求(官方方法论为底) | 场景标题 4 个 #、SHALL/MUST、ADDED/MODIFIED/REMOVED
|
|
71
|
-
| `openspec/config.yaml` rules | **仅** schema 未覆盖的项目补充规则 | MODIFIED 终态契约表述、同语义一条需求、前置 change
|
|
76
|
+
| `schema.yaml` instruction | 生成方法论与文档格式要求(官方方法论为底) | 场景标题 4 个 #、SHALL/MUST、ADDED/MODIFIED/REMOVED、任务组格式、每组自带测试与质量门、关联场景清点维度、实施逻辑小节 |
|
|
77
|
+
| `openspec/config.yaml` rules | **仅** schema 未覆盖的项目补充规则 | MODIFIED 终态契约表述、同语义一条需求、前置 change 标注格式、契约变更详列、访谈门与用户确认门、实施逻辑与方案摘要结构 |
|
|
72
78
|
| 项目 `AGENTS.md` | 项目特化约束 | 技术栈、分层架构、数据库约定、质量门工具、方法论 |
|
|
73
79
|
|
|
74
80
|
同一规则只写一处:config.yaml rules 不重复 schema instruction 已有内容;
|
|
@@ -108,16 +108,27 @@ artifacts:
|
|
|
108
108
|
branches into the decisions that hang off it):
|
|
109
109
|
* Problem & intent: what problem is being solved? what is the success criteria?
|
|
110
110
|
* Scope: what is in/out of bounds? is the change decomposable into sub-changes?
|
|
111
|
-
* Related-surface survey (关联场景清点):
|
|
111
|
+
* Related-surface survey (关联场景清点): complete the survey BEFORE the
|
|
112
|
+
first interview round and treat it as input to the decision tree, not
|
|
113
|
+
just a documentation step. Enumerate every consumer of the
|
|
112
114
|
capability being changed, grounded in real code — functional entry points
|
|
113
115
|
(API endpoints, import/export entries, scheduled jobs, message consumers,
|
|
114
116
|
frontend calls), shared processing logic reused by multiple entry points,
|
|
115
117
|
and downstream data consumers (readers/writers of the tables, fields,
|
|
116
118
|
events, or export artifacts involved). Record each finding with code
|
|
117
119
|
evidence (file/class/method) in clarify.md's Related-Surface Survey
|
|
118
|
-
section.
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
section. Every surveyed surface becomes a decision-tree branch that
|
|
121
|
+
MUST be resolved IN the interview: for each, ask the user whether it
|
|
122
|
+
is adapted in this change or excluded with a reason, and record the
|
|
123
|
+
verdict. Consistency branches are mandatory: when the same data has
|
|
124
|
+
multiple persistence paths (e.g. import, save, and update all writing
|
|
125
|
+
the same table), whether and how their validation and processing
|
|
126
|
+
logic stay shared and consistent must be asked and decided by the
|
|
127
|
+
user — touching only the requested path while skipping the related
|
|
128
|
+
paths means the interview is NOT exhausted. When shared processing
|
|
129
|
+
logic changes, every consuming entry point must be adapted within
|
|
130
|
+
the SAME change; splitting into separate changes requires an
|
|
131
|
+
explicit boundary rationale confirmed by the user.
|
|
121
132
|
* Data model: which entities/tables/fields are involved? any new tables or columns?
|
|
122
133
|
* Business rules: which are deterministic vs non-deterministic, per the
|
|
123
134
|
project's conventions (see AGENTS.md)?
|
|
@@ -125,22 +136,50 @@ artifacts:
|
|
|
125
136
|
* Architecture direction: propose 2-3 approaches with tradeoffs and a recommendation.
|
|
126
137
|
- Round mechanics: each round asks every question whose prerequisites
|
|
127
138
|
are already settled (the whole frontier at once) via the question
|
|
128
|
-
tool, numbered, each with your recommended answer; then wait for
|
|
129
|
-
|
|
139
|
+
tool, numbered, each with your recommended answer; then wait for the
|
|
140
|
+
user's answers before the next round. A question whose answer
|
|
130
141
|
depends on one still open in this round belongs to a later round.
|
|
131
142
|
Facts are yours to find (codebase, specs, tools) - never ask the
|
|
132
143
|
user for anything you can look up yourself; the decisions are the
|
|
133
144
|
user's. Rounds continue until the frontier is empty: every branch
|
|
134
|
-
visited, no doubt left unresolved, nothing silently assumed
|
|
145
|
+
visited, no doubt left unresolved, nothing silently assumed, and
|
|
146
|
+
every related-surface survey entry carries a user-confirmed scoping
|
|
147
|
+
verdict.
|
|
148
|
+
- Sequencing prohibition: while the interview is still open (the
|
|
149
|
+
frontier is not empty, or any related-surface survey entry lacks a
|
|
150
|
+
user-confirmed verdict), do NOT present the plan or a plan summary
|
|
151
|
+
and ask the user to confirm it. The recommended answer attached to
|
|
152
|
+
each question is not the plan and must never be presented as one;
|
|
153
|
+
do not fold plan confirmation into any interview round. If the user
|
|
154
|
+
asks for the plan mid-interview, say the interview is not finished
|
|
155
|
+
yet, you may recap what has already been settled, and keep asking
|
|
156
|
+
until the frontier is empty — only then use the plan confirmation
|
|
157
|
+
gate.
|
|
135
158
|
- Challenge assumptions (yours and the user's). Reframe the problem when needed.
|
|
136
159
|
- Plan confirmation gate (HARD-GATE): once the interview rounds have
|
|
137
|
-
exhausted every open doubt (frontier empty
|
|
160
|
+
exhausted every open doubt (frontier empty — see the sequencing
|
|
161
|
+
prohibition above) and before starting
|
|
138
162
|
Phase 2 (authoring — whether by you or
|
|
139
|
-
by a generation subagent), present a
|
|
140
|
-
the user and stop for confirmation. The summary
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
by a generation subagent), present a direct plan summary to
|
|
164
|
+
the user and stop for confirmation. The summary MUST be structured:
|
|
165
|
+
number each element below and give it real substance — a
|
|
166
|
+
two-or-three-sentence blob is an invalid summary, expand it before
|
|
167
|
+
presenting. The elements:
|
|
168
|
+
1. what the change does, in one sentence;
|
|
169
|
+
2. whether it touches contracts (API parameters or DDL);
|
|
170
|
+
3. the chosen approach and its key trade-offs versus the
|
|
171
|
+
alternatives;
|
|
172
|
+
4. the implementation logic, step by step, in plain business
|
|
173
|
+
language (人话) — where the data comes from, what validation and
|
|
174
|
+
processing it goes through, where it lands, and how each related
|
|
175
|
+
path is adapted. Do NOT use code-level identifiers (class,
|
|
176
|
+
method, or internal function names) unless the user explicitly
|
|
177
|
+
asked for technical depth; contract facts (table, column, and
|
|
178
|
+
endpoint names) are exempt and stay precise;
|
|
179
|
+
5. the related-surface verdicts: for every surveyed surface,
|
|
180
|
+
adapted in this change or excluded, each with a one-line reason;
|
|
181
|
+
6. the capabilities affected.
|
|
182
|
+
Present it as a
|
|
144
183
|
plain message — do NOT use the question tool for this confirmation
|
|
145
184
|
(the question tool belongs to the interview rounds, not to this
|
|
146
185
|
confirmation) — and stop for the user's free-text reply. Interview
|
|
@@ -157,12 +196,17 @@ artifacts:
|
|
|
157
196
|
- **User Answers**: the user's responses, captured verbatim or summarized.
|
|
158
197
|
- **Related-Surface Survey**: the related-surface survey output (关联场景清点) —
|
|
159
198
|
every consuming entry point / shared processing logic / downstream
|
|
160
|
-
consumer with code evidence, and the scoping verdict for each
|
|
199
|
+
consumer with code evidence, and the scoping verdict for each —
|
|
200
|
+
every verdict confirmed by the user during the interview. Shared
|
|
161
201
|
processing logic changes must be covered together with all their
|
|
162
202
|
consuming entry points in the same change.
|
|
163
203
|
- **Architecture Direction**: the crystallized approach, key decisions,
|
|
164
204
|
alternatives considered, and the confirmed plan summary with
|
|
165
|
-
the user's explicit confirmation
|
|
205
|
+
the user's explicit confirmation — the summary structured per the
|
|
206
|
+
six elements of the plan confirmation gate (including the
|
|
207
|
+
plain-language implementation outline and the related-surface
|
|
208
|
+
verdicts), presented as a plain message only after the interview
|
|
209
|
+
was exhausted. This is the HARD-GATE output.
|
|
166
210
|
- **Initial Capabilities**: preliminary guess of which specs will be touched
|
|
167
211
|
(kebab-case). Finalized in proposal.md.
|
|
168
212
|
|
|
@@ -196,11 +240,23 @@ artifacts:
|
|
|
196
240
|
- **Related Surface (关联场景)**: the survey conclusions from clarify.md —
|
|
197
241
|
every consuming entry point / shared processing logic / downstream
|
|
198
242
|
consumer with code evidence and its scoping verdict (adapted in this
|
|
199
|
-
change, or explicitly justified out).
|
|
243
|
+
change, or explicitly justified out). The verdicts recorded here must
|
|
244
|
+
match the user-confirmed verdicts from the clarify.md interview.
|
|
245
|
+
When shared processing logic
|
|
200
246
|
changes, every consuming entry point must be adapted in THIS change;
|
|
201
247
|
splitting into separate changes requires a boundary rationale section
|
|
202
248
|
confirmed by the user. A bare "no related impact" without search
|
|
203
249
|
evidence counts as an incomplete proposal.
|
|
250
|
+
- **Implementation Outline (实施逻辑)**: how the change plays out, step by
|
|
251
|
+
step, in plain business language (人话) — where the data comes from,
|
|
252
|
+
what validation and processing it goes through, where it lands, and
|
|
253
|
+
how each related path scoped into this change (per the Related
|
|
254
|
+
Surface section above) is adapted. What Changes lists WHAT changes;
|
|
255
|
+
this section walks through HOW it plays out, as numbered steps. Same
|
|
256
|
+
language rule as the plan summary: no code-level identifiers (class,
|
|
257
|
+
method, or internal function names) unless the user explicitly asked
|
|
258
|
+
for technical depth; contract facts (table, column, and endpoint
|
|
259
|
+
names) are exempt and stay precise.
|
|
204
260
|
- **Capabilities**: Identify which specs will be created or modified:
|
|
205
261
|
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
|
|
206
262
|
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
|
|
@@ -218,8 +274,10 @@ artifacts:
|
|
|
218
274
|
behavior, so if behavior does not change, no spec should change either.
|
|
219
275
|
Do not invent a requirement just to satisfy validation.
|
|
220
276
|
|
|
221
|
-
Keep it concise (1-2 pages).
|
|
222
|
-
|
|
277
|
+
Keep it concise (1-2 pages). The proposal covers the why and the
|
|
278
|
+
plain-language how (the Implementation Outline); code-level details
|
|
279
|
+
(class/method structure, DDL design, technology-choice argumentation)
|
|
280
|
+
belong in design.md. Stay consistent with
|
|
223
281
|
clarify.md's architecture direction; if the proposal drifts from clarify,
|
|
224
282
|
update clarify first and get user confirmation.
|
|
225
283
|
|
package/assets/skills/openspec-standard/templates/schemas/spec-driven-standard/templates/clarify.md
CHANGED
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
import/export entries, scheduled jobs, message consumers, frontend calls),
|
|
23
23
|
shared processing logic reused by multiple entry points, and downstream
|
|
24
24
|
data consumers (readers/writers of the tables, fields, events, or export
|
|
25
|
-
artifacts involved).
|
|
25
|
+
artifacts involved). Every entry carries a scoping verdict (adapted in
|
|
26
|
+
this change, or excluded with a reason) confirmed by the user during the
|
|
27
|
+
interview — the frontier is not empty until every entry has one. When
|
|
28
|
+
shared processing logic changes, every consuming
|
|
26
29
|
entry point must be adapted in the SAME change; splitting into separate
|
|
27
30
|
changes requires an explicit boundary rationale confirmed by the user. -->
|
|
28
31
|
|
|
@@ -32,8 +35,13 @@
|
|
|
32
35
|
|
|
33
36
|
<!-- The crystallized approach: chosen direction, key decisions, 2-3 alternatives
|
|
34
37
|
considered with tradeoffs, and the plan summary the user explicitly confirmed
|
|
35
|
-
(interview answers alone do not count as confirmation; the summary must be
|
|
36
|
-
|
|
38
|
+
(interview answers alone do not count as confirmation; the summary must be
|
|
39
|
+
structured per the six elements of the plan confirmation gate — including
|
|
40
|
+
the plain-language implementation outline and the related-surface verdicts —
|
|
41
|
+
presented as a plain message listing the key points, never a question-tool
|
|
42
|
+
prompt, and only AFTER the interview was exhausted: presenting the plan or
|
|
43
|
+
asking for its confirmation mid-interview is forbidden, and the recommended
|
|
44
|
+
answer attached to a question is not the plan). This is
|
|
37
45
|
the HARD-GATE output — proposal, specs, design, and tasks must stay consistent
|
|
38
46
|
with what is recorded here. -->
|
|
39
47
|
|
package/assets/skills/openspec-standard/templates/schemas/spec-driven-standard/templates/proposal.md
CHANGED
|
@@ -11,10 +11,20 @@
|
|
|
11
11
|
<!-- 关联场景:survey conclusions from clarify.md — each entry point / shared
|
|
12
12
|
processing logic / downstream consumer with code evidence and its scoping
|
|
13
13
|
verdict (adapted in this change, or explicitly justified out with a boundary
|
|
14
|
-
rationale confirmed by the user).
|
|
15
|
-
|
|
14
|
+
rationale confirmed by the user). Verdicts must match the user-confirmed
|
|
15
|
+
verdicts from the clarify.md interview. Shared processing logic changes must
|
|
16
|
+
cover all consuming entry points in this change. A bare "no related impact"
|
|
16
17
|
without search evidence counts as an incomplete proposal. -->
|
|
17
18
|
|
|
19
|
+
## Implementation Outline
|
|
20
|
+
|
|
21
|
+
<!-- 实施逻辑:how the change plays out, step by step, in plain business
|
|
22
|
+
language (人话) — where the data comes from, what validation and processing
|
|
23
|
+
it goes through, where it lands, and how each related path scoped in above
|
|
24
|
+
is adapted. Numbered steps. No code-level identifiers (class/method/function
|
|
25
|
+
names) unless the user explicitly asked for technical depth; contract facts
|
|
26
|
+
(table/column/endpoint names) are exempt and stay precise. -->
|
|
27
|
+
|
|
18
28
|
## Capabilities
|
|
19
29
|
|
|
20
30
|
### New Capabilities
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifrankwang/openspec-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.142.0",
|
|
4
4
|
"description": "OpenSpec change orchestration plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode)",
|
|
5
5
|
"author": "ifrankwang",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@ const TOOL_SPECS: Record<string, ToolSpec> = {
|
|
|
55
55
|
},
|
|
56
56
|
opx_orch_complete_task_group: {
|
|
57
57
|
description:
|
|
58
|
-
"完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch
|
|
58
|
+
"完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch(漂移无害——文档、openspec 规划路径或仅版本号变更的 package.json——时直接合并收口;漂移含任一其他文件或文本冲突时回退到收尾验证 verify_cleanup 并返回 blocked),成功后销毁 worktree 并删分支。须在收尾验证(verify_cleanup)通过后调用。主仓库本地改动文件与合并写入文件重合、或存在部分暂存文件时中止并返回 blocked(保留 worktree/分支);主仓库无关脏文件不阻塞合并。",
|
|
59
59
|
schema: completeTaskGroupSchema,
|
|
60
60
|
execute: (args, ctx) => completeTaskGroupExecute(args as any, ctx),
|
|
61
61
|
},
|
package/src/core/git.ts
CHANGED
|
@@ -172,13 +172,36 @@ export async function isAncestor(worktree: string, a: string, b: string): Promis
|
|
|
172
172
|
/** 查询基准侧漂移文件清单:`git diff --name-only --no-renames <sourceBranch>...<targetBranch>`
|
|
173
173
|
* 三点区间 = merge-base(source, target) 到 target 的内容差异(基准侧自分支切出点以来的净变化,非 commit 口径)。
|
|
174
174
|
* 参数方向固定:source 传变更分支,target 传基准分支(切出点在 source 一侧)。
|
|
175
|
-
* 返回 null 表示 git
|
|
175
|
+
* 返回 null 表示 git 查询失败,调用方须按非无害保守回退。 */
|
|
176
176
|
export async function listBranchDriftFiles(worktree: string, sourceBranch: string, targetBranch: string): Promise<string[] | null> {
|
|
177
177
|
const r = await runGitChecked(worktree, ["diff", "--name-only", "--no-renames", `${sourceBranch}...${targetBranch}`])
|
|
178
178
|
if (!r.success) return null
|
|
179
179
|
return parseDiffNameOnly(r.stdout)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* 判定单文件在三点区间(merge-base(source, target) → target)内的变更内容是否仅含 `"version"` 字段行:
|
|
184
|
+
* package.json 的版本号推进属发布元数据,无代码语义,收口漂移直通按无害漂移放行。
|
|
185
|
+
* 逐行解析 `git diff --no-renames <source>...<target> -- <filePath>` 的变更行(`+`/`-` 开头,跳过
|
|
186
|
+
* `+++`/`---` 文件头行):每一行都须匹配 `"version"` 字段行才算 version-only;存在任一其他变更行
|
|
187
|
+
* (依赖、脚本等,具代码语义)→ false;无任何变更行(空 diff,内容无净变化)→ true。
|
|
188
|
+
* 返回 null 表示 git 查询失败,调用方须按非无害保守回退。
|
|
189
|
+
*/
|
|
190
|
+
export async function isVersionOnlyDrift(
|
|
191
|
+
worktree: string,
|
|
192
|
+
sourceBranch: string,
|
|
193
|
+
targetBranch: string,
|
|
194
|
+
filePath: string
|
|
195
|
+
): Promise<boolean | null> {
|
|
196
|
+
const r = await runGitChecked(worktree, ["diff", "--no-renames", `${sourceBranch}...${targetBranch}`, "--", filePath])
|
|
197
|
+
if (!r.success) return null
|
|
198
|
+
for (const line of r.stdout.split("\n")) {
|
|
199
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue
|
|
200
|
+
if ((line.startsWith("+") || line.startsWith("-")) && !/^[+-]\s*"version"\s*:/.test(line)) return false
|
|
201
|
+
}
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
182
205
|
/** 校验本地分支存在(refs/heads/<name> 严格本地命名空间,不含远端跟踪 ref)。 */
|
|
183
206
|
export async function isLocalBranch(worktree: string, branch: string): Promise<boolean> {
|
|
184
207
|
const r = await runGitChecked(worktree, ["rev-parse", "--verify", `refs/heads/${branch}`])
|
|
@@ -3,7 +3,7 @@ import { rmdir } from "node:fs/promises"
|
|
|
3
3
|
import type { OrchestrateState, TaskItem, TaskStatus, WorkflowMode, ReviewScope } from "../types.ts"
|
|
4
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, isAncestor, isWorktreeClean, listBranchDriftFiles, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, removeTaskGroupWorktree, isLocalBranch, listLocalBranches, pathExists, type DetectChangesResult } from "../git.ts"
|
|
6
|
+
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isAncestor, isWorktreeClean, listBranchDriftFiles, isVersionOnlyDrift, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, removeTaskGroupWorktree, isLocalBranch, listLocalBranches, pathExists, type DetectChangesResult } from "../git.ts"
|
|
7
7
|
import { readStateByWorktree, readStateByChangeId, writeState, writeContextToWorktree, getLockPath, acquireLock, releaseLock } from "../state.ts"
|
|
8
8
|
import { generateIsolationNamespace } from "../namespace.ts"
|
|
9
9
|
import { readExemptions } from "../exemptions.ts"
|
|
@@ -1193,12 +1193,37 @@ function renderReviewIssueSummary(item: WorkItem): string {
|
|
|
1193
1193
|
return lines.join("\n")
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
-
/**
|
|
1197
|
-
* 全部文件后缀属文档格式才放行。.txt 刻意排除(requirements.txt 等依赖清单具代码语义)。 */
|
|
1196
|
+
/** 漂移无害判定之文档后缀白名单:.txt 刻意排除(requirements.txt 等依赖清单具代码语义)。 */
|
|
1198
1197
|
const DOCUMENTATION_EXTENSIONS = [".md", ".mdx", ".markdown", ".rst", ".adoc"]
|
|
1199
|
-
|
|
1198
|
+
/** 漂移无害判定之路径前缀白名单:openspec/ 为编排规划文档空间(提案/设计/任务/spec 等),不承载运行时代码。 */
|
|
1199
|
+
const DRIFT_NEUTRAL_PATH_PREFIXES = ["openspec/"]
|
|
1200
|
+
/**
|
|
1201
|
+
* 漂移内容无害判定,满足其一即无害:文档后缀 / openspec 规划路径前缀 / package.json 且三点区间内容
|
|
1202
|
+
* 仅含 `"version"` 字段行变化(版本号语义,无代码语义)。
|
|
1203
|
+
* null(清单查询失败)按非无害保守回退;空清单 = 内容无净变化(如基准上改了又回滚)视为无害;
|
|
1204
|
+
* version-only 内容查询失败(null)同样按非无害保守回退。
|
|
1205
|
+
*/
|
|
1206
|
+
async function isHarmlessDrift(
|
|
1207
|
+
worktree: string,
|
|
1208
|
+
sourceBranch: string,
|
|
1209
|
+
targetBranch: string,
|
|
1210
|
+
files: string[] | null
|
|
1211
|
+
): Promise<boolean> {
|
|
1200
1212
|
if (files === null) return false
|
|
1201
|
-
|
|
1213
|
+
const versionCandidates: string[] = []
|
|
1214
|
+
for (const f of files) {
|
|
1215
|
+
if (DOCUMENTATION_EXTENSIONS.some((ext) => f.endsWith(ext))) continue
|
|
1216
|
+
if (DRIFT_NEUTRAL_PATH_PREFIXES.some((p) => f.startsWith(p))) continue
|
|
1217
|
+
if (f === "package.json" || f.endsWith("/package.json")) {
|
|
1218
|
+
versionCandidates.push(f)
|
|
1219
|
+
continue
|
|
1220
|
+
}
|
|
1221
|
+
return false
|
|
1222
|
+
}
|
|
1223
|
+
for (const f of versionCandidates) {
|
|
1224
|
+
if ((await isVersionOnlyDrift(worktree, sourceBranch, targetBranch, f)) !== true) return false
|
|
1225
|
+
}
|
|
1226
|
+
return true
|
|
1202
1227
|
}
|
|
1203
1228
|
|
|
1204
1229
|
/**
|
|
@@ -1330,17 +1355,18 @@ async function completeTaskGroupLocked(params: { change_id: string }, ctx: ToolC
|
|
|
1330
1355
|
|
|
1331
1356
|
// 最后任务组 → change 级收口:合并 change 分支回基准分支(一次性),成功后销毁 worktree 并删分支
|
|
1332
1357
|
const mergeTarget = state.baseBranch
|
|
1333
|
-
//
|
|
1358
|
+
// 无害漂移直通时的事实说明:仅在本次收口实际发生直通时非空;合并 blocked/conflict 分支不带该说明
|
|
1334
1359
|
let docDriftNote = ""
|
|
1335
1360
|
if (branchName) {
|
|
1336
1361
|
// create-or-reuse(合并目标解析前):在途旧模型会话升级时从当前基准 tip 创建,天然含已合入代码
|
|
1337
1362
|
await ensureChangeBranch(ctx.worktree, branchName, mergeTarget)
|
|
1338
1363
|
|
|
1339
1364
|
// 前置漂移检查(独立封装):基准 tip 须为 change 分支祖先;不满足时先比对基准侧净变化内容——
|
|
1340
|
-
//
|
|
1365
|
+
// 无害漂移(文档 / openspec 规划路径 / 仅版本号变更的 package.json,含空清单 = 无净变化)不回退、
|
|
1366
|
+
// 直接合并收口;含任一其他文件或清单/内容查询失败 → 回退收尾验证并 blocked
|
|
1341
1367
|
if (!(await isAncestor(ctx.worktree, mergeTarget, branchName))) {
|
|
1342
1368
|
const driftFiles = await listBranchDriftFiles(ctx.worktree, branchName, mergeTarget)
|
|
1343
|
-
if (!
|
|
1369
|
+
if (!(await isHarmlessDrift(ctx.worktree, branchName, mergeTarget, driftFiles))) {
|
|
1344
1370
|
const workflow = loadWorkflowFile(resolveWorkflowPath(state))
|
|
1345
1371
|
const rolledBack = rollbackToCleanupStep(item, workflow)
|
|
1346
1372
|
if (rolledBack) await writeState(ctx.worktree, state)
|
|
@@ -1359,9 +1385,9 @@ async function completeTaskGroupLocked(params: { change_id: string }, ctx: ToolC
|
|
|
1359
1385
|
? `${files.slice(0, 10).join(", ")} 等 ${files.length} 个`
|
|
1360
1386
|
: files.join(", ")
|
|
1361
1387
|
docDriftNote =
|
|
1362
|
-
`-
|
|
1388
|
+
`- **基准漂移(无代码语义)**: 基准分支 \`${mergeTarget}\` 相对变更分支切出点净变化 ${files.length} 个文件` +
|
|
1363
1389
|
(files.length > 0 ? `(${driftList})` : "") +
|
|
1364
|
-
"
|
|
1390
|
+
",均为文档、openspec 规划路径或仅版本号变更,不影响代码语义,未回退收尾验证直接合并。"
|
|
1365
1391
|
}
|
|
1366
1392
|
|
|
1367
1393
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget)
|