wogiflow 1.0.14 → 1.0.16

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