@syntesseraai/opencode-feature-factory 0.2.11 → 0.2.13

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,4 +1,4 @@
1
- import type { CommandStep, QualityGateConfig } from './types';
1
+ import type { CommandStep, QualityGateConfig } from './types.js';
2
2
  type BunShell = any;
3
3
  /**
4
4
  * Resolve the command plan based on configuration and discovery.
package/dist/discovery.js CHANGED
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveCommands = resolveCommands;
4
- const quality_gate_config_1 = require("./quality-gate-config");
1
+ import { DEFAULT_QUALITY_GATE, fileExists, hasConfiguredCommands, readJsonFile, } from './quality-gate-config.js';
5
2
  // ============================================================================
6
3
  // Package Manager Detection
7
4
  // ============================================================================
@@ -14,15 +11,15 @@ async function detectPackageManager($, directory, override) {
14
11
  return override;
15
12
  }
16
13
  // Priority order: pnpm > bun > yarn > npm
17
- if (await (0, quality_gate_config_1.fileExists)($, `${directory}/pnpm-lock.yaml`))
14
+ if (await fileExists($, `${directory}/pnpm-lock.yaml`))
18
15
  return 'pnpm';
19
- if (await (0, quality_gate_config_1.fileExists)($, `${directory}/bun.lockb`))
16
+ if (await fileExists($, `${directory}/bun.lockb`))
20
17
  return 'bun';
21
- if (await (0, quality_gate_config_1.fileExists)($, `${directory}/bun.lock`))
18
+ if (await fileExists($, `${directory}/bun.lock`))
22
19
  return 'bun';
23
- if (await (0, quality_gate_config_1.fileExists)($, `${directory}/yarn.lock`))
20
+ if (await fileExists($, `${directory}/yarn.lock`))
24
21
  return 'yarn';
25
- if (await (0, quality_gate_config_1.fileExists)($, `${directory}/package-lock.json`))
22
+ if (await fileExists($, `${directory}/package-lock.json`))
26
23
  return 'npm';
27
24
  return 'npm'; // fallback
28
25
  }
@@ -48,7 +45,7 @@ function buildRunCommand(pm, script) {
48
45
  * Discover Node.js lint/build/test commands from package.json scripts
49
46
  */
50
47
  async function discoverNodeCommands($, directory, pm) {
51
- const pkgJson = await (0, quality_gate_config_1.readJsonFile)($, `${directory}/package.json`);
48
+ const pkgJson = await readJsonFile($, `${directory}/package.json`);
52
49
  if (!pkgJson?.scripts)
53
50
  return [];
54
51
  const scripts = pkgJson.scripts;
@@ -77,7 +74,7 @@ async function discoverNodeCommands($, directory, pm) {
77
74
  * Discover Rust commands from Cargo.toml presence
78
75
  */
79
76
  async function discoverRustCommands($, directory, includeClippy) {
80
- if (!(await (0, quality_gate_config_1.fileExists)($, `${directory}/Cargo.toml`)))
77
+ if (!(await fileExists($, `${directory}/Cargo.toml`)))
81
78
  return [];
82
79
  const steps = [{ step: 'lint (fmt)', cmd: 'cargo fmt --check' }];
83
80
  if (includeClippy) {
@@ -97,7 +94,7 @@ async function discoverRustCommands($, directory, includeClippy) {
97
94
  * Discover Go commands from go.mod presence
98
95
  */
99
96
  async function discoverGoCommands($, directory) {
100
- if (!(await (0, quality_gate_config_1.fileExists)($, `${directory}/go.mod`)))
97
+ if (!(await fileExists($, `${directory}/go.mod`)))
101
98
  return [];
102
99
  return [{ step: 'test', cmd: 'go test ./...' }];
103
100
  }
@@ -109,8 +106,8 @@ async function discoverGoCommands($, directory) {
109
106
  */
110
107
  async function discoverPythonCommands($, directory) {
111
108
  // Only add pytest if we have strong signal
112
- const hasPytestIni = await (0, quality_gate_config_1.fileExists)($, `${directory}/pytest.ini`);
113
- const hasPyproject = await (0, quality_gate_config_1.fileExists)($, `${directory}/pyproject.toml`);
109
+ const hasPytestIni = await fileExists($, `${directory}/pytest.ini`);
110
+ const hasPyproject = await fileExists($, `${directory}/pyproject.toml`);
114
111
  if (!hasPytestIni && !hasPyproject)
115
112
  return [];
116
113
  // pytest.ini is strong signal
@@ -145,11 +142,11 @@ async function discoverPythonCommands($, directory) {
145
142
  *
146
143
  * @returns Array of command steps to execute, or empty if nothing found
147
144
  */
148
- async function resolveCommands(args) {
145
+ export async function resolveCommands(args) {
149
146
  const { $, directory, config } = args;
150
- const mergedConfig = { ...quality_gate_config_1.DEFAULT_QUALITY_GATE, ...config };
147
+ const mergedConfig = { ...DEFAULT_QUALITY_GATE, ...config };
151
148
  // 1. Configured commands take priority (do NOT run ci.sh if these exist)
152
- if ((0, quality_gate_config_1.hasConfiguredCommands)(config)) {
149
+ if (hasConfiguredCommands(config)) {
153
150
  const steps = [];
154
151
  const order = mergedConfig.steps;
155
152
  for (const stepName of order) {
@@ -163,14 +160,14 @@ async function resolveCommands(args) {
163
160
  // 2. Feature Factory CI script (only if no configured commands)
164
161
  if (mergedConfig.useCiSh !== 'never') {
165
162
  const ciShPath = `${directory}/management/ci.sh`;
166
- if (await (0, quality_gate_config_1.fileExists)($, ciShPath)) {
163
+ if (await fileExists($, ciShPath)) {
167
164
  return [{ step: 'ci', cmd: `bash ${ciShPath}` }];
168
165
  }
169
166
  }
170
167
  // 3. Conventional discovery (only if no ci.sh)
171
168
  const pm = await detectPackageManager($, directory, mergedConfig.packageManager);
172
169
  // Try Node first (most common)
173
- if (await (0, quality_gate_config_1.fileExists)($, `${directory}/package.json`)) {
170
+ if (await fileExists($, `${directory}/package.json`)) {
174
171
  const nodeSteps = await discoverNodeCommands($, directory, pm);
175
172
  if (nodeSteps.length > 0)
176
173
  return nodeSteps;
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * Unit tests for discovery module
4
3
  *
@@ -8,7 +7,6 @@
8
7
  * Note: Most functions in discovery.ts require shell access ($) so they're
9
8
  * integration-tested elsewhere. This file tests the pure utility functions.
10
9
  */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
10
  // Re-implement buildRunCommand for testing since it's not exported
13
11
  function buildRunCommand(pm, script) {
14
12
  switch (pm) {
@@ -94,3 +92,4 @@ describe('PackageManager type', () => {
94
92
  });
95
93
  });
96
94
  });
95
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,19 +1,4 @@
1
- import type { Plugin } from '@opencode-ai/plugin' with {
2
- 'resolution-mode': 'import'
3
- };
4
- export declare const SKILL_PATHS: {
5
- 'ff-mini-plan': string;
6
- 'ff-todo-management': string;
7
- 'ff-severity-classification': string;
8
- 'ff-report-templates': string;
9
- };
10
- export declare const AGENT_PATHS: {
11
- 'ff-acceptance': string;
12
- 'ff-review': string;
13
- 'ff-security': string;
14
- 'ff-well-architected': string;
15
- 'ff-validate': string;
16
- };
1
+ import type { Plugin } from '@opencode-ai/plugin';
17
2
  /**
18
3
  * Stop Quality Gate Plugin
19
4
  *
@@ -29,4 +14,4 @@ export declare const AGENT_PATHS: {
29
14
  */
30
15
  export declare const StopQualityGatePlugin: Plugin;
31
16
  export default StopQualityGatePlugin;
32
- export type { QualityGateConfig, CommandStep, StepResult, SessionState, PackageManager, } from './types';
17
+ export type { QualityGateConfig, CommandStep, StepResult, SessionState, PackageManager, } from './types.js';
package/dist/index.js CHANGED
@@ -1,21 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StopQualityGatePlugin = exports.AGENT_PATHS = exports.SKILL_PATHS = void 0;
4
- const stop_quality_gate_1 = require("./stop-quality-gate");
5
- // Export skill and agent paths for programmatic access
6
- exports.SKILL_PATHS = {
7
- 'ff-mini-plan': './skills/ff-mini-plan/SKILL.md',
8
- 'ff-todo-management': './skills/ff-todo-management/SKILL.md',
9
- 'ff-severity-classification': './skills/ff-severity-classification/SKILL.md',
10
- 'ff-report-templates': './skills/ff-report-templates/SKILL.md',
11
- };
12
- exports.AGENT_PATHS = {
13
- 'ff-acceptance': './agents/ff-acceptance.md',
14
- 'ff-review': './agents/ff-review.md',
15
- 'ff-security': './agents/ff-security.md',
16
- 'ff-well-architected': './agents/ff-well-architected.md',
17
- 'ff-validate': './agents/ff-validate.md',
18
- };
1
+ import { createQualityGateHooks } from './stop-quality-gate.js';
19
2
  const SERVICE_NAME = 'feature-factory';
20
3
  /**
21
4
  * Log a message using the OpenCode client's structured logging.
@@ -59,7 +42,7 @@ function resolveRootDir(input) {
59
42
  * - On failure: passes full CI output to LLM for fix instructions
60
43
  * - If management/ci.sh does not exist, quality gate does not run
61
44
  */
62
- const StopQualityGatePlugin = async (input) => {
45
+ export const StopQualityGatePlugin = async (input) => {
63
46
  const { worktree, directory, client } = input;
64
47
  const rootDir = resolveRootDir({ worktree, directory });
65
48
  // Skip quality gate if no valid directory (e.g., global config with no project)
@@ -69,7 +52,7 @@ const StopQualityGatePlugin = async (input) => {
69
52
  // Create quality gate hooks
70
53
  let qualityGateHooks = {};
71
54
  try {
72
- qualityGateHooks = await (0, stop_quality_gate_1.createQualityGateHooks)(input);
55
+ qualityGateHooks = await createQualityGateHooks(input);
73
56
  }
74
57
  catch (error) {
75
58
  await log(client, 'error', 'quality-gate.init-error', {
@@ -80,6 +63,5 @@ const StopQualityGatePlugin = async (input) => {
80
63
  ...qualityGateHooks,
81
64
  };
82
65
  };
83
- exports.StopQualityGatePlugin = StopQualityGatePlugin;
84
66
  // Default export for OpenCode plugin discovery
85
- exports.default = exports.StopQualityGatePlugin;
67
+ export default StopQualityGatePlugin;
package/dist/output.js CHANGED
@@ -1,7 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractErrorLines = extractErrorLines;
4
- exports.tailLines = tailLines;
5
1
  /**
6
2
  * Regex pattern for detecting error-like lines in command output
7
3
  */
@@ -14,7 +10,7 @@ const ERROR_PATTERNS = /(error|failed|failure|panic|assert|exception|traceback|T
14
10
  * @param maxLines - Maximum number of error lines to extract
15
11
  * @returns Array of error-like lines (up to maxLines)
16
12
  */
17
- function extractErrorLines(output, maxLines) {
13
+ export function extractErrorLines(output, maxLines) {
18
14
  const lines = output.split('\n');
19
15
  const errorLines = [];
20
16
  for (let i = 0; i < lines.length && errorLines.length < maxLines; i++) {
@@ -41,7 +37,7 @@ function extractErrorLines(output, maxLines) {
41
37
  * @param maxLines - Maximum number of lines to return
42
38
  * @returns Truncated output with notice, or full output if short enough
43
39
  */
44
- function tailLines(output, maxLines) {
40
+ export function tailLines(output, maxLines) {
45
41
  const lines = output.split('\n');
46
42
  if (lines.length <= maxLines) {
47
43
  return output;
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * Unit tests for output module
4
3
  *
@@ -6,11 +5,10 @@
6
5
  * - extractErrorLines: extracts error-like lines from command output
7
6
  * - tailLines: returns last N lines with truncation notice
8
7
  */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- const output_1 = require("./output");
8
+ import { extractErrorLines, tailLines } from './output.js';
11
9
  describe('extractErrorLines', () => {
12
10
  it('should return empty array for empty input', () => {
13
- expect((0, output_1.extractErrorLines)('', 10)).toEqual([]);
11
+ expect(extractErrorLines('', 10)).toEqual([]);
14
12
  });
15
13
  it('should return empty array when no errors found', () => {
16
14
  const output = `
@@ -18,7 +16,7 @@ Build started
18
16
  Compiling modules...
19
17
  Build completed successfully
20
18
  `;
21
- expect((0, output_1.extractErrorLines)(output, 10)).toEqual([]);
19
+ expect(extractErrorLines(output, 10)).toEqual([]);
22
20
  });
23
21
  it('should extract lines containing "error"', () => {
24
22
  const output = `
@@ -26,7 +24,7 @@ Starting build
26
24
  error: Cannot find module 'missing'
27
25
  Build failed
28
26
  `;
29
- const result = (0, output_1.extractErrorLines)(output, 10);
27
+ const result = extractErrorLines(output, 10);
30
28
  expect(result.some((line) => line.includes('error:'))).toBe(true);
31
29
  });
32
30
  it('should extract lines containing "Error" (case insensitive)', () => {
@@ -35,7 +33,7 @@ Running tests
35
33
  TypeError: undefined is not a function
36
34
  at test.js:10:5
37
35
  `;
38
- const result = (0, output_1.extractErrorLines)(output, 10);
36
+ const result = extractErrorLines(output, 10);
39
37
  expect(result.some((line) => line.includes('TypeError'))).toBe(true);
40
38
  });
41
39
  it('should extract lines containing "failed"', () => {
@@ -44,7 +42,7 @@ Test suite: MyTests
44
42
  Test failed: should work correctly
45
43
  1 test failed
46
44
  `;
47
- const result = (0, output_1.extractErrorLines)(output, 10);
45
+ const result = extractErrorLines(output, 10);
48
46
  expect(result.some((line) => line.toLowerCase().includes('failed'))).toBe(true);
49
47
  });
50
48
  it('should extract lines containing "FAILED"', () => {
@@ -52,7 +50,7 @@ Test failed: should work correctly
52
50
  Running: test_function
53
51
  FAILED test_function - assertion error
54
52
  `;
55
- const result = (0, output_1.extractErrorLines)(output, 10);
53
+ const result = extractErrorLines(output, 10);
56
54
  expect(result.some((line) => line.includes('FAILED'))).toBe(true);
57
55
  });
58
56
  it('should extract lines containing "panic"', () => {
@@ -60,7 +58,7 @@ FAILED test_function - assertion error
60
58
  thread 'main' panicked at 'assertion failed'
61
59
  note: run with RUST_BACKTRACE=1
62
60
  `;
63
- const result = (0, output_1.extractErrorLines)(output, 10);
61
+ const result = extractErrorLines(output, 10);
64
62
  expect(result.some((line) => line.includes('panic'))).toBe(true);
65
63
  });
66
64
  it('should extract lines containing "exception"', () => {
@@ -68,7 +66,7 @@ note: run with RUST_BACKTRACE=1
68
66
  Exception in thread "main" java.lang.NullPointerException
69
67
  at Main.main(Main.java:5)
70
68
  `;
71
- const result = (0, output_1.extractErrorLines)(output, 10);
69
+ const result = extractErrorLines(output, 10);
72
70
  expect(result.some((line) => line.includes('Exception'))).toBe(true);
73
71
  });
74
72
  it('should extract lines containing "traceback"', () => {
@@ -77,7 +75,7 @@ Traceback (most recent call last):
77
75
  File "test.py", line 10
78
76
  NameError: name 'x' is not defined
79
77
  `;
80
- const result = (0, output_1.extractErrorLines)(output, 10);
78
+ const result = extractErrorLines(output, 10);
81
79
  expect(result.some((line) => line.includes('Traceback'))).toBe(true);
82
80
  });
83
81
  it('should include 2 lines of context after error', () => {
@@ -86,7 +84,7 @@ error: something went wrong
86
84
  context line 1
87
85
  context line 2
88
86
  line5`;
89
- const result = (0, output_1.extractErrorLines)(output, 10);
87
+ const result = extractErrorLines(output, 10);
90
88
  expect(result).toContain('error: something went wrong');
91
89
  expect(result).toContain('context line 1');
92
90
  expect(result).toContain('context line 2');
@@ -103,36 +101,36 @@ error: third error
103
101
  context5
104
102
  context6
105
103
  `;
106
- const result = (0, output_1.extractErrorLines)(output, 5);
104
+ const result = extractErrorLines(output, 5);
107
105
  expect(result.length).toBeLessThanOrEqual(5);
108
106
  });
109
107
  it('should handle error at end of output without enough context lines', () => {
110
108
  const output = `line1
111
109
  line2
112
110
  error: final error`;
113
- const result = (0, output_1.extractErrorLines)(output, 10);
111
+ const result = extractErrorLines(output, 10);
114
112
  expect(result).toContain('error: final error');
115
113
  });
116
114
  it('should extract ReferenceError', () => {
117
115
  const output = `ReferenceError: x is not defined`;
118
- const result = (0, output_1.extractErrorLines)(output, 10);
116
+ const result = extractErrorLines(output, 10);
119
117
  expect(result).toContain('ReferenceError: x is not defined');
120
118
  });
121
119
  it('should extract SyntaxError', () => {
122
120
  const output = `SyntaxError: Unexpected token '}'`;
123
- const result = (0, output_1.extractErrorLines)(output, 10);
121
+ const result = extractErrorLines(output, 10);
124
122
  expect(result).toContain("SyntaxError: Unexpected token '}'");
125
123
  });
126
124
  it('should extract ERR! (npm style errors)', () => {
127
125
  const output = `npm ERR! code ENOENT
128
126
  npm ERR! path /app/package.json`;
129
- const result = (0, output_1.extractErrorLines)(output, 10);
127
+ const result = extractErrorLines(output, 10);
130
128
  expect(result.some((line) => line.includes('ERR!'))).toBe(true);
131
129
  });
132
130
  it('should extract FAIL (test runner style)', () => {
133
131
  const output = `FAIL src/test.ts
134
132
  Test suite failed to run`;
135
- const result = (0, output_1.extractErrorLines)(output, 10);
133
+ const result = extractErrorLines(output, 10);
136
134
  expect(result.some((line) => line.includes('FAIL'))).toBe(true);
137
135
  });
138
136
  it('should extract multiple error types in same output', () => {
@@ -143,7 +141,7 @@ TypeError: Cannot read property 'x'
143
141
  FAILED: test_something
144
142
  Build process ended
145
143
  `;
146
- const result = (0, output_1.extractErrorLines)(output, 20);
144
+ const result = extractErrorLines(output, 20);
147
145
  expect(result.some((line) => line.includes('error:'))).toBe(true);
148
146
  expect(result.some((line) => line.includes('TypeError'))).toBe(true);
149
147
  expect(result.some((line) => line.includes('FAILED'))).toBe(true);
@@ -152,15 +150,15 @@ Build process ended
152
150
  describe('tailLines', () => {
153
151
  it('should return full output when lines count is less than maxLines', () => {
154
152
  const output = 'line1\nline2\nline3';
155
- expect((0, output_1.tailLines)(output, 10)).toBe(output);
153
+ expect(tailLines(output, 10)).toBe(output);
156
154
  });
157
155
  it('should return full output when lines count equals maxLines', () => {
158
156
  const output = 'line1\nline2\nline3';
159
- expect((0, output_1.tailLines)(output, 3)).toBe(output);
157
+ expect(tailLines(output, 3)).toBe(output);
160
158
  });
161
159
  it('should truncate and add notice when output exceeds maxLines', () => {
162
160
  const output = 'line1\nline2\nline3\nline4\nline5';
163
- const result = (0, output_1.tailLines)(output, 3);
161
+ const result = tailLines(output, 3);
164
162
  expect(result).toContain('... (2 lines truncated)');
165
163
  expect(result).toContain('line3');
166
164
  expect(result).toContain('line4');
@@ -170,38 +168,38 @@ describe('tailLines', () => {
170
168
  });
171
169
  it('should handle single line output', () => {
172
170
  const output = 'single line';
173
- expect((0, output_1.tailLines)(output, 5)).toBe('single line');
171
+ expect(tailLines(output, 5)).toBe('single line');
174
172
  });
175
173
  it('should handle empty output', () => {
176
- expect((0, output_1.tailLines)('', 5)).toBe('');
174
+ expect(tailLines('', 5)).toBe('');
177
175
  });
178
176
  it('should handle maxLines of 1', () => {
179
177
  const output = 'line1\nline2\nline3';
180
- const result = (0, output_1.tailLines)(output, 1);
178
+ const result = tailLines(output, 1);
181
179
  expect(result).toContain('... (2 lines truncated)');
182
180
  expect(result).toContain('line3');
183
181
  });
184
182
  it('should correctly count truncated lines', () => {
185
183
  const lines = Array.from({ length: 100 }, (_, i) => `line${i + 1}`);
186
184
  const output = lines.join('\n');
187
- const result = (0, output_1.tailLines)(output, 10);
185
+ const result = tailLines(output, 10);
188
186
  expect(result).toContain('... (90 lines truncated)');
189
187
  });
190
188
  it('should preserve line content in tail', () => {
191
189
  const output = 'first\nsecond\nthird\nfourth\nfifth';
192
- const result = (0, output_1.tailLines)(output, 2);
190
+ const result = tailLines(output, 2);
193
191
  const resultLines = result.split('\n');
194
192
  expect(resultLines[resultLines.length - 1]).toBe('fifth');
195
193
  expect(resultLines[resultLines.length - 2]).toBe('fourth');
196
194
  });
197
195
  it('should handle output with empty lines', () => {
198
196
  const output = 'line1\n\nline3\n\nline5';
199
- const result = (0, output_1.tailLines)(output, 3);
197
+ const result = tailLines(output, 3);
200
198
  expect(result).toContain('... (2 lines truncated)');
201
199
  });
202
200
  it('should handle output with only newlines', () => {
203
201
  const output = '\n\n\n\n';
204
- const result = (0, output_1.tailLines)(output, 2);
202
+ const result = tailLines(output, 2);
205
203
  expect(result).toContain('... (3 lines truncated)');
206
204
  });
207
205
  });
@@ -1,4 +1,4 @@
1
- import type { QualityGateConfig } from './types';
1
+ import type { QualityGateConfig } from './types.js';
2
2
  /**
3
3
  * Default configuration values for StopQualityGate
4
4
  */
@@ -1,15 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_QUALITY_GATE = void 0;
4
- exports.readJsonFile = readJsonFile;
5
- exports.fileExists = fileExists;
6
- exports.mergeQualityGateConfig = mergeQualityGateConfig;
7
- exports.loadQualityGateConfig = loadQualityGateConfig;
8
- exports.hasConfiguredCommands = hasConfiguredCommands;
9
1
  /**
10
2
  * Default configuration values for StopQualityGate
11
3
  */
12
- exports.DEFAULT_QUALITY_GATE = {
4
+ export const DEFAULT_QUALITY_GATE = {
13
5
  steps: ['lint', 'build', 'test'],
14
6
  useCiSh: 'auto',
15
7
  packageManager: 'auto',
@@ -23,7 +15,7 @@ exports.DEFAULT_QUALITY_GATE = {
23
15
  /**
24
16
  * Read a JSON file safely, returning null if not found or invalid
25
17
  */
26
- async function readJsonFile($, path) {
18
+ export async function readJsonFile($, path) {
27
19
  try {
28
20
  const result = await $ `cat ${path}`.quiet();
29
21
  return JSON.parse(result.text());
@@ -35,7 +27,7 @@ async function readJsonFile($, path) {
35
27
  /**
36
28
  * Check if a file exists
37
29
  */
38
- async function fileExists($, path) {
30
+ export async function fileExists($, path) {
39
31
  try {
40
32
  await $ `test -f ${path}`.quiet();
41
33
  return true;
@@ -50,7 +42,7 @@ async function fileExists($, path) {
50
42
  * Precedence: `.opencode/opencode.json` overrides `opencode.json` (root).
51
43
  * Deep-merges nested objects like `include`.
52
44
  */
53
- function mergeQualityGateConfig(root, dotOpencode) {
45
+ export function mergeQualityGateConfig(root, dotOpencode) {
54
46
  const base = root ?? {};
55
47
  const override = dotOpencode ?? {};
56
48
  return {
@@ -72,7 +64,7 @@ function mergeQualityGateConfig(root, dotOpencode) {
72
64
  *
73
65
  * Merges ONLY the `qualityGate` key. `.opencode/opencode.json` takes precedence.
74
66
  */
75
- async function loadQualityGateConfig($, directory) {
67
+ export async function loadQualityGateConfig($, directory) {
76
68
  const rootConfigPath = `${directory}/opencode.json`;
77
69
  const dotOpencodeConfigPath = `${directory}/.opencode/opencode.json`;
78
70
  const [rootJson, dotOpencodeJson] = await Promise.all([
@@ -87,6 +79,6 @@ async function loadQualityGateConfig($, directory) {
87
79
  * Check if any explicit commands are configured (lint/build/test).
88
80
  * If so, these take priority over ci.sh and discovery.
89
81
  */
90
- function hasConfiguredCommands(config) {
82
+ export function hasConfiguredCommands(config) {
91
83
  return !!(config.lint || config.build || config.test);
92
84
  }
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * Unit tests for quality-gate-config module
4
3
  *
@@ -7,22 +6,21 @@
7
6
  * - hasConfiguredCommands: checks if explicit commands are configured
8
7
  * - DEFAULT_QUALITY_GATE: verify default values
9
8
  */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- const quality_gate_config_1 = require("./quality-gate-config");
9
+ import { mergeQualityGateConfig, hasConfiguredCommands, DEFAULT_QUALITY_GATE, } from './quality-gate-config.js';
12
10
  describe('DEFAULT_QUALITY_GATE', () => {
13
11
  it('should have correct default values', () => {
14
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.steps).toEqual(['lint', 'build', 'test']);
15
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.useCiSh).toBe('auto');
16
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.packageManager).toBe('auto');
17
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.cacheSeconds).toBe(30);
18
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.maxOutputLines).toBe(160);
19
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.maxErrorLines).toBe(60);
20
- expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.include.rustClippy).toBe(true);
12
+ expect(DEFAULT_QUALITY_GATE.steps).toEqual(['lint', 'build', 'test']);
13
+ expect(DEFAULT_QUALITY_GATE.useCiSh).toBe('auto');
14
+ expect(DEFAULT_QUALITY_GATE.packageManager).toBe('auto');
15
+ expect(DEFAULT_QUALITY_GATE.cacheSeconds).toBe(30);
16
+ expect(DEFAULT_QUALITY_GATE.maxOutputLines).toBe(160);
17
+ expect(DEFAULT_QUALITY_GATE.maxErrorLines).toBe(60);
18
+ expect(DEFAULT_QUALITY_GATE.include.rustClippy).toBe(true);
21
19
  });
22
20
  });
23
21
  describe('mergeQualityGateConfig', () => {
24
22
  it('should return empty config when both inputs are undefined', () => {
25
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(undefined, undefined);
23
+ const result = mergeQualityGateConfig(undefined, undefined);
26
24
  expect(result).toEqual({ include: {} });
27
25
  });
28
26
  it('should return root config when dotOpencode is undefined', () => {
@@ -30,7 +28,7 @@ describe('mergeQualityGateConfig', () => {
30
28
  lint: 'pnpm lint',
31
29
  cacheSeconds: 60,
32
30
  };
33
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, undefined);
31
+ const result = mergeQualityGateConfig(root, undefined);
34
32
  expect(result).toEqual({ lint: 'pnpm lint', cacheSeconds: 60, include: {} });
35
33
  });
36
34
  it('should return dotOpencode config when root is undefined', () => {
@@ -38,7 +36,7 @@ describe('mergeQualityGateConfig', () => {
38
36
  build: 'npm run build',
39
37
  maxOutputLines: 200,
40
38
  };
41
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(undefined, dotOpencode);
39
+ const result = mergeQualityGateConfig(undefined, dotOpencode);
42
40
  expect(result).toEqual({ build: 'npm run build', maxOutputLines: 200, include: {} });
43
41
  });
44
42
  it('should override root values with dotOpencode values', () => {
@@ -51,7 +49,7 @@ describe('mergeQualityGateConfig', () => {
51
49
  lint: 'npm run lint:strict',
52
50
  cacheSeconds: 60,
53
51
  };
54
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
52
+ const result = mergeQualityGateConfig(root, dotOpencode);
55
53
  expect(result.lint).toBe('npm run lint:strict');
56
54
  expect(result.build).toBe('pnpm build');
57
55
  expect(result.cacheSeconds).toBe(60);
@@ -67,7 +65,7 @@ describe('mergeQualityGateConfig', () => {
67
65
  rustClippy: true,
68
66
  },
69
67
  };
70
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
68
+ const result = mergeQualityGateConfig(root, dotOpencode);
71
69
  expect(result.include?.rustClippy).toBe(true);
72
70
  });
73
71
  it('should preserve root include values not overridden by dotOpencode', () => {
@@ -80,7 +78,7 @@ describe('mergeQualityGateConfig', () => {
80
78
  lint: 'custom lint',
81
79
  // No include specified
82
80
  };
83
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
81
+ const result = mergeQualityGateConfig(root, dotOpencode);
84
82
  expect(result.include?.rustClippy).toBe(true);
85
83
  expect(result.lint).toBe('custom lint');
86
84
  });
@@ -104,7 +102,7 @@ describe('mergeQualityGateConfig', () => {
104
102
  useCiSh: 'never',
105
103
  include: { rustClippy: true },
106
104
  };
107
- const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
105
+ const result = mergeQualityGateConfig(root, dotOpencode);
108
106
  expect(result.lint).toBe('lint-override');
109
107
  expect(result.build).toBe('build-root');
110
108
  expect(result.test).toBe('test-root');
@@ -120,7 +118,7 @@ describe('mergeQualityGateConfig', () => {
120
118
  });
121
119
  describe('hasConfiguredCommands', () => {
122
120
  it('should return false for empty config', () => {
123
- expect((0, quality_gate_config_1.hasConfiguredCommands)({})).toBe(false);
121
+ expect(hasConfiguredCommands({})).toBe(false);
124
122
  });
125
123
  it('should return false when only non-command properties are set', () => {
126
124
  const config = {
@@ -129,25 +127,25 @@ describe('hasConfiguredCommands', () => {
129
127
  useCiSh: 'always',
130
128
  packageManager: 'pnpm',
131
129
  };
132
- expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(false);
130
+ expect(hasConfiguredCommands(config)).toBe(false);
133
131
  });
134
132
  it('should return true when lint is configured', () => {
135
133
  const config = {
136
134
  lint: 'pnpm lint',
137
135
  };
138
- expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
136
+ expect(hasConfiguredCommands(config)).toBe(true);
139
137
  });
140
138
  it('should return true when build is configured', () => {
141
139
  const config = {
142
140
  build: 'pnpm build',
143
141
  };
144
- expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
142
+ expect(hasConfiguredCommands(config)).toBe(true);
145
143
  });
146
144
  it('should return true when test is configured', () => {
147
145
  const config = {
148
146
  test: 'pnpm test',
149
147
  };
150
- expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
148
+ expect(hasConfiguredCommands(config)).toBe(true);
151
149
  });
152
150
  it('should return true when multiple commands are configured', () => {
153
151
  const config = {
@@ -155,12 +153,12 @@ describe('hasConfiguredCommands', () => {
155
153
  build: 'pnpm build',
156
154
  test: 'pnpm test',
157
155
  };
158
- expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
156
+ expect(hasConfiguredCommands(config)).toBe(true);
159
157
  });
160
158
  it('should return false for empty string commands', () => {
161
159
  const config = {
162
160
  lint: '',
163
161
  };
164
- expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(false);
162
+ expect(hasConfiguredCommands(config)).toBe(false);
165
163
  });
166
164
  });
@@ -1,4 +1,4 @@
1
- import type { PluginInput, Hooks } from '@opencode-ai/plugin' with { 'resolution-mode': 'import' };
1
+ import type { PluginInput, Hooks } from '@opencode-ai/plugin';
2
2
  export declare const SECRET_PATTERNS: Array<{
3
3
  pattern: RegExp;
4
4
  replacement: string;
@@ -1,15 +1,9 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SECRET_PATTERNS = void 0;
4
- exports.sanitizeOutput = sanitizeOutput;
5
- exports.truncateOutput = truncateOutput;
6
- exports.createQualityGateHooks = createQualityGateHooks;
7
1
  const SERVICE_NAME = 'feature-factory';
8
2
  const IDLE_DEBOUNCE_MS = 500;
9
3
  const CI_TIMEOUT_MS = 300000; // 5 minutes
10
4
  const SESSION_TTL_MS = 3600000; // 1 hour
11
5
  const CLEANUP_INTERVAL_MS = 600000; // 10 minutes
12
- exports.SECRET_PATTERNS = [
6
+ export const SECRET_PATTERNS = [
13
7
  // AWS Access Key IDs
14
8
  { pattern: /AKIA[0-9A-Z]{16}/g, replacement: '[REDACTED_AWS_KEY]' },
15
9
  // GitHub Personal Access Tokens (classic)
@@ -73,9 +67,9 @@ exports.SECRET_PATTERNS = [
73
67
  * Sanitizes CI output by redacting common secret patterns before sending to the LLM.
74
68
  * This helps prevent accidental exposure of sensitive information in prompts.
75
69
  */
76
- function sanitizeOutput(output) {
70
+ export function sanitizeOutput(output) {
77
71
  let sanitized = output;
78
- for (const { pattern, replacement } of exports.SECRET_PATTERNS) {
72
+ for (const { pattern, replacement } of SECRET_PATTERNS) {
79
73
  sanitized = sanitized.replace(pattern, replacement);
80
74
  }
81
75
  return sanitized;
@@ -84,7 +78,7 @@ function sanitizeOutput(output) {
84
78
  * Truncates CI output to the last N lines to reduce prompt size and focus on relevant errors.
85
79
  * Adds a header indicating truncation if the output was longer than the limit.
86
80
  */
87
- function truncateOutput(output, maxLines = 20) {
81
+ export function truncateOutput(output, maxLines = 20) {
88
82
  const lines = output.split('\n');
89
83
  if (lines.length <= maxLines) {
90
84
  return output;
@@ -160,7 +154,7 @@ async function log(client, level, message, extra) {
160
154
  return undefined;
161
155
  }
162
156
  }
163
- async function createQualityGateHooks(input) {
157
+ export async function createQualityGateHooks(input) {
164
158
  const { client, $, directory } = input;
165
159
  async function ciShExists() {
166
160
  try {
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * Unit tests for stop-quality-gate module
4
3
  *
@@ -6,8 +5,7 @@
6
5
  * - sanitizeOutput: redacts secrets from CI output
7
6
  * - isSessionReadOnly: determines if session has write permissions
8
7
  */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- const stop_quality_gate_1 = require("./stop-quality-gate");
8
+ import { sanitizeOutput, truncateOutput } from './stop-quality-gate.js';
11
9
  function isSessionReadOnly(permission) {
12
10
  if (!permission)
13
11
  return false;
@@ -17,178 +15,178 @@ describe('sanitizeOutput', () => {
17
15
  describe('AWS credentials', () => {
18
16
  it('should redact AWS Access Key IDs', () => {
19
17
  const input = 'Found credentials: AKIAIOSFODNN7EXAMPLE in config';
20
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
18
+ const result = sanitizeOutput(input);
21
19
  expect(result).toBe('Found credentials: [REDACTED_AWS_KEY] in config');
22
20
  });
23
21
  it('should redact multiple AWS keys', () => {
24
22
  const input = 'Key1: AKIAIOSFODNN7EXAMPLE Key2: AKIAI44QH8DHBEXAMPLE';
25
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
23
+ const result = sanitizeOutput(input);
26
24
  expect(result).toBe('Key1: [REDACTED_AWS_KEY] Key2: [REDACTED_AWS_KEY]');
27
25
  });
28
26
  it('should not redact partial AWS key patterns', () => {
29
27
  const input = 'AKIA123'; // Too short
30
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
28
+ const result = sanitizeOutput(input);
31
29
  expect(result).toBe('AKIA123');
32
30
  });
33
31
  });
34
32
  describe('GitHub tokens', () => {
35
33
  it('should redact GitHub Personal Access Tokens (classic)', () => {
36
34
  const input = 'Using ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
37
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
35
+ const result = sanitizeOutput(input);
38
36
  expect(result).toBe('Using [REDACTED_GH_TOKEN]');
39
37
  });
40
38
  it('should redact GitHub Personal Access Tokens (fine-grained)', () => {
41
39
  const input = 'Using github_pat_11ABCDEFG_abcdefghijklmnopqrstuvwxyz';
42
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
40
+ const result = sanitizeOutput(input);
43
41
  expect(result).toBe('Using [REDACTED_GH_TOKEN]');
44
42
  });
45
43
  it('should redact GitHub OAuth tokens', () => {
46
44
  const input = 'oauth=gho_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
47
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
45
+ const result = sanitizeOutput(input);
48
46
  expect(result).toBe('oauth=[REDACTED_GH_TOKEN]');
49
47
  });
50
48
  it('should redact GitHub App user-to-server tokens', () => {
51
49
  const input = 'Using ghu_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
52
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
50
+ const result = sanitizeOutput(input);
53
51
  expect(result).toBe('Using [REDACTED_GH_TOKEN]');
54
52
  });
55
53
  it('should redact GitHub App server-to-server tokens', () => {
56
54
  const input = 'Using ghs_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
57
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
55
+ const result = sanitizeOutput(input);
58
56
  expect(result).toBe('Using [REDACTED_GH_TOKEN]');
59
57
  });
60
58
  it('should not redact partial GitHub token patterns', () => {
61
59
  const input = 'ghp_abc'; // Too short
62
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
60
+ const result = sanitizeOutput(input);
63
61
  expect(result).toBe('ghp_abc');
64
62
  });
65
63
  });
66
64
  describe('GitLab tokens', () => {
67
65
  it('should redact GitLab Personal Access Tokens', () => {
68
66
  const input = 'Using glpat-abcdefghij1234567890';
69
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
67
+ const result = sanitizeOutput(input);
70
68
  expect(result).toBe('Using [REDACTED_GITLAB_TOKEN]');
71
69
  });
72
70
  it('should redact GitLab tokens with hyphens', () => {
73
71
  const input = 'Using glpat-abc-def-ghi-jkl-mnop-qrs';
74
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
72
+ const result = sanitizeOutput(input);
75
73
  expect(result).toBe('Using [REDACTED_GITLAB_TOKEN]');
76
74
  });
77
75
  it('should not redact partial GitLab token patterns', () => {
78
76
  const input = 'glpat-short'; // Too short
79
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
77
+ const result = sanitizeOutput(input);
80
78
  expect(result).toBe('glpat-short');
81
79
  });
82
80
  });
83
81
  describe('npm tokens', () => {
84
82
  it('should redact npm tokens', () => {
85
83
  const input = 'Using npm_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
86
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
84
+ const result = sanitizeOutput(input);
87
85
  expect(result).toBe('Using [REDACTED_NPM_TOKEN]');
88
86
  });
89
87
  it('should not redact partial npm token patterns', () => {
90
88
  const input = 'npm_abc'; // Too short
91
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
89
+ const result = sanitizeOutput(input);
92
90
  expect(result).toBe('npm_abc');
93
91
  });
94
92
  });
95
93
  describe('Bearer tokens', () => {
96
94
  it('should redact Bearer tokens', () => {
97
95
  const input = 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
98
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
96
+ const result = sanitizeOutput(input);
99
97
  expect(result).toBe('Authorization: Bearer [REDACTED]');
100
98
  });
101
99
  it('should handle case-insensitive Bearer', () => {
102
100
  const input = 'bearer abc123-token.value';
103
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
101
+ const result = sanitizeOutput(input);
104
102
  expect(result).toBe('Bearer [REDACTED]');
105
103
  });
106
104
  });
107
105
  describe('API keys', () => {
108
106
  it('should redact api_key assignments', () => {
109
107
  const input = 'api_key=sk-1234567890abcdef';
110
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
108
+ const result = sanitizeOutput(input);
111
109
  expect(result).toBe('api_key=[REDACTED]');
112
110
  });
113
111
  it('should redact api-key with hyphen', () => {
114
112
  const input = 'api-key: my-secret-key';
115
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
113
+ const result = sanitizeOutput(input);
116
114
  expect(result).toBe('api_key=[REDACTED]');
117
115
  });
118
116
  it('should redact apikey without separator', () => {
119
117
  const input = 'apikey=abc123';
120
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
118
+ const result = sanitizeOutput(input);
121
119
  expect(result).toBe('api_key=[REDACTED]');
122
120
  });
123
121
  it('should redact quoted api keys', () => {
124
122
  const input = "api_key='secret-value'";
125
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
123
+ const result = sanitizeOutput(input);
126
124
  expect(result).toBe('api_key=[REDACTED]');
127
125
  });
128
126
  });
129
127
  describe('tokens', () => {
130
128
  it('should redact token assignments with 8+ char values', () => {
131
129
  const input = 'token=ghp_xxxxxxxxxxxxxxxxxxxx';
132
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
130
+ const result = sanitizeOutput(input);
133
131
  expect(result).toBe('token=[REDACTED]');
134
132
  });
135
133
  it('should redact tokens plural with 8+ char values', () => {
136
134
  const input = 'tokens: secret12345';
137
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
135
+ const result = sanitizeOutput(input);
138
136
  expect(result).toBe('token=[REDACTED]');
139
137
  });
140
138
  it('should NOT redact token with short values (less than 8 chars)', () => {
141
139
  const input = 'token=abc123';
142
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
140
+ const result = sanitizeOutput(input);
143
141
  expect(result).toBe('token=abc123');
144
142
  });
145
143
  it('should NOT redact phrases like "token count: 5" (value too short)', () => {
146
144
  const input = 'token count: 5';
147
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
145
+ const result = sanitizeOutput(input);
148
146
  expect(result).toBe('token count: 5');
149
147
  });
150
148
  it('should NOT redact "token: abc" (value too short)', () => {
151
149
  const input = 'token: abc';
152
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
150
+ const result = sanitizeOutput(input);
153
151
  expect(result).toBe('token: abc');
154
152
  });
155
153
  it('should redact actual token values that are 8+ chars', () => {
156
154
  const input = 'token=abcd1234efgh';
157
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
155
+ const result = sanitizeOutput(input);
158
156
  expect(result).toBe('token=[REDACTED]');
159
157
  });
160
158
  it('should redact quoted tokens with 8+ char values', () => {
161
159
  const input = 'token="my-secret-token-value"';
162
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
160
+ const result = sanitizeOutput(input);
163
161
  expect(result).toBe('token=[REDACTED]');
164
162
  });
165
163
  });
166
164
  describe('passwords', () => {
167
165
  it('should redact password assignments', () => {
168
166
  const input = 'password=super-secret-123!';
169
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
167
+ const result = sanitizeOutput(input);
170
168
  expect(result).toBe('password=[REDACTED]');
171
169
  });
172
170
  it('should redact passwords plural', () => {
173
171
  const input = 'passwords: "admin123"';
174
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
172
+ const result = sanitizeOutput(input);
175
173
  expect(result).toBe('password=[REDACTED]');
176
174
  });
177
175
  it('should handle special characters in passwords', () => {
178
176
  const input = 'password=P@$$w0rd!#%';
179
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
177
+ const result = sanitizeOutput(input);
180
178
  expect(result).toBe('password=[REDACTED]');
181
179
  });
182
180
  });
183
181
  describe('generic secrets', () => {
184
182
  it('should redact secret assignments', () => {
185
183
  const input = 'secret=my-app-secret-key';
186
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
184
+ const result = sanitizeOutput(input);
187
185
  expect(result).toBe('secret=[REDACTED]');
188
186
  });
189
187
  it('should redact secrets plural', () => {
190
188
  const input = 'secrets: "confidential-data"';
191
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
189
+ const result = sanitizeOutput(input);
192
190
  expect(result).toBe('secret=[REDACTED]');
193
191
  });
194
192
  });
@@ -196,40 +194,40 @@ describe('sanitizeOutput', () => {
196
194
  it('should redact long base64-like strings', () => {
197
195
  const base64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODk=';
198
196
  const input = `Encoded value: ${base64}`;
199
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
197
+ const result = sanitizeOutput(input);
200
198
  expect(result).toBe('Encoded value: [REDACTED_BASE64]');
201
199
  });
202
200
  it('should not redact short base64 strings', () => {
203
201
  const input = 'Short: abc123';
204
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
202
+ const result = sanitizeOutput(input);
205
203
  expect(result).toBe('Short: abc123');
206
204
  });
207
205
  it('should redact base64 without padding', () => {
208
206
  const base64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw';
209
- const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
207
+ const result = sanitizeOutput(base64);
210
208
  expect(result).toBe('[REDACTED_BASE64]');
211
209
  });
212
210
  it('should redact base64 strings up to 500 chars (ReDoS prevention)', () => {
213
211
  // Generate a 500-char base64 string
214
212
  const base64 = 'A'.repeat(500);
215
- const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
213
+ const result = sanitizeOutput(base64);
216
214
  expect(result).toBe('[REDACTED_BASE64]');
217
215
  });
218
216
  it('should handle base64 strings over 500 chars by matching only first 500 (ReDoS prevention)', () => {
219
217
  // Generate a 501-char base64 string - only first 500 chars match
220
218
  const base64 = 'A'.repeat(501);
221
- const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
219
+ const result = sanitizeOutput(base64);
222
220
  // Pattern matches the first 500 chars, leaving 1 char behind
223
221
  expect(result).toBe('[REDACTED_BASE64]A');
224
222
  });
225
223
  it('should handle base64 at exactly 40 chars (minimum threshold)', () => {
226
224
  const base64 = 'A'.repeat(40);
227
- const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
225
+ const result = sanitizeOutput(base64);
228
226
  expect(result).toBe('[REDACTED_BASE64]');
229
227
  });
230
228
  it('should NOT redact base64 strings under 40 chars', () => {
231
229
  const base64 = 'A'.repeat(39);
232
- const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
230
+ const result = sanitizeOutput(base64);
233
231
  expect(result).toBe(base64);
234
232
  });
235
233
  });
@@ -240,7 +238,7 @@ describe('sanitizeOutput', () => {
240
238
  Auth: Bearer my-jwt-token
241
239
  password=admin123
242
240
  `;
243
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
241
+ const result = sanitizeOutput(input);
244
242
  expect(result).toContain('[REDACTED_AWS_KEY]');
245
243
  expect(result).toContain('Bearer [REDACTED]');
246
244
  expect(result).toContain('password=[REDACTED]');
@@ -254,28 +252,28 @@ describe('sanitizeOutput', () => {
254
252
  const input = `-----BEGIN RSA PRIVATE KEY-----
255
253
  MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
256
254
  -----END RSA PRIVATE KEY-----`;
257
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
255
+ const result = sanitizeOutput(input);
258
256
  expect(result).toBe('[REDACTED_PRIVATE_KEY]');
259
257
  });
260
258
  it('should redact OpenSSH private keys', () => {
261
259
  const input = `-----BEGIN OPENSSH PRIVATE KEY-----
262
260
  b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAA
263
261
  -----END OPENSSH PRIVATE KEY-----`;
264
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
262
+ const result = sanitizeOutput(input);
265
263
  expect(result).toBe('[REDACTED_PRIVATE_KEY]');
266
264
  });
267
265
  it('should redact EC private keys', () => {
268
266
  const input = `-----BEGIN EC PRIVATE KEY-----
269
267
  MHQCAQEEICg7E4NN6YPWoU6/FXa5ON6Pt6LKBfA8WL
270
268
  -----END EC PRIVATE KEY-----`;
271
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
269
+ const result = sanitizeOutput(input);
272
270
  expect(result).toBe('[REDACTED_PRIVATE_KEY]');
273
271
  });
274
272
  it('should redact generic private keys', () => {
275
273
  const input = `-----BEGIN PRIVATE KEY-----
276
274
  MIIEvgIBADANBgkqhkiG9w0BAQEFAASC
277
275
  -----END PRIVATE KEY-----`;
278
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
276
+ const result = sanitizeOutput(input);
279
277
  expect(result).toBe('[REDACTED_PRIVATE_KEY]');
280
278
  });
281
279
  it('should redact private keys embedded in output', () => {
@@ -284,7 +282,7 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASC
284
282
  MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
285
283
  -----END RSA PRIVATE KEY-----
286
284
  Done loading.`;
287
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
285
+ const result = sanitizeOutput(input);
288
286
  expect(result).toBe(`Loading configuration...
289
287
  [REDACTED_PRIVATE_KEY]
290
288
  Done loading.`);
@@ -293,49 +291,49 @@ Done loading.`);
293
291
  describe('GCP credentials', () => {
294
292
  it('should redact GCP API keys', () => {
295
293
  const input = 'Using GCP key: AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe';
296
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
294
+ const result = sanitizeOutput(input);
297
295
  expect(result).toBe('Using GCP key: [REDACTED_GCP_KEY]');
298
296
  });
299
297
  it('should redact multiple GCP API keys', () => {
300
298
  const input = 'Key1: AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe Key2: AIzaSyB-1234567890abcdefghijklmnopqrstu';
301
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
299
+ const result = sanitizeOutput(input);
302
300
  expect(result).toBe('Key1: [REDACTED_GCP_KEY] Key2: [REDACTED_GCP_KEY]');
303
301
  });
304
302
  it('should not redact partial GCP API key patterns', () => {
305
303
  const input = 'AIza123'; // Too short
306
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
304
+ const result = sanitizeOutput(input);
307
305
  expect(result).toBe('AIza123');
308
306
  });
309
307
  it('should redact GCP OAuth tokens', () => {
310
308
  const input = 'Authorization: ya29.a0AfH6SMBx-example-token-value_123';
311
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
309
+ const result = sanitizeOutput(input);
312
310
  expect(result).toBe('Authorization: [REDACTED_GCP_TOKEN]');
313
311
  });
314
312
  it('should redact GCP OAuth tokens with various characters', () => {
315
313
  const input = 'token=ya29.Gl-abc_XYZ-123';
316
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
314
+ const result = sanitizeOutput(input);
317
315
  expect(result).toBe('token=[REDACTED_GCP_TOKEN]');
318
316
  });
319
317
  });
320
318
  describe('Slack tokens', () => {
321
319
  it('should redact Slack bot tokens', () => {
322
320
  const input = 'SLACK_BOT_TOKEN=xoxb-123456789012-1234567890123-AbCdEfGhIjKlMnOpQrStUvWx';
323
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
321
+ const result = sanitizeOutput(input);
324
322
  expect(result).toBe('SLACK_BOT_TOKEN=[REDACTED_SLACK_TOKEN]');
325
323
  });
326
324
  it('should redact Slack user tokens', () => {
327
325
  const input = 'Using xoxp-123456789012-123456789012-123456789012-abcdef1234567890abcdef1234567890';
328
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
326
+ const result = sanitizeOutput(input);
329
327
  expect(result).toBe('Using [REDACTED_SLACK_TOKEN]');
330
328
  });
331
329
  it('should redact Slack app tokens', () => {
332
330
  const input = 'APP_TOKEN=xapp-1-A0123BCDEFG-1234567890123-abcdefghijklmnopqrstuvwxyz0123456789';
333
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
331
+ const result = sanitizeOutput(input);
334
332
  expect(result).toBe('APP_TOKEN=[REDACTED_SLACK_TOKEN]');
335
333
  });
336
334
  it('should redact Slack webhook URLs', () => {
337
335
  const input = 'Webhook: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX';
338
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
336
+ const result = sanitizeOutput(input);
339
337
  expect(result).toBe('Webhook: https://[REDACTED_SLACK_WEBHOOK]');
340
338
  });
341
339
  it('should redact multiple different Slack token types', () => {
@@ -344,7 +342,7 @@ Done loading.`);
344
342
  User: xoxp-789-012-def
345
343
  App: xapp-345-ghi
346
344
  `;
347
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
345
+ const result = sanitizeOutput(input);
348
346
  expect(result).toContain('[REDACTED_SLACK_TOKEN]');
349
347
  expect(result).not.toContain('xoxb-');
350
348
  expect(result).not.toContain('xoxp-');
@@ -355,100 +353,100 @@ Done loading.`);
355
353
  it('should redact Stripe live secret keys', () => {
356
354
  // 24 chars after sk_live_: 51ABC123DEF456GHI789JKLM
357
355
  const input = 'STRIPE_SECRET_KEY=sk_live_51ABC123DEF456GHI789JKLM';
358
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
356
+ const result = sanitizeOutput(input);
359
357
  expect(result).toBe('STRIPE_SECRET_KEY=[REDACTED_STRIPE_KEY]');
360
358
  });
361
359
  it('should redact Stripe test secret keys', () => {
362
360
  // 24 chars after sk_test_: 51ABC123DEF456GHI789JKLM
363
361
  const input = 'Using sk_test_51ABC123DEF456GHI789JKLM for testing';
364
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
362
+ const result = sanitizeOutput(input);
365
363
  expect(result).toBe('Using [REDACTED_STRIPE_KEY] for testing');
366
364
  });
367
365
  it('should redact Stripe live restricted keys', () => {
368
366
  // 24 chars after rk_live_: 51ABC123DEF456GHI789JKLM
369
367
  const input = 'RESTRICTED_KEY=rk_live_51ABC123DEF456GHI789JKLM';
370
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
368
+ const result = sanitizeOutput(input);
371
369
  expect(result).toBe('RESTRICTED_KEY=[REDACTED_STRIPE_KEY]');
372
370
  });
373
371
  it('should redact Stripe test restricted keys', () => {
374
372
  // 24 chars after rk_test_: 51ABC123DEF456GHI789JKLM
375
373
  const input = 'Using rk_test_51ABC123DEF456GHI789JKLM for testing';
376
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
374
+ const result = sanitizeOutput(input);
377
375
  expect(result).toBe('Using [REDACTED_STRIPE_KEY] for testing');
378
376
  });
379
377
  it('should not redact Stripe keys that are too short', () => {
380
378
  const input = 'sk_live_short'; // Less than 24 characters after prefix
381
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
379
+ const result = sanitizeOutput(input);
382
380
  expect(result).toBe('sk_live_short');
383
381
  });
384
382
  it('should redact multiple Stripe keys', () => {
385
383
  // 24 chars after each prefix
386
384
  const input = 'Live: sk_live_51ABC123DEF456GHI789JKLM Test: sk_test_51XYZ789ABC123DEF456GHIJ';
387
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
385
+ const result = sanitizeOutput(input);
388
386
  expect(result).toBe('Live: [REDACTED_STRIPE_KEY] Test: [REDACTED_STRIPE_KEY]');
389
387
  });
390
388
  it('should redact long Stripe keys', () => {
391
389
  const input = 'sk_live_51ABC123DEF456GHI789JKLmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOP';
392
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
390
+ const result = sanitizeOutput(input);
393
391
  expect(result).toBe('[REDACTED_STRIPE_KEY]');
394
392
  });
395
393
  });
396
394
  describe('database connection strings', () => {
397
395
  it('should redact PostgreSQL connection strings', () => {
398
396
  const input = 'DATABASE_URL=postgres://admin:secretpass123@db.example.com:5432/mydb';
399
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
397
+ const result = sanitizeOutput(input);
400
398
  expect(result).toBe('DATABASE_URL=[REDACTED_CONNECTION_STRING]');
401
399
  });
402
400
  it('should redact MongoDB connection strings with +srv', () => {
403
401
  const input = 'Connecting to mongodb+srv://user:p@ssw0rd@cluster.mongodb.net/database';
404
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
402
+ const result = sanitizeOutput(input);
405
403
  expect(result).toBe('Connecting to [REDACTED_CONNECTION_STRING]');
406
404
  });
407
405
  it('should redact Redis connection strings', () => {
408
406
  const input = 'REDIS_URL=redis://default:myredispassword@redis.example.com:6379';
409
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
407
+ const result = sanitizeOutput(input);
410
408
  expect(result).toBe('REDIS_URL=[REDACTED_CONNECTION_STRING]');
411
409
  });
412
410
  it('should redact MySQL connection strings', () => {
413
411
  const input = 'mysql://root:rootpassword@localhost:3306/testdb';
414
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
412
+ const result = sanitizeOutput(input);
415
413
  expect(result).toBe('[REDACTED_CONNECTION_STRING]');
416
414
  });
417
415
  it('should redact rediss (TLS) connection strings', () => {
418
416
  const input = 'rediss://user:password@secure-redis.example.com:6380';
419
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
417
+ const result = sanitizeOutput(input);
420
418
  expect(result).toBe('[REDACTED_CONNECTION_STRING]');
421
419
  });
422
420
  it('should redact connection strings with URL-encoded passwords', () => {
423
421
  const input = 'mongodb://user:p%40ss%23word@host/db';
424
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
422
+ const result = sanitizeOutput(input);
425
423
  expect(result).toBe('[REDACTED_CONNECTION_STRING]');
426
424
  });
427
425
  it('should redact PostgreSQL with URL-encoded @ in password', () => {
428
426
  const input = 'postgres://admin:secret%40pass@db.example.com:5432/mydb';
429
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
427
+ const result = sanitizeOutput(input);
430
428
  expect(result).toBe('[REDACTED_CONNECTION_STRING]');
431
429
  });
432
430
  it('should redact connection strings with multiple URL-encoded characters', () => {
433
431
  const input = 'mysql://root:p%40ss%3Dw%26rd%21@localhost:3306/testdb';
434
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
432
+ const result = sanitizeOutput(input);
435
433
  expect(result).toBe('[REDACTED_CONNECTION_STRING]');
436
434
  });
437
435
  it('should redact MongoDB+srv with URL-encoded password', () => {
438
436
  const input = 'mongodb+srv://user:my%40complex%23pass@cluster.mongodb.net/database';
439
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
437
+ const result = sanitizeOutput(input);
440
438
  expect(result).toBe('[REDACTED_CONNECTION_STRING]');
441
439
  });
442
440
  });
443
441
  describe('non-secret content', () => {
444
442
  it('should preserve normal log output', () => {
445
443
  const input = 'Build completed successfully in 2.5s';
446
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
444
+ const result = sanitizeOutput(input);
447
445
  expect(result).toBe(input);
448
446
  });
449
447
  it('should preserve error messages without secrets', () => {
450
448
  const input = 'Error: Cannot find module "./missing-file"';
451
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
449
+ const result = sanitizeOutput(input);
452
450
  expect(result).toBe(input);
453
451
  });
454
452
  it('should preserve stack traces', () => {
@@ -457,7 +455,7 @@ Done loading.`);
457
455
  at Object.<anonymous> (/app/test.js:10:5)
458
456
  at Module._compile (internal/modules/cjs/loader.js:1085:14)
459
457
  `;
460
- const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
458
+ const result = sanitizeOutput(input);
461
459
  expect(result).toBe(input);
462
460
  });
463
461
  });
@@ -508,17 +506,17 @@ describe('isSessionReadOnly', () => {
508
506
  describe('truncateOutput', () => {
509
507
  it('should return output unchanged if under maxLines', () => {
510
508
  const input = 'line1\nline2\nline3';
511
- expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
509
+ expect(truncateOutput(input, 20)).toBe(input);
512
510
  });
513
511
  it('should return output unchanged if exactly at maxLines', () => {
514
512
  const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`);
515
513
  const input = lines.join('\n');
516
- expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
514
+ expect(truncateOutput(input, 20)).toBe(input);
517
515
  });
518
516
  it('should truncate to last 20 lines by default', () => {
519
517
  const lines = Array.from({ length: 30 }, (_, i) => `line${i + 1}`);
520
518
  const input = lines.join('\n');
521
- const result = (0, stop_quality_gate_1.truncateOutput)(input);
519
+ const result = truncateOutput(input);
522
520
  expect(result).toContain('... (10 lines omitted)');
523
521
  expect(result).toContain('line11');
524
522
  expect(result).toContain('line30');
@@ -528,7 +526,7 @@ describe('truncateOutput', () => {
528
526
  it('should truncate to custom maxLines', () => {
529
527
  const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`);
530
528
  const input = lines.join('\n');
531
- const result = (0, stop_quality_gate_1.truncateOutput)(input, 5);
529
+ const result = truncateOutput(input, 5);
532
530
  expect(result).toContain('... (10 lines omitted)');
533
531
  expect(result).toContain('line11');
534
532
  expect(result).toContain('line15');
@@ -536,15 +534,15 @@ describe('truncateOutput', () => {
536
534
  });
537
535
  it('should handle single line output', () => {
538
536
  const input = 'single line';
539
- expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
537
+ expect(truncateOutput(input, 20)).toBe(input);
540
538
  });
541
539
  it('should handle empty output', () => {
542
- expect((0, stop_quality_gate_1.truncateOutput)('', 20)).toBe('');
540
+ expect(truncateOutput('', 20)).toBe('');
543
541
  });
544
542
  it('should preserve line content exactly', () => {
545
543
  const lines = Array.from({ length: 25 }, (_, i) => `Error at line ${i + 1}: something failed`);
546
544
  const input = lines.join('\n');
547
- const result = (0, stop_quality_gate_1.truncateOutput)(input, 10);
545
+ const result = truncateOutput(input, 10);
548
546
  expect(result).toContain('Error at line 16: something failed');
549
547
  expect(result).toContain('Error at line 25: something failed');
550
548
  });
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@syntesseraai/opencode-feature-factory",
4
- "version": "0.2.11",
4
+ "version": "0.2.13",
5
+ "type": "module",
5
6
  "description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
6
7
  "license": "MIT",
7
8
  "main": "./dist/index.js",