@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,324 @@
1
+ import { readSource } from '../../core/source-operations.js';
2
+ import { taskContextRel } from '../../core/worker.js';
3
+ import { Option } from 'commander';
4
+ import { runWorkerCheck, recordWorkerReview, reviewRounds, repairRounds } from '../../core/worker-evidence.js';
5
+ import { readJson } from '../../shared/fs.js';
6
+ import { resolveInside } from '../../shared/paths.js';
7
+ import { controlSummary, inferWorkflow } from '../../core/instructions.js';
8
+ import { artifactRel } from '../../core/change.js';
9
+ import { abortUpdate, acceptWorker, answerWorker, addSources, approveGate, archiveChange, buildInstructions, cancelWorker, changedFilesFromState, collect, collectPendingWork, renderPendingWork, createWorker, isKnownGate, parseSourceArg, parseSourceFileArg, project, recordEvidence, rejectGate, renderReportMarkdown, requestFix, requireChange, resolveChangeName, runVerifyCommands, writeReport, } from '../../index.js';
10
+ import { isWorkflow } from '../../delivery/skills.js';
11
+ import { AppError } from '../../shared/errors.js';
12
+ import { emitJson, jsonMode } from '../output.js';
13
+ import { registerFigma } from './figma.js';
14
+ export function registerOpsx(program) {
15
+ program.command('todo').description('汇总当前 change 的非阻塞待办')
16
+ .requiredOption('--change <name>', 'change 名称')
17
+ .action((options, command) => {
18
+ const { projectRoot, config } = project();
19
+ const pending = collectPendingWork(projectRoot, options.change, config);
20
+ emitJson(jsonMode(command), pending, () => renderPendingWork(pending));
21
+ });
22
+ const sources = program.command('sources').description('登记与采集来源');
23
+ registerFigma(sources);
24
+ sources.command('read').description('无损读取原文,或调用绑定 adapter 的定向读取')
25
+ .requiredOption('--change <name>', 'change 名称')
26
+ .requiredOption('--source <id>', '来源 id')
27
+ .option('--input <path>', '项目内 JSON 输入文件')
28
+ .option('--use <selector>', '为旧账本或手工来源显式指定 adapter')
29
+ .action((options) => {
30
+ const { projectRoot } = project();
31
+ const result = readSource(projectRoot, options.change, options.source, options.input, options.use);
32
+ emitJson(true, result, () => JSON.stringify(result));
33
+ });
34
+ sources
35
+ .command('add')
36
+ .requiredOption('--change <name>', 'change 名称')
37
+ .option('--notes <text>', '目标说明')
38
+ .option('--source <spec>', 'capability=locator,可重复', collectOption, [])
39
+ .option('--source-file <spec>', 'capability=path,可重复(capability: requirements/interfaces/design;path: 项目内已有文件的相对路径)', collectOption, [])
40
+ .addOption(viewOption())
41
+ .action((options, command) => {
42
+ const { projectRoot } = project();
43
+ const sourcesInput = [
44
+ ...options.source.map(parseSourceArg),
45
+ ...options.sourceFile.map(parseSourceFileArg),
46
+ ];
47
+ const ledger = addSources(projectRoot, options.change, { notes: options.notes, sources: sourcesInput });
48
+ const payload = options.view === 'summary' ? controlSummary(requireChange(projectRoot, options.change), {
49
+ revision: ledger.revision,
50
+ sourcesPath: artifactRel(options.change, `sources/sources-r${ledger.revision}.json`),
51
+ sources: ledger.items.map(({ id, capability, kind, artifact }) => ({ id, capability, kind, artifact })),
52
+ }) : ledger;
53
+ emitJson(options.view === 'summary' || jsonMode(command), payload, () => `sources r${ledger.revision}`);
54
+ });
55
+ sources
56
+ .command('collect')
57
+ .requiredOption('--change <name>', 'change 名称')
58
+ .option('--refresh', '忽略本次未完成采集的成功记录,重新采集')
59
+ .addOption(viewOption())
60
+ .action((options, command) => {
61
+ const { projectRoot, config } = project();
62
+ const result = collect(projectRoot, options.change, config, { refresh: options.refresh });
63
+ const payload = options.view === 'summary' ? controlSummary(requireChange(projectRoot, options.change), {
64
+ revision: result.ledger.revision,
65
+ sourcesPath: artifactRel(options.change, `sources/sources-r${result.ledger.revision}.json`),
66
+ artifacts: result.artifacts, deferred: result.deferred, covered: result.covered, warnings: result.warnings,
67
+ }) : result;
68
+ emitJson(options.view === 'summary' || jsonMode(command), payload, () => `collected r${result.ledger.revision}`);
69
+ });
70
+ sources
71
+ .command('abort-update')
72
+ .requiredOption('--change <name>', 'change 名称')
73
+ .option('--reason <text>', '原因', 'withdrawn')
74
+ .action((options, command) => {
75
+ const { projectRoot } = project();
76
+ const ledger = abortUpdate(projectRoot, options.change, options.reason);
77
+ emitJson(jsonMode(command), ledger, () => `aborted to r${ledger.revision}`);
78
+ });
79
+ const worker = program.command('worker').description('创建、验收或取消 worker');
80
+ worker
81
+ .command('create')
82
+ .requiredOption('--change <name>', 'change 名称')
83
+ .requiredOption('--step <id>', 'parse | tech | plan | code-<id> | verify')
84
+ .option('--refresh', '显式重建活动 Worker 的 Hook 绑定,保留原始差异基线;检查需重跑')
85
+ .addOption(viewOption())
86
+ .action((options, command) => {
87
+ const { projectRoot, config } = project();
88
+ const result = createWorker(projectRoot, options.change, options.step, config, { refresh: options.refresh });
89
+ // 控制会话只靠这些字段路由 code task 的四个阶段,不必读取 packet 正文。
90
+ const codeTask = result.packet.task ? {
91
+ category: result.packet.task.category,
92
+ taskContext: result.packet.refs.taskContext ?? null,
93
+ taskContextPath: taskContextRel(result.packet.changeName, result.packet.task.id),
94
+ reviewRounds: reviewRounds(projectRoot, options.change, result.packet),
95
+ repairRounds: repairRounds(projectRoot, options.change, result.packet),
96
+ } : {};
97
+ const payload = options.view === 'summary' ? controlSummary(requireChange(projectRoot, options.change), {
98
+ workerId: result.packet.workerId, stepId: result.packet.stepId,
99
+ packetPath: result.path, resultPath: result.packet.expectedResult,
100
+ ...codeTask,
101
+ }) : result;
102
+ emitJson(options.view === 'summary' || jsonMode(command), payload, () => `created ${result.packet.workerId}`);
103
+ });
104
+ worker.command('check').description('执行阶段检查并保存真实回执;插件调用不缓存')
105
+ .requiredOption('--change <name>', 'change 名称')
106
+ .requiredOption('--id <id>', '稳定检查 id;重试沿用同一 id')
107
+ .option('--command <command>', '检查命令;command Hook 可省略')
108
+ .option('--force', '强制重跑,不复用成功回执')
109
+ .option('--input <path>', '插件 command Hook 的项目内 JSON 输入文件')
110
+ .action((options) => {
111
+ const { projectRoot } = project();
112
+ const receipt = runWorkerCheck(projectRoot, options.change, options.id, options.command, options.force, options.input);
113
+ emitJson(true, receipt, () => receipt.log);
114
+ if (!receipt.ok)
115
+ process.exitCode = 1;
116
+ });
117
+ worker.command('review').description('控制会话批量登记或解决返修问题;遗漏的旧问题保持未解决')
118
+ .requiredOption('--change <name>', 'change 名称')
119
+ .requiredOption('--file <path>', '问题更新 JSON 数组,项目内路径')
120
+ .option('--repair', '登记实际代码返修后的审查;首轮与只刷新证据时省略')
121
+ .addOption(viewOption())
122
+ .action((options) => {
123
+ const { projectRoot } = project();
124
+ const receipt = recordWorkerReview(projectRoot, options.change, readJson(resolveInside(projectRoot, options.file)), options.repair);
125
+ // --view summary keeps the same shape as create/accept so the controller does not need a second call.
126
+ const payload = options.view === 'summary' ? controlSummary(requireChange(projectRoot, options.change), receipt) : receipt;
127
+ emitJson(true, payload, () => receipt.path);
128
+ });
129
+ worker
130
+ .command('accept')
131
+ .requiredOption('--change <name>', 'change 名称')
132
+ .requiredOption('--result <path>', 'result JSON 路径')
133
+ .addOption(viewOption())
134
+ .action((options, command) => {
135
+ const { projectRoot, config } = project();
136
+ const active = requireChange(projectRoot, options.change).activeWorker;
137
+ const state = acceptWorker(projectRoot, options.change, options.result, config);
138
+ const payload = options.view === 'summary' ? controlSummary(state, {
139
+ workerId: active?.workerId, stepId: active?.stepId,
140
+ packetPath: active?.packet, resultPath: active?.expectedResult,
141
+ }) : state;
142
+ emitJson(options.view === 'summary' || jsonMode(command), payload, () => `accepted → ${state.node}`);
143
+ });
144
+ worker
145
+ .command('answer')
146
+ .description('记录用户对 blocked worker 的答复,写入 packet.decisions 后可重新 create 同一 step')
147
+ .requiredOption('--change <name>', 'change 名称')
148
+ .requiredOption('--answer <text>', '答复,可重复;建议与 questions 一一对应', (value, previous = []) => [...previous, value])
149
+ .addOption(viewOption())
150
+ .action((options, command) => {
151
+ const { projectRoot } = project();
152
+ const result = answerWorker(projectRoot, options.change, options.answer);
153
+ const payload = options.view === 'summary' ? controlSummary(result.state, {
154
+ workerId: result.packet.workerId, stepId: result.packet.stepId,
155
+ packetPath: result.path, resultPath: result.packet.expectedResult,
156
+ }) : result;
157
+ emitJson(options.view === 'summary' || jsonMode(command), payload, () => `answered ${result.packet.workerId}`);
158
+ });
159
+ worker
160
+ .command('cancel')
161
+ .requiredOption('--change <name>', 'change 名称')
162
+ .option('--reason <text>', '原因', 'cancelled')
163
+ .action((options, command) => {
164
+ const { projectRoot } = project();
165
+ const state = cancelWorker(projectRoot, options.change, options.reason);
166
+ emitJson(jsonMode(command), state, () => 'cancelled');
167
+ });
168
+ const gate = program.command('gate').description('批准或拒绝 Gate');
169
+ gate
170
+ .command('approve <gate>')
171
+ .requiredOption('--change <name>', 'change 名称')
172
+ .option('--comment <text>', '说明', '')
173
+ .action((gateId, options, command) => {
174
+ if (!isKnownGate(gateId))
175
+ throw new AppError('invalid_gate', `未知 gate: ${gateId}`);
176
+ const { projectRoot } = project();
177
+ const state = approveGate(projectRoot, options.change, gateId, options.comment);
178
+ emitJson(jsonMode(command), state, () => `approved ${gateId}`);
179
+ });
180
+ gate
181
+ .command('reject <gate>')
182
+ .requiredOption('--change <name>', 'change 名称')
183
+ .requiredOption('--reason <text>', '原因')
184
+ .action((gateId, options, command) => {
185
+ if (!isKnownGate(gateId))
186
+ throw new AppError('invalid_gate', `未知 gate: ${gateId}`);
187
+ const { projectRoot } = project();
188
+ const state = rejectGate(projectRoot, options.change, gateId, options.reason);
189
+ emitJson(jsonMode(command), state, () => `rejected ${gateId}`);
190
+ });
191
+ const verify = program.command('verify').description('验证钩子与报告');
192
+ verify
193
+ .command('commands')
194
+ .requiredOption('--change <name>', 'change 名称')
195
+ .action((options, command) => {
196
+ const { projectRoot, config } = project();
197
+ const state = requireChange(projectRoot, options.change);
198
+ const runs = runVerifyCommands(projectRoot, options.change, config, changedFilesFromState(state));
199
+ emitJson(jsonMode(command), { commands: runs }, () => `ran ${runs.length} commands`);
200
+ });
201
+ verify
202
+ .command('skill-evidence')
203
+ .requiredOption('--change <name>', 'change 名称')
204
+ .requiredOption('--hook <id>', 'hook id')
205
+ .requiredOption('--status <status>', 'passed | failed | skipped')
206
+ .option('--notes <text>', '说明')
207
+ .action((options, command) => {
208
+ const { projectRoot, config } = project();
209
+ const record = recordEvidence(projectRoot, options.change, {
210
+ hookId: options.hook,
211
+ type: 'skill',
212
+ status: evidenceStatus(options.status),
213
+ notes: options.notes,
214
+ }, config);
215
+ emitJson(jsonMode(command), record, () => `recorded ${record.hookId}`);
216
+ });
217
+ verify
218
+ .command('hook-result')
219
+ .requiredOption('--change <name>', 'change 名称')
220
+ .requiredOption('--hook <id>', 'hook id')
221
+ .requiredOption('--status <status>', 'passed | failed | skipped')
222
+ .option('--notes <text>', '说明')
223
+ .action((options, command) => {
224
+ const { projectRoot, config } = project();
225
+ const record = recordEvidence(projectRoot, options.change, {
226
+ hookId: options.hook,
227
+ type: 'file',
228
+ status: evidenceStatus(options.status),
229
+ notes: options.notes,
230
+ }, config);
231
+ emitJson(jsonMode(command), record, () => `recorded ${record.hookId}`);
232
+ });
233
+ verify
234
+ .command('check-result')
235
+ .requiredOption('--change <name>', 'change 名称')
236
+ .requiredOption('--hook <id>', 'hook id')
237
+ .requiredOption('--status <status>', 'passed | failed | skipped')
238
+ .option('--notes <text>', '说明')
239
+ .action((options, command) => {
240
+ const { projectRoot, config } = project();
241
+ const record = recordEvidence(projectRoot, options.change, {
242
+ hookId: options.hook,
243
+ type: 'check',
244
+ status: evidenceStatus(options.status),
245
+ notes: options.notes,
246
+ }, config);
247
+ emitJson(jsonMode(command), record, () => `recorded ${record.hookId}`);
248
+ });
249
+ verify
250
+ .command('report')
251
+ .requiredOption('--change <name>', 'change 名称')
252
+ .option('--format <fmt>', 'json | md', 'json')
253
+ .action((options, command) => {
254
+ const { projectRoot, config } = project();
255
+ if (!['json', 'md'].includes(options.format))
256
+ throw new AppError('invalid_format', `未知报告格式: ${options.format}`);
257
+ const state = requireChange(projectRoot, options.change);
258
+ const result = writeReport(projectRoot, options.change, config, {
259
+ changedFiles: changedFilesFromState(state),
260
+ });
261
+ if (result.status === 'failed')
262
+ process.exitCode = 1;
263
+ if (options.format === 'md') {
264
+ console.log(renderReportMarkdown(result.report));
265
+ return;
266
+ }
267
+ emitJson(jsonMode(command), result, () => `report ${result.status}`);
268
+ });
269
+ verify
270
+ .command('fix')
271
+ .requiredOption('--change <name>', 'change 名称')
272
+ .option('--reason <text>', '原因', 'qa failed')
273
+ .action((options, command) => {
274
+ const { projectRoot } = project();
275
+ const state = requestFix(projectRoot, options.change, options.reason);
276
+ emitJson(jsonMode(command), state, () => `fix → ${state.node}`);
277
+ });
278
+ program
279
+ .command('archive [change]')
280
+ .description('归档已完成的 change')
281
+ .action((changeName, options, command) => {
282
+ const { projectRoot } = project();
283
+ const name = resolveChangeName(projectRoot, changeName);
284
+ const result = archiveChange(projectRoot, name);
285
+ emitJson(jsonMode(command), result, () => `archived ${result.to}`);
286
+ void options;
287
+ });
288
+ program
289
+ .command('instructions [workflow]')
290
+ .description('返回当前节点的轻量指令')
291
+ .option('--change <name>', 'change 名称')
292
+ .option('--protocol <n>', '协议版本', '1')
293
+ .action((workflow, options, command) => {
294
+ const { projectRoot } = project();
295
+ const name = resolveChangeName(projectRoot, options.change);
296
+ const state = requireChange(projectRoot, name);
297
+ if (options.protocol !== '1')
298
+ throw new AppError('unsupported_protocol', `不支持 protocol: ${options.protocol}`);
299
+ let wf;
300
+ if (workflow) {
301
+ if (!isWorkflow(workflow))
302
+ throw new AppError('invalid_workflow', `未知 workflow: ${workflow}`);
303
+ wf = workflow;
304
+ }
305
+ else {
306
+ wf = inferWorkflow(state);
307
+ }
308
+ const payload = buildInstructions(wf, state);
309
+ emitJson(true, payload, () => JSON.stringify(payload));
310
+ void command;
311
+ });
312
+ }
313
+ function evidenceStatus(value) {
314
+ if (!['passed', 'failed', 'skipped'].includes(value))
315
+ throw new AppError('invalid_evidence_status', `未知 evidence status: ${value}`);
316
+ return value;
317
+ }
318
+ function collectOption(value, previous) {
319
+ return [...previous, value];
320
+ }
321
+ function viewOption() {
322
+ return new Option('--view <view>', '完整或精简响应').choices(['full', 'summary']).default('full');
323
+ }
324
+ //# sourceMappingURL=opsx.js.map
@@ -0,0 +1,3 @@
1
+ import { type Command } from 'commander';
2
+ export declare function registerUsage(program: Command): void;
3
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { Option } from 'commander';
2
+ import { readJson } from '../../shared/fs.js';
3
+ import { analyzeUsage, renderUsageMarkdown } from '../../usage/analyze.js';
4
+ export function registerUsage(program) {
5
+ program.command('usage').description('只读用量分析').command('analyze')
6
+ .requiredOption('--change <name>', '报告所属 change,不静默过滤未归属用量')
7
+ .requiredOption('--session <path...>', 'Codex/Claude JSONL,可重复', (value, previous) => [...previous, value], [])
8
+ .addOption(new Option('--format <format>', '报告格式').choices(['json', 'md']).default('json'))
9
+ .option('--events <path>', 'OpenSpec events.jsonl,补充阶段归属')
10
+ .option('--expect-model <model>', '用于比较的固定模型')
11
+ .option('--expect-effort <effort>', '用于比较的固定推理配置')
12
+ .option('--rates <path>', '有来源的每百万 token 费率 JSON')
13
+ .option('--billing <path>', '按 threadId 归属的实际账单 JSON')
14
+ .action((options) => {
15
+ const report = analyzeUsage({ changeName: options.change, sessions: options.session, events: options.events,
16
+ expectedModel: options.expectModel, expectedEffort: options.expectEffort,
17
+ rates: options.rates ? readJson(options.rates) : undefined, billing: options.billing ? readJson(options.billing) : undefined });
18
+ console.log(options.format === 'md' ? renderUsageMarkdown(report) : JSON.stringify(report, null, 2));
19
+ });
20
+ }
21
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1,8 @@
1
+ export declare function emitJson(flag: boolean | undefined, data: unknown, text: () => string): void;
2
+ export declare function jsonMode(command: {
3
+ opts: () => {
4
+ json?: boolean;
5
+ };
6
+ parent?: unknown;
7
+ }): boolean;
8
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1,17 @@
1
+ export function emitJson(flag, data, text) {
2
+ if (flag)
3
+ console.log(JSON.stringify(data, null, 2));
4
+ else
5
+ console.log(text());
6
+ }
7
+ export function jsonMode(command) {
8
+ const self = command.opts().json;
9
+ let current = command;
10
+ while (current) {
11
+ if (current.opts?.().json)
12
+ return true;
13
+ current = current.parent;
14
+ }
15
+ return Boolean(self);
16
+ }
17
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ export declare function createProgram(): Command;
3
+ export declare function run(argv?: string[]): Promise<void>;
4
+ //# sourceMappingURL=program.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { isAppError } from '../shared/errors.js';
4
+ import { VERSION } from '../shared/version.js';
5
+ import { registerCommands } from './register.js';
6
+ export function createProgram() {
7
+ const program = new Command();
8
+ program
9
+ .name('openspec')
10
+ .description('OpenSpec v2:确定性产品规格工作流')
11
+ .version(VERSION)
12
+ .showHelpAfterError()
13
+ .option('--json', '以 JSON 输出');
14
+ registerCommands(program);
15
+ return program;
16
+ }
17
+ export async function run(argv = process.argv) {
18
+ const program = createProgram();
19
+ program.exitOverride();
20
+ try {
21
+ await program.parseAsync(argv);
22
+ }
23
+ catch (error) {
24
+ const json = argv.includes('--json') || program.getOptionValue('json');
25
+ if (isCommanderSuccess(error))
26
+ return;
27
+ const payload = isAppError(error)
28
+ ? { ok: false, code: error.code, error: error.message, details: error.details }
29
+ : { ok: false, code: 'unexpected', error: error instanceof Error ? error.message : String(error) };
30
+ if (json)
31
+ console.error(JSON.stringify(payload, null, 2));
32
+ else
33
+ console.error(chalk.red(payload.error));
34
+ process.exitCode = 1;
35
+ }
36
+ }
37
+ function isCommanderSuccess(error) {
38
+ if (!error || typeof error !== 'object' || !('code' in error))
39
+ return false;
40
+ return ['commander.helpDisplayed', 'commander.version'].includes(error.code);
41
+ }
42
+ //# sourceMappingURL=program.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCommands(program: Command): void;
3
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { registerInit } from './commands/init.js';
2
+ import { registerChange } from './commands/change.js';
3
+ import { registerOpsx } from './commands/opsx.js';
4
+ import { registerExt } from './commands/ext.js';
5
+ import { registerUsage } from './commands/usage.js';
6
+ export function registerCommands(program) {
7
+ registerInit(program);
8
+ registerChange(program);
9
+ registerOpsx(program);
10
+ registerExt(program);
11
+ registerUsage(program);
12
+ }
13
+ //# sourceMappingURL=register.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ import { run } from './cli/program.js';
2
+ await run();
3
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,7 @@
1
+ import { type ProjectConfig } from './schema.js';
2
+ export declare function findProjectRoot(start?: string): string;
3
+ export declare function configPathOf(projectRoot: string): string | null;
4
+ export declare function loadConfig(projectRoot: string): ProjectConfig;
5
+ export declare function saveConfig(projectRoot: string, config: ProjectConfig): void;
6
+ export declare function defaultConfig(): ProjectConfig;
7
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
3
+ import { exists, readText, writeText } from '../shared/fs.js';
4
+ import { AppError } from '../shared/errors.js';
5
+ import { CONFIG_FILES, OPENSPEC_DIR, resolveInside } from '../shared/paths.js';
6
+ import { ProjectConfigSchema } from './schema.js';
7
+ export function findProjectRoot(start = process.cwd()) {
8
+ let dir = path.resolve(start);
9
+ while (true) {
10
+ if (configPathOf(dir))
11
+ return dir;
12
+ const parent = path.dirname(dir);
13
+ if (parent === dir) {
14
+ throw new AppError('project_uninitialized', '未找到 openspec/config.yaml,请先运行 openspec init');
15
+ }
16
+ dir = parent;
17
+ }
18
+ }
19
+ export function configPathOf(projectRoot) {
20
+ for (const name of CONFIG_FILES) {
21
+ const file = resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, name), 'config_path_outside_project');
22
+ if (exists(file))
23
+ return file;
24
+ }
25
+ return null;
26
+ }
27
+ export function loadConfig(projectRoot) {
28
+ const file = configPathOf(projectRoot);
29
+ if (!file)
30
+ throw new AppError('project_uninitialized', '缺少 openspec/config.yaml');
31
+ let parsed;
32
+ try {
33
+ parsed = parseYaml(readText(file));
34
+ }
35
+ catch (error) {
36
+ throw new AppError('invalid_config', '项目配置 YAML 无法解析', error instanceof Error ? error.message : String(error));
37
+ }
38
+ const result = ProjectConfigSchema.safeParse(parsed);
39
+ if (!result.success) {
40
+ throw new AppError('invalid_config', '项目配置不合法', result.error.issues);
41
+ }
42
+ return result.data;
43
+ }
44
+ export function saveConfig(projectRoot, config) {
45
+ const parsed = ProjectConfigSchema.safeParse(config);
46
+ if (!parsed.success)
47
+ throw new AppError('invalid_config', '拒绝写入不合法的项目配置', parsed.error.issues);
48
+ const file = configPathOf(projectRoot)
49
+ ?? resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'config.yaml'), 'config_path_outside_project');
50
+ writeText(file, stringifyYaml(parsed.data, { lineWidth: 0 }));
51
+ }
52
+ export function defaultConfig() {
53
+ return { schema: 'spec-product' };
54
+ }
55
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { type ProjectConfig } from './schema.js';
3
+ declare const ScaffoldEntrySchema: z.ZodObject<{
4
+ path: z.ZodString;
5
+ template: z.ZodString;
6
+ description: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strict>;
8
+ export type ScaffoldEntry = z.infer<typeof ScaffoldEntrySchema>;
9
+ export type ScaffoldResult = {
10
+ path: string;
11
+ status: 'created' | 'exists';
12
+ description?: string;
13
+ };
14
+ export declare const TeamPresetSchema: z.ZodObject<{
15
+ schemaVersion: z.ZodLiteral<"openspec.team-preset.v1">;
16
+ id: z.ZodString;
17
+ version: z.ZodString;
18
+ title: z.ZodString;
19
+ description: z.ZodOptional<z.ZodString>;
20
+ marketplaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
21
+ name: z.ZodString;
22
+ repo: z.ZodString;
23
+ ref: z.ZodString;
24
+ }, z.core.$strict>>>;
25
+ plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
26
+ name: z.ZodString;
27
+ marketplace: z.ZodString;
28
+ minVersion: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strict>>>;
30
+ configPatch: z.ZodOptional<z.ZodObject<{
31
+ schema: z.ZodOptional<z.ZodLiteral<"spec-product">>;
32
+ taskCategories: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
33
+ notes: z.ZodOptional<z.ZodString>;
34
+ }, z.core.$strict>>>>>;
35
+ hooks: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
36
+ id: z.ZodString;
37
+ stage: z.ZodEnum<{
38
+ collect: "collect";
39
+ parse: "parse";
40
+ tech: "tech";
41
+ plan: "plan";
42
+ execute: "execute";
43
+ verify: "verify";
44
+ }>;
45
+ type: z.ZodOptional<z.ZodEnum<{
46
+ command: "command";
47
+ skill: "skill";
48
+ notes: "notes";
49
+ file: "file";
50
+ check: "check";
51
+ }>>;
52
+ capability: z.ZodOptional<z.ZodEnum<{
53
+ requirements: "requirements";
54
+ interfaces: "interfaces";
55
+ design: "design";
56
+ }>>;
57
+ use: z.ZodOptional<z.ZodString>;
58
+ operation: z.ZodOptional<z.ZodLiteral<"preview">>;
59
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
60
+ inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
+ name: z.ZodOptional<z.ZodString>;
62
+ notes: z.ZodOptional<z.ZodString>;
63
+ taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
64
+ command: z.ZodOptional<z.ZodString>;
65
+ target: z.ZodOptional<z.ZodEnum<{
66
+ project: "project";
67
+ inScopeChangedFiles: "inScopeChangedFiles";
68
+ }>>;
69
+ include: z.ZodOptional<z.ZodArray<z.ZodString>>;
70
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ allowEmpty: z.ZodOptional<z.ZodBoolean>;
72
+ required: z.ZodOptional<z.ZodBoolean>;
73
+ purpose: z.ZodOptional<z.ZodString>;
74
+ path: z.ZodOptional<z.ZodString>;
75
+ category: z.ZodOptional<z.ZodString>;
76
+ description: z.ZodOptional<z.ZodString>;
77
+ }, z.core.$strict>>>>;
78
+ verify: z.ZodOptional<z.ZodOptional<z.ZodObject<{
79
+ reviewScope: z.ZodOptional<z.ZodObject<{
80
+ includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
81
+ excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
82
+ }, z.core.$strict>>;
83
+ failurePolicy: z.ZodOptional<z.ZodObject<{
84
+ failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
85
+ failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
86
+ }, z.core.$strict>>;
87
+ }, z.core.$strict>>>;
88
+ plugins: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
89
+ name: z.ZodString;
90
+ marketplace: z.ZodString;
91
+ minVersion: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strict>>>>;
93
+ marketplaces: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
94
+ name: z.ZodString;
95
+ repo: z.ZodString;
96
+ ref: z.ZodString;
97
+ }, z.core.$strict>>>>;
98
+ tools: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodEnum<{
99
+ claude: "claude";
100
+ codex: "codex";
101
+ cursor: "cursor";
102
+ }>>>>;
103
+ }, z.core.$strict>>;
104
+ scaffold: z.ZodOptional<z.ZodArray<z.ZodObject<{
105
+ path: z.ZodString;
106
+ template: z.ZodString;
107
+ description: z.ZodOptional<z.ZodString>;
108
+ }, z.core.$strict>>>;
109
+ notes: z.ZodOptional<z.ZodArray<z.ZodString>>;
110
+ warnings: z.ZodOptional<z.ZodArray<z.ZodString>>;
111
+ }, z.core.$strict>;
112
+ export type TeamPreset = z.infer<typeof TeamPresetSchema>;
113
+ export declare function presetsDir(): string;
114
+ export declare function listPresets(): TeamPreset[];
115
+ export declare function loadPreset(id: string): TeamPreset;
116
+ export declare function mergePreset(base: ProjectConfig, preset: TeamPreset): ProjectConfig;
117
+ /**
118
+ * 把 preset 声明的模板文件放到项目里。已存在的文件一律不动——
119
+ * 团队填写后的内容不能被重复 init 覆盖;模板本身带 openspec:fill-me 标记,未填写时 file hook 不会放行。
120
+ */
121
+ export declare function scaffoldPreset(projectRoot: string, preset: TeamPreset): ScaffoldResult[];
122
+ /** 合并时保留了宿主 ref、且与 preset 基线不同的 marketplace,供 init / preset apply 提示。 */
123
+ export declare function keptMarketplaceRefs(base: ProjectConfig, preset: TeamPreset): Array<{
124
+ name: string;
125
+ ref: string;
126
+ presetRef: string;
127
+ }>;
128
+ export {};
129
+ //# sourceMappingURL=presets.d.ts.map