wogiflow 2.4.2 → 2.4.3

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 (196) hide show
  1. package/.claude/docs/claude-code-compatibility.md +27 -0
  2. package/.claude/settings.json +1 -1
  3. package/.workflow/models/registry.json +1 -1
  4. package/package.json +4 -4
  5. package/scripts/MEMORY-ARCHITECTURE.md +1 -1
  6. package/scripts/base-workflow-step.js +136 -0
  7. package/scripts/flow-adaptive-learning.js +8 -9
  8. package/scripts/flow-aggregate.js +11 -6
  9. package/scripts/flow-api-index.js +4 -6
  10. package/scripts/flow-assumption-detector.js +0 -2
  11. package/scripts/flow-audit.js +15 -2
  12. package/scripts/flow-auto-context.js +8 -12
  13. package/scripts/flow-auto-learn.js +49 -49
  14. package/scripts/flow-background.js +5 -6
  15. package/scripts/flow-bridge-state.js +8 -10
  16. package/scripts/flow-bulk-loop.js +1 -3
  17. package/scripts/flow-bulk-orchestrator.js +1 -3
  18. package/scripts/flow-cascade-completion.js +0 -2
  19. package/scripts/flow-cascade.js +4 -4
  20. package/scripts/flow-checkpoint.js +10 -13
  21. package/scripts/flow-code-intelligence.js +10 -12
  22. package/scripts/flow-community-sync.js +4 -4
  23. package/scripts/flow-community.js +12 -20
  24. package/scripts/flow-config-defaults.js +0 -2
  25. package/scripts/flow-config-interactive.js +9 -5
  26. package/scripts/flow-config-loader.js +49 -92
  27. package/scripts/flow-config-substitution.js +0 -2
  28. package/scripts/flow-context-estimator.js +4 -4
  29. package/scripts/flow-context-init.js +10 -12
  30. package/scripts/flow-context-manager.js +0 -2
  31. package/scripts/flow-context-scoring.js +2 -2
  32. package/scripts/flow-contract-scan.js +6 -9
  33. package/scripts/flow-correct.js +29 -27
  34. package/scripts/flow-correction-detector.js +5 -1
  35. package/scripts/flow-damage-control.js +47 -54
  36. package/scripts/flow-decisions-merge.js +4 -14
  37. package/scripts/flow-diff.js +5 -8
  38. package/scripts/flow-done-gates.js +786 -0
  39. package/scripts/flow-done-report.js +123 -0
  40. package/scripts/flow-done.js +71 -717
  41. package/scripts/flow-entropy-monitor.js +1 -3
  42. package/scripts/flow-eval.js +5 -5
  43. package/scripts/flow-extraction-review.js +1 -0
  44. package/scripts/flow-failure-categories.js +0 -2
  45. package/scripts/flow-figma-confirm.js +5 -9
  46. package/scripts/flow-figma-generate.js +8 -10
  47. package/scripts/flow-figma-index.js +8 -10
  48. package/scripts/flow-figma-match.js +3 -5
  49. package/scripts/flow-figma-mcp-server.js +2 -4
  50. package/scripts/flow-figma-orchestrator.js +2 -3
  51. package/scripts/flow-figma-registry.js +2 -3
  52. package/scripts/flow-framework-resolver.js +0 -2
  53. package/scripts/flow-function-index.js +4 -6
  54. package/scripts/flow-gate-confidence.js +2 -2
  55. package/scripts/flow-gitignore.js +0 -2
  56. package/scripts/flow-guided-edit.js +5 -6
  57. package/scripts/flow-health.js +5 -6
  58. package/scripts/flow-hook-errors.js +6 -0
  59. package/scripts/flow-hook-status.js +263 -0
  60. package/scripts/flow-hooks.js +17 -29
  61. package/scripts/flow-http-client.js +9 -8
  62. package/scripts/flow-hybrid-interactive.js +7 -12
  63. package/scripts/flow-hybrid-test.js +12 -13
  64. package/scripts/flow-instruction-richness.js +1 -1
  65. package/scripts/flow-io.js +21 -4
  66. package/scripts/flow-knowledge-router.js +9 -3
  67. package/scripts/flow-learning-orchestrator.js +318 -13
  68. package/scripts/flow-links.js +5 -7
  69. package/scripts/flow-long-input-association.js +275 -0
  70. package/scripts/flow-long-input-chunking.js +1 -0
  71. package/scripts/flow-long-input-cli.js +0 -2
  72. package/scripts/flow-long-input-complexity.js +0 -2
  73. package/scripts/flow-long-input-constants.js +0 -2
  74. package/scripts/flow-long-input-contradictions.js +351 -0
  75. package/scripts/flow-long-input-detection.js +0 -2
  76. package/scripts/flow-long-input-passes.js +885 -0
  77. package/scripts/flow-long-input-stories.js +1 -1
  78. package/scripts/flow-long-input-voice.js +0 -2
  79. package/scripts/flow-long-input.js +425 -3005
  80. package/scripts/flow-loop-retry-learning.js +2 -3
  81. package/scripts/flow-lsp.js +3 -3
  82. package/scripts/flow-mcp-docs.js +3 -4
  83. package/scripts/flow-memory-db.js +6 -8
  84. package/scripts/flow-memory-sync.js +18 -11
  85. package/scripts/flow-metrics.js +1 -2
  86. package/scripts/flow-model-adapter.js +2 -3
  87. package/scripts/flow-model-config.js +72 -104
  88. package/scripts/flow-model-router.js +2 -2
  89. package/scripts/flow-model-types.js +0 -2
  90. package/scripts/flow-multi-approach.js +5 -6
  91. package/scripts/flow-orchestrate-context.js +3 -7
  92. package/scripts/flow-orchestrate-rollback.js +3 -8
  93. package/scripts/flow-orchestrate-state.js +8 -14
  94. package/scripts/flow-orchestrate-templates.js +2 -6
  95. package/scripts/flow-orchestrate-validator.js +5 -9
  96. package/scripts/flow-orchestrate.js +126 -103
  97. package/scripts/flow-output.js +0 -2
  98. package/scripts/flow-parallel.js +1 -1
  99. package/scripts/flow-paths.js +23 -2
  100. package/scripts/flow-pattern-enforcer.js +30 -28
  101. package/scripts/flow-pattern-extractor.js +3 -4
  102. package/scripts/flow-pending.js +0 -2
  103. package/scripts/flow-permissions.js +2 -3
  104. package/scripts/flow-plugin-registry.js +10 -12
  105. package/scripts/flow-prd-manager.js +1 -1
  106. package/scripts/flow-progress.js +7 -9
  107. package/scripts/flow-prompt-composer.js +3 -3
  108. package/scripts/flow-prompt-template.js +2 -2
  109. package/scripts/flow-providers.js +7 -4
  110. package/scripts/flow-registry-manager.js +7 -12
  111. package/scripts/flow-regression.js +9 -11
  112. package/scripts/flow-roadmap.js +2 -2
  113. package/scripts/flow-run-trace.js +16 -15
  114. package/scripts/flow-safety.js +2 -5
  115. package/scripts/flow-scanner-base.js +5 -7
  116. package/scripts/flow-scenario-engine.js +1 -5
  117. package/scripts/flow-security.js +29 -0
  118. package/scripts/flow-session-end.js +32 -41
  119. package/scripts/flow-session-learning.js +53 -49
  120. package/scripts/flow-setup-hooks.js +2 -3
  121. package/scripts/flow-skill-create.js +7 -12
  122. package/scripts/flow-skill-generator.js +12 -16
  123. package/scripts/flow-skill-learn.js +17 -8
  124. package/scripts/flow-skill-matcher.js +1 -2
  125. package/scripts/flow-spec-generator.js +2 -4
  126. package/scripts/flow-stack-wizard.js +5 -7
  127. package/scripts/flow-standards-learner.js +35 -16
  128. package/scripts/flow-start.js +2 -0
  129. package/scripts/flow-stats-collector.js +2 -2
  130. package/scripts/flow-status.js +10 -10
  131. package/scripts/flow-statusline-setup.js +2 -2
  132. package/scripts/flow-step-changelog.js +2 -3
  133. package/scripts/flow-step-comments.js +66 -81
  134. package/scripts/flow-step-complexity.js +50 -70
  135. package/scripts/flow-step-coverage.js +3 -5
  136. package/scripts/flow-step-knowledge.js +2 -3
  137. package/scripts/flow-step-pr-tests.js +64 -74
  138. package/scripts/flow-step-regression.js +3 -5
  139. package/scripts/flow-step-review.js +86 -103
  140. package/scripts/flow-step-security.js +111 -121
  141. package/scripts/flow-step-silent-failures.js +56 -83
  142. package/scripts/flow-step-simplifier.js +52 -70
  143. package/scripts/flow-story.js +4 -7
  144. package/scripts/flow-strict-adherence.js +3 -4
  145. package/scripts/flow-task-checkpoint.js +36 -5
  146. package/scripts/flow-task-enforcer.js +2 -24
  147. package/scripts/flow-tech-debt.js +1 -1
  148. package/scripts/flow-template-extractor.js +1 -0
  149. package/scripts/flow-templates.js +11 -13
  150. package/scripts/flow-test-api.js +9 -13
  151. package/scripts/flow-test-discovery.js +1 -1
  152. package/scripts/flow-test-generate.js +5 -9
  153. package/scripts/flow-test-integrity.js +3 -7
  154. package/scripts/flow-test-ui.js +5 -9
  155. package/scripts/flow-testing-deps.js +1 -3
  156. package/scripts/flow-tiered-learning.js +4 -4
  157. package/scripts/flow-todowrite-sync.js +1 -1
  158. package/scripts/flow-tokens.js +0 -2
  159. package/scripts/flow-verification-profile.js +6 -10
  160. package/scripts/flow-verify.js +12 -16
  161. package/scripts/flow-version-check.js +4 -12
  162. package/scripts/flow-webmcp-generator.js +3 -5
  163. package/scripts/flow-workflow-steps.js +0 -2
  164. package/scripts/flow-workflow.js +9 -11
  165. package/scripts/hooks/adapters/claude-code.js +2 -0
  166. package/scripts/hooks/core/config-change.js +1 -0
  167. package/scripts/hooks/core/extension-registry.js +0 -2
  168. package/scripts/hooks/core/instructions-loaded.js +1 -1
  169. package/scripts/hooks/core/observation-capture.js +5 -5
  170. package/scripts/hooks/core/phase-gate.js +5 -0
  171. package/scripts/hooks/core/post-compact.js +1 -12
  172. package/scripts/hooks/core/research-gate.js +2 -12
  173. package/scripts/hooks/core/routing-gate.js +6 -0
  174. package/scripts/hooks/core/task-completed.js +12 -0
  175. package/scripts/hooks/core/worktree-lifecycle.js +1 -1
  176. package/scripts/hooks/entry/claude-code/config-change.js +6 -29
  177. package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
  178. package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
  179. package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
  180. package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
  181. package/scripts/hooks/entry/claude-code/session-end.js +4 -28
  182. package/scripts/hooks/entry/claude-code/session-start.js +205 -243
  183. package/scripts/hooks/entry/claude-code/setup.js +8 -49
  184. package/scripts/hooks/entry/claude-code/stop.js +40 -72
  185. package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
  186. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
  187. package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
  188. package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
  189. package/scripts/hooks/entry/shared/hook-runner.js +99 -0
  190. package/scripts/hooks/entry/shared/read-stdin.js +0 -2
  191. package/scripts/registries/api-registry.js +0 -2
  192. package/scripts/registries/component-registry.js +5 -9
  193. package/scripts/registries/contract-scanner.js +2 -9
  194. package/scripts/registries/function-registry.js +0 -2
  195. package/scripts/registries/schema-registry.js +14 -18
  196. package/scripts/registries/service-registry.js +23 -27
@@ -14,9 +14,7 @@
14
14
 
15
15
  const fs = require('node:fs');
16
16
  const path = require('node:path');
17
- const { getProjectRoot, getConfig, color, success, warn, error } = require('./flow-utils');
18
-
19
- const PROJECT_ROOT = getProjectRoot();
17
+ const { getProjectRoot, getConfig, color, success, warn, error, PATHS } = require('./flow-utils');
20
18
 
21
19
  // ============================================================
22
20
  // Base Scanner Class
@@ -81,7 +79,7 @@ class BaseScanner {
81
79
 
82
80
  // 1. Explicit directories from config
83
81
  for (const dir of this.config.directories) {
84
- const fullPath = path.join(PROJECT_ROOT, dir);
82
+ const fullPath = path.join(PATHS.root, dir);
85
83
  if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
86
84
  found.add(fullPath);
87
85
  }
@@ -161,7 +159,7 @@ class BaseScanner {
161
159
  }
162
160
  };
163
161
 
164
- walk(PROJECT_ROOT, 0, 0);
162
+ walk(PATHS.root, 0, 0);
165
163
  return results;
166
164
  }
167
165
 
@@ -171,7 +169,7 @@ class BaseScanner {
171
169
  * @returns {boolean} True if file should be excluded
172
170
  */
173
171
  shouldExclude(filePath) {
174
- const relativePath = path.relative(PROJECT_ROOT, filePath);
172
+ const relativePath = path.relative(PATHS.root, filePath);
175
173
 
176
174
  for (const regex of this._excludeRegexps) {
177
175
  if (regex.test(relativePath)) {
@@ -494,5 +492,5 @@ class BaseScanner {
494
492
 
495
493
  module.exports = {
496
494
  BaseScanner,
497
- PROJECT_ROOT
495
+ PROJECT_ROOT: PATHS.root
498
496
  };
@@ -16,12 +16,10 @@
16
16
  * const { executeScenario, generateScenario, resolveVariables, extractByPath } = require('./flow-scenario-engine');
17
17
  */
18
18
 
19
- 'use strict';
20
-
21
19
  const fs = require('node:fs');
22
20
  const path = require('node:path');
23
21
  const crypto = require('node:crypto');
24
- const { getProjectRoot, safeJsonParseString } = require('./flow-utils');
22
+ const { getProjectRoot, safeJsonParseString, PATHS } = require('./flow-utils');
25
23
 
26
24
  let verificationProfile;
27
25
  try {
@@ -30,8 +28,6 @@ try {
30
28
  verificationProfile = null;
31
29
  }
32
30
 
33
- const PROJECT_ROOT = getProjectRoot();
34
-
35
31
  // ============================================================
36
32
  // Constants
37
33
  // ============================================================
@@ -578,6 +578,31 @@ const CREDENTIAL_SCAN_PATTERNS = [
578
578
  }
579
579
  ];
580
580
 
581
+ // ============================================================
582
+ // Shell Argument Sanitization
583
+ // ============================================================
584
+
585
+ /**
586
+ * Sanitize a string for safe use in shell commands.
587
+ * Only allows alphanumeric, underscore, hyphen, and dot characters.
588
+ * @param {string} str - String to sanitize
589
+ * @returns {string} Sanitized string
590
+ */
591
+ function sanitizeShellArg(str) {
592
+ if (!str || typeof str !== 'string') return '';
593
+ return str.replace(/[^a-zA-Z0-9_.-]/g, '');
594
+ }
595
+
596
+ /**
597
+ * Escape a path for safe use in shell commands.
598
+ * @param {string} p - Path to escape
599
+ * @returns {string} Escaped path
600
+ */
601
+ function escapeShellPath(p) {
602
+ if (!p || typeof p !== 'string') return '';
603
+ return p.replace(/(["\s'$`\\!*?#~<>^()[\]{}|;&])/g, '\\$1');
604
+ }
605
+
581
606
  // ============================================================
582
607
  // Exports
583
608
  // ============================================================
@@ -608,6 +633,10 @@ module.exports = {
608
633
  // URL/Network safety
609
634
  isPrivateIP,
610
635
 
636
+ // Shell argument safety
637
+ sanitizeShellArg,
638
+ escapeShellPath,
639
+
611
640
  // Constants
612
641
  MAX_REGEX_LENGTH,
613
642
  VALID_CODE_EXTENSIONS,
@@ -10,7 +10,7 @@ const {
10
10
  execSync,
11
11
  execFileSync,
12
12
  spawnSync } = require('node:child_process');
13
- const readline = require('node:readline');
13
+ const readline = require('node:readline/promises');
14
14
  const path = require('node:path');
15
15
  const {
16
16
  PATHS,
@@ -22,7 +22,8 @@ const {
22
22
  readFile,
23
23
  writeFile,
24
24
  isGitRepo,
25
- getGitStatus
25
+ getGitStatus,
26
+ safeJsonParse
26
27
  } = require('./flow-utils')
27
28
  const { color, printSection, success, warn, error } = require('./flow-output');;
28
29
 
@@ -109,18 +110,15 @@ try {
109
110
  /**
110
111
  * Prompt user for input
111
112
  */
112
- function prompt(question) {
113
+ async function prompt(question) {
113
114
  const rl = readline.createInterface({
114
115
  input: process.stdin,
115
116
  output: process.stdout
116
117
  });
117
118
 
118
- return new Promise(resolve => {
119
- rl.question(question, answer => {
120
- rl.close();
121
- resolve(answer);
122
- });
123
- });
119
+ const answer = await rl.question(question);
120
+ rl.close();
121
+ return answer;
124
122
  }
125
123
 
126
124
  /**
@@ -132,7 +130,7 @@ function checkRequirements() {
132
130
  console.log(color('yellow', 'Checking session-end requirements...'));
133
131
 
134
132
  const config = getConfig();
135
- const steps = config.mandatorySteps?.onSessionEnd || [];
133
+ const steps = config.mandatorySteps?.onSessionEnd ?? [];
136
134
 
137
135
  if (steps.length > 0) {
138
136
  console.log('Required:');
@@ -244,7 +242,7 @@ function analyzeSessionForLearnings() {
244
242
  if (!sessionLearning) return;
245
243
 
246
244
  const config = getConfig();
247
- const sessionLearningConfig = config.learning?.session || {};
245
+ const sessionLearningConfig = config.learning?.session ?? {};
248
246
 
249
247
  // Check if enabled (default: true)
250
248
  if (sessionLearningConfig.enabled === false) return;
@@ -350,7 +348,7 @@ function reviewPendingCorrections() {
350
348
 
351
349
  const truncatedMsg = correction.userMessage?.length > 60
352
350
  ? correction.userMessage.slice(0, 60) + '...'
353
- : correction.userMessage || '(empty)';
351
+ : correction.userMessage ?? '(empty)';
354
352
 
355
353
  console.log(` ${i + 1}. ${color(confidenceColor, `[${correction.confidence}%]`)} ${correction.correctionType || 'unknown'}`);
356
354
  console.log(` "${truncatedMsg}"`);
@@ -407,7 +405,7 @@ function analyzeCrossSessionPatterns() {
407
405
  if (!sessionLearning || !patternEnforcer) return null;
408
406
 
409
407
  const config = getConfig();
410
- const crossSessionConfig = config.learning?.crossSession || {};
408
+ const crossSessionConfig = config.learning?.crossSession ?? {};
411
409
 
412
410
  // Check if enabled (default: true)
413
411
  if (crossSessionConfig.enabled === false) return null;
@@ -420,8 +418,8 @@ function analyzeCrossSessionPatterns() {
420
418
  }
421
419
 
422
420
  const patterns = sessionLearning.detectCrossSessionPatterns({
423
- lookbackDays: crossSessionConfig.lookbackDays || 30,
424
- minOccurrences: crossSessionConfig.minOccurrences || 3,
421
+ lookbackDays: crossSessionConfig.lookbackDays ?? 30,
422
+ minOccurrences: crossSessionConfig.minOccurrences ?? 3,
425
423
  similarityThreshold: threshold
426
424
  });
427
425
 
@@ -480,9 +478,9 @@ function saveSessionSummaryToState() {
480
478
 
481
479
  // Build summary from session data
482
480
  const summary = {
483
- tasksCompleted: sessionState.metrics?.tasksCompleted || 0,
484
- filesModified: sessionState.recentFiles?.slice(0, 5) || [],
485
- decisions: sessionState.recentDecisions?.map(d => d.decision).slice(0, 3) || [],
481
+ tasksCompleted: sessionState.metrics?.tasksCompleted ?? 0,
482
+ filesModified: sessionState.recentFiles?.slice(0, 5) ?? [],
483
+ decisions: sessionState.recentDecisions?.map(d => d.decision).slice(0, 3) ?? [],
486
484
  summary: memoryBlocks?.keyFacts?.slice(-3).join('; ') || 'Session ended'
487
485
  };
488
486
 
@@ -624,7 +622,7 @@ async function automaticMemoryManagement() {
624
622
  if (!memoryDb) return;
625
623
 
626
624
  const config = getConfig();
627
- const memConfig = config.memory?.automatic || {};
625
+ const memConfig = config.memory?.automatic ?? {};
628
626
 
629
627
  if (!memConfig.enabled) return;
630
628
 
@@ -635,8 +633,8 @@ async function automaticMemoryManagement() {
635
633
  // 1. Apply relevance decay
636
634
  if (memConfig.relevanceDecay?.enabled !== false) {
637
635
  const decayResult = await memoryDb.applyRelevanceDecay({
638
- decayRate: memConfig.relevanceDecay?.decayRate || 0.033,
639
- neverAccessedPenalty: memConfig.relevanceDecay?.neverAccessedPenalty || 0.1
636
+ decayRate: memConfig.relevanceDecay?.decayRate ?? 0.033,
637
+ neverAccessedPenalty: memConfig.relevanceDecay?.neverAccessedPenalty ?? 0.1
640
638
  });
641
639
  if (decayResult.decayed > 0) {
642
640
  console.log(` Relevance decay: ${decayResult.decayed} facts updated`);
@@ -644,10 +642,10 @@ async function automaticMemoryManagement() {
644
642
  }
645
643
 
646
644
  // 2. Check entropy and compact if needed
647
- const memoryConfig = { maxLocalFacts: config.memory?.maxLocalFacts || 1000 };
645
+ const memoryConfig = { maxLocalFacts: config.memory?.maxLocalFacts ?? 1000 };
648
646
  const entropy = await memoryDb.getEntropyStats(memoryConfig);
649
647
 
650
- const _threshold = memConfig.entropyThreshold || 0.7;
648
+ const _threshold = memConfig.entropyThreshold ?? 0.7;
651
649
  const statusColor = entropy.status === 'healthy' ? 'green'
652
650
  : entropy.status === 'moderate' ? 'yellow' : 'red';
653
651
 
@@ -659,7 +657,7 @@ async function automaticMemoryManagement() {
659
657
 
660
658
  // Demote low-relevance facts
661
659
  const demotion = await memoryDb.demoteToColdStorage({
662
- relevanceThreshold: memConfig.demotion?.relevanceThreshold || 0.3
660
+ relevanceThreshold: memConfig.demotion?.relevanceThreshold ?? 0.3
663
661
  });
664
662
  if (demotion.demoted > 0) {
665
663
  console.log(` Demoted: ${demotion.demoted} facts`);
@@ -673,7 +671,7 @@ async function automaticMemoryManagement() {
673
671
 
674
672
  // Purge old cold facts
675
673
  const purge = await memoryDb.purgeColdFacts({
676
- coldRetentionDays: memConfig.demotion?.coldRetentionDays || 90
674
+ coldRetentionDays: memConfig.demotion?.coldRetentionDays ?? 90
677
675
  });
678
676
  if (purge.purged > 0) {
679
677
  console.log(` Purged: ${purge.purged} old facts`);
@@ -681,11 +679,11 @@ async function automaticMemoryManagement() {
681
679
  }
682
680
 
683
681
  // 3. Check for promotion candidates and auto-promote if enabled
684
- const promoConfig = config.memory?.promotion || {};
682
+ const promoConfig = config.memory?.promotion ?? {};
685
683
  if (promoConfig.enabled) {
686
684
  const candidates = await memoryDb.getPromotionCandidates({
687
- minRelevance: promoConfig.minRelevance || 0.8,
688
- minAccessCount: promoConfig.threshold || 3
685
+ minRelevance: promoConfig.minRelevance ?? 0.8,
686
+ minAccessCount: promoConfig.threshold ?? 3
689
687
  });
690
688
 
691
689
  const unpromoted = candidates.filter(c => !c.promoted_to);
@@ -752,15 +750,8 @@ async function syncRulesIfChanged() {
752
750
 
753
751
  // Load last hash from state
754
752
  const hashStatePath = path.join(STATE_DIR, 'decisions-hash.json');
755
- let lastHash = null;
756
- if (fileExists(hashStatePath)) {
757
- try {
758
- const hashState = JSON.parse(readFile(hashStatePath));
759
- lastHash = hashState.hash;
760
- } catch (_err) {
761
- // Ignore parse errors
762
- }
763
- }
753
+ const hashState = safeJsonParse(hashStatePath, null);
754
+ const lastHash = hashState ? hashState.hash : null;
764
755
 
765
756
  // Compare hashes
766
757
  if (currentHash === lastHash) {
@@ -809,7 +800,7 @@ async function syncRulesIfChanged() {
809
800
  */
810
801
  async function offerKnowledgeSync() {
811
802
  const config = getConfig();
812
- const morningConfig = config.morningBriefing || {};
803
+ const morningConfig = config.morningBriefing ?? {};
813
804
 
814
805
  // Skip if disabled or if auto-regenerate handled it in morning
815
806
  if (morningConfig.checkKnowledgeSync === false) {
@@ -873,7 +864,7 @@ async function offerKnowledgeSync() {
873
864
  */
874
865
  async function offerDebtCleanup() {
875
866
  const config = getConfig();
876
- const techDebtConfig = config.techDebt || {};
867
+ const techDebtConfig = config.techDebt ?? {};
877
868
 
878
869
  if (!techDebtConfig.promptOnSessionEnd) {
879
870
  return;
@@ -996,7 +987,7 @@ async function offerDebtCleanup() {
996
987
  async function cleanupStaleTasks() {
997
988
  try {
998
989
  const readyData = getReadyData();
999
- const inProgress = readyData.inProgress || [];
990
+ const inProgress = readyData.inProgress ?? [];
1000
991
 
1001
992
  // Find auto-created tasks
1002
993
  const autoCreatedTasks = inProgress.filter(task =>
@@ -1094,7 +1085,7 @@ async function cleanupStaleTasks() {
1094
1085
  completedBy: 'session-end-cleanup'
1095
1086
  };
1096
1087
 
1097
- readyData.recentlyCompleted = readyData.recentlyCompleted || [];
1088
+ readyData.recentlyCompleted = readyData.recentlyCompleted ?? [];
1098
1089
  readyData.recentlyCompleted.unshift(completedTask);
1099
1090
  readyData.recentlyCompleted = readyData.recentlyCompleted.slice(0, 10);
1100
1091
  }
@@ -586,6 +586,7 @@ function applyLearnings(learnings, options = {}) {
586
586
 
587
587
  /**
588
588
  * Add learning to feedback-patterns.md
589
+ * Routes through the learning orchestrator for locking and dedup.
589
590
  */
590
591
  function addToFeedbackPatterns(learning) {
591
592
  if (!fileExists(FEEDBACK_PATTERNS_PATH)) {
@@ -594,45 +595,44 @@ function addToFeedbackPatterns(learning) {
594
595
  }
595
596
 
596
597
  try {
597
- let content = fs.readFileSync(FEEDBACK_PATTERNS_PATH, 'utf-8');
598
+ const { modifyFeedbackPatterns } = require('./flow-learning-orchestrator');
598
599
  const today = getTodayDateString();
599
600
 
600
- // Check if pattern already exists
601
- if (content.includes(learning.pattern)) {
602
- // Update count instead of adding duplicate
603
- return true;
604
- }
605
-
606
- // Find or create Session Analysis section
607
- const sectionHeader = '## Session Analysis Patterns';
608
- const tableHeader = '| Date | Pattern | Source | Count | Confidence | Status |';
609
- const tableSeparator = '|------|---------|--------|-------|------------|--------|';
610
-
611
601
  const newRow = `| ${today} | ${learning.pattern} | ${learning.source} | ${learning.occurrences} | ${learning.confidence}% | Monitor |`;
612
602
 
613
- if (content.includes(sectionHeader)) {
614
- // Add to existing section
615
- const sectionEnd = content.indexOf('\n## ', content.indexOf(sectionHeader) + 1);
616
- const insertPos = sectionEnd > 0 ? sectionEnd : content.length;
603
+ modifyFeedbackPatterns((currentContent) => {
604
+ let content = currentContent;
617
605
 
618
- // Find the table and add row
619
- const tableEnd = content.lastIndexOf('|', insertPos);
620
- if (tableEnd > content.indexOf(sectionHeader)) {
621
- const lineEnd = content.indexOf('\n', tableEnd);
622
- content = content.slice(0, lineEnd + 1) + newRow + '\n' + content.slice(lineEnd + 1);
606
+ // Check if pattern already exists
607
+ if (content.includes(learning.pattern)) {
608
+ return null; // Signal no write needed (already exists)
623
609
  }
624
- } else {
625
- // Add new section before "## Promotion History" or at end
626
- const newSection = `\n---\n\n${sectionHeader}\n\n${tableHeader}\n${tableSeparator}\n${newRow}\n`;
627
610
 
628
- if (content.includes('## Promotion History')) {
629
- content = content.replace('## Promotion History', newSection + '\n## Promotion History');
611
+ // Find or create Session Analysis section
612
+ const sectionHeader = '## Session Analysis Patterns';
613
+ const tableHeader = '| Date | Pattern | Source | Count | Confidence | Status |';
614
+ const tableSeparator = '|------|---------|--------|-------|------------|--------|';
615
+
616
+ if (content.includes(sectionHeader)) {
617
+ const sectionEnd = content.indexOf('\n## ', content.indexOf(sectionHeader) + 1);
618
+ const insertPos = sectionEnd > 0 ? sectionEnd : content.length;
619
+ const tableEnd = content.lastIndexOf('|', insertPos);
620
+ if (tableEnd > content.indexOf(sectionHeader)) {
621
+ const lineEnd = content.indexOf('\n', tableEnd);
622
+ content = content.slice(0, lineEnd + 1) + newRow + '\n' + content.slice(lineEnd + 1);
623
+ }
630
624
  } else {
631
- content = content.trimEnd() + newSection;
625
+ const newSection = `\n---\n\n${sectionHeader}\n\n${tableHeader}\n${tableSeparator}\n${newRow}\n`;
626
+ if (content.includes('## Promotion History')) {
627
+ content = content.replace('## Promotion History', newSection + '\n## Promotion History');
628
+ } else {
629
+ content = content.trimEnd() + newSection;
630
+ }
632
631
  }
633
- }
634
632
 
635
- fs.writeFileSync(FEEDBACK_PATTERNS_PATH, content, 'utf-8');
633
+ return { content, entryText: learning.pattern };
634
+ }, { caller: 'flow-session-learning/addToFeedbackPatterns' });
635
+
636
636
  return true;
637
637
  } catch (err) {
638
638
  if (process.env.DEBUG) console.error(`[DEBUG] addToFeedbackPatterns: ${err.message}`);
@@ -642,6 +642,7 @@ function addToFeedbackPatterns(learning) {
642
642
 
643
643
  /**
644
644
  * Add high-confidence learning to decisions.md
645
+ * Routes through the learning orchestrator for locking and dedup.
645
646
  */
646
647
  function addToDecisions(learning) {
647
648
  if (!fileExists(DECISIONS_PATH)) {
@@ -650,16 +651,19 @@ function addToDecisions(learning) {
650
651
  }
651
652
 
652
653
  try {
653
- let content = fs.readFileSync(DECISIONS_PATH, 'utf-8');
654
+ const { modifyDecisions } = require('./flow-learning-orchestrator');
654
655
  const today = getTodayDateString();
655
656
 
656
- // Check if pattern already exists
657
- if (content.includes(learning.pattern)) {
658
- return true; // Already exists
659
- }
657
+ modifyDecisions((currentContent) => {
658
+ let content = currentContent;
660
659
 
661
- // Create decision entry
662
- const decisionEntry = `
660
+ // Check if pattern already exists
661
+ if (content.includes(learning.pattern)) {
662
+ return null; // Already exists, no write needed
663
+ }
664
+
665
+ // Create decision entry
666
+ const decisionEntry = `
663
667
  ### ${learning.pattern} (${today})
664
668
  <!-- PIN: ${learning.pattern} -->
665
669
 
@@ -670,21 +674,21 @@ ${learning.description}
670
674
  **Recommendation**: ${learning.recommendation}
671
675
  `;
672
676
 
673
- // Find Session Learnings section or create one
674
- const sectionHeader = '## Session-Learned Patterns';
677
+ // Find Session Learnings section or create one
678
+ const sectionHeader = '## Session-Learned Patterns';
675
679
 
676
- if (content.includes(sectionHeader)) {
677
- // Add after section header
678
- const sectionIndex = content.indexOf(sectionHeader);
679
- const nextSection = content.indexOf('\n## ', sectionIndex + sectionHeader.length);
680
- const insertPos = nextSection > 0 ? nextSection : content.length;
681
- content = content.slice(0, insertPos) + decisionEntry + content.slice(insertPos);
682
- } else {
683
- // Add new section at the end
684
- content = content.trimEnd() + '\n\n---\n\n' + sectionHeader + '\n' + decisionEntry;
685
- }
680
+ if (content.includes(sectionHeader)) {
681
+ const sectionIndex = content.indexOf(sectionHeader);
682
+ const nextSection = content.indexOf('\n## ', sectionIndex + sectionHeader.length);
683
+ const insertPos = nextSection > 0 ? nextSection : content.length;
684
+ content = content.slice(0, insertPos) + decisionEntry + content.slice(insertPos);
685
+ } else {
686
+ content = content.trimEnd() + '\n\n---\n\n' + sectionHeader + '\n' + decisionEntry;
687
+ }
688
+
689
+ return { content, entryText: learning.pattern };
690
+ }, { caller: 'flow-session-learning/addToDecisions' });
686
691
 
687
- fs.writeFileSync(DECISIONS_PATH, content, 'utf-8');
688
692
  return true;
689
693
  } catch (err) {
690
694
  if (process.env.DEBUG) console.error(`[DEBUG] addToDecisions: ${err.message}`);
@@ -17,12 +17,11 @@ const fs = require('node:fs');
17
17
  const path = require('node:path');
18
18
  const {
19
19
  getProjectRoot,
20
- getConfig
20
+ getConfig, PATHS
21
21
  } = require('./flow-utils')
22
22
  const { color, success, warn, error } = require('./flow-output');;
23
23
 
24
- const PROJECT_ROOT = getProjectRoot();
25
- const GIT_DIR = path.join(PROJECT_ROOT, '.git');
24
+ const GIT_DIR = path.join(PATHS.root, '.git');
26
25
  const HOOKS_DIR = path.join(GIT_DIR, 'hooks');
27
26
 
28
27
  // Hook marker to identify our managed hooks
@@ -13,13 +13,12 @@
13
13
 
14
14
  const fs = require('node:fs');
15
15
  const path = require('node:path');
16
- const readline = require('node:readline');
17
- const { getProjectRoot, colors } = require('./flow-utils');
16
+ const readline = require('node:readline/promises');
17
+ const { getProjectRoot, colors, PATHS } = require('./flow-utils');
18
18
  const { success, warn, error: errorMsg, info, print } = require('./flow-output');
19
19
  const { getAllSkills, getSkillDir } = require('./flow-skill-matcher');
20
20
 
21
- const PROJECT_ROOT = getProjectRoot();
22
- const SKILLS_DIR = path.join(PROJECT_ROOT, '.claude', 'skills');
21
+ const SKILLS_DIR = path.join(PATHS.root, '.claude', 'skills');
23
22
  const TEMPLATE_DIR = path.join(SKILLS_DIR, '_template');
24
23
 
25
24
  function log(color, ...args) {
@@ -33,13 +32,9 @@ async function prompt(question, defaultValue = '') {
33
32
  });
34
33
 
35
34
  const defaultHint = defaultValue ? ` (${defaultValue})` : '';
36
-
37
- return new Promise(resolve => {
38
- rl.question(`${question}${defaultHint}: `, answer => {
39
- rl.close();
40
- resolve(answer.trim() || defaultValue);
41
- });
42
- });
35
+ const answer = await rl.question(`${question}${defaultHint}: `);
36
+ rl.close();
37
+ return answer.trim() || defaultValue;
43
38
  }
44
39
 
45
40
  async function confirm(question) {
@@ -187,7 +182,7 @@ _Add key patterns here._
187
182
  }
188
183
 
189
184
  async function createFromPatterns() {
190
- const feedbackPath = path.join(PROJECT_ROOT, '.workflow', 'state', 'feedback-patterns.md');
185
+ const feedbackPath = PATHS.feedbackPatterns;
191
186
 
192
187
  if (!fs.existsSync(feedbackPath)) {
193
188
  warn('No feedback-patterns.md found');
@@ -7,7 +7,7 @@
7
7
 
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
- const { ensureDir, getConfig, invalidateConfigCache, writeJson, PATHS, success } = require('./flow-utils');
10
+ const { ensureDir, getConfig, invalidateConfigCache, writeJson, PATHS, success, safeJsonParse } = require('./flow-utils');
11
11
  const { getTodayDate } = require('./flow-output');
12
12
 
13
13
  // Import helper functions from tech options
@@ -890,10 +890,14 @@ function updateDecisionsMd(selections, technologies, projectRoot) {
890
890
  // Append tech stack section
891
891
  content = content.trimEnd() + '\n' + techStackSection;
892
892
 
893
- fs.writeFileSync(decisionsPath, content, 'utf8');
894
-
895
- // Sync to .claude/rules/ for Claude Code integration
896
- syncDecisionsToRules();
893
+ // Route through orchestrator for locking and dedup
894
+ try {
895
+ const { writeToDecisions } = require('./flow-learning-orchestrator');
896
+ writeToDecisions({ content, entryText: 'Tech Stack (auto-generated)', caller: 'flow-skill-generator/updateDecisions', skipDedup: true, syncRules: true }).catch(() => {});
897
+ } catch (_err) {
898
+ fs.writeFileSync(decisionsPath, content, 'utf8');
899
+ syncDecisionsToRules();
900
+ }
897
901
  }
898
902
 
899
903
  function updateConfigJson(technologies, _projectRoot) {
@@ -1466,17 +1470,9 @@ For manual use, run the wizard first: node flow-stack-wizard.js
1466
1470
  process.exit(1);
1467
1471
  }
1468
1472
 
1469
- let selections;
1470
- try {
1471
- const content = fs.readFileSync(selectionsPath, 'utf8');
1472
- const parsed = JSON.parse(content);
1473
- if (typeof parsed !== 'object' || parsed === null) {
1474
- console.error('stack-selections.json is not a valid object');
1475
- process.exit(1);
1476
- }
1477
- selections = parsed;
1478
- } catch (err) {
1479
- console.error(`Failed to parse stack-selections.json: ${err.message}`);
1473
+ const selections = safeJsonParse(selectionsPath, null);
1474
+ if (!selections) {
1475
+ console.error('stack-selections.json is not a valid object or could not be parsed');
1480
1476
  process.exit(1);
1481
1477
  }
1482
1478
  const { collectTechnologiesFromSelections } = require('./flow-tech-options');
@@ -20,13 +20,10 @@
20
20
  const fs = require('node:fs');
21
21
  const path = require('node:path');
22
22
  const { execSync } = require('node:child_process');
23
- const { getProjectRoot, getConfig, colors } = require('./flow-utils');
23
+ const { getProjectRoot, getConfig, colors, PATHS } = require('./flow-utils');
24
24
  const { getAllSkills, getSkillDir } = require('./flow-skill-matcher');
25
25
 
26
- const PROJECT_ROOT = getProjectRoot();
27
- const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
28
- const SKILLS_DIR = path.join(PROJECT_ROOT, '.claude', 'skills');
29
- const STATE_DIR = path.join(WORKFLOW_DIR, 'state');
26
+ const SKILLS_DIR = path.join(PATHS.root, '.claude', 'skills');
30
27
 
31
28
  function log(color, ...args) {
32
29
  console.log(colors[color] + args.join(' ') + colors.reset);
@@ -582,7 +579,7 @@ function getFileSignature(files) {
582
579
  * Log unmatched files to feedback-patterns.md with proper count tracking
583
580
  */
584
581
  function logToFeedbackPatterns(context, unmatchedFiles) {
585
- const feedbackPath = path.join(STATE_DIR, 'feedback-patterns.md');
582
+ const feedbackPath = PATHS.feedbackPatterns;
586
583
 
587
584
  if (!fs.existsSync(feedbackPath)) return;
588
585
 
@@ -641,7 +638,19 @@ function logToFeedbackPatterns(context, unmatchedFiles) {
641
638
  content = content.slice(0, headerEnd) + entry + '\n' + content.slice(headerEnd);
642
639
  }
643
640
 
644
- fs.writeFileSync(feedbackPath, content);
641
+ try {
642
+ const { writeToFeedbackPatterns } = require('./flow-learning-orchestrator');
643
+ writeToFeedbackPatterns({
644
+ content,
645
+ entryText: `Files changed with no matching skill: ${filesPreview}`,
646
+ caller: 'flow-skill-learn/logToFeedbackPatterns',
647
+ skipDedup: true // We already handle dedup above via foundExisting check
648
+ }).catch(_err => {
649
+ if (process.env.DEBUG) console.error(`[DEBUG] logToFeedbackPatterns: ${_err.message}`);
650
+ });
651
+ } catch (_err) {
652
+ if (process.env.DEBUG) console.error(`[DEBUG] logToFeedbackPatterns: ${_err.message}`);
653
+ }
645
654
  }
646
655
 
647
656
  // ============================================================
@@ -814,7 +823,7 @@ module.exports = {
814
823
  };
815
824
 
816
825
  if (require.main === module) {
817
- main().catch(e => {
826
+ main().catch(err => {
818
827
  log('red', `Error: ${err.message}`);
819
828
  process.exit(1);
820
829
  });
@@ -31,8 +31,7 @@ function getSkillFilePath(dir) {
31
31
  const { getProjectRoot, getConfig, PATHS, colors } = require('./flow-utils');
32
32
  const { error: errorMsg } = require('./flow-output');
33
33
 
34
- const PROJECT_ROOT = getProjectRoot();
35
- const SKILLS_DIR = path.join(PROJECT_ROOT, '.claude', 'skills');
34
+ const SKILLS_DIR = path.join(PATHS.root, '.claude', 'skills');
36
35
 
37
36
  // Maximum nesting depth for skill directories (prevents runaway recursion)
38
37
  const MAX_SKILL_NESTING_DEPTH = 3;