@syntesseraai/opencode-feature-factory 0.2.22 → 0.2.24

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,54 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ export interface AgentContext {
3
+ /** Unique UUID for this agent instance */
4
+ id: string;
5
+ /** Agent type (planning, building, reviewing, etc.) */
6
+ agent: string;
7
+ /** Task title */
8
+ title: string;
9
+ /** Task description */
10
+ description: string;
11
+ /** Working directory/folder */
12
+ folder: string;
13
+ /** Current status */
14
+ status: 'in-progress' | 'completed' | 'delegated' | 'failed';
15
+ /** ISO timestamp when agent started */
16
+ started: string;
17
+ /** OpenCode session ID */
18
+ session: string;
19
+ /** Parent agent UUID (if delegated) */
20
+ parent?: string;
21
+ /** List of child agent UUIDs (if this agent delegated work) */
22
+ delegated_to?: string[];
23
+ /** Additional notes */
24
+ notes?: string;
25
+ }
26
+ /**
27
+ * Write an agent context file
28
+ * File naming: {agent}-{uuid}.md
29
+ */
30
+ export declare function writeAgentContext(input: PluginInput, context: AgentContext): Promise<string>;
31
+ /**
32
+ * Read an agent context file by UUID
33
+ */
34
+ export declare function readAgentContextById(input: PluginInput, id: string): Promise<AgentContext | null>;
35
+ /**
36
+ * Update agent status in context file
37
+ */
38
+ export declare function updateAgentStatus(input: PluginInput, id: string, status: AgentContext['status']): Promise<boolean>;
39
+ /**
40
+ * List all active agents
41
+ */
42
+ export declare function listActiveAgents(input: PluginInput, sessionId?: string, agentType?: string): Promise<AgentContext[]>;
43
+ /**
44
+ * Find agent files by various criteria
45
+ */
46
+ export declare function findAgentFiles(input: PluginInput, agentType?: string, sessionId?: string): Promise<string[]>;
47
+ /**
48
+ * Find agent file by UUID
49
+ */
50
+ export declare function findAgentFilesById(input: PluginInput, id: string): Promise<string[]>;
51
+ /**
52
+ * Find all agent files
53
+ */
54
+ export declare function findAllAgentFiles(input: PluginInput): Promise<string[]>;
@@ -0,0 +1,273 @@
1
+ import { isValidUUID } from './uuid.js';
2
+ /**
3
+ * Generate the content for an agent context file
4
+ */
5
+ function generateContextFileContent(context) {
6
+ const frontmatter = `---
7
+ id: "${context.id}"
8
+ agent: ${context.agent}
9
+ title: "${context.title}"
10
+ description: "${context.description}"
11
+ folder: "${context.folder}"
12
+ status: ${context.status}
13
+ started: "${context.started}"
14
+ session: "${context.session}"
15
+ ${context.parent ? `parent: "${context.parent}"` : 'parent: null'}
16
+ ${context.delegated_to && context.delegated_to.length > 0 ? `delegated_to:\n${context.delegated_to.map((id) => ` - "${id}"`).join('\n')}` : 'delegated_to: []'}
17
+ ---`;
18
+ const body = `
19
+
20
+ ## Task Context
21
+
22
+ ${context.notes || 'No additional notes.'}
23
+
24
+ ## Progress
25
+
26
+ - [ ] Task started
27
+
28
+ ## Delegated Work
29
+
30
+ ${context.delegated_to && context.delegated_to.length > 0 ? context.delegated_to.map((id) => `- Agent ${id} (pending)`).join('\n') : 'No delegated work.'}
31
+ `;
32
+ return frontmatter + body;
33
+ }
34
+ /**
35
+ * Write an agent context file
36
+ * File naming: {agent}-{uuid}.md
37
+ */
38
+ export async function writeAgentContext(input, context) {
39
+ const { directory, $ } = input;
40
+ const fileName = `${context.agent}-${context.id}.md`;
41
+ const filePath = `${directory}/.feature-factory/agents/${fileName}`;
42
+ const content = generateContextFileContent(context);
43
+ try {
44
+ // Use echo to write file (Bun shell)
45
+ await $ `echo ${content} > ${filePath}`.quiet();
46
+ return filePath;
47
+ }
48
+ catch (error) {
49
+ throw new Error(`Failed to write agent context file: ${error}`);
50
+ }
51
+ }
52
+ /**
53
+ * Read an agent context file by UUID
54
+ */
55
+ export async function readAgentContextById(input, id) {
56
+ const { directory, $ } = input;
57
+ if (!isValidUUID(id)) {
58
+ return null;
59
+ }
60
+ try {
61
+ // Find file with this UUID
62
+ const result = await $ `ls ${directory}/.feature-factory/agents/ | grep "-${id}.md"`.quiet();
63
+ const fileName = result.text().trim();
64
+ if (!fileName) {
65
+ return null;
66
+ }
67
+ const filePath = `${directory}/.feature-factory/agents/${fileName}`;
68
+ const content = await $ `cat ${filePath}`.quiet();
69
+ return parseAgentContext(content.text());
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /**
76
+ * Parse agent context from markdown content
77
+ */
78
+ function parseAgentContext(content) {
79
+ try {
80
+ // Extract frontmatter
81
+ const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/);
82
+ if (!frontmatterMatch) {
83
+ return null;
84
+ }
85
+ const frontmatter = frontmatterMatch[1];
86
+ const lines = frontmatter.split('\n');
87
+ const context = {};
88
+ for (const line of lines) {
89
+ const match = line.match(/^([a-z_]+):\s*(.*)$/);
90
+ if (match) {
91
+ const [, key, value] = match;
92
+ const cleanValue = value.replace(/^["']|["']$/g, ''); // Remove quotes
93
+ if (key === 'delegated_to') {
94
+ // Handle array - this is simplified, real YAML parsing would be better
95
+ continue;
96
+ }
97
+ else if (key === 'parent' && cleanValue === 'null') {
98
+ context[key] = undefined;
99
+ }
100
+ else {
101
+ context[key] = cleanValue;
102
+ }
103
+ }
104
+ }
105
+ // Parse delegated_to array manually from content
106
+ const delegatedMatch = content.match(/delegated_to:\n((?: {2}- ".*"\n?)*)/);
107
+ if (delegatedMatch) {
108
+ const delegatedLines = delegatedMatch[1].trim().split('\n');
109
+ context.delegated_to = delegatedLines
110
+ .map((line) => line.match(/- "([^"]+)"/)?.[1])
111
+ .filter((id) => !!id);
112
+ }
113
+ return context;
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Update agent status in context file
121
+ */
122
+ export async function updateAgentStatus(input, id, status) {
123
+ const { directory, $ } = input;
124
+ try {
125
+ const result = await $ `ls ${directory}/.feature-factory/agents/ | grep "-${id}.md"`.quiet();
126
+ const fileName = result.text().trim();
127
+ if (!fileName) {
128
+ return false;
129
+ }
130
+ const filePath = `${directory}/.feature-factory/agents/${fileName}`;
131
+ // Read current content
132
+ const content = await $ `cat ${filePath}`.quiet();
133
+ let text = content.text();
134
+ // Replace status line
135
+ text = text.replace(/status: \w+/, `status: ${status}`);
136
+ // Write back
137
+ await $ `echo ${text} > ${filePath}`.quiet();
138
+ return true;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ /**
145
+ * List all active agents
146
+ */
147
+ export async function listActiveAgents(input, sessionId, agentType) {
148
+ const { directory, $ } = input;
149
+ const agentsDir = `${directory}/.feature-factory/agents`;
150
+ try {
151
+ // Check if directory exists
152
+ await $ `test -d ${agentsDir}`.quiet();
153
+ }
154
+ catch {
155
+ return [];
156
+ }
157
+ try {
158
+ const result = await $ `ls ${agentsDir}/*.md 2>/dev/null || echo ""`.quiet();
159
+ const files = result
160
+ .text()
161
+ .trim()
162
+ .split('\n')
163
+ .filter((f) => f.endsWith('.md'));
164
+ const agents = [];
165
+ for (const filePath of files) {
166
+ try {
167
+ const content = await $ `cat ${filePath}`.quiet();
168
+ const context = parseAgentContext(content.text());
169
+ if (context) {
170
+ // Apply filters
171
+ if (sessionId && context.session !== sessionId) {
172
+ continue;
173
+ }
174
+ if (agentType && context.agent !== agentType) {
175
+ continue;
176
+ }
177
+ agents.push(context);
178
+ }
179
+ }
180
+ catch {
181
+ // Skip files that can't be read
182
+ continue;
183
+ }
184
+ }
185
+ return agents;
186
+ }
187
+ catch {
188
+ return [];
189
+ }
190
+ }
191
+ /**
192
+ * Find agent files by various criteria
193
+ */
194
+ export async function findAgentFiles(input, agentType, sessionId) {
195
+ const { directory, $ } = input;
196
+ const agentsDir = `${directory}/.feature-factory/agents`;
197
+ try {
198
+ await $ `test -d ${agentsDir}`.quiet();
199
+ }
200
+ catch {
201
+ return [];
202
+ }
203
+ try {
204
+ let pattern = '*.md';
205
+ if (agentType) {
206
+ pattern = `${agentType}-*.md`;
207
+ }
208
+ const result = await $ `ls ${agentsDir}/${pattern} 2>/dev/null || echo ""`.quiet();
209
+ const files = result
210
+ .text()
211
+ .trim()
212
+ .split('\n')
213
+ .filter((f) => f && f.endsWith('.md'));
214
+ if (sessionId) {
215
+ // Filter by session ID (need to read files)
216
+ const filteredFiles = [];
217
+ for (const file of files) {
218
+ try {
219
+ const content = await $ `cat ${file}`.quiet();
220
+ if (content.text().includes(`session: "${sessionId}"`)) {
221
+ filteredFiles.push(file);
222
+ }
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ }
228
+ return filteredFiles;
229
+ }
230
+ return files;
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
236
+ /**
237
+ * Find agent file by UUID
238
+ */
239
+ export async function findAgentFilesById(input, id) {
240
+ const { directory, $ } = input;
241
+ if (!isValidUUID(id)) {
242
+ return [];
243
+ }
244
+ try {
245
+ const result = await $ `ls ${directory}/.feature-factory/agents/*-${id}.md 2>/dev/null || echo ""`.quiet();
246
+ return result
247
+ .text()
248
+ .trim()
249
+ .split('\n')
250
+ .filter((f) => f && f.endsWith('.md'));
251
+ }
252
+ catch {
253
+ return [];
254
+ }
255
+ }
256
+ /**
257
+ * Find all agent files
258
+ */
259
+ export async function findAllAgentFiles(input) {
260
+ const { directory, $ } = input;
261
+ const agentsDir = `${directory}/.feature-factory/agents`;
262
+ try {
263
+ const result = await $ `ls ${agentsDir}/*.md 2>/dev/null || echo ""`.quiet();
264
+ return result
265
+ .text()
266
+ .trim()
267
+ .split('\n')
268
+ .filter((f) => f && f.endsWith('.md'));
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ }
@@ -0,0 +1,18 @@
1
+ import type { CommandStep, QualityGateConfig } from './types.js';
2
+ type BunShell = any;
3
+ /**
4
+ * Resolve the command plan based on configuration and discovery.
5
+ *
6
+ * Resolution order:
7
+ * 1. Configured commands (qualityGate.lint/build/test) - highest priority
8
+ * 2. management/ci.sh (Feature Factory convention) - only if no configured commands
9
+ * 3. Conventional discovery (Node/Rust/Go/Python) - only if no ci.sh
10
+ *
11
+ * @returns Array of command steps to execute, or empty if nothing found
12
+ */
13
+ export declare function resolveCommands(args: {
14
+ $: BunShell;
15
+ directory: string;
16
+ config: QualityGateConfig;
17
+ }): Promise<CommandStep[]>;
18
+ export {};
@@ -0,0 +1,189 @@
1
+ import { DEFAULT_QUALITY_GATE, fileExists, hasConfiguredCommands, readJsonFile, } from './quality-gate-config.js';
2
+ // ============================================================================
3
+ // Package Manager Detection
4
+ // ============================================================================
5
+ /**
6
+ * Detect the package manager based on lockfile presence.
7
+ * Priority: pnpm > bun > yarn > npm
8
+ */
9
+ async function detectPackageManager($, directory, override) {
10
+ if (override && override !== 'auto') {
11
+ return override;
12
+ }
13
+ // Priority order: pnpm > bun > yarn > npm
14
+ if (await fileExists($, `${directory}/pnpm-lock.yaml`))
15
+ return 'pnpm';
16
+ if (await fileExists($, `${directory}/bun.lockb`))
17
+ return 'bun';
18
+ if (await fileExists($, `${directory}/bun.lock`))
19
+ return 'bun';
20
+ if (await fileExists($, `${directory}/yarn.lock`))
21
+ return 'yarn';
22
+ if (await fileExists($, `${directory}/package-lock.json`))
23
+ return 'npm';
24
+ return 'npm'; // fallback
25
+ }
26
+ /**
27
+ * Build the run command for a given package manager and script name
28
+ */
29
+ function buildRunCommand(pm, script) {
30
+ switch (pm) {
31
+ case 'pnpm':
32
+ return `pnpm -s run ${script}`;
33
+ case 'bun':
34
+ return `bun run ${script}`;
35
+ case 'yarn':
36
+ return `yarn -s ${script}`;
37
+ case 'npm':
38
+ return `npm run -s ${script}`;
39
+ }
40
+ }
41
+ // ============================================================================
42
+ // Node.js Discovery
43
+ // ============================================================================
44
+ /**
45
+ * Discover Node.js lint/build/test commands from package.json scripts
46
+ */
47
+ async function discoverNodeCommands($, directory, pm) {
48
+ const pkgJson = await readJsonFile($, `${directory}/package.json`);
49
+ if (!pkgJson?.scripts)
50
+ return [];
51
+ const scripts = pkgJson.scripts;
52
+ const steps = [];
53
+ // Lint: prefer lint:ci, then lint
54
+ const lintScript = scripts['lint:ci'] ? 'lint:ci' : scripts['lint'] ? 'lint' : null;
55
+ if (lintScript) {
56
+ steps.push({ step: 'lint', cmd: buildRunCommand(pm, lintScript) });
57
+ }
58
+ // Build: prefer build:ci, then build
59
+ const buildScript = scripts['build:ci'] ? 'build:ci' : scripts['build'] ? 'build' : null;
60
+ if (buildScript) {
61
+ steps.push({ step: 'build', cmd: buildRunCommand(pm, buildScript) });
62
+ }
63
+ // Test: prefer test:ci, then test
64
+ const testScript = scripts['test:ci'] ? 'test:ci' : scripts['test'] ? 'test' : null;
65
+ if (testScript) {
66
+ steps.push({ step: 'test', cmd: buildRunCommand(pm, testScript) });
67
+ }
68
+ return steps;
69
+ }
70
+ // ============================================================================
71
+ // Rust Discovery
72
+ // ============================================================================
73
+ /**
74
+ * Discover Rust commands from Cargo.toml presence
75
+ */
76
+ async function discoverRustCommands($, directory, includeClippy) {
77
+ if (!(await fileExists($, `${directory}/Cargo.toml`)))
78
+ return [];
79
+ const steps = [{ step: 'lint (fmt)', cmd: 'cargo fmt --check' }];
80
+ if (includeClippy) {
81
+ steps.push({
82
+ step: 'lint (clippy)',
83
+ cmd: 'cargo clippy --all-targets --all-features',
84
+ });
85
+ }
86
+ steps.push({ step: 'build', cmd: 'cargo build' });
87
+ steps.push({ step: 'test', cmd: 'cargo test' });
88
+ return steps;
89
+ }
90
+ // ============================================================================
91
+ // Go Discovery
92
+ // ============================================================================
93
+ /**
94
+ * Discover Go commands from go.mod presence
95
+ */
96
+ async function discoverGoCommands($, directory) {
97
+ if (!(await fileExists($, `${directory}/go.mod`)))
98
+ return [];
99
+ return [{ step: 'test', cmd: 'go test ./...' }];
100
+ }
101
+ // ============================================================================
102
+ // Python Discovery
103
+ // ============================================================================
104
+ /**
105
+ * Discover Python test commands with strong signal detection
106
+ */
107
+ async function discoverPythonCommands($, directory) {
108
+ // Only add pytest if we have strong signal
109
+ const hasPytestIni = await fileExists($, `${directory}/pytest.ini`);
110
+ const hasPyproject = await fileExists($, `${directory}/pyproject.toml`);
111
+ if (!hasPytestIni && !hasPyproject)
112
+ return [];
113
+ // pytest.ini is strong signal
114
+ if (hasPytestIni) {
115
+ return [{ step: 'test', cmd: 'pytest' }];
116
+ }
117
+ // Check if pyproject.toml mentions pytest
118
+ if (hasPyproject) {
119
+ try {
120
+ const result = await $ `cat ${directory}/pyproject.toml`.quiet();
121
+ const content = result.text();
122
+ if (content.includes('pytest')) {
123
+ return [{ step: 'test', cmd: 'pytest' }];
124
+ }
125
+ }
126
+ catch {
127
+ // Ignore read errors - return empty array if file can't be read
128
+ }
129
+ }
130
+ return [];
131
+ }
132
+ // ============================================================================
133
+ // Main Resolution Logic
134
+ // ============================================================================
135
+ /**
136
+ * Resolve the command plan based on configuration and discovery.
137
+ *
138
+ * Resolution order:
139
+ * 1. Configured commands (qualityGate.lint/build/test) - highest priority
140
+ * 2. management/ci.sh (Feature Factory convention) - only if no configured commands
141
+ * 3. Conventional discovery (Node/Rust/Go/Python) - only if no ci.sh
142
+ *
143
+ * @returns Array of command steps to execute, or empty if nothing found
144
+ */
145
+ export async function resolveCommands(args) {
146
+ const { $, directory, config } = args;
147
+ const mergedConfig = { ...DEFAULT_QUALITY_GATE, ...config };
148
+ // 1. Configured commands take priority (do NOT run ci.sh if these exist)
149
+ if (hasConfiguredCommands(config)) {
150
+ const steps = [];
151
+ const order = mergedConfig.steps;
152
+ for (const stepName of order) {
153
+ const cmd = config[stepName];
154
+ if (cmd) {
155
+ steps.push({ step: stepName, cmd });
156
+ }
157
+ }
158
+ return steps;
159
+ }
160
+ // 2. Feature Factory CI script (only if no configured commands)
161
+ if (mergedConfig.useCiSh !== 'never') {
162
+ const ciShPath = `${directory}/management/ci.sh`;
163
+ if (await fileExists($, ciShPath)) {
164
+ return [{ step: 'ci', cmd: `bash ${ciShPath}` }];
165
+ }
166
+ }
167
+ // 3. Conventional discovery (only if no ci.sh)
168
+ const pm = await detectPackageManager($, directory, mergedConfig.packageManager);
169
+ // Try Node first (most common)
170
+ if (await fileExists($, `${directory}/package.json`)) {
171
+ const nodeSteps = await discoverNodeCommands($, directory, pm);
172
+ if (nodeSteps.length > 0)
173
+ return nodeSteps;
174
+ }
175
+ // Rust
176
+ const rustSteps = await discoverRustCommands($, directory, mergedConfig.include?.rustClippy ?? true);
177
+ if (rustSteps.length > 0)
178
+ return rustSteps;
179
+ // Go
180
+ const goSteps = await discoverGoCommands($, directory);
181
+ if (goSteps.length > 0)
182
+ return goSteps;
183
+ // Python
184
+ const pythonSteps = await discoverPythonCommands($, directory);
185
+ if (pythonSteps.length > 0)
186
+ return pythonSteps;
187
+ // No commands discovered
188
+ return [];
189
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Unit tests for discovery module
3
+ *
4
+ * Tests focus on the pure function:
5
+ * - buildRunCommand: builds package manager-specific run commands
6
+ *
7
+ * Note: Most functions in discovery.ts require shell access ($) so they're
8
+ * integration-tested elsewhere. This file tests the pure utility functions.
9
+ */
10
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Unit tests for discovery module
3
+ *
4
+ * Tests focus on the pure function:
5
+ * - buildRunCommand: builds package manager-specific run commands
6
+ *
7
+ * Note: Most functions in discovery.ts require shell access ($) so they're
8
+ * integration-tested elsewhere. This file tests the pure utility functions.
9
+ */
10
+ // Re-implement buildRunCommand for testing since it's not exported
11
+ function buildRunCommand(pm, script) {
12
+ switch (pm) {
13
+ case 'pnpm':
14
+ return `pnpm -s run ${script}`;
15
+ case 'bun':
16
+ return `bun run ${script}`;
17
+ case 'yarn':
18
+ return `yarn -s ${script}`;
19
+ case 'npm':
20
+ return `npm run -s ${script}`;
21
+ default:
22
+ return `npm run -s ${script}`;
23
+ }
24
+ }
25
+ describe('buildRunCommand', () => {
26
+ describe('pnpm', () => {
27
+ it('should build correct pnpm command', () => {
28
+ expect(buildRunCommand('pnpm', 'lint')).toBe('pnpm -s run lint');
29
+ });
30
+ it('should build correct pnpm command for build script', () => {
31
+ expect(buildRunCommand('pnpm', 'build')).toBe('pnpm -s run build');
32
+ });
33
+ it('should build correct pnpm command for test script', () => {
34
+ expect(buildRunCommand('pnpm', 'test')).toBe('pnpm -s run test');
35
+ });
36
+ it('should handle scripts with colons', () => {
37
+ expect(buildRunCommand('pnpm', 'lint:ci')).toBe('pnpm -s run lint:ci');
38
+ });
39
+ });
40
+ describe('bun', () => {
41
+ it('should build correct bun command', () => {
42
+ expect(buildRunCommand('bun', 'lint')).toBe('bun run lint');
43
+ });
44
+ it('should build correct bun command for build script', () => {
45
+ expect(buildRunCommand('bun', 'build')).toBe('bun run build');
46
+ });
47
+ it('should build correct bun command for test script', () => {
48
+ expect(buildRunCommand('bun', 'test')).toBe('bun run test');
49
+ });
50
+ });
51
+ describe('yarn', () => {
52
+ it('should build correct yarn command', () => {
53
+ expect(buildRunCommand('yarn', 'lint')).toBe('yarn -s lint');
54
+ });
55
+ it('should build correct yarn command for build script', () => {
56
+ expect(buildRunCommand('yarn', 'build')).toBe('yarn -s build');
57
+ });
58
+ it('should build correct yarn command for test script', () => {
59
+ expect(buildRunCommand('yarn', 'test')).toBe('yarn -s test');
60
+ });
61
+ });
62
+ describe('npm', () => {
63
+ it('should build correct npm command', () => {
64
+ expect(buildRunCommand('npm', 'lint')).toBe('npm run -s lint');
65
+ });
66
+ it('should build correct npm command for build script', () => {
67
+ expect(buildRunCommand('npm', 'build')).toBe('npm run -s build');
68
+ });
69
+ it('should build correct npm command for test script', () => {
70
+ expect(buildRunCommand('npm', 'test')).toBe('npm run -s test');
71
+ });
72
+ });
73
+ describe('edge cases', () => {
74
+ it('should handle script names with hyphens', () => {
75
+ expect(buildRunCommand('npm', 'type-check')).toBe('npm run -s type-check');
76
+ });
77
+ it('should handle script names with underscores', () => {
78
+ expect(buildRunCommand('pnpm', 'lint_fix')).toBe('pnpm -s run lint_fix');
79
+ });
80
+ it('should handle long script names', () => {
81
+ expect(buildRunCommand('yarn', 'test:unit:coverage')).toBe('yarn -s test:unit:coverage');
82
+ });
83
+ });
84
+ });
85
+ /**
86
+ * PackageManager type validation tests
87
+ * These ensure the type constraints are working correctly
88
+ */
89
+ describe('PackageManager type', () => {
90
+ it('should accept valid package manager values', () => {
91
+ const validManagers = ['pnpm', 'bun', 'yarn', 'npm'];
92
+ validManagers.forEach((pm) => {
93
+ expect(['pnpm', 'bun', 'yarn', 'npm']).toContain(pm);
94
+ });
95
+ });
96
+ });
97
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ type BunShell = any;
3
+ /**
4
+ * Initialize the Feature Factory directory structure.
5
+ * Creates .feature-factory/ and .feature-factory/agents/ directories.
6
+ * Migrates management/ci.sh to .feature-factory/ci.sh if it exists.
7
+ */
8
+ export declare function initializeFeatureFactory(input: PluginInput, $: BunShell): Promise<void>;
9
+ export {};