wogiflow 2.9.0 → 2.10.0

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.
@@ -69,6 +69,18 @@ const WORKSPACE_GATES = [
69
69
  description: 'Verify changes are committed and pushed before handoff to downstream workers',
70
70
  phase: 'post',
71
71
  severity: 'error'
72
+ },
73
+ {
74
+ name: 'crossRepoEnumVerification',
75
+ description: 'Verify type/enum mappings between repos by grepping BOTH repos for actual values',
76
+ phase: 'pre',
77
+ severity: 'error'
78
+ },
79
+ {
80
+ name: 'dispatchVerification',
81
+ description: 'Verify dispatch claims have corresponding tool calls — prevents narrate-without-execute',
82
+ phase: 'post',
83
+ severity: 'error'
72
84
  }
73
85
  ];
74
86
 
@@ -538,6 +550,12 @@ function runWorkspaceGate(gateName, workspaceRoot, context, taskMeta = {}) {
538
550
  case 'deploymentReadiness':
539
551
  return gateDeploymentReadiness(workspaceRoot, context, taskMeta);
540
552
 
553
+ case 'crossRepoEnumVerification':
554
+ return gateCrossRepoEnumVerification(workspaceRoot, context, taskMeta);
555
+
556
+ case 'dispatchVerification':
557
+ return gateDispatchVerification(context, taskMeta);
558
+
541
559
  default:
542
560
  return { passed: true, message: `Unknown gate: ${gateName}`, severity: 'warning' };
543
561
  }
@@ -800,6 +818,137 @@ function runAllWorkspaceGates(workspaceRoot, context, taskMeta = {}) {
800
818
  };
801
819
  }
802
820
 
821
+ /**
822
+ * Gate: dispatchVerification
823
+ * After task completion in workspace manager mode, verify that any response
824
+ * claiming to have dispatched/sent/forwarded work to a worker actually
825
+ * contains corresponding tool calls (Bash with dispatch URL, Agent with
826
+ * worker target, etc.).
827
+ *
828
+ * Prevents the 'narrate without execute' failure class where the manager
829
+ * describes sending work but never actually sends it.
830
+ *
831
+ * Source: Manager mistake #1 (~15K tokens wasted on phantom dispatch).
832
+ */
833
+ function gateDispatchVerification(context, taskMeta) {
834
+ const gate = WORKSPACE_GATES.find(g => g.name === 'dispatchVerification');
835
+
836
+ // Only meaningful in manager mode
837
+ if (!context.currentMember || context.currentMember.role !== 'manager') {
838
+ return { passed: true, message: 'Not in manager mode, dispatch verification skipped', severity: gate.severity };
839
+ }
840
+
841
+ // Check if the task output contains dispatch-claim keywords
842
+ const dispatchKeywords = [
843
+ /\bdispatched\b/i,
844
+ /\bsent to worker\b/i,
845
+ /\bforwarded to\b/i,
846
+ /\bdelegated to\b/i,
847
+ /\brouted to worker\b/i,
848
+ /\bspawned.*agent\b/i
849
+ ];
850
+
851
+ const taskOutput = taskMeta.taskOutput ?? '';
852
+ const hasDispatchClaim = dispatchKeywords.some(kw => kw.test(taskOutput));
853
+
854
+ if (!hasDispatchClaim) {
855
+ return { passed: true, message: 'No dispatch claims in task output', severity: gate.severity };
856
+ }
857
+
858
+ // If dispatch was claimed, verify tool calls exist
859
+ const toolCalls = taskMeta.toolCalls ?? [];
860
+
861
+ // Graceful degradation: if tool call tracking is not wired up yet, skip
862
+ if (toolCalls.length === 0 && !taskMeta.toolCallsTracked) {
863
+ return { passed: true, message: 'Tool call tracking not available — dispatch verification skipped', severity: 'warning' };
864
+ }
865
+
866
+ const hasDispatchToolCall = toolCalls.some(tc =>
867
+ (tc.tool === 'Bash' && /dispatch|curl.*channel|http.*localhost.*task/i.test(tc.input ?? '')) ||
868
+ (tc.tool === 'Agent' && tc.input) ||
869
+ (tc.tool === 'Bash' && /node.*workspace-routing/i.test(tc.input ?? ''))
870
+ );
871
+
872
+ if (hasDispatchToolCall) {
873
+ return { passed: true, message: 'Dispatch claims verified — tool calls found', severity: gate.severity };
874
+ }
875
+
876
+ // Dispatch claimed but no matching tool call found
877
+ return {
878
+ passed: false,
879
+ message: `Response contains dispatch claims ("dispatched", "sent to worker", etc.) ` +
880
+ `but NO corresponding tool call was found. ` +
881
+ `This is the "narrate without execute" failure — the manager described sending work ` +
882
+ `but never actually sent it. Execute the dispatch before marking complete.`,
883
+ severity: gate.severity,
884
+ action: 'Find the dispatch claim, execute the actual dispatch tool call, then re-verify.'
885
+ };
886
+ }
887
+
888
+ /**
889
+ * Gate: crossRepoEnumVerification
890
+ * When a decision involves type/enum mapping between repos (frontend↔backend),
891
+ * mandate grep/read to BOTH repos for actual values, display side-by-side,
892
+ * THEN present the mapping to the owner.
893
+ *
894
+ * Detects keywords: 'maps to', 'rename to', 'corresponds to', 'enum', 'type mapping'.
895
+ * Source: Manager mistake #5 (DECISION-7 axis mismatch — payment cadence vs compensation model).
896
+ */
897
+ function gateCrossRepoEnumVerification(workspaceRoot, context, taskMeta) {
898
+ const gate = WORKSPACE_GATES.find(g => g.name === 'crossRepoEnumVerification');
899
+
900
+ // Only activate when workspace is active and has multiple members
901
+ if (!context.config?.members || Object.keys(context.config.members).length < 2) {
902
+ return { passed: true, message: 'Single-repo workspace, enum verification skipped', severity: gate.severity };
903
+ }
904
+
905
+ // Check if the task description or decisions contain cross-repo type mapping keywords
906
+ const mappingKeywords = [
907
+ /\bmaps?\s*to\b/i,
908
+ /\brename[sd]?\s*to\b/i,
909
+ /\bcorresponds?\s*to\b/i,
910
+ /\benum\s+mapping/i,
911
+ /\btype\s+mapping/i,
912
+ /\bfrontend.*backend.*(?:type|enum|interface)/i,
913
+ /\bbackend.*frontend.*(?:type|enum|interface)/i,
914
+ /\bclient.*server.*(?:type|enum|interface)/i
915
+ ];
916
+
917
+ const taskText = [
918
+ taskMeta.taskTitle ?? '',
919
+ taskMeta.taskDescription ?? '',
920
+ ...(taskMeta.decisions ?? [])
921
+ ].join(' ');
922
+
923
+ const hasMappingDecision = mappingKeywords.some(kw => kw.test(taskText));
924
+
925
+ if (!hasMappingDecision) {
926
+ return { passed: true, message: 'No cross-repo type/enum mapping detected', severity: gate.severity };
927
+ }
928
+
929
+ // If mapping decision detected, check if verification was performed
930
+ if (taskMeta.enumVerificationPerformed) {
931
+ return { passed: true, message: 'Cross-repo enum values verified in both repos', severity: gate.severity };
932
+ }
933
+
934
+ // Build the list of member repos for the error message
935
+ const memberNames = Object.keys(context.config.members);
936
+
937
+ return {
938
+ passed: false,
939
+ message: `Cross-repo type/enum mapping detected but not verified. ` +
940
+ `Before presenting a mapping decision, grep BOTH repos (${memberNames.join(', ')}) ` +
941
+ `for actual enum/type values and display them side-by-side. ` +
942
+ `Never allow 'A maps to B' without verified evidence of both A and B.`,
943
+ severity: gate.severity,
944
+ action: 'Dispatch a grep/read to BOTH repos for actual enum values, display side-by-side, THEN present the mapping.',
945
+ memberRepos: memberNames.map(name => ({
946
+ name,
947
+ path: path.resolve(workspaceRoot, context.config.members[name].path)
948
+ }))
949
+ };
950
+ }
951
+
803
952
  // ============================================================
804
953
  // Exports
805
954
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -187,7 +187,13 @@ const CONFIG_DEFAULTS = {
187
187
  implementationGate: { enabled: true },
188
188
  todoWriteGate: { enabled: true, blockImplementationWithoutTask: true },
189
189
  routingGate: { enabled: true },
190
- loopEnforcement: { enabled: true }
190
+ loopEnforcement: { enabled: true },
191
+ hypothesisGate: {
192
+ enabled: true,
193
+ _comment_hypothesisGate: 'Blocks premature "fixed"/"should work" claims during bug investigation until hypothesis is verified. Pattern: hypothesis → verify → confirm → communicate.',
194
+ blockedPhrases: ['fixed', 'should work', 'go try', 'go refresh'],
195
+ requireExplicitVerification: true
196
+ }
191
197
  },
192
198
 
193
199
  // --- Execution ---
@@ -167,10 +167,12 @@ function extractCriteriaCount(specContent) {
167
167
 
168
168
  let count = 0;
169
169
 
170
- // Count Given/When/Then patterns (use [\s\S] to match across newlines)
171
- const gwtMatches = specContent.match(/\bGiven\b[\s\S]*?\bWhen\b[\s\S]*?\bThen\b/gi);
172
- if (gwtMatches) {
173
- count += gwtMatches.length;
170
+ // Count Given/When/Then by individual keyword minimum (avoids ReDoS from nested [\s\S]*?)
171
+ const givenCount = (specContent.match(/\bGiven\b/gi) || []).length;
172
+ const whenCount = (specContent.match(/\bWhen\b/gi) || []).length;
173
+ const thenCount = (specContent.match(/\bThen\b/gi) || []).length;
174
+ if (givenCount > 0 && whenCount > 0 && thenCount > 0) {
175
+ count += Math.min(givenCount, whenCount, thenCount);
174
176
  }
175
177
 
176
178
  // Count numbered acceptance criteria
@@ -284,6 +286,26 @@ function estimateTaskContextNeeds(task, configOverride = null) {
284
286
  estimate += est.refactorBuffer;
285
287
  }
286
288
 
289
+ // Blast-radius buffer — factor in consumer impact from explore phase
290
+ // Validate task ID before path construction (security rule #4)
291
+ if (task.id && validateTaskId(task.id).valid) {
292
+ const blastRadiusPath = path.join(PATHS.state, `blast-radius-${task.id}.json`);
293
+ try {
294
+ const blastRadius = safeJsonParse(blastRadiusPath, null);
295
+ if (blastRadius && blastRadius.breakingCount > 0) {
296
+ factors.blastRadius = blastRadius.breakingCount;
297
+ // Each breaking consumer adds ~1% context (for reading + updating)
298
+ estimate += blastRadius.breakingCount * 0.01;
299
+ if (blastRadius.risk === 'HIGH') {
300
+ // HIGH risk (10+ breaking) adds extra buffer for phased migration planning
301
+ estimate += 0.05;
302
+ }
303
+ }
304
+ } catch (_err) {
305
+ // No blast-radius file — that's fine, proceed without it
306
+ }
307
+ }
308
+
287
309
  // Parent task: scale by subtask count
288
310
  if (task.type === 'parent' && task.subTasks && task.subTasks.length > 0) {
289
311
  factors.parentMultiplier = 1 + (task.subTasks.length * 0.3);
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Decision Authority Framework
5
+ *
6
+ * Config-driven classification of decisions into authority levels.
7
+ * Prevents question flooding by classifying engineering vs product decisions
8
+ * and routing them to either agent-decides or owner-decides.
9
+ *
10
+ * Source: Retrospective — manager mistake #4 (12-question flooding),
11
+ * process gap #4 (no framework for engineering vs product authority).
12
+ *
13
+ * Usage:
14
+ * node flow-decision-authority.js classify <decision-text>
15
+ * node flow-decision-authority.js batch <json-array-of-decisions>
16
+ * node flow-decision-authority.js update-category <category> <authority>
17
+ *
18
+ * Programmatic:
19
+ * const { classifyDecision, batchClassify, getAuthorityConfig } = require('./flow-decision-authority');
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+ const { PATHS, getConfig, safeJsonParse } = require('./flow-utils');
25
+
26
+ // ============================================================
27
+ // Constants
28
+ // ============================================================
29
+
30
+ const AUTHORITY_LEVELS = {
31
+ 'agent-decides': 'Agent decides autonomously, reports in completion summary',
32
+ 'agent-decides-report-after': 'Agent decides, explicitly reports the decision after',
33
+ 'owner-decides': 'Present to user, wait for answer before proceeding',
34
+ 'auto-fix-report-after': 'Agent fixes automatically, reports what was fixed'
35
+ };
36
+
37
+ const DEFAULT_AUTHORITY_CONFIG = {
38
+ engineering: 'agent-decides',
39
+ infrastructure: 'agent-decides-report-after',
40
+ productBehavior: 'owner-decides',
41
+ security: 'auto-fix-report-after',
42
+ ux: 'owner-decides',
43
+ naming: 'agent-decides',
44
+ performance: 'agent-decides-report-after',
45
+ maxOwnerQuestionsPerBatch: 5
46
+ };
47
+
48
+ /**
49
+ * Keywords and patterns for classifying decisions into categories.
50
+ * Each category has patterns that match against the decision text.
51
+ * IMPORTANT: Do NOT add the /g flag — these are used with .test() in a loop.
52
+ * Patterns use compound terms to avoid false positives from common single words.
53
+ */
54
+ const CATEGORY_PATTERNS = {
55
+ engineering: [
56
+ /\b(refactor|extract|inline|rename|move file|split|merge|consolidat)\w*/i,
57
+ /\b(import|export|module structure|dependency|circular)\b/i,
58
+ /\b(error handling|try.catch|fallback|retry|timeout)\b/i,
59
+ /\b(lint|format|code style|indentation|semicolons?)\b/i,
60
+ /\b(type system|type annotation|type hierarchy|interface|generic|abstract|class hierarchy)\b/i,
61
+ /\b(cache strategy|memoiz|lazy load|bundle|tree.shak)\w*/i
62
+ ],
63
+ infrastructure: [
64
+ /\b(docker|kubernetes|ci\/cd|pipeline|deploy|build)\b/i,
65
+ /\b(database|migration|schema|index|query optimiz)\w*/i,
66
+ /\b(aws|gcp|azure|cloud|server|hosting)\b/i,
67
+ /\b(environment|env var|config file|secret|credential)\b/i,
68
+ /\b(monitoring|logging|alerting|metrics|observability)\b/i
69
+ ],
70
+ productBehavior: [
71
+ /\b(user.facing|user experience|product behavior|workflow)\b/i,
72
+ /\b(default value|default behavior|fallback behavior)\b/i,
73
+ /\b(business logic|business rule|domain)\b/i,
74
+ /\b(permission|role|access control|authorization)\b/i,
75
+ /\b(notification|email|alert to user)\b/i,
76
+ /\b(pricing|billing|subscription|plan|tier)\b/i,
77
+ /\b(feature flag|feature toggle|product feature)\b/i
78
+ ],
79
+ security: [
80
+ /\b(security|vulnerabilit|exploit|injection|xss|csrf)\w*/i,
81
+ /\b(authentication|auth|token|session|password|hash)\b/i,
82
+ /\b(sanitiz|escap|validat|whitelist|blocklist)\w*/i,
83
+ /\b(cors|csp|header|ssl|tls|certificate)\b/i,
84
+ /\b(rate.limit|brute.force|ddos|firewall)\b/i
85
+ ],
86
+ ux: [
87
+ /\b(layout|design|color|font|spacing|margin|padding)\b/i,
88
+ /\b(animation|transition|hover|focus|accessibility)\b/i,
89
+ /\b(copy text|label|placeholder|tooltip|user message)\b/i,
90
+ /\b(navigation|menu|sidebar|header|footer)\b/i,
91
+ /\b(responsive|mobile|breakpoint|viewport)\b/i
92
+ ],
93
+ naming: [
94
+ /\b(naming convention|naming pattern|rename)\b/i,
95
+ /\b(variable name|function name|file name|class name)\b/i,
96
+ /\b(camelCase|snake_case|PascalCase|kebab-case)\b/i,
97
+ /\b(prefix|suffix|abbreviat)\w*/i
98
+ ],
99
+ performance: [
100
+ /\b(performance|speed|latency|throughput|benchmark)\b/i,
101
+ /\b(memory leak|garbage|allocation|buffer)\b/i,
102
+ /\b(batch|chunk|stream|pagina)\w*/i,
103
+ /\b(optimize|optimis|efficient|bottleneck)\w*/i
104
+ ]
105
+ };
106
+
107
+ // ============================================================
108
+ // Core Functions
109
+ // ============================================================
110
+
111
+ /**
112
+ * Get the decision authority config, merging defaults with user overrides
113
+ * @returns {Object} Authority config
114
+ */
115
+ function getAuthorityConfig() {
116
+ const config = getConfig();
117
+ const userConfig = config.decisionAuthority ?? {};
118
+ return {
119
+ ...DEFAULT_AUTHORITY_CONFIG,
120
+ ...userConfig
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Classify a decision text into a category
126
+ * @param {string} decisionText - The decision to classify
127
+ * @returns {{ category: string, authority: string, confidence: string }}
128
+ */
129
+ function classifyDecision(decisionText) {
130
+ const authorityConfig = getAuthorityConfig();
131
+ // Patterns already use /i flag, no need for toLowerCase()
132
+ const text = decisionText;
133
+
134
+ // Score each category
135
+ const scores = {};
136
+ for (const [category, patterns] of Object.entries(CATEGORY_PATTERNS)) {
137
+ scores[category] = 0;
138
+ for (const pattern of patterns) {
139
+ if (pattern.test(text)) {
140
+ scores[category]++;
141
+ }
142
+ }
143
+ }
144
+
145
+ // Find highest scoring category
146
+ let bestCategory = 'productBehavior'; // Default to owner-decides (safest)
147
+ let bestScore = 0;
148
+
149
+ for (const [category, score] of Object.entries(scores)) {
150
+ if (score > bestScore) {
151
+ bestScore = score;
152
+ bestCategory = category;
153
+ }
154
+ }
155
+
156
+ // Determine confidence
157
+ const totalMatches = Object.values(scores).reduce((a, b) => a + b, 0);
158
+ let confidence = 'low';
159
+ if (bestScore >= 3) confidence = 'high';
160
+ else if (bestScore >= 2) confidence = 'medium';
161
+ else if (bestScore === 1 && totalMatches <= 2) confidence = 'medium';
162
+
163
+ // Low-confidence decisions default to owner-decides for safety
164
+ const authority = confidence === 'low'
165
+ ? 'owner-decides'
166
+ : (authorityConfig[bestCategory] ?? 'owner-decides');
167
+
168
+ return {
169
+ category: bestCategory,
170
+ authority,
171
+ confidence,
172
+ score: bestScore,
173
+ description: AUTHORITY_LEVELS[authority] ?? 'Unknown authority level'
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Classify a batch of decisions and enforce maxOwnerQuestionsPerBatch
179
+ * @param {string[]} decisions - Array of decision texts
180
+ * @returns {{ classified: Object[], ownerQuestions: Object[], agentDecisions: Object[], truncated: boolean }}
181
+ */
182
+ function batchClassify(decisions) {
183
+ const authorityConfig = getAuthorityConfig();
184
+ const maxOwner = authorityConfig.maxOwnerQuestionsPerBatch ?? 5;
185
+
186
+ const classified = decisions.map((text, idx) => ({
187
+ index: idx,
188
+ text,
189
+ ...classifyDecision(text)
190
+ }));
191
+
192
+ // Separate owner-decides from agent-decides
193
+ const ownerQuestions = classified.filter(d => d.authority === 'owner-decides');
194
+ const agentDecisions = classified.filter(d => d.authority !== 'owner-decides');
195
+
196
+ // Enforce max owner questions — overflow becomes agent-decides-report-after
197
+ let truncated = false;
198
+ if (ownerQuestions.length > maxOwner) {
199
+ truncated = true;
200
+ // Keep the first maxOwner, downgrade the rest
201
+ const overflow = ownerQuestions.splice(maxOwner);
202
+ for (const decision of overflow) {
203
+ decision.authority = 'agent-decides-report-after';
204
+ decision.description = AUTHORITY_LEVELS['agent-decides-report-after'];
205
+ decision.downgraded = true;
206
+ decision.downgradeReason = `Exceeded maxOwnerQuestionsPerBatch (${maxOwner})`;
207
+ agentDecisions.push(decision);
208
+ }
209
+ }
210
+
211
+ return {
212
+ classified,
213
+ ownerQuestions,
214
+ agentDecisions,
215
+ truncated,
216
+ maxOwner,
217
+ stats: {
218
+ total: decisions.length,
219
+ ownerDecides: ownerQuestions.length,
220
+ agentDecides: agentDecisions.length,
221
+ downgraded: truncated ? agentDecisions.filter(d => d.downgraded).length : 0
222
+ }
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Update a category's authority level in config.json
228
+ * @param {string} category - Category name
229
+ * @param {string} authority - New authority level
230
+ * @returns {{ success: boolean, message: string }}
231
+ */
232
+ function updateCategoryAuthority(category, authority) {
233
+ if (!CATEGORY_PATTERNS[category] && category !== 'maxOwnerQuestionsPerBatch') {
234
+ return { success: false, message: `Unknown category: ${category}. Valid: ${Object.keys(CATEGORY_PATTERNS).join(', ')}` };
235
+ }
236
+ if (category !== 'maxOwnerQuestionsPerBatch' && !AUTHORITY_LEVELS[authority]) {
237
+ return { success: false, message: `Unknown authority: ${authority}. Valid: ${Object.keys(AUTHORITY_LEVELS).join(', ')}` };
238
+ }
239
+
240
+ try {
241
+ const configPath = path.join(PATHS.workflow, 'config.json');
242
+ const config = safeJsonParse(configPath, {});
243
+
244
+ if (!config.decisionAuthority) {
245
+ config.decisionAuthority = {};
246
+ }
247
+
248
+ if (category === 'maxOwnerQuestionsPerBatch') {
249
+ const num = parseInt(authority, 10);
250
+ if (isNaN(num) || num < 1 || num > 20) {
251
+ return { success: false, message: 'maxOwnerQuestionsPerBatch must be a number between 1 and 20' };
252
+ }
253
+ config.decisionAuthority.maxOwnerQuestionsPerBatch = num;
254
+ } else {
255
+ config.decisionAuthority[category] = authority;
256
+ }
257
+
258
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
259
+ return { success: true, message: `Updated ${category} → ${authority}` };
260
+ } catch (err) {
261
+ return { success: false, message: `Failed to update config: ${err.message}` };
262
+ }
263
+ }
264
+
265
+ // ============================================================
266
+ // CLI Interface
267
+ // ============================================================
268
+
269
+ function main() {
270
+ const [,, command, ...args] = process.argv;
271
+
272
+ switch (command) {
273
+ case 'classify': {
274
+ const text = args.join(' ');
275
+ if (!text) {
276
+ console.error('Usage: flow-decision-authority.js classify <decision-text>');
277
+ process.exit(1);
278
+ }
279
+ const result = classifyDecision(text);
280
+ console.log(JSON.stringify(result, null, 2));
281
+ break;
282
+ }
283
+
284
+ case 'batch': {
285
+ const jsonInput = args[0];
286
+ if (!jsonInput) {
287
+ console.error('Usage: flow-decision-authority.js batch <json-array>');
288
+ process.exit(1);
289
+ }
290
+ try {
291
+ const decisions = JSON.parse(jsonInput);
292
+ if (!Array.isArray(decisions)) {
293
+ console.error('Input must be a JSON array of decision strings');
294
+ process.exit(1);
295
+ }
296
+ const result = batchClassify(decisions);
297
+ console.log(JSON.stringify(result, null, 2));
298
+ } catch (err) {
299
+ console.error(`Invalid JSON: ${err.message}`);
300
+ process.exit(1);
301
+ }
302
+ break;
303
+ }
304
+
305
+ case 'update-category': {
306
+ const [category, authority] = args;
307
+ if (!category || !authority) {
308
+ console.error('Usage: flow-decision-authority.js update-category <category> <authority>');
309
+ process.exit(1);
310
+ }
311
+ const result = updateCategoryAuthority(category, authority);
312
+ if (result.success) {
313
+ console.log(result.message);
314
+ } else {
315
+ console.error(result.message);
316
+ process.exit(1);
317
+ }
318
+ break;
319
+ }
320
+
321
+ case 'config': {
322
+ const config = getAuthorityConfig();
323
+ console.log(JSON.stringify(config, null, 2));
324
+ break;
325
+ }
326
+
327
+ default:
328
+ console.log('Wogi Flow - Decision Authority Framework');
329
+ console.log('');
330
+ console.log('Commands:');
331
+ console.log(' classify <text> Classify a decision');
332
+ console.log(' batch <json-array> Classify multiple decisions');
333
+ console.log(' update-category <cat> <authority> Update a category authority');
334
+ console.log(' config Show current config');
335
+ console.log('');
336
+ console.log('Authority levels:');
337
+ for (const [level, desc] of Object.entries(AUTHORITY_LEVELS)) {
338
+ console.log(` ${level}: ${desc}`);
339
+ }
340
+ }
341
+ }
342
+
343
+ // ============================================================
344
+ // Exports
345
+ // ============================================================
346
+
347
+ module.exports = {
348
+ classifyDecision,
349
+ batchClassify,
350
+ getAuthorityConfig,
351
+ updateCategoryAuthority,
352
+ AUTHORITY_LEVELS,
353
+ DEFAULT_AUTHORITY_CONFIG,
354
+ CATEGORY_PATTERNS
355
+ };
356
+
357
+ if (require.main === module) {
358
+ main();
359
+ }