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.
@@ -1,3 +1,4 @@
1
+ import { parseGherkinBlock } from '../gherkin.js';
1
2
  const STATUS_VALUES = new Set([
2
3
  'pending',
3
4
  'in_progress',
@@ -347,6 +348,80 @@ export function refinePlanFromNotes(plan, notesContent) {
347
348
  changes.push(`~ ${phase.id} task ${taskId}: "${old}" -> "${name}"`);
348
349
  }
349
350
  }
351
+ if (matched)
352
+ continue;
353
+ const addCriteria = /^@phase\s+([A-Za-z0-9_-]+)\s+add_criteria\s*:\s*(.+)$/is.exec(line);
354
+ if (addCriteria) {
355
+ directivesParsed += 1;
356
+ matched = true;
357
+ const phaseId = addCriteria[1].toUpperCase();
358
+ const rawBlock = addCriteria[2].trim();
359
+ const phase = findPhase(updated, phaseId);
360
+ if (!phase) {
361
+ warnings.push(`Line ${i + 1}: phase ${phaseId} not found (skipped criteria add).`);
362
+ continue;
363
+ }
364
+ const scenarios = parseGherkinBlock(rawBlock);
365
+ if (scenarios.length === 0) {
366
+ warnings.push(`Line ${i + 1}: could not parse Gherkin block for ${phaseId}.`);
367
+ continue;
368
+ }
369
+ if (!phase.acceptanceCriteria)
370
+ phase.acceptanceCriteria = [];
371
+ phase.acceptanceCriteria.push(...scenarios);
372
+ changes.push(`+ ${phaseId} acceptance criteria: ${scenarios.length} scenario(s)`);
373
+ }
374
+ if (matched)
375
+ continue;
376
+ const replaceCriteria = /^@phase\s+([A-Za-z0-9_-]+)\s+replace_criteria\s*:\s*(.+)$/is.exec(line);
377
+ if (replaceCriteria) {
378
+ directivesParsed += 1;
379
+ matched = true;
380
+ const phaseId = replaceCriteria[1].toUpperCase();
381
+ const rawBlock = replaceCriteria[2].trim();
382
+ const phase = findPhase(updated, phaseId);
383
+ if (!phase) {
384
+ warnings.push(`Line ${i + 1}: phase ${phaseId} not found (skipped criteria replace).`);
385
+ continue;
386
+ }
387
+ const scenarios = parseGherkinBlock(rawBlock);
388
+ if (scenarios.length === 0) {
389
+ warnings.push(`Line ${i + 1}: could not parse Gherkin block for ${phaseId}.`);
390
+ continue;
391
+ }
392
+ phase.acceptanceCriteria = scenarios;
393
+ changes.push(`~ ${phaseId} acceptance criteria replaced: ${scenarios.length} scenario(s)`);
394
+ }
395
+ if (matched)
396
+ continue;
397
+ const taskCriteria = /^@task\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)\s+criteria\s*:\s*(.+)$/is.exec(line);
398
+ if (taskCriteria) {
399
+ directivesParsed += 1;
400
+ matched = true;
401
+ const phaseId = taskCriteria[1].toUpperCase();
402
+ const taskId = taskCriteria[2].trim();
403
+ const rawBlock = taskCriteria[3].trim();
404
+ const phase = findPhase(updated, phaseId);
405
+ if (!phase) {
406
+ warnings.push(`Line ${i + 1}: phase ${phaseId} not found (skipped task criteria).`);
407
+ continue;
408
+ }
409
+ const task = phase.tasks.find(t => t.id === taskId);
410
+ if (!task) {
411
+ warnings.push(`Line ${i + 1}: task ${taskId} not found in ${phaseId} (skipped criteria).`);
412
+ continue;
413
+ }
414
+ const scenarios = parseGherkinBlock(rawBlock);
415
+ if (scenarios.length === 0) {
416
+ warnings.push(`Line ${i + 1}: could not parse Gherkin block for ${taskId}.`);
417
+ continue;
418
+ }
419
+ task.acceptanceCriteria = scenarios;
420
+ if (!task.verify)
421
+ task.verify = [];
422
+ task.verify.push({ kind: 'gherkin', value: `${scenarios.length} scenario(s)` });
423
+ changes.push(`~ ${phaseId}/${taskId} acceptance criteria: ${scenarios.length} scenario(s)`);
424
+ }
350
425
  if (matched)
351
426
  continue;
352
427
  warnings.push(`Line ${i + 1}: unrecognized directive: ${line}`);
@@ -123,6 +123,15 @@ export interface WeavePhase {
123
123
  masksUsed?: string[];
124
124
  startedAt?: string;
125
125
  completedAt?: string;
126
+ acceptanceCriteria?: GherkinScenario[];
127
+ featurePath?: string;
128
+ }
129
+ export interface GherkinScenario {
130
+ feature: string;
131
+ scenario: string;
132
+ given: string[];
133
+ when: string[];
134
+ then: string[];
126
135
  }
127
136
  export interface WeaveTask {
128
137
  id: string;
@@ -134,10 +143,11 @@ export interface WeaveTask {
134
143
  files?: string[];
135
144
  dependsOn?: string[];
136
145
  verify?: Array<{
137
- kind: 'command' | 'checklist';
146
+ kind: 'command' | 'checklist' | 'gherkin';
138
147
  value: string;
139
148
  }>;
140
149
  acceptanceRefs?: string[];
150
+ acceptanceCriteria?: GherkinScenario[];
141
151
  retryCount: number;
142
152
  maxRetries: number;
143
153
  lastError?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maskweaver",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "AI Expert Persona System - Give your AI coding assistant expert personalities (가면술사)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/postinstall.mjs CHANGED
@@ -71,112 +71,209 @@ function getPackageVersion() {
71
71
  }
72
72
  }
73
73
 
74
- const DEFAULT_GLOBAL_CONFIG_TEMPLATE = {
75
- dummyHumans: {
76
- pool: [
77
- {
78
- id: 'deepseek-flash',
79
- model: 'opencode-go/deepseek-v4-flash',
80
- tier: 'flash',
81
- maxConcurrent: 5,
82
- capabilities: ['search', 'formatting', 'simple-coding', 'file-ops'],
83
- costTier: 'low',
84
- description: 'DeepSeek V4 Flash - 빠름. 단순 검색/포매팅/파일작업',
85
- },
86
- {
87
- id: 'deepseek-general',
88
- model: 'opencode-go/deepseek-v4-flash',
89
- tier: 'human',
90
- maxConcurrent: 3,
91
- capabilities: ['coding', 'testing', 'refactoring', 'backend'],
92
- costTier: 'medium',
93
- description: 'DeepSeek V4 Flash - 일반. 코딩/리팩토링/백엔드',
94
- },
95
- {
96
- id: 'qwen-vision',
97
- model: 'opencode-go/qwen3.6-plus',
98
- tier: 'human',
99
- maxConcurrent: 3,
100
- capabilities: ['vision', 'frontend', 'testing'],
101
- costTier: 'medium',
102
- description: 'Qwen 3.6 Plus - 비전. 이미지 분석/프론트엔드/테스트',
103
- },
104
- {
105
- id: 'deepseek-pro',
106
- model: 'opencode-go/deepseek-v4-pro',
107
- tier: 'premium',
108
- maxConcurrent: 2,
109
- capabilities: ['architecture', 'debugging', 'reasoning', 'complex-coding', 'refactoring'],
110
- costTier: 'high',
111
- description: 'DeepSeek V4 Pro - 고급 추론. 아키텍처/복잡 디버깅',
112
- },
113
- {
114
- id: 'kimi-vision',
115
- model: 'opencode-go/kimi-k2.6',
116
- tier: 'premium',
117
- maxConcurrent: 2,
118
- capabilities: ['vision', 'reasoning', 'complex-coding', 'architecture', 'debugging'],
119
- costTier: 'high',
120
- description: 'Kimi K2.6 - 비전 고급. 이미지 분석/복잡 추론',
121
- },
122
- ],
74
+ const ZAI_POOL = [
75
+ {
76
+ id: 'glm-flash',
77
+ model: 'zai-coding-plan/glm-5-turbo',
78
+ tier: 'flash',
79
+ maxConcurrent: 1,
80
+ capabilities: ['search', 'formatting', 'simple-coding', 'file-ops'],
81
+ costTier: 'low',
82
+ description: 'GLM-5 Turbo - 빠름. 단순 검색/포매팅/파일작업',
123
83
  },
124
- operator: {
84
+ {
85
+ id: 'glm-general',
86
+ model: 'zai-coding-plan/glm-5.1',
87
+ tier: 'human',
88
+ maxConcurrent: 10,
89
+ capabilities: ['coding', 'testing', 'refactoring', 'backend'],
90
+ costTier: 'medium',
91
+ description: 'GLM-5.1 - 일반. 코딩/리팩토링/백엔드',
92
+ },
93
+ {
94
+ id: 'glm-premium',
95
+ model: 'zai-coding-plan/glm-5.1',
96
+ tier: 'premium',
97
+ maxConcurrent: 10,
98
+ capabilities: ['architecture', 'debugging', 'reasoning', 'complex-coding', 'refactoring'],
99
+ costTier: 'high',
100
+ description: 'GLM-5.1 - 고급 추론. 아키텍처/복잡 디버깅',
101
+ },
102
+ ];
103
+
104
+ const OPENCODE_GO_POOL = [
105
+ {
106
+ id: 'deepseek-flash',
107
+ model: 'opencode-go/deepseek-v4-flash',
108
+ tier: 'flash',
109
+ maxConcurrent: 5,
110
+ capabilities: ['search', 'formatting', 'simple-coding', 'file-ops'],
111
+ costTier: 'low',
112
+ description: 'DeepSeek V4 Flash - 빠름. 단순 검색/포매팅/파일작업',
113
+ },
114
+ {
115
+ id: 'deepseek-general',
116
+ model: 'opencode-go/deepseek-v4-flash',
117
+ tier: 'human',
118
+ maxConcurrent: 3,
119
+ capabilities: ['coding', 'testing', 'refactoring', 'backend'],
120
+ costTier: 'medium',
121
+ description: 'DeepSeek V4 Flash - 일반. 코딩/리팩토링/백엔드',
122
+ },
123
+ {
124
+ id: 'qwen-vision',
125
+ model: 'opencode-go/qwen3.6-plus',
126
+ tier: 'human',
127
+ maxConcurrent: 3,
128
+ capabilities: ['vision', 'frontend', 'testing'],
129
+ costTier: 'medium',
130
+ description: 'Qwen 3.6 Plus - 비전. 이미지 분석/프론트엔드/테스트',
131
+ },
132
+ {
133
+ id: 'deepseek-pro',
125
134
  model: 'opencode-go/deepseek-v4-pro',
135
+ tier: 'premium',
126
136
  maxConcurrent: 2,
127
- description: 'Squad Operator model - 작업 오케스트레이션 및 고급 추론',
128
- },
129
- memory: {
130
- provider: 'text-only',
131
- enabled: false,
137
+ capabilities: ['architecture', 'debugging', 'reasoning', 'complex-coding', 'refactoring'],
138
+ costTier: 'high',
139
+ description: 'DeepSeek V4 Pro - 고급 추론. 아키텍처/복잡 디버깅',
132
140
  },
133
- gdc: {
134
- enabled: 'auto',
135
- strictVerify: false,
136
- autoSyncOnPrepare: true,
141
+ {
142
+ id: 'kimi-vision',
143
+ model: 'opencode-go/kimi-k2.6',
144
+ tier: 'premium',
145
+ maxConcurrent: 2,
146
+ capabilities: ['vision', 'reasoning', 'complex-coding', 'architecture', 'debugging'],
147
+ costTier: 'high',
148
+ description: 'Kimi K2.6 - 비전 고급. 이미지 분석/복잡 추론',
137
149
  },
138
- language: 'ko',
139
- };
150
+ ];
151
+
152
+ function runCli(command, args) {
153
+ try {
154
+ const result = spawnSync(command, args, {
155
+ encoding: 'utf-8',
156
+ stdio: ['pipe', 'pipe', 'pipe'],
157
+ timeout: 8000,
158
+ windowsHide: true,
159
+ });
160
+ if (result.error || result.status !== 0) return null;
161
+ return result.stdout || null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ function detectSubscription() {
168
+ let hasOpencodeGo = false;
169
+ let hasZai = false;
170
+
171
+ const providersOutput = runCli('opencode', ['providers', 'list']);
172
+ if (providersOutput) {
173
+ const stripped = providersOutput.replace(/\x1b\[[0-9;]*m/g, '');
174
+ if (/opencode\s*go/i.test(stripped)) hasOpencodeGo = true;
175
+ if (/z\.ai\s*coding\s*plan|zai-coding-plan/i.test(stripped)) hasZai = true;
176
+ }
177
+
178
+ const modelsOutput = runCli('opencode', ['models']);
179
+ if (modelsOutput) {
180
+ if (/^opencode-go\//m.test(modelsOutput)) hasOpencodeGo = true;
181
+ if (/^zai-coding-plan\//m.test(modelsOutput)) hasZai = true;
182
+ }
183
+
184
+ if (hasZai) return 'zai-coding-plan';
185
+ if (hasOpencodeGo) return 'opencode-go';
186
+
187
+ const candidates = [
188
+ join(homedir(), '.config', 'opencode', 'opencode.json'),
189
+ join(homedir(), '.config', 'opencode', 'opencode.jsonc'),
190
+ ];
191
+
192
+ for (const candidate of candidates) {
193
+ if (!existsSync(candidate)) continue;
194
+ try {
195
+ let content = readFileSync(candidate, 'utf-8');
196
+ content = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
197
+ const parsed = JSON.parse(content);
198
+ if (!parsed || typeof parsed !== 'object') continue;
199
+
200
+ const modelFields = ['model', 'small_model', 'large_model'];
201
+ for (const field of modelFields) {
202
+ const val = parsed[field];
203
+ if (typeof val !== 'string' || !val) continue;
204
+ if (val.startsWith('opencode-go/')) hasOpencodeGo = true;
205
+ if (val.startsWith('zai-coding-plan/')) hasZai = true;
206
+ }
207
+ } catch { continue; }
208
+ }
209
+
210
+ if (hasZai) return 'zai-coding-plan';
211
+ if (hasOpencodeGo) return 'opencode-go';
212
+ return 'opencode-go';
213
+ }
214
+
215
+ function buildConfigForSubscription(subscription) {
216
+ const pool = subscription === 'zai-coding-plan'
217
+ ? [...ZAI_POOL, ...OPENCODE_GO_POOL]
218
+ : [...OPENCODE_GO_POOL];
219
+
220
+ const operatorModel = subscription === 'zai-coding-plan'
221
+ ? 'zai-coding-plan/glm-5.1'
222
+ : 'opencode-go/deepseek-v4-pro';
223
+
224
+ const operatorConcurrent = subscription === 'zai-coding-plan' ? 10 : 2;
225
+
226
+ return {
227
+ dummyHumans: { pool },
228
+ operator: {
229
+ model: operatorModel,
230
+ maxConcurrent: operatorConcurrent,
231
+ description: 'Squad Operator model - 작업 오케스트레이션 및 고급 추론',
232
+ },
233
+ memory: { provider: 'text-only', enabled: false },
234
+ gdc: { enabled: 'auto', strictVerify: false, autoSyncOnPrepare: true },
235
+ language: 'ko',
236
+ };
237
+ }
238
+
239
+ const DEFAULT_GLOBAL_CONFIG_TEMPLATE = buildConfigForSubscription('opencode-go');
140
240
 
141
241
  function ensureGlobalConfig() {
142
242
  const globalDir = join(homedir(), '.config', 'opencode');
143
243
  const globalConfigPath = join(globalDir, 'maskweaver.config.json');
144
244
 
145
245
  if (!existsSync(globalConfigPath)) {
146
- // Fresh install: create config from template
246
+ const subscription = detectSubscription();
247
+ const template = buildConfigForSubscription(subscription);
147
248
  try {
148
249
  if (!existsSync(globalDir)) {
149
250
  mkdirSync(globalDir, { recursive: true });
150
251
  }
151
252
  writeFileSync(
152
253
  globalConfigPath,
153
- JSON.stringify(DEFAULT_GLOBAL_CONFIG_TEMPLATE, null, 2) + '\n',
254
+ JSON.stringify(template, null, 2) + '\n',
154
255
  'utf-8'
155
256
  );
156
- return { created: true, migrated: false };
257
+ return { created: true, migrated: false, subscription };
157
258
  } catch {
158
- return { created: false, migrated: false };
259
+ return { created: false, migrated: false, subscription };
159
260
  }
160
261
  }
161
262
 
162
- // Existing config: migrate — add missing fields without overwriting user edits
163
263
  try {
164
264
  const existing = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
165
265
  let changed = false;
166
266
 
167
- // Migrate: add operator if missing
168
267
  if (!existing.operator) {
169
268
  existing.operator = DEFAULT_GLOBAL_CONFIG_TEMPLATE.operator;
170
269
  changed = true;
171
270
  }
172
271
 
173
- // Migrate: add gdc if missing
174
272
  if (!existing.gdc) {
175
273
  existing.gdc = DEFAULT_GLOBAL_CONFIG_TEMPLATE.gdc;
176
274
  changed = true;
177
275
  }
178
276
 
179
- // Migrate: add dummyHumans if missing
180
277
  if (!existing.dummyHumans) {
181
278
  existing.dummyHumans = DEFAULT_GLOBAL_CONFIG_TEMPLATE.dummyHumans;
182
279
  changed = true;
@@ -190,9 +287,10 @@ function ensureGlobalConfig() {
190
287
  );
191
288
  }
192
289
 
193
- return { created: false, migrated: changed };
290
+ const subscription = detectSubscription();
291
+ return { created: false, migrated: changed, subscription };
194
292
  } catch {
195
- return { created: false, migrated: false };
293
+ return { created: false, migrated: false, subscription: 'opencode-go' };
196
294
  }
197
295
  }
198
296
 
@@ -204,6 +302,7 @@ function main() {
204
302
  if (configResult.created) {
205
303
  console.log(`✓ maskweaver v${pkgVersion}: 글로벌 설정 파일 생성됨`);
206
304
  console.log(` → ~/.config/opencode/maskweaver.config.json`);
305
+ console.log(` 감지된 구독: ${configResult.subscription}`);
207
306
  console.log(` 편집 후 프로젝트에서 \`weave sync-agents\`를 실행하세요`);
208
307
  console.log('');
209
308
  } else if (configResult.migrated) {
@@ -211,6 +310,10 @@ function main() {
211
310
  console.log(` → ~/.config/opencode/maskweaver.config.json`);
212
311
  console.log(` operator 및 gdc 설정이 추가되었습니다.`);
213
312
  console.log('');
313
+ } else if (configResult.subscription === 'zai-coding-plan') {
314
+ console.log(`✓ maskweaver v${pkgVersion}: zai-coding-plan 구독 감지됨`);
315
+ console.log(` GLM-5.1 모델이 풀에 포함되어 있습니다.`);
316
+ console.log('');
214
317
  }
215
318
 
216
319
  const versionCheck = checkOpenCodeVersion();