@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
package/src/core/git.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { execFile } from "node:child_process"
|
|
3
|
+
import { readFile, writeFile, stat } from "node:fs/promises"
|
|
4
|
+
|
|
5
|
+
export interface GitRunner {
|
|
6
|
+
run(worktree: string, args: string[]): Promise<string>
|
|
7
|
+
runChecked(
|
|
8
|
+
worktree: string,
|
|
9
|
+
args: string[]
|
|
10
|
+
): Promise<{ success: boolean; stdout: string; stderr: string }>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function execGit(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const child = execFile("git", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 } as any, (err: Error | null, stdout: string, stderr: string) => {
|
|
16
|
+
resolve({
|
|
17
|
+
stdout: stdout || "",
|
|
18
|
+
stderr: stderr || "",
|
|
19
|
+
exitCode: err ? (err as any).code || 1 : 0,
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultRunner: GitRunner = {
|
|
26
|
+
async run(worktree, args) {
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await execGit(["-C", worktree, ...args])
|
|
29
|
+
return stdout.trim()
|
|
30
|
+
} catch {
|
|
31
|
+
return ""
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async runChecked(worktree, args) {
|
|
35
|
+
const { stdout, stderr, exitCode } = await execGit(["-C", worktree, ...args])
|
|
36
|
+
return { success: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim() }
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let gitRunner: GitRunner = defaultRunner
|
|
41
|
+
|
|
42
|
+
export function __setGitRunner(r: GitRunner | null): void {
|
|
43
|
+
gitRunner = r ?? defaultRunner
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runGit(worktree: string, args: string[]): Promise<string> {
|
|
47
|
+
return gitRunner.run(worktree, args)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runGitChecked(
|
|
51
|
+
worktree: string,
|
|
52
|
+
args: string[]
|
|
53
|
+
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
54
|
+
return gitRunner.runChecked(worktree, args)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getCurrentHead(worktree: string): Promise<string> {
|
|
58
|
+
return runGit(worktree, ["rev-parse", "HEAD"])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function getCurrentBranch(worktree: string): Promise<string> {
|
|
62
|
+
const branch = (await runGit(worktree, ["rev-parse", "--abbrev-ref", "HEAD"])).trim()
|
|
63
|
+
if (branch === "HEAD") throw new Error("当前处于 detached HEAD 状态,无法自动推断 base_branch。请显式传入 base_branch 参数。")
|
|
64
|
+
return branch
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getMergeBase(worktree: string, baseBranch: string): Promise<string> {
|
|
68
|
+
return runGit(worktree, ["merge-base", "HEAD", baseBranch])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function isWorktreeClean(worktree: string): Promise<boolean> {
|
|
72
|
+
const out = await runGit(worktree, ["status", "--porcelain"])
|
|
73
|
+
return out.length === 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function markTaskGroupCheckboxesComplete(
|
|
77
|
+
worktree: string,
|
|
78
|
+
changeId: string,
|
|
79
|
+
taskGroupId: string
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const tasksMdPath = path.join(worktree, "openspec", "changes", changeId, "tasks.md")
|
|
82
|
+
let content: string
|
|
83
|
+
try {
|
|
84
|
+
content = await readFile(tasksMdPath, "utf-8")
|
|
85
|
+
} catch {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const lines = content.split("\n")
|
|
89
|
+
let inGroup = false
|
|
90
|
+
let modified = false
|
|
91
|
+
const result: string[] = []
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const groupMatch = line.match(/^##\s+(\d+)\./)
|
|
94
|
+
if (groupMatch) {
|
|
95
|
+
inGroup = groupMatch[1] === taskGroupId
|
|
96
|
+
result.push(line)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (inGroup && /^-\s+\[\s\]\s+/.test(line)) {
|
|
100
|
+
result.push(line.replace(/^(-\s+)\[\s\](\s+)/, "$1[x]$2"))
|
|
101
|
+
modified = true
|
|
102
|
+
} else {
|
|
103
|
+
result.push(line)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!modified) return
|
|
107
|
+
await writeFile(tasksMdPath, result.join("\n"))
|
|
108
|
+
const addResult = await runGitChecked(worktree, ["add", tasksMdPath])
|
|
109
|
+
if (!addResult.success) {
|
|
110
|
+
throw new Error(`git add tasks.md 失败:${addResult.stderr}`)
|
|
111
|
+
}
|
|
112
|
+
const commitResult = await runGitChecked(worktree, ["commit", "-m", "docs(tasks): mark completed task checkboxes"])
|
|
113
|
+
if (!commitResult.success) {
|
|
114
|
+
throw new Error(`git commit tasks.md 失败:${commitResult.stderr}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function mergeBranchToTarget(
|
|
119
|
+
worktree: string,
|
|
120
|
+
sourceBranch: string,
|
|
121
|
+
targetBranch: string
|
|
122
|
+
): Promise<{ success: boolean; conflict: boolean }> {
|
|
123
|
+
const checkoutResult = await runGitChecked(worktree, ["checkout", targetBranch])
|
|
124
|
+
if (!checkoutResult.success) {
|
|
125
|
+
throw new Error(`无法切到目标分支 "${targetBranch}":${checkoutResult.stderr}`)
|
|
126
|
+
}
|
|
127
|
+
const mergeResult = await runGitChecked(worktree, ["merge", "--no-ff", sourceBranch])
|
|
128
|
+
if (!mergeResult.success) {
|
|
129
|
+
await runGitChecked(worktree, ["merge", "--abort"])
|
|
130
|
+
return { success: false, conflict: true }
|
|
131
|
+
}
|
|
132
|
+
return { success: true, conflict: false }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function discoverDiskWorktrees(worktree: string): Promise<{ branch: string; path: string }[]> {
|
|
136
|
+
const result: { branch: string; path: string }[] = []
|
|
137
|
+
const wtList = await runGit(worktree, ["worktree", "list"])
|
|
138
|
+
for (const line of wtList.split("\n")) {
|
|
139
|
+
const m = line.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/)
|
|
140
|
+
if (m) {
|
|
141
|
+
const branch = m[2].trim()
|
|
142
|
+
if (branch.startsWith("task-group/")) {
|
|
143
|
+
result.push({ branch, path: m[1].trim() })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 从 git worktree 的 .git 文件中解析主仓库根路径。
|
|
152
|
+
*
|
|
153
|
+
* Git worktree 的 .git 是一个文本文件,内容为 `gitdir: /path/to/main/.git/worktrees/...`。
|
|
154
|
+
* 切割 `/.git/worktrees/` 得到主仓库路径。
|
|
155
|
+
*
|
|
156
|
+
* @param worktreePath - worktree 的绝对路径
|
|
157
|
+
* @returns 主仓库根路径
|
|
158
|
+
* @throws 无法解析 .git 文件或 gitdir 行时抛出错误
|
|
159
|
+
*/
|
|
160
|
+
export async function discoverRepoRoot(worktreePath: string): Promise<string> {
|
|
161
|
+
const gitFile = path.join(worktreePath, ".git")
|
|
162
|
+
let content: string
|
|
163
|
+
try {
|
|
164
|
+
content = await readFile(gitFile, "utf-8")
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error(`无法读取 ${gitFile},请确认该路径是一个有效的 git worktree。`)
|
|
167
|
+
}
|
|
168
|
+
const match = content.match(/^gitdir:\s*(.+)$/m)
|
|
169
|
+
if (!match) {
|
|
170
|
+
throw new Error(`无法从 ${gitFile} 解析 gitdir 行。文件内容:${content.slice(0, 200)}`)
|
|
171
|
+
}
|
|
172
|
+
const gitdir = match[1].trim()
|
|
173
|
+
const idx = gitdir.indexOf("/.git/worktrees/")
|
|
174
|
+
if (idx === -1) {
|
|
175
|
+
throw new Error(`无法从 gitdir "${gitdir}" 推导主仓库路径,缺少 "/.git/worktrees/" 标记。`)
|
|
176
|
+
}
|
|
177
|
+
return gitdir.slice(0, idx)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 解析 `git status --porcelain` / `git diff --name-only` 输出为文件路径列表。
|
|
182
|
+
* 兼容 rename 条目(`R old -> new`)与引号包裹路径。
|
|
183
|
+
*/
|
|
184
|
+
function parsePorcelainPaths(out: string): string[] {
|
|
185
|
+
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
186
|
+
return lines.map((l) => {
|
|
187
|
+
let f = l.replace(/^\S+\s+/, "")
|
|
188
|
+
if (f.includes(" -> ")) f = f.split(" -> ").pop()!.trim()
|
|
189
|
+
return f.replace(/^"+|"+$/g, "")
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 检测主仓库下 openspec 文档是否存在未提交变更(修改/新增/改名),用于编排者视图的主分支污染诊断。
|
|
195
|
+
* 兼容两种形态:`.git` 为目录(入参即主仓库,repoRoot 取自身)与 `.git` 为文件(走 discoverRepoRoot)。
|
|
196
|
+
* 无法解析主仓库路径或主仓库干净时返回 null。
|
|
197
|
+
*/
|
|
198
|
+
export async function detectMainRepoPollution(worktreePath: string): Promise<{ repoRoot: string; files: string[] } | null> {
|
|
199
|
+
let repoRoot: string
|
|
200
|
+
try {
|
|
201
|
+
const st = await stat(path.join(worktreePath, ".git"))
|
|
202
|
+
repoRoot = st.isDirectory() ? worktreePath : await discoverRepoRoot(worktreePath)
|
|
203
|
+
} catch {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
// 健壮性:git 调用失败(如非 git 仓库/权限/损坏)时返回 null,不使 opx_status 整体抛错;
|
|
207
|
+
// 污染诊断属编排者视图的辅助信息,缺失不应阻断状态视图渲染。
|
|
208
|
+
let out: string
|
|
209
|
+
try {
|
|
210
|
+
out = await runGit(repoRoot, ["status", "--porcelain", "--", "openspec/"])
|
|
211
|
+
} catch {
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
const files = parsePorcelainPaths(out)
|
|
215
|
+
if (files.length === 0) return null
|
|
216
|
+
return { repoRoot, files }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface DetectChangesResult {
|
|
220
|
+
files: string[]
|
|
221
|
+
hasNonDocChange: boolean
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 工具层检查点增量检测(A1):比较「上次工具检查点 → 当前 HEAD」区间的变更,供 verify_tool step
|
|
226
|
+
* 判断是否需要运行全量工具检查。
|
|
227
|
+
*
|
|
228
|
+
* 两个来源合并判定:
|
|
229
|
+
* 1. 已提交变更:`git diff --name-only <检查点>..HEAD`;无检查点时用 base_ref(worktree 创建时存的
|
|
230
|
+
* merge-base);base_ref 缺失或 git 调用失败 → 降级 hasNonDocChange=true(安全侧,走全量)。
|
|
231
|
+
* 2. 未提交变更:`git status --porcelain`,过滤 `openspec/` 目录下的路径后仍有剩余 → 视为有变更。
|
|
232
|
+
*
|
|
233
|
+
* hasNonDocChange 仅按「变更文件是否在 openspec/ 文档目录以外」判定:openspec/ 下文件算文档(不算变更),
|
|
234
|
+
* 其余一律算变更。本函数不做流转方向判定(不推断"应直提/应全量"),也不对工具配置文件做分类——该语义由
|
|
235
|
+
* reviewer 结合已加载 skill 判断,工具层不硬编码技术栈文件类型。
|
|
236
|
+
*
|
|
237
|
+
* @param worktreePath worktree 路径
|
|
238
|
+
* @param opts.checkpoint 上次工具检查点的 commit sha;undefined/null 视为无检查点
|
|
239
|
+
* @param opts.baseRef 无检查点时的兜底基准 ref(worktree 创建时的 merge-base)
|
|
240
|
+
*/
|
|
241
|
+
export async function detectChanges(
|
|
242
|
+
worktreePath: string,
|
|
243
|
+
opts: { checkpoint?: string | null; baseRef?: string | null },
|
|
244
|
+
): Promise<DetectChangesResult> {
|
|
245
|
+
const files: string[] = []
|
|
246
|
+
let gitFailed = false
|
|
247
|
+
const range = opts.checkpoint || opts.baseRef || undefined
|
|
248
|
+
|
|
249
|
+
// 已提交变更:diff --name-only <range>..HEAD。diff 输出无状态码前缀,直接整行取路径。
|
|
250
|
+
if (range) {
|
|
251
|
+
try {
|
|
252
|
+
const res = await runGitChecked(worktreePath, ["diff", "--name-only", `${range}..HEAD`])
|
|
253
|
+
if (res.success) {
|
|
254
|
+
for (const line of res.stdout.split("\n")) {
|
|
255
|
+
const f = line.trim()
|
|
256
|
+
if (f) files.push(f)
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
gitFailed = true
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
gitFailed = true
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
// 无检查点也无 base_ref:无法界定变更区间,安全侧降级为全量
|
|
266
|
+
gitFailed = true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 未提交变更:status --porcelain,过滤 openspec/ 文档目录下的路径
|
|
270
|
+
const uncommitted: string[] = []
|
|
271
|
+
try {
|
|
272
|
+
const res = await runGitChecked(worktreePath, ["status", "--porcelain"])
|
|
273
|
+
if (res.success) {
|
|
274
|
+
for (const f of parsePorcelainPaths(res.stdout)) {
|
|
275
|
+
if (!f.startsWith("openspec/")) uncommitted.push(f)
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
gitFailed = true
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
gitFailed = true
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const all = [...files, ...uncommitted]
|
|
285
|
+
const hasNonDocChange = gitFailed || all.some((f) => !f.startsWith("openspec/"))
|
|
286
|
+
return { files: all, hasNonDocChange }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 将主仓库 `openspec/changes/<changeId>/` 下的未提交污染文档并入 worktree 分支,并清理主仓库工作树。
|
|
291
|
+
*
|
|
292
|
+
* 序列:scoped 检测(仅限本 changeId 目录)→ 预检(主仓库 index 除 change 目录外无其它 staged、
|
|
293
|
+
* worktree 干净、污染路径在 baseRef 与 worktree tip 间无分叉、主仓库 HEAD 未相对 worktree 前进)→
|
|
294
|
+
* 主仓库 add → write-tree/commit-tree 创建不可达合并提交(main 分支 ref 不动)→
|
|
295
|
+
* worktree 真 3-way merge --no-ff <pollSha> → 主仓库 restore --staged --worktree 清理。
|
|
296
|
+
*
|
|
297
|
+
* 失败路径:预检失败时未进入 add 流程,绝不执行 --worktree 恢复(污染内容原样保留待人工处理);
|
|
298
|
+
* 已进入 merge 流程(add 后)失败时在 finally 中补 restore --staged --worktree,防主仓库 index 残留。
|
|
299
|
+
*
|
|
300
|
+
* @param mainRepo 主仓库路径(orchestrator/architect 会话工作目录)
|
|
301
|
+
* @param worktreePath 任务组 worktree 路径
|
|
302
|
+
* @param opts.changeId 需兜底的 changeId(scope 限定)
|
|
303
|
+
* @param opts.baseRef 基准分支 ref;为 null 时降级跳过分叉预检
|
|
304
|
+
* @returns 已并入的污染文件相对路径列表;无污染返回空数组
|
|
305
|
+
*/
|
|
306
|
+
export async function reconcileMainPollution(
|
|
307
|
+
mainRepo: string,
|
|
308
|
+
worktreePath: string,
|
|
309
|
+
opts: { changeId: string; baseRef: string | null }
|
|
310
|
+
): Promise<string[]> {
|
|
311
|
+
const changeDir = `openspec/changes/${opts.changeId}`
|
|
312
|
+
const files = parsePorcelainPaths(await runGit(mainRepo, ["status", "--porcelain", "--", changeDir]))
|
|
313
|
+
if (files.length === 0) return []
|
|
314
|
+
|
|
315
|
+
let staged = false
|
|
316
|
+
try {
|
|
317
|
+
const cachedOut = await runGit(mainRepo, ["diff", "--cached", "--name-only"])
|
|
318
|
+
const stagedOutside = cachedOut.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
319
|
+
.filter((p) => p !== changeDir && !p.startsWith(`${changeDir}/`))
|
|
320
|
+
if (stagedOutside.length > 0) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`主仓库 index 除 \`${changeDir}\` 外存在其它已暂存内容,拒绝自动合并污染文档:\n` +
|
|
323
|
+
stagedOutside.map((p) => `- \`${p}\``).join("\n") +
|
|
324
|
+
`\n请人工核对处理。`
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
if (!(await isWorktreeClean(worktreePath))) {
|
|
328
|
+
throw new Error(`worktree "${worktreePath}" 存在未 commit 内容,拒绝自动合并主仓库污染文档,请先 commit 再重试。`)
|
|
329
|
+
}
|
|
330
|
+
const wtTip = await runGit(worktreePath, ["rev-parse", "HEAD"])
|
|
331
|
+
if (opts.baseRef) {
|
|
332
|
+
const diverged = (await runGit(worktreePath, ["diff", "--name-only", opts.baseRef, wtTip, "--", ...files])).trim()
|
|
333
|
+
if (diverged.length > 0) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`以下污染文件在 worktree 分支中已相对基准分支发生变更(分叉),拒绝自动合并:\n` +
|
|
336
|
+
diverged.split("\n").filter(Boolean).map((p) => `- \`${p}\``).join("\n") +
|
|
337
|
+
`\n请人工核对处理。`
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const aheadCount = parseInt((await runGit(mainRepo, ["rev-list", "--count", `${wtTip}..HEAD`])).trim(), 10)
|
|
342
|
+
if (!Number.isNaN(aheadCount) && aheadCount > 0) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`主仓库分支已相对 worktree 分支前进 ${aheadCount} 个提交,自动合并将夹带其它变更内容,拒绝自动合并。\n` +
|
|
345
|
+
`请人工核对处理。`
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const addResult = await runGitChecked(mainRepo, ["add", "--", changeDir])
|
|
350
|
+
if (!addResult.success) throw new Error(`git add 主仓库 ${changeDir} 失败:${addResult.stderr}`)
|
|
351
|
+
staged = true
|
|
352
|
+
const tree = (await runGit(mainRepo, ["write-tree"])).trim()
|
|
353
|
+
if (!tree) throw new Error("git write-tree 失败:主仓库暂存区为空,无法生成合并提交。")
|
|
354
|
+
const pollCommit = (await runGit(mainRepo, [
|
|
355
|
+
"commit-tree", tree, "-p", "HEAD", "-m", "docs(openspec): reconcile main-repo edits into worktree branch",
|
|
356
|
+
])).trim()
|
|
357
|
+
if (!pollCommit) throw new Error("git commit-tree 失败:无法创建合并提交。")
|
|
358
|
+
|
|
359
|
+
const mergeResult = await runGitChecked(worktreePath, ["merge", "--no-ff", pollCommit])
|
|
360
|
+
if (!mergeResult.success) {
|
|
361
|
+
await runGitChecked(worktreePath, ["merge", "--abort"])
|
|
362
|
+
throw new Error(
|
|
363
|
+
`worktree 合并主仓库污染文档失败(冲突):${mergeResult.stderr}\n` +
|
|
364
|
+
`已中止合并,请人工核对处理。`
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
return files
|
|
368
|
+
} finally {
|
|
369
|
+
if (staged) {
|
|
370
|
+
await runGitChecked(mainRepo, ["restore", "--staged", "--worktree", "--", changeDir])
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// types
|
|
2
|
+
export type {
|
|
3
|
+
OrchestrateState, TaskGroupState, TaskItem, IssueItem, BlockerItem,
|
|
4
|
+
ExecutionBoundary, ValidationStep, QualityLayerProgress,
|
|
5
|
+
Phase, BuildPhaseTarget, OrchestrateStatus, TaskStatus, IssueStatus,
|
|
6
|
+
ReviewDimension, Dimension, BlockerStatus,
|
|
7
|
+
} from "./types.ts"
|
|
8
|
+
export { CODE_DIMENSIONS, REVIEW_DIMENSIONS, TASK_STATUSES, ISSUE_STATUSES } from "./types.ts"
|
|
9
|
+
|
|
10
|
+
// provider 契约(agent 无关)
|
|
11
|
+
export type { JSONSchema, RuntimeTool, RuntimeToolContext, RuntimeToolResult, ToolRegistry, IRuntimeConfig, IRuntimeProvider } from "./provider.ts"
|
|
12
|
+
|
|
13
|
+
// tools
|
|
14
|
+
export type { ToolContext, ToolResult } from "./tools/types.ts"
|
|
15
|
+
export type {
|
|
16
|
+
InitParams, SetWorktreeParams, UnattendedParams,
|
|
17
|
+
AgentSubmitParams,
|
|
18
|
+
} from "./tools/types.ts"
|
|
19
|
+
export {
|
|
20
|
+
initExecute, setWorktreeExecute, statusExecute, completeTaskGroupExecute, setUnattendedExecute,
|
|
21
|
+
} from "./tools/lifecycle.ts"
|
|
22
|
+
export { agentSubmitExecute } from "./tools/submit.ts"
|
|
23
|
+
|
|
24
|
+
// tool 参数 schema(纯 JSON Schema)
|
|
25
|
+
export {
|
|
26
|
+
executionBoundarySchema, boundaryExpansionSchema, taskVerifyItem, validationStepSchema,
|
|
27
|
+
blockerItem, exemptAdjudicationItem, recheckAdjudicationItem, checkpointSkipReasonItem,
|
|
28
|
+
newChildIssueItem, recoverySchema,
|
|
29
|
+
orchInitSchema, setWorktreeSchema, statusSchema, completeTaskGroupSchema,
|
|
30
|
+
setUnattendedSchema, agentSubmitSchema, TOOL_SCHEMAS,
|
|
31
|
+
} from "./tools/schemas.ts"
|
|
32
|
+
|
|
33
|
+
// state
|
|
34
|
+
export {
|
|
35
|
+
readStateByWorktree, readStateByChangeId, writeState,
|
|
36
|
+
getStateDir, getStatePath, upgradeWorkItemsFromTaskGroups,
|
|
37
|
+
} from "./state.ts"
|
|
38
|
+
|
|
39
|
+
// git
|
|
40
|
+
export {
|
|
41
|
+
runGit, runGitChecked, __setGitRunner,
|
|
42
|
+
getCurrentHead, getCurrentBranch, getMergeBase,
|
|
43
|
+
isWorktreeClean, markTaskGroupCheckboxesComplete,
|
|
44
|
+
mergeBranchToTarget, discoverDiskWorktrees,
|
|
45
|
+
} from "./git.ts"
|
|
46
|
+
export type { GitRunner } from "./git.ts"
|
|
47
|
+
|
|
48
|
+
// tasks-md
|
|
49
|
+
export type { ParsedTask } from "./tasks-md.ts"
|
|
50
|
+
export {
|
|
51
|
+
parseAllTaskGroupsFromMd, parseTasksMdForGroup, extractRelevantSpecsFromTasks,
|
|
52
|
+
parseSpecTrace,
|
|
53
|
+
} from "./tasks-md.ts"
|
|
54
|
+
|
|
55
|
+
// derive
|
|
56
|
+
export {
|
|
57
|
+
assertOrchestrator, assertAgent,
|
|
58
|
+
findTaskGroup, hasBlockingIssues, isBlockingIssue, isStatusUnresolved,
|
|
59
|
+
createEmptyQualityProgress, blockingIssues, taskGroupFromWorkItem,
|
|
60
|
+
} from "./derive.ts"
|
|
61
|
+
|
|
62
|
+
// views
|
|
63
|
+
export {
|
|
64
|
+
renderSkillSuggestions, renderEfficiencySteps, renderWorktreeSection,
|
|
65
|
+
renderAgentSummaries, formatFilePath, renderTaskItem, formatSeverity,
|
|
66
|
+
} from "./views.ts"
|
|
67
|
+
|
|
68
|
+
// dashboard
|
|
69
|
+
export { readDashboardState } from "./dashboard.ts"
|
|
70
|
+
|
|
71
|
+
// constants
|
|
72
|
+
export {
|
|
73
|
+
STATE_DIR_NAME, STATE_SUBDIR_NAME,
|
|
74
|
+
SEVERITY_LEVELS, BLOCKING_SEVERITIES,
|
|
75
|
+
DIMENSION_AGENT_MAP,
|
|
76
|
+
} from "./constants.ts"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
/** 由 changeId 生成稳定隔离标识:SHA256 前 6 位 hex(如 "a3f2b1") */
|
|
4
|
+
export function generateIsolationNamespace(changeId: string): string {
|
|
5
|
+
return createHash("sha256").update(changeId).digest("hex").slice(0, 6)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 由 namespace 派生建议端口:20000 + (hex 数值 mod 30000),范围 [20000, 50000) */
|
|
9
|
+
export function derivePortFromNamespace(namespace: string): number {
|
|
10
|
+
const n = parseInt(namespace, 16)
|
|
11
|
+
return 20000 + (n % 30000)
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
export function isPathWithin(base: string, target: string): boolean {
|
|
4
|
+
const rel = path.relative(path.resolve(base), path.resolve(base, target))
|
|
5
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function assertPathWithin(base: string, target: string, subject: string): string {
|
|
9
|
+
if (!isPathWithin(base, target)) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`${subject}路径 "${target}" 超出允许目录(\`${path.resolve(base)}\`),已拒绝。\n` +
|
|
12
|
+
`路径必须位于该目录之内,禁止用 ".." 或绝对路径逃逸到主仓库/主分支。`
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
return path.resolve(base, target)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function assertIssueFilesWithin(issues: Array<{ file?: string }>, base: string | null | undefined): void {
|
|
19
|
+
if (!base) return
|
|
20
|
+
for (const iss of issues) {
|
|
21
|
+
if (iss.file) assertPathWithin(base, iss.file, "issue 文件")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内核 Provider 契约:agent 无关的工具注册、参数 schema、context 注入、agent/skill 注入与用户交互回调。
|
|
3
|
+
* 各 agent 适配层(opencode / claude-code / codex / zcode / MCP server)实现本接口接入同一套编排状态机。
|
|
4
|
+
* 本文件 MUST NOT import 任何 agent 专有 API(@opencode-ai/* 等)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 纯 JSON Schema(draft-07 子集),工具参数统一用该形态定义,MCP 与各 agent 原生机制均可消费。 */
|
|
8
|
+
export interface JSONSchema {
|
|
9
|
+
type?: "object" | "string" | "number" | "integer" | "boolean" | "array" | "null"
|
|
10
|
+
description?: string
|
|
11
|
+
properties?: Record<string, JSONSchema>
|
|
12
|
+
items?: JSONSchema
|
|
13
|
+
required?: string[]
|
|
14
|
+
enum?: readonly (string | number | boolean)[]
|
|
15
|
+
default?: unknown
|
|
16
|
+
minLength?: number
|
|
17
|
+
minItems?: number
|
|
18
|
+
minimum?: number
|
|
19
|
+
maximum?: number
|
|
20
|
+
additionalProperties?: boolean
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 工具 context 注入:适配层在调用工具执行器时透传。 */
|
|
25
|
+
export interface RuntimeToolContext {
|
|
26
|
+
worktree: string
|
|
27
|
+
agent: string
|
|
28
|
+
/** 编排视角角色判定:true 表示调用者承担编排者职责(各 agent 主代理),替代 agent 名硬编码。 */
|
|
29
|
+
orchestrator?: boolean
|
|
30
|
+
remote?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 工具执行结果。 */
|
|
34
|
+
export interface RuntimeToolResult {
|
|
35
|
+
content: string
|
|
36
|
+
isError?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 工具定义:描述 + 纯 JSON Schema 参数 + 执行器。 */
|
|
40
|
+
export interface RuntimeTool {
|
|
41
|
+
description: string
|
|
42
|
+
args: JSONSchema
|
|
43
|
+
execute(args: Record<string, unknown>, ctx: RuntimeToolContext): Promise<RuntimeToolResult | string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 工具注册表:工具名 → 定义。 */
|
|
47
|
+
export type ToolRegistry = Record<string, RuntimeTool>
|
|
48
|
+
|
|
49
|
+
/** 运行时配置注入:agent/skill 注入指令与可选 dashboard 配置。 */
|
|
50
|
+
export interface IRuntimeConfig {
|
|
51
|
+
/** 分发的 agent 定义根目录(各适配器按自身注入格式消费)。 */
|
|
52
|
+
agentsDir?: string
|
|
53
|
+
/** 分发的 skill 根目录列表(各适配器按自身发现机制注册)。 */
|
|
54
|
+
skillsDirs?: string[]
|
|
55
|
+
/** 可选编排进度看板。 */
|
|
56
|
+
dashboard?: { port?: number; hostname?: string }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* agent 无关运行时提供者:各 agent 适配层实现该接口接入编排内核。
|
|
61
|
+
* - tools:注册 6 个 opx_* 工具(参数为纯 JSON Schema)
|
|
62
|
+
* - initialize:适配器会话初始化(如非 OpenCode agent 默认开启无人值守)
|
|
63
|
+
* - ask:用户交互回调(无人值守/环境不支持时返回 null,调用方自行裁决)
|
|
64
|
+
* - injectAgents / injectSkills:按各 agent 原生机制注入 agent 定义与 skill 路径
|
|
65
|
+
*/
|
|
66
|
+
export interface IRuntimeProvider {
|
|
67
|
+
readonly id: string
|
|
68
|
+
readonly tools: ToolRegistry
|
|
69
|
+
readonly config?: IRuntimeConfig
|
|
70
|
+
initialize?(input: unknown): Promise<void> | void
|
|
71
|
+
ask?(message: string): Promise<string | null>
|
|
72
|
+
injectAgents?(target: unknown): void
|
|
73
|
+
injectSkills?(target: unknown): void
|
|
74
|
+
}
|