@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.
Files changed (186) hide show
  1. package/.agents/plugins/marketplace.json +13 -0
  2. package/.claude-plugin/marketplace.json +12 -0
  3. package/.claude-plugin/plugin.json +9 -0
  4. package/.codex-plugin/plugin.json +9 -0
  5. package/LICENSE +21 -0
  6. package/README.md +111 -0
  7. package/bin/openspec.js +3 -0
  8. package/dist/cli/commands/change.d.ts +3 -0
  9. package/dist/cli/commands/change.js +37 -0
  10. package/dist/cli/commands/ext.d.ts +3 -0
  11. package/dist/cli/commands/ext.js +196 -0
  12. package/dist/cli/commands/figma.d.ts +3 -0
  13. package/dist/cli/commands/figma.js +73 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.js +71 -0
  16. package/dist/cli/commands/opsx.d.ts +3 -0
  17. package/dist/cli/commands/opsx.js +324 -0
  18. package/dist/cli/commands/usage.d.ts +3 -0
  19. package/dist/cli/commands/usage.js +21 -0
  20. package/dist/cli/output.d.ts +8 -0
  21. package/dist/cli/output.js +17 -0
  22. package/dist/cli/program.d.ts +4 -0
  23. package/dist/cli/program.js +42 -0
  24. package/dist/cli/register.d.ts +3 -0
  25. package/dist/cli/register.js +13 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +3 -0
  28. package/dist/config/load.d.ts +7 -0
  29. package/dist/config/load.js +55 -0
  30. package/dist/config/presets.d.ts +129 -0
  31. package/dist/config/presets.js +125 -0
  32. package/dist/config/schema.d.ts +281 -0
  33. package/dist/config/schema.js +178 -0
  34. package/dist/core/archive.d.ts +5 -0
  35. package/dist/core/archive.js +33 -0
  36. package/dist/core/change.d.ts +12 -0
  37. package/dist/core/change.js +69 -0
  38. package/dist/core/code-changes.d.ts +9 -0
  39. package/dist/core/code-changes.js +31 -0
  40. package/dist/core/flow.d.ts +6 -0
  41. package/dist/core/flow.js +99 -0
  42. package/dist/core/gates.d.ts +5 -0
  43. package/dist/core/gates.js +71 -0
  44. package/dist/core/instructions.d.ts +21 -0
  45. package/dist/core/instructions.js +102 -0
  46. package/dist/core/source-operations.d.ts +25 -0
  47. package/dist/core/source-operations.js +94 -0
  48. package/dist/core/sources.d.ts +33 -0
  49. package/dist/core/sources.js +234 -0
  50. package/dist/core/state.d.ts +15 -0
  51. package/dist/core/state.js +75 -0
  52. package/dist/core/status.d.ts +31 -0
  53. package/dist/core/status.js +22 -0
  54. package/dist/core/tasks.d.ts +15 -0
  55. package/dist/core/tasks.js +143 -0
  56. package/dist/core/todo.d.ts +31 -0
  57. package/dist/core/todo.js +114 -0
  58. package/dist/core/types.d.ts +297 -0
  59. package/dist/core/types.js +227 -0
  60. package/dist/core/verify.d.ts +229 -0
  61. package/dist/core/verify.js +391 -0
  62. package/dist/core/worker-evidence.d.ts +71 -0
  63. package/dist/core/worker-evidence.js +253 -0
  64. package/dist/core/worker.d.ts +22 -0
  65. package/dist/core/worker.js +478 -0
  66. package/dist/delivery/claude-install.d.ts +4 -0
  67. package/dist/delivery/claude-install.js +44 -0
  68. package/dist/delivery/distribution.d.ts +3 -0
  69. package/dist/delivery/distribution.js +53 -0
  70. package/dist/delivery/skills.d.ts +36 -0
  71. package/dist/delivery/skills.js +178 -0
  72. package/dist/figma/cache.d.ts +19 -0
  73. package/dist/figma/cache.js +115 -0
  74. package/dist/figma/claude.d.ts +14 -0
  75. package/dist/figma/claude.js +121 -0
  76. package/dist/figma/collect.d.ts +3 -0
  77. package/dist/figma/collect.js +57 -0
  78. package/dist/figma/index.d.ts +35 -0
  79. package/dist/figma/index.js +178 -0
  80. package/dist/figma/media.d.ts +5 -0
  81. package/dist/figma/media.js +77 -0
  82. package/dist/figma/response.d.ts +9 -0
  83. package/dist/figma/response.js +27 -0
  84. package/dist/hooks/command-context.d.ts +19 -0
  85. package/dist/hooks/command-context.js +33 -0
  86. package/dist/hooks/engine.d.ts +61 -0
  87. package/dist/hooks/engine.js +314 -0
  88. package/dist/hooks/plugin-runtime.d.ts +188 -0
  89. package/dist/hooks/plugin-runtime.js +198 -0
  90. package/dist/index.d.ts +99 -0
  91. package/dist/index.js +185 -0
  92. package/dist/marketplace/adapters.d.ts +202 -0
  93. package/dist/marketplace/adapters.js +203 -0
  94. package/dist/marketplace/builtin.d.ts +9 -0
  95. package/dist/marketplace/builtin.js +9 -0
  96. package/dist/marketplace/install.d.ts +38 -0
  97. package/dist/marketplace/install.js +108 -0
  98. package/dist/marketplace/registry.d.ts +71 -0
  99. package/dist/marketplace/registry.js +342 -0
  100. package/dist/marketplace/upgrade.d.ts +39 -0
  101. package/dist/marketplace/upgrade.js +104 -0
  102. package/dist/shared/command-log.d.ts +9 -0
  103. package/dist/shared/command-log.js +55 -0
  104. package/dist/shared/errors.d.ts +7 -0
  105. package/dist/shared/errors.js +14 -0
  106. package/dist/shared/exec.d.ts +16 -0
  107. package/dist/shared/exec.js +86 -0
  108. package/dist/shared/fs.d.ts +18 -0
  109. package/dist/shared/fs.js +93 -0
  110. package/dist/shared/git.d.ts +22 -0
  111. package/dist/shared/git.js +132 -0
  112. package/dist/shared/hash.d.ts +5 -0
  113. package/dist/shared/hash.js +29 -0
  114. package/dist/shared/paths.d.ts +16 -0
  115. package/dist/shared/paths.js +97 -0
  116. package/dist/shared/pkg.d.ts +3 -0
  117. package/dist/shared/pkg.js +23 -0
  118. package/dist/shared/version.d.ts +9 -0
  119. package/dist/shared/version.js +34 -0
  120. package/dist/usage/analyze.d.ts +5 -0
  121. package/dist/usage/analyze.js +212 -0
  122. package/dist/usage/claude.d.ts +9 -0
  123. package/dist/usage/claude.js +59 -0
  124. package/dist/usage/collect.d.ts +11 -0
  125. package/dist/usage/collect.js +71 -0
  126. package/dist/usage/doctor.d.ts +72 -0
  127. package/dist/usage/doctor.js +121 -0
  128. package/dist/usage/parse.d.ts +28 -0
  129. package/dist/usage/parse.js +345 -0
  130. package/dist/usage/types.d.ts +103 -0
  131. package/dist/usage/types.js +2 -0
  132. package/docs/getting-started.md +20 -0
  133. package/docs/guides/configuration.md +33 -0
  134. package/docs/guides/cost-benchmark.md +88 -0
  135. package/docs/guides/plugin-migration.md +65 -0
  136. package/docs/guides/smart-figma.md +51 -0
  137. package/docs/guides/source-reading.md +35 -0
  138. package/docs/guides/ui-conventions-template.md +39 -0
  139. package/docs/guides/worker-evidence.md +73 -0
  140. package/docs/guides/workflow.md +74 -0
  141. package/docs/maintainers/architecture.md +27 -0
  142. package/docs/maintainers/plugin-runtime-contract.md +55 -0
  143. package/package.json +78 -0
  144. package/plugin.json +9 -0
  145. package/presets/backend-service.yaml +19 -0
  146. package/presets/flutter-mobile/plugin-hooks.json +68 -0
  147. package/presets/flutter-mobile/ui-conventions.md +122 -0
  148. package/presets/flutter-mobile.yaml +205 -0
  149. package/presets/web-product.yaml +23 -0
  150. package/skills/opsx-apply/SKILL.md +35 -0
  151. package/skills/opsx-apply/nodes/blocked.md +7 -0
  152. package/skills/opsx-apply/nodes/code-task.md +29 -0
  153. package/skills/opsx-apply/nodes/gate-a.md +9 -0
  154. package/skills/opsx-apply/nodes/stop.md +5 -0
  155. package/skills/opsx-apply/nodes/task-build.md +50 -0
  156. package/skills/opsx-apply/nodes/task-repair.md +20 -0
  157. package/skills/opsx-apply/nodes/task-scout.md +28 -0
  158. package/skills/opsx-apply/nodes/task-verify.md +38 -0
  159. package/skills/opsx-archive/SKILL.md +24 -0
  160. package/skills/opsx-archive/nodes/archive.md +15 -0
  161. package/skills/opsx-explore/SKILL.md +20 -0
  162. package/skills/opsx-explore/nodes/explore.md +3 -0
  163. package/skills/opsx-fix-verify/SKILL.md +30 -0
  164. package/skills/opsx-fix-verify/nodes/repair.md +11 -0
  165. package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
  166. package/skills/opsx-fix-verify/nodes/stop.md +3 -0
  167. package/skills/opsx-gate-decision/SKILL.md +20 -0
  168. package/skills/opsx-gate-decision/nodes/decide.md +15 -0
  169. package/skills/opsx-propose/SKILL.md +36 -0
  170. package/skills/opsx-propose/nodes/blocked.md +13 -0
  171. package/skills/opsx-propose/nodes/collect.md +35 -0
  172. package/skills/opsx-propose/nodes/parse.md +72 -0
  173. package/skills/opsx-propose/nodes/plan.md +65 -0
  174. package/skills/opsx-propose/nodes/stop.md +17 -0
  175. package/skills/opsx-propose/nodes/tech.md +77 -0
  176. package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
  177. package/skills/opsx-resume/SKILL.md +21 -0
  178. package/skills/opsx-resume/nodes/route.md +12 -0
  179. package/skills/opsx-source-update/SKILL.md +23 -0
  180. package/skills/opsx-source-update/nodes/update.md +14 -0
  181. package/skills/opsx-verify/SKILL.md +32 -0
  182. package/skills/opsx-verify/nodes/blocked.md +3 -0
  183. package/skills/opsx-verify/nodes/failed.md +3 -0
  184. package/skills/opsx-verify/nodes/gate-b.md +7 -0
  185. package/skills/opsx-verify/nodes/stop.md +5 -0
  186. package/skills/opsx-verify/nodes/verify.md +10 -0
@@ -0,0 +1,178 @@
1
+ import { SaxesParser } from 'saxes';
2
+ import { sha256 } from '../shared/hash.js';
3
+ import { AppError } from '../shared/errors.js';
4
+ export const FIGMA_ADAPTER_VERSION = 2;
5
+ const CONTAINERS = new Set(['document', 'canvas', 'page', 'section']);
6
+ const CANDIDATES = new Set(['frame', 'component', 'instance', 'component-set', 'group']);
7
+ // Known drawing primitives cannot contain page candidates. Retain their identity
8
+ // in the full index and raw response, without asking collect to expand them.
9
+ const LEAVES = new Set(['text', 'rectangle', 'rounded-rectangle', 'ellipse', 'line', 'vector', 'star', 'regular-polygon', 'polygon', 'slice']);
10
+ export function normalizeNodeId(value) {
11
+ const id = value.replace(/-/g, ':');
12
+ if (!/^I?\d+:\d+(?:;\d+:\d+)*$/.test(id))
13
+ throw new AppError('figma_node_invalid', 'Figma nodeId 格式无效');
14
+ return id;
15
+ }
16
+ export function figmaSource(value) {
17
+ let url;
18
+ try {
19
+ url = new URL(value);
20
+ }
21
+ catch {
22
+ throw new AppError('figma_url_invalid', '需要完整 Figma URL');
23
+ }
24
+ const match = url.pathname.match(/^\/(?:design|file|proto)\/([a-zA-Z0-9]+)(?:\/|$)/);
25
+ if (url.protocol !== 'https:' || !['figma.com', 'www.figma.com'].includes(url.hostname) || url.port || url.username || url.password || !match) {
26
+ throw new AppError('figma_url_invalid', '需要官方 Figma design/file URL');
27
+ }
28
+ const nodeId = url.searchParams.has('node-id') ? normalizeNodeId(url.searchParams.get('node-id')) : undefined;
29
+ return { url: `https://www.figma.com/design/${match[1]}${nodeId ? `?node-id=${nodeId.replace(':', '-')}` : ''}`, fileKey: match[1], ...(nodeId ? { nodeId } : {}) };
30
+ }
31
+ /** Parse only complete XML documents. Never infer an empty design from sparse metadata. */
32
+ export function projectMetadata(response, source) {
33
+ const encoded = JSON.stringify(response);
34
+ if (Buffer.byteLength(encoded) > 16 * 1024 * 1024)
35
+ throw new AppError('figma_response_too_large', '响应超过 16 MiB;需缩小来源容器');
36
+ const blocks = textBlocks(response);
37
+ const docs = blocks.filter(text => text.trimStart().startsWith('<'));
38
+ if (!docs.length && !source.nodeId) {
39
+ const catalog = blocks.find(text => text.startsWith('No nodeId was provided. Listing the top-level pages of the document.') && text.includes('\nTop-level pages of the document:\n'));
40
+ if (catalog) {
41
+ const lines = catalog.split('\nTop-level pages of the document:\n')[1].trim().split('\n');
42
+ const pages = lines.map(line => {
43
+ const match = /^- (\d+:\d+): (.+)$/.exec(line);
44
+ if (!match)
45
+ throw new AppError('figma_catalog_invalid', '官方画布目录格式不完整');
46
+ return { id: match[1], name: match[2], type: 'page', childrenObserved: false };
47
+ });
48
+ if (new Set(pages.map(page => page.id)).size !== pages.length)
49
+ throw new AppError('figma_catalog_invalid', '画布目录含重复节点');
50
+ return { adapter: 'smart-figma', version: FIGMA_ADAPTER_VERSION, source, responseSha256: sha256(encoded),
51
+ roots: pages, candidates: [], containers: pages, leaves: [], unresolved: pages, status: 'needs-container-expansion' };
52
+ }
53
+ }
54
+ if (!docs.length)
55
+ throw new AppError('figma_metadata_missing', 'MCP 未返回完整 XML metadata;不能宣称采集成功');
56
+ const trees = [];
57
+ let count = 0;
58
+ for (const xml of docs) {
59
+ const stack = [];
60
+ const parser = new SaxesParser({ xmlns: false });
61
+ parser.on('doctype', () => { throw new Error('DOCTYPE is forbidden'); });
62
+ parser.on('opentag', tag => {
63
+ if (stack.length > 512)
64
+ throw new Error('XML too deep');
65
+ if (++count > 100000)
66
+ throw new Error('XML node budget exceeded');
67
+ const a = tag.attributes;
68
+ if (typeof a.id !== 'string')
69
+ throw new Error('Node id missing');
70
+ const id = normalizeNodeId(a.id);
71
+ const dimensions = ['x', 'y', 'width', 'height'].map(key => Number(a[key]));
72
+ const node = { id, name: String(a.name ?? ''), type: tag.name.toLowerCase(), childrenObserved: false };
73
+ if (stack.length)
74
+ node.parent = stack.at(-1).node.id;
75
+ if (dimensions.every(Number.isFinite))
76
+ node.bounds = dimensions;
77
+ const tree = { node, children: [] };
78
+ if (stack.length) {
79
+ stack.at(-1).children.push(tree);
80
+ stack.at(-1).node.childrenObserved = true;
81
+ }
82
+ else
83
+ trees.push(tree);
84
+ stack.push(tree);
85
+ });
86
+ parser.on('closetag', () => { stack.pop(); });
87
+ parser.on('error', error => { throw error; });
88
+ try {
89
+ parser.write(xml).close();
90
+ }
91
+ catch (error) {
92
+ throw new AppError('figma_metadata_invalid', `XML 不完整或无效: ${error.message}`);
93
+ }
94
+ }
95
+ const nodes = new Map();
96
+ const pending = [...trees];
97
+ while (pending.length) {
98
+ const tree = pending.pop();
99
+ if (nodes.has(tree.node.id))
100
+ throw new AppError('figma_duplicate_node', `响应含重复节点: ${tree.node.id}`);
101
+ nodes.set(tree.node.id, tree);
102
+ pending.push(...tree.children);
103
+ }
104
+ const roots = source.nodeId ? [nodes.get(source.nodeId)] : trees;
105
+ if (roots.some(root => !root))
106
+ throw new AppError('figma_root_missing', '返回内容不含请求节点;不能使用服务端回退的其他页面');
107
+ const candidates = [];
108
+ const containers = [];
109
+ const leaves = [];
110
+ const unresolved = [];
111
+ const queue = [...roots];
112
+ while (queue.length) {
113
+ const tree = queue.shift();
114
+ if (CONTAINERS.has(tree.node.type)) {
115
+ containers.push(tree.node);
116
+ if (tree.children.length)
117
+ queue.push(...tree.children);
118
+ else
119
+ unresolved.push(tree.node);
120
+ }
121
+ else if (CANDIDATES.has(tree.node.type))
122
+ candidates.push(tree.node);
123
+ else if (LEAVES.has(tree.node.type) && !tree.children.length)
124
+ leaves.push(tree.node);
125
+ else
126
+ unresolved.push(tree.node); // Unknown containers are not silently discarded or expanded as pages.
127
+ }
128
+ const sort = (values) => values.sort((a, b) => a.id.localeCompare(b.id, 'en', { numeric: true }));
129
+ return { adapter: 'smart-figma', version: FIGMA_ADAPTER_VERSION, source, responseSha256: sha256(encoded),
130
+ roots: roots.map(root => root.node), candidates: sort(candidates), containers: sort(containers), leaves: sort(leaves), unresolved: sort(unresolved),
131
+ status: unresolved.length ? 'needs-container-expansion' : 'candidates' };
132
+ }
133
+ function textBlocks(value) {
134
+ if (typeof value === 'string') {
135
+ if (value.trimStart().startsWith('<'))
136
+ return [value];
137
+ try {
138
+ return textBlocks(JSON.parse(value));
139
+ }
140
+ catch {
141
+ return [value];
142
+ }
143
+ }
144
+ if (Array.isArray(value))
145
+ return value.flatMap(textBlocks);
146
+ if (value && typeof value === 'object') {
147
+ const obj = value;
148
+ if (obj.isError === true)
149
+ throw new AppError('figma_tool_failed', '官方 MCP 返回错误,未形成页面索引');
150
+ if (obj.type === 'text' && typeof obj.text === 'string')
151
+ return [obj.text];
152
+ if (Array.isArray(obj.content))
153
+ return textBlocks(obj.content);
154
+ }
155
+ return [];
156
+ }
157
+ /** Bounded model view; full candidates remain in the machine artifact, with explicit pagination. */
158
+ export function indexPage(index, offset = 0, budget = 8000) {
159
+ const entries = [...index.candidates.map(node => ({ ...node, role: 'candidate' })), ...index.unresolved.map(node => ({ ...node, role: 'unresolved' }))];
160
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > entries.length || budget < 1024)
161
+ throw new AppError('figma_page_invalid', '索引分页参数无效');
162
+ const result = { adapter: index.adapter, version: index.version, source: index.source,
163
+ status: index.status, total: entries.length, candidateCount: index.candidates.length, offset, entries: [],
164
+ unresolvedCount: index.unresolved.length, leafCount: index.leaves?.length ?? null, note: '页面候选,不推断业务角色;无子层不表示设计为空。parse 可确认形态,tech/plan 可定向核对复用与范围;完整实现证据在 coding 读取。' };
165
+ const candidates = result.entries;
166
+ for (const node of entries.slice(offset)) {
167
+ candidates.push(node);
168
+ if (Buffer.byteLength(JSON.stringify(result)) > budget - 100) {
169
+ candidates.pop();
170
+ break;
171
+ }
172
+ }
173
+ if (!candidates.length && offset < entries.length)
174
+ throw new AppError('figma_entry_too_large', '单个候选超过输出预算,完整索引仍保留');
175
+ result.nextOffset = offset + candidates.length < entries.length ? offset + candidates.length : null;
176
+ return result;
177
+ }
178
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ /** Provider-specific opt-in. Production assets are never resized or rewritten. */
2
+ export declare function imagePixelLimit(value: string | number): number;
3
+ export declare function guardImageRead(root: string, input: Record<string, unknown>, limit: number): Record<string, unknown>;
4
+ export declare function compatibleImages(root: string, change: string, response: unknown, limit: number): Record<string, unknown>;
5
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { artifactPath, artifactRel } from '../core/change.js';
4
+ import { resolveInside } from '../shared/paths.js';
5
+ import { sha256 } from '../shared/hash.js';
6
+ import { writeOnce } from './cache.js';
7
+ /** Provider-specific opt-in. Production assets are never resized or rewritten. */
8
+ export function imagePixelLimit(value) {
9
+ const limit = Number(value);
10
+ if (!Number.isSafeInteger(limit) || limit < 1)
11
+ throw new Error('min-image-pixels 必须为正整数');
12
+ return limit;
13
+ }
14
+ function pngSize(bytes) {
15
+ if (bytes.length < 24 || bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a'
16
+ || bytes.toString('ascii', 12, 16) !== 'IHDR')
17
+ return;
18
+ const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
19
+ return width && height ? { width, height } : undefined;
20
+ }
21
+ export function guardImageRead(root, input, limit) {
22
+ if (typeof input.file_path !== 'string')
23
+ return {};
24
+ const relative = path.isAbsolute(input.file_path) ? path.relative(root, input.file_path) : input.file_path;
25
+ let file;
26
+ try {
27
+ file = resolveInside(root, relative);
28
+ }
29
+ catch {
30
+ return {};
31
+ } // Never inspect another project.
32
+ if (!existsSync(file) || !statSync(file).isFile() || statSync(file).size > 20 * 1024 * 1024)
33
+ return {};
34
+ const size = pngSize(readFileSync(file));
35
+ if (!size || size.width * size.height >= limit)
36
+ return {};
37
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
38
+ permissionDecisionReason: `当前会话的模型图片限制为 ${limit} 总像素;此 PNG 为 ${size.width}×${size.height}。原文件可用于应用,请读取父级按钮/组件/页面截图观察,不通过 Read 重送这张小图。` } };
39
+ }
40
+ export function compatibleImages(root, change, response, limit) {
41
+ // Claude currently delivers either content blocks directly or an MCP result envelope.
42
+ const envelope = response && typeof response === 'object' && !Array.isArray(response) ? response : undefined;
43
+ const blocks = Array.isArray(response) ? response : envelope?.content;
44
+ if (!Array.isArray(blocks))
45
+ return {};
46
+ let changed = false;
47
+ const raw = JSON.stringify(response);
48
+ const rawRel = `.opsx/figma-media/${sha256(raw)}.response.json`;
49
+ const content = blocks.map(block => {
50
+ if (!block || typeof block !== 'object' || block.type !== 'image')
51
+ return block;
52
+ const data = block.data ?? (block.source?.type === 'base64' ? block.source.data : undefined);
53
+ if (typeof data !== 'string')
54
+ return block;
55
+ const bytes = Buffer.from(data, 'base64'), size = pngSize(bytes);
56
+ if (!size || size.width * size.height >= limit)
57
+ return block;
58
+ const relative = `.opsx/figma-media/${sha256(bytes)}.png`;
59
+ const file = artifactPath(root, change, relative);
60
+ mkdirSync(path.dirname(file), { recursive: true });
61
+ if (existsSync(file)) {
62
+ if (!readFileSync(file).equals(bytes))
63
+ throw new Error('Figma 原图归档摘要不符');
64
+ }
65
+ else
66
+ writeFileSync(file, bytes, { flag: 'wx' });
67
+ changed = true;
68
+ return { type: 'text', text: JSON.stringify({ kind: 'figma-image-input-limit', ...size,
69
+ minImagePixels: limit, imagePath: artifactRel(change, relative), rawEvidencePath: artifactRel(change, rawRel),
70
+ instruction: '原图已完整保存,下载未失败;用于应用时保持原件。当前模型不接收此尺寸,请读取父级按钮/组件/页面截图观察,不通过 Read 重送小图。' }) };
71
+ });
72
+ if (!changed)
73
+ return {};
74
+ writeOnce(artifactPath(root, change, rawRel), raw);
75
+ return { hookSpecificOutput: { hookEventName: 'PostToolUse', updatedMCPToolOutput: envelope ? { ...envelope, content } : content } };
76
+ }
77
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1,9 @@
1
+ /** Claude moves large MCP results to its session directory BEFORE PostToolUse.
2
+ * Accept only that precise envelope and the current host session's tool-results file.
3
+ * Instructions inside the envelope or fetched source are data, never commands.
4
+ */
5
+ export declare function unwrapClaudeResponse(response: unknown, context: {
6
+ root: string;
7
+ sessionId?: string;
8
+ }, projectsRoot?: string): unknown;
9
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { homedir } from 'node:os';
2
+ import path from 'node:path';
3
+ import { readFileSync, statSync } from 'node:fs';
4
+ import { resolveInside } from '../shared/paths.js';
5
+ import { AppError } from '../shared/errors.js';
6
+ /** Claude moves large MCP results to its session directory BEFORE PostToolUse.
7
+ * Accept only that precise envelope and the current host session's tool-results file.
8
+ * Instructions inside the envelope or fetched source are data, never commands.
9
+ */
10
+ export function unwrapClaudeResponse(response, context, projectsRoot = path.join(homedir(), '.claude/projects')) {
11
+ if (typeof response !== 'string')
12
+ return response;
13
+ const match = response.match(/^Error: result \([\d,]+ characters\) exceeds maximum allowed tokens\. Output has been saved to ([^\n]+)\.\r?\nFormat: JSON array with schema:/);
14
+ if (!match)
15
+ return response;
16
+ if (!context.sessionId || !/^[a-f0-9-]{36}$/.test(context.sessionId))
17
+ throw new AppError('figma_session_invalid', '大响应缺少当前 Claude session id');
18
+ const base = path.join(projectsRoot, context.root.replace(/[^a-zA-Z0-9]/g, '-'), context.sessionId, 'tool-results');
19
+ const relative = path.relative(base, match[1]);
20
+ if (!/^mcp-(?:plugin_figma_figma|figma)-get_metadata-[\d]+\.txt$/.test(relative))
21
+ throw new AppError('figma_result_outside_session', '大响应文件不属于当前 Figma 调用的会话目录');
22
+ const file = resolveInside(base, relative, 'figma_result_outside_session');
23
+ if (!statSync(file).isFile() || statSync(file).size > 16 * 1024 * 1024)
24
+ throw new AppError('figma_response_too_large', '大响应文件无效或超过 16 MiB');
25
+ return JSON.parse(readFileSync(file, 'utf8'));
26
+ }
27
+ //# sourceMappingURL=response.js.map
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ import type { WorkerPacket } from '../core/types.js';
3
+ export declare const CommandContextEvidenceSchema: z.ZodObject<{
4
+ path: z.ZodString;
5
+ digest: z.ZodString;
6
+ }, z.core.$strict>;
7
+ export type CommandContextEvidence = z.infer<typeof CommandContextEvidenceSchema>;
8
+ /** Generic invocation facts; host policies remain outside the scheduler. */
9
+ export declare function prepareCommandContext(root: string, change: string, relative: string, runId: string, hookId: string, packet?: WorkerPacket): {
10
+ evidence: {
11
+ path: string;
12
+ digest: string;
13
+ };
14
+ env: {
15
+ OPENSPEC_COMMAND_CONTEXT: string;
16
+ };
17
+ };
18
+ export declare function commandContextCurrent(root: string, evidence: CommandContextEvidence): boolean;
19
+ //# sourceMappingURL=command-context.d.ts.map
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { z } from 'zod';
3
+ import { exists, writeJson } from '../shared/fs.js';
4
+ import { sha256File } from '../shared/hash.js';
5
+ import { resolveInside } from '../shared/paths.js';
6
+ import { codeChangeScope } from '../core/code-changes.js';
7
+ import { artifactRel, requireChange } from '../core/change.js';
8
+ export const CommandContextEvidenceSchema = z.object({ path: z.string(), digest: z.string() }).strict();
9
+ /** Generic invocation facts; host policies remain outside the scheduler. */
10
+ export function prepareCommandContext(root, change, relative, runId, hookId, packet) {
11
+ const state = requireChange(root, change);
12
+ const changesCode = packet && (packet.stepId.startsWith('code-') || packet.stepId === 'repair-verify');
13
+ const scope = changesCode ? codeChangeScope(root, state, packet) : {
14
+ workerChangedFiles: [], changedFiles: packet ? [] : state.engineChangedFiles, changeChangedFiles: state.engineChangedFiles,
15
+ };
16
+ const file = resolveInside(root, relative, 'command_context_outside_project');
17
+ writeJson(file, {
18
+ version: 1, projectRoot: root, change: { name: change, root: path.join(root, artifactRel(change, '.')) },
19
+ runId, hookId, worker: { id: packet?.workerId ?? runId, stepId: packet?.stepId ?? 'verify' },
20
+ task: packet?.task ?? null, ...scope,
21
+ });
22
+ return { evidence: { path: relative, digest: sha256File(file) }, env: { OPENSPEC_COMMAND_CONTEXT: file } };
23
+ }
24
+ export function commandContextCurrent(root, evidence) {
25
+ try {
26
+ const file = resolveInside(root, evidence.path, 'command_context_outside_project');
27
+ return exists(file) && sha256File(file) === evidence.digest;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ //# sourceMappingURL=command-context.js.map
@@ -0,0 +1,61 @@
1
+ import type { ProjectConfig, ProjectHook } from '../config/schema.js';
2
+ import type { ExecuteHookDescriptor, SourceCapability, SourceItem, TaskInitContext } from '../core/types.js';
3
+ export declare function hooksFor(config: ProjectConfig, stage: ProjectHook['stage']): ProjectHook[];
4
+ export interface HookEntryCheck {
5
+ hook: string;
6
+ use: string;
7
+ ok: boolean;
8
+ code?: string;
9
+ error?: string;
10
+ }
11
+ /**
12
+ * 按运行时相同的规则预先解析配置中的全部插件入口(collect 解析 adapter 并核对 capability,其余走 pluginHook),
13
+ * 让入口缺失、类型/阶段不符在安装与 doctor 时暴露,而不是等到某个阶段运行才失败。
14
+ * 只证明接得上,不证明插件行为与之前一致。
15
+ */
16
+ export declare function inspectHookEntries(projectRoot: string, config: ProjectConfig): HookEntryCheck[];
17
+ export declare function buildTaskInitContext(projectRoot: string, config: ProjectConfig, category: string): TaskInitContext;
18
+ /**
19
+ * repair-verify Worker 的初始化上下文:修复可能触及任何已完成 task 的代码,
20
+ * 因此下发这些 task 所属分类的 notes 以及匹配其中任一分类的 execute Hook(按 id 去重,保持配置顺序)。
21
+ */
22
+ export declare function buildRepairInitContext(projectRoot: string, config: ProjectConfig, categories: string[]): TaskInitContext;
23
+ /** parse/tech/plan Worker 的初始化上下文:该 stage 全部 hook 都以 descriptor 下发并要求回执。 */
24
+ export declare function buildStageInitContext(projectRoot: string, config: ProjectConfig, stage: 'parse' | 'tech' | 'plan', capabilities?: SourceCapability[]): TaskInitContext;
25
+ /** verify Worker 需要登记证据的 hook 清单(command 由引擎执行,也一并列出便于核对)。 */
26
+ export declare function buildVerifyHookDescriptors(projectRoot: string, config: ProjectConfig): ExecuteHookDescriptor[];
27
+ /**
28
+ * 在项目锁定的 marketplace 插件中查找 `skills/<name>/SKILL.md`。
29
+ * 宿主(如 Cursor)没有把 marketplace 插件注册成 skill 时,Worker 仍可按该路径读取。
30
+ * 任何 marketplace 未锁定 / cache 缺失都静默返回 undefined:path 只是提示,不是硬依赖。
31
+ */
32
+ export declare function resolveSkillPath(projectRoot: string, config: ProjectConfig, skillName: string): string | undefined;
33
+ export declare function hookDescriptor(projectRoot: string, hook: ProjectHook, config: ProjectConfig): ExecuteHookDescriptor;
34
+ export interface CollectOutcome {
35
+ artifacts: string[];
36
+ sourceArtifacts: Array<{
37
+ sourceId: string;
38
+ path: string;
39
+ }>;
40
+ deferred: string[];
41
+ /** 没有 adapter 的 url source,被同 capability 的手工 file/text source 覆盖:`<hookId|unhandled>:<urlSourceId>:<manualSourceId>` */
42
+ covered: string[];
43
+ warnings: string[];
44
+ }
45
+ /**
46
+ * 按 capability 执行 collect 阶段 hook;无 adapter 时允许 notes/手工源。
47
+ *
48
+ * skill 型 hook(或没有 `use` 的 hook)无法自动采集 url source。skills 的 collect 节点要求 agent 用宿主工具人工采集后
49
+ * 以 `--source-file <capability>=<path>` 登记再重新 collect,因此同 capability 只要存在手工 file/text source,
50
+ * 对应 url source 就视为已被覆盖:它的 artifact 指向该手工产物,不再阻塞进入 parse。
51
+ */
52
+ export declare function collectSources(projectRoot: string, changeName: string, config: ProjectConfig, items: SourceItem[], options?: {
53
+ refresh?: boolean;
54
+ checkpoint?: (sourceId: string, artifact: string, collection: NonNullable<SourceItem['collection']>) => void;
55
+ }): CollectOutcome;
56
+ export declare function fileHookDigest(projectRoot: string, hook: ProjectHook): string | null;
57
+ /** preset scaffold 出来的模板带此标记;团队填写完删掉它,file hook 才算就位。 */
58
+ export declare const TEMPLATE_MARKER = "<!-- openspec:fill-me -->";
59
+ export type FileHookState = 'missing' | 'unfilled' | 'ready';
60
+ export declare function fileHookState(projectRoot: string, relativePath: string): FileHookState;
61
+ //# sourceMappingURL=engine.d.ts.map