maskweaver 0.9.9 → 0.10.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.
@@ -0,0 +1,7 @@
1
+ export interface NpmDistTags {
2
+ latest?: string;
3
+ beta?: string;
4
+ next?: string;
5
+ [tag: string]: string | undefined;
6
+ }
7
+ export declare function fetchNpmDistTags(packageName: string): Promise<NpmDistTags | null>;
@@ -0,0 +1,15 @@
1
+ const NPM_FETCH_TIMEOUT_MS = 5000;
2
+ export async function fetchNpmDistTags(packageName) {
3
+ try {
4
+ const res = await fetch(`https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`, {
5
+ signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
6
+ });
7
+ if (!res.ok)
8
+ return null;
9
+ const data = (await res.json());
10
+ return data;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export declare function getPluginNameWithVersion(currentVersion: string, packageName: string): Promise<string>;
@@ -0,0 +1,21 @@
1
+ import { fetchNpmDistTags } from "./npm-dist-tags.js";
2
+ const PRIORITIZED_TAGS = ["latest", "beta", "next"];
3
+ function getFallbackEntry(version, packageName) {
4
+ const prereleaseMatch = version.match(/-([a-zA-Z][a-zA-Z0-9-]*)(?:\.|$)/);
5
+ if (prereleaseMatch) {
6
+ return `${packageName}@${prereleaseMatch[1]}`;
7
+ }
8
+ return packageName;
9
+ }
10
+ export async function getPluginNameWithVersion(currentVersion, packageName) {
11
+ const distTags = await fetchNpmDistTags(packageName);
12
+ if (distTags) {
13
+ const allTags = new Set([...PRIORITIZED_TAGS, ...Object.keys(distTags)]);
14
+ for (const tag of allTags) {
15
+ if (distTags[tag] === currentVersion) {
16
+ return `${packageName}@${tag}`;
17
+ }
18
+ }
19
+ }
20
+ return getFallbackEntry(currentVersion, packageName);
21
+ }
@@ -0,0 +1,9 @@
1
+ export interface VersionCompatibility {
2
+ canUpgrade: boolean;
3
+ reason?: string;
4
+ isDowngrade: boolean;
5
+ isMajorBump: boolean;
6
+ requiresMigration: boolean;
7
+ }
8
+ export declare function checkVersionCompatibility(currentVersion: string | null, newVersion: string): VersionCompatibility;
9
+ export declare function extractVersionFromPluginEntry(entry: string): string | null;
@@ -0,0 +1,81 @@
1
+ function parseVersion(version) {
2
+ const clean = version.replace(/^v/, "").split("-")[0];
3
+ return clean.split(".").map(Number);
4
+ }
5
+ function compareVersions(a, b) {
6
+ const partsA = parseVersion(a);
7
+ const partsB = parseVersion(b);
8
+ const maxLen = Math.max(partsA.length, partsB.length);
9
+ for (let i = 0; i < maxLen; i++) {
10
+ const numA = partsA[i] ?? 0;
11
+ const numB = partsB[i] ?? 0;
12
+ if (numA !== numB) {
13
+ return numA - numB;
14
+ }
15
+ }
16
+ return 0;
17
+ }
18
+ export function checkVersionCompatibility(currentVersion, newVersion) {
19
+ if (!currentVersion) {
20
+ return {
21
+ canUpgrade: true,
22
+ isDowngrade: false,
23
+ isMajorBump: false,
24
+ requiresMigration: false,
25
+ };
26
+ }
27
+ const cleanCurrent = currentVersion.replace(/^v/, "");
28
+ const cleanNew = newVersion.replace(/^v/, "");
29
+ try {
30
+ const comparison = compareVersions(cleanNew, cleanCurrent);
31
+ if (comparison < 0) {
32
+ return {
33
+ canUpgrade: false,
34
+ reason: `Downgrade from ${currentVersion} to ${newVersion} is not allowed`,
35
+ isDowngrade: true,
36
+ isMajorBump: false,
37
+ requiresMigration: false,
38
+ };
39
+ }
40
+ if (comparison === 0) {
41
+ return {
42
+ canUpgrade: true,
43
+ reason: `Version ${newVersion} is already installed`,
44
+ isDowngrade: false,
45
+ isMajorBump: false,
46
+ requiresMigration: false,
47
+ };
48
+ }
49
+ const currentMajor = cleanCurrent.split(".")[0];
50
+ const newMajor = cleanNew.split(".")[0];
51
+ const isMajorBump = currentMajor !== newMajor;
52
+ if (isMajorBump) {
53
+ return {
54
+ canUpgrade: true,
55
+ reason: `Major version upgrade from ${currentVersion} to ${newVersion} - configuration migration may be required`,
56
+ isDowngrade: false,
57
+ isMajorBump: true,
58
+ requiresMigration: true,
59
+ };
60
+ }
61
+ return {
62
+ canUpgrade: true,
63
+ isDowngrade: false,
64
+ isMajorBump: false,
65
+ requiresMigration: false,
66
+ };
67
+ }
68
+ catch {
69
+ return {
70
+ canUpgrade: true,
71
+ reason: `Unable to compare versions ${currentVersion} and ${newVersion} - proceeding with caution`,
72
+ isDowngrade: false,
73
+ isMajorBump: false,
74
+ requiresMigration: false,
75
+ };
76
+ }
77
+ }
78
+ export function extractVersionFromPluginEntry(entry) {
79
+ const match = entry.match(/@(.+)$/);
80
+ return match ? match[1] : null;
81
+ }
@@ -7,8 +7,10 @@ import { VERSION } from "../version.js";
7
7
  import { runDoctor, printDoctorReport } from "./doctor.js";
8
8
  import { writeDefaultRuntimeConfig, writeDefaultPluginConfig } from "../shared/generate-agents.js";
9
9
  const PLUGIN_NAME = "maskweaver";
10
+ const PLUGIN_LATEST = "maskweaver@latest";
10
11
  const LEGACY_PLUGIN_NAMES = ["maskweaver/plugin", "@maskweaver/plugin"];
11
- const ALL_PLUGIN_NAMES = [PLUGIN_NAME, ...LEGACY_PLUGIN_NAMES];
12
+ const ALL_PLUGIN_NAMES = [PLUGIN_NAME, PLUGIN_LATEST, ...LEGACY_PLUGIN_NAMES];
13
+ const ALL_BARE_OR_VERSIONED = [PLUGIN_NAME, PLUGIN_LATEST];
12
14
  const CONFIG_NAME = "opencode.json";
13
15
  const colors = {
14
16
  reset: "\x1b[0m",
@@ -83,19 +85,31 @@ function getInstalledPluginEntries(config) {
83
85
  return config.plugin.filter((plugin) => ALL_PLUGIN_NAMES.includes(plugin));
84
86
  }
85
87
  function isPluginInstalled(config) {
86
- return getInstalledPluginEntries(config).length > 0;
88
+ if (!config.plugin || !Array.isArray(config.plugin))
89
+ return false;
90
+ return config.plugin.some((p) => ALL_BARE_OR_VERSIONED.some(name => p === name || p.startsWith(`${name}@`)));
87
91
  }
88
92
  function normalizePluginEntries(config) {
89
93
  if (!config.plugin || !Array.isArray(config.plugin)) {
90
94
  return { migratedFrom: [], changed: false };
91
95
  }
92
96
  const migratedFrom = config.plugin.filter((plugin) => LEGACY_PLUGIN_NAMES.includes(plugin));
93
- if (migratedFrom.length === 0) {
97
+ const hasBare = config.plugin.some((p) => p === PLUGIN_NAME);
98
+ const hasVersioned = config.plugin.some((p) => p.startsWith(`${PLUGIN_NAME}@`));
99
+ if (migratedFrom.length === 0 && !hasBare && !hasVersioned) {
100
+ if (config.plugin.some((p) => p === PLUGIN_LATEST)) {
101
+ return { migratedFrom: [], changed: false };
102
+ }
94
103
  return { migratedFrom: [], changed: false };
95
104
  }
96
- const preserved = config.plugin.filter((plugin) => !ALL_PLUGIN_NAMES.includes(plugin));
97
- config.plugin = [...preserved, PLUGIN_NAME];
98
- return { migratedFrom, changed: true };
105
+ const preserved = config.plugin.filter((plugin) => !ALL_PLUGIN_NAMES.includes(plugin) &&
106
+ !plugin.startsWith(`${PLUGIN_NAME}@`));
107
+ const existingVersioned = config.plugin.find((p) => p.startsWith(`${PLUGIN_NAME}@`));
108
+ config.plugin = [...preserved, existingVersioned || PLUGIN_LATEST];
109
+ if (migratedFrom.length > 0 || hasBare) {
110
+ return { migratedFrom: [...migratedFrom, hasBare ? PLUGIN_NAME : ""].filter(Boolean), changed: true };
111
+ }
112
+ return { migratedFrom: [], changed: false };
99
113
  }
100
114
  async function installPlugin(options) {
101
115
  const isLocal = options.local || false;
@@ -120,13 +134,14 @@ async function installPlugin(options) {
120
134
  log("");
121
135
  return;
122
136
  }
123
- if (config.plugin.includes(PLUGIN_NAME)) {
137
+ const alreadyInstalled = config.plugin.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`));
138
+ if (alreadyInstalled) {
124
139
  warning(`플러그인이 이미 설치되어 있습니다.`);
125
- info(`현재 ${scope} 설정에 등록되어 있습니다: ${PLUGIN_NAME}`);
140
+ info(`현재 ${scope} 설정에 등록되어 있습니다: ${PLUGIN_LATEST}`);
126
141
  log("");
127
142
  return;
128
143
  }
129
- config.plugin.push(PLUGIN_NAME);
144
+ config.plugin.push(PLUGIN_LATEST);
130
145
  writeConfig(configPath, config);
131
146
  let createdRuntimeConfig = false;
132
147
  let createdPluginConfig = false;
@@ -150,7 +165,7 @@ async function installPlugin(options) {
150
165
  }
151
166
  success(`플러그인이 성공적으로 설치되었습니다!`);
152
167
  log("");
153
- log(`📦 설치된 플러그인: ${PLUGIN_NAME}`, "bright");
168
+ log(`📦 설치된 플러그인: ${PLUGIN_LATEST}`, "bright");
154
169
  log(`📍 위치: ${configPath}`, "dim");
155
170
  log("");
156
171
  log(`다음 단계:`, "bright");
@@ -190,7 +205,7 @@ async function uninstallPlugin(options) {
190
205
  log("");
191
206
  return;
192
207
  }
193
- config.plugin = config.plugin.filter((p) => !ALL_PLUGIN_NAMES.includes(p));
208
+ config.plugin = config.plugin.filter((p) => !ALL_PLUGIN_NAMES.includes(p) && !p.startsWith(`${PLUGIN_NAME}@`));
194
209
  writeConfig(configPath, config);
195
210
  success(`플러그인이 성공적으로 제거되었습니다.`);
196
211
  log("");
@@ -5,6 +5,8 @@ import * as os from 'node:os';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { VERSION } from '../version.js';
8
+ import { fetchNpmDistTags } from '../cli/config-manager/npm-dist-tags.js';
9
+ import { checkVersionCompatibility } from '../cli/config-manager/version-compatibility.js';
8
10
  import { loadPluginConfig, isMaskEnabled, isToolEnabled, getDefaultMask, isAutoActivateEnabled, isVerboseLoggingEnabled, isCompletionSoundEnabled, validateConfig, } from './config/index.js';
9
11
  import { createMemorySearchTool } from './tools/memorySearch.js';
10
12
  import { createMemoryWriteTool } from './tools/memoryWrite.js';
@@ -595,10 +597,10 @@ function playCompletionSound(config) {
595
597
  }
596
598
  let state = null;
597
599
  const OPENCODE_PACKAGES_DIR = path.join(os.homedir(), '.cache', 'opencode', 'packages');
598
- const MASKWEAVER_PACKAGE_GLOB = 'maskweaver@*';
599
- function getCacheVersion() {
600
- if (!fs.existsSync(OPENCODE_PACKAGES_DIR))
601
- return null;
600
+ function getCachedPackageVersion() {
601
+ if (!fs.existsSync(OPENCODE_PACKAGES_DIR)) {
602
+ return { version: null, pkgDir: null };
603
+ }
602
604
  const entries = fs.readdirSync(OPENCODE_PACKAGES_DIR);
603
605
  for (const entry of entries) {
604
606
  if (!entry.startsWith('maskweaver@'))
@@ -615,51 +617,88 @@ function getCacheVersion() {
615
617
  continue;
616
618
  }
617
619
  }
618
- return null;
620
+ return { version: null, pkgDir: null };
619
621
  }
620
- function getLatestNpmVersion() {
621
- try {
622
- const result = spawnSync('npm', ['view', 'maskweaver', 'version'], {
623
- encoding: 'utf-8',
624
- stdio: ['pipe', 'pipe', 'pipe'],
625
- timeout: 10000,
626
- windowsHide: true,
627
- });
628
- if (result.status === 0 && result.stdout) {
629
- return result.stdout.trim();
630
- }
631
- }
632
- catch { }
633
- return null;
622
+ async function fetchLatestFromRegistry() {
623
+ return fetchNpmDistTags('maskweaver');
634
624
  }
635
- function checkAndInvalidateCache() {
636
- const cached = getCacheVersion();
637
- if (!cached) {
638
- return { invalidated: false, cachedVersion: null, latestVersion: null };
625
+ async function checkForUpdates() {
626
+ const cached = getCachedPackageVersion();
627
+ const installedVersion = cached.version;
628
+ const latestTags = await fetchLatestFromRegistry();
629
+ const latestVersion = latestTags?.latest ?? null;
630
+ if (!installedVersion) {
631
+ return {
632
+ updateAvailable: false,
633
+ installedVersion: null,
634
+ latestVersion,
635
+ isDowngrade: false,
636
+ isMajorBump: false,
637
+ requiresMigration: false,
638
+ message: null,
639
+ };
639
640
  }
640
- if (cached.version === VERSION) {
641
- return { invalidated: false, cachedVersion: cached.version, latestVersion: null };
641
+ if (installedVersion === VERSION) {
642
+ return {
643
+ updateAvailable: false,
644
+ installedVersion,
645
+ latestVersion,
646
+ isDowngrade: false,
647
+ isMajorBump: false,
648
+ requiresMigration: false,
649
+ message: null,
650
+ };
642
651
  }
643
- const latest = getLatestNpmVersion();
644
- if (latest && latest !== cached.version) {
645
- try {
646
- fs.rmSync(cached.pkgDir, { recursive: true, force: true });
647
- return { invalidated: true, cachedVersion: cached.version, latestVersion: latest };
648
- }
649
- catch {
650
- return { invalidated: false, cachedVersion: cached.version, latestVersion: latest };
651
- }
652
+ if (!latestVersion || latestVersion === installedVersion) {
653
+ return {
654
+ updateAvailable: false,
655
+ installedVersion,
656
+ latestVersion,
657
+ isDowngrade: false,
658
+ isMajorBump: false,
659
+ requiresMigration: false,
660
+ message: `Plugin cache v${installedVersion} — current is v${VERSION}.`,
661
+ };
662
+ }
663
+ const compatibility = checkVersionCompatibility(installedVersion, latestVersion);
664
+ const currentIsLatest = installedVersion === latestVersion;
665
+ if (currentIsLatest) {
666
+ return {
667
+ updateAvailable: false,
668
+ installedVersion,
669
+ latestVersion,
670
+ isDowngrade: false,
671
+ isMajorBump: false,
672
+ requiresMigration: false,
673
+ message: `Plugin cache v${installedVersion} — current is v${VERSION}.`,
674
+ };
652
675
  }
653
- return { invalidated: false, cachedVersion: cached.version, latestVersion: latest };
676
+ return {
677
+ updateAvailable: true,
678
+ installedVersion,
679
+ latestVersion,
680
+ isDowngrade: compatibility.isDowngrade,
681
+ isMajorBump: compatibility.isMajorBump,
682
+ requiresMigration: compatibility.requiresMigration,
683
+ message: `Update available: v${installedVersion} → v${latestVersion}${compatibility.requiresMigration ? ' (major upgrade — migration may be required)' : ''}.`,
684
+ };
654
685
  }
655
686
  export const MaskweaverPlugin = async ({ client, directory, project, worktree, $, serverUrl }) => {
656
- const cacheCheck = checkAndInvalidateCache();
657
- if (cacheCheck.invalidated) {
658
- pluginLog(client, 'warn', `Stale plugin cache detected (v${cacheCheck.cachedVersion}). Cleared — v${cacheCheck.latestVersion} will install on next restart.`);
659
- pluginLog(client, 'warn', `Please restart OpenCode to activate maskweaver v${VERSION}.`);
687
+ const updateCheck = await checkForUpdates();
688
+ if (updateCheck.updateAvailable) {
689
+ if (updateCheck.isDowngrade) {
690
+ pluginLog(client, 'warn', `Downgrade detected (v${updateCheck.installedVersion} v${updateCheck.latestVersion}) — not allowed. Please use opencode --upgrade.`);
691
+ }
692
+ else {
693
+ pluginLog(client, 'warn', `Update available: maskweaver v${updateCheck.installedVersion} → v${updateCheck.latestVersion}`);
694
+ if (updateCheck.requiresMigration) {
695
+ pluginLog(client, 'warn', `Major version upgrade — configuration migration may be required.`);
696
+ }
697
+ pluginLog(client, 'info', `Run \`opencode --upgrade\` or \`npm update -g maskweaver\` to update.`);
698
+ }
660
699
  }
661
- else if (cacheCheck.cachedVersion && cacheCheck.cachedVersion !== VERSION) {
662
- pluginLog(client, 'info', `Plugin cache v${cacheCheck.cachedVersion} — current is v${VERSION}. Restart recommended.`);
700
+ else if (updateCheck.message) {
701
+ pluginLog(client, 'info', updateCheck.message);
663
702
  }
664
703
  const pluginConfig = loadPluginConfig(directory, { client, verbose: false });
665
704
  const configErrors = validateConfig(pluginConfig);
@@ -17,7 +17,7 @@ import { preparePhaseExecution, formatExecutionPlan, runAIVerification, generate
17
17
  import { archiveChange } from '../../weave/stages/archive.js';
18
18
  import { generateStatusReport, handleUserResponse } from '../../weave/stages/handoff.js';
19
19
  import { analyzeCodebase, runGraphifyAnalysis, readMapResult } from '../../weave/stages/map.js';
20
- import { interview as interviewStage } from '../../weave/stages/intake.js';
20
+ import { interview as interviewStage, listInterviewStates } from '../../weave/stages/intake.js';
21
21
  import { executeBuildLoop, generateBuildState, generateBuildId, loadBuildState } from '../../weave/stages/build.js';
22
22
  import { WeaveOrchestrator } from '../../weave/orchestrator.js';
23
23
  import { getPhaseManager } from '../../weave/phase-manager.js';
@@ -1778,8 +1778,22 @@ async function handleMap(args, basePath) {
1778
1778
  async function handleInterview(args, basePath) {
1779
1779
  const docsPath = args.docsPath || 'docs';
1780
1780
  const lines = [];
1781
+ const existingStates = listInterviewStates(basePath);
1782
+ const canResume = existingStates.some(s => s.status === 'in_progress');
1781
1783
  lines.push('## 💬 Interview');
1782
1784
  lines.push('');
1785
+ if (canResume && !args.resumeId) {
1786
+ lines.push('### 📋 Existing Interviews');
1787
+ lines.push('');
1788
+ for (const state of existingStates) {
1789
+ const statusIcon = state.status === 'in_progress' ? '🔄' : '✅';
1790
+ lines.push(`- ${statusIcon} \`${state.id}\` — ${state.rounds} rounds, **${state.status}**`);
1791
+ }
1792
+ lines.push('');
1793
+ lines.push('To resume an interview, use: `weave command=interview resumeId="<id>"`');
1794
+ lines.push('To start fresh: `weave command=interview`');
1795
+ lines.push('');
1796
+ }
1783
1797
  const mapResult = await readMapResult(basePath);
1784
1798
  if (mapResult) {
1785
1799
  lines.push(`📍 Map available: \`${mapResult.mapPath}\``);
@@ -1797,14 +1811,56 @@ async function handleInterview(args, basePath) {
1797
1811
  docsPath,
1798
1812
  basePath,
1799
1813
  mapResult,
1814
+ resumeId: args.resumeId,
1800
1815
  });
1801
- lines.push(`### Documents Analyzed`);
1816
+ lines.push('### 📄 Documents Analyzed');
1802
1817
  lines.push(`- ${interviewResult.intake.documents.length} document(s) analyzed`);
1803
1818
  lines.push(`- ${interviewResult.intake.features.length} feature(s) identified`);
1804
1819
  lines.push('');
1820
+ if (interviewResult.intake.features.length > 0) {
1821
+ lines.push('**Features:**');
1822
+ for (const feature of interviewResult.intake.features) {
1823
+ lines.push(` - ${feature}`);
1824
+ }
1825
+ lines.push('');
1826
+ }
1827
+ if (interviewResult.ambiguityScore) {
1828
+ const score = interviewResult.ambiguityScore;
1829
+ const milestoneIcon = score.milestone === 'ready' ? '✅'
1830
+ : score.milestone === 'refined' ? '🟡'
1831
+ : score.milestone === 'progress' ? '🟠'
1832
+ : '🔴';
1833
+ lines.push('### 📊 Ambiguity Score');
1834
+ lines.push('');
1835
+ lines.push(`**${milestoneIcon} ${(score.overallScore * 100).toFixed(1)}% Ambiguity [${score.milestone.toUpperCase()}]**`);
1836
+ lines.push(`_${score.milestoneDescription}_`);
1837
+ lines.push('');
1838
+ lines.push('| Component | Clarity | Weight |');
1839
+ lines.push('|-----------|---------|--------|');
1840
+ const components = [
1841
+ score.breakdown.goalClarity,
1842
+ score.breakdown.constraintClarity,
1843
+ score.breakdown.successCriteriaClarity,
1844
+ ];
1845
+ if (score.breakdown.contextClarity) {
1846
+ components.push(score.breakdown.contextClarity);
1847
+ }
1848
+ for (const comp of components) {
1849
+ const bar = '█'.repeat(Math.round(comp.clarityScore * 10)) + '░'.repeat(10 - Math.round(comp.clarityScore * 10));
1850
+ lines.push(`| ${comp.name} | ${bar} ${(comp.clarityScore * 100).toFixed(0)}% | ${(comp.weight * 100).toFixed(0)}% |`);
1851
+ }
1852
+ lines.push('');
1853
+ lines.push(`**Weakest area:** ${score.weakestArea}`);
1854
+ if (score.nextMilestone) {
1855
+ lines.push(`**Next milestone:** ${score.nextMilestone.label} (<= ${(score.nextMilestone.threshold * 100).toFixed(0)}%) — ${score.nextMilestone.description}`);
1856
+ }
1857
+ lines.push('');
1858
+ lines.push(`**Ready for Seed:** ${score.isReadyForSeed ? '✅ Yes' : '❌ No (ambiguity > 20%)'}`);
1859
+ lines.push(`**Ready for Gherkin:** ${score.readinessForGherkin ? '✅ Yes' : '❌ No (ambiguity > 30%)'}`);
1860
+ lines.push('');
1861
+ }
1805
1862
  if (interviewResult.intake.structuralChanges && interviewResult.intake.structuralChanges.length > 0) {
1806
1863
  lines.push('### ⚠️ Structural Changes Detected');
1807
- lines.push('The following structural changes were identified from codebase analysis:');
1808
1864
  lines.push('');
1809
1865
  for (const sc of interviewResult.intake.structuralChanges) {
1810
1866
  const icon = sc.breaking ? '🔴' : '🟡';
@@ -1814,13 +1870,73 @@ async function handleInterview(args, basePath) {
1814
1870
  lines.push(` - Status: ${sc.agreed ? '✅ Agreed' : '⏳ Pending approval'}`);
1815
1871
  }
1816
1872
  lines.push('');
1817
- lines.push('To proceed, agree to structural changes via `weave command=approve`.');
1873
+ }
1874
+ if (interviewResult.generatedScenarios && interviewResult.generatedScenarios.length > 0) {
1875
+ lines.push('### 🥒 Generated Gherkin Scenarios');
1818
1876
  lines.push('');
1877
+ const byFeature = new Map();
1878
+ for (const scenario of interviewResult.generatedScenarios) {
1879
+ const existing = byFeature.get(scenario.feature) || [];
1880
+ existing.push(scenario);
1881
+ byFeature.set(scenario.feature, existing);
1882
+ }
1883
+ for (const [feature, scenarios] of byFeature) {
1884
+ lines.push(`**Feature: ${feature}**`);
1885
+ lines.push('');
1886
+ for (const scenario of scenarios) {
1887
+ lines.push(` \`\`\`gherkin`);
1888
+ lines.push(` Scenario: ${scenario.scenario}`);
1889
+ for (const g of scenario.given)
1890
+ lines.push(` Given ${g}`);
1891
+ for (const w of scenario.when)
1892
+ lines.push(` When ${w}`);
1893
+ for (const t of scenario.then)
1894
+ lines.push(` Then ${t}`);
1895
+ lines.push(` \`\`\``);
1896
+ lines.push('');
1897
+ }
1898
+ }
1899
+ }
1900
+ if (interviewResult.intake.questions.length > 0) {
1901
+ lines.push('### ❓ Questions');
1902
+ lines.push('');
1903
+ lines.push('Answer the following questions to reduce ambiguity and generate better Gherkin scenarios:');
1904
+ lines.push('');
1905
+ for (const q of interviewResult.intake.questions) {
1906
+ const typeLabel = q.questionType === 'gherkin-given' ? '[GIVEN]'
1907
+ : q.questionType === 'gherkin-when' ? '[WHEN]'
1908
+ : q.questionType === 'gherkin-then' ? '[THEN]'
1909
+ : q.questionType === 'edge-case' ? '[EDGE]'
1910
+ : q.questionType === 'constraint' ? '[CONSTRAINT]'
1911
+ : '';
1912
+ lines.push(`**Q${q.id.replace('Q', '')}:** ${typeLabel ? typeLabel + ' ' : ''}${q.question}`);
1913
+ if (q.targetFeature) {
1914
+ lines.push(` → Target: _${q.targetFeature}_`);
1915
+ }
1916
+ if (q.options && q.options.length > 0) {
1917
+ lines.push(` Options: ${q.options.join(' | ')}`);
1918
+ }
1919
+ if (q.required) {
1920
+ lines.push(` ⚠️ _Required_`);
1921
+ }
1922
+ lines.push('');
1923
+ }
1924
+ }
1925
+ if (interviewResult.isMultiRound) {
1926
+ lines.push('---');
1927
+ lines.push('');
1928
+ lines.push(`**Interview Round ${(interviewResult.interviewState?.currentRound || 1)} complete.**`);
1929
+ lines.push('More questions exist to refine requirements. Answer the remaining questions above and run `weave command=interview` again.');
1930
+ lines.push(`Interview ID: \`${interviewResult.interviewState?.interviewId}\``);
1931
+ lines.push('');
1932
+ }
1933
+ else if (interviewResult.satisfied) {
1934
+ lines.push('---');
1935
+ lines.push('');
1936
+ lines.push('✅ **Requirements are clear. Ready to generate specification and plan.**');
1937
+ lines.push('');
1938
+ lines.push('Next: `weave command=design docsPath="docs/"` to create the plan with full Gherkin acceptance criteria.');
1819
1939
  }
1820
- lines.push('### Questions');
1821
- lines.push('Review the features and technical requirements above.');
1822
- lines.push('');
1823
- lines.push('Next: `weave command=design docsPath="docs/"` to create the plan.');
1824
1940
  }
1825
1941
  catch (e) {
1826
1942
  return `Error during interview: ${e.message || e}`;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.9.9";
1
+ export declare const VERSION = "0.10.1";
2
2
  export declare function getVersionString(): string;
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = '0.9.9';
1
+ export const VERSION = '0.10.1';
2
2
  export function getVersionString() {
3
3
  return `Maskweaver v${VERSION}`;
4
4
  }
@@ -0,0 +1,5 @@
1
+ import type { GherkinScenario } from '../types.js';
2
+ import type { AmbiguityScore, IntakeResult } from './intake-types.js';
3
+ export declare const AMBIGUITY_THRESHOLD = 0.2;
4
+ export declare const GHERKIN_READINESS_THRESHOLD = 0.3;
5
+ export declare function scoreAmbiguity(features: string[], techReqs: IntakeResult['technicalRequirements'], answers: Record<string, string>, gherkinScenarios?: GherkinScenario[], isBrownfield?: boolean, codebaseMapAvailable?: boolean): AmbiguityScore;