@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,478 @@
1
+ import path from 'node:path';
2
+ import { codeChangeScope, retainCodeChanges } from './code-changes.js';
3
+ import { prepareWorkerEvidence, validateWorkerEvidence } from './worker-evidence.js';
4
+ import { pluginBindingCurrent } from '../hooks/plugin-runtime.js';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { exists, readJson, writeJson } from '../shared/fs.js';
7
+ import { AppError } from '../shared/errors.js';
8
+ import { captureDiffBaseline, changedFilesSince, parseDiffBaseline } from '../shared/git.js';
9
+ import { effectiveTaskCategories } from '../config/schema.js';
10
+ import { buildRepairInitContext, buildStageInitContext, buildTaskInitContext, buildVerifyHookDescriptors, fileHookState } from '../hooks/engine.js';
11
+ import { artifactRel, requireChange } from './change.js';
12
+ import { assertStepAllowed } from './flow.js';
13
+ import { nextPendingTask, requireValidTaskPlan, taskProgress } from './tasks.js';
14
+ import { appendEvent, saveState, workersDir } from './state.js';
15
+ import { loadLedger } from './sources.js';
16
+ import { loadVerifiedReport } from './verify.js';
17
+ import { isOpsxInternal, resolveInside } from '../shared/paths.js';
18
+ import { NODES, PACKET_VERSION, WorkerPacketSchema, TaskStateSchema, WorkerResultSchema, } from './types.js';
19
+ const baselines = new Map();
20
+ export function createWorker(projectRoot, changeName, stepId, config, options = {}) {
21
+ const state = requireChange(projectRoot, changeName);
22
+ let previous;
23
+ let retainedBaseline;
24
+ if (options.refresh && !state.activeWorker)
25
+ throw new AppError('no_active_worker', '刷新需要同一步骤的活动 Worker');
26
+ if (state.activeWorker && state.activeWorker.stepId === stepId) {
27
+ const existing = WorkerPacketSchema.safeParse(readJson(resolveInside(projectRoot, state.activeWorker.packet)));
28
+ if (!existing.success)
29
+ throw new AppError('invalid_worker_packet', '现有 worker packet 不合法', existing.error.issues);
30
+ assertPacketMatchesState(state, existing.data);
31
+ if (!options.refresh) {
32
+ prepareWorkerEvidence(projectRoot, changeName, existing.data);
33
+ // scout 可能在首次 create 之后才写出摘要;重派时把它暴露给 build/repair。
34
+ if (attachTaskContext(projectRoot, existing.data))
35
+ writeJson(resolveInside(projectRoot, state.activeWorker.packet), existing.data);
36
+ return { packet: existing.data, path: state.activeWorker.packet };
37
+ }
38
+ previous = existing.data;
39
+ if (isCodeChangingStep(stepId)) {
40
+ retainedBaseline = readBaseline(projectRoot, state, previous.workerId);
41
+ // Refreshing a binding must never reset the task's original diff boundary.
42
+ changedFilesSince(projectRoot, retainedBaseline);
43
+ }
44
+ }
45
+ if (state.activeWorker && !previous) {
46
+ throw new AppError('worker_active', `已有进行中的 worker: ${state.activeWorker.workerId}`);
47
+ }
48
+ if (state.node === NODES.COLLECT && stepId === 'parse' && state.collectedRevision >= state.sourceRevision && state.sourceRevision > 0) {
49
+ state.node = NODES.PARSE;
50
+ }
51
+ assertStepAllowed(state, stepId);
52
+ const workerId = `${stepId}-${randomUUID()}`;
53
+ const dir = workersDir(projectRoot, changeName);
54
+ const packetRel = `${artifactRel(changeName, '.opsx/workers')}/${workerId}.packet.json`;
55
+ const resultRel = `${artifactRel(changeName, '.opsx/workers')}/${workerId}.result.json`;
56
+ const packet = buildPacket(projectRoot, state, stepId, workerId, resultRel, config);
57
+ if (previous?.decisions)
58
+ packet.decisions = previous.decisions;
59
+ const baseline = retainedBaseline ?? (isCodeChangingStep(stepId) ? captureDiffBaseline(projectRoot) : null);
60
+ prepareWorkerEvidence(projectRoot, changeName, packet);
61
+ attachTaskContext(projectRoot, packet);
62
+ if (Object.keys(state.codeChanges?.filesByStep ?? {}).length)
63
+ packet.refs.codeChanges = artifactRel(changeName, '.opsx/code-changes.json');
64
+ writeJson(path.join(projectRoot, packetRel), packet);
65
+ if (baseline) {
66
+ baselines.set(baselineKey(projectRoot, workerId), baseline);
67
+ writeJson(path.join(dir, `${workerId}.baseline.json`), baseline);
68
+ state.codeChanges ??= { baseline: `${artifactRel(changeName, '.opsx/workers')}/${workerId}.baseline.json`, filesByStep: {} };
69
+ }
70
+ state.activeWorker = {
71
+ workerId,
72
+ stepId,
73
+ packet: packetRel,
74
+ expectedResult: resultRel,
75
+ createdAt: new Date().toISOString(),
76
+ };
77
+ if (stepId === 'parse')
78
+ state.node = NODES.PARSE;
79
+ if (stepId === 'verify')
80
+ state.verifyRunId = workerId;
81
+ saveState(projectRoot, state);
82
+ if (previous) {
83
+ baselines.delete(baselineKey(projectRoot, previous.workerId));
84
+ appendEvent(projectRoot, changeName, 'worker.cancelled', {
85
+ workerId: previous.workerId, reason: 'explicit refresh', replacementWorkerId: workerId,
86
+ preservedBaseline: Boolean(retainedBaseline),
87
+ });
88
+ }
89
+ appendEvent(projectRoot, changeName, 'worker.created', { workerId, stepId });
90
+ return { packet, path: packetRel };
91
+ }
92
+ export function acceptWorker(projectRoot, changeName, resultPath, config) {
93
+ const state = requireChange(projectRoot, changeName);
94
+ if (!state.activeWorker)
95
+ throw new AppError('no_active_worker', '当前没有待验收的 worker');
96
+ const expectedAbs = resolveInside(projectRoot, state.activeWorker.expectedResult, 'worker_result_outside_project');
97
+ const actualAbs = path.isAbsolute(resultPath) ? resultPath : path.join(projectRoot, resultPath);
98
+ if (path.resolve(expectedAbs) !== path.resolve(actualAbs)) {
99
+ throw new AppError('result_path_mismatch', `result 路径必须是 ${state.activeWorker.expectedResult}`);
100
+ }
101
+ if (!exists(actualAbs))
102
+ throw new AppError('result_missing', '找不到 worker result 文件');
103
+ const parsed = WorkerResultSchema.safeParse(readJson(actualAbs));
104
+ if (!parsed.success)
105
+ throw new AppError('invalid_worker_result', 'worker result 不合法', parsed.error.issues);
106
+ const result = parsed.data;
107
+ if (result.workerId !== state.activeWorker.workerId || result.stepId !== state.activeWorker.stepId) {
108
+ throw new AppError('result_mismatch', 'result 的 workerId/stepId 与 packet 不一致');
109
+ }
110
+ if (!['completed', 'failed', 'blocked'].includes(result.status)) {
111
+ throw new AppError('invalid_result_status', 'status 只能是 completed / failed / blocked');
112
+ }
113
+ const packetParsed = WorkerPacketSchema.safeParse(readJson(resolveInside(projectRoot, state.activeWorker.packet, 'worker_packet_outside_project')));
114
+ if (!packetParsed.success)
115
+ throw new AppError('invalid_worker_packet', 'worker packet 不合法', packetParsed.error.issues);
116
+ assertPacketMatchesState(state, packetParsed.data);
117
+ validateExecuteHookResults(projectRoot, packetParsed.data, result);
118
+ if (result.executeHookResults?.length) {
119
+ appendEvent(projectRoot, changeName, 'worker.execute_hooks', {
120
+ workerId: result.workerId,
121
+ results: result.executeHookResults,
122
+ });
123
+ }
124
+ const now = new Date().toISOString();
125
+ if (result.status === 'blocked') {
126
+ state.lastIssue = { type: 'worker_blocked', stepId: result.stepId, questions: result.questions, updatedAt: now, reason: result.notes };
127
+ appendEvent(projectRoot, changeName, 'worker.blocked', { workerId: result.workerId });
128
+ saveState(projectRoot, state);
129
+ return state;
130
+ }
131
+ if (result.status === 'failed') {
132
+ state.lastIssue = {
133
+ type: 'worker_failed',
134
+ stepId: result.stepId,
135
+ fixable: false,
136
+ reason: result.notes,
137
+ updatedAt: now,
138
+ };
139
+ appendEvent(projectRoot, changeName, 'worker.failed', { workerId: result.workerId });
140
+ saveState(projectRoot, state);
141
+ return state;
142
+ }
143
+ validateWorkerEvidence(projectRoot, changeName, packetParsed.data);
144
+ if (result.skipTech && packetParsed.data.skipTechAllowed === false)
145
+ throw new AppError('required_tech_command', 'required tech command 存在,不能跳过 tech');
146
+ const changedFiles = applyCompleted(projectRoot, state, result, config);
147
+ state.activeWorker = null;
148
+ saveState(projectRoot, state);
149
+ appendEvent(projectRoot, changeName, 'worker.accepted', { workerId: result.workerId, stepId: result.stepId, node: state.node, changedFiles, notes: result.notes });
150
+ return state;
151
+ }
152
+ /**
153
+ * 记录用户对 blocked Worker 的答复:写入 packet.decisions 并清除 blocked issue。
154
+ * 之后 `worker create` 同一 step 会复用该 packet,Worker 按 decisions 完成产物后重新 accept。
155
+ */
156
+ export function answerWorker(projectRoot, changeName, answers) {
157
+ const state = requireChange(projectRoot, changeName);
158
+ if (!state.activeWorker)
159
+ throw new AppError('no_active_worker', '当前没有等待答复的 worker');
160
+ if (state.lastIssue?.type !== 'worker_blocked') {
161
+ throw new AppError('worker_not_blocked', '当前 worker 不处于 blocked 状态,无需答复');
162
+ }
163
+ const cleaned = answers.map((answer) => answer.trim()).filter(Boolean);
164
+ if (cleaned.length === 0)
165
+ throw new AppError('empty_answers', '至少需要一条答复');
166
+ const packetAbs = resolveInside(projectRoot, state.activeWorker.packet, 'worker_packet_outside_project');
167
+ const parsed = WorkerPacketSchema.safeParse(readJson(packetAbs));
168
+ if (!parsed.success)
169
+ throw new AppError('invalid_worker_packet', 'worker packet 不合法', parsed.error.issues);
170
+ assertPacketMatchesState(state, parsed.data);
171
+ const decision = { questions: state.lastIssue.questions ?? [], answers: cleaned, answeredAt: new Date().toISOString() };
172
+ const packet = { ...parsed.data, decisions: [...(parsed.data.decisions ?? []), decision] };
173
+ writeJson(packetAbs, packet);
174
+ state.lastIssue = null;
175
+ saveState(projectRoot, state);
176
+ appendEvent(projectRoot, changeName, 'worker.answered', { workerId: state.activeWorker.workerId, ...decision });
177
+ return { state, packet, path: state.activeWorker.packet };
178
+ }
179
+ export function cancelWorker(projectRoot, changeName, reason) {
180
+ const state = requireChange(projectRoot, changeName);
181
+ if (!state.activeWorker)
182
+ throw new AppError('no_active_worker', '当前没有可取消的 worker');
183
+ if (isCodeChangingStep(state.activeWorker.stepId)) {
184
+ const scope = codeChangeScope(projectRoot, state, state.activeWorker);
185
+ retainCodeChanges(projectRoot, state, state.activeWorker, scope.changedFiles, true);
186
+ }
187
+ appendEvent(projectRoot, changeName, 'worker.cancelled', { workerId: state.activeWorker.workerId, reason, retainedFiles: state.codeChanges?.filesByStep[state.activeWorker.stepId] ?? [] });
188
+ baselines.delete(baselineKey(projectRoot, state.activeWorker.workerId));
189
+ state.activeWorker = null;
190
+ saveState(projectRoot, state);
191
+ return state;
192
+ }
193
+ function applyCompleted(projectRoot, state, result, config) {
194
+ state.lastIssue = null;
195
+ const stepId = result.stepId;
196
+ if (stepId === 'parse') {
197
+ assertArtifact(projectRoot, state.changeName, 'spec.md');
198
+ if (result.skipTech) {
199
+ const reason = result.notes?.trim();
200
+ if (!reason)
201
+ throw new AppError('skip_tech_reason_required', 'skipTech 需要在 notes 说明满足跳过判据的理由');
202
+ appendEvent(projectRoot, state.changeName, 'worker.tech_skipped', { workerId: result.workerId, reason });
203
+ }
204
+ state.node = result.skipTech ? NODES.PLAN : NODES.TECH;
205
+ state.techSkipped = Boolean(result.skipTech);
206
+ return;
207
+ }
208
+ if (stepId === 'tech') {
209
+ assertArtifact(projectRoot, state.changeName, 'tech.md');
210
+ state.node = NODES.PLAN;
211
+ return;
212
+ }
213
+ if (stepId === 'plan') {
214
+ const plan = readJson(path.join(projectRoot, artifactRel(state.changeName, 'plan/task-plan.json')));
215
+ const categories = Object.keys(effectiveTaskCategories(config));
216
+ const sourceRefs = loadLedger(projectRoot, state.changeName, state.sourceRevision).items.map((item) => item.id);
217
+ const tasks = requireValidTaskPlan(plan, categories, sourceRefs).map((task) => TaskStateSchema.parse(task));
218
+ const unmapped = Object.entries(state.codeChanges?.filesByStep ?? {})
219
+ .filter(([step, files]) => files.length && state.codeChanges?.pendingSteps?.includes(step) && step.startsWith('code-') && !tasks.some((task) => `code-${task.id}` === step));
220
+ if (unmapped.length)
221
+ throw new AppError('retained_task_unmapped', '新计划必须保留尚需复核的任务 ID 与文件归属', { retained: Object.fromEntries(unmapped) });
222
+ state.tasks = tasks;
223
+ state.node = NODES.GATE_A;
224
+ if (state.sourceUpdate?.hadCompletedTasks) {
225
+ writeJson(path.join(projectRoot, artifactRel(state.changeName, `plan/source-reconciliation-r${state.sourceRevision}.json`)), {
226
+ revision: state.sourceRevision,
227
+ sourceIds: state.sourceUpdate.changedSourceIds,
228
+ completedTaskIds: state.sourceUpdate.completedTaskIds,
229
+ });
230
+ }
231
+ return;
232
+ }
233
+ if (stepId.startsWith('code-')) {
234
+ const taskId = stepId.slice('code-'.length);
235
+ const expectedTask = nextPendingTask(state);
236
+ if (!expectedTask || expectedTask.id !== taskId) {
237
+ throw new AppError('task_not_ready', `当前只允许执行下一个 pending task: ${expectedTask?.id ?? 'none'}`);
238
+ }
239
+ const task = state.tasks.find((item) => item.id === taskId);
240
+ if (!task)
241
+ throw new AppError('unknown_task', `找不到 task: ${taskId}`);
242
+ const actual = codeChangeScope(projectRoot, state, result).changedFiles;
243
+ const declared = [...new Set(result.changedFiles ?? [])].filter((file) => !isOpsxInternal(file)).sort();
244
+ if (JSON.stringify(declared) !== JSON.stringify(actual)) {
245
+ appendEvent(projectRoot, state.changeName, 'worker.changed_files_mismatch', {
246
+ workerId: result.workerId,
247
+ declared,
248
+ actual,
249
+ });
250
+ // Omitted declarations retain legacy engine-computed semantics; explicit claims must match.
251
+ if (result.changedFiles !== undefined)
252
+ throw new AppError('worker_changed_files_mismatch', '声明文件与任务累计待验收范围不一致;核对原始基线和保留文件后重新声明', { declared, actual });
253
+ }
254
+ task.status = 'completed';
255
+ task.completedAt = new Date().toISOString();
256
+ retainCodeChanges(projectRoot, state, result, actual);
257
+ baselines.delete(baselineKey(projectRoot, result.workerId));
258
+ const progress = taskProgress(state);
259
+ state.node = progress.completed === progress.total ? NODES.GATE_B : NODES.CODE;
260
+ state.verifyRunId = null;
261
+ return actual;
262
+ }
263
+ if (stepId === 'repair-verify') {
264
+ const actual = codeChangeScope(projectRoot, state, result).changedFiles;
265
+ const declared = [...new Set(result.changedFiles ?? [])].filter((file) => !isOpsxInternal(file)).sort();
266
+ if (JSON.stringify(declared) !== JSON.stringify(actual)) {
267
+ appendEvent(projectRoot, state.changeName, 'worker.changed_files_mismatch', {
268
+ workerId: result.workerId,
269
+ declared,
270
+ actual,
271
+ });
272
+ // Omitted declarations retain legacy engine-computed semantics; explicit claims must match.
273
+ if (result.changedFiles !== undefined)
274
+ throw new AppError('worker_changed_files_mismatch', '声明文件与任务累计待验收范围不一致;核对原始基线和保留文件后重新声明', { declared, actual });
275
+ }
276
+ retainCodeChanges(projectRoot, state, result, actual);
277
+ baselines.delete(baselineKey(projectRoot, result.workerId));
278
+ state.node = NODES.VERIFY;
279
+ state.repair = null;
280
+ state.verifyRunId = null;
281
+ return actual;
282
+ }
283
+ if (stepId === 'verify') {
284
+ if (!state.verifyRunId || state.verifyRunId !== result.workerId) {
285
+ throw new AppError('verify_run_mismatch', 'verify Worker 与当前 verify run 不一致');
286
+ }
287
+ const report = loadVerifiedReport(projectRoot, state.changeName, state.verifyRunId);
288
+ if (report.status === 'failed') {
289
+ state.lastIssue = {
290
+ type: 'worker_failed',
291
+ stepId: 'verify',
292
+ fixable: true,
293
+ reason: 'QA 未通过',
294
+ updatedAt: new Date().toISOString(),
295
+ };
296
+ state.node = NODES.VERIFY;
297
+ return;
298
+ }
299
+ state.node = NODES.GATE_C;
300
+ state.repair = null;
301
+ }
302
+ }
303
+ function buildPacket(projectRoot, state, stepId, workerId, expectedResult, config) {
304
+ const refs = {
305
+ spec: artifactRel(state.changeName, 'spec.md'),
306
+ taskPlan: artifactRel(state.changeName, 'plan/task-plan.json'),
307
+ sources: artifactRel(state.changeName, `sources/sources-r${state.sourceRevision}.json`),
308
+ };
309
+ if (!state.techSkipped)
310
+ refs.tech = artifactRel(state.changeName, 'tech.md');
311
+ const packet = {
312
+ version: PACKET_VERSION,
313
+ workerId,
314
+ stepId,
315
+ changeName: state.changeName,
316
+ node: state.node,
317
+ refs,
318
+ expectedResult,
319
+ };
320
+ const sourceCapabilities = loadLedger(projectRoot, state.changeName, state.sourceRevision).items.map((item) => item.capability);
321
+ if (['parse', 'tech', 'plan'].includes(stepId))
322
+ packet.executionEvidence = true;
323
+ if (stepId === 'parse') {
324
+ packet.skipTechAllowed = !(config.hooks ?? []).some((hook) => hook.stage === 'tech' && hook.type === 'command' && hook.required !== false && (!hook.capability || sourceCapabilities.includes(hook.capability)));
325
+ packet.initContext = buildStageInitContext(projectRoot, config, 'parse', sourceCapabilities);
326
+ }
327
+ if (stepId === 'tech')
328
+ packet.initContext = buildStageInitContext(projectRoot, config, 'tech', sourceCapabilities);
329
+ if (stepId === 'plan') {
330
+ const categories = effectiveTaskCategories(config);
331
+ packet.allowedCategories = Object.keys(categories);
332
+ packet.planNotes = planNotes(config);
333
+ packet.initContext = buildStageInitContext(projectRoot, config, 'plan', sourceCapabilities);
334
+ }
335
+ if (stepId.startsWith('code-')) {
336
+ packet.executionEvidence = true;
337
+ const taskId = stepId.slice('code-'.length);
338
+ const scheduled = state.tasks.find((item) => item.id === taskId);
339
+ if (!scheduled)
340
+ throw new AppError('unknown_task', `找不到 task: ${taskId}`);
341
+ packet.initContext = buildTaskInitContext(projectRoot, config, scheduled.category);
342
+ const plan = readJson(resolveInside(projectRoot, refs.taskPlan));
343
+ const tasks = requireValidTaskPlan(plan, Object.keys(effectiveTaskCategories(config)));
344
+ const task = tasks.find((item) => item.id === taskId);
345
+ if (!task)
346
+ throw new AppError('unknown_task', `找不到 task: ${taskId}`);
347
+ if (task.title !== scheduled.title || task.category !== scheduled.category || task.type !== scheduled.type
348
+ || JSON.stringify(task.dependsOn) !== JSON.stringify(scheduled.dependsOn)) {
349
+ throw new AppError('task_plan_state_mismatch', `task-plan 与已确认调度状态不一致: ${taskId}`);
350
+ }
351
+ packet.task = {
352
+ id: task.id,
353
+ title: task.title,
354
+ category: task.category,
355
+ type: task.type,
356
+ refScope: task.refScope,
357
+ dependsOn: task.dependsOn,
358
+ notes: task.notes,
359
+ designRefs: task.designRefs,
360
+ sourceRefs: task.sourceRefs,
361
+ capability: task.capability,
362
+ operation: task.operation,
363
+ provider: task.provider,
364
+ };
365
+ }
366
+ const designSources = loadLedger(projectRoot, state.changeName, state.sourceRevision).items
367
+ .filter((source) => source.capability === 'design');
368
+ if (designSources.length)
369
+ packet.refs.design = designSources.map((source) => ({
370
+ sourceId: source.id, kind: source.kind,
371
+ locator: source.kind === 'text' ? undefined : source.locator,
372
+ artifact: source.artifact ? artifactRel(state.changeName, source.artifact) : undefined,
373
+ }));
374
+ if (stepId === 'verify')
375
+ packet.verifyHooks = buildVerifyHookDescriptors(projectRoot, config);
376
+ if (stepId === 'repair-verify') {
377
+ packet.executionEvidence = true;
378
+ if (!state.repair)
379
+ throw new AppError('repair_not_requested', '当前没有 verify repair 请求');
380
+ packet.refs.qaReport = artifactRel(state.changeName, `.opsx/verify/${state.repair.sourceVerifyRunId}/qa-report.json`);
381
+ packet.initContext = buildRepairInitContext(projectRoot, config, state.tasks.filter((task) => task.status === 'completed').map((task) => task.category));
382
+ }
383
+ if (packet.executionEvidence)
384
+ packet.executionEvidenceVersion = 2;
385
+ const parsed = WorkerPacketSchema.safeParse(packet);
386
+ if (!parsed.success)
387
+ throw new AppError('invalid_worker_packet', '生成的 worker packet 不合法', parsed.error.issues);
388
+ return parsed.data;
389
+ }
390
+ /** scout 子代理写出的 task 摘要路径;按 taskId 固定,与 workerId 无关,返修与重派都能复用。 */
391
+ export function taskContextRel(changeName, taskId) {
392
+ return `${artifactRel(changeName, '.opsx/workers')}/${taskId}.context.md`;
393
+ }
394
+ /**
395
+ * 摘要存在时通过 `refs.taskContext` 暴露给 build/repair;不存在则不加字段,也不做任何校验。
396
+ * 摘要是普通 markdown,不进 schema,避免重蹈引擎级上下文投影的复杂度。
397
+ */
398
+ function attachTaskContext(projectRoot, packet) {
399
+ if (!packet.stepId.startsWith('code-'))
400
+ return false;
401
+ const relative = taskContextRel(packet.changeName, packet.stepId.slice('code-'.length));
402
+ if (packet.refs.taskContext === relative || !exists(path.join(projectRoot, relative)))
403
+ return false;
404
+ packet.refs.taskContext = relative;
405
+ return true;
406
+ }
407
+ /** 只列各分类说明;plan stage 的 hook 走 initContext descriptor 并要求回执。 */
408
+ function planNotes(config) {
409
+ return Object.entries(effectiveTaskCategories(config))
410
+ .map(([id, value]) => `${id}: ${value.notes}`)
411
+ .join('\n');
412
+ }
413
+ function assertArtifact(projectRoot, changeName, relative) {
414
+ if (!exists(path.join(projectRoot, artifactRel(changeName, relative)))) {
415
+ throw new AppError('artifact_missing', `缺少产物: ${relative}`);
416
+ }
417
+ }
418
+ function readBaseline(projectRoot, state, workerId) {
419
+ const cached = baselines.get(baselineKey(projectRoot, workerId));
420
+ if (cached)
421
+ return cached;
422
+ const file = path.join(projectRoot, artifactRel(state.changeName, `.opsx/workers/${workerId}.baseline.json`));
423
+ if (!exists(file))
424
+ throw new AppError('worker_baseline_missing', `缺少 worker baseline: ${workerId}`);
425
+ return parseDiffBaseline(readJson(file));
426
+ }
427
+ function baselineKey(projectRoot, workerId) {
428
+ return `${path.resolve(projectRoot)}\0${workerId}`;
429
+ }
430
+ function isCodeChangingStep(stepId) {
431
+ return stepId.startsWith('code-') || stepId === 'repair-verify';
432
+ }
433
+ function assertPacketMatchesState(state, packet) {
434
+ if (!state.activeWorker
435
+ || packet.workerId !== state.activeWorker.workerId
436
+ || packet.stepId !== state.activeWorker.stepId
437
+ || packet.changeName !== state.changeName
438
+ || packet.expectedResult !== state.activeWorker.expectedResult) {
439
+ throw new AppError('worker_packet_mismatch', 'worker packet 与当前权威状态不一致');
440
+ }
441
+ }
442
+ function validateExecuteHookResults(projectRoot, packet, result) {
443
+ const hooks = packet.initContext?.hooks ?? [];
444
+ const known = new Map(hooks.map((hook) => [hook.id, hook]));
445
+ const seen = new Set();
446
+ const results = result.executeHookResults ?? [];
447
+ for (const hookResult of results) {
448
+ if (!known.has(hookResult.id))
449
+ throw new AppError('unknown_execute_hook_result', `result 包含未知 execute hook: ${hookResult.id}`);
450
+ if (seen.has(hookResult.id))
451
+ throw new AppError('duplicate_execute_hook_result', `result 重复 execute hook: ${hookResult.id}`);
452
+ seen.add(hookResult.id);
453
+ }
454
+ if (result.status !== 'completed')
455
+ return;
456
+ const byId = new Map(results.map((item) => [item.id, item]));
457
+ for (const hook of hooks) {
458
+ if (hook.plugin && byId.get(hook.id)?.status === 'passed' && !pluginBindingCurrent(projectRoot, hook.plugin)) {
459
+ throw new AppError('plugin_identity_changed', `插件身份已变化,重新创建 Worker: ${hook.id}`);
460
+ }
461
+ if (hook.type !== 'file' || byId.get(hook.id)?.status !== 'passed')
462
+ continue;
463
+ const state = fileHookState(projectRoot, hook.path);
464
+ if (state === 'missing') {
465
+ throw new AppError('execute_hook_file_missing', `file Hook 不存在,不能回执 passed: ${hook.id}`);
466
+ }
467
+ if (state === 'unfilled') {
468
+ throw new AppError('execute_hook_file_unfilled', `file Hook 仍是未填写的模板(含 openspec:fill-me 标记),不能回执 passed: ${hook.id} → ${hook.path}`);
469
+ }
470
+ }
471
+ const failures = hooks
472
+ .filter((hook) => hook.required && byId.get(hook.id)?.status !== 'passed')
473
+ .map((hook) => hook.id);
474
+ if (failures.length > 0) {
475
+ throw new AppError('required_execute_hooks_incomplete', `必需 execute Hook 未通过: ${failures.join(', ')}`, { hooks: failures });
476
+ }
477
+ }
478
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1,4 @@
1
+ import { type ExecResult } from '../shared/exec.js';
2
+ /** Native install is a no-op for existing registrations; upgrade and verify its actual files. */
3
+ export declare function installClaudePlugin(projectRoot: string, selector: string, expectedRoot: string): ExecResult;
4
+ //# sourceMappingURL=claude-install.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ import { execFile } from '../shared/exec.js';
4
+ import { digestDirectory } from '../shared/hash.js';
5
+ const InstalledPlugin = z.object({
6
+ id: z.string(), scope: z.string(), projectPath: z.string().optional(),
7
+ enabled: z.boolean(), installPath: z.string(),
8
+ });
9
+ /** Native install is a no-op for existing registrations; upgrade and verify its actual files. */
10
+ export function installClaudePlugin(projectRoot, selector, expectedRoot) {
11
+ const run = (args) => execFile('claude', ['plugin', ...args], { cwd: projectRoot });
12
+ const installed = run(['install', selector, '--scope', 'local', '--yes']);
13
+ if (!installed.ok)
14
+ return installed;
15
+ const updated = run(['update', selector, '--scope', 'local', '--yes']);
16
+ if (!updated.ok)
17
+ return updated;
18
+ const listed = run(['list', '--json']);
19
+ if (!listed.ok)
20
+ return listed;
21
+ try {
22
+ const root = realpathSync(projectRoot);
23
+ const sameProject = (candidate) => {
24
+ try {
25
+ return realpathSync(candidate) === root;
26
+ }
27
+ catch {
28
+ return false;
29
+ } // Native history may retain deleted worktrees from other tasks.
30
+ };
31
+ const matches = z.array(InstalledPlugin).parse(JSON.parse(listed.stdout)).filter(item => item.id === selector && item.scope === 'local' && item.projectPath
32
+ && sameProject(item.projectPath));
33
+ if (matches.length !== 1 || !matches[0].enabled)
34
+ throw new Error('未找到唯一启用的 local 安装记录');
35
+ if (digestDirectory(matches[0].installPath) !== digestDirectory(expectedRoot)) {
36
+ throw new Error('原生缓存内容与固定制品不一致;确认发布版本已更新,重新升级后再验证');
37
+ }
38
+ return { ok: true, code: 0, stdout: updated.stdout, stderr: '' };
39
+ }
40
+ catch (error) {
41
+ return { ok: false, code: 1, stdout: '', stderr: `Claude 插件实装校验失败 (${selector}): ${String(error)}` };
42
+ }
43
+ }
44
+ //# sourceMappingURL=claude-install.js.map
@@ -0,0 +1,3 @@
1
+ /** Native plugin clients receive the same runtime allowlist as npm consumers. */
2
+ export declare function preparePluginDistribution(root: string): string;
3
+ //# sourceMappingURL=distribution.d.ts.map
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { renameSync, statSync } from 'node:fs';
4
+ import { copyPath, ensureDir, exists, listFilesRecursive, readJson, removePath } from '../shared/fs.js';
5
+ import { digestDirectory, sha256, sha256File } from '../shared/hash.js';
6
+ import { openspecHome } from '../marketplace/adapters.js';
7
+ import { AppError } from '../shared/errors.js';
8
+ import { matchPattern, resolveInside } from '../shared/paths.js';
9
+ /** Native plugin clients receive the same runtime allowlist as npm consumers. */
10
+ export function preparePluginDistribution(root) {
11
+ const manifest = readJson(path.join(root, 'package.json'));
12
+ const files = manifest.files.filter((file) => !file.startsWith('!'));
13
+ // Patterns cannot describe a reviewable native artifact; the package uses explicit paths.
14
+ if (files.some((file) => /[*?]/.test(file) || path.isAbsolute(file) || file.split('/').includes('..'))) {
15
+ throw new AppError('invalid_distribution_files', '插件分发需要明确的 package.files 路径');
16
+ }
17
+ for (const required of ['package.json', 'LICENSE'])
18
+ if (!files.includes(required))
19
+ files.push(required);
20
+ const contents = files.filter((file) => exists(resolveInside(root, file)));
21
+ const digest = sha256(contents.map((file) => {
22
+ const absolute = path.join(root, file);
23
+ return `${file}:${statSync(absolute).isDirectory() ? digestDirectory(absolute) : sha256File(absolute)}`;
24
+ }).join('\n'));
25
+ const base = path.join(openspecHome(), 'distributions');
26
+ const target = path.join(base, digest);
27
+ const temp = path.join(base, `.prepare-${randomUUID()}`);
28
+ ensureDir(temp);
29
+ try {
30
+ for (const file of contents)
31
+ copyPath(path.join(root, file), path.join(temp, file));
32
+ const excluded = manifest.files.filter((file) => file.startsWith('!')).map((file) => file.slice(1));
33
+ for (const file of listFilesRecursive(temp)) {
34
+ const relative = path.relative(temp, file).replaceAll('\\', '/');
35
+ if (excluded.some((pattern) => matchPattern(relative, pattern) || matchPattern(relative, pattern.replaceAll('/**/', '/'))))
36
+ removePath(file);
37
+ }
38
+ const expected = digestDirectory(temp);
39
+ if (exists(target)) {
40
+ if (digestDirectory(target) !== expected)
41
+ throw new AppError('distribution_changed', '已准备的插件制品被修改');
42
+ removePath(temp);
43
+ }
44
+ else
45
+ renameSync(temp, target);
46
+ return target;
47
+ }
48
+ catch (error) {
49
+ removePath(temp);
50
+ throw error;
51
+ }
52
+ }
53
+ //# sourceMappingURL=distribution.js.map
@@ -0,0 +1,36 @@
1
+ import { type WorkflowName } from '../core/types.js';
2
+ export type HostId = 'claude' | 'codex' | 'cursor';
3
+ export type DeliveryStatus = 'installed' | 'pending_user_action' | 'skipped_by_user' | 'failed';
4
+ export interface DeliveryResult {
5
+ host: HostId;
6
+ status: DeliveryStatus;
7
+ digest: string;
8
+ skills: string[];
9
+ target: string;
10
+ error?: string;
11
+ }
12
+ export declare function deliverSkills(projectRoot: string, hosts: HostId[], mode: 'install' | 'pending' | 'skip'): DeliveryResult[];
13
+ export declare function verifyDelivery(projectRoot: string, host: HostId): {
14
+ ok: boolean;
15
+ status: DeliveryStatus | 'missing';
16
+ expected: string;
17
+ actual: string;
18
+ };
19
+ export declare function digestDelivery(root: string, host: HostId): string;
20
+ /** Entire executable artifact, excluding installation dependencies and local state. */
21
+ export declare function digestPackage(root: string): string;
22
+ export declare function generateInstallCli(version: string, packageName: string): string;
23
+ export declare function writeInstallCli(projectRoot: string, version: string, packageName: string): void;
24
+ export declare function verifyInstallCli(projectRoot: string, version: string, packageName: string): {
25
+ ok: boolean;
26
+ expected: {
27
+ packageName: string;
28
+ packageVersion: string;
29
+ };
30
+ actual: {
31
+ packageName?: string;
32
+ packageVersion?: string;
33
+ } | null;
34
+ };
35
+ export declare function isWorkflow(value: string): value is WorkflowName;
36
+ //# sourceMappingURL=skills.d.ts.map