wogiflow 1.0.11 → 1.0.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.
Files changed (46) hide show
  1. package/.workflow/specs/architecture.md.template +24 -0
  2. package/.workflow/specs/stack.md.template +33 -0
  3. package/.workflow/specs/testing.md.template +36 -0
  4. package/README.md +90 -1
  5. package/lib/unified-wizard.js +569 -30
  6. package/package.json +1 -1
  7. package/scripts/MEMORY-ARCHITECTURE.md +150 -0
  8. package/scripts/flow +20 -19
  9. package/scripts/flow-auto-context.js +97 -3
  10. package/scripts/flow-conflict-resolver.js +735 -0
  11. package/scripts/flow-context-gatherer.js +520 -0
  12. package/scripts/flow-context-monitor.js +148 -19
  13. package/scripts/flow-damage-control.js +5 -1
  14. package/scripts/flow-export-profile +168 -1
  15. package/scripts/flow-import-profile +257 -6
  16. package/scripts/flow-instruction-richness.js +182 -18
  17. package/scripts/flow-knowledge-router.js +2 -0
  18. package/scripts/flow-knowledge-sync.js +2 -0
  19. package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
  20. package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
  21. package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
  22. package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
  23. package/scripts/flow-memory-db.js +386 -1
  24. package/scripts/flow-memory-sync.js +2 -0
  25. package/scripts/flow-model-adapter.js +53 -29
  26. package/scripts/flow-model-router.js +246 -1
  27. package/scripts/flow-morning.js +94 -0
  28. package/scripts/flow-onboard +223 -10
  29. package/scripts/flow-orchestrate-validation.js +539 -0
  30. package/scripts/flow-orchestrate.js +16 -507
  31. package/scripts/flow-pattern-extractor.js +1265 -0
  32. package/scripts/flow-prompt-composer.js +222 -2
  33. package/scripts/flow-quality-guard.js +594 -0
  34. package/scripts/flow-section-index.js +713 -0
  35. package/scripts/flow-section-resolver.js +484 -0
  36. package/scripts/flow-session-end.js +188 -2
  37. package/scripts/flow-skill-create.js +19 -3
  38. package/scripts/flow-skill-matcher.js +122 -7
  39. package/scripts/flow-statusline-setup.js +218 -0
  40. package/scripts/flow-step-review.js +19 -0
  41. package/scripts/flow-tech-debt.js +734 -0
  42. package/scripts/flow-utils.js +2 -0
  43. package/scripts/hooks/core/long-input-gate.js +293 -0
  44. package/scripts/flow-parallel-detector.js +0 -399
  45. package/scripts/flow-parallel-dispatch.js +0 -987
  46. /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * Creates new skills from templates with interactive prompts.
7
7
  *
8
+ * TODO: Consider consolidating with flow-skill-creator.js and
9
+ * flow-skill-generator.js into a single flow-skill.js with subcommands.
10
+ * See audit plan from 2026-01-12.
11
+ *
8
12
  * Usage:
9
13
  * flow skill-create # Interactive mode
10
14
  * flow skill-create <name> # Create with name
@@ -76,14 +80,26 @@ function copyDirectory(src, dest, replacements = {}) {
76
80
  }
77
81
 
78
82
  async function createSkill(name, options = {}) {
79
- const skillPath = path.join(SKILLS_DIR, name);
83
+ // Support nested paths like "frontend/react" or "backend/nestjs"
84
+ const pathParts = name.split('/').filter(Boolean);
85
+ const skillPath = path.join(SKILLS_DIR, ...pathParts);
86
+ const displayName = pathParts[pathParts.length - 1]; // Base name for display
80
87
 
81
88
  if (fs.existsSync(skillPath)) {
82
89
  log('red', `Skill '${name}' already exists at ${skillPath}`);
83
90
  return false;
84
91
  }
85
92
 
93
+ // Create parent directories if nested
94
+ if (pathParts.length > 1) {
95
+ const parentDir = path.join(SKILLS_DIR, ...pathParts.slice(0, -1));
96
+ fs.mkdirSync(parentDir, { recursive: true });
97
+ }
98
+
86
99
  log('cyan', `\n📦 Creating skill: ${name}\n`);
100
+ if (pathParts.length > 1) {
101
+ log('dim', ` (nested skill at ${skillPath})\n`);
102
+ }
87
103
 
88
104
  // Gather information
89
105
  const description = options.description || await prompt('Short description');
@@ -323,7 +339,7 @@ async function main() {
323
339
  await createSkill(name);
324
340
  }
325
341
 
326
- main().catch(e => {
327
- log('red', `Error: ${e.message}`);
342
+ main().catch(err => {
343
+ log('red', `Error: ${err.message}`);
328
344
  process.exit(1);
329
345
  });
@@ -23,6 +23,70 @@ const { getProjectRoot, getConfig, PATHS, colors } = require('./flow-utils');
23
23
  const PROJECT_ROOT = getProjectRoot();
24
24
  const SKILLS_DIR = path.join(PROJECT_ROOT, '.claude', 'skills');
25
25
 
26
+ // Maximum nesting depth for skill directories (prevents runaway recursion)
27
+ const MAX_SKILL_NESTING_DEPTH = 3;
28
+
29
+ // ============================================================
30
+ // Nested Skill Discovery
31
+ // ============================================================
32
+
33
+ /**
34
+ * Recursively discover all skills in the skills directory
35
+ * Looks for directories containing skill.md files
36
+ *
37
+ * @param {string} baseDir - Base directory to search
38
+ * @param {string} prefix - Path prefix for nested skills
39
+ * @param {number} depth - Current recursion depth
40
+ * @returns {string[]} Array of skill paths (e.g., ["nestjs", "frontend/react"])
41
+ */
42
+ function discoverNestedSkills(baseDir = SKILLS_DIR, prefix = '', depth = 0) {
43
+ if (depth > MAX_SKILL_NESTING_DEPTH) {
44
+ return [];
45
+ }
46
+
47
+ const skills = [];
48
+
49
+ try {
50
+ if (!fs.existsSync(baseDir)) {
51
+ return [];
52
+ }
53
+
54
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
55
+
56
+ for (const entry of entries) {
57
+ // Skip hidden directories, _template, and non-directories
58
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
59
+ continue;
60
+ }
61
+
62
+ const entryPath = path.join(baseDir, entry.name);
63
+ const skillPath = prefix ? `${prefix}/${entry.name}` : entry.name;
64
+ const skillMdPath = path.join(entryPath, 'skill.md');
65
+
66
+ // If this directory has a skill.md, it's a skill
67
+ if (fs.existsSync(skillMdPath)) {
68
+ skills.push(skillPath);
69
+ }
70
+
71
+ // Recursively check subdirectories for more skills
72
+ const nestedSkills = discoverNestedSkills(entryPath, skillPath, depth + 1);
73
+ skills.push(...nestedSkills);
74
+ }
75
+ } catch (err) {
76
+ // Silently ignore permission errors, etc.
77
+ }
78
+
79
+ return skills;
80
+ }
81
+
82
+ /**
83
+ * Get the absolute path to a skill directory
84
+ * Handles both flat ("nestjs") and nested ("frontend/react") paths
85
+ */
86
+ function getSkillDir(skillName) {
87
+ return path.join(SKILLS_DIR, ...skillName.split('/'));
88
+ }
89
+
26
90
  // ============================================================
27
91
  // Skill Trigger Definitions
28
92
  // ============================================================
@@ -71,6 +135,7 @@ const DEFAULT_TRIGGERS = {
71
135
  /**
72
136
  * Load skill metadata from skill.md
73
137
  * Parses YAML frontmatter and extracts trigger configuration
138
+ * Supports both flat ("nestjs") and nested ("frontend/react") skill paths
74
139
  */
75
140
  function loadSkillMetadata(skillName) {
76
141
  // Skip template skills
@@ -78,7 +143,14 @@ function loadSkillMetadata(skillName) {
78
143
  return null;
79
144
  }
80
145
 
81
- const skillPath = path.join(SKILLS_DIR, skillName, 'skill.md');
146
+ // Handle nested paths - get the base name for template checks
147
+ const baseName = skillName.split('/').pop();
148
+ if (baseName.startsWith('_')) {
149
+ return null;
150
+ }
151
+
152
+ const skillDir = getSkillDir(skillName);
153
+ const skillPath = path.join(skillDir, 'skill.md');
82
154
 
83
155
  if (!fs.existsSync(skillPath)) {
84
156
  return null;
@@ -206,15 +278,24 @@ function parseListSection(section) {
206
278
 
207
279
  /**
208
280
  * Get all installed skills with their triggers
281
+ * Combines skills from config.json with auto-discovered nested skills
209
282
  */
210
283
  function getAllSkills() {
211
284
  const config = getConfig();
212
- const installedSkills = config.skills?.installed || [];
285
+ const configuredSkills = config.skills?.installed || [];
286
+ const autoDiscover = config.skills?.autoDiscoverNested !== false; // Default: true
213
287
  const skills = [];
288
+ const seenSkills = new Set();
289
+
290
+ // First, load configured skills (these take priority)
291
+ for (const skillName of configuredSkills) {
292
+ if (seenSkills.has(skillName)) continue;
293
+ seenSkills.add(skillName);
214
294
 
215
- for (const skillName of installedSkills) {
216
295
  const metadata = loadSkillMetadata(skillName);
217
- const defaultTriggers = DEFAULT_TRIGGERS[skillName] || {
296
+ // Get default triggers - check both full path and base name
297
+ const baseName = skillName.split('/').pop();
298
+ const defaultTriggers = DEFAULT_TRIGGERS[skillName] || DEFAULT_TRIGGERS[baseName] || {
218
299
  keywords: [],
219
300
  filePatterns: [],
220
301
  taskTypes: ['feature', 'bugfix', 'refactor'],
@@ -229,6 +310,35 @@ function getAllSkills() {
229
310
  });
230
311
  }
231
312
 
313
+ // Then, auto-discover nested skills if enabled
314
+ if (autoDiscover) {
315
+ const discoveredSkills = discoverNestedSkills();
316
+
317
+ for (const skillName of discoveredSkills) {
318
+ if (seenSkills.has(skillName)) continue;
319
+ seenSkills.add(skillName);
320
+
321
+ const metadata = loadSkillMetadata(skillName);
322
+ if (!metadata) continue; // Skip if can't load metadata
323
+
324
+ // Get default triggers - check both full path and base name
325
+ const baseName = skillName.split('/').pop();
326
+ const defaultTriggers = DEFAULT_TRIGGERS[skillName] || DEFAULT_TRIGGERS[baseName] || {
327
+ keywords: [],
328
+ filePatterns: [],
329
+ taskTypes: ['feature', 'bugfix', 'refactor'],
330
+ categories: []
331
+ };
332
+
333
+ skills.push({
334
+ name: skillName,
335
+ metadata: metadata || {},
336
+ triggers: metadata?.triggers || defaultTriggers,
337
+ filePatterns: metadata?.filePatterns || defaultTriggers.filePatterns
338
+ });
339
+ }
340
+ }
341
+
232
342
  return skills;
233
343
  }
234
344
 
@@ -323,11 +433,12 @@ function matchSkills(taskDescription, options = {}) {
323
433
 
324
434
  /**
325
435
  * Convert glob pattern to regex
436
+ * Uses [^/]* instead of .* to prevent matching directory separators (security)
326
437
  */
327
438
  function patternToRegex(pattern) {
328
439
  const escaped = pattern
329
440
  .replace(/[.+^${}()|[\]\\]/g, '\\$&')
330
- .replace(/\*/g, '.*')
441
+ .replace(/\*/g, '[^/]*') // Security: don't match path separators
331
442
  .replace(/\?/g, '.');
332
443
  return new RegExp(escaped, 'i');
333
444
  }
@@ -360,7 +471,8 @@ async function loadSkillContext(matchedSkills, options = {}) {
360
471
  };
361
472
 
362
473
  for (const skill of skillsToLoad) {
363
- const skillDir = path.join(SKILLS_DIR, skill.name);
474
+ // Use getSkillDir to handle nested paths
475
+ const skillDir = getSkillDir(skill.name);
364
476
  const skillContext = {
365
477
  name: skill.name,
366
478
  score: skill.score,
@@ -567,7 +679,10 @@ module.exports = {
567
679
  loadSkillContext,
568
680
  formatSkillContext,
569
681
  getSkillSummary,
570
- DEFAULT_TRIGGERS
682
+ discoverNestedSkills,
683
+ getSkillDir,
684
+ DEFAULT_TRIGGERS,
685
+ MAX_SKILL_NESTING_DEPTH
571
686
  };
572
687
 
573
688
  if (require.main === module) {
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Status Line Setup
5
+ *
6
+ * Configures Claude Code's status line to show WogiFlow task information.
7
+ * Uses the new context_window.used_percentage field from Claude Code v1.0.52+.
8
+ *
9
+ * Usage:
10
+ * flow statusline-setup # Interactive setup
11
+ * flow statusline-setup --format compact
12
+ * flow statusline-setup --format detailed
13
+ * flow statusline-setup --show # Show current config
14
+ * flow statusline-setup --disable # Disable status line
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const readline = require('readline');
21
+ const { colors, printHeader, safeJsonParse } = require('./flow-utils');
22
+
23
+ // Status line format presets
24
+ const FORMATS = {
25
+ minimal: {
26
+ name: 'Minimal',
27
+ description: 'Just model and context percentage',
28
+ format: '{{model}} | {{context_window.used_percentage}}%'
29
+ },
30
+ compact: {
31
+ name: 'Compact',
32
+ description: 'Task ID + model + context',
33
+ format: '{{#if task}}[{{task.id}}] {{/if}}{{model}} | {{context_window.used_percentage}}%'
34
+ },
35
+ standard: {
36
+ name: 'Standard (Recommended)',
37
+ description: 'Task + model + labeled context',
38
+ format: '{{#if task}}[{{task.id}}] {{/if}}{{model}} | Ctx: {{context_window.used_percentage}}%'
39
+ },
40
+ detailed: {
41
+ name: 'Detailed',
42
+ description: 'Full info including skill',
43
+ format: '{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}% used{{#if skill}} | {{skill}}{{/if}}'
44
+ }
45
+ };
46
+
47
+ // Claude settings file location
48
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
49
+
50
+ function loadClaudeSettings() {
51
+ if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
52
+ return {};
53
+ }
54
+ // Use safeJsonParse for prototype pollution protection
55
+ return safeJsonParse(CLAUDE_SETTINGS_PATH, {});
56
+ }
57
+
58
+ function saveClaudeSettings(settings) {
59
+ try {
60
+ // Ensure directory exists
61
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
62
+ if (!fs.existsSync(dir)) {
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ }
65
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
66
+ return true;
67
+ } catch (err) {
68
+ console.error(`${colors.red}Error: Could not save Claude settings: ${err.message}${colors.reset}`);
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function showCurrentConfig() {
74
+ const settings = loadClaudeSettings();
75
+ const statusLine = settings.statusLine || {};
76
+
77
+ printHeader('Current Status Line Configuration');
78
+
79
+ if (!statusLine.enabled && statusLine.enabled !== undefined) {
80
+ console.log(`${colors.dim}Status: ${colors.yellow}Disabled${colors.reset}`);
81
+ } else if (statusLine.format) {
82
+ console.log(`${colors.dim}Status: ${colors.green}Enabled${colors.reset}`);
83
+ console.log(`${colors.dim}Format:${colors.reset} ${statusLine.format}`);
84
+ } else {
85
+ console.log(`${colors.dim}Status: ${colors.yellow}Not configured${colors.reset}`);
86
+ }
87
+ console.log('');
88
+ }
89
+
90
+ function showFormats() {
91
+ console.log(`${colors.bold}Available Formats:${colors.reset}\n`);
92
+
93
+ for (const [key, preset] of Object.entries(FORMATS)) {
94
+ console.log(` ${colors.cyan}${key}${colors.reset} - ${preset.name}`);
95
+ console.log(` ${colors.dim}${preset.description}${colors.reset}`);
96
+ console.log(` ${colors.dim}Preview: ${preset.format}${colors.reset}`);
97
+ console.log('');
98
+ }
99
+ }
100
+
101
+ async function interactiveSetup() {
102
+ const rl = readline.createInterface({
103
+ input: process.stdin,
104
+ output: process.stdout
105
+ });
106
+
107
+ const question = (q) => new Promise(resolve => rl.question(q, resolve));
108
+
109
+ printHeader('Status Line Setup');
110
+ showCurrentConfig();
111
+ showFormats();
112
+
113
+ const format = await question(`\nChoose format (minimal/compact/standard/detailed) [standard]: `);
114
+ const selectedFormat = format.trim() || 'standard';
115
+
116
+ if (!FORMATS[selectedFormat]) {
117
+ console.log(`${colors.red}Invalid format: ${selectedFormat}${colors.reset}`);
118
+ rl.close();
119
+ process.exit(1);
120
+ }
121
+
122
+ const settings = loadClaudeSettings();
123
+ settings.statusLine = {
124
+ enabled: true,
125
+ format: FORMATS[selectedFormat].format
126
+ };
127
+
128
+ const confirm = await question(`\nApply "${FORMATS[selectedFormat].name}" format? (y/N): `);
129
+
130
+ if (confirm.toLowerCase() === 'y') {
131
+ if (saveClaudeSettings(settings)) {
132
+ console.log(`\n${colors.green}✓ Status line configured successfully!${colors.reset}`);
133
+ console.log(`${colors.dim}Restart Claude Code to see changes.${colors.reset}`);
134
+ }
135
+ } else {
136
+ console.log(`${colors.dim}Setup cancelled.${colors.reset}`);
137
+ }
138
+
139
+ rl.close();
140
+ }
141
+
142
+ async function main() {
143
+ const args = process.argv.slice(2);
144
+
145
+ if (args.includes('--help') || args.includes('-h')) {
146
+ console.log(`
147
+ Wogi Flow - Status Line Setup
148
+
149
+ Configure Claude Code's status line to show task and context info.
150
+
151
+ Usage:
152
+ flow statusline-setup Interactive setup
153
+ flow statusline-setup --format X Set format directly
154
+ flow statusline-setup --show Show current config
155
+ flow statusline-setup --disable Disable status line
156
+ flow statusline-setup --formats List available formats
157
+
158
+ Formats:
159
+ minimal - Model + context %
160
+ compact - Task ID + model + context %
161
+ standard - Task ID + model + labeled context (recommended)
162
+ detailed - Full info including skill name
163
+
164
+ Examples:
165
+ flow statusline-setup --format standard
166
+ flow statusline-setup --format detailed
167
+ `);
168
+ process.exit(0);
169
+ }
170
+
171
+ if (args.includes('--show')) {
172
+ showCurrentConfig();
173
+ process.exit(0);
174
+ }
175
+
176
+ if (args.includes('--formats')) {
177
+ showFormats();
178
+ process.exit(0);
179
+ }
180
+
181
+ if (args.includes('--disable')) {
182
+ const settings = loadClaudeSettings();
183
+ settings.statusLine = { enabled: false };
184
+ if (saveClaudeSettings(settings)) {
185
+ console.log(`${colors.green}✓ Status line disabled.${colors.reset}`);
186
+ }
187
+ process.exit(0);
188
+ }
189
+
190
+ const formatIndex = args.indexOf('--format');
191
+ if (formatIndex >= 0) {
192
+ const format = args[formatIndex + 1];
193
+ if (!format || !FORMATS[format]) {
194
+ console.log(`${colors.red}Invalid format. Use: minimal, compact, standard, or detailed${colors.reset}`);
195
+ process.exit(1);
196
+ }
197
+
198
+ const settings = loadClaudeSettings();
199
+ settings.statusLine = {
200
+ enabled: true,
201
+ format: FORMATS[format].format
202
+ };
203
+
204
+ if (saveClaudeSettings(settings)) {
205
+ console.log(`${colors.green}✓ Status line configured with "${format}" format.${colors.reset}`);
206
+ console.log(`${colors.dim}Restart Claude Code to see changes.${colors.reset}`);
207
+ }
208
+ process.exit(0);
209
+ }
210
+
211
+ // Default: interactive mode
212
+ await interactiveSetup();
213
+ }
214
+
215
+ main().catch(err => {
216
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
217
+ process.exit(1);
218
+ });
@@ -116,6 +116,25 @@ async function run(options = {}) {
116
116
  } catch (err) {
117
117
  // Auto-learn not available or failed - continue silently
118
118
  }
119
+
120
+ // Also capture to tech debt ledger for persistent tracking
121
+ try {
122
+ const { TechDebtManager } = require('./flow-tech-debt');
123
+ const debtManager = new TechDebtManager();
124
+ const { added, updated } = debtManager.addIssues(reportableIssues.map(issue => ({
125
+ file: issue.file,
126
+ line: issue.line,
127
+ category: issue.perspective || 'code',
128
+ severity: issue.severity === 'critical' ? 'critical' : (issue.severity === 'important' ? 'high' : 'medium'),
129
+ description: issue.description,
130
+ fix: issue.fix || ''
131
+ })));
132
+ if (added > 0 || updated > 0) {
133
+ console.log(colors.dim + ` Tech debt: ${added} new, ${updated} updated` + colors.reset);
134
+ }
135
+ } catch (err) {
136
+ // Tech debt manager not available or failed - continue silently
137
+ }
119
138
  }
120
139
 
121
140
  return {