wogiflow 2.4.2 → 2.4.4

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 (210) hide show
  1. package/.claude/commands/wogi-start.md +124 -0
  2. package/.claude/docs/claude-code-compatibility.md +51 -0
  3. package/.claude/docs/explore-agents.md +11 -0
  4. package/.claude/settings.json +12 -1
  5. package/.workflow/models/registry.json +1 -1
  6. package/bin/flow +11 -1
  7. package/lib/workspace-contracts.js +599 -0
  8. package/lib/workspace-intelligence.js +600 -0
  9. package/lib/workspace-messages.js +441 -0
  10. package/lib/workspace-routing.js +485 -0
  11. package/lib/workspace-sync.js +339 -0
  12. package/lib/workspace.js +1073 -0
  13. package/package.json +4 -4
  14. package/scripts/MEMORY-ARCHITECTURE.md +1 -1
  15. package/scripts/base-workflow-step.js +136 -0
  16. package/scripts/flow-adaptive-learning.js +8 -9
  17. package/scripts/flow-aggregate.js +11 -6
  18. package/scripts/flow-api-index.js +4 -6
  19. package/scripts/flow-assumption-detector.js +0 -2
  20. package/scripts/flow-audit.js +15 -2
  21. package/scripts/flow-auto-context.js +8 -12
  22. package/scripts/flow-auto-learn.js +49 -49
  23. package/scripts/flow-background.js +5 -6
  24. package/scripts/flow-bridge-state.js +8 -10
  25. package/scripts/flow-bulk-loop.js +1 -3
  26. package/scripts/flow-bulk-orchestrator.js +1 -3
  27. package/scripts/flow-cascade-completion.js +0 -2
  28. package/scripts/flow-cascade.js +4 -4
  29. package/scripts/flow-checkpoint.js +10 -13
  30. package/scripts/flow-code-intelligence.js +10 -12
  31. package/scripts/flow-community-sync.js +4 -4
  32. package/scripts/flow-community.js +12 -20
  33. package/scripts/flow-config-defaults.js +28 -2
  34. package/scripts/flow-config-interactive.js +9 -5
  35. package/scripts/flow-config-loader.js +49 -92
  36. package/scripts/flow-config-substitution.js +0 -2
  37. package/scripts/flow-context-estimator.js +4 -4
  38. package/scripts/flow-context-init.js +10 -12
  39. package/scripts/flow-context-manager.js +0 -2
  40. package/scripts/flow-context-scoring.js +2 -2
  41. package/scripts/flow-contract-scan.js +6 -9
  42. package/scripts/flow-correct.js +29 -27
  43. package/scripts/flow-correction-detector.js +5 -1
  44. package/scripts/flow-damage-control.js +47 -54
  45. package/scripts/flow-decisions-merge.js +4 -14
  46. package/scripts/flow-diff.js +5 -8
  47. package/scripts/flow-done-gates.js +786 -0
  48. package/scripts/flow-done-report.js +123 -0
  49. package/scripts/flow-done.js +71 -717
  50. package/scripts/flow-entropy-monitor.js +1 -3
  51. package/scripts/flow-eval-calibration.js +257 -0
  52. package/scripts/flow-eval-judge.js +10 -1
  53. package/scripts/flow-eval.js +14 -5
  54. package/scripts/flow-extraction-review.js +1 -0
  55. package/scripts/flow-failure-categories.js +0 -2
  56. package/scripts/flow-figma-confirm.js +5 -9
  57. package/scripts/flow-figma-generate.js +8 -10
  58. package/scripts/flow-figma-index.js +8 -10
  59. package/scripts/flow-figma-match.js +3 -5
  60. package/scripts/flow-figma-mcp-server.js +2 -4
  61. package/scripts/flow-figma-orchestrator.js +2 -3
  62. package/scripts/flow-figma-registry.js +2 -3
  63. package/scripts/flow-framework-resolver.js +0 -2
  64. package/scripts/flow-function-index.js +4 -6
  65. package/scripts/flow-gate-confidence.js +2 -2
  66. package/scripts/flow-gitignore.js +0 -2
  67. package/scripts/flow-guided-edit.js +5 -6
  68. package/scripts/flow-health.js +5 -6
  69. package/scripts/flow-hook-errors.js +6 -0
  70. package/scripts/flow-hook-status.js +263 -0
  71. package/scripts/flow-hooks.js +17 -29
  72. package/scripts/flow-http-client.js +9 -8
  73. package/scripts/flow-hybrid-interactive.js +7 -12
  74. package/scripts/flow-hybrid-test.js +12 -13
  75. package/scripts/flow-instruction-richness.js +1 -1
  76. package/scripts/flow-io.js +21 -4
  77. package/scripts/flow-knowledge-router.js +9 -3
  78. package/scripts/flow-learning-orchestrator.js +318 -13
  79. package/scripts/flow-links.js +5 -7
  80. package/scripts/flow-long-input-association.js +275 -0
  81. package/scripts/flow-long-input-chunking.js +1 -0
  82. package/scripts/flow-long-input-cli.js +0 -2
  83. package/scripts/flow-long-input-complexity.js +0 -2
  84. package/scripts/flow-long-input-constants.js +0 -2
  85. package/scripts/flow-long-input-contradictions.js +351 -0
  86. package/scripts/flow-long-input-detection.js +0 -2
  87. package/scripts/flow-long-input-passes.js +885 -0
  88. package/scripts/flow-long-input-stories.js +1 -1
  89. package/scripts/flow-long-input-voice.js +0 -2
  90. package/scripts/flow-long-input.js +425 -3005
  91. package/scripts/flow-loop-retry-learning.js +2 -3
  92. package/scripts/flow-lsp.js +3 -3
  93. package/scripts/flow-mcp-docs.js +3 -4
  94. package/scripts/flow-memory-db.js +6 -8
  95. package/scripts/flow-memory-sync.js +18 -11
  96. package/scripts/flow-metrics.js +1 -2
  97. package/scripts/flow-model-adapter.js +2 -3
  98. package/scripts/flow-model-config.js +72 -104
  99. package/scripts/flow-model-router.js +2 -2
  100. package/scripts/flow-model-types.js +0 -2
  101. package/scripts/flow-multi-approach.js +5 -6
  102. package/scripts/flow-orchestrate-context.js +3 -7
  103. package/scripts/flow-orchestrate-rollback.js +3 -8
  104. package/scripts/flow-orchestrate-state.js +8 -14
  105. package/scripts/flow-orchestrate-templates.js +2 -6
  106. package/scripts/flow-orchestrate-validator.js +5 -9
  107. package/scripts/flow-orchestrate.js +126 -103
  108. package/scripts/flow-output.js +0 -2
  109. package/scripts/flow-parallel.js +1 -1
  110. package/scripts/flow-paths.js +23 -2
  111. package/scripts/flow-pattern-enforcer.js +30 -28
  112. package/scripts/flow-pattern-extractor.js +3 -4
  113. package/scripts/flow-pending.js +0 -2
  114. package/scripts/flow-permissions.js +2 -3
  115. package/scripts/flow-plugin-registry.js +10 -12
  116. package/scripts/flow-prd-manager.js +1 -1
  117. package/scripts/flow-progress.js +7 -9
  118. package/scripts/flow-prompt-composer.js +3 -3
  119. package/scripts/flow-prompt-template.js +2 -2
  120. package/scripts/flow-providers.js +7 -4
  121. package/scripts/flow-registry-manager.js +7 -12
  122. package/scripts/flow-regression.js +9 -11
  123. package/scripts/flow-roadmap.js +2 -2
  124. package/scripts/flow-run-trace.js +16 -15
  125. package/scripts/flow-safety.js +2 -5
  126. package/scripts/flow-scanner-base.js +5 -7
  127. package/scripts/flow-scenario-engine.js +1 -5
  128. package/scripts/flow-security.js +29 -0
  129. package/scripts/flow-session-end.js +32 -41
  130. package/scripts/flow-session-learning.js +53 -49
  131. package/scripts/flow-setup-hooks.js +2 -3
  132. package/scripts/flow-skill-create.js +7 -12
  133. package/scripts/flow-skill-generator.js +12 -16
  134. package/scripts/flow-skill-learn.js +17 -8
  135. package/scripts/flow-skill-matcher.js +1 -2
  136. package/scripts/flow-spec-generator.js +2 -4
  137. package/scripts/flow-stack-wizard.js +5 -7
  138. package/scripts/flow-standards-learner.js +35 -16
  139. package/scripts/flow-start.js +2 -0
  140. package/scripts/flow-stats-collector.js +2 -2
  141. package/scripts/flow-status.js +10 -10
  142. package/scripts/flow-statusline-setup.js +2 -2
  143. package/scripts/flow-step-changelog.js +2 -3
  144. package/scripts/flow-step-comments.js +66 -81
  145. package/scripts/flow-step-complexity.js +50 -70
  146. package/scripts/flow-step-coverage.js +3 -5
  147. package/scripts/flow-step-knowledge.js +2 -3
  148. package/scripts/flow-step-pr-tests.js +64 -74
  149. package/scripts/flow-step-regression.js +3 -5
  150. package/scripts/flow-step-review.js +86 -103
  151. package/scripts/flow-step-security.js +111 -121
  152. package/scripts/flow-step-silent-failures.js +56 -83
  153. package/scripts/flow-step-simplifier.js +52 -70
  154. package/scripts/flow-story.js +4 -7
  155. package/scripts/flow-strict-adherence.js +3 -4
  156. package/scripts/flow-task-checkpoint.js +36 -5
  157. package/scripts/flow-task-enforcer.js +2 -24
  158. package/scripts/flow-tech-debt.js +1 -1
  159. package/scripts/flow-template-extractor.js +1 -0
  160. package/scripts/flow-templates.js +11 -13
  161. package/scripts/flow-test-api.js +9 -13
  162. package/scripts/flow-test-discovery.js +1 -1
  163. package/scripts/flow-test-generate.js +5 -9
  164. package/scripts/flow-test-integrity.js +3 -7
  165. package/scripts/flow-test-ui.js +5 -9
  166. package/scripts/flow-testing-deps.js +1 -3
  167. package/scripts/flow-tiered-learning.js +4 -4
  168. package/scripts/flow-todowrite-sync.js +1 -1
  169. package/scripts/flow-tokens.js +0 -2
  170. package/scripts/flow-verification-profile.js +6 -10
  171. package/scripts/flow-verify.js +12 -16
  172. package/scripts/flow-version-check.js +4 -12
  173. package/scripts/flow-webmcp-generator.js +3 -5
  174. package/scripts/flow-workflow-steps.js +0 -2
  175. package/scripts/flow-workflow.js +9 -11
  176. package/scripts/hooks/adapters/claude-code.js +31 -0
  177. package/scripts/hooks/core/config-change.js +1 -0
  178. package/scripts/hooks/core/extension-registry.js +0 -2
  179. package/scripts/hooks/core/instructions-loaded.js +1 -1
  180. package/scripts/hooks/core/observation-capture.js +5 -5
  181. package/scripts/hooks/core/phase-gate.js +5 -0
  182. package/scripts/hooks/core/post-compact.js +1 -12
  183. package/scripts/hooks/core/research-gate.js +2 -12
  184. package/scripts/hooks/core/routing-gate.js +6 -0
  185. package/scripts/hooks/core/task-completed.js +12 -0
  186. package/scripts/hooks/core/task-created.js +83 -0
  187. package/scripts/hooks/core/worktree-lifecycle.js +1 -1
  188. package/scripts/hooks/entry/claude-code/config-change.js +6 -29
  189. package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
  190. package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
  191. package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
  192. package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
  193. package/scripts/hooks/entry/claude-code/session-end.js +4 -28
  194. package/scripts/hooks/entry/claude-code/session-start.js +205 -243
  195. package/scripts/hooks/entry/claude-code/setup.js +8 -49
  196. package/scripts/hooks/entry/claude-code/stop.js +40 -72
  197. package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
  198. package/scripts/hooks/entry/claude-code/task-created.js +15 -0
  199. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
  200. package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
  201. package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
  202. package/scripts/hooks/entry/shared/hook-runner.js +99 -0
  203. package/scripts/hooks/entry/shared/read-stdin.js +0 -2
  204. package/scripts/postinstall.js +2 -0
  205. package/scripts/registries/api-registry.js +0 -2
  206. package/scripts/registries/component-registry.js +5 -9
  207. package/scripts/registries/contract-scanner.js +2 -9
  208. package/scripts/registries/function-registry.js +0 -2
  209. package/scripts/registries/schema-registry.js +14 -18
  210. package/scripts/registries/service-registry.js +23 -27
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * Wogi Flow - File I/O Operations
5
3
  *
@@ -510,7 +508,7 @@ async function acquireLock(filePath, options = {}) {
510
508
  if (process.env.DEBUG) {
511
509
  console.warn(`[DEBUG] Stale lock cleanup failed: ${err.message}`);
512
510
  }
513
- await new Promise(resolve => setTimeout(resolve, retryDelay));
511
+ await require('node:timers/promises').setTimeout(retryDelay);
514
512
  }
515
513
  // Try again
516
514
  continue;
@@ -521,7 +519,7 @@ async function acquireLock(filePath, options = {}) {
521
519
  const delay = exponentialBackoff
522
520
  ? retryDelay * Math.pow(2, attempt)
523
521
  : retryDelay * (attempt + 1);
524
- await new Promise(resolve => setTimeout(resolve, delay));
522
+ await require('node:timers/promises').setTimeout(delay);
525
523
  continue;
526
524
  }
527
525
  }
@@ -646,6 +644,22 @@ function cleanupStaleLocks(dirPath, staleMs = CLEANUP_LOCK_STALE_MS) {
646
644
  }
647
645
  }
648
646
 
647
+ // ============================================================
648
+ // String Sanitization (for AI context injection)
649
+ // ============================================================
650
+
651
+ /**
652
+ * Sanitize a string value before injecting into AI context.
653
+ * Strips markdown heading markers and truncates to prevent prompt manipulation.
654
+ *
655
+ * @param {string} value - Raw string from state files
656
+ * @param {number} [maxLen=200] - Maximum length
657
+ * @returns {string} Sanitized string
658
+ */
659
+ function sanitizeForContext(value, maxLen = 200) {
660
+ return String(value).replace(/^#+\s/gm, '').slice(0, maxLen);
661
+ }
662
+
649
663
  // ============================================================
650
664
  // Exports
651
665
  // ============================================================
@@ -689,4 +703,7 @@ module.exports = {
689
703
  withLock,
690
704
  withLockSync,
691
705
  cleanupStaleLocks,
706
+
707
+ // String Sanitization
708
+ sanitizeForContext,
692
709
  };
@@ -303,10 +303,16 @@ ${correction}
303
303
  `;
304
304
 
305
305
  content += entry;
306
- fs.writeFileSync(decisionsPath, content);
307
306
 
308
- // Sync to .claude/rules/ for Claude Code integration
309
- syncDecisionsToRules();
307
+ // Route through orchestrator for locking and dedup
308
+ try {
309
+ const { writeToDecisions } = require('./flow-learning-orchestrator');
310
+ await writeToDecisions({ content, entryText: correction.slice(0, 100), caller: 'flow-knowledge-router', skipDedup: false, syncRules: true });
311
+ } catch (_err) {
312
+ // Fallback to direct write if orchestrator unavailable
313
+ fs.writeFileSync(decisionsPath, content);
314
+ syncDecisionsToRules();
315
+ }
310
316
 
311
317
  return {
312
318
  success: true,
@@ -1,10 +1,13 @@
1
- 'use strict';
2
-
3
1
  /**
4
- * Learning Orchestrator Facade
2
+ * Learning Orchestrator — Centralized Write Mediator
3
+ *
4
+ * ALL writes to feedback-patterns.md and decisions.md MUST go through this module.
5
+ * Direct fs.writeFileSync to these files from learning modules is prohibited.
5
6
  *
6
- * Coordinates all learning pipeline modules with a unified API.
7
- * Existing direct imports continue working — this facade is additive.
7
+ * Features:
8
+ * - Centralized write API with dedup checking
9
+ * - Write locking via acquireLock to prevent race conditions
10
+ * - Fuzzy dedup: rejects writes that duplicate existing entries
8
11
  *
9
12
  * Sub-modules (lazy-loaded to avoid startup cost):
10
13
  * - flow-skill-learn.js — Skill discovery and learning
@@ -20,6 +23,16 @@
20
23
  * - flow-standards-learner.js — Standards violation learning
21
24
  */
22
25
 
26
+ const fs = require('node:fs');
27
+ const { PATHS } = require('./flow-paths');
28
+ const { acquireLock, readFile, writeFile, fileExists } = require('./flow-io');
29
+
30
+ // ============================================================
31
+ // Constants
32
+ // ============================================================
33
+
34
+ const DEDUP_SIMILARITY_THRESHOLD = 0.85; // 85% similarity = duplicate
35
+
23
36
  // ============================================================
24
37
  // Lazy Loaders
25
38
  // ============================================================
@@ -36,6 +49,287 @@ function getFailureLearning() { return require('./flow-failure-learning'); }
36
49
  function getAutoLearn() { return require('./flow-auto-learn'); }
37
50
  function getStandardsLearner() { return require('./flow-standards-learner'); }
38
51
 
52
+ // ============================================================
53
+ // Dedup Utilities
54
+ // ============================================================
55
+
56
+ /**
57
+ * Normalize text for dedup comparison: lowercase, strip punctuation, collapse whitespace.
58
+ * @param {string} text
59
+ * @returns {string}
60
+ */
61
+ function normalizeForDedup(text) {
62
+ if (!text || typeof text !== 'string') return '';
63
+ return text
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9\s]/g, ' ')
66
+ .replace(/\s+/g, ' ')
67
+ .trim();
68
+ }
69
+
70
+ /**
71
+ * Compute bigram similarity between two strings (Dice coefficient).
72
+ * Returns 0..1 where 1 = identical.
73
+ * @param {string} a
74
+ * @param {string} b
75
+ * @returns {number}
76
+ */
77
+ function bigramSimilarity(a, b) {
78
+ const na = normalizeForDedup(a);
79
+ const nb = normalizeForDedup(b);
80
+
81
+ if (na === nb) return 1.0;
82
+ if (na.length < 2 || nb.length < 2) return 0;
83
+
84
+ const bigramsA = new Set();
85
+ for (let i = 0; i < na.length - 1; i++) bigramsA.add(na.slice(i, i + 2));
86
+ const bigramsB = new Set();
87
+ for (let i = 0; i < nb.length - 1; i++) bigramsB.add(nb.slice(i, i + 2));
88
+
89
+ let intersection = 0;
90
+ for (const bg of bigramsA) {
91
+ if (bigramsB.has(bg)) intersection++;
92
+ }
93
+
94
+ return (2 * intersection) / (bigramsA.size + bigramsB.size);
95
+ }
96
+
97
+ /**
98
+ * Check if an entry already exists in file content (fuzzy match).
99
+ * Checks both exact substring and bigram similarity against existing entries.
100
+ *
101
+ * @param {string} content - Current file content
102
+ * @param {string} entryText - The new entry text to check
103
+ * @returns {{ isDuplicate: boolean, matchedText?: string, similarity?: number }}
104
+ */
105
+ function checkDuplicate(content, entryText) {
106
+ if (!content || !entryText) return { isDuplicate: false };
107
+
108
+ const normalizedEntry = normalizeForDedup(entryText);
109
+ if (!normalizedEntry || normalizedEntry.length < 5) return { isDuplicate: false };
110
+
111
+ // Exact substring check (case-insensitive)
112
+ if (content.toLowerCase().includes(normalizedEntry)) {
113
+ return { isDuplicate: true, matchedText: entryText, similarity: 1.0 };
114
+ }
115
+
116
+ // Extract existing entries from tables (pipe-delimited rows) and headings
117
+ const lines = content.split('\n');
118
+ for (const line of lines) {
119
+ const trimmed = line.trim();
120
+
121
+ // Skip header rows and separators
122
+ if (!trimmed || trimmed.startsWith('|---') || trimmed.startsWith('| Date')) continue;
123
+
124
+ // Table row: extract content cells
125
+ if (trimmed.startsWith('|')) {
126
+ const cells = trimmed.split('|').slice(1, -1).map(c => c.trim());
127
+ // Check cells 1 and 2 (pattern/correction columns)
128
+ for (let i = 1; i < Math.min(cells.length, 3); i++) {
129
+ const sim = bigramSimilarity(cells[i], entryText);
130
+ if (sim >= DEDUP_SIMILARITY_THRESHOLD) {
131
+ return { isDuplicate: true, matchedText: cells[i], similarity: sim };
132
+ }
133
+ }
134
+ }
135
+
136
+ // Heading: ### Pattern Name
137
+ if (trimmed.startsWith('###')) {
138
+ const heading = trimmed.replace(/^###\s*/, '').replace(/\s*\([\d-]+\)$/, '');
139
+ const sim = bigramSimilarity(heading, entryText);
140
+ if (sim >= DEDUP_SIMILARITY_THRESHOLD) {
141
+ return { isDuplicate: true, matchedText: heading, similarity: sim };
142
+ }
143
+ }
144
+ }
145
+
146
+ return { isDuplicate: false };
147
+ }
148
+
149
+ // ============================================================
150
+ // Centralized Write API
151
+ // ============================================================
152
+
153
+ /**
154
+ * Write to feedback-patterns.md through the orchestrator.
155
+ * Acquires a lock, checks for duplicates, then applies the write.
156
+ *
157
+ * @param {Object} params
158
+ * @param {string} params.content - Full new content to write (replaces file)
159
+ * @param {string} [params.entryText] - Key text of the new entry (for dedup)
160
+ * @param {string} params.caller - Calling module name (for logging)
161
+ * @param {boolean} [params.skipDedup=false] - Skip dedup check (for bulk rewrites)
162
+ * @returns {Promise<{ success: boolean, reason?: string }>}
163
+ */
164
+ async function writeToFeedbackPatterns({ content, entryText, caller, skipDedup = false }) {
165
+ const filePath = PATHS.feedbackPatterns;
166
+
167
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
168
+ try {
169
+ // Dedup check
170
+ if (!skipDedup && entryText) {
171
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
172
+ const dupCheck = checkDuplicate(currentContent, entryText);
173
+ if (dupCheck.isDuplicate) {
174
+ if (process.env.DEBUG) {
175
+ console.log(`[orchestrator] Dedup rejected write from ${caller}: "${entryText}" matches "${dupCheck.matchedText}" (${(dupCheck.similarity * 100).toFixed(0)}%)`);
176
+ }
177
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText, similarity: dupCheck.similarity };
178
+ }
179
+ }
180
+
181
+ writeFile(filePath, content);
182
+ if (process.env.DEBUG) {
183
+ console.log(`[orchestrator] ${caller} wrote to feedback-patterns.md`);
184
+ }
185
+ return { success: true };
186
+ } catch (err) {
187
+ return { success: false, reason: err.message };
188
+ } finally {
189
+ release();
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Write to decisions.md through the orchestrator.
195
+ * Acquires a lock, checks for duplicates, then applies the write.
196
+ *
197
+ * @param {Object} params
198
+ * @param {string} params.content - Full new content to write (replaces file)
199
+ * @param {string} [params.entryText] - Key text of the new entry (for dedup)
200
+ * @param {string} params.caller - Calling module name (for logging)
201
+ * @param {boolean} [params.skipDedup=false] - Skip dedup check (for bulk rewrites)
202
+ * @param {boolean} [params.syncRules=false] - Trigger rules sync after write
203
+ * @returns {Promise<{ success: boolean, reason?: string }>}
204
+ */
205
+ async function writeToDecisions({ content, entryText, caller, skipDedup = false, syncRules = false }) {
206
+ const filePath = PATHS.decisions;
207
+
208
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
209
+ try {
210
+ // Dedup check
211
+ if (!skipDedup && entryText) {
212
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
213
+ const dupCheck = checkDuplicate(currentContent, entryText);
214
+ if (dupCheck.isDuplicate) {
215
+ if (process.env.DEBUG) {
216
+ console.log(`[orchestrator] Dedup rejected write from ${caller}: "${entryText}" matches "${dupCheck.matchedText}" (${(dupCheck.similarity * 100).toFixed(0)}%)`);
217
+ }
218
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText, similarity: dupCheck.similarity };
219
+ }
220
+ }
221
+
222
+ writeFile(filePath, content);
223
+ if (process.env.DEBUG) {
224
+ console.log(`[orchestrator] ${caller} wrote to decisions.md`);
225
+ }
226
+
227
+ // Optionally sync rules after write
228
+ if (syncRules) {
229
+ try {
230
+ require('./flow-rules-sync');
231
+ } catch (_err) {
232
+ // Non-fatal — rules sync is optional
233
+ }
234
+ }
235
+
236
+ return { success: true };
237
+ } catch (err) {
238
+ return { success: false, reason: err.message };
239
+ } finally {
240
+ release();
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Read-modify-write helper for feedback-patterns.md.
246
+ * Acquires lock, reads current content, calls modifier, writes result.
247
+ *
248
+ * @param {Function} modifier - (currentContent: string) => { content: string, entryText?: string }
249
+ * @param {Object} opts - { caller: string, skipDedup?: boolean }
250
+ * @returns {Promise<{ success: boolean, reason?: string }>}
251
+ */
252
+ async function modifyFeedbackPatterns(modifier, { caller, skipDedup = false } = {}) {
253
+ const filePath = PATHS.feedbackPatterns;
254
+
255
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
256
+ try {
257
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
258
+ const result = modifier(currentContent);
259
+
260
+ if (!result || !result.content) {
261
+ return { success: false, reason: 'modifier returned no content' };
262
+ }
263
+
264
+ // Dedup check
265
+ if (!skipDedup && result.entryText) {
266
+ const dupCheck = checkDuplicate(currentContent, result.entryText);
267
+ if (dupCheck.isDuplicate) {
268
+ if (process.env.DEBUG) {
269
+ console.log(`[orchestrator] Dedup rejected modify from ${caller}: "${result.entryText}" matches "${dupCheck.matchedText}"`);
270
+ }
271
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText };
272
+ }
273
+ }
274
+
275
+ writeFile(filePath, result.content);
276
+ return { success: true };
277
+ } catch (err) {
278
+ return { success: false, reason: err.message };
279
+ } finally {
280
+ release();
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Read-modify-write helper for decisions.md.
286
+ * Acquires lock, reads current content, calls modifier, writes result.
287
+ *
288
+ * @param {Function} modifier - (currentContent: string) => { content: string, entryText?: string }
289
+ * @param {Object} opts - { caller: string, skipDedup?: boolean, syncRules?: boolean }
290
+ * @returns {Promise<{ success: boolean, reason?: string }>}
291
+ */
292
+ async function modifyDecisions(modifier, { caller, skipDedup = false, syncRules = false } = {}) {
293
+ const filePath = PATHS.decisions;
294
+
295
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
296
+ try {
297
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
298
+ const result = modifier(currentContent);
299
+
300
+ if (!result || !result.content) {
301
+ return { success: false, reason: 'modifier returned no content' };
302
+ }
303
+
304
+ // Dedup check
305
+ if (!skipDedup && result.entryText) {
306
+ const dupCheck = checkDuplicate(currentContent, result.entryText);
307
+ if (dupCheck.isDuplicate) {
308
+ if (process.env.DEBUG) {
309
+ console.log(`[orchestrator] Dedup rejected modify from ${caller}: "${result.entryText}" matches "${dupCheck.matchedText}"`);
310
+ }
311
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText };
312
+ }
313
+ }
314
+
315
+ writeFile(filePath, result.content);
316
+
317
+ if (syncRules) {
318
+ try {
319
+ require('./flow-rules-sync');
320
+ } catch (_err) {
321
+ // Non-fatal
322
+ }
323
+ }
324
+
325
+ return { success: true };
326
+ } catch (err) {
327
+ return { success: false, reason: err.message };
328
+ } finally {
329
+ release();
330
+ }
331
+ }
332
+
39
333
  // ============================================================
40
334
  // Unified API
41
335
  // ============================================================
@@ -114,10 +408,10 @@ async function learn(type, context = {}) {
114
408
  function getStats() {
115
409
  const stats = {};
116
410
 
117
- try { stats.tiered = getTieredLearning().getLearningStats(); } catch (err) { stats.tiered = null; }
118
- try { stats.failure = getFailureLearning().getLearningStats(); } catch (err) { stats.failure = null; }
119
- try { stats.loop = getLoopRetryLearning().getLearningStats(); } catch (err) { stats.loop = null; }
120
- try { stats.auto = getAutoLearn().showStatus(); } catch (err) { stats.auto = null; }
411
+ try { stats.tiered = getTieredLearning().getLearningStats(); } catch (_err) { stats.tiered = null; }
412
+ try { stats.failure = getFailureLearning().getLearningStats(); } catch (_err) { stats.failure = null; }
413
+ try { stats.loop = getLoopRetryLearning().getLearningStats(); } catch (_err) { stats.loop = null; }
414
+ try { stats.auto = getAutoLearn().showStatus(); } catch (_err) { stats.auto = null; }
121
415
 
122
416
  return stats;
123
417
  }
@@ -134,19 +428,19 @@ function loadRelevantPatterns(context = {}) {
134
428
 
135
429
  try {
136
430
  patterns.decisions = getPatternEnforcer().loadDecisionPatterns();
137
- } catch (err) {
431
+ } catch (_err) {
138
432
  patterns.decisions = [];
139
433
  }
140
434
 
141
435
  try {
142
436
  patterns.skills = getPatternEnforcer().loadSkillPatterns(context.skills || []);
143
- } catch (err) {
437
+ } catch (_err) {
144
438
  patterns.skills = [];
145
439
  }
146
440
 
147
441
  try {
148
442
  patterns.tiered = getTieredLearning().getPatternsByTier(context.tier || 'all');
149
- } catch (err) {
443
+ } catch (_err) {
150
444
  patterns.tiered = [];
151
445
  }
152
446
 
@@ -158,7 +452,18 @@ function loadRelevantPatterns(context = {}) {
158
452
  // ============================================================
159
453
 
160
454
  module.exports = {
161
- // Unified API
455
+ // Centralized write API (MUST be used for all learning file writes)
456
+ writeToFeedbackPatterns,
457
+ writeToDecisions,
458
+ modifyFeedbackPatterns,
459
+ modifyDecisions,
460
+
461
+ // Dedup utilities (exposed for testing)
462
+ checkDuplicate,
463
+ bigramSimilarity,
464
+ normalizeForDedup,
465
+
466
+ // Unified learning API
162
467
  learn,
163
468
  getStats,
164
469
  loadRelevantPatterns,
@@ -25,14 +25,12 @@ const path = require('node:path');
25
25
  const https = require('node:https');
26
26
  const http = require('node:http');
27
27
  const dns = require('dns');
28
- const { getProjectRoot, colors: c, readJson } = require('./flow-utils');
28
+ const { getProjectRoot, colors: c, readJson, PATHS } = require('./flow-utils');
29
29
  const { success: printSuccess } = require('./flow-output');
30
30
 
31
- const PROJECT_ROOT = getProjectRoot();
32
- const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
33
- const LINKS_PATH = path.join(WORKFLOW_DIR, 'links.yaml');
34
- const LINKS_JSON_PATH = path.join(WORKFLOW_DIR, 'links.json');
35
- const CACHE_DIR = path.join(WORKFLOW_DIR, 'cache', 'links');
31
+ const LINKS_PATH = path.join(PATHS.workflow, 'links.yaml');
32
+ const LINKS_JSON_PATH = path.join(PATHS.workflow, 'links.json');
33
+ const CACHE_DIR = path.join(PATHS.workflow, 'cache', 'links');
36
34
 
37
35
  /**
38
36
  * Link types
@@ -357,7 +355,7 @@ async function fetchLink(name, links = null) {
357
355
  const filePath = url.startsWith('file://') ? url.slice(7) : url;
358
356
  const absPath = path.isAbsolute(filePath)
359
357
  ? filePath
360
- : path.join(PROJECT_ROOT, filePath);
358
+ : path.join(PATHS.root, filePath);
361
359
 
362
360
  if (fs.existsSync(absPath)) {
363
361
  content = fs.readFileSync(absPath, 'utf-8');