wogiflow 1.0.13 → 1.0.15

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.
package/lib/installer.js CHANGED
@@ -477,8 +477,14 @@ async function init(args) {
477
477
  console.log('\n✅ Wogi Flow initialized successfully!\n');
478
478
  console.log('Next steps:');
479
479
  console.log(' 1. Review .workflow/config.json');
480
- console.log(' 2. Run `./scripts/flow status` to see project status');
481
- console.log(' 3. Create your first task with `./scripts/flow story "Task title"`');
480
+ console.log(' 2. Run `/wogi-status` to see project status');
481
+ console.log(' 3. Create your first task with `/wogi-story "Task title"`');
482
+ console.log('');
483
+ console.log('Available commands:');
484
+ console.log(' /wogi-ready - View available tasks');
485
+ console.log(' /wogi-status - Project overview');
486
+ console.log(' /wogi-health - Check workflow health');
487
+ console.log(' /wogi-story - Create a new story');
482
488
  console.log('');
483
489
  }
484
490
 
package/lib/upgrader.js CHANGED
@@ -393,7 +393,7 @@ async function upgrade(args) {
393
393
  console.log('\n✅ Upgrade complete!\n');
394
394
  console.log('Next steps:');
395
395
  console.log(' 1. Review changes in .workflow/');
396
- console.log(' 2. Run `./scripts/flow health` to verify installation');
396
+ console.log(' 2. Run `/wogi-health` to verify installation');
397
397
  console.log(' 3. Commit the upgraded files');
398
398
  }
399
399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/scripts/flow CHANGED
@@ -37,9 +37,8 @@ show_help() {
37
37
  echo "Usage: flow <command> [options]"
38
38
  echo ""
39
39
  echo "Setup & Updates:"
40
- echo " install Interactive setup wizard"
41
- echo " install --quick Quick setup with defaults"
42
- echo " onboard Analyze existing project & set up context"
40
+ echo " init Setup guide (directs to AI assistant)"
41
+ echo " onboard Project onboarding (directs to AI assistant)"
43
42
  echo " migrate Migrate to universal CLI-agnostic structure"
44
43
  echo " migrate --dry-run Preview migration without changes"
45
44
  echo " update Update to latest version (preserves data)"
@@ -383,12 +382,9 @@ case "${1:-}" in
383
382
  session-end)
384
383
  node "$SCRIPT_DIR/flow-session-end.js" "${@:2}"
385
384
  ;;
386
- init)
385
+ init|install)
387
386
  "$SCRIPT_DIR/flow-init" "${@:2}"
388
387
  ;;
389
- install)
390
- "$SCRIPT_DIR/flow-install" "${@:2}"
391
- ;;
392
388
  onboard)
393
389
  "$SCRIPT_DIR/flow-onboard" "${@:2}"
394
390
  ;;
@@ -0,0 +1,475 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Context Orchestrator
5
+ *
6
+ * Enables targeted context loading for tasks using the PIN system.
7
+ * Supports orchestrator pattern where cheaper models (Haiku) gather
8
+ * relevant context for expensive models (Opus).
9
+ *
10
+ * Features:
11
+ * - PIN-based section lookup
12
+ * - Task description to relevant sections mapping
13
+ * - Token-aware context truncation
14
+ * - Product context integration
15
+ *
16
+ * Usage:
17
+ * const { getTargetedContext } = require('./flow-context-orchestrator');
18
+ * const context = await getTargetedContext({ task: "Add user auth" });
19
+ */
20
+
21
+ const path = require('path');
22
+ const {
23
+ PATHS,
24
+ fileExists,
25
+ readFile,
26
+ parseFlags,
27
+ outputJson,
28
+ info,
29
+ warn
30
+ } = require('./flow-utils');
31
+
32
+ const {
33
+ getSectionsForTask,
34
+ getSectionsByPins,
35
+ formatSectionsAsContext,
36
+ formatSectionsAsReferences
37
+ } = require('./flow-section-resolver');
38
+
39
+ // ============================================================
40
+ // Configuration
41
+ // ============================================================
42
+
43
+ // Approximate tokens per character (conservative estimate)
44
+ const CHARS_PER_TOKEN = 4;
45
+
46
+ // Default limits
47
+ const DEFAULT_MAX_TOKENS = 8000;
48
+ const DEFAULT_SECTION_LIMIT = 10;
49
+
50
+ // ============================================================
51
+ // Token Estimation
52
+ // ============================================================
53
+
54
+ /**
55
+ * Estimate token count for a string
56
+ * @param {string} text - Text to estimate
57
+ * @returns {number} - Estimated token count
58
+ */
59
+ function estimateTokens(text) {
60
+ if (!text) return 0;
61
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
62
+ }
63
+
64
+ /**
65
+ * Truncate sections to fit within token limit
66
+ * @param {Object[]} sections - Sections to truncate
67
+ * @param {number} maxTokens - Max tokens
68
+ * @returns {Object[]} - Truncated sections
69
+ */
70
+ function truncateToTokenLimit(sections, maxTokens) {
71
+ const result = [];
72
+ let currentTokens = 0;
73
+
74
+ for (const section of sections) {
75
+ const sectionTokens = estimateTokens(section.content || '');
76
+
77
+ if (currentTokens + sectionTokens <= maxTokens) {
78
+ result.push(section);
79
+ currentTokens += sectionTokens;
80
+ } else if (result.length === 0) {
81
+ // Always include at least one section, even if truncated
82
+ const availableChars = (maxTokens - currentTokens) * CHARS_PER_TOKEN;
83
+ result.push({
84
+ ...section,
85
+ content: section.content?.substring(0, availableChars) + '...[truncated]',
86
+ truncated: true
87
+ });
88
+ break;
89
+ } else {
90
+ break;
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ // ============================================================
98
+ // Section Merging
99
+ // ============================================================
100
+
101
+ /**
102
+ * Merge and deduplicate sections from multiple sources
103
+ * @param {Object[][]} sectionArrays - Arrays of sections to merge
104
+ * @returns {Object[]} - Merged and deduplicated sections
105
+ */
106
+ function mergeSections(...sectionArrays) {
107
+ const seen = new Map();
108
+
109
+ for (const sections of sectionArrays) {
110
+ for (const section of sections) {
111
+ if (!seen.has(section.id)) {
112
+ seen.set(section.id, section);
113
+ } else {
114
+ // Keep the one with higher score
115
+ const existing = seen.get(section.id);
116
+ const existingScore = existing.score || existing.matchScore || 0;
117
+ const newScore = section.score || section.matchScore || 0;
118
+ if (newScore > existingScore) {
119
+ seen.set(section.id, section);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ return Array.from(seen.values());
126
+ }
127
+
128
+ // ============================================================
129
+ // Product Context
130
+ // ============================================================
131
+
132
+ /**
133
+ * Get product context from product.md
134
+ * @param {Object} options - { format: 'full' | 'summary' }
135
+ * @returns {Object|null} - Product context or null
136
+ */
137
+ async function getProductContext(options = {}) {
138
+ const { format = 'summary' } = options;
139
+ const productPath = path.join(PATHS.specs, 'product.md');
140
+
141
+ if (!fileExists(productPath)) {
142
+ return null;
143
+ }
144
+
145
+ try {
146
+ // Get product sections via PINs
147
+ const productPins = ['product-name', 'target-users', 'value-prop', 'core-features'];
148
+ const sections = await getSectionsByPins(productPins, { limit: 5 });
149
+
150
+ if (sections.length === 0) {
151
+ // Fall back to reading the file directly
152
+ const content = readFile(productPath);
153
+ return {
154
+ context: content,
155
+ source: 'file',
156
+ tokenEstimate: estimateTokens(content)
157
+ };
158
+ }
159
+
160
+ const context = formatSectionsAsContext(sections, { format });
161
+ return {
162
+ context,
163
+ sections: sections.map(s => s.id),
164
+ source: 'pins',
165
+ tokenEstimate: estimateTokens(context)
166
+ };
167
+ } catch (err) {
168
+ warn(`Error loading product context: ${err.message}`);
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get product overview (name, tagline, type only)
175
+ * @returns {Object|null} - Brief product info
176
+ */
177
+ function getProductOverview() {
178
+ const productPath = path.join(PATHS.specs, 'product.md');
179
+
180
+ if (!fileExists(productPath)) {
181
+ return null;
182
+ }
183
+
184
+ try {
185
+ const content = readFile(productPath);
186
+
187
+ // Extract key fields using regex
188
+ const nameMatch = content.match(/\*\*Name\*\*:\s*(.+)/);
189
+ const taglineMatch = content.match(/\*\*Tagline\*\*:\s*(.+)/);
190
+ const typeMatch = content.match(/\*\*Type\*\*:\s*(.+)/);
191
+
192
+ return {
193
+ name: nameMatch ? nameMatch[1].trim() : null,
194
+ tagline: taglineMatch ? taglineMatch[1].trim() : null,
195
+ type: typeMatch ? typeMatch[1].trim() : null
196
+ };
197
+ } catch (err) {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ // ============================================================
203
+ // Main Context Gathering
204
+ // ============================================================
205
+
206
+ /**
207
+ * Get targeted context for a task
208
+ * @param {Object} options
209
+ * @param {string} options.task - Task description
210
+ * @param {string[]} options.pins - Explicit pins to include
211
+ * @param {number} options.maxTokens - Max tokens for context
212
+ * @param {string} options.format - 'full' | 'summary' | 'reference'
213
+ * @param {boolean} options.includeProduct - Include product context
214
+ * @returns {Object} - { context, sections, tokenEstimate }
215
+ */
216
+ async function getTargetedContext(options = {}) {
217
+ const {
218
+ task = '',
219
+ pins = [],
220
+ maxTokens = DEFAULT_MAX_TOKENS,
221
+ format = 'full',
222
+ includeProduct = true
223
+ } = options;
224
+
225
+ const allSections = [];
226
+
227
+ // Get sections by task description
228
+ if (task) {
229
+ const taskSections = await getSectionsForTask(task, {
230
+ limit: DEFAULT_SECTION_LIMIT
231
+ });
232
+ allSections.push(...taskSections);
233
+ }
234
+
235
+ // Get sections by explicit pins
236
+ if (pins.length > 0) {
237
+ const pinSections = await getSectionsByPins(pins, {
238
+ limit: Math.floor(DEFAULT_SECTION_LIMIT / 2)
239
+ });
240
+ allSections.push(...pinSections);
241
+ }
242
+
243
+ // Merge and deduplicate
244
+ const mergedSections = mergeSections(allSections);
245
+
246
+ // Calculate available tokens for sections
247
+ let availableTokens = maxTokens;
248
+ let productContextResult = null;
249
+
250
+ // Include product context if requested
251
+ if (includeProduct) {
252
+ productContextResult = await getProductContext({ format: 'summary' });
253
+ if (productContextResult) {
254
+ availableTokens -= productContextResult.tokenEstimate;
255
+ }
256
+ }
257
+
258
+ // Truncate sections to fit
259
+ const truncatedSections = truncateToTokenLimit(mergedSections, availableTokens);
260
+
261
+ // Format sections
262
+ const sectionsContext = formatSectionsAsContext(truncatedSections, { format });
263
+
264
+ // Combine contexts
265
+ let fullContext = '';
266
+ if (productContextResult) {
267
+ fullContext += '## Product Context\n\n' + productContextResult.context + '\n\n';
268
+ }
269
+ if (sectionsContext) {
270
+ fullContext += sectionsContext;
271
+ }
272
+
273
+ return {
274
+ context: fullContext.trim(),
275
+ sections: truncatedSections.map(s => ({
276
+ id: s.id,
277
+ score: s.score || s.matchScore || 0,
278
+ truncated: s.truncated || false
279
+ })),
280
+ productIncluded: !!productContextResult,
281
+ tokenEstimate: estimateTokens(fullContext)
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Get context for a specific task ID
287
+ * Loads task details and gathers relevant context
288
+ * @param {string} taskId - Task ID (e.g., "wf-abc123")
289
+ * @returns {Object} - Task context
290
+ */
291
+ async function getContextForTaskId(taskId) {
292
+ // Try to find task in ready.json
293
+ const readyPath = path.join(PATHS.state, 'ready.json');
294
+ if (!fileExists(readyPath)) {
295
+ return getTargetedContext({ task: taskId });
296
+ }
297
+
298
+ try {
299
+ const ready = JSON.parse(readFile(readyPath));
300
+ const allTasks = [
301
+ ...(ready.ready || []),
302
+ ...(ready.inProgress || []),
303
+ ...(ready.blocked || [])
304
+ ];
305
+
306
+ const task = allTasks.find(t =>
307
+ (typeof t === 'string' && t === taskId) ||
308
+ (typeof t === 'object' && t.id === taskId)
309
+ );
310
+
311
+ if (task && typeof task === 'object' && task.title) {
312
+ return getTargetedContext({
313
+ task: `${task.title} ${task.description || ''}`,
314
+ pins: task.tags || []
315
+ });
316
+ }
317
+ } catch (err) {
318
+ // Fall back to using taskId as description
319
+ }
320
+
321
+ return getTargetedContext({ task: taskId });
322
+ }
323
+
324
+ /**
325
+ * Get minimal context references (for orchestrator hints)
326
+ * @param {string} task - Task description
327
+ * @returns {string} - Reference string
328
+ */
329
+ async function getContextReferences(task) {
330
+ const sections = await getSectionsForTask(task, { limit: 5 });
331
+ return formatSectionsAsReferences(sections);
332
+ }
333
+
334
+ // ============================================================
335
+ // CLI
336
+ // ============================================================
337
+
338
+ async function main() {
339
+ const { args, flags } = parseFlags(process.argv.slice(2));
340
+
341
+ if (flags.help) {
342
+ console.log(`
343
+ Usage: node scripts/flow-context-orchestrator.js [command] [options]
344
+
345
+ Get targeted context for tasks using the PIN system.
346
+
347
+ Commands:
348
+ task "<description>" Get context for a task description
349
+ taskid <id> Get context for a task ID
350
+ product Get product context only
351
+ refs "<description>" Get section references only
352
+
353
+ Options:
354
+ --max-tokens <n> Max tokens for context (default: 8000)
355
+ --format <type> Output format: full, summary, reference
356
+ --no-product Exclude product context
357
+ --json Output as JSON
358
+ --help Show this help
359
+
360
+ Examples:
361
+ node scripts/flow-context-orchestrator.js task "Add user authentication"
362
+ node scripts/flow-context-orchestrator.js taskid wf-abc123 --json
363
+ node scripts/flow-context-orchestrator.js product --format summary
364
+ `);
365
+ process.exit(0);
366
+ }
367
+
368
+ const command = args[0];
369
+ const maxTokens = parseInt(flags['max-tokens']) || DEFAULT_MAX_TOKENS;
370
+ const format = flags.format || 'full';
371
+ const includeProduct = flags.product !== false;
372
+
373
+ switch (command) {
374
+ case 'task': {
375
+ const task = args.slice(1).join(' ');
376
+ if (!task) {
377
+ console.error('Usage: flow-context-orchestrator task "<description>"');
378
+ process.exit(1);
379
+ }
380
+
381
+ const result = await getTargetedContext({
382
+ task,
383
+ maxTokens,
384
+ format,
385
+ includeProduct
386
+ });
387
+
388
+ if (flags.json) {
389
+ outputJson(result);
390
+ } else {
391
+ console.log(result.context);
392
+ console.log(`\n--- ${result.sections.length} sections, ~${result.tokenEstimate} tokens ---`);
393
+ }
394
+ break;
395
+ }
396
+
397
+ case 'taskid': {
398
+ const taskId = args[1];
399
+ if (!taskId) {
400
+ console.error('Usage: flow-context-orchestrator taskid <id>');
401
+ process.exit(1);
402
+ }
403
+
404
+ const result = await getContextForTaskId(taskId);
405
+
406
+ if (flags.json) {
407
+ outputJson(result);
408
+ } else {
409
+ console.log(result.context);
410
+ console.log(`\n--- ${result.sections.length} sections, ~${result.tokenEstimate} tokens ---`);
411
+ }
412
+ break;
413
+ }
414
+
415
+ case 'product': {
416
+ const result = await getProductContext({ format });
417
+
418
+ if (!result) {
419
+ console.log('No product.md found');
420
+ process.exit(1);
421
+ }
422
+
423
+ if (flags.json) {
424
+ outputJson(result);
425
+ } else {
426
+ console.log(result.context);
427
+ }
428
+ break;
429
+ }
430
+
431
+ case 'refs': {
432
+ const task = args.slice(1).join(' ');
433
+ if (!task) {
434
+ console.error('Usage: flow-context-orchestrator refs "<description>"');
435
+ process.exit(1);
436
+ }
437
+
438
+ const refs = await getContextReferences(task);
439
+ console.log(refs || 'No relevant sections found');
440
+ break;
441
+ }
442
+
443
+ default:
444
+ console.error('Unknown command. Use --help for usage.');
445
+ process.exit(1);
446
+ }
447
+ }
448
+
449
+ // ============================================================
450
+ // Exports
451
+ // ============================================================
452
+
453
+ module.exports = {
454
+ // Main functions
455
+ getTargetedContext,
456
+ getContextForTaskId,
457
+ getContextReferences,
458
+
459
+ // Product context
460
+ getProductContext,
461
+ getProductOverview,
462
+
463
+ // Utilities
464
+ estimateTokens,
465
+ truncateToTokenLimit,
466
+ mergeSections
467
+ };
468
+
469
+ // Run if called directly
470
+ if (require.main === module) {
471
+ main().catch(err => {
472
+ console.error(`Error: ${err.message}`);
473
+ process.exit(1);
474
+ });
475
+ }