wogiflow 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -141,3 +141,36 @@ execFileSync('sg', ['--pattern', pattern, '--lang', lang, '--json', path]);
141
141
  - Prefer `execFile`/`execFileSync` with array arguments over `exec`/`execSync` with template strings
142
142
  - When using template strings, escape all user-controlled values
143
143
  - Never interpolate user input directly into shell commands
144
+
145
+ ## 9. Temp Directory Isolation (Claude Code 2.1.23+)
146
+
147
+ On shared systems (CI servers, multi-user machines), use per-user temp directories to prevent permission conflicts.
148
+
149
+ **Fixed in Claude Code 2.1.23**: Per-user temp directory isolation prevents permission conflicts.
150
+
151
+ ```javascript
152
+ // Good - per-user isolation
153
+ const userId = process.getuid?.() ?? process.env.USER ?? process.env.USERNAME ?? 'default';
154
+ const tempDir = path.join(os.tmpdir(), `myapp-${userId}`);
155
+
156
+ // Bad - global temp path on shared systems
157
+ const tempDir = path.join(os.tmpdir(), 'myapp');
158
+ ```
159
+
160
+ **Best practices:**
161
+ - Use UID on Unix systems (`process.getuid()`)
162
+ - Fall back to username environment variables on Windows
163
+ - Always provide a 'default' fallback for edge cases
164
+ - This pattern is used in `flow-worktree.js` for worktree isolation
165
+
166
+ ## 10. Search/Grep Timeout Handling (Claude Code 2.1.23+)
167
+
168
+ **Fixed in Claude Code 2.1.23**: Ripgrep search timeouts now report errors instead of silently returning empty results.
169
+
170
+ **Impact on WogiFlow:** Component detection, auto-context loading, and pattern matching rely on search operations. Before 2.1.23, search timeouts could cause false negatives.
171
+
172
+ **Best practices:**
173
+ - Handle empty search results gracefully - they may indicate timeout
174
+ - Add retry logic for search-dependent operations
175
+ - Log warnings when searches return unexpectedly empty
176
+ - Consider fallback strategies (glob-based search if grep fails)
@@ -317,21 +317,49 @@ class BaseBridge {
317
317
 
318
318
  /**
319
319
  * Generate and write the main rules file (e.g., CLAUDE.md)
320
+ * @param {Object} options - Generation options
321
+ * @param {boolean} options.force - Force overwrite even if locally modified
320
322
  */
321
- generateRulesFile() {
323
+ generateRulesFile(options = {}) {
322
324
  const config = this.readConfig();
323
325
  const content = this.generateRulesContent(config);
324
326
  const rulesFilePath = path.join(this.projectDir, this.getRulesFileName());
325
327
 
328
+ // Check for local modifications before overwriting
329
+ if (fs.existsSync(rulesFilePath) && !options.force) {
330
+ const existingContent = fs.readFileSync(rulesFilePath, 'utf-8');
331
+
332
+ // Check if file was manually modified (missing our generation marker)
333
+ const hasMarker = existingContent.includes('Generated by CLI Bridge');
334
+
335
+ if (!hasMarker) {
336
+ // File exists but wasn't generated by us - likely manually created/modified
337
+ this.log(`⚠️ ${this.getRulesFileName()} appears to be manually maintained (no generation marker)`);
338
+ this.log(` Skipping to preserve local customizations. Use --force to overwrite.`);
339
+ return false;
340
+ }
341
+
342
+ // Check if content would actually change
343
+ if (existingContent === content) {
344
+ this.log(`${this.getRulesFileName()} is up to date`);
345
+ return true;
346
+ }
347
+
348
+ // File has our marker - safe to overwrite as it's generated content
349
+ }
350
+
326
351
  fs.writeFileSync(rulesFilePath, content, 'utf-8');
327
352
  this.log(`Generated ${this.getRulesFileName()}`);
353
+ return true;
328
354
  }
329
355
 
330
356
  /**
331
357
  * Run full sync: skills, rules, and CLI-specific setup
358
+ * @param {Object} options - Sync options
359
+ * @param {boolean} options.force - Force overwrite of locally modified files
332
360
  * @returns {Object} Sync result summary
333
361
  */
334
- async sync() {
362
+ async sync(options = {}) {
335
363
  const startTime = Date.now();
336
364
  const results = {
337
365
  cliType: this.cliType,
@@ -371,10 +399,14 @@ class BaseBridge {
371
399
  results.errors.push({ step: 'knowledge-files', error: err.message });
372
400
  }
373
401
 
374
- // 5. Generate rules file
402
+ // 5. Generate rules file (respects local modifications unless --force)
375
403
  try {
376
- this.generateRulesFile();
377
- results.synced.push('rules-file');
404
+ const generated = this.generateRulesFile({ force: options.force });
405
+ if (generated) {
406
+ results.synced.push('rules-file');
407
+ } else {
408
+ results.synced.push('rules-file-skipped');
409
+ }
378
410
  } catch (err) {
379
411
  results.errors.push({ step: 'rules-file', error: err.message });
380
412
  }
@@ -590,13 +622,18 @@ class BaseBridge {
590
622
  * @returns {string} Content with conditionals evaluated
591
623
  */
592
624
  processConditionals(content, config) {
593
- const ifRegex = /\{\{#if\s+([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
625
+ // Match INNERMOST conditionals first - those with no nested {{#if}} in body
626
+ // The negative lookahead (?!{{#if) ensures body contains no nested conditionals
627
+ const innerIfRegex = /\{\{#if\s+([^}]+)\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{\/if\}\}/g;
594
628
 
595
629
  let lastContent;
630
+ let iterations = 0;
631
+ const maxIterations = 100; // Safety limit for deeply nested templates
632
+
596
633
  // Keep processing until no more changes (handles nested conditions)
597
634
  do {
598
635
  lastContent = content;
599
- content = content.replace(ifRegex, (match, condition, body) => {
636
+ content = content.replace(innerIfRegex, (match, condition, body) => {
600
637
  let value;
601
638
  if (condition.startsWith('config.')) {
602
639
  value = this.getNestedValue(config, condition.replace('config.', ''));
@@ -607,7 +644,12 @@ class BaseBridge {
607
644
  }
608
645
  return value ? body : '';
609
646
  });
610
- } while (content !== lastContent);
647
+ iterations++;
648
+ } while (content !== lastContent && iterations < maxIterations);
649
+
650
+ if (iterations >= maxIterations) {
651
+ this.log('Warning: Max iterations reached in processConditionals - possible malformed template');
652
+ }
611
653
 
612
654
  return content;
613
655
  }
@@ -419,7 +419,7 @@ Last synced: ${new Date().toISOString()}
419
419
  }
420
420
  }
421
421
 
422
- return {
422
+ const settings = {
423
423
  permissions: {
424
424
  allow: wildcardPermissions,
425
425
  },
@@ -428,6 +428,20 @@ Last synced: ${new Date().toISOString()}
428
428
  _wogiFlowVersion: '2.0.0',
429
429
  _generatedAt: new Date().toISOString(),
430
430
  };
431
+
432
+ // Add spinnerVerbs if configured (requires Claude Code 2.1.23+)
433
+ const spinnerVerbs = config.hooks?.claudeCode?.spinnerVerbs;
434
+ if (Array.isArray(spinnerVerbs) && spinnerVerbs.length > 0) {
435
+ settings.spinnerVerbs = spinnerVerbs;
436
+ }
437
+
438
+ // Add spinnerTipsEnabled if explicitly set (default is true in Claude Code)
439
+ const spinnerTipsEnabled = config.hooks?.claudeCode?.spinnerTipsEnabled;
440
+ if (spinnerTipsEnabled === false) {
441
+ settings.spinnerTipsEnabled = false;
442
+ }
443
+
444
+ return settings;
431
445
  }
432
446
 
433
447
  /**
@@ -448,7 +462,7 @@ Last synced: ${new Date().toISOString()}
448
462
 
449
463
  const newSettings = this.generateSettings(config);
450
464
 
451
- // Merge: keep existing hooks, use new permissions
465
+ // Merge: keep existing hooks, use new permissions and spinner settings
452
466
  const mergedSettings = {
453
467
  permissions: newSettings.permissions,
454
468
  respectGitignore: newSettings.respectGitignore,
@@ -458,6 +472,16 @@ Last synced: ${new Date().toISOString()}
458
472
  _generatedAt: newSettings._generatedAt,
459
473
  };
460
474
 
475
+ // Include spinnerVerbs if configured
476
+ if (newSettings.spinnerVerbs) {
477
+ mergedSettings.spinnerVerbs = newSettings.spinnerVerbs;
478
+ }
479
+
480
+ // Include spinnerTipsEnabled if explicitly disabled
481
+ if (newSettings.spinnerTipsEnabled === false) {
482
+ mergedSettings.spinnerTipsEnabled = false;
483
+ }
484
+
461
485
  fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2));
462
486
  this.log(`Synced settings.local.json with wildcard permissions (${newSettings.permissions.allow.length} rules)`);
463
487
 
@@ -136,7 +136,8 @@ async function syncBridge(options = {}) {
136
136
  };
137
137
  }
138
138
 
139
- return await bridge.sync();
139
+ // Pass through sync options (e.g., force: true to overwrite locally modified files)
140
+ return await bridge.sync({ force: options.force });
140
141
  }
141
142
 
142
143
  /**
package/README.md CHANGED
@@ -19,14 +19,16 @@ npx flow bridge sync
19
19
 
20
20
  WogiFlow works with 6 AI coding CLIs. Use whichever you prefer - the workflow state is shared.
21
21
 
22
- | CLI | Enforcement | Rules File | Guide |
23
- |-----|-------------|------------|-------|
24
- | **Claude Code** | Hard (hooks) | `CLAUDE.md` | [Guide](.workflow/docs/cli-guides/claude-code.md) |
25
- | **Gemini CLI** | Hard (hooks) | `GEMINI.md` | [Guide](.workflow/docs/cli-guides/gemini-cli.md) |
26
- | **Cursor** | Mixed | `.cursor/rules/wogiflow.mdc` | [Guide](.workflow/docs/cli-guides/cursor.md) |
27
- | **OpenCode** | Hard (plugins) | `AGENTS.md` | [Guide](.workflow/docs/cli-guides/opencode.md) |
28
- | **Codex** | Soft (rules) | `AGENTS.md` | [Guide](.workflow/docs/cli-guides/codex.md) |
29
- | **Kimi** | Soft (rules) | `AGENTS.md` | [Guide](.workflow/docs/cli-guides/kimi.md) |
22
+ | CLI | Enforcement | Rules File | Min Version | Guide |
23
+ |-----|-------------|------------|-------------|-------|
24
+ | **Claude Code** | Hard (hooks) | `CLAUDE.md` | **2.1.23+** | [Guide](.workflow/docs/cli-guides/claude-code.md) |
25
+ | **Gemini CLI** | Hard (hooks) | `GEMINI.md` | - | [Guide](.workflow/docs/cli-guides/gemini-cli.md) |
26
+ | **Cursor** | Mixed | `.cursor/rules/wogiflow.mdc` | - | [Guide](.workflow/docs/cli-guides/cursor.md) |
27
+ | **OpenCode** | Hard (plugins) | `AGENTS.md` | - | [Guide](.workflow/docs/cli-guides/opencode.md) |
28
+ | **Codex** | Soft (rules) | `AGENTS.md` | - | [Guide](.workflow/docs/cli-guides/codex.md) |
29
+ | **Kimi** | Soft (rules) | `AGENTS.md` | - | [Guide](.workflow/docs/cli-guides/kimi.md) |
30
+
31
+ > **Claude Code 2.1.23+ Recommended**: Includes critical fixes for per-user temp directory isolation (shared systems), async hook cancellation, and ripgrep timeout reporting. Earlier versions may experience silent search failures.
30
32
 
31
33
  **Enforcement levels:**
32
34
  - **Hard**: Blocks operations before execution (best protection)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -190,6 +190,7 @@ function normalizeCliType(input) {
190
190
  */
191
191
  async function syncBridge(options = {}) {
192
192
  const verbose = options.verbose || process.argv.includes('--verbose') || process.argv.includes('-v');
193
+ const force = options.force || process.argv.includes('--force') || process.argv.includes('-f');
193
194
 
194
195
  // Check for CLI type argument (e.g., "flow bridge sync gemini")
195
196
  const cliTypeArg = process.argv[3];
@@ -219,6 +220,7 @@ async function syncBridge(options = {}) {
219
220
 
220
221
  const result = await bridges.syncBridge({
221
222
  verbose,
223
+ force,
222
224
  projectDir: PROJECT_ROOT,
223
225
  cliType: targetCliType
224
226
  });
@@ -271,11 +273,16 @@ switch (command) {
271
273
  console.log(' status Show current bridge configuration');
272
274
  console.log(' list List available CLI bridges');
273
275
  console.log('');
276
+ console.log('Options:');
277
+ console.log(' --force, -f Overwrite locally modified rules files (CLAUDE.md, GEMINI.md, etc.)');
278
+ console.log(' --verbose, -v Show detailed output');
279
+ console.log('');
274
280
  console.log('CLI Types:');
275
281
  console.log(' claude-code, gemini-cli (or gemini), cursor, opencode, codex, kimi');
276
282
  console.log('');
277
283
  console.log('Examples:');
278
284
  console.log(' flow bridge sync # Sync default CLI from config');
279
285
  console.log(' flow bridge sync gemini # Sync Gemini CLI specifically');
286
+ console.log(' flow bridge sync --force # Force overwrite even if locally modified');
280
287
  process.exit(1);
281
288
  }
@@ -38,6 +38,39 @@ const {
38
38
  checkSpecMigration
39
39
  } = require('./flow-utils');
40
40
 
41
+ const { execSync } = require('child_process');
42
+
43
+ /**
44
+ * Check Claude Code version and compare against minimum recommended (2.1.23)
45
+ * @returns {{ version: string|null, meetsMinimum: boolean }}
46
+ */
47
+ function checkClaudeCodeVersion() {
48
+ try {
49
+ const output = execSync('claude --version 2>/dev/null || echo ""', {
50
+ encoding: 'utf-8',
51
+ stdio: ['pipe', 'pipe', 'pipe']
52
+ }).trim();
53
+
54
+ // Parse version from output like "claude 2.1.23" or "Claude Code 2.1.23"
55
+ const match = output.match(/(\d+\.\d+\.\d+)/);
56
+ if (!match) {
57
+ return { version: null, meetsMinimum: true };
58
+ }
59
+
60
+ const version = match[1];
61
+ const [major, minor, patch] = version.split('.').map(Number);
62
+
63
+ // Minimum recommended: 2.1.23
64
+ const meetsMinimum = major > 2 ||
65
+ (major === 2 && minor > 1) ||
66
+ (major === 2 && minor === 1 && patch >= 23);
67
+
68
+ return { version, meetsMinimum };
69
+ } catch {
70
+ return { version: null, meetsMinimum: true };
71
+ }
72
+ }
73
+
41
74
  function main() {
42
75
  console.log(color('cyan', 'Wogi Flow Health Check'));
43
76
  console.log('========================');
@@ -90,6 +123,20 @@ function main() {
90
123
  issues++;
91
124
  }
92
125
 
126
+ // Check Claude Code version (if applicable)
127
+ if (cliType === 'claude-code') {
128
+ const versionCheck = checkClaudeCodeVersion();
129
+ if (versionCheck.version) {
130
+ if (versionCheck.meetsMinimum) {
131
+ console.log(` ${color('green', '✓')} Claude Code version: ${versionCheck.version}`);
132
+ } else {
133
+ console.log(` ${color('yellow', '○')} Claude Code version: ${versionCheck.version} (2.1.23+ recommended)`);
134
+ console.log(` ${color('dim', '→ Older versions may have silent search failures and shared system issues')}`);
135
+ warnings++;
136
+ }
137
+ }
138
+ }
139
+
93
140
  // Check required directories
94
141
  console.log('');
95
142
  printSection('Checking directories...');
@@ -3024,6 +3024,101 @@ function checkSpecMigration() {
3024
3024
  return needsMigration;
3025
3025
  }
3026
3026
 
3027
+ // ============================================================
3028
+ // Safe Search Utilities (Claude Code 2.1.23+ compatibility)
3029
+ // ============================================================
3030
+
3031
+ /**
3032
+ * Perform a safe grep search with proper timeout and error handling.
3033
+ * Returns results and metadata about search status.
3034
+ *
3035
+ * Before Claude Code 2.1.23, ripgrep timeouts would silently return empty results.
3036
+ * This utility provides explicit handling for timeouts and errors.
3037
+ *
3038
+ * @param {string} pattern - Search pattern
3039
+ * @param {Object} options - Search options
3040
+ * @param {string} [options.path] - Directory to search (default: PROJECT_ROOT)
3041
+ * @param {string[]} [options.extensions] - File extensions to include (e.g., ['ts', 'tsx'])
3042
+ * @param {number} [options.timeout] - Timeout in ms (default: 10000)
3043
+ * @param {number} [options.maxResults] - Maximum results to return (default: 50)
3044
+ * @param {boolean} [options.caseInsensitive] - Case insensitive search (default: true)
3045
+ * @returns {{ results: string[], timedOut: boolean, error: string|null }}
3046
+ */
3047
+ function safeGrepSearch(pattern, options = {}) {
3048
+ const { spawnSync } = require('child_process');
3049
+
3050
+ const searchPath = options.path || PROJECT_ROOT;
3051
+ const extensions = options.extensions || ['ts', 'tsx', 'js', 'jsx'];
3052
+ const timeout = options.timeout || 10000;
3053
+ const maxResults = options.maxResults || 50;
3054
+ const caseInsensitive = options.caseInsensitive !== false;
3055
+
3056
+ const args = ['-rl'];
3057
+ if (caseInsensitive) args.push('-i');
3058
+
3059
+ // Add include patterns for extensions
3060
+ for (const ext of extensions) {
3061
+ args.push(`--include=*.${ext}`);
3062
+ }
3063
+
3064
+ args.push(pattern, searchPath);
3065
+
3066
+ try {
3067
+ const result = spawnSync('grep', args, {
3068
+ encoding: 'utf-8',
3069
+ timeout,
3070
+ stdio: ['pipe', 'pipe', 'pipe']
3071
+ });
3072
+
3073
+ // Check for timeout
3074
+ if (result.signal === 'SIGTERM') {
3075
+ return {
3076
+ results: [],
3077
+ timedOut: true,
3078
+ error: `Search timed out after ${timeout}ms`
3079
+ };
3080
+ }
3081
+
3082
+ // Check for error (but exit code 1 just means no matches)
3083
+ if (result.status > 1) {
3084
+ return {
3085
+ results: [],
3086
+ timedOut: false,
3087
+ error: result.stderr?.trim() || `grep exited with code ${result.status}`
3088
+ };
3089
+ }
3090
+
3091
+ const files = (result.stdout || '')
3092
+ .split('\n')
3093
+ .filter(f => f.trim())
3094
+ .slice(0, maxResults);
3095
+
3096
+ return {
3097
+ results: files,
3098
+ timedOut: false,
3099
+ error: null
3100
+ };
3101
+ } catch (err) {
3102
+ // Handle ETIMEDOUT and other errors
3103
+ const isTimeout = err.code === 'ETIMEDOUT' || err.killed;
3104
+ return {
3105
+ results: [],
3106
+ timedOut: isTimeout,
3107
+ error: isTimeout ? `Search timed out after ${timeout}ms` : err.message
3108
+ };
3109
+ }
3110
+ }
3111
+
3112
+ /**
3113
+ * Configuration defaults for search operations
3114
+ */
3115
+ const SEARCH_DEFAULTS = {
3116
+ timeout: 10000, // 10 seconds
3117
+ maxResults: 50, // Maximum files to return
3118
+ retryOnTimeout: true, // Whether to retry with reduced scope on timeout
3119
+ extensions: ['ts', 'tsx', 'js', 'jsx', 'vue', 'svelte']
3120
+ };
3121
+
3027
3122
  // ============================================================
3028
3123
  // Exports
3029
3124
  // ============================================================
@@ -3172,6 +3267,10 @@ module.exports = {
3172
3267
  findTaskInAllLists,
3173
3268
  analyzeRequest,
3174
3269
  estimateComplexity,
3270
+
3271
+ // Safe Search (Claude Code 2.1.23+ compatibility)
3272
+ safeGrepSearch,
3273
+ SEARCH_DEFAULTS,
3175
3274
  };
3176
3275
 
3177
3276
  // ============================================================
@@ -33,7 +33,19 @@ const { sanitizeCommitMessage } = require('./flow-security');
33
33
  // ============================================================
34
34
 
35
35
  const WORKTREE_PREFIX = 'wogi-task-';
36
- const WORKTREE_BASE_DIR = path.join(os.tmpdir(), 'wogi-worktrees');
36
+
37
+ /**
38
+ * Get user-specific worktree base directory.
39
+ * Uses UID on Unix, username on Windows, with 'default' fallback.
40
+ * This prevents permission conflicts on shared systems (CI, multi-user machines).
41
+ * See: Claude Code 2.1.23 per-user temp directory isolation fix.
42
+ */
43
+ function getWorktreeBaseDir() {
44
+ const userId = process.getuid?.() ?? process.env.USER ?? process.env.USERNAME ?? 'default';
45
+ return path.join(os.tmpdir(), `wogi-worktrees-${userId}`);
46
+ }
47
+
48
+ const WORKTREE_BASE_DIR = getWorktreeBaseDir();
37
49
 
38
50
  // ============================================================
39
51
  // Helper Functions