@localsummer/incspec 0.0.7 → 0.0.8

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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * reset command - Reset current workflow
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import {
8
+ ensureInitialized,
9
+ INCSPEC_DIR,
10
+ DIRS,
11
+ } from '../lib/config.mjs';
12
+ import {
13
+ readWorkflow,
14
+ archiveWorkflow,
15
+ STATUS,
16
+ } from '../lib/workflow.mjs';
17
+ import { archiveSpec } from '../lib/spec.mjs';
18
+ import {
19
+ colors,
20
+ colorize,
21
+ print,
22
+ printSuccess,
23
+ printInfo,
24
+ printWarning,
25
+ } from '../lib/terminal.mjs';
26
+
27
+ /**
28
+ * Extract module name from workflow name
29
+ * e.g., "analyze-incspec-cli" -> "incspec-cli"
30
+ * @param {string} workflowName
31
+ * @returns {string}
32
+ */
33
+ function extractModuleName(workflowName) {
34
+ if (!workflowName) {
35
+ return 'unknown';
36
+ }
37
+
38
+ // Remove common prefixes like "analyze-"
39
+ const prefixes = ['analyze-', 'feature-', 'fix-', 'refactor-'];
40
+ let moduleName = workflowName;
41
+
42
+ for (const prefix of prefixes) {
43
+ if (moduleName.startsWith(prefix)) {
44
+ moduleName = moduleName.slice(prefix.length);
45
+ break;
46
+ }
47
+ }
48
+
49
+ return moduleName || workflowName;
50
+ }
51
+
52
+ /**
53
+ * Get output directory for a step index
54
+ * @param {number} stepIndex - 0-based step index
55
+ * @returns {string|null}
56
+ */
57
+ function getOutputDirForStepIndex(stepIndex) {
58
+ // Step 1 (index 0) and Step 6 (index 5) -> baselines
59
+ if (stepIndex === 0 || stepIndex === 5) {
60
+ return DIRS.baselines;
61
+ }
62
+
63
+ // Step 2-3 (index 1-2) -> requirements
64
+ if (stepIndex === 1 || stepIndex === 2) {
65
+ return DIRS.requirements;
66
+ }
67
+
68
+ // Step 4 (index 3) -> increments
69
+ if (stepIndex === 3) {
70
+ return DIRS.increments;
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Collect output files from workflow steps
78
+ * @param {string} projectRoot
79
+ * @param {Object} workflow
80
+ * @returns {Array<{path: string, name: string}>}
81
+ */
82
+ function collectWorkflowOutputs(projectRoot, workflow) {
83
+ const outputs = [];
84
+
85
+ if (!workflow?.steps) {
86
+ return outputs;
87
+ }
88
+
89
+ workflow.steps.forEach((step, index) => {
90
+ // Only collect completed steps with output
91
+ if (step.status !== STATUS.COMPLETED || !step.output || step.output === '-') {
92
+ return;
93
+ }
94
+
95
+ const dir = getOutputDirForStepIndex(index);
96
+ if (!dir) {
97
+ return;
98
+ }
99
+
100
+ const outputPath = path.join(projectRoot, INCSPEC_DIR, dir, step.output);
101
+ if (fs.existsSync(outputPath)) {
102
+ outputs.push({
103
+ path: outputPath,
104
+ name: step.output,
105
+ });
106
+ }
107
+ });
108
+
109
+ return outputs;
110
+ }
111
+
112
+ /**
113
+ * Execute reset command
114
+ * @param {Object} ctx - Command context
115
+ */
116
+ export async function resetCommand(ctx) {
117
+ const { cwd } = ctx;
118
+
119
+ // Ensure initialized
120
+ const projectRoot = ensureInitialized(cwd);
121
+
122
+ // Read current workflow
123
+ const workflow = readWorkflow(projectRoot);
124
+
125
+ if (!workflow?.currentWorkflow) {
126
+ print('');
127
+ printInfo('当前无活跃工作流,无需重置');
128
+ return;
129
+ }
130
+
131
+ const workflowName = workflow.currentWorkflow;
132
+ const moduleName = extractModuleName(workflowName);
133
+
134
+ print('');
135
+ print(colorize(' incspec 工作流重置', colors.bold, colors.cyan));
136
+ print(colorize(' ─────────────────', colors.dim));
137
+ print('');
138
+ print(colorize(`工作流: ${workflowName}`, colors.dim));
139
+ print('');
140
+
141
+ // Collect output files
142
+ const outputs = collectWorkflowOutputs(projectRoot, workflow);
143
+
144
+ // Archive output files
145
+ if (outputs.length > 0) {
146
+ print(colorize('归档产出文件:', colors.dim));
147
+
148
+ for (const output of outputs) {
149
+ try {
150
+ const archivePath = archiveSpec(projectRoot, output.path, true, moduleName);
151
+ print(colorize(` - ${output.name}`, colors.dim));
152
+ } catch (e) {
153
+ printWarning(`归档失败: ${output.name} - ${e.message}`);
154
+ }
155
+ }
156
+
157
+ print('');
158
+ printSuccess(`已归档 ${outputs.length} 个产出文件到 archives/${new Date().toISOString().slice(0, 7)}/${moduleName}/`);
159
+ } else {
160
+ printInfo('无产出文件需要归档');
161
+ }
162
+
163
+ // Reset workflow state
164
+ archiveWorkflow(projectRoot);
165
+ printSuccess('工作流已重置');
166
+ }
@@ -10,6 +10,9 @@ import {
10
10
  readWorkflow,
11
11
  STEPS,
12
12
  STATUS,
13
+ MODE,
14
+ QUICK_MODE_SKIPPED,
15
+ isQuickMode,
13
16
  } from '../lib/workflow.mjs';
14
17
  import {
15
18
  colors,
@@ -49,7 +52,11 @@ export async function statusCommand(ctx) {
49
52
 
50
53
  // Current workflow
51
54
  if (workflow?.currentWorkflow) {
55
+ const quickMode = isQuickMode(workflow);
56
+ const modeLabel = quickMode ? '快速模式 (5步)' : '完整模式 (7步)';
57
+
52
58
  print(colorize(`当前工作流: `, colors.bold) + colorize(workflow.currentWorkflow, colors.cyan));
59
+ print(colorize(`工作流模式: ${modeLabel}`, colors.dim));
53
60
  print(colorize(`开始时间: ${workflow.startTime || '-'}`, colors.dim));
54
61
  print(colorize(`最后更新: ${workflow.lastUpdate || '-'}`, colors.dim));
55
62
  print('');
@@ -60,8 +67,14 @@ export async function statusCommand(ctx) {
60
67
 
61
68
  STEPS.forEach((step, index) => {
62
69
  const stepData = workflow.steps[index] || {};
63
- const status = stepData.status || STATUS.PENDING;
70
+ let status = stepData.status || STATUS.PENDING;
64
71
  const isCurrent = workflow.currentStep === step.id;
72
+ const isSkipped = quickMode && QUICK_MODE_SKIPPED.includes(step.id);
73
+
74
+ // Quick mode: mark skipped steps
75
+ if (isSkipped && status !== STATUS.SKIPPED) {
76
+ status = STATUS.SKIPPED;
77
+ }
65
78
 
66
79
  // Determine display status
67
80
  let displayStatus = status;
@@ -69,7 +82,12 @@ export async function statusCommand(ctx) {
69
82
  displayStatus = STATUS.IN_PROGRESS;
70
83
  }
71
84
 
72
- printStep(step.id, step.label, displayStatus);
85
+ // Use dimmed style for skipped steps
86
+ if (status === STATUS.SKIPPED) {
87
+ print(colorize(` [-] ${step.id}. ${step.label}`, colors.dim) + colorize(' (已跳过)', colors.dim));
88
+ } else {
89
+ printStep(step.id, step.label, displayStatus);
90
+ }
73
91
 
74
92
  // Show output file if completed
75
93
  if (stepData.output && status === STATUS.COMPLETED) {
@@ -80,9 +98,14 @@ export async function statusCommand(ctx) {
80
98
  print('');
81
99
 
82
100
  // Next step hint
83
- const nextStepIndex = workflow.steps.findIndex(
84
- step => (step?.status || STATUS.PENDING) !== STATUS.COMPLETED
85
- );
101
+ const nextStepIndex = workflow.steps.findIndex((step, index) => {
102
+ const stepNumber = index + 1;
103
+ // Skip excluded steps in quick mode
104
+ if (quickMode && QUICK_MODE_SKIPPED.includes(stepNumber)) {
105
+ return false;
106
+ }
107
+ return (step?.status || STATUS.PENDING) !== STATUS.COMPLETED;
108
+ });
86
109
  if (nextStepIndex >= 0) {
87
110
  const nextStep = STEPS[nextStepIndex];
88
111
  print(colorize('下一步:', colors.bold));
@@ -107,8 +130,8 @@ export async function statusCommand(ctx) {
107
130
 
108
131
  const recentHistory = workflow.history.slice(0, 5);
109
132
  recentHistory.forEach(item => {
110
- const statusIcon = item.status === 'completed'
111
- ? colorize('✓', colors.green)
133
+ const statusIcon = item.status === 'completed'
134
+ ? colorize('✓', colors.green)
112
135
  : colorize('○', colors.dim);
113
136
  print(` ${statusIcon} ${item.name} (${item.startTime || '-'})`);
114
137
  });
package/index.mjs CHANGED
@@ -17,6 +17,7 @@ import { mergeCommand } from './commands/merge.mjs';
17
17
  import { listCommand } from './commands/list.mjs';
18
18
  import { validateCommand } from './commands/validate.mjs';
19
19
  import { archiveCommand } from './commands/archive.mjs';
20
+ import { resetCommand } from './commands/reset.mjs';
20
21
  import { syncCommand } from './commands/sync.mjs';
21
22
  import { helpCommand } from './commands/help.mjs';
22
23
  import { colors, colorize } from './lib/terminal.mjs';
@@ -156,7 +157,7 @@ async function main() {
156
157
  await statusCommand(commandContext);
157
158
  break;
158
159
 
159
- // Workflow commands (Step 1-6)
160
+ // Workflow commands (Step 1-6, Step 7 archive is separate)
160
161
  case 'analyze':
161
162
  case 'a':
162
163
  await analyzeCommand(commandContext);
@@ -203,6 +204,12 @@ async function main() {
203
204
  await archiveCommand(commandContext);
204
205
  break;
205
206
 
207
+ // Reset workflow
208
+ case 'reset':
209
+ case 'rs':
210
+ await resetCommand(commandContext);
211
+ break;
212
+
206
213
  // Sync integrations
207
214
  case 'sync':
208
215
  case 's':
package/lib/agents.mjs CHANGED
@@ -43,7 +43,7 @@ export function getIncspecBlockTemplate() {
43
43
  - 请求含义模糊,需要先了解规范工作流再编码
44
44
 
45
45
  通过 \`@/incspec/AGENTS.md\` 可以了解:
46
- - 如何使用 6 步增量编码工作流
46
+ - 如何使用 7 步增量编码工作流
47
47
  - 规范格式与约定
48
48
  - 项目结构与指南
49
49
 
package/lib/config.mjs CHANGED
@@ -200,23 +200,23 @@ export function writeProjectConfig(projectRoot, config) {
200
200
  */
201
201
  function generateProjectContent(config) {
202
202
  const templatePath = path.join(getTemplatesDir(), 'project.md');
203
-
203
+
204
204
  if (fs.existsSync(templatePath)) {
205
205
  let content = fs.readFileSync(templatePath, 'utf-8');
206
-
206
+
207
207
  // Replace variables
208
208
  content = content.replace(/\{\{name\}\}/g, config.name || '');
209
209
  content = content.replace(/\{\{version\}\}/g, config.version || '1.0.0');
210
210
  content = content.replace(/\{\{source_dir\}\}/g, config.source_dir || 'src');
211
211
  content = content.replace(/\{\{created_at\}\}/g, config.created_at || formatLocalDate(new Date()));
212
-
212
+
213
213
  // Handle tech_stack array
214
214
  const techStackLines = (config.tech_stack || []).map(item => ` - ${item}`).join('\n');
215
215
  content = content.replace(/\{\{tech_stack\}\}/g, techStackLines);
216
-
216
+
217
217
  return content;
218
218
  }
219
-
219
+
220
220
  // Fallback to hardcoded content if template not found
221
221
  return generateFallbackProjectContent(config);
222
222
  }
@@ -321,7 +321,7 @@ function writeAgentsFile(projectRoot) {
321
321
  ## 快速开始
322
322
 
323
323
  1. 运行 \`incspec status\` 查看当前工作流状态
324
- 2. 按顺序执行6步工作流: analyze → collect-req → collect-dep → design → apply → merge
324
+ 2. 按顺序执行7步工作流: analyze → collect-req → collect-dep → design → apply → merge → archive
325
325
  3. 使用 \`incspec help\` 获取更多帮助
326
326
  `;
327
327
  fs.writeFileSync(agentsPath, fallbackContent, 'utf-8');
@@ -335,7 +335,7 @@ function writeAgentsFile(projectRoot) {
335
335
  */
336
336
  export function ensureInitialized(cwd) {
337
337
  const projectRoot = findProjectRoot(cwd);
338
-
338
+
339
339
  if (!projectRoot || !isInitialized(projectRoot)) {
340
340
  throw new Error(
341
341
  `incspec 未初始化。请先运行 'incspec init' 初始化项目。`
package/lib/cursor.mjs CHANGED
@@ -68,6 +68,13 @@ const COMMAND_MAP = [
68
68
  label: '步骤6: 合并到基线',
69
69
  description: '[incspec] 将增量融合为新的代码流基线快照',
70
70
  },
71
+ {
72
+ source: 'inc-archive.md',
73
+ target: 'inc-archive.md',
74
+ step: 7,
75
+ label: '步骤7: 归档工作流产出',
76
+ description: '[incspec] 归档工作流产出文件到历史记录目录',
77
+ },
71
78
  ];
72
79
 
73
80
  /**
@@ -174,16 +181,24 @@ description: [incspec] 显示帮助信息
174
181
 
175
182
  ## 工作流步骤
176
183
 
184
+ **完整模式 (7步):**
177
185
  1. \`/incspec/inc-analyze\` - 分析代码流程,生成基线快照
178
186
  2. \`/incspec/inc-collect-req\` - 收集结构化需求
179
187
  3. \`/incspec/inc-collect-dep\` - 采集UI依赖
180
188
  4. \`/incspec/inc-design\` - 生成增量设计蓝图
181
189
  5. \`/incspec/inc-apply\` - 应用代码变更
182
190
  6. \`/incspec/inc-merge\` - 合并到新基线
191
+ 7. \`/incspec/inc-archive\` - 归档工作流产出
192
+
193
+ **快速模式 (5步):**
194
+ 1. \`/incspec/inc-analyze --quick\` - 分析代码流程 (快速模式)
195
+ 2. \`/incspec/inc-collect-req\` - 收集结构化需求
196
+ 5. \`/incspec/inc-apply\` - 应用代码变更
197
+ 6. \`/incspec/inc-merge\` - 合并到新基线
198
+ 7. \`/incspec/inc-archive\` - 归档工作流产出
183
199
 
184
200
  ## 辅助命令
185
201
 
186
- - \`/incspec/inc-archive\` - 归档规范文件到 archives 目录
187
202
  - \`/incspec/inc-status\` - 查看当前工作流状态
188
203
  - \`/incspec/inc-help\` - 显示帮助信息
189
204
 
@@ -194,7 +209,7 @@ incspec init # 初始化项目
194
209
  incspec status # 查看工作流状态
195
210
  incspec list # 列出规范文件
196
211
  incspec validate # 验证规范完整性
197
- incspec cursor-sync # 同步 Cursor 命令
212
+ incspec sync # 同步 IDE 命令
198
213
  incspec help # 显示帮助
199
214
  \`\`\`
200
215
 
@@ -255,14 +270,14 @@ incspec archive <file-path> --keep --yes
255
270
  */
256
271
  export function syncToProject(projectRoot) {
257
272
  const targetDir = path.join(projectRoot, CURSOR_COMMANDS_DIR);
258
-
273
+
259
274
  // Create directory
260
275
  if (!fs.existsSync(targetDir)) {
261
276
  fs.mkdirSync(targetDir, { recursive: true });
262
277
  }
263
278
 
264
279
  const commands = generateCursorCommands(projectRoot);
265
-
280
+
266
281
  for (const cmd of commands) {
267
282
  const filePath = path.join(targetDir, cmd.name);
268
283
  fs.writeFileSync(filePath, cmd.content, 'utf-8');
@@ -282,7 +297,7 @@ export function syncToGlobal() {
282
297
  }
283
298
 
284
299
  const commands = generateCursorCommands();
285
-
300
+
286
301
  for (const cmd of commands) {
287
302
  const filePath = path.join(GLOBAL_CURSOR_DIR, cmd.name);
288
303
  fs.writeFileSync(filePath, cmd.content, 'utf-8');
package/lib/workflow.mjs CHANGED
@@ -18,6 +18,7 @@ export const STEPS = [
18
18
  { id: 4, name: 'design-increment', label: '增量设计', command: 'design' },
19
19
  { id: 5, name: 'apply-code', label: '应用代码变更', command: 'apply' },
20
20
  { id: 6, name: 'merge-baseline', label: '合并到基线', command: 'merge' },
21
+ { id: 7, name: 'archive-workflow', label: '归档工作流产出', command: 'archive' },
21
22
  ];
22
23
 
23
24
  /** Step status */
@@ -25,8 +26,21 @@ export const STATUS = {
25
26
  PENDING: 'pending',
26
27
  IN_PROGRESS: 'in_progress',
27
28
  COMPLETED: 'completed',
29
+ SKIPPED: 'skipped',
28
30
  };
29
31
 
32
+ /** Workflow mode */
33
+ export const MODE = {
34
+ FULL: 'full',
35
+ QUICK: 'quick',
36
+ };
37
+
38
+ /** Quick mode valid steps (1-based) */
39
+ export const QUICK_MODE_STEPS = [1, 2, 5, 6, 7];
40
+
41
+ /** Quick mode skipped steps (1-based) */
42
+ export const QUICK_MODE_SKIPPED = [3, 4];
43
+
30
44
  function normalizeOutputName(outputFile) {
31
45
  if (!outputFile || typeof outputFile !== 'string') {
32
46
  return outputFile;
@@ -117,6 +131,7 @@ export function parseWorkflow(content) {
117
131
  const workflow = {
118
132
  currentWorkflow: null,
119
133
  currentStep: null,
134
+ mode: MODE.FULL,
120
135
  startTime: null,
121
136
  lastUpdate: null,
122
137
  steps: [],
@@ -137,6 +152,13 @@ export function parseWorkflow(content) {
137
152
  workflow.currentStep = parseInt(stepMatch[1], 10);
138
153
  }
139
154
 
155
+ // Parse mode field
156
+ const modeMatch = content.match(/\*\*工作流模式\*\*:\s*(.+)/);
157
+ if (modeMatch) {
158
+ const modeValue = modeMatch[1].trim().toLowerCase();
159
+ workflow.mode = modeValue === 'quick' ? MODE.QUICK : MODE.FULL;
160
+ }
161
+
140
162
  const startMatch = content.match(/\*\*开始时间\*\*:\s*(.+)/);
141
163
  if (startMatch) {
142
164
  workflow.startTime = startMatch[1].trim();
@@ -213,12 +235,14 @@ export function readWorkflow(projectRoot) {
213
235
  */
214
236
  export function generateWorkflowContent(workflow) {
215
237
  const now = formatLocalDateTime(new Date());
238
+ const mode = workflow.mode || MODE.FULL;
216
239
 
217
240
  const lines = [
218
241
  '# Workflow Status',
219
242
  '',
220
243
  `**当前工作流**: ${workflow.currentWorkflow || '-'}`,
221
244
  `**当前步骤**: ${workflow.currentStep || '-'}`,
245
+ `**工作流模式**: ${mode}`,
222
246
  `**开始时间**: ${workflow.startTime || '-'}`,
223
247
  `**最后更新**: ${now}`,
224
248
  '',
@@ -272,10 +296,11 @@ export function initWorkflow(projectRoot) {
272
296
  const workflowPath = getWorkflowPath(projectRoot);
273
297
  const content = generateInitialWorkflowContent();
274
298
  fs.writeFileSync(workflowPath, content, 'utf-8');
275
-
299
+
276
300
  return {
277
301
  currentWorkflow: null,
278
302
  currentStep: null,
303
+ mode: MODE.FULL,
279
304
  startTime: null,
280
305
  lastUpdate: null,
281
306
  steps: STEPS.map(() => ({
@@ -294,13 +319,13 @@ export function initWorkflow(projectRoot) {
294
319
  function generateInitialWorkflowContent() {
295
320
  const templatePath = path.join(getTemplatesDir(), 'WORKFLOW.md');
296
321
  const now = formatLocalDateTime(new Date());
297
-
322
+
298
323
  if (fs.existsSync(templatePath)) {
299
324
  let content = fs.readFileSync(templatePath, 'utf-8');
300
325
  content = content.replace(/\{\{last_update\}\}/g, now);
301
326
  return content;
302
327
  }
303
-
328
+
304
329
  // Fallback to generated content
305
330
  const workflow = {
306
331
  currentWorkflow: null,
@@ -314,7 +339,7 @@ function generateInitialWorkflowContent() {
314
339
  })),
315
340
  history: [],
316
341
  };
317
-
342
+
318
343
  return generateWorkflowContent(workflow);
319
344
  }
320
345
 
@@ -322,10 +347,12 @@ function generateInitialWorkflowContent() {
322
347
  * Start a new workflow
323
348
  * @param {string} projectRoot
324
349
  * @param {string} workflowName
350
+ * @param {Object} options - { mode: 'full' | 'quick' }
325
351
  * @returns {Object}
326
352
  */
327
- export function startWorkflow(projectRoot, workflowName) {
353
+ export function startWorkflow(projectRoot, workflowName, options = {}) {
328
354
  const now = formatLocalDateTime(new Date());
355
+ const mode = options.mode || MODE.FULL;
329
356
  let workflow = readWorkflow(projectRoot);
330
357
 
331
358
  if (!workflow) {
@@ -336,12 +363,12 @@ export function startWorkflow(projectRoot, workflowName) {
336
363
  if (workflow.currentWorkflow) {
337
364
  const progress = getWorkflowProgress(workflow);
338
365
  const isComplete = progress.completed === progress.total;
339
-
366
+
340
367
  // Format: "workflowName (completed/total)" for incomplete, just name for complete
341
- const historyName = isComplete
342
- ? workflow.currentWorkflow
368
+ const historyName = isComplete
369
+ ? workflow.currentWorkflow
343
370
  : `${workflow.currentWorkflow} (${progress.completed}/${progress.total})`;
344
-
371
+
345
372
  workflow.history.unshift({
346
373
  name: historyName,
347
374
  status: isComplete ? 'completed' : 'incomplete',
@@ -353,12 +380,24 @@ export function startWorkflow(projectRoot, workflowName) {
353
380
  // Start new workflow
354
381
  workflow.currentWorkflow = workflowName;
355
382
  workflow.currentStep = 1;
383
+ workflow.mode = mode;
356
384
  workflow.startTime = now;
357
- workflow.steps = STEPS.map(() => ({
358
- status: STATUS.PENDING,
359
- output: null,
360
- completedAt: null,
361
- }));
385
+
386
+ // Initialize steps - mark skipped steps in quick mode
387
+ workflow.steps = STEPS.map((step) => {
388
+ if (mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(step.id)) {
389
+ return {
390
+ status: STATUS.SKIPPED,
391
+ output: null,
392
+ completedAt: now,
393
+ };
394
+ }
395
+ return {
396
+ status: STATUS.PENDING,
397
+ output: null,
398
+ completedAt: null,
399
+ };
400
+ });
362
401
 
363
402
  writeWorkflow(projectRoot, workflow);
364
403
  return workflow;
@@ -384,6 +423,7 @@ export function updateStep(projectRoot, stepNumber, status, outputFile = null) {
384
423
 
385
424
  const now = formatLocalDateTime(new Date());
386
425
  const normalizedOutput = normalizeOutputName(outputFile);
426
+ const mode = workflow.mode || MODE.FULL;
387
427
 
388
428
  workflow.steps[index] = {
389
429
  status,
@@ -391,11 +431,15 @@ export function updateStep(projectRoot, stepNumber, status, outputFile = null) {
391
431
  completedAt: status === STATUS.COMPLETED ? now : null,
392
432
  };
393
433
 
394
- // Update current step
434
+ // Update current step based on mode
395
435
  if (status === STATUS.IN_PROGRESS) {
396
436
  workflow.currentStep = stepNumber;
397
- } else if (status === STATUS.COMPLETED && stepNumber < STEPS.length) {
398
- workflow.currentStep = stepNumber + 1;
437
+ } else if (status === STATUS.COMPLETED) {
438
+ const nextStep = getNextStep(stepNumber, mode);
439
+ if (nextStep) {
440
+ workflow.currentStep = nextStep;
441
+ }
442
+ // nextStep is null means workflow is complete
399
443
  }
400
444
 
401
445
  writeWorkflow(projectRoot, workflow);
@@ -484,27 +528,39 @@ export function getStepByCommand(command) {
484
528
  /**
485
529
  * Calculate workflow completion progress
486
530
  * @param {Object} workflow
487
- * @returns {{completed: number, total: number, lastCompletedStep: number|null}}
531
+ * @returns {{completed: number, total: number, lastCompletedStep: number|null, mode: string}}
488
532
  */
489
533
  export function getWorkflowProgress(workflow) {
490
534
  if (!workflow || !workflow.steps) {
491
- return { completed: 0, total: STEPS.length, lastCompletedStep: null };
535
+ return { completed: 0, total: STEPS.length, lastCompletedStep: null, mode: MODE.FULL };
492
536
  }
493
537
 
538
+ const mode = workflow.mode || MODE.FULL;
494
539
  let completed = 0;
495
540
  let lastCompletedStep = null;
496
541
 
497
542
  workflow.steps.forEach((step, index) => {
543
+ const stepNumber = index + 1;
544
+
545
+ // Skip counting skipped steps in quick mode
546
+ if (mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(stepNumber)) {
547
+ return;
548
+ }
549
+
498
550
  if (step && step.status === STATUS.COMPLETED) {
499
551
  completed++;
500
- lastCompletedStep = index + 1;
552
+ lastCompletedStep = stepNumber;
501
553
  }
502
554
  });
503
555
 
556
+ // Quick mode has 5 steps (1,2,5,6,7), full mode has 7 steps
557
+ const total = mode === MODE.QUICK ? QUICK_MODE_STEPS.length : STEPS.length;
558
+
504
559
  return {
505
560
  completed,
506
- total: STEPS.length,
561
+ total,
507
562
  lastCompletedStep,
563
+ mode,
508
564
  };
509
565
  }
510
566
 
@@ -534,7 +590,7 @@ export function isWorkflowIncomplete(workflow) {
534
590
  */
535
591
  export function addToHistory(projectRoot, entry) {
536
592
  let workflow = readWorkflow(projectRoot);
537
-
593
+
538
594
  if (!workflow) {
539
595
  workflow = {
540
596
  currentWorkflow: null,
@@ -562,3 +618,40 @@ export function addToHistory(projectRoot, entry) {
562
618
  writeWorkflow(projectRoot, workflow);
563
619
  return workflow;
564
620
  }
621
+
622
+ /**
623
+ * Check if workflow is in quick mode
624
+ * @param {Object} workflow
625
+ * @returns {boolean}
626
+ */
627
+ export function isQuickMode(workflow) {
628
+ return workflow?.mode === MODE.QUICK;
629
+ }
630
+
631
+ /**
632
+ * Get next valid step based on mode
633
+ * @param {number} currentStep - Current step number (1-based)
634
+ * @param {string} mode - Workflow mode
635
+ * @returns {number|null} - Next step number or null if complete
636
+ */
637
+ export function getNextStep(currentStep, mode) {
638
+ if (mode === MODE.QUICK) {
639
+ const currentIndex = QUICK_MODE_STEPS.indexOf(currentStep);
640
+ if (currentIndex >= 0 && currentIndex < QUICK_MODE_STEPS.length - 1) {
641
+ return QUICK_MODE_STEPS[currentIndex + 1];
642
+ }
643
+ return null; // Workflow complete
644
+ }
645
+ // Full mode
646
+ return currentStep < STEPS.length ? currentStep + 1 : null;
647
+ }
648
+
649
+ /**
650
+ * Check if a step should be skipped based on mode
651
+ * @param {number} stepNumber - Step number (1-based)
652
+ * @param {string} mode - Workflow mode
653
+ * @returns {boolean}
654
+ */
655
+ export function shouldSkipStep(stepNumber, mode) {
656
+ return mode === MODE.QUICK && QUICK_MODE_SKIPPED.includes(stepNumber);
657
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localsummer/incspec",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "面向 AI 编程助手的增量规范驱动开发工具",
5
5
  "bin": {
6
6
  "incspec": "index.mjs"