@birdie_moblie/open_spec 2.1.7
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/.agents/plugins/marketplace.json +13 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.codex-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/commands/change.d.ts +3 -0
- package/dist/cli/commands/change.js +37 -0
- package/dist/cli/commands/ext.d.ts +3 -0
- package/dist/cli/commands/ext.js +196 -0
- package/dist/cli/commands/figma.d.ts +3 -0
- package/dist/cli/commands/figma.js +73 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +71 -0
- package/dist/cli/commands/opsx.d.ts +3 -0
- package/dist/cli/commands/opsx.js +324 -0
- package/dist/cli/commands/usage.d.ts +3 -0
- package/dist/cli/commands/usage.js +21 -0
- package/dist/cli/output.d.ts +8 -0
- package/dist/cli/output.js +17 -0
- package/dist/cli/program.d.ts +4 -0
- package/dist/cli/program.js +42 -0
- package/dist/cli/register.d.ts +3 -0
- package/dist/cli/register.js +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +55 -0
- package/dist/config/presets.d.ts +129 -0
- package/dist/config/presets.js +125 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/schema.js +178 -0
- package/dist/core/archive.d.ts +5 -0
- package/dist/core/archive.js +33 -0
- package/dist/core/change.d.ts +12 -0
- package/dist/core/change.js +69 -0
- package/dist/core/code-changes.d.ts +9 -0
- package/dist/core/code-changes.js +31 -0
- package/dist/core/flow.d.ts +6 -0
- package/dist/core/flow.js +99 -0
- package/dist/core/gates.d.ts +5 -0
- package/dist/core/gates.js +71 -0
- package/dist/core/instructions.d.ts +21 -0
- package/dist/core/instructions.js +102 -0
- package/dist/core/source-operations.d.ts +25 -0
- package/dist/core/source-operations.js +94 -0
- package/dist/core/sources.d.ts +33 -0
- package/dist/core/sources.js +234 -0
- package/dist/core/state.d.ts +15 -0
- package/dist/core/state.js +75 -0
- package/dist/core/status.d.ts +31 -0
- package/dist/core/status.js +22 -0
- package/dist/core/tasks.d.ts +15 -0
- package/dist/core/tasks.js +143 -0
- package/dist/core/todo.d.ts +31 -0
- package/dist/core/todo.js +114 -0
- package/dist/core/types.d.ts +297 -0
- package/dist/core/types.js +227 -0
- package/dist/core/verify.d.ts +229 -0
- package/dist/core/verify.js +391 -0
- package/dist/core/worker-evidence.d.ts +71 -0
- package/dist/core/worker-evidence.js +253 -0
- package/dist/core/worker.d.ts +22 -0
- package/dist/core/worker.js +478 -0
- package/dist/delivery/claude-install.d.ts +4 -0
- package/dist/delivery/claude-install.js +44 -0
- package/dist/delivery/distribution.d.ts +3 -0
- package/dist/delivery/distribution.js +53 -0
- package/dist/delivery/skills.d.ts +36 -0
- package/dist/delivery/skills.js +178 -0
- package/dist/figma/cache.d.ts +19 -0
- package/dist/figma/cache.js +115 -0
- package/dist/figma/claude.d.ts +14 -0
- package/dist/figma/claude.js +121 -0
- package/dist/figma/collect.d.ts +3 -0
- package/dist/figma/collect.js +57 -0
- package/dist/figma/index.d.ts +35 -0
- package/dist/figma/index.js +178 -0
- package/dist/figma/media.d.ts +5 -0
- package/dist/figma/media.js +77 -0
- package/dist/figma/response.d.ts +9 -0
- package/dist/figma/response.js +27 -0
- package/dist/hooks/command-context.d.ts +19 -0
- package/dist/hooks/command-context.js +33 -0
- package/dist/hooks/engine.d.ts +61 -0
- package/dist/hooks/engine.js +314 -0
- package/dist/hooks/plugin-runtime.d.ts +188 -0
- package/dist/hooks/plugin-runtime.js +198 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +185 -0
- package/dist/marketplace/adapters.d.ts +202 -0
- package/dist/marketplace/adapters.js +203 -0
- package/dist/marketplace/builtin.d.ts +9 -0
- package/dist/marketplace/builtin.js +9 -0
- package/dist/marketplace/install.d.ts +38 -0
- package/dist/marketplace/install.js +108 -0
- package/dist/marketplace/registry.d.ts +71 -0
- package/dist/marketplace/registry.js +342 -0
- package/dist/marketplace/upgrade.d.ts +39 -0
- package/dist/marketplace/upgrade.js +104 -0
- package/dist/shared/command-log.d.ts +9 -0
- package/dist/shared/command-log.js +55 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.js +14 -0
- package/dist/shared/exec.d.ts +16 -0
- package/dist/shared/exec.js +86 -0
- package/dist/shared/fs.d.ts +18 -0
- package/dist/shared/fs.js +93 -0
- package/dist/shared/git.d.ts +22 -0
- package/dist/shared/git.js +132 -0
- package/dist/shared/hash.d.ts +5 -0
- package/dist/shared/hash.js +29 -0
- package/dist/shared/paths.d.ts +16 -0
- package/dist/shared/paths.js +97 -0
- package/dist/shared/pkg.d.ts +3 -0
- package/dist/shared/pkg.js +23 -0
- package/dist/shared/version.d.ts +9 -0
- package/dist/shared/version.js +34 -0
- package/dist/usage/analyze.d.ts +5 -0
- package/dist/usage/analyze.js +212 -0
- package/dist/usage/claude.d.ts +9 -0
- package/dist/usage/claude.js +59 -0
- package/dist/usage/collect.d.ts +11 -0
- package/dist/usage/collect.js +71 -0
- package/dist/usage/doctor.d.ts +72 -0
- package/dist/usage/doctor.js +121 -0
- package/dist/usage/parse.d.ts +28 -0
- package/dist/usage/parse.js +345 -0
- package/dist/usage/types.d.ts +103 -0
- package/dist/usage/types.js +2 -0
- package/docs/getting-started.md +20 -0
- package/docs/guides/configuration.md +33 -0
- package/docs/guides/cost-benchmark.md +88 -0
- package/docs/guides/plugin-migration.md +65 -0
- package/docs/guides/smart-figma.md +51 -0
- package/docs/guides/source-reading.md +35 -0
- package/docs/guides/ui-conventions-template.md +39 -0
- package/docs/guides/worker-evidence.md +73 -0
- package/docs/guides/workflow.md +74 -0
- package/docs/maintainers/architecture.md +27 -0
- package/docs/maintainers/plugin-runtime-contract.md +55 -0
- package/package.json +78 -0
- package/plugin.json +9 -0
- package/presets/backend-service.yaml +19 -0
- package/presets/flutter-mobile/plugin-hooks.json +68 -0
- package/presets/flutter-mobile/ui-conventions.md +122 -0
- package/presets/flutter-mobile.yaml +205 -0
- package/presets/web-product.yaml +23 -0
- package/skills/opsx-apply/SKILL.md +35 -0
- package/skills/opsx-apply/nodes/blocked.md +7 -0
- package/skills/opsx-apply/nodes/code-task.md +29 -0
- package/skills/opsx-apply/nodes/gate-a.md +9 -0
- package/skills/opsx-apply/nodes/stop.md +5 -0
- package/skills/opsx-apply/nodes/task-build.md +50 -0
- package/skills/opsx-apply/nodes/task-repair.md +20 -0
- package/skills/opsx-apply/nodes/task-scout.md +28 -0
- package/skills/opsx-apply/nodes/task-verify.md +38 -0
- package/skills/opsx-archive/SKILL.md +24 -0
- package/skills/opsx-archive/nodes/archive.md +15 -0
- package/skills/opsx-explore/SKILL.md +20 -0
- package/skills/opsx-explore/nodes/explore.md +3 -0
- package/skills/opsx-fix-verify/SKILL.md +30 -0
- package/skills/opsx-fix-verify/nodes/repair.md +11 -0
- package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
- package/skills/opsx-fix-verify/nodes/stop.md +3 -0
- package/skills/opsx-gate-decision/SKILL.md +20 -0
- package/skills/opsx-gate-decision/nodes/decide.md +15 -0
- package/skills/opsx-propose/SKILL.md +36 -0
- package/skills/opsx-propose/nodes/blocked.md +13 -0
- package/skills/opsx-propose/nodes/collect.md +35 -0
- package/skills/opsx-propose/nodes/parse.md +72 -0
- package/skills/opsx-propose/nodes/plan.md +65 -0
- package/skills/opsx-propose/nodes/stop.md +17 -0
- package/skills/opsx-propose/nodes/tech.md +77 -0
- package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
- package/skills/opsx-resume/SKILL.md +21 -0
- package/skills/opsx-resume/nodes/route.md +12 -0
- package/skills/opsx-source-update/SKILL.md +23 -0
- package/skills/opsx-source-update/nodes/update.md +14 -0
- package/skills/opsx-verify/SKILL.md +32 -0
- package/skills/opsx-verify/nodes/blocked.md +3 -0
- package/skills/opsx-verify/nodes/failed.md +3 -0
- package/skills/opsx-verify/nodes/gate-b.md +7 -0
- package/skills/opsx-verify/nodes/stop.md +5 -0
- package/skills/opsx-verify/nodes/verify.md +10 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { exists, readText } from '../shared/fs.js';
|
|
2
|
+
import { sha256 } from '../shared/hash.js';
|
|
3
|
+
import { effectiveTaskCategories } from '../config/schema.js';
|
|
4
|
+
import { discoverPluginDirs, marketplaceRootForProject, resolveAdapterEntry, readMarketplaceLock, parseSelector } from '../marketplace/adapters.js';
|
|
5
|
+
import { execFile } from '../shared/exec.js';
|
|
6
|
+
import { AppError } from '../shared/errors.js';
|
|
7
|
+
import { writeText } from '../shared/fs.js';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { artifactPath } from '../core/change.js';
|
|
10
|
+
import { resolveInside } from '../shared/paths.js';
|
|
11
|
+
import { collectSmartFigma } from '../figma/collect.js';
|
|
12
|
+
import { pluginHook } from './plugin-runtime.js';
|
|
13
|
+
import { refreshFigma } from '../figma/cache.js';
|
|
14
|
+
export function hooksFor(config, stage) {
|
|
15
|
+
return (config.hooks ?? []).filter((hook) => hook.stage === stage);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 按运行时相同的规则预先解析配置中的全部插件入口(collect 解析 adapter 并核对 capability,其余走 pluginHook),
|
|
19
|
+
* 让入口缺失、类型/阶段不符在安装与 doctor 时暴露,而不是等到某个阶段运行才失败。
|
|
20
|
+
* 只证明接得上,不证明插件行为与之前一致。
|
|
21
|
+
*/
|
|
22
|
+
export function inspectHookEntries(projectRoot, config) {
|
|
23
|
+
const checks = [];
|
|
24
|
+
for (const hook of config.hooks ?? []) {
|
|
25
|
+
const use = hook.use;
|
|
26
|
+
if (!use || (hook.stage === 'collect' && hook.type === 'skill'))
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
if (hook.stage === 'collect') {
|
|
30
|
+
const adapter = resolveAdapterEntry(projectRoot, use).entry;
|
|
31
|
+
if (hook.capability && adapter.capability !== hook.capability) {
|
|
32
|
+
throw new AppError('adapter_capability_mismatch', `adapter capability 不符: ${use}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
pluginHook(projectRoot, hook);
|
|
37
|
+
}
|
|
38
|
+
checks.push({ hook: hook.id, use, ok: true });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
checks.push({
|
|
42
|
+
hook: hook.id, use, ok: false,
|
|
43
|
+
code: error instanceof AppError ? error.code : 'hook_entry_check_failed',
|
|
44
|
+
error: error instanceof Error ? error.message : String(error),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return checks;
|
|
49
|
+
}
|
|
50
|
+
export function buildTaskInitContext(projectRoot, config, category) {
|
|
51
|
+
const categoryConfig = effectiveTaskCategories(config)[category];
|
|
52
|
+
if (!categoryConfig) {
|
|
53
|
+
throw new AppError('task_category_unavailable', `task category 已不可用,请重新生成 plan: ${category}`);
|
|
54
|
+
}
|
|
55
|
+
const rules = [];
|
|
56
|
+
if (categoryConfig.notes)
|
|
57
|
+
rules.push({ source: 'category', text: categoryConfig.notes });
|
|
58
|
+
const hooks = hooksFor(config, 'execute')
|
|
59
|
+
.filter((hook) => !hook.taskCategories || hook.taskCategories.includes(category))
|
|
60
|
+
.map((hook) => hookDescriptor(projectRoot, hook, config));
|
|
61
|
+
return { category, rules, hooks };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* repair-verify Worker 的初始化上下文:修复可能触及任何已完成 task 的代码,
|
|
65
|
+
* 因此下发这些 task 所属分类的 notes 以及匹配其中任一分类的 execute Hook(按 id 去重,保持配置顺序)。
|
|
66
|
+
*/
|
|
67
|
+
export function buildRepairInitContext(projectRoot, config, categories) {
|
|
68
|
+
const effective = effectiveTaskCategories(config);
|
|
69
|
+
const unique = [...new Set(categories)].filter((category) => effective[category]);
|
|
70
|
+
const rules = [];
|
|
71
|
+
for (const category of unique) {
|
|
72
|
+
const notes = effective[category]?.notes;
|
|
73
|
+
if (notes)
|
|
74
|
+
rules.push({ source: 'category', text: `[${category}] ${notes}` });
|
|
75
|
+
}
|
|
76
|
+
const hooks = hooksFor(config, 'execute')
|
|
77
|
+
.filter((hook) => !hook.taskCategories || hook.taskCategories.some((category) => unique.includes(category)))
|
|
78
|
+
.map((hook) => hookDescriptor(projectRoot, hook, config));
|
|
79
|
+
return { rules, hooks };
|
|
80
|
+
}
|
|
81
|
+
/** parse/tech/plan Worker 的初始化上下文:该 stage 全部 hook 都以 descriptor 下发并要求回执。 */
|
|
82
|
+
export function buildStageInitContext(projectRoot, config, stage, capabilities) {
|
|
83
|
+
return {
|
|
84
|
+
rules: [],
|
|
85
|
+
hooks: hooksFor(config, stage).filter((hook) => !hook.capability || !capabilities || capabilities.includes(hook.capability)).map((hook) => hookDescriptor(projectRoot, hook, config)),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** verify Worker 需要登记证据的 hook 清单(command 由引擎执行,也一并列出便于核对)。 */
|
|
89
|
+
export function buildVerifyHookDescriptors(projectRoot, config) {
|
|
90
|
+
return hooksFor(config, 'verify').map((hook) => hookDescriptor(projectRoot, hook, config));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 在项目锁定的 marketplace 插件中查找 `skills/<name>/SKILL.md`。
|
|
94
|
+
* 宿主(如 Cursor)没有把 marketplace 插件注册成 skill 时,Worker 仍可按该路径读取。
|
|
95
|
+
* 任何 marketplace 未锁定 / cache 缺失都静默返回 undefined:path 只是提示,不是硬依赖。
|
|
96
|
+
*/
|
|
97
|
+
export function resolveSkillPath(projectRoot, config, skillName) {
|
|
98
|
+
for (const plugin of config.plugins ?? []) {
|
|
99
|
+
let root;
|
|
100
|
+
try {
|
|
101
|
+
root = marketplaceRootForProject(projectRoot, plugin.marketplace);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const dir of discoverPluginDirs(root)) {
|
|
107
|
+
if (path.basename(dir) !== plugin.name)
|
|
108
|
+
continue;
|
|
109
|
+
const candidate = path.join(dir, 'skills', skillName, 'SKILL.md');
|
|
110
|
+
if (exists(candidate))
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
export function hookDescriptor(projectRoot, hook, config) {
|
|
117
|
+
const required = hook.required ?? true;
|
|
118
|
+
const purpose = hook.purpose === undefined ? {} : { purpose: hook.purpose };
|
|
119
|
+
if (hook.use) {
|
|
120
|
+
const { binding, skillPath } = pluginHook(projectRoot, hook);
|
|
121
|
+
if (hook.type === 'skill')
|
|
122
|
+
return { ...purpose, id: hook.id, type: 'skill', required, name: hook.use, path: skillPath, plugin: binding };
|
|
123
|
+
return { ...purpose, id: hook.id, type: 'command', required, command: `plugin:${hook.use}:${binding.operation}`, plugin: binding };
|
|
124
|
+
}
|
|
125
|
+
if (hook.type === 'notes' && hook.notes)
|
|
126
|
+
return { ...purpose, id: hook.id, type: 'notes', required, notes: hook.notes };
|
|
127
|
+
if (hook.type === 'skill' && hook.name) {
|
|
128
|
+
const skillPath = resolveSkillPath(projectRoot, config, hook.name);
|
|
129
|
+
return skillPath
|
|
130
|
+
? { ...purpose, id: hook.id, type: 'skill', required, name: hook.name, path: skillPath }
|
|
131
|
+
: { ...purpose, id: hook.id, type: 'skill', required, name: hook.name };
|
|
132
|
+
}
|
|
133
|
+
if (hook.type === 'file' && hook.path) {
|
|
134
|
+
resolveInside(projectRoot, hook.path, 'hook_file_outside_project');
|
|
135
|
+
return { ...purpose, id: hook.id, type: 'file', required, path: hook.path };
|
|
136
|
+
}
|
|
137
|
+
if (hook.type === 'check' && hook.category && hook.description) {
|
|
138
|
+
return { ...purpose, id: hook.id, type: 'check', required, category: hook.category, description: hook.description };
|
|
139
|
+
}
|
|
140
|
+
if (hook.type === 'command' && hook.command)
|
|
141
|
+
return { ...purpose, id: hook.id, type: 'command', required, command: hook.command };
|
|
142
|
+
throw new AppError('invalid_execute_hook', `${hook.stage} hook 缺少必要字段: ${hook.id}`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 按 capability 执行 collect 阶段 hook;无 adapter 时允许 notes/手工源。
|
|
146
|
+
*
|
|
147
|
+
* skill 型 hook(或没有 `use` 的 hook)无法自动采集 url source。skills 的 collect 节点要求 agent 用宿主工具人工采集后
|
|
148
|
+
* 以 `--source-file <capability>=<path>` 登记再重新 collect,因此同 capability 只要存在手工 file/text source,
|
|
149
|
+
* 对应 url source 就视为已被覆盖:它的 artifact 指向该手工产物,不再阻塞进入 parse。
|
|
150
|
+
*/
|
|
151
|
+
export function collectSources(projectRoot, changeName, config, items, options = {}) {
|
|
152
|
+
const artifacts = [];
|
|
153
|
+
const sourceArtifacts = [];
|
|
154
|
+
const deferred = [];
|
|
155
|
+
const covered = [];
|
|
156
|
+
const warnings = [];
|
|
157
|
+
const byCap = new Map();
|
|
158
|
+
for (const item of items) {
|
|
159
|
+
const list = byCap.get(item.capability) ?? [];
|
|
160
|
+
list.push(item);
|
|
161
|
+
byCap.set(item.capability, list);
|
|
162
|
+
}
|
|
163
|
+
const reusable = (item, relative, fingerprint) => {
|
|
164
|
+
const abs = artifactPath(projectRoot, changeName, relative);
|
|
165
|
+
return !options.refresh && item.artifact === relative && item.collection?.fingerprint === fingerprint
|
|
166
|
+
&& exists(abs) && sha256(readText(abs)) === item.collection.artifactSha256;
|
|
167
|
+
};
|
|
168
|
+
const checkpoint = (item, relative, fingerprint) => {
|
|
169
|
+
options.checkpoint?.(item.id, relative, { fingerprint, artifactSha256: sha256(readText(artifactPath(projectRoot, changeName, relative))) });
|
|
170
|
+
};
|
|
171
|
+
const handled = new Set();
|
|
172
|
+
const smartHandled = new Set();
|
|
173
|
+
if (options.refresh)
|
|
174
|
+
refreshFigma(projectRoot, changeName);
|
|
175
|
+
for (const item of items) {
|
|
176
|
+
if (item.kind !== 'url' || item.capability !== 'design')
|
|
177
|
+
continue;
|
|
178
|
+
const smart = collectSmartFigma(projectRoot, changeName, item.locator);
|
|
179
|
+
if (smart === undefined)
|
|
180
|
+
continue;
|
|
181
|
+
handled.add(item.id);
|
|
182
|
+
smartHandled.add(item.id);
|
|
183
|
+
if (smart === null)
|
|
184
|
+
deferred.push(`smart-figma:${item.id}`);
|
|
185
|
+
else {
|
|
186
|
+
artifacts.push(smart);
|
|
187
|
+
sourceArtifacts.push({ sourceId: item.id, path: smart });
|
|
188
|
+
checkpoint(item, smart, sha256(JSON.stringify(['smart-figma', 1, item.locator, smart])));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const manualByCap = new Map();
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
if (item.kind !== 'text' && item.kind !== 'file')
|
|
194
|
+
continue;
|
|
195
|
+
const relative = manualArtifact(item);
|
|
196
|
+
const abs = artifactPath(projectRoot, changeName, relative);
|
|
197
|
+
const content = item.kind === 'file'
|
|
198
|
+
? readText(resolveInside(projectRoot, item.locator, 'source_file_outside_project'))
|
|
199
|
+
: item.locator;
|
|
200
|
+
const fingerprint = sha256(JSON.stringify([item.id, item.capability, item.kind, item.locator, sha256(content)]));
|
|
201
|
+
if (!reusable(item, relative, fingerprint))
|
|
202
|
+
writeText(abs, content);
|
|
203
|
+
checkpoint(item, relative, fingerprint);
|
|
204
|
+
artifacts.push(relative);
|
|
205
|
+
sourceArtifacts.push({ sourceId: item.id, path: relative });
|
|
206
|
+
handled.add(item.id);
|
|
207
|
+
if (!manualByCap.has(item.capability))
|
|
208
|
+
manualByCap.set(item.capability, { sourceId: item.id, path: relative });
|
|
209
|
+
}
|
|
210
|
+
const coverOrDefer = (label, item) => {
|
|
211
|
+
const manual = manualByCap.get(item.capability);
|
|
212
|
+
if (manual) {
|
|
213
|
+
delete item.adapter;
|
|
214
|
+
sourceArtifacts.push({ sourceId: item.id, path: manual.path });
|
|
215
|
+
handled.add(item.id);
|
|
216
|
+
covered.push(`${label}:${item.id}:${manual.sourceId}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
deferred.push(`${label}:${item.id}`);
|
|
220
|
+
};
|
|
221
|
+
for (const hook of hooksFor(config, 'collect')) {
|
|
222
|
+
const capability = hook.capability;
|
|
223
|
+
if (!capability)
|
|
224
|
+
continue;
|
|
225
|
+
const matched = (byCap.get(capability) ?? []).filter((item) => item.kind === 'url' && !smartHandled.has(item.id));
|
|
226
|
+
if (matched.length === 0)
|
|
227
|
+
continue;
|
|
228
|
+
if (hook.type === 'skill' || !hook.use) {
|
|
229
|
+
for (const item of matched)
|
|
230
|
+
coverOrDefer(hook.id, item);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const resolved = resolveAdapterEntry(projectRoot, hook.use);
|
|
234
|
+
const adapter = resolved.entry;
|
|
235
|
+
if (adapter.capability !== capability)
|
|
236
|
+
throw new AppError('adapter_capability_mismatch', `adapter capability 不符: ${hook.use}`);
|
|
237
|
+
const { marketplace } = parseSelector(hook.use);
|
|
238
|
+
const locked = readMarketplaceLock(projectRoot).marketplaces.find((entry) => entry.name === marketplace);
|
|
239
|
+
for (const item of matched) {
|
|
240
|
+
const op = adapter.operations.collect ?? adapter.operations.discovery;
|
|
241
|
+
const relative = op ? uniqueArtifact(op.artifact ?? `sources/artifacts/${item.capability}.md`, item.id) : null;
|
|
242
|
+
const fingerprint = sha256(JSON.stringify([item.id, item.capability, item.kind, item.locator, hook, adapter, locked]));
|
|
243
|
+
const produced = relative && reusable(item, relative, fingerprint)
|
|
244
|
+
? { kind: 'artifact', path: relative }
|
|
245
|
+
: runAdapter(projectRoot, changeName, adapter, item, resolved.pluginRoot);
|
|
246
|
+
if (produced.kind === 'artifact') {
|
|
247
|
+
item.adapter = { use: hook.use, identity: resolved.identity };
|
|
248
|
+
checkpoint(item, produced.path, fingerprint);
|
|
249
|
+
artifacts.push(produced.path);
|
|
250
|
+
sourceArtifacts.push({ sourceId: item.id, path: produced.path });
|
|
251
|
+
handled.add(item.id);
|
|
252
|
+
}
|
|
253
|
+
else if (produced.kind === 'deferred')
|
|
254
|
+
coverOrDefer(hook.id, item);
|
|
255
|
+
else
|
|
256
|
+
warnings.push(produced.message);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const item of items) {
|
|
260
|
+
if (item.kind !== 'url' || handled.has(item.id))
|
|
261
|
+
continue;
|
|
262
|
+
if (deferred.some((entry) => entry.endsWith(`:${item.id}`)))
|
|
263
|
+
continue;
|
|
264
|
+
coverOrDefer('unhandled', item);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
artifacts: [...new Set(artifacts)],
|
|
268
|
+
sourceArtifacts,
|
|
269
|
+
deferred: [...new Set(deferred)],
|
|
270
|
+
covered: [...new Set(covered)],
|
|
271
|
+
warnings,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function manualArtifact(item) {
|
|
275
|
+
const extension = item.kind === 'file' ? path.extname(item.locator) || '.txt' : '.md';
|
|
276
|
+
return `sources/artifacts/${item.id}${extension}`;
|
|
277
|
+
}
|
|
278
|
+
function runAdapter(projectRoot, changeName, adapter, item, pluginRoot) {
|
|
279
|
+
const op = adapter.operations.collect ?? adapter.operations.discovery;
|
|
280
|
+
if (!op || adapter.kind === 'deferred' || adapter.kind === 'skill')
|
|
281
|
+
return { kind: 'deferred' };
|
|
282
|
+
const args = (op.args ?? []).map((arg) => arg.replaceAll('{locator}', item.locator).replaceAll('{pluginRoot}', pluginRoot));
|
|
283
|
+
const result = execFile(op.command.replaceAll('{pluginRoot}', pluginRoot), args, { cwd: projectRoot, timeoutMs: op.timeoutMs });
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
throw new AppError('adapter_failed', `adapter ${adapter.id} 执行失败: ${result.stderr || result.stdout}`);
|
|
286
|
+
}
|
|
287
|
+
const relative = uniqueArtifact(op.artifact ?? `sources/artifacts/${item.capability}.md`, item.id);
|
|
288
|
+
writeText(artifactPath(projectRoot, changeName, relative), result.stdout);
|
|
289
|
+
return { kind: 'artifact', path: relative };
|
|
290
|
+
}
|
|
291
|
+
function uniqueArtifact(template, sourceId) {
|
|
292
|
+
if (template.includes('{sourceId}'))
|
|
293
|
+
return template.replaceAll('{sourceId}', sourceId);
|
|
294
|
+
const extension = path.posix.extname(template);
|
|
295
|
+
const base = extension ? template.slice(0, -extension.length) : template;
|
|
296
|
+
return `${base}-${sourceId}${extension}`;
|
|
297
|
+
}
|
|
298
|
+
export function fileHookDigest(projectRoot, hook) {
|
|
299
|
+
if (hook.type !== 'file' || !hook.path)
|
|
300
|
+
return null;
|
|
301
|
+
const abs = resolveInside(projectRoot, hook.path, 'hook_file_outside_project');
|
|
302
|
+
if (!exists(abs))
|
|
303
|
+
return null;
|
|
304
|
+
return sha256(readText(abs));
|
|
305
|
+
}
|
|
306
|
+
/** preset scaffold 出来的模板带此标记;团队填写完删掉它,file hook 才算就位。 */
|
|
307
|
+
export const TEMPLATE_MARKER = '<!-- openspec:fill-me -->';
|
|
308
|
+
export function fileHookState(projectRoot, relativePath) {
|
|
309
|
+
const abs = resolveInside(projectRoot, relativePath, 'hook_file_outside_project');
|
|
310
|
+
if (!exists(abs))
|
|
311
|
+
return 'missing';
|
|
312
|
+
return readText(abs).includes(TEMPLATE_MARKER) ? 'unfilled' : 'ready';
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type AdapterOperation, type PluginIdentity } from '../marketplace/adapters.js';
|
|
3
|
+
import type { ProjectHook } from '../config/schema.js';
|
|
4
|
+
export declare const PluginBindingSchema: z.ZodObject<{
|
|
5
|
+
use: z.ZodString;
|
|
6
|
+
kind: z.ZodEnum<{
|
|
7
|
+
hook: "hook";
|
|
8
|
+
adapter: "adapter";
|
|
9
|
+
}>;
|
|
10
|
+
operation: z.ZodEnum<{
|
|
11
|
+
preview: "preview";
|
|
12
|
+
hook: "hook";
|
|
13
|
+
read: "read";
|
|
14
|
+
}>;
|
|
15
|
+
identity: z.ZodObject<{
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
marketplace: z.ZodString;
|
|
18
|
+
resolvedSha: z.ZodString;
|
|
19
|
+
digest: z.ZodString;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
22
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
23
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, z.core.$strict>;
|
|
25
|
+
export type PluginBinding = z.infer<typeof PluginBindingSchema>;
|
|
26
|
+
export declare const PluginReceiptSchema: z.ZodObject<{
|
|
27
|
+
binding: z.ZodObject<{
|
|
28
|
+
use: z.ZodString;
|
|
29
|
+
kind: z.ZodEnum<{
|
|
30
|
+
hook: "hook";
|
|
31
|
+
adapter: "adapter";
|
|
32
|
+
}>;
|
|
33
|
+
operation: z.ZodEnum<{
|
|
34
|
+
preview: "preview";
|
|
35
|
+
hook: "hook";
|
|
36
|
+
read: "read";
|
|
37
|
+
}>;
|
|
38
|
+
identity: z.ZodObject<{
|
|
39
|
+
name: z.ZodString;
|
|
40
|
+
marketplace: z.ZodString;
|
|
41
|
+
resolvedSha: z.ZodString;
|
|
42
|
+
digest: z.ZodString;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
45
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
inputDigest: z.ZodString;
|
|
49
|
+
files: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
50
|
+
outputs: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
51
|
+
workerId: z.ZodOptional<z.ZodString>;
|
|
52
|
+
stepId: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strict>;
|
|
54
|
+
export type PluginReceipt = z.infer<typeof PluginReceiptSchema>;
|
|
55
|
+
export declare const PluginEvidenceSchema: z.ZodObject<{
|
|
56
|
+
path: z.ZodString;
|
|
57
|
+
sha256: z.ZodString;
|
|
58
|
+
binding: z.ZodObject<{
|
|
59
|
+
use: z.ZodString;
|
|
60
|
+
kind: z.ZodEnum<{
|
|
61
|
+
hook: "hook";
|
|
62
|
+
adapter: "adapter";
|
|
63
|
+
}>;
|
|
64
|
+
operation: z.ZodEnum<{
|
|
65
|
+
preview: "preview";
|
|
66
|
+
hook: "hook";
|
|
67
|
+
read: "read";
|
|
68
|
+
}>;
|
|
69
|
+
identity: z.ZodObject<{
|
|
70
|
+
name: z.ZodString;
|
|
71
|
+
marketplace: z.ZodString;
|
|
72
|
+
resolvedSha: z.ZodString;
|
|
73
|
+
digest: z.ZodString;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
76
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
77
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
78
|
+
}, z.core.$strict>;
|
|
79
|
+
workerId: z.ZodOptional<z.ZodString>;
|
|
80
|
+
stepId: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
export type PluginEvidence = z.infer<typeof PluginEvidenceSchema>;
|
|
83
|
+
/** Keep dependency closures out of control responses and shared worker indexes. */
|
|
84
|
+
export declare function savePluginEvidence(root: string, outputDir: string, receipt: PluginReceipt): PluginEvidence;
|
|
85
|
+
export declare function pluginEvidenceCurrent(root: string, reference: PluginEvidence): boolean;
|
|
86
|
+
export declare function pluginHook(projectRoot: string, hook: ProjectHook): {
|
|
87
|
+
binding: {
|
|
88
|
+
use: string;
|
|
89
|
+
kind: "hook" | "adapter";
|
|
90
|
+
operation: "preview" | "hook" | "read";
|
|
91
|
+
identity: {
|
|
92
|
+
name: string;
|
|
93
|
+
marketplace: string;
|
|
94
|
+
resolvedSha: string;
|
|
95
|
+
digest: string;
|
|
96
|
+
};
|
|
97
|
+
input?: Record<string, unknown> | undefined;
|
|
98
|
+
inputFiles?: string[] | undefined;
|
|
99
|
+
inputDescription?: string | undefined;
|
|
100
|
+
};
|
|
101
|
+
skillPath: string;
|
|
102
|
+
} | {
|
|
103
|
+
binding: {
|
|
104
|
+
use: string;
|
|
105
|
+
kind: "hook" | "adapter";
|
|
106
|
+
operation: "preview" | "hook" | "read";
|
|
107
|
+
identity: {
|
|
108
|
+
name: string;
|
|
109
|
+
marketplace: string;
|
|
110
|
+
resolvedSha: string;
|
|
111
|
+
digest: string;
|
|
112
|
+
};
|
|
113
|
+
input?: Record<string, unknown> | undefined;
|
|
114
|
+
inputFiles?: string[] | undefined;
|
|
115
|
+
inputDescription?: string | undefined;
|
|
116
|
+
};
|
|
117
|
+
skillPath?: undefined;
|
|
118
|
+
};
|
|
119
|
+
export declare function sameIdentity(a: PluginIdentity, b: PluginIdentity): boolean;
|
|
120
|
+
export declare function pluginBindingCurrent(root: string, binding: PluginBinding): boolean;
|
|
121
|
+
export declare function resolvePluginCommand(root: string, binding: PluginBinding): {
|
|
122
|
+
operation: AdapterOperation;
|
|
123
|
+
pluginRoot: string;
|
|
124
|
+
};
|
|
125
|
+
export interface PluginSource {
|
|
126
|
+
id: string;
|
|
127
|
+
capability: string;
|
|
128
|
+
kind: string;
|
|
129
|
+
locator: string;
|
|
130
|
+
artifact: string;
|
|
131
|
+
artifactSha256: string;
|
|
132
|
+
}
|
|
133
|
+
export interface PluginContext {
|
|
134
|
+
version: 1;
|
|
135
|
+
operation: 'read' | 'preview' | 'hook';
|
|
136
|
+
projectRoot: string;
|
|
137
|
+
change: {
|
|
138
|
+
name: string;
|
|
139
|
+
root: string;
|
|
140
|
+
};
|
|
141
|
+
input: Record<string, unknown>;
|
|
142
|
+
outputDir: string;
|
|
143
|
+
worker?: {
|
|
144
|
+
workerId: string;
|
|
145
|
+
stepId: string;
|
|
146
|
+
};
|
|
147
|
+
source?: PluginSource;
|
|
148
|
+
sources?: PluginSource[];
|
|
149
|
+
}
|
|
150
|
+
export declare function readPluginInput(root: string, binding: Pick<PluginBinding, 'input' | 'inputFiles'>, file?: string): {
|
|
151
|
+
input: {
|
|
152
|
+
[x: string]: unknown;
|
|
153
|
+
};
|
|
154
|
+
files: string[];
|
|
155
|
+
};
|
|
156
|
+
export declare function digestFiles(root: string, files: string[]): Record<string, string>;
|
|
157
|
+
export declare function digestsMatch(root: string, files: Record<string, string>): boolean;
|
|
158
|
+
/** Commands run as argv with JSON stdin. Complete stdout/stderr never share a file. */
|
|
159
|
+
export declare function runPluginCommand(root: string, binding: PluginBinding, context: PluginContext, inputFiles?: string[]): {
|
|
160
|
+
ok: boolean;
|
|
161
|
+
code: number;
|
|
162
|
+
receipt: {
|
|
163
|
+
binding: {
|
|
164
|
+
use: string;
|
|
165
|
+
kind: "hook" | "adapter";
|
|
166
|
+
operation: "preview" | "hook" | "read";
|
|
167
|
+
identity: {
|
|
168
|
+
name: string;
|
|
169
|
+
marketplace: string;
|
|
170
|
+
resolvedSha: string;
|
|
171
|
+
digest: string;
|
|
172
|
+
};
|
|
173
|
+
input?: Record<string, unknown> | undefined;
|
|
174
|
+
inputFiles?: string[] | undefined;
|
|
175
|
+
inputDescription?: string | undefined;
|
|
176
|
+
};
|
|
177
|
+
inputDigest: string;
|
|
178
|
+
files: Record<string, string>;
|
|
179
|
+
outputs: Record<string, string>;
|
|
180
|
+
workerId?: string | undefined;
|
|
181
|
+
stepId?: string | undefined;
|
|
182
|
+
};
|
|
183
|
+
data: unknown;
|
|
184
|
+
stdoutPath: string;
|
|
185
|
+
stderrPath: string;
|
|
186
|
+
};
|
|
187
|
+
export declare function pluginReceiptCurrent(root: string, receipt: PluginReceipt): boolean;
|
|
188
|
+
//# sourceMappingURL=plugin-runtime.d.ts.map
|