@ifrankwang/openspec-agents 0.122.0 → 0.123.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 +45 -17
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/tools/lifecycle.ts +13 -2
- package/src/core/tools/submit.ts +1 -12
- package/src/skills/scan.ts +44 -7
package/.mcp-server/cli.mjs
CHANGED
|
@@ -34345,21 +34345,43 @@ function loadWorkflow(yamlText) {
|
|
|
34345
34345
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
34346
34346
|
|
|
34347
34347
|
// src/skills/scan.ts
|
|
34348
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
34348
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
34349
34349
|
import { resolve, dirname as dirname2, join as join2 } from "node:path";
|
|
34350
34350
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
34351
34351
|
import { homedir } from "node:os";
|
|
34352
34352
|
var projectRoot = resolve(dirname2(fileURLToPath2(import.meta.url)), "..", "..");
|
|
34353
|
+
function resolveSkillScanRoot(moduleUrl) {
|
|
34354
|
+
const startDir = dirname2(fileURLToPath2(moduleUrl));
|
|
34355
|
+
let dir = startDir;
|
|
34356
|
+
for (;; ) {
|
|
34357
|
+
for (const candidate of [resolve(dir, "assets", "skills"), resolve(dir, "skills")]) {
|
|
34358
|
+
if (isSkillRoot(candidate))
|
|
34359
|
+
return candidate;
|
|
34360
|
+
}
|
|
34361
|
+
const parent = dirname2(dir);
|
|
34362
|
+
if (parent === dir)
|
|
34363
|
+
break;
|
|
34364
|
+
dir = parent;
|
|
34365
|
+
}
|
|
34366
|
+
return null;
|
|
34367
|
+
}
|
|
34368
|
+
function isSkillRoot(candidate) {
|
|
34369
|
+
if (!existsSync3(candidate))
|
|
34370
|
+
return false;
|
|
34371
|
+
try {
|
|
34372
|
+
return readdirSync2(candidate, { withFileTypes: true }).some((entry) => entry.isDirectory() && existsSync3(join2(candidate, entry.name, "SKILL.md")));
|
|
34373
|
+
} catch {
|
|
34374
|
+
return false;
|
|
34375
|
+
}
|
|
34376
|
+
}
|
|
34377
|
+
var scanRoot = resolveSkillScanRoot(import.meta.url);
|
|
34353
34378
|
var SKILL_SCAN_ROOTS = [
|
|
34354
|
-
|
|
34379
|
+
...scanRoot ? [scanRoot] : [],
|
|
34355
34380
|
resolve(projectRoot, ".agents", "skills"),
|
|
34356
34381
|
resolve(projectRoot, ".opencode", "skills"),
|
|
34357
34382
|
join2(homedir(), ".agents", "skills"),
|
|
34358
34383
|
join2(homedir(), ".config", "opencode", "skills")
|
|
34359
34384
|
];
|
|
34360
|
-
var DISTRIBUTED_SKILL_ROOTS = [
|
|
34361
|
-
resolve(projectRoot, "assets", "skills")
|
|
34362
|
-
];
|
|
34363
34385
|
function findSkillPath(name) {
|
|
34364
34386
|
for (const root of SKILL_SCAN_ROOTS) {
|
|
34365
34387
|
const p = resolve(root, name, "SKILL.md");
|
|
@@ -34370,7 +34392,7 @@ function findSkillPath(name) {
|
|
|
34370
34392
|
}
|
|
34371
34393
|
|
|
34372
34394
|
// src/skills/resolve.ts
|
|
34373
|
-
import { readdirSync as
|
|
34395
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4 } from "node:fs";
|
|
34374
34396
|
import { resolve as resolve2 } from "node:path";
|
|
34375
34397
|
var cacheKey = null;
|
|
34376
34398
|
var cacheIndex = null;
|
|
@@ -34385,7 +34407,7 @@ function scanSkillTags(roots = SKILL_SCAN_ROOTS) {
|
|
|
34385
34407
|
for (const root of roots) {
|
|
34386
34408
|
if (!existsSync4(root))
|
|
34387
34409
|
continue;
|
|
34388
|
-
for (const entry of
|
|
34410
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
34389
34411
|
if (!entry.isDirectory())
|
|
34390
34412
|
continue;
|
|
34391
34413
|
if (seen.has(entry.name))
|
|
@@ -36029,6 +36051,14 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
36029
36051
|
if (unresolvedBlockers.length > 0) {
|
|
36030
36052
|
throw new Error(`存在 ${unresolvedBlockers.length} 个未解决 blocker,无法完成任务组。`);
|
|
36031
36053
|
}
|
|
36054
|
+
let checkboxWarning = "";
|
|
36055
|
+
if (worktreePath) {
|
|
36056
|
+
try {
|
|
36057
|
+
await markTaskGroupCheckboxesComplete(worktreePath, params.change_id, state.taskGroupId);
|
|
36058
|
+
} catch (e) {
|
|
36059
|
+
checkboxWarning = `- **tasks.md 复选框勾选失败**: ${e instanceof Error ? e.message : String(e)}(不影响收尾)`;
|
|
36060
|
+
}
|
|
36061
|
+
}
|
|
36032
36062
|
const mergeTarget = state.baseBranch;
|
|
36033
36063
|
if (branchName) {
|
|
36034
36064
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
@@ -36050,7 +36080,9 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
36050
36080
|
}
|
|
36051
36081
|
item.metadata["completed_at"] = new Date().toISOString();
|
|
36052
36082
|
await writeState(ctx.worktree, state);
|
|
36053
|
-
|
|
36083
|
+
const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`;
|
|
36084
|
+
return checkboxWarning ? `${doneMessage}
|
|
36085
|
+
${checkboxWarning}` : doneMessage;
|
|
36054
36086
|
}
|
|
36055
36087
|
async function setUnattendedExecute(params, ctx) {
|
|
36056
36088
|
assertOrchestrator(ctx, "opx_orch_set_unattended");
|
|
@@ -36527,10 +36559,6 @@ async function agentSubmitExecute(params, ctx) {
|
|
|
36527
36559
|
baseRef
|
|
36528
36560
|
});
|
|
36529
36561
|
}
|
|
36530
|
-
if (params.step_id === "verify_task" && params.verdict === "passed" && taskChildrenOf(item).every((c) => isTerminalPhase(c.phase))) {
|
|
36531
|
-
const worktreePath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : ctx.worktree;
|
|
36532
|
-
await markTaskGroupCheckboxesComplete(worktreePath, params.change_id, state.taskGroupId);
|
|
36533
|
-
}
|
|
36534
36562
|
if (params.step_id === "verify_tool" && agentToReviewLayer(ctx.agent) === "tool") {
|
|
36535
36563
|
const head = await getCurrentHead(wtPath);
|
|
36536
36564
|
if (head)
|
|
@@ -37070,7 +37098,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
37070
37098
|
}
|
|
37071
37099
|
} catch {}
|
|
37072
37100
|
}
|
|
37073
|
-
var PKG_VERSION = "0.
|
|
37101
|
+
var PKG_VERSION = "0.123.0";
|
|
37074
37102
|
function buildMcpServer(worktree, opts = {}) {
|
|
37075
37103
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
37076
37104
|
for (const [name, spec] of Object.entries(TOOL_SPECS)) {
|
|
@@ -39211,7 +39239,7 @@ class StreamableHTTPServerTransport {
|
|
|
39211
39239
|
import { createServer } from "node:http";
|
|
39212
39240
|
|
|
39213
39241
|
// src/core/dashboard.ts
|
|
39214
|
-
import { readdirSync as
|
|
39242
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5 } from "node:fs";
|
|
39215
39243
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
39216
39244
|
import path7 from "path";
|
|
39217
39245
|
function toWorkItemCard(item) {
|
|
@@ -39268,7 +39296,7 @@ async function readDashboardState(worktree, changeId) {
|
|
|
39268
39296
|
const stateDir = getStateDir(worktree);
|
|
39269
39297
|
if (!existsSync5(stateDir))
|
|
39270
39298
|
return null;
|
|
39271
|
-
const files =
|
|
39299
|
+
const files = readdirSync4(stateDir).filter((f) => f.endsWith(".json"));
|
|
39272
39300
|
const results = [];
|
|
39273
39301
|
for (const f of files) {
|
|
39274
39302
|
try {
|
|
@@ -39356,7 +39384,7 @@ async function startDashboard(worktree) {
|
|
|
39356
39384
|
console.error(`[dashboard] 无法启动编排进度看板:端口 ${BASE_PORT}-${BASE_PORT + MAX_ATTEMPTS - 1} 均被占用`);
|
|
39357
39385
|
}
|
|
39358
39386
|
// src/core/workflow/collector.ts
|
|
39359
|
-
import { readdirSync as
|
|
39387
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
39360
39388
|
import { join as join4 } from "node:path";
|
|
39361
39389
|
var DEFAULT_POLL_INTERVAL_MS = 30000;
|
|
39362
39390
|
var SOURCE = "openspec";
|
|
@@ -39394,7 +39422,7 @@ class OpenSpecCollector {
|
|
|
39394
39422
|
const changesDir = join4(this.openspecDir, "changes");
|
|
39395
39423
|
let entries;
|
|
39396
39424
|
try {
|
|
39397
|
-
entries =
|
|
39425
|
+
entries = readdirSync5(changesDir, { withFileTypes: true });
|
|
39398
39426
|
} catch {
|
|
39399
39427
|
return [];
|
|
39400
39428
|
}
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ OpenSpec 把需求、设计、任务拆解成规范文档后,实施阶段仍
|
|
|
27
27
|
|
|
28
28
|
`implement → quality_review → done`
|
|
29
29
|
|
|
30
|
-
面向轻量变更的精简流程:无 analyze 环节(执行边界默认整个 worktree),无 verify_tool / verify_task / verify_cleanup 环节。implement 失败自循环重试;quality_review 由单一审查者合并承担工具检查、任务验证与质量审查(工具改进建议双报机制保留,由开发者实施),失败回 implement 整步重审;开发者提交 implement 成果时强检查工作区干净(不干净拒绝并提示先 commit),收尾为直接合并分支并清理(裸合并,无回归、无环境清理;合并冲突由开发者解决后直接收尾)。tasks.md
|
|
30
|
+
面向轻量变更的精简流程:无 analyze 环节(执行边界默认整个 worktree),无 verify_tool / verify_task / verify_cleanup 环节。implement 失败自循环重试;quality_review 由单一审查者合并承担工具检查、任务验证与质量审查(工具改进建议双报机制保留,由开发者实施),失败回 implement 整步重审;开发者提交 implement 成果时强检查工作区干净(不干净拒绝并提示先 commit),收尾为直接合并分支并清理(裸合并,无回归、无环境清理;合并冲突由开发者解决后直接收尾)。tasks.md 复选框在任务组收尾(opx_orch_complete_task_group)时统一勾选当前任务组,勾选提交落在 worktree 分支、随合并带回主分支;full 与 simple 行为一致,任务完成状态以状态文件为准。
|
|
31
31
|
|
|
32
32
|
## 身份与角色
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import path from "path"
|
|
|
2
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
|
-
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, type DetectChangesResult } from "../git.ts"
|
|
5
|
+
import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, type DetectChangesResult } from "../git.ts"
|
|
6
6
|
import { readStateByWorktree, readStateByChangeId, writeState, writeContextToWorktree } from "../state.ts"
|
|
7
7
|
import { generateIsolationNamespace } from "../namespace.ts"
|
|
8
8
|
import { readExemptions } from "../exemptions.ts"
|
|
@@ -663,6 +663,16 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
|
|
|
663
663
|
throw new Error(`存在 ${unresolvedBlockers.length} 个未解决 blocker,无法完成任务组。`)
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
// tasks.md 复选框统一在收尾勾选(原 G19 语义迁移):仅写 worktree,随合并带回主分支;失败不阻断收尾
|
|
667
|
+
let checkboxWarning = ""
|
|
668
|
+
if (worktreePath) {
|
|
669
|
+
try {
|
|
670
|
+
await markTaskGroupCheckboxesComplete(worktreePath, params.change_id, state.taskGroupId)
|
|
671
|
+
} catch (e) {
|
|
672
|
+
checkboxWarning = `- **tasks.md 复选框勾选失败**: ${e instanceof Error ? e.message : String(e)}(不影响收尾)`
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
666
676
|
const mergeTarget = state.baseBranch
|
|
667
677
|
if (branchName) {
|
|
668
678
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget)
|
|
@@ -684,7 +694,8 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
|
|
|
684
694
|
}
|
|
685
695
|
item.metadata["completed_at"] = new Date().toISOString()
|
|
686
696
|
await writeState(ctx.worktree, state)
|
|
687
|
-
|
|
697
|
+
const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`
|
|
698
|
+
return checkboxWarning ? `${doneMessage}\n${checkboxWarning}` : doneMessage
|
|
688
699
|
}
|
|
689
700
|
|
|
690
701
|
export async function setUnattendedExecute(params: UnattendedParams, ctx: ToolContext): Promise<string> {
|
package/src/core/tools/submit.ts
CHANGED
|
@@ -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 {
|
|
18
|
+
import { reconcileMainPollution, getCurrentHead, isWorktreeClean } from "../git.ts"
|
|
19
19
|
import { assertIssueFilesWithin } from "../paths.ts"
|
|
20
20
|
import {
|
|
21
21
|
readStateByWorktree, writeState, getLockPath, acquireLock, releaseLock,
|
|
@@ -750,17 +750,6 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
|
|
|
750
750
|
})
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
// G19:verify_task passed 且全部 task child 达终态(done)→ 同步 tasks.md 复选框 [ ] → [x]
|
|
754
|
-
if (
|
|
755
|
-
params.step_id === "verify_task" &&
|
|
756
|
-
params.verdict === "passed" &&
|
|
757
|
-
taskChildrenOf(item).every((c) => isTerminalPhase(c.phase))
|
|
758
|
-
) {
|
|
759
|
-
const worktreePath =
|
|
760
|
-
typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : ctx.worktree
|
|
761
|
-
await markTaskGroupCheckboxesComplete(worktreePath, params.change_id, state.taskGroupId)
|
|
762
|
-
}
|
|
763
|
-
|
|
764
753
|
// tool review 检查点增量检测(A3):verify_tool 的 reviewer-tool 提交成功后记录当前 HEAD,
|
|
765
754
|
// 供下次重进 verify_tool 时仅对比「检查点 → 当前 HEAD」区间的变更(无代码/配置变更时直提 passed,
|
|
766
755
|
// 省去全量工具扫描)。写入位于 submitForStep 成功与 writeState 之间,任何异常路径均不落盘(零状态变更)。
|
package/src/skills/scan.ts
CHANGED
|
@@ -1,12 +1,52 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
2
2
|
import { resolve, dirname, join } from "node:path"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
4
|
import { homedir } from "node:os"
|
|
5
5
|
|
|
6
6
|
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 定位随包分发的 skill 根目录:源码(src/skills/ 上溯 2 级=仓库根 assets/skills)、插件包 dist 形态
|
|
10
|
+
* (<插件根>/.mcp-server/ 上溯 1 级=插件根 skills/,plugin-common buildSkills 输出)与 ZCode 缓存嵌套形态
|
|
11
|
+
* (cache/<marketplace>/<plugin>/<version>/.mcp-server/ 上溯 1 级=版本目录 skills/)部署深度不同,
|
|
12
|
+
* 故从模块所在目录逐级上溯探测(与 resolveWorkflowFilePath 同范式)。每级按固定顺序探测两个候选:
|
|
13
|
+
* assets/skills 优先、skills 其次;命中要求目录下至少有一个子目录含 SKILL.md(防御空骨架目录,
|
|
14
|
+
* 如仓库根遗留的空 skills/)。首个命中即采用(首中即用,不做多根收集——避免 npm 形态上溯越过
|
|
15
|
+
* 包根误收消费方项目根)。bundle 单文件合并后 import.meta.url 指向部署位置(cli.mjs),源码形态
|
|
16
|
+
* 指向本文件自身,两者自然成立。
|
|
17
|
+
*/
|
|
18
|
+
export function resolveSkillScanRoot(moduleUrl: string): string | null {
|
|
19
|
+
const startDir = dirname(fileURLToPath(moduleUrl))
|
|
20
|
+
let dir = startDir
|
|
21
|
+
for (;;) {
|
|
22
|
+
for (const candidate of [resolve(dir, "assets", "skills"), resolve(dir, "skills")]) {
|
|
23
|
+
if (isSkillRoot(candidate)) return candidate
|
|
24
|
+
}
|
|
25
|
+
const parent = dirname(dir)
|
|
26
|
+
if (parent === dir) break // 到达文件系统根
|
|
27
|
+
dir = parent
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 目录存在且至少有一个子目录含 SKILL.md 才算 skill 根;空骨架目录(子目录无 SKILL.md)不命中。 */
|
|
33
|
+
function isSkillRoot(candidate: string): boolean {
|
|
34
|
+
if (!existsSync(candidate)) return false
|
|
35
|
+
try {
|
|
36
|
+
return readdirSync(candidate, { withFileTypes: true }).some(
|
|
37
|
+
(entry) => entry.isDirectory() && existsSync(join(candidate, entry.name, "SKILL.md")),
|
|
38
|
+
)
|
|
39
|
+
} catch {
|
|
40
|
+
return false // 不可读目录按未命中处理(优雅降级,不阻断扫描)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 随包分发 skill 缺失时优雅降级(返回 null):只影响视图加载建议与门禁索引,不阻断 server 启动;
|
|
45
|
+
// 与 loader 的 workflow 缺失即抛错差异是有意为之——opx_* 工具全部依赖 workflow,缺失时 server 无可用性。
|
|
46
|
+
const scanRoot = resolveSkillScanRoot(import.meta.url)
|
|
47
|
+
|
|
8
48
|
export const SKILL_SCAN_ROOTS: string[] = [
|
|
9
|
-
|
|
49
|
+
...(scanRoot ? [scanRoot] : []),
|
|
10
50
|
resolve(projectRoot, ".agents", "skills"),
|
|
11
51
|
resolve(projectRoot, ".opencode", "skills"),
|
|
12
52
|
join(homedir(), ".agents", "skills"),
|
|
@@ -14,9 +54,8 @@ export const SKILL_SCAN_ROOTS: string[] = [
|
|
|
14
54
|
]
|
|
15
55
|
|
|
16
56
|
// 仅随插件分发注入 opencode 的 skill 根目录。.agents/skills 靠 opencode 标准发现对本仓库自用生效,非插件注入职责。
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
]
|
|
57
|
+
// 源码形态探测根即仓库根 assets/skills(与改动前行为一致);插件形态为插件根 skills/。
|
|
58
|
+
export const DISTRIBUTED_SKILL_ROOTS: string[] = scanRoot ? [scanRoot] : []
|
|
20
59
|
|
|
21
60
|
export function findSkillPath(name: string): string | null {
|
|
22
61
|
for (const root of SKILL_SCAN_ROOTS) {
|
|
@@ -25,5 +64,3 @@ export function findSkillPath(name: string): string | null {
|
|
|
25
64
|
}
|
|
26
65
|
return null
|
|
27
66
|
}
|
|
28
|
-
|
|
29
|
-
|