@ifrankwang/openspec-agents 0.121.1 → 0.122.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp-server/cli.mjs +45 -37
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/adapters/mcp-common/server.ts +1 -1
- package/src/core/tools/lifecycle.ts +7 -4
- package/src/core/tools/schemas.ts +6 -0
- package/src/core/tools/types.ts +3 -1
- package/src/core/types.ts +2 -2
- package/src/core/workflow/loader.ts +1 -33
- package/src/skills/scan.ts +44 -7
package/.mcp-server/cli.mjs
CHANGED
|
@@ -34170,28 +34170,6 @@ var SIMPLE_WORKFLOW_PATH = resolveWorkflowFilePath(import.meta.url, "task-simple
|
|
|
34170
34170
|
function resolveWorkflowPath(state) {
|
|
34171
34171
|
return state.mode === "simple" ? SIMPLE_WORKFLOW_PATH : TASK_WORKFLOW_PATH;
|
|
34172
34172
|
}
|
|
34173
|
-
var WORKFLOW_MODE_CONFIG_FILE = "openspec/workflow.yaml";
|
|
34174
|
-
function readWorkflowModeConfig(worktree) {
|
|
34175
|
-
const configPath = pathResolve(worktree, WORKFLOW_MODE_CONFIG_FILE);
|
|
34176
|
-
if (!existsSync2(configPath))
|
|
34177
|
-
return "full";
|
|
34178
|
-
let raw;
|
|
34179
|
-
try {
|
|
34180
|
-
raw = yaml.load(readFileSync2(configPath, "utf-8"));
|
|
34181
|
-
} catch (err) {
|
|
34182
|
-
fail(`${WORKFLOW_MODE_CONFIG_FILE} 解析失败:${err.message}`);
|
|
34183
|
-
}
|
|
34184
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
34185
|
-
fail(`${WORKFLOW_MODE_CONFIG_FILE} 配置非法:顶层必须是 YAML 映射对象`);
|
|
34186
|
-
}
|
|
34187
|
-
const mode = raw.mode;
|
|
34188
|
-
if (mode === undefined)
|
|
34189
|
-
return "full";
|
|
34190
|
-
if (mode !== "full" && mode !== "simple") {
|
|
34191
|
-
fail(`${WORKFLOW_MODE_CONFIG_FILE} 配置非法:mode 值域为 full/simple,收到 ${JSON.stringify(mode)}`);
|
|
34192
|
-
}
|
|
34193
|
-
return mode;
|
|
34194
|
-
}
|
|
34195
34173
|
var workflowFileCache = new Map;
|
|
34196
34174
|
function loadWorkflowFile(filePath) {
|
|
34197
34175
|
let wf = workflowFileCache.get(filePath);
|
|
@@ -34367,21 +34345,43 @@ function loadWorkflow(yamlText) {
|
|
|
34367
34345
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
34368
34346
|
|
|
34369
34347
|
// src/skills/scan.ts
|
|
34370
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
34348
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
34371
34349
|
import { resolve, dirname as dirname2, join as join2 } from "node:path";
|
|
34372
34350
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
34373
34351
|
import { homedir } from "node:os";
|
|
34374
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);
|
|
34375
34378
|
var SKILL_SCAN_ROOTS = [
|
|
34376
|
-
|
|
34379
|
+
...scanRoot ? [scanRoot] : [],
|
|
34377
34380
|
resolve(projectRoot, ".agents", "skills"),
|
|
34378
34381
|
resolve(projectRoot, ".opencode", "skills"),
|
|
34379
34382
|
join2(homedir(), ".agents", "skills"),
|
|
34380
34383
|
join2(homedir(), ".config", "opencode", "skills")
|
|
34381
34384
|
];
|
|
34382
|
-
var DISTRIBUTED_SKILL_ROOTS = [
|
|
34383
|
-
resolve(projectRoot, "assets", "skills")
|
|
34384
|
-
];
|
|
34385
34385
|
function findSkillPath(name) {
|
|
34386
34386
|
for (const root of SKILL_SCAN_ROOTS) {
|
|
34387
34387
|
const p = resolve(root, name, "SKILL.md");
|
|
@@ -34392,7 +34392,7 @@ function findSkillPath(name) {
|
|
|
34392
34392
|
}
|
|
34393
34393
|
|
|
34394
34394
|
// src/skills/resolve.ts
|
|
34395
|
-
import { readdirSync as
|
|
34395
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4 } from "node:fs";
|
|
34396
34396
|
import { resolve as resolve2 } from "node:path";
|
|
34397
34397
|
var cacheKey = null;
|
|
34398
34398
|
var cacheIndex = null;
|
|
@@ -34407,7 +34407,7 @@ function scanSkillTags(roots = SKILL_SCAN_ROOTS) {
|
|
|
34407
34407
|
for (const root of roots) {
|
|
34408
34408
|
if (!existsSync4(root))
|
|
34409
34409
|
continue;
|
|
34410
|
-
for (const entry of
|
|
34410
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
34411
34411
|
if (!entry.isDirectory())
|
|
34412
34412
|
continue;
|
|
34413
34413
|
if (seen.has(entry.name))
|
|
@@ -35752,6 +35752,9 @@ async function initExecute(params, ctx) {
|
|
|
35752
35752
|
args.recovery = parsed;
|
|
35753
35753
|
}
|
|
35754
35754
|
assertValidRecovery(args.recovery);
|
|
35755
|
+
if (args.mode !== undefined && args.mode !== "full" && args.mode !== "simple") {
|
|
35756
|
+
throw new Error(`mode 参数不合法,合法值:full、simple。传入值:"${String(args.mode)}"。`);
|
|
35757
|
+
}
|
|
35755
35758
|
const parsedGroups = await parseAllTaskGroupsFromMd(ctx.worktree, args.change_id);
|
|
35756
35759
|
if (parsedGroups.length === 0) {
|
|
35757
35760
|
throw new Error(`无法从 tasks.md 解析出任务组,请检查文件 openspec/changes/${args.change_id}/tasks.md。`);
|
|
@@ -35784,7 +35787,7 @@ async function initExecute(params, ctx) {
|
|
|
35784
35787
|
workItems: [],
|
|
35785
35788
|
createdAt: new Date().toISOString(),
|
|
35786
35789
|
updatedAt: new Date().toISOString(),
|
|
35787
|
-
mode:
|
|
35790
|
+
mode: args.mode ?? "simple"
|
|
35788
35791
|
};
|
|
35789
35792
|
} else {
|
|
35790
35793
|
state.baseBranch = state.baseBranch || baseBranch;
|
|
@@ -36833,7 +36836,12 @@ var orchInitSchema = {
|
|
|
36833
36836
|
type: "string",
|
|
36834
36837
|
description: "基准分支名(如 main、develop),用于计算 merge-base 和 worktree fork 源。未传则自动从当前 git 分支推导。"
|
|
36835
36838
|
},
|
|
36836
|
-
recovery: recoverySchema
|
|
36839
|
+
recovery: recoverySchema,
|
|
36840
|
+
mode: {
|
|
36841
|
+
type: "string",
|
|
36842
|
+
enum: ["full", "simple"],
|
|
36843
|
+
description: "流程模式选择:full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,缺省)。仅新建编排状态(首次 opx_orch_init)时生效;已开始的变更沿用固化模式,不支持中途切换。"
|
|
36844
|
+
}
|
|
36837
36845
|
},
|
|
36838
36846
|
required: ["change_id", "task_group_id"],
|
|
36839
36847
|
additionalProperties: false
|
|
@@ -37024,7 +37032,7 @@ function jsonSchemaToZod(schema2) {
|
|
|
37024
37032
|
var AGENT_ARG = "_agent";
|
|
37025
37033
|
var TOOL_SPECS = {
|
|
37026
37034
|
opx_orch_init: {
|
|
37027
|
-
description: "初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery
|
|
37035
|
+
description: "初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery 重复初始化当前任务组时保留其阶段和进度;切换到其它任务组时初始化该组。可选 mode 参数(full/simple,缺省 simple)选择流程模式,仅新建状态生效。",
|
|
37028
37036
|
schema: orchInitSchema,
|
|
37029
37037
|
execute: (args, ctx) => initExecute(args, ctx)
|
|
37030
37038
|
},
|
|
@@ -37084,7 +37092,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
37084
37092
|
}
|
|
37085
37093
|
} catch {}
|
|
37086
37094
|
}
|
|
37087
|
-
var PKG_VERSION = "0.
|
|
37095
|
+
var PKG_VERSION = "0.122.1";
|
|
37088
37096
|
function buildMcpServer(worktree, opts = {}) {
|
|
37089
37097
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
37090
37098
|
for (const [name, spec] of Object.entries(TOOL_SPECS)) {
|
|
@@ -39225,7 +39233,7 @@ class StreamableHTTPServerTransport {
|
|
|
39225
39233
|
import { createServer } from "node:http";
|
|
39226
39234
|
|
|
39227
39235
|
// src/core/dashboard.ts
|
|
39228
|
-
import { readdirSync as
|
|
39236
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5 } from "node:fs";
|
|
39229
39237
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
39230
39238
|
import path7 from "path";
|
|
39231
39239
|
function toWorkItemCard(item) {
|
|
@@ -39282,7 +39290,7 @@ async function readDashboardState(worktree, changeId) {
|
|
|
39282
39290
|
const stateDir = getStateDir(worktree);
|
|
39283
39291
|
if (!existsSync5(stateDir))
|
|
39284
39292
|
return null;
|
|
39285
|
-
const files =
|
|
39293
|
+
const files = readdirSync4(stateDir).filter((f) => f.endsWith(".json"));
|
|
39286
39294
|
const results = [];
|
|
39287
39295
|
for (const f of files) {
|
|
39288
39296
|
try {
|
|
@@ -39370,7 +39378,7 @@ async function startDashboard(worktree) {
|
|
|
39370
39378
|
console.error(`[dashboard] 无法启动编排进度看板:端口 ${BASE_PORT}-${BASE_PORT + MAX_ATTEMPTS - 1} 均被占用`);
|
|
39371
39379
|
}
|
|
39372
39380
|
// src/core/workflow/collector.ts
|
|
39373
|
-
import { readdirSync as
|
|
39381
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
39374
39382
|
import { join as join4 } from "node:path";
|
|
39375
39383
|
var DEFAULT_POLL_INTERVAL_MS = 30000;
|
|
39376
39384
|
var SOURCE = "openspec";
|
|
@@ -39408,7 +39416,7 @@ class OpenSpecCollector {
|
|
|
39408
39416
|
const changesDir = join4(this.openspecDir, "changes");
|
|
39409
39417
|
let entries;
|
|
39410
39418
|
try {
|
|
39411
|
-
entries =
|
|
39419
|
+
entries = readdirSync5(changesDir, { withFileTypes: true });
|
|
39412
39420
|
} catch {
|
|
39413
39421
|
return [];
|
|
39414
39422
|
}
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ OpenSpec 把需求、设计、任务拆解成规范文档后,实施阶段仍
|
|
|
15
15
|
|
|
16
16
|
## 编排流程
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
实施流程按变更开始时固化的模式执行:模式经 `opx_orch_init` 的 `mode` 参数在变更开始时固化(`full` 完整流程 / `simple` 精简流程,不传缺省 simple)。已开始的变更不受后续参数影响;恢复(recovery)沿用固化模式;旧变更(状态无模式存档)一律按 full 处理,不支持中途切换。
|
|
19
19
|
|
|
20
20
|
### full 模式
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -37,7 +37,7 @@ interface ToolSpec {
|
|
|
37
37
|
const TOOL_SPECS: Record<string, ToolSpec> = {
|
|
38
38
|
opx_orch_init: {
|
|
39
39
|
description:
|
|
40
|
-
"初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery
|
|
40
|
+
"初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery 重复初始化当前任务组时保留其阶段和进度;切换到其它任务组时初始化该组。可选 mode 参数(full/simple,缺省 simple)选择流程模式,仅新建状态生效。",
|
|
41
41
|
schema: orchInitSchema,
|
|
42
42
|
execute: (args, ctx) => initExecute(args as any, ctx),
|
|
43
43
|
},
|
|
@@ -10,7 +10,7 @@ import { parseAllTaskGroupsFromMd, parseTasksMdForGroup, extractRelevantSpecsFro
|
|
|
10
10
|
import type { ParsedTask } from "../tasks-md.ts"
|
|
11
11
|
import { assertOrchestrator, findTaskGroup } from "../derive.ts"
|
|
12
12
|
import { assertPathWithin } from "../paths.ts"
|
|
13
|
-
import { loadWorkflowFile, resolveWorkflowPath,
|
|
13
|
+
import { loadWorkflowFile, resolveWorkflowPath, type LoadedWorkflow } from "../workflow/loader.ts"
|
|
14
14
|
import { createInitialWorkItem, isBlockingSeverity, isTerminalPhase, recommendForItem, resetInternalRetryCount, adjudicateStep, clearStepTags } from "../workflow/engine.ts"
|
|
15
15
|
import { renderWorkflowStatusView } from "../workflow/status.ts"
|
|
16
16
|
import { taskChildrenOf } from "../task-children.ts"
|
|
@@ -293,6 +293,10 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
293
293
|
(args as any).recovery = parsed
|
|
294
294
|
}
|
|
295
295
|
assertValidRecovery(args.recovery)
|
|
296
|
+
// mode 值域校验:值域外一律拒绝(无论 state 是否已存在),错误早于任何状态变更且不落盘
|
|
297
|
+
if (args.mode !== undefined && args.mode !== "full" && args.mode !== "simple") {
|
|
298
|
+
throw new Error(`mode 参数不合法,合法值:full、simple。传入值:"${String(args.mode)}"。`)
|
|
299
|
+
}
|
|
296
300
|
|
|
297
301
|
const parsedGroups = await parseAllTaskGroupsFromMd(ctx.worktree, args.change_id)
|
|
298
302
|
if (parsedGroups.length === 0) {
|
|
@@ -331,10 +335,9 @@ export async function initExecute(params: InitParams, ctx: ToolContext): Promise
|
|
|
331
335
|
workItems: [],
|
|
332
336
|
createdAt: new Date().toISOString(),
|
|
333
337
|
updatedAt: new Date().toISOString(),
|
|
334
|
-
// 新建 state
|
|
335
|
-
// 值域外 / YAML 解析失败抛错);state 已存在(recovery/重复初始化/切换任务组)不读配置,
|
|
338
|
+
// 新建 state 时固化模式:取 init 参数 mode(缺省 simple);state 已存在不读参数,
|
|
336
339
|
// 沿用 state 既有 mode;旧 state 缺 mode 由消费端读时兜底 full,不写回。
|
|
337
|
-
mode:
|
|
340
|
+
mode: args.mode ?? "simple",
|
|
338
341
|
}
|
|
339
342
|
} else {
|
|
340
343
|
state.baseBranch = state.baseBranch || baseBranch
|
|
@@ -213,6 +213,12 @@ export const orchInitSchema: JSONSchema = {
|
|
|
213
213
|
description: "基准分支名(如 main、develop),用于计算 merge-base 和 worktree fork 源。未传则自动从当前 git 分支推导。",
|
|
214
214
|
},
|
|
215
215
|
recovery: recoverySchema,
|
|
216
|
+
mode: {
|
|
217
|
+
type: "string",
|
|
218
|
+
enum: ["full", "simple"],
|
|
219
|
+
description:
|
|
220
|
+
"流程模式选择:full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,缺省)。仅新建编排状态(首次 opx_orch_init)时生效;已开始的变更沿用固化模式,不支持中途切换。",
|
|
221
|
+
},
|
|
216
222
|
},
|
|
217
223
|
required: ["change_id", "task_group_id"],
|
|
218
224
|
additionalProperties: false,
|
package/src/core/tools/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildPhaseTarget, ReviewLayer, ReviewVerifyStep } from "../types.ts"
|
|
1
|
+
import type { BuildPhaseTarget, ReviewLayer, ReviewVerifyStep, WorkflowMode } from "../types.ts"
|
|
2
2
|
|
|
3
3
|
export interface ToolContext {
|
|
4
4
|
worktree: string
|
|
@@ -28,6 +28,8 @@ export interface InitParams {
|
|
|
28
28
|
* 恢复后 currentStep 落在第一个未全部通过的 verify step,可能早于被重置的 step。 */
|
|
29
29
|
reset_steps?: ReviewVerifyStep[]
|
|
30
30
|
}
|
|
31
|
+
/** 流程模式(full/simple),仅新建编排状态时生效;已开始的变更沿用固化模式,不支持中途切换。 */
|
|
32
|
+
mode?: WorkflowMode
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export interface SetWorktreeParams {
|
package/src/core/types.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface OrchestrateState {
|
|
|
135
135
|
createdAt: string
|
|
136
136
|
updatedAt: string
|
|
137
137
|
unattended?: boolean
|
|
138
|
-
/**
|
|
139
|
-
* 缺失即旧变更,一律按 full 处理(读时兜底,不写回);已存在的 state
|
|
138
|
+
/** 变更开始时固化的流程模式(经 opx_orch_init 的 mode 参数写入,缺省 simple)。
|
|
139
|
+
* 缺失即旧变更,一律按 full 处理(读时兜底,不写回);已存在的 state 不再被参数改动覆盖。 */
|
|
140
140
|
mode?: WorkflowMode
|
|
141
141
|
}
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from "./types.ts"
|
|
14
14
|
import { WORK_ITEM_PHASES, stepAgentIds } from "./types.ts"
|
|
15
15
|
import { DIMENSION_AGENT_MAP } from "../constants.ts"
|
|
16
|
-
import type { OrchestrateState
|
|
16
|
+
import type { OrchestrateState } from "../types.ts"
|
|
17
17
|
|
|
18
18
|
const SPECIAL_TRANSITIONS = ["done", "halt"] as const
|
|
19
19
|
const PHASE_NAMES = new Set<string>(WORK_ITEM_PHASES)
|
|
@@ -63,38 +63,6 @@ export function resolveWorkflowPath(state: Pick<OrchestrateState, "mode">): stri
|
|
|
63
63
|
return state.mode === "simple" ? SIMPLE_WORKFLOW_PATH : TASK_WORKFLOW_PATH
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** 项目级模式配置文件路径(<worktree>/openspec/workflow.yaml,与 openspec/config.yaml 同级)。 */
|
|
67
|
-
const WORKFLOW_MODE_CONFIG_FILE = "openspec/workflow.yaml"
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 读取项目级流程模式配置(<worktree>/openspec/workflow.yaml):
|
|
71
|
-
* - 文件缺失 → "full"(缺省)
|
|
72
|
-
* - YAML 解析失败 → 抛错
|
|
73
|
-
* - 顶层非 YAML 映射对象 → 抛错
|
|
74
|
-
* - mode 字段缺失 → "full"(缺省)
|
|
75
|
-
* - mode 值域外(非 full/simple)→ 抛错
|
|
76
|
-
* 仅由 opx_orch_init 新建 state 时调用;已存在的 state 不读配置,沿用既有 mode。
|
|
77
|
-
*/
|
|
78
|
-
export function readWorkflowModeConfig(worktree: string): WorkflowMode {
|
|
79
|
-
const configPath = pathResolve(worktree, WORKFLOW_MODE_CONFIG_FILE)
|
|
80
|
-
if (!existsSync(configPath)) return "full"
|
|
81
|
-
let raw: unknown
|
|
82
|
-
try {
|
|
83
|
-
raw = yaml.load(readFileSync(configPath, "utf-8"))
|
|
84
|
-
} catch (err) {
|
|
85
|
-
fail(`${WORKFLOW_MODE_CONFIG_FILE} 解析失败:${(err as Error).message}`)
|
|
86
|
-
}
|
|
87
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
88
|
-
fail(`${WORKFLOW_MODE_CONFIG_FILE} 配置非法:顶层必须是 YAML 映射对象`)
|
|
89
|
-
}
|
|
90
|
-
const mode = (raw as { mode?: unknown }).mode
|
|
91
|
-
if (mode === undefined) return "full"
|
|
92
|
-
if (mode !== "full" && mode !== "simple") {
|
|
93
|
-
fail(`${WORKFLOW_MODE_CONFIG_FILE} 配置非法:mode 值域为 full/simple,收到 ${JSON.stringify(mode)}`)
|
|
94
|
-
}
|
|
95
|
-
return mode
|
|
96
|
-
}
|
|
97
|
-
|
|
98
66
|
const workflowFileCache = new Map<string, LoadedWorkflow>()
|
|
99
67
|
|
|
100
68
|
/** 读取 workflow YAML 文件并解析,进程内按路径缓存(submit / status 共用)。 */
|
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
|
-
|