@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.
Files changed (84) hide show
  1. package/README.md +91 -0
  2. package/assets/agents/openspec-architect.md +61 -0
  3. package/assets/agents/openspec-developer.md +21 -0
  4. package/assets/agents/openspec-main.md +35 -0
  5. package/assets/agents/openspec-reviewer-architecture.md +60 -0
  6. package/assets/agents/openspec-reviewer-maintainability.md +59 -0
  7. package/assets/agents/openspec-reviewer-performance.md +49 -0
  8. package/assets/agents/openspec-reviewer-security.md +51 -0
  9. package/assets/agents/openspec-reviewer-style.md +50 -0
  10. package/assets/agents/openspec-reviewer-task.md +40 -0
  11. package/assets/agents/openspec-reviewer-tool.md +30 -0
  12. package/assets/dashboard/index.html +295 -0
  13. package/assets/skills/api-conventions/SKILL.md +60 -0
  14. package/assets/skills/api-test/SKILL.md +175 -0
  15. package/assets/skills/architecture-evolution/SKILL.md +60 -0
  16. package/assets/skills/db-conventions/SKILL.md +62 -0
  17. package/assets/skills/ddd-architecture/SKILL.md +261 -0
  18. package/assets/skills/environment-cleanup/SKILL.md +44 -0
  19. package/assets/skills/java-api-conventions/SKILL.md +30 -0
  20. package/assets/skills/java-code-maintainability/SKILL.md +62 -0
  21. package/assets/skills/java-code-performance/SKILL.md +49 -0
  22. package/assets/skills/java-code-style/SKILL.md +54 -0
  23. package/assets/skills/java-db-conventions/SKILL.md +44 -0
  24. package/assets/skills/java-ddd-architecture/SKILL.md +533 -0
  25. package/assets/skills/java-dev-practices/SKILL.md +110 -0
  26. package/assets/skills/java-quality-gate/SKILL.md +455 -0
  27. package/assets/skills/java-quality-gate/reference/pmd-rules.md +16 -0
  28. package/assets/skills/java-quality-tool-improve/SKILL.md +142 -0
  29. package/assets/skills/java-security/SKILL.md +59 -0
  30. package/assets/skills/java-technical-debt/SKILL.md +36 -0
  31. package/assets/skills/orchestrator/SKILL.md +65 -0
  32. package/assets/skills/quality-gate/SKILL.md +84 -0
  33. package/assets/skills/security-baseline/SKILL.md +101 -0
  34. package/assets/skills/technical-debt/SKILL.md +78 -0
  35. package/assets/workflows/task.yaml +177 -0
  36. package/package.json +79 -0
  37. package/src/adapters/agent-md.ts +37 -0
  38. package/src/adapters/claude-code/index.ts +35 -0
  39. package/src/adapters/codex/index.ts +161 -0
  40. package/src/adapters/mcp-common/cli.ts +64 -0
  41. package/src/adapters/mcp-common/index.ts +137 -0
  42. package/src/adapters/mcp-common/json-schema.ts +53 -0
  43. package/src/adapters/mcp-common/server.ts +157 -0
  44. package/src/adapters/opencode/agents.ts +80 -0
  45. package/src/adapters/opencode/dashboard.ts +64 -0
  46. package/src/adapters/opencode/index.ts +34 -0
  47. package/src/adapters/opencode/json-schema.ts +60 -0
  48. package/src/adapters/opencode/skills.ts +19 -0
  49. package/src/adapters/opencode/tools.ts +78 -0
  50. package/src/adapters/plugin-common/index.ts +173 -0
  51. package/src/adapters/zcode/index.ts +32 -0
  52. package/src/core/constants.ts +46 -0
  53. package/src/core/dashboard.ts +101 -0
  54. package/src/core/derive.ts +146 -0
  55. package/src/core/exemptions.ts +168 -0
  56. package/src/core/git.ts +373 -0
  57. package/src/core/index.ts +76 -0
  58. package/src/core/namespace.ts +12 -0
  59. package/src/core/paths.ts +23 -0
  60. package/src/core/provider.ts +74 -0
  61. package/src/core/state.ts +357 -0
  62. package/src/core/task-children.ts +59 -0
  63. package/src/core/tasks-md.ts +89 -0
  64. package/src/core/tools/gate.ts +165 -0
  65. package/src/core/tools/lifecycle.ts +645 -0
  66. package/src/core/tools/schemas.ts +363 -0
  67. package/src/core/tools/submit.ts +863 -0
  68. package/src/core/tools/types.ts +129 -0
  69. package/src/core/types.ts +135 -0
  70. package/src/core/views.ts +195 -0
  71. package/src/core/workflow/collector.ts +150 -0
  72. package/src/core/workflow/engine.ts +576 -0
  73. package/src/core/workflow/index.ts +56 -0
  74. package/src/core/workflow/loader.ts +277 -0
  75. package/src/core/workflow/poller.ts +159 -0
  76. package/src/core/workflow/reset.ts +93 -0
  77. package/src/core/workflow/status.ts +1010 -0
  78. package/src/core/workflow/submit.ts +331 -0
  79. package/src/core/workflow/types.ts +100 -0
  80. package/src/core/workflow/writeback.ts +48 -0
  81. package/src/dashboard/page.ts +19 -0
  82. package/src/index.ts +2 -0
  83. package/src/skills/resolve.ts +95 -0
  84. package/src/skills/scan.ts +29 -0
@@ -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
+ }