maskweaver 0.9.6 → 0.9.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.
@@ -0,0 +1,307 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ export function parseGherkinText(text) {
5
+ const lines = text.split(/\r?\n/).map(l => l.trim());
6
+ let feature = '';
7
+ let description = '';
8
+ const scenarios = [];
9
+ let current = null;
10
+ let inFeature = false;
11
+ for (const line of lines) {
12
+ if (!line || line.startsWith('#'))
13
+ continue;
14
+ const featureMatch = /^Feature:\s*(.+)$/i.exec(line);
15
+ if (featureMatch) {
16
+ feature = featureMatch[1].trim();
17
+ inFeature = true;
18
+ current = null;
19
+ continue;
20
+ }
21
+ if (!inFeature)
22
+ continue;
23
+ const scenarioMatch = /^Scenario:\s*(.+)$/i.exec(line);
24
+ if (scenarioMatch) {
25
+ if (current)
26
+ scenarios.push(current);
27
+ current = { name: scenarioMatch[1].trim(), given: [], when: [], then: [] };
28
+ continue;
29
+ }
30
+ if (!current) {
31
+ if (!scenarioMatch && feature && !line.startsWith('Given') && !line.startsWith('When') && !line.startsWith('Then') && !line.startsWith('And')) {
32
+ description += (description ? ' ' : '') + line;
33
+ }
34
+ continue;
35
+ }
36
+ const stepMatch = /^(Given|When|Then|And)\s+(.+)$/i.exec(line);
37
+ if (stepMatch) {
38
+ const keyword = stepMatch[1].toLowerCase();
39
+ const step = stepMatch[2].trim();
40
+ switch (keyword) {
41
+ case 'given':
42
+ current.given.push(step);
43
+ break;
44
+ case 'when':
45
+ current.when.push(step);
46
+ break;
47
+ case 'then':
48
+ current.then.push(step);
49
+ break;
50
+ case 'and':
51
+ if (current.then.length > 0)
52
+ current.then.push(step);
53
+ else if (current.when.length > 0)
54
+ current.when.push(step);
55
+ else
56
+ current.given.push(step);
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ if (current)
62
+ scenarios.push(current);
63
+ if (!feature && scenarios.length === 0)
64
+ return null;
65
+ return { feature: feature || 'Untitled', description, scenarios };
66
+ }
67
+ export function parseGherkinBlock(block) {
68
+ const parsed = parseGherkinText(block);
69
+ if (!parsed)
70
+ return [];
71
+ return parsed.scenarios.map(scenario => ({
72
+ feature: parsed.feature,
73
+ scenario: scenario.name,
74
+ given: scenario.given,
75
+ when: scenario.when,
76
+ then: scenario.then,
77
+ }));
78
+ }
79
+ export function formatGherkinScenario(scenario) {
80
+ const lines = [];
81
+ lines.push(` Scenario: ${scenario.scenario}`);
82
+ for (const g of scenario.given)
83
+ lines.push(` Given ${g}`);
84
+ for (const w of scenario.when)
85
+ lines.push(` When ${w}`);
86
+ for (const t of scenario.then)
87
+ lines.push(` Then ${t}`);
88
+ return lines.join('\n');
89
+ }
90
+ export function formatGherkinFeature(featureName, scenarios) {
91
+ const lines = [];
92
+ lines.push(`Feature: ${featureName}`);
93
+ lines.push('');
94
+ for (const scenario of scenarios) {
95
+ lines.push(formatGherkinScenario(scenario));
96
+ lines.push('');
97
+ }
98
+ return lines.join('\n');
99
+ }
100
+ export function formatGherkinForTask(task) {
101
+ if (!task.acceptanceCriteria || task.acceptanceCriteria.length === 0)
102
+ return '';
103
+ return formatGherkinFeature(task.name, task.acceptanceCriteria);
104
+ }
105
+ export function formatGherkinChecklist(scenarios) {
106
+ const lines = [];
107
+ for (const scenario of scenarios) {
108
+ lines.push(`- [ ] ${scenario.scenario}`);
109
+ for (const t of scenario.then) {
110
+ lines.push(` - Then ${t}`);
111
+ }
112
+ }
113
+ return lines.join('\n');
114
+ }
115
+ const FEATURES_DIR = '.opencode/weave/features';
116
+ function getFeaturesDir(basePath) {
117
+ return path.join(basePath, FEATURES_DIR);
118
+ }
119
+ export async function ensureFeaturesDir(basePath) {
120
+ const dir = getFeaturesDir(basePath);
121
+ await mkdir(dir, { recursive: true });
122
+ return dir;
123
+ }
124
+ export async function writeFeatureFile(basePath, phaseId, phaseName, scenarios) {
125
+ if (scenarios.length === 0)
126
+ return '';
127
+ const dir = await ensureFeaturesDir(basePath);
128
+ const fileName = `${phaseId.toLowerCase()}.feature`;
129
+ const filePath = path.join(dir, fileName);
130
+ const content = formatGherkinFeature(phaseName, scenarios);
131
+ await writeFile(filePath, content, 'utf-8');
132
+ return path.relative(basePath, filePath).replace(/\\/g, '/');
133
+ }
134
+ export async function writeAllFeatureFiles(basePath, phases) {
135
+ const results = new Map();
136
+ for (const phase of phases) {
137
+ const allScenarios = collectPhaseScenarios(phase);
138
+ if (allScenarios.length === 0)
139
+ continue;
140
+ const featurePath = await writeFeatureFile(basePath, phase.id, phase.name, allScenarios);
141
+ if (featurePath) {
142
+ results.set(phase.id, featurePath);
143
+ }
144
+ }
145
+ return results;
146
+ }
147
+ function collectPhaseScenarios(phase) {
148
+ const scenarios = [];
149
+ if (phase.acceptanceCriteria) {
150
+ scenarios.push(...phase.acceptanceCriteria);
151
+ }
152
+ for (const task of phase.tasks) {
153
+ if (task.acceptanceCriteria) {
154
+ scenarios.push(...task.acceptanceCriteria);
155
+ }
156
+ }
157
+ return scenarios;
158
+ }
159
+ export function generateGherkinForPhase(phase) {
160
+ return [
161
+ {
162
+ feature: phase.name,
163
+ scenario: `${phase.name} - 정상 동작`,
164
+ given: [`${phase.name} 관련 기능이 구현되어 있다`],
165
+ when: [`유저가 ${phase.name} 기능을 사용한다`],
166
+ then: [`${phase.doneWhen}`],
167
+ },
168
+ {
169
+ feature: phase.name,
170
+ scenario: `${phase.name} - 에러 처리`,
171
+ given: [`${phase.name} 관련 기능이 구현되어 있다`],
172
+ when: [`유저가 ${phase.name} 기능을 비정상적으로 사용한다`],
173
+ then: ['적절한 에러 메시지가 표시된다'],
174
+ },
175
+ ];
176
+ }
177
+ export function generateGherkinForTask(task, phase) {
178
+ return {
179
+ feature: phase.name,
180
+ scenario: task.name,
181
+ given: [`${phase.name} 기능의 기본 환경이 준비되어 있다`],
182
+ when: [`${task.name}을/를 실행한다`],
183
+ then: [task.testCase || phase.doneWhen],
184
+ };
185
+ }
186
+ export function detectBDDFramework(projectPath) {
187
+ const checks = [
188
+ () => detectCucumber(projectPath),
189
+ () => detectJestCucumber(projectPath),
190
+ () => detectPytestBdd(projectPath),
191
+ ];
192
+ for (const check of checks) {
193
+ const result = check();
194
+ if (result.detected)
195
+ return result;
196
+ }
197
+ const featureDir = findFeatureDir(projectPath);
198
+ if (featureDir) {
199
+ return {
200
+ detected: true,
201
+ framework: 'unknown',
202
+ testCommand: null,
203
+ featureDir,
204
+ };
205
+ }
206
+ return { detected: false, framework: null, testCommand: null, featureDir: null };
207
+ }
208
+ function detectCucumber(projectPath) {
209
+ try {
210
+ const pkgPath = path.join(projectPath, 'package.json');
211
+ if (!fs.existsSync(pkgPath))
212
+ return { detected: false, framework: null, testCommand: null, featureDir: null };
213
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
214
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
215
+ if (deps['@cucumber/cucumber']) {
216
+ const featureDir = findFeatureDir(projectPath) || 'features';
217
+ return {
218
+ detected: true,
219
+ framework: 'cucumber',
220
+ testCommand: `npx cucumber-js`,
221
+ featureDir,
222
+ };
223
+ }
224
+ }
225
+ catch { }
226
+ return { detected: false, framework: null, testCommand: null, featureDir: null };
227
+ }
228
+ function detectJestCucumber(projectPath) {
229
+ try {
230
+ const pkgPath = path.join(projectPath, 'package.json');
231
+ if (!fs.existsSync(pkgPath))
232
+ return { detected: false, framework: null, testCommand: null, featureDir: null };
233
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
234
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
235
+ if (deps['jest-cucumber'] || deps['@jest-cucumber/core']) {
236
+ const featureDir = findFeatureDir(projectPath) || 'features';
237
+ return {
238
+ detected: true,
239
+ framework: 'jest-cucumber',
240
+ testCommand: `npx jest`,
241
+ featureDir,
242
+ };
243
+ }
244
+ }
245
+ catch { }
246
+ return { detected: false, framework: null, testCommand: null, featureDir: null };
247
+ }
248
+ function detectPytestBdd(projectPath) {
249
+ const indicators = [
250
+ path.join(projectPath, 'pyproject.toml'),
251
+ path.join(projectPath, 'requirements.txt'),
252
+ ];
253
+ for (const indicator of indicators) {
254
+ if (!fs.existsSync(indicator))
255
+ continue;
256
+ try {
257
+ const content = fs.readFileSync(indicator, 'utf-8');
258
+ if (content.includes('pytest-bdd')) {
259
+ const featureDir = findFeatureDir(projectPath) || 'features';
260
+ return {
261
+ detected: true,
262
+ framework: 'pytest-bdd',
263
+ testCommand: 'pytest',
264
+ featureDir,
265
+ };
266
+ }
267
+ }
268
+ catch {
269
+ break;
270
+ }
271
+ }
272
+ return { detected: false, framework: null, testCommand: null, featureDir: null };
273
+ }
274
+ function findFeatureDir(projectPath) {
275
+ const candidates = ['features', 'test/features', 'tests/features', 'specs/features'];
276
+ for (const candidate of candidates) {
277
+ if (fs.existsSync(path.join(projectPath, candidate))) {
278
+ return candidate;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+ export function generateGherkinVerificationPrompt(phase, task) {
284
+ const scenarios = task?.acceptanceCriteria || phase.acceptanceCriteria || [];
285
+ if (scenarios.length === 0)
286
+ return '';
287
+ const lines = [];
288
+ lines.push('### Acceptance Criteria (Gherkin)');
289
+ lines.push('');
290
+ lines.push('Verify each scenario is satisfied by the implementation:');
291
+ lines.push('');
292
+ for (const scenario of scenarios) {
293
+ lines.push(`**${scenario.scenario}**`);
294
+ for (const g of scenario.given)
295
+ lines.push(`- Given: ${g}`);
296
+ for (const w of scenario.when)
297
+ lines.push(`- When: ${w}`);
298
+ for (const t of scenario.then)
299
+ lines.push(`- Then: ${t}`);
300
+ lines.push('');
301
+ }
302
+ lines.push('For each scenario, check:');
303
+ lines.push('1. Are all "Given" preconditions met?');
304
+ lines.push('2. Can the "When" action be performed?');
305
+ lines.push('3. Do all "Then" expected outcomes hold?');
306
+ return lines.join('\n');
307
+ }
@@ -149,6 +149,25 @@ function serializePlan(plan) {
149
149
  if (phase.completedAt) {
150
150
  lines.push(` completed_at: ${yamlEscapeString(phase.completedAt)}`);
151
151
  }
152
+ if (phase.featurePath) {
153
+ lines.push(` feature_path: ${yamlEscapeString(phase.featurePath)}`);
154
+ }
155
+ if (phase.acceptanceCriteria && phase.acceptanceCriteria.length > 0) {
156
+ lines.push(' acceptance_criteria:');
157
+ for (const ac of phase.acceptanceCriteria) {
158
+ lines.push(` - feature: ${yamlEscapeString(ac.feature)}`);
159
+ lines.push(` scenario: ${yamlEscapeString(ac.scenario)}`);
160
+ lines.push(' given:');
161
+ for (const g of ac.given)
162
+ lines.push(` - ${yamlEscapeString(g)}`);
163
+ lines.push(' when:');
164
+ for (const w of ac.when)
165
+ lines.push(` - ${yamlEscapeString(w)}`);
166
+ lines.push(' then:');
167
+ for (const t of ac.then)
168
+ lines.push(` - ${yamlEscapeString(t)}`);
169
+ }
170
+ }
152
171
  lines.push(' checklist:');
153
172
  for (const item of phase.checklist) {
154
173
  lines.push(` - ${yamlEscapeString(item)}`);
@@ -185,6 +204,22 @@ function serializePlan(plan) {
185
204
  if (task.acceptanceRefs && task.acceptanceRefs.length > 0) {
186
205
  lines.push(` acceptance_refs: [${task.acceptanceRefs.map(ref => yamlEscapeString(ref)).join(', ')}]`);
187
206
  }
207
+ if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
208
+ lines.push(' acceptance_criteria:');
209
+ for (const ac of task.acceptanceCriteria) {
210
+ lines.push(` - feature: ${yamlEscapeString(ac.feature)}`);
211
+ lines.push(` scenario: ${yamlEscapeString(ac.scenario)}`);
212
+ lines.push(' given:');
213
+ for (const g of ac.given)
214
+ lines.push(` - ${yamlEscapeString(g)}`);
215
+ lines.push(' when:');
216
+ for (const w of ac.when)
217
+ lines.push(` - ${yamlEscapeString(w)}`);
218
+ lines.push(' then:');
219
+ for (const t of ac.then)
220
+ lines.push(` - ${yamlEscapeString(t)}`);
221
+ }
222
+ }
188
223
  if (task.maskUsed) {
189
224
  lines.push(` mask_used: ${yamlEscapeString(task.maskUsed)}`);
190
225
  }
@@ -202,6 +237,27 @@ function serializePlan(plan) {
202
237
  }
203
238
  return lines.join('\n');
204
239
  }
240
+ function deserializeGherkinList(raw) {
241
+ if (!raw || !Array.isArray(raw))
242
+ return undefined;
243
+ const scenarios = [];
244
+ for (const item of raw) {
245
+ if (!item || typeof item !== 'object')
246
+ continue;
247
+ const feature = item.feature || '';
248
+ const scenario = item.scenario || '';
249
+ if (!feature && !scenario)
250
+ continue;
251
+ scenarios.push({
252
+ feature,
253
+ scenario,
254
+ given: Array.isArray(item.given) ? item.given.filter((g) => typeof g === 'string') : [],
255
+ when: Array.isArray(item.when) ? item.when.filter((w) => typeof w === 'string') : [],
256
+ then: Array.isArray(item.then) ? item.then.filter((t) => typeof t === 'string') : [],
257
+ });
258
+ }
259
+ return scenarios.length > 0 ? scenarios : undefined;
260
+ }
205
261
  export class PhaseManager {
206
262
  basePath;
207
263
  plan = null;
@@ -550,6 +606,7 @@ export class PhaseManager {
550
606
  dependsOn: t.depends_on || t.dependsOn,
551
607
  verify: t.verify,
552
608
  acceptanceRefs: t.acceptance_refs || t.acceptanceRefs,
609
+ acceptanceCriteria: deserializeGherkinList(t.acceptance_criteria || t.acceptanceCriteria),
553
610
  retryCount: t.retry_count || t.retryCount || 0,
554
611
  maxRetries: t.max_retries || t.maxRetries || 5,
555
612
  lastError: t.last_error || t.lastError,
@@ -561,6 +618,8 @@ export class PhaseManager {
561
618
  startedAt: p.started_at || p.startedAt,
562
619
  completedAt: p.completed_at || p.completedAt,
563
620
  masksUsed: p.masks_used || p.masksUsed,
621
+ acceptanceCriteria: deserializeGherkinList(p.acceptance_criteria || p.acceptanceCriteria),
622
+ featurePath: p.feature_path || p.featurePath,
564
623
  })),
565
624
  };
566
625
  }
@@ -204,6 +204,22 @@ function generateBrief(task, phase, plan, agentTier, mask) {
204
204
  lines.push(`- [${v.kind}] ${v.value}`);
205
205
  lines.push(``);
206
206
  }
207
+ if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
208
+ lines.push(`### Acceptance Criteria (Gherkin)`);
209
+ lines.push(``);
210
+ lines.push(`Every scenario MUST pass before this task can be marked as completed.`);
211
+ lines.push(``);
212
+ for (const scenario of task.acceptanceCriteria) {
213
+ lines.push(`**Scenario: ${scenario.scenario}**`);
214
+ for (const g of scenario.given)
215
+ lines.push(` Given ${g}`);
216
+ for (const w of scenario.when)
217
+ lines.push(` When ${w}`);
218
+ for (const t of scenario.then)
219
+ lines.push(` Then ${t}`);
220
+ lines.push(``);
221
+ }
222
+ }
207
223
  if (plan.structuralChanges && plan.structuralChanges.length > 0) {
208
224
  const agreed = plan.structuralChanges.filter(sc => sc.agreed);
209
225
  if (agreed.length > 0) {
@@ -36,7 +36,7 @@ export declare function execute(options: ExecuteOptions): Promise<ExecuteResult>
36
36
  export interface VerificationLayer {
37
37
  name: string;
38
38
  order: number;
39
- type: 'build' | 'test' | 'visual' | 'api' | 'accessibility';
39
+ type: 'build' | 'test' | 'visual' | 'api' | 'accessibility' | 'gherkin';
40
40
  enabled: boolean;
41
41
  }
42
42
  export interface VerificationResult {
@@ -55,6 +55,13 @@ export interface AIVerificationOptions {
55
55
  enablePlaywright?: boolean;
56
56
  enableDevTools?: boolean;
57
57
  mode?: 'quick' | 'full';
58
+ gherkinScenarios?: Array<{
59
+ feature: string;
60
+ scenario: string;
61
+ given: string[];
62
+ when: string[];
63
+ then: string[];
64
+ }>;
58
65
  }
59
66
  export declare function runAIVerification(options: AIVerificationOptions): Promise<{
60
67
  passed: boolean;
@@ -2,6 +2,7 @@ import { getOrchestrator } from '../orchestrator.js';
2
2
  import { getPhaseManager } from '../phase-manager.js';
3
3
  import { searchTroubleshooting } from '../knowledge/global.js';
4
4
  import { analyzeParallelOpportunities, formatParallelAnalysis } from '../bridge.js';
5
+ import { detectBDDFramework } from '../gherkin.js';
5
6
  import { spawn } from 'node:child_process';
6
7
  import * as path from 'node:path';
7
8
  import { copyFile, mkdir } from 'node:fs/promises';
@@ -177,6 +178,50 @@ export function formatExecutionPlan(plan) {
177
178
  lines.push(formatParallelAnalysis(parallelAnalysis));
178
179
  lines.push('');
179
180
  }
181
+ const allScenarios = plan.taskPlans
182
+ .filter(tp => tp.task.acceptanceCriteria && tp.task.acceptanceCriteria.length > 0)
183
+ .flatMap(tp => tp.task.acceptanceCriteria);
184
+ if (allScenarios.length > 0) {
185
+ lines.push('### Acceptance Criteria (Gherkin)');
186
+ lines.push('');
187
+ lines.push('Each task MUST satisfy its acceptance criteria before marking as passed.');
188
+ lines.push('');
189
+ for (const tp of plan.taskPlans) {
190
+ if (!tp.task.acceptanceCriteria || tp.task.acceptanceCriteria.length === 0)
191
+ continue;
192
+ lines.push(`**${tp.task.name}** (${tp.task.id}):`);
193
+ for (const scenario of tp.task.acceptanceCriteria) {
194
+ lines.push(` - Scenario: ${scenario.scenario}`);
195
+ for (const g of scenario.given)
196
+ lines.push(` - Given ${g}`);
197
+ for (const w of scenario.when)
198
+ lines.push(` - When ${w}`);
199
+ for (const t of scenario.then)
200
+ lines.push(` - Then ${t}`);
201
+ }
202
+ lines.push('');
203
+ }
204
+ }
205
+ const basePathForDetect = plan.taskPlans[0]?.task?.files?.[0]
206
+ ? path.dirname(plan.taskPlans[0].task.files[0])
207
+ : undefined;
208
+ if (allScenarios.length > 0 && basePathForDetect) {
209
+ try {
210
+ const bdd = detectBDDFramework(basePathForDetect);
211
+ if (bdd.detected) {
212
+ lines.push('### BDD Framework Detected');
213
+ lines.push('');
214
+ lines.push(`- Framework: ${bdd.framework}`);
215
+ if (bdd.testCommand)
216
+ lines.push(`- Test command: \`${bdd.testCommand}\``);
217
+ if (bdd.featureDir)
218
+ lines.push(`- Feature directory: \`${bdd.featureDir}\``);
219
+ lines.push('');
220
+ }
221
+ }
222
+ catch {
223
+ }
224
+ }
180
225
  lines.push('### Instructions');
181
226
  lines.push('');
182
227
  lines.push('For each task above, delegate using `Task(<agent_tier>)` with the specified mask.');
@@ -254,6 +299,9 @@ export async function runAIVerification(options) {
254
299
  case 'accessibility':
255
300
  result = await runAccessibilityVerification(options);
256
301
  break;
302
+ case 'gherkin':
303
+ result = await runGherkinVerification(options);
304
+ break;
257
305
  default:
258
306
  continue;
259
307
  }
@@ -293,6 +341,7 @@ function getVerificationLayers(options) {
293
341
  { name: 'Screenshot', order: 6, type: 'visual', enabled: options.enableScreenshots ?? false },
294
342
  { name: 'APICheck', order: 7, type: 'api', enabled: !!options.devServerUrl },
295
343
  { name: 'Accessibility', order: 8, type: 'accessibility', enabled: false },
344
+ { name: 'GherkinAcceptance', order: 9, type: 'gherkin', enabled: (options.gherkinScenarios?.length ?? 0) > 0 },
296
345
  ];
297
346
  }
298
347
  async function runShellCommand(cmd, cwd, options) {
@@ -606,6 +655,62 @@ async function runAccessibilityVerification(options) {
606
655
  logs.push('[A11y] Would run axe-core accessibility scan');
607
656
  return { passed: true, logs, layer: 'Accessibility', duration: 0 };
608
657
  }
658
+ async function runGherkinVerification(options) {
659
+ const startTime = Date.now();
660
+ const scenarios = options.gherkinScenarios || [];
661
+ const logs = [];
662
+ if (scenarios.length === 0) {
663
+ return { passed: true, logs: ['[Gherkin] No scenarios to verify'], layer: 'GherkinAcceptance', duration: 0 };
664
+ }
665
+ logs.push(`[Gherkin] Verifying ${scenarios.length} acceptance scenario(s)...`);
666
+ const bdd = detectBDDFramework(options.projectPath);
667
+ if (bdd.detected && bdd.testCommand) {
668
+ logs.push(`[Gherkin] BDD framework detected: ${bdd.framework}`);
669
+ logs.push(`[Gherkin] Running: ${bdd.testCommand}`);
670
+ const result = await runShellCommand(bdd.testCommand, options.projectPath);
671
+ if (result.stdout) {
672
+ for (const line of result.stdout.split(/\r?\n/).slice(-10)) {
673
+ if (line.trim())
674
+ logs.push(` > ${line}`);
675
+ }
676
+ }
677
+ if (result.exitCode === 0) {
678
+ logs.push(`[Gherkin] BDD tests passed`);
679
+ return { passed: true, logs, layer: 'GherkinAcceptance', duration: result.durationMs };
680
+ }
681
+ logs.push(`[Gherkin] BDD tests failed (exit=${result.exitCode})`);
682
+ if (result.stderr) {
683
+ for (const line of result.stderr.split(/\r?\n/).slice(-5)) {
684
+ if (line.trim())
685
+ logs.push(` > ${line}`);
686
+ }
687
+ }
688
+ return {
689
+ passed: false,
690
+ error: `${scenarios.length} Gherkin scenario(s) failed BDD verification`,
691
+ logs,
692
+ layer: 'GherkinAcceptance',
693
+ duration: result.durationMs,
694
+ };
695
+ }
696
+ logs.push(`[Gherkin] No BDD framework detected. Generating AI verification checklist.`);
697
+ logs.push('');
698
+ logs.push('## Acceptance Scenarios to Verify:');
699
+ logs.push('');
700
+ for (const scenario of scenarios) {
701
+ logs.push(`**Scenario: ${scenario.scenario}**`);
702
+ for (const g of scenario.given)
703
+ logs.push(` Given ${g}`);
704
+ for (const w of scenario.when)
705
+ logs.push(` When ${w}`);
706
+ for (const t of scenario.then)
707
+ logs.push(` Then ${t}`);
708
+ logs.push('');
709
+ }
710
+ logs.push('[Gherkin] AI should verify each scenario by examining the implementation.');
711
+ logs.push('[Gherkin] If any "Then" assertion is not satisfied, report failure.');
712
+ return { passed: true, logs, layer: 'GherkinAcceptance', duration: Date.now() - startTime };
713
+ }
609
714
  export function generateVerificationReport(results) {
610
715
  const lines = [
611
716
  '## AI Verification Results\n',
@@ -1,6 +1,7 @@
1
1
  import { getPhaseManager } from '../phase-manager.js';
2
2
  import { getEffectiveGdcConfig, runGdcMachineCommand, getGraphNodeIds, getGraphEdges, countGdcCheckIssues, } from '../gdc.js';
3
3
  import { generateOpenSpecArtifacts, ensureOpenSpecWorkspace } from './openspec.js';
4
+ import { generateGherkinForPhase, generateGherkinForTask, writeAllFeatureFiles, } from '../gherkin.js';
4
5
  const PHASE_SIZE_GUIDE = {
5
6
  tooSmall: ['변수명 변경', '오타 수정'],
6
7
  justRight: ['UI 컴포넌트 하나', '저장 기능', 'API 엔드포인트 하나'],
@@ -406,6 +407,9 @@ export async function plan(options) {
406
407
  : undefined;
407
408
  const shardScope = shardOriginalPhases.map(phase => phase.name).join(', ');
408
409
  const shardHours = shardOriginalPhases.reduce((sum, phase) => sum + (phase.estimatedHours || 3), 0);
410
+ for (const phase of shardOriginalPhases) {
411
+ phase.acceptanceCriteria = generateGherkinForPhase(phase);
412
+ }
409
413
  const shardPlan = await manager.createPlan({
410
414
  planName: shardPlanName,
411
415
  projectName,
@@ -490,6 +494,22 @@ export async function plan(options) {
490
494
  }
491
495
  catch {
492
496
  }
497
+ for (const phase of weavePlan.phases) {
498
+ if (!phase.acceptanceCriteria || phase.acceptanceCriteria.length === 0) {
499
+ phase.acceptanceCriteria = generateGherkinForPhase(phase);
500
+ }
501
+ }
502
+ try {
503
+ const featurePaths = await writeAllFeatureFiles(basePath || process.cwd(), weavePlan.phases);
504
+ for (const [phaseId, featurePath] of featurePaths) {
505
+ const phase = weavePlan.phases.find(p => p.id === phaseId);
506
+ if (phase)
507
+ phase.featurePath = featurePath;
508
+ }
509
+ await manager.savePlan(weavePlan);
510
+ }
511
+ catch {
512
+ }
493
513
  const summary = generatePlanSummary(weavePlan, estimatedTotalHours);
494
514
  return {
495
515
  plan: weavePlan,
@@ -508,6 +528,8 @@ function generateDefaultPhaseTasks(phase, gdc) {
508
528
  .flatMap(nodeId => gdc?.nodeFileMap?.get(nodeId) || [])
509
529
  .filter(Boolean)
510
530
  .slice(0, 8);
531
+ const implCriteria = generateGherkinForTask({ id: `${baseId}-T1`, name: `${title} 구현`, testCase: phase.doneWhen }, phase);
532
+ const testCriteria = generateGherkinForTask({ id: `${baseId}-T2`, name: `${title} 테스트 추가/수정`, testCase: '관련 테스트가 통과한다' }, phase);
511
533
  return [
512
534
  {
513
535
  id: `${baseId}-T1`,
@@ -523,6 +545,7 @@ function generateDefaultPhaseTasks(phase, gdc) {
523
545
  `phase:${phase.id}`,
524
546
  `done_when:${phase.doneWhen}`,
525
547
  ],
548
+ acceptanceCriteria: [implCriteria],
526
549
  maxRetries: 3,
527
550
  },
528
551
  {
@@ -538,6 +561,7 @@ function generateDefaultPhaseTasks(phase, gdc) {
538
561
  { kind: 'command', value: 'gdc check --machine' },
539
562
  ],
540
563
  acceptanceRefs: [`phase:${phase.id}:tests`],
564
+ acceptanceCriteria: [testCriteria],
541
565
  maxRetries: 2,
542
566
  },
543
567
  {