@clawplays/ospec-cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/.ospec/templates/hooks/post-merge +8 -0
  2. package/.ospec/templates/hooks/pre-commit +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +549 -0
  5. package/README.zh-CN.md +549 -0
  6. package/assets/for-ai/en-US/ai-guide.md +98 -0
  7. package/assets/for-ai/en-US/execution-protocol.md +64 -0
  8. package/assets/for-ai/zh-CN/ai-guide.md +102 -0
  9. package/assets/for-ai/zh-CN/execution-protocol.md +68 -0
  10. package/assets/git-hooks/post-merge +12 -0
  11. package/assets/git-hooks/pre-commit +12 -0
  12. package/assets/global-skills/claude/ospec-change/SKILL.md +116 -0
  13. package/assets/global-skills/codex/ospec-change/SKILL.md +117 -0
  14. package/assets/global-skills/codex/ospec-change/agents/openai.yaml +7 -0
  15. package/assets/global-skills/codex/ospec-change/skill.yaml +19 -0
  16. package/assets/project-conventions/en-US/development-guide.md +32 -0
  17. package/assets/project-conventions/en-US/naming-conventions.md +51 -0
  18. package/assets/project-conventions/en-US/skill-conventions.md +40 -0
  19. package/assets/project-conventions/en-US/workflow-conventions.md +70 -0
  20. package/assets/project-conventions/zh-CN/development-guide.md +32 -0
  21. package/assets/project-conventions/zh-CN/naming-conventions.md +51 -0
  22. package/assets/project-conventions/zh-CN/skill-conventions.md +40 -0
  23. package/assets/project-conventions/zh-CN/workflow-conventions.md +74 -0
  24. package/dist/adapters/codex-stitch-adapter.js +420 -0
  25. package/dist/adapters/gemini-stitch-adapter.js +408 -0
  26. package/dist/adapters/playwright-checkpoint-adapter.js +2260 -0
  27. package/dist/advanced/BatchOperations.d.ts +36 -0
  28. package/dist/advanced/BatchOperations.js +159 -0
  29. package/dist/advanced/CachingLayer.d.ts +66 -0
  30. package/dist/advanced/CachingLayer.js +136 -0
  31. package/dist/advanced/FeatureUpdater.d.ts +46 -0
  32. package/dist/advanced/FeatureUpdater.js +151 -0
  33. package/dist/advanced/PerformanceMonitor.d.ts +52 -0
  34. package/dist/advanced/PerformanceMonitor.js +129 -0
  35. package/dist/advanced/StatePersistence.d.ts +61 -0
  36. package/dist/advanced/StatePersistence.js +168 -0
  37. package/dist/advanced/index.d.ts +14 -0
  38. package/dist/advanced/index.js +22 -0
  39. package/dist/cli/commands/config.d.ts +5 -0
  40. package/dist/cli/commands/config.js +6 -0
  41. package/dist/cli/commands/feature.d.ts +5 -0
  42. package/dist/cli/commands/feature.js +6 -0
  43. package/dist/cli/commands/index.d.ts +5 -0
  44. package/dist/cli/commands/index.js +6 -0
  45. package/dist/cli/commands/project.d.ts +5 -0
  46. package/dist/cli/commands/project.js +6 -0
  47. package/dist/cli/commands/validate.d.ts +5 -0
  48. package/dist/cli/commands/validate.js +6 -0
  49. package/dist/cli/index.d.ts +5 -0
  50. package/dist/cli/index.js +6 -0
  51. package/dist/cli.d.ts +3 -0
  52. package/dist/cli.js +1007 -0
  53. package/dist/commands/ArchiveCommand.d.ts +14 -0
  54. package/dist/commands/ArchiveCommand.js +241 -0
  55. package/dist/commands/BaseCommand.d.ts +33 -0
  56. package/dist/commands/BaseCommand.js +46 -0
  57. package/dist/commands/BatchCommand.d.ts +5 -0
  58. package/dist/commands/BatchCommand.js +42 -0
  59. package/dist/commands/ChangesCommand.d.ts +3 -0
  60. package/dist/commands/ChangesCommand.js +71 -0
  61. package/dist/commands/DocsCommand.d.ts +5 -0
  62. package/dist/commands/DocsCommand.js +118 -0
  63. package/dist/commands/FinalizeCommand.d.ts +3 -0
  64. package/dist/commands/FinalizeCommand.js +24 -0
  65. package/dist/commands/IndexCommand.d.ts +5 -0
  66. package/dist/commands/IndexCommand.js +57 -0
  67. package/dist/commands/InitCommand.d.ts +5 -0
  68. package/dist/commands/InitCommand.js +65 -0
  69. package/dist/commands/NewCommand.d.ts +11 -0
  70. package/dist/commands/NewCommand.js +262 -0
  71. package/dist/commands/PluginsCommand.d.ts +58 -0
  72. package/dist/commands/PluginsCommand.js +2491 -0
  73. package/dist/commands/ProgressCommand.d.ts +5 -0
  74. package/dist/commands/ProgressCommand.js +103 -0
  75. package/dist/commands/QueueCommand.d.ts +10 -0
  76. package/dist/commands/QueueCommand.js +147 -0
  77. package/dist/commands/RunCommand.d.ts +13 -0
  78. package/dist/commands/RunCommand.js +200 -0
  79. package/dist/commands/SkillCommand.d.ts +31 -0
  80. package/dist/commands/SkillCommand.js +1216 -0
  81. package/dist/commands/SkillsCommand.d.ts +5 -0
  82. package/dist/commands/SkillsCommand.js +68 -0
  83. package/dist/commands/StatusCommand.d.ts +6 -0
  84. package/dist/commands/StatusCommand.js +140 -0
  85. package/dist/commands/UpdateCommand.d.ts +8 -0
  86. package/dist/commands/UpdateCommand.js +251 -0
  87. package/dist/commands/VerifyCommand.d.ts +5 -0
  88. package/dist/commands/VerifyCommand.js +278 -0
  89. package/dist/commands/WorkflowCommand.d.ts +12 -0
  90. package/dist/commands/WorkflowCommand.js +150 -0
  91. package/dist/commands/index.d.ts +43 -0
  92. package/dist/commands/index.js +85 -0
  93. package/dist/core/constants.d.ts +41 -0
  94. package/dist/core/constants.js +73 -0
  95. package/dist/core/errors.d.ts +36 -0
  96. package/dist/core/errors.js +72 -0
  97. package/dist/core/index.d.ts +7 -0
  98. package/dist/core/index.js +23 -0
  99. package/dist/core/types.d.ts +369 -0
  100. package/dist/core/types.js +3 -0
  101. package/dist/index.d.ts +11 -0
  102. package/dist/index.js +27 -0
  103. package/dist/presets/ProjectPresets.d.ts +41 -0
  104. package/dist/presets/ProjectPresets.js +190 -0
  105. package/dist/scaffolds/ProjectScaffoldPresets.d.ts +20 -0
  106. package/dist/scaffolds/ProjectScaffoldPresets.js +151 -0
  107. package/dist/services/ConfigManager.d.ts +14 -0
  108. package/dist/services/ConfigManager.js +386 -0
  109. package/dist/services/FeatureManager.d.ts +5 -0
  110. package/dist/services/FeatureManager.js +6 -0
  111. package/dist/services/FileService.d.ts +21 -0
  112. package/dist/services/FileService.js +152 -0
  113. package/dist/services/IndexBuilder.d.ts +12 -0
  114. package/dist/services/IndexBuilder.js +130 -0
  115. package/dist/services/Logger.d.ts +20 -0
  116. package/dist/services/Logger.js +48 -0
  117. package/dist/services/ProjectAssetRegistry.d.ts +12 -0
  118. package/dist/services/ProjectAssetRegistry.js +96 -0
  119. package/dist/services/ProjectAssetService.d.ts +49 -0
  120. package/dist/services/ProjectAssetService.js +223 -0
  121. package/dist/services/ProjectScaffoldCommandService.d.ts +73 -0
  122. package/dist/services/ProjectScaffoldCommandService.js +159 -0
  123. package/dist/services/ProjectScaffoldService.d.ts +44 -0
  124. package/dist/services/ProjectScaffoldService.js +507 -0
  125. package/dist/services/ProjectService.d.ts +209 -0
  126. package/dist/services/ProjectService.js +13239 -0
  127. package/dist/services/QueueService.d.ts +17 -0
  128. package/dist/services/QueueService.js +142 -0
  129. package/dist/services/RunService.d.ts +40 -0
  130. package/dist/services/RunService.js +420 -0
  131. package/dist/services/SkillParser.d.ts +30 -0
  132. package/dist/services/SkillParser.js +88 -0
  133. package/dist/services/StateManager.d.ts +16 -0
  134. package/dist/services/StateManager.js +127 -0
  135. package/dist/services/TemplateEngine.d.ts +43 -0
  136. package/dist/services/TemplateEngine.js +119 -0
  137. package/dist/services/TemplateGenerator.d.ts +40 -0
  138. package/dist/services/TemplateGenerator.js +273 -0
  139. package/dist/services/ValidationService.d.ts +19 -0
  140. package/dist/services/ValidationService.js +44 -0
  141. package/dist/services/Validator.d.ts +5 -0
  142. package/dist/services/Validator.js +6 -0
  143. package/dist/services/index.d.ts +52 -0
  144. package/dist/services/index.js +91 -0
  145. package/dist/services/templates/ExecutionTemplateBuilder.d.ts +12 -0
  146. package/dist/services/templates/ExecutionTemplateBuilder.js +300 -0
  147. package/dist/services/templates/ProjectTemplateBuilder.d.ts +38 -0
  148. package/dist/services/templates/ProjectTemplateBuilder.js +1897 -0
  149. package/dist/services/templates/TemplateBuilderBase.d.ts +19 -0
  150. package/dist/services/templates/TemplateBuilderBase.js +60 -0
  151. package/dist/services/templates/TemplateInputFactory.d.ts +16 -0
  152. package/dist/services/templates/TemplateInputFactory.js +298 -0
  153. package/dist/services/templates/templateTypes.d.ts +90 -0
  154. package/dist/services/templates/templateTypes.js +3 -0
  155. package/dist/tools/build-index.js +632 -0
  156. package/dist/utils/DateUtils.d.ts +18 -0
  157. package/dist/utils/DateUtils.js +40 -0
  158. package/dist/utils/PathUtils.d.ts +9 -0
  159. package/dist/utils/PathUtils.js +66 -0
  160. package/dist/utils/StringUtils.d.ts +26 -0
  161. package/dist/utils/StringUtils.js +47 -0
  162. package/dist/utils/helpers.d.ts +5 -0
  163. package/dist/utils/helpers.js +6 -0
  164. package/dist/utils/index.d.ts +7 -0
  165. package/dist/utils/index.js +23 -0
  166. package/dist/utils/logger.d.ts +5 -0
  167. package/dist/utils/logger.js +6 -0
  168. package/dist/utils/path.d.ts +5 -0
  169. package/dist/utils/path.js +6 -0
  170. package/dist/utils/subcommandHelp.d.ts +11 -0
  171. package/dist/utils/subcommandHelp.js +119 -0
  172. package/dist/workflow/ArchiveGate.d.ts +30 -0
  173. package/dist/workflow/ArchiveGate.js +93 -0
  174. package/dist/workflow/ConfigurableWorkflow.d.ts +89 -0
  175. package/dist/workflow/ConfigurableWorkflow.js +186 -0
  176. package/dist/workflow/HookSystem.d.ts +38 -0
  177. package/dist/workflow/HookSystem.js +66 -0
  178. package/dist/workflow/IndexRegenerator.d.ts +49 -0
  179. package/dist/workflow/IndexRegenerator.js +147 -0
  180. package/dist/workflow/PluginWorkflowComposer.d.ts +138 -0
  181. package/dist/workflow/PluginWorkflowComposer.js +239 -0
  182. package/dist/workflow/SkillUpdateEngine.d.ts +26 -0
  183. package/dist/workflow/SkillUpdateEngine.js +113 -0
  184. package/dist/workflow/VerificationSystem.d.ts +24 -0
  185. package/dist/workflow/VerificationSystem.js +116 -0
  186. package/dist/workflow/WorkflowEngine.d.ts +15 -0
  187. package/dist/workflow/WorkflowEngine.js +57 -0
  188. package/dist/workflow/index.d.ts +19 -0
  189. package/dist/workflow/index.js +32 -0
  190. package/package.json +78 -0
  191. package/scripts/postinstall.js +43 -0
@@ -0,0 +1,632 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const fsp = require('fs/promises');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'changes', 'for-ai']);
9
+ const INDEX_FILE = 'SKILL.index.json';
10
+ const SKILL_FILE = 'SKILL.md';
11
+
12
+ async function main() {
13
+ try {
14
+ const action = process.argv[2] || 'build';
15
+ const rootDir = process.cwd();
16
+
17
+ switch (action) {
18
+ case 'build':
19
+ await writeIndex(rootDir, { silent: false });
20
+ break;
21
+ case 'hook-check':
22
+ process.exitCode = await runHookCheck(rootDir, process.argv[3] || 'pre-commit');
23
+ break;
24
+ default:
25
+ console.error(`[ospec] unknown action: ${action}`);
26
+ process.exitCode = 1;
27
+ }
28
+ } catch (error) {
29
+ console.error(`[ospec] ${error.message}`);
30
+ process.exitCode = 1;
31
+ }
32
+ }
33
+
34
+ async function runHookCheck(rootDir, event) {
35
+ const config = await loadHookConfig(rootDir);
36
+ if (event === 'pre-commit' && config.preCommit === false) {
37
+ return 0;
38
+ }
39
+ if (event === 'post-merge' && config.postMerge === false) {
40
+ return 0;
41
+ }
42
+
43
+ const activeChanges = await listActiveChanges(rootDir);
44
+ if (activeChanges.length === 0) {
45
+ console.log('[ospec] no active changes, hook check skipped');
46
+ return 0;
47
+ }
48
+
49
+ const stagedFiles = event === 'pre-commit' ? getStagedFiles(rootDir) : [];
50
+ if (event === 'pre-commit') {
51
+ const relevantPaths = stagedFiles.filter(isHookRelevantPath);
52
+ if (relevantPaths.length === 0) {
53
+ console.log('[ospec] no staged OSpec files, hook check skipped');
54
+ return 0;
55
+ }
56
+ }
57
+
58
+ let shouldBlock = false;
59
+ const shouldCheckIndex =
60
+ config.indexCheck !== 'off' &&
61
+ (event === 'post-merge' || stagedFiles.some(filePath => isIndexRelevantPath(filePath)));
62
+
63
+ if (shouldCheckIndex) {
64
+ const indexStatus = await computeIndexStatus(rootDir);
65
+ if (indexStatus.stale) {
66
+ console.log('[ospec] SKILL.index.json is stale');
67
+ console.log('[ospec] run "ospec index build" or "node build-index-auto.cjs" to refresh it');
68
+ if (event === 'pre-commit' && config.indexCheck === 'error') {
69
+ shouldBlock = true;
70
+ }
71
+ } else {
72
+ console.log('[ospec] SKILL.index.json is up to date');
73
+ }
74
+ }
75
+
76
+ if (event === 'pre-commit' && config.changeCheck !== 'off') {
77
+ const affectedChanges = collectAffectedChanges(stagedFiles, activeChanges);
78
+ if (affectedChanges.length === 0) {
79
+ console.log('[ospec] no active change files staged, change summary skipped');
80
+ } else {
81
+ console.log('[ospec] active change summary');
82
+ for (const changeName of affectedChanges) {
83
+ const summary = await buildChangeSummary(rootDir, changeName, config);
84
+ if (!summary) {
85
+ continue;
86
+ }
87
+
88
+ console.log(
89
+ `${summary.summaryStatus.toUpperCase()} ${summary.name} [${summary.status}] ${summary.progress}%`
90
+ );
91
+
92
+ const issues = summary.checks.filter(check => check.status !== 'pass');
93
+ if (issues.length === 0) {
94
+ console.log(' protocol files and checklists are aligned');
95
+ } else {
96
+ for (const issue of issues) {
97
+ console.log(` ${issue.status.toUpperCase()} ${issue.name}: ${issue.message}`);
98
+ }
99
+ }
100
+
101
+ if (summary.summaryStatus !== 'pass' && config.changeCheck === 'error') {
102
+ shouldBlock = true;
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ if (shouldBlock) {
109
+ console.log('[ospec] hook blocked by current hook policy');
110
+ return 1;
111
+ }
112
+
113
+ return 0;
114
+ }
115
+
116
+ async function writeIndex(rootDir, options) {
117
+ const indexPath = path.join(rootDir, INDEX_FILE);
118
+ const nextIndex = await buildIndex(rootDir);
119
+ const currentIndex = await readJsonIfExists(indexPath);
120
+
121
+ if (currentIndex && isSameIndex(currentIndex, nextIndex)) {
122
+ if (!options.silent) {
123
+ console.log('[ospec] SKILL.index.json already up to date');
124
+ printIndexStats(currentIndex);
125
+ }
126
+ return { changed: false, index: currentIndex };
127
+ }
128
+
129
+ const output = {
130
+ ...nextIndex,
131
+ generated: new Date().toISOString(),
132
+ };
133
+ await fsp.writeFile(indexPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
134
+
135
+ if (!options.silent) {
136
+ console.log('[ospec] SKILL.index.json rebuilt');
137
+ printIndexStats(output);
138
+ }
139
+
140
+ return { changed: true, index: output };
141
+ }
142
+
143
+ async function computeIndexStatus(rootDir) {
144
+ const currentIndex = await readJsonIfExists(path.join(rootDir, INDEX_FILE));
145
+ const nextIndex = await buildIndex(rootDir);
146
+ return {
147
+ stale: !currentIndex || !isSameIndex(currentIndex, nextIndex),
148
+ currentIndex,
149
+ nextIndex,
150
+ };
151
+ }
152
+
153
+ async function buildIndex(rootDir) {
154
+ const modules = {};
155
+ const tagIndex = {};
156
+ let totalFiles = 0;
157
+ let totalSections = 0;
158
+
159
+ await walk(rootDir, async fullPath => {
160
+ totalFiles += 1;
161
+ const relativePath = normalizePath(path.relative(rootDir, fullPath));
162
+ const content = await fsp.readFile(fullPath, 'utf8');
163
+ const parsed = parseSkillFile(content);
164
+ const moduleName = parsed.frontmatter.name || relativePath;
165
+ const title = parsed.frontmatter.title || parsed.frontmatter.name || relativePath;
166
+ const tags = Array.isArray(parsed.frontmatter.tags) ? parsed.frontmatter.tags : [];
167
+
168
+ totalSections += Object.keys(parsed.sections).length;
169
+ modules[moduleName] = {
170
+ file: relativePath,
171
+ title,
172
+ tags,
173
+ sections: parsed.sections,
174
+ };
175
+
176
+ for (const tag of tags) {
177
+ if (!tagIndex[tag]) {
178
+ tagIndex[tag] = [];
179
+ }
180
+ tagIndex[tag].push(moduleName);
181
+ }
182
+ });
183
+
184
+ for (const tag of Object.keys(tagIndex).sort((left, right) => left.localeCompare(right))) {
185
+ tagIndex[tag] = tagIndex[tag].sort((left, right) => left.localeCompare(right));
186
+ }
187
+
188
+ const activeChanges = await listActiveChanges(rootDir);
189
+ return {
190
+ version: '1.0',
191
+ generated: new Date().toISOString(),
192
+ git_commit: null,
193
+ active_changes: activeChanges,
194
+ stats: {
195
+ totalFiles,
196
+ totalModules: Object.keys(modules).length,
197
+ totalSections,
198
+ },
199
+ modules,
200
+ tagIndex,
201
+ };
202
+ }
203
+
204
+ async function walk(currentDir, onSkillFile) {
205
+ const entries = (await fsp.readdir(currentDir, { withFileTypes: true })).sort((left, right) =>
206
+ left.name.localeCompare(right.name)
207
+ );
208
+
209
+ for (const entry of entries) {
210
+ const fullPath = path.join(currentDir, entry.name);
211
+ if (entry.isDirectory()) {
212
+ if (!SKIP_DIRS.has(entry.name)) {
213
+ await walk(fullPath, onSkillFile);
214
+ }
215
+ continue;
216
+ }
217
+
218
+ if (entry.name === SKILL_FILE) {
219
+ await onSkillFile(fullPath);
220
+ }
221
+ }
222
+ }
223
+
224
+ async function buildChangeSummary(rootDir, changeName, config) {
225
+ const featureDir = path.join(rootDir, 'changes', 'active', changeName);
226
+ const state = await readJsonIfExists(path.join(featureDir, 'state.json'));
227
+ if (!state) {
228
+ return null;
229
+ }
230
+
231
+ const proposalPath = path.join(featureDir, 'proposal.md');
232
+ const tasksPath = path.join(featureDir, 'tasks.md');
233
+ const verificationPath = path.join(featureDir, 'verification.md');
234
+ const proposalExists = await exists(proposalPath);
235
+ const tasksExists = await exists(tasksPath);
236
+ const verificationExists = await exists(verificationPath);
237
+
238
+ const checks = [
239
+ {
240
+ name: 'proposal.md',
241
+ status: proposalExists ? 'pass' : 'fail',
242
+ message: proposalExists ? 'Proposal file exists' : 'proposal.md is missing',
243
+ },
244
+ {
245
+ name: 'tasks.md',
246
+ status: tasksExists ? 'pass' : 'fail',
247
+ message: tasksExists ? 'Tasks file exists' : 'tasks.md is missing',
248
+ },
249
+ {
250
+ name: 'verification.md',
251
+ status: verificationExists ? 'pass' : 'fail',
252
+ message: verificationExists ? 'Verification file exists' : 'verification.md is missing',
253
+ },
254
+ ];
255
+
256
+ let flags = [];
257
+ let activatedSteps = [];
258
+ if (proposalExists) {
259
+ const proposal = parseFrontmatter(await fsp.readFile(proposalPath, 'utf8'));
260
+ flags = ensureArray(proposal.data.flags);
261
+ activatedSteps = getActivatedSteps(config.workflow, flags);
262
+ const unsupportedFlags = flags.filter(
263
+ flag => !ensureArray(config.workflow?.feature_flags?.supported).includes(flag)
264
+ );
265
+
266
+ checks.push({
267
+ name: 'proposal.flags',
268
+ status: 'pass',
269
+ message:
270
+ activatedSteps.length > 0
271
+ ? `Activated optional steps: ${activatedSteps.join(', ')}`
272
+ : 'No optional steps activated',
273
+ });
274
+
275
+ if (unsupportedFlags.length > 0) {
276
+ checks.push({
277
+ name: 'proposal.unsupported_flags',
278
+ status: 'warn',
279
+ message: `Unsupported flags: ${unsupportedFlags.join(', ')}`,
280
+ });
281
+ }
282
+ }
283
+
284
+ if (tasksExists) {
285
+ const tasks = parseFrontmatter(await fsp.readFile(tasksPath, 'utf8'));
286
+ const optionalSteps = ensureArray(tasks.data.optional_steps);
287
+ const missing = activatedSteps.filter(step => !optionalSteps.includes(step));
288
+ const checklistComplete = !/- \[ \]/.test(tasks.body);
289
+ checks.push({
290
+ name: 'tasks.md.optional_steps',
291
+ status: missing.length === 0 ? 'pass' : 'fail',
292
+ message:
293
+ missing.length === 0
294
+ ? 'All activated optional steps are present in tasks.md'
295
+ : `Missing optional steps in tasks.md: ${missing.join(', ')}`,
296
+ });
297
+ checks.push({
298
+ name: 'tasks.md.checklist',
299
+ status: checklistComplete ? 'pass' : 'warn',
300
+ message: checklistComplete ? 'tasks.md checklist is complete' : 'tasks.md still has unchecked items',
301
+ });
302
+ }
303
+
304
+ if (verificationExists) {
305
+ const verification = parseFrontmatter(await fsp.readFile(verificationPath, 'utf8'));
306
+ const optionalSteps = ensureArray(verification.data.optional_steps);
307
+ const missing = activatedSteps.filter(step => !optionalSteps.includes(step));
308
+ const checklistComplete = !/- \[ \]/.test(verification.body);
309
+ checks.push({
310
+ name: 'verification.md.optional_steps',
311
+ status: missing.length === 0 ? 'pass' : 'fail',
312
+ message:
313
+ missing.length === 0
314
+ ? 'All activated optional steps are present in verification.md'
315
+ : `Missing optional steps in verification.md: ${missing.join(', ')}`,
316
+ });
317
+ checks.push({
318
+ name: 'verification.md.checklist',
319
+ status: checklistComplete ? 'pass' : 'warn',
320
+ message:
321
+ checklistComplete
322
+ ? 'verification.md checklist is complete'
323
+ : 'verification.md still has unchecked items',
324
+ });
325
+ }
326
+
327
+ const hasProtocolIssues = checks.some(check => check.status !== 'pass');
328
+ if (state.status === 'archived') {
329
+ checks.push({
330
+ name: 'archive.location',
331
+ status: 'fail',
332
+ message: 'state.json.status is archived but the change is still under changes/active',
333
+ });
334
+ } else if (state.status === 'ready_to_archive' && !hasProtocolIssues) {
335
+ checks.push({
336
+ name: 'archive.pending',
337
+ status: 'warn',
338
+ message: `Change is ready to archive. Run "ospec archive changes/active/${changeName}" before commit.`,
339
+ });
340
+ }
341
+
342
+ const failCount = checks.filter(check => check.status === 'fail').length;
343
+ const warnCount = checks.filter(check => check.status === 'warn').length;
344
+
345
+ return {
346
+ name: state.feature || changeName,
347
+ status: state.status || 'draft',
348
+ progress: calculateProgress(state),
349
+ summaryStatus: failCount > 0 ? 'fail' : warnCount > 0 ? 'warn' : 'pass',
350
+ checks,
351
+ };
352
+ }
353
+
354
+ function calculateProgress(state) {
355
+ const completed = Array.isArray(state.completed) ? state.completed.length : 0;
356
+ const pending = Array.isArray(state.pending) ? state.pending.length : 0;
357
+ const total = completed + pending;
358
+ if (total === 0) {
359
+ return 0;
360
+ }
361
+
362
+ return Math.round((completed / total) * 100);
363
+ }
364
+
365
+ function collectAffectedChanges(stagedFiles, activeChanges) {
366
+ const affected = new Set();
367
+
368
+ for (const filePath of stagedFiles) {
369
+ const match = filePath.match(/^changes\/active\/([^/]+)\//);
370
+ if (match) {
371
+ affected.add(match[1]);
372
+ }
373
+ }
374
+
375
+ if (affected.size === 0 && stagedFiles.includes('.skillrc')) {
376
+ for (const changeName of activeChanges) {
377
+ affected.add(changeName);
378
+ }
379
+ }
380
+
381
+ return Array.from(affected).sort((left, right) => left.localeCompare(right));
382
+ }
383
+
384
+ function isHookRelevantPath(filePath) {
385
+ return filePath === '.skillrc' || isIndexRelevantPath(filePath);
386
+ }
387
+
388
+ function isIndexRelevantPath(filePath) {
389
+ return filePath === SKILL_FILE || /(^|\/)SKILL\.md$/.test(filePath) || filePath.startsWith('changes/active/');
390
+ }
391
+
392
+ async function listActiveChanges(rootDir) {
393
+ const activeDir = path.join(rootDir, 'changes', 'active');
394
+ if (!(await exists(activeDir))) {
395
+ return [];
396
+ }
397
+
398
+ return (await fsp.readdir(activeDir, { withFileTypes: true }))
399
+ .filter(entry => entry.isDirectory())
400
+ .map(entry => entry.name)
401
+ .sort((left, right) => left.localeCompare(right));
402
+ }
403
+
404
+ async function loadHookConfig(rootDir) {
405
+ const config = (await readJsonIfExists(path.join(rootDir, '.skillrc'))) || {};
406
+ const hooks = config.hooks || {};
407
+ const fallback = hooks['spec-check'] || 'error';
408
+ const normalized = {
409
+ preCommit: hooks['pre-commit'] !== false,
410
+ postMerge: hooks['post-merge'] !== false,
411
+ changeCheck: hooks['change-check'] || fallback,
412
+ indexCheck: hooks['index-check'] || fallback,
413
+ };
414
+ const legacyWarnDefaults =
415
+ config.version === '3.0' &&
416
+ config.mode !== 'lite' &&
417
+ normalized.preCommit &&
418
+ normalized.postMerge &&
419
+ fallback === 'warn' &&
420
+ normalized.changeCheck === 'warn' &&
421
+ normalized.indexCheck === 'warn';
422
+
423
+ return {
424
+ preCommit: normalized.preCommit,
425
+ postMerge: normalized.postMerge,
426
+ changeCheck: legacyWarnDefaults ? 'error' : normalized.changeCheck,
427
+ indexCheck: legacyWarnDefaults ? 'error' : normalized.indexCheck,
428
+ workflow: config.workflow || {},
429
+ };
430
+ }
431
+
432
+ function getActivatedSteps(workflowConfig, flags) {
433
+ const optionalSteps = workflowConfig && workflowConfig.optional_steps ? workflowConfig.optional_steps : {};
434
+ const activated = [];
435
+
436
+ for (const [stepName, stepConfig] of Object.entries(optionalSteps)) {
437
+ if (!stepConfig || stepConfig.enabled === false) {
438
+ continue;
439
+ }
440
+
441
+ const when = ensureArray(stepConfig.when);
442
+ if (when.some(flag => flags.includes(flag))) {
443
+ activated.push(stepName);
444
+ }
445
+ }
446
+
447
+ return activated.sort((left, right) => left.localeCompare(right));
448
+ }
449
+
450
+ function getStagedFiles(rootDir) {
451
+ const result = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], {
452
+ cwd: rootDir,
453
+ encoding: 'utf8',
454
+ });
455
+
456
+ if (result.status !== 0) {
457
+ return [];
458
+ }
459
+
460
+ return result.stdout
461
+ .split(/\r?\n/)
462
+ .map(item => normalizePath(item.trim()))
463
+ .filter(Boolean);
464
+ }
465
+
466
+ function parseSkillFile(content) {
467
+ const normalizedContent = normalizeLineEndings(content);
468
+ const parsed = parseFrontmatter(normalizedContent);
469
+ return {
470
+ frontmatter: {
471
+ name: typeof parsed.data.name === 'string' ? parsed.data.name : undefined,
472
+ title: typeof parsed.data.title === 'string' ? parsed.data.title : undefined,
473
+ tags: ensureArray(parsed.data.tags),
474
+ },
475
+ sections: extractSections(parsed.body),
476
+ };
477
+ }
478
+
479
+ function normalizeLineEndings(content) {
480
+ return String(content || '').replace(/\r\n?/g, '\n');
481
+ }
482
+
483
+ function parseFrontmatter(content) {
484
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
485
+ if (!match) {
486
+ return { data: {}, body: content };
487
+ }
488
+
489
+ const data = {};
490
+ const lines = match[1].split(/\r?\n/);
491
+ let currentKey = null;
492
+
493
+ for (const line of lines) {
494
+ if (/^\s*-\s+/.test(line) && currentKey) {
495
+ if (!Array.isArray(data[currentKey])) {
496
+ data[currentKey] = [];
497
+ }
498
+ data[currentKey].push(parseValue(line.replace(/^\s*-\s+/, '').trim()));
499
+ continue;
500
+ }
501
+
502
+ const keyMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
503
+ if (!keyMatch) {
504
+ currentKey = null;
505
+ continue;
506
+ }
507
+
508
+ const key = keyMatch[1];
509
+ const rawValue = keyMatch[2].trim();
510
+ data[key] = parseValue(rawValue);
511
+ currentKey = Array.isArray(data[key]) && rawValue === '' ? key : null;
512
+ }
513
+
514
+ return {
515
+ data,
516
+ body: content.slice(match[0].length),
517
+ };
518
+ }
519
+
520
+ function parseValue(rawValue) {
521
+ if (rawValue === '') {
522
+ return [];
523
+ }
524
+ if (rawValue === '[]') {
525
+ return [];
526
+ }
527
+ if (rawValue === 'true') {
528
+ return true;
529
+ }
530
+ if (rawValue === 'false') {
531
+ return false;
532
+ }
533
+ if (/^\[(.*)\]$/.test(rawValue)) {
534
+ const inner = rawValue.slice(1, -1).trim();
535
+ if (!inner) {
536
+ return [];
537
+ }
538
+
539
+ return inner
540
+ .split(',')
541
+ .map(item => stripQuotes(item.trim()))
542
+ .filter(Boolean);
543
+ }
544
+
545
+ return stripQuotes(rawValue);
546
+ }
547
+
548
+ function stripQuotes(value) {
549
+ return value.replace(/^['"]|['"]$/g, '');
550
+ }
551
+
552
+ function extractSections(content) {
553
+ const sections = {};
554
+ const matches = [];
555
+ const headingRegex = /^(#{1,6})\s+(.+?)$/gm;
556
+ let match;
557
+
558
+ while ((match = headingRegex.exec(content)) !== null) {
559
+ matches.push({
560
+ level: match[1].length,
561
+ title: match[2].trim(),
562
+ start: match.index,
563
+ });
564
+ }
565
+
566
+ for (let index = 0; index < matches.length; index += 1) {
567
+ const current = matches[index];
568
+ const next = matches[index + 1];
569
+ sections[current.title] = {
570
+ level: current.level,
571
+ title: current.title,
572
+ start: current.start,
573
+ end: next ? next.start : content.length,
574
+ };
575
+ }
576
+
577
+ return sections;
578
+ }
579
+
580
+ function ensureArray(value) {
581
+ if (Array.isArray(value)) {
582
+ return value.map(item => String(item).trim()).filter(Boolean);
583
+ }
584
+ if (typeof value === 'string' && value.trim()) {
585
+ return value
586
+ .split(',')
587
+ .map(item => item.trim())
588
+ .filter(Boolean);
589
+ }
590
+
591
+ return [];
592
+ }
593
+
594
+ function isSameIndex(left, right) {
595
+ return JSON.stringify(stripVolatileFields(left)) === JSON.stringify(stripVolatileFields(right));
596
+ }
597
+
598
+ function stripVolatileFields(index) {
599
+ const clone = JSON.parse(JSON.stringify(index));
600
+ delete clone.generated;
601
+ return clone;
602
+ }
603
+
604
+ function printIndexStats(index) {
605
+ console.log(
606
+ `[ospec] files ${index.stats.totalFiles}, modules ${index.stats.totalModules}, sections ${index.stats.totalSections}`
607
+ );
608
+ console.log(`[ospec] active changes: ${index.active_changes.join(', ') || 'none'}`);
609
+ }
610
+
611
+ function normalizePath(filePath) {
612
+ return filePath.replace(/\\/g, '/');
613
+ }
614
+
615
+ async function exists(targetPath) {
616
+ try {
617
+ await fsp.access(targetPath);
618
+ return true;
619
+ } catch {
620
+ return false;
621
+ }
622
+ }
623
+
624
+ async function readJsonIfExists(targetPath) {
625
+ if (!(await exists(targetPath))) {
626
+ return null;
627
+ }
628
+
629
+ return JSON.parse(await fsp.readFile(targetPath, 'utf8'));
630
+ }
631
+
632
+ main();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 日期工具函数
3
+ */
4
+ export declare class DateUtils {
5
+ /**
6
+ * 获取 ISO 格式的当前时间
7
+ */
8
+ static now(): string;
9
+ /**
10
+ * 格式化日期
11
+ */
12
+ static format(date: Date, format?: string): string;
13
+ /**
14
+ * 解析 ISO 字符串
15
+ */
16
+ static parseISO(dateString: string): Date;
17
+ }
18
+ //# sourceMappingURL=DateUtils.d.ts.map
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * 日期工具函数
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DateUtils = void 0;
7
+ class DateUtils {
8
+ /**
9
+ * 获取 ISO 格式的当前时间
10
+ */
11
+ static now() {
12
+ return new Date().toISOString();
13
+ }
14
+ /**
15
+ * 格式化日期
16
+ */
17
+ static format(date, format = 'YYYY-MM-DD HH:mm:ss') {
18
+ const year = date.getFullYear();
19
+ const month = String(date.getMonth() + 1).padStart(2, '0');
20
+ const day = String(date.getDate()).padStart(2, '0');
21
+ const hours = String(date.getHours()).padStart(2, '0');
22
+ const minutes = String(date.getMinutes()).padStart(2, '0');
23
+ const seconds = String(date.getSeconds()).padStart(2, '0');
24
+ return format
25
+ .replace('YYYY', String(year))
26
+ .replace('MM', month)
27
+ .replace('DD', day)
28
+ .replace('HH', hours)
29
+ .replace('mm', minutes)
30
+ .replace('ss', seconds);
31
+ }
32
+ /**
33
+ * 解析 ISO 字符串
34
+ */
35
+ static parseISO(dateString) {
36
+ return new Date(dateString);
37
+ }
38
+ }
39
+ exports.DateUtils = DateUtils;
40
+ //# sourceMappingURL=DateUtils.js.map
@@ -0,0 +1,9 @@
1
+ export declare class PathUtils {
2
+ static getChangeDir(rootDir: string, bucket: string, featureName: string): string;
3
+ static getFeatureDir(rootDir: string, featureName: string): string;
4
+ static getFeatureFile(featureDir: string, type: 'proposal' | 'tasks' | 'state' | 'verification'): string;
5
+ static normalize(filePath: string): string;
6
+ static isAbsolute(filePath: string): boolean;
7
+ static getRelative(from: string, to: string): string;
8
+ }
9
+ //# sourceMappingURL=PathUtils.d.ts.map