wogiflow 1.0.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.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,752 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Team Sync (Project-Based)
5
+ *
6
+ * Synchronizes workflow knowledge between team members at the PROJECT level.
7
+ * Unlike user-based sync, this ensures the entire team shares:
8
+ * - decisions.md (project patterns and conventions)
9
+ * - app-map.md (component registry)
10
+ * - skill learnings (accumulated knowledge)
11
+ *
12
+ * Architecture:
13
+ * - Each project has a unique projectId
14
+ * - All sync happens at project scope, not user scope
15
+ * - Conflict resolution via timestamps (newest-wins) or manual merge
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const crypto = require('crypto');
21
+ const { getConfig, getProjectRoot } = require('./flow-utils');
22
+
23
+ // Lazy-load to avoid circular dependency
24
+ let _syncDecisionsToRules = null;
25
+ function syncDecisionsToRules() {
26
+ if (!_syncDecisionsToRules) {
27
+ _syncDecisionsToRules = require('./flow-rules-sync').syncDecisionsToRules;
28
+ }
29
+ return _syncDecisionsToRules();
30
+ }
31
+
32
+ /**
33
+ * Get team sync configuration
34
+ */
35
+ function getTeamConfig() {
36
+ const config = getConfig();
37
+ return config.team || {};
38
+ }
39
+
40
+ /**
41
+ * Generate a project ID from the project root path
42
+ */
43
+ function generateProjectId() {
44
+ const projectRoot = getProjectRoot();
45
+ const packageJsonPath = path.join(projectRoot, 'package.json');
46
+
47
+ let projectName = path.basename(projectRoot);
48
+
49
+ // Try to get name from package.json
50
+ if (fs.existsSync(packageJsonPath)) {
51
+ try {
52
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
53
+ if (pkg.name) {
54
+ projectName = pkg.name;
55
+ }
56
+ } catch {
57
+ // Use directory name
58
+ }
59
+ }
60
+
61
+ // Create a hash from the project name and git remote
62
+ const gitConfigPath = path.join(projectRoot, '.git', 'config');
63
+ let gitRemote = '';
64
+
65
+ if (fs.existsSync(gitConfigPath)) {
66
+ try {
67
+ const gitConfig = fs.readFileSync(gitConfigPath, 'utf-8');
68
+ const match = gitConfig.match(/url = (.+)/);
69
+ if (match) {
70
+ gitRemote = match[1];
71
+ }
72
+ } catch {
73
+ // No git remote
74
+ }
75
+ }
76
+
77
+ const hash = crypto.createHash('sha256')
78
+ .update(projectName + gitRemote)
79
+ .digest('hex')
80
+ .substring(0, 12);
81
+
82
+ return `proj_${projectName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${hash}`;
83
+ }
84
+
85
+ /**
86
+ * Get sync-able files and their content
87
+ */
88
+ function getSyncableFiles() {
89
+ const projectRoot = getProjectRoot();
90
+ const teamConfig = getTeamConfig();
91
+ const files = {};
92
+
93
+ // Always sync decisions.md if enabled
94
+ if (teamConfig.syncDecisions !== false) {
95
+ const decisionsPath = path.join(projectRoot, '.workflow', 'state', 'decisions.md');
96
+ if (fs.existsSync(decisionsPath)) {
97
+ files.decisions = {
98
+ path: decisionsPath,
99
+ content: fs.readFileSync(decisionsPath, 'utf-8'),
100
+ hash: hashContent(fs.readFileSync(decisionsPath, 'utf-8')),
101
+ lastModified: fs.statSync(decisionsPath).mtime.toISOString()
102
+ };
103
+ }
104
+ }
105
+
106
+ // Sync app-map.md if enabled
107
+ if (teamConfig.syncAppMap !== false) {
108
+ const appMapPath = path.join(projectRoot, '.workflow', 'state', 'app-map.md');
109
+ if (fs.existsSync(appMapPath)) {
110
+ files.appMap = {
111
+ path: appMapPath,
112
+ content: fs.readFileSync(appMapPath, 'utf-8'),
113
+ hash: hashContent(fs.readFileSync(appMapPath, 'utf-8')),
114
+ lastModified: fs.statSync(appMapPath).mtime.toISOString()
115
+ };
116
+ }
117
+ }
118
+
119
+ // Sync skill learnings if enabled
120
+ if (teamConfig.syncSkillLearnings !== false) {
121
+ const skillsDir = path.join(projectRoot, '.claude', 'skills');
122
+ if (fs.existsSync(skillsDir)) {
123
+ const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
124
+ .filter(d => d.isDirectory() && !d.name.startsWith('_'))
125
+ .map(d => d.name);
126
+
127
+ files.skillLearnings = {};
128
+
129
+ for (const skill of skillDirs) {
130
+ const learningsPath = path.join(skillsDir, skill, 'knowledge', 'learnings.md');
131
+ if (fs.existsSync(learningsPath)) {
132
+ files.skillLearnings[skill] = {
133
+ path: learningsPath,
134
+ content: fs.readFileSync(learningsPath, 'utf-8'),
135
+ hash: hashContent(fs.readFileSync(learningsPath, 'utf-8')),
136
+ lastModified: fs.statSync(learningsPath).mtime.toISOString()
137
+ };
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ // Sync component-index.json if enabled
144
+ if (teamConfig.syncComponentIndex !== false) {
145
+ const indexPath = path.join(projectRoot, '.workflow', 'state', 'component-index.json');
146
+ if (fs.existsSync(indexPath)) {
147
+ files.componentIndex = {
148
+ path: indexPath,
149
+ content: fs.readFileSync(indexPath, 'utf-8'),
150
+ hash: hashContent(fs.readFileSync(indexPath, 'utf-8')),
151
+ lastModified: fs.statSync(indexPath).mtime.toISOString()
152
+ };
153
+ }
154
+ }
155
+
156
+ // Sync request-log.md if enabled (recent entries only if configured)
157
+ const syncRequestLog = teamConfig.syncRequestLog || teamConfig.sync?.requestLog;
158
+ if (syncRequestLog && syncRequestLog !== false) {
159
+ const logPath = path.join(projectRoot, '.workflow', 'state', 'request-log.md');
160
+ if (fs.existsSync(logPath)) {
161
+ let content = fs.readFileSync(logPath, 'utf-8');
162
+
163
+ // If "recent" mode, only sync last 20 entries
164
+ if (syncRequestLog === 'recent') {
165
+ const entries = content.split(/(?=### R-\d+)/);
166
+ const recentEntries = entries.slice(-20);
167
+ content = recentEntries.join('');
168
+ }
169
+
170
+ files.requestLog = {
171
+ path: logPath,
172
+ content,
173
+ hash: hashContent(content),
174
+ lastModified: fs.statSync(logPath).mtime.toISOString(),
175
+ mode: syncRequestLog
176
+ };
177
+ }
178
+ }
179
+
180
+ // Sync ready.json (tasks) if enabled
181
+ if (teamConfig.syncTasks !== false && teamConfig.sync?.tasks !== false) {
182
+ const tasksPath = path.join(projectRoot, '.workflow', 'state', 'ready.json');
183
+ if (fs.existsSync(tasksPath)) {
184
+ files.tasks = {
185
+ path: tasksPath,
186
+ content: fs.readFileSync(tasksPath, 'utf-8'),
187
+ hash: hashContent(fs.readFileSync(tasksPath, 'utf-8')),
188
+ lastModified: fs.statSync(tasksPath).mtime.toISOString()
189
+ };
190
+ }
191
+ }
192
+
193
+ // Sync memory database if enabled
194
+ // Note: We export facts as JSON rather than syncing the raw SQLite file
195
+ const syncMemory = teamConfig.syncMemory || teamConfig.sync?.memory;
196
+ if (syncMemory) {
197
+ const memoryDbPath = path.join(projectRoot, '.workflow', 'memory', 'local.db');
198
+ if (fs.existsSync(memoryDbPath)) {
199
+ try {
200
+ // Try to export facts as JSON for safer sync
201
+ const facts = exportMemoryFacts(memoryDbPath);
202
+ if (facts && facts.length > 0) {
203
+ const factsJson = JSON.stringify(facts, null, 2);
204
+ files.memory = {
205
+ path: memoryDbPath,
206
+ content: factsJson,
207
+ hash: hashContent(factsJson),
208
+ lastModified: fs.statSync(memoryDbPath).mtime.toISOString(),
209
+ factCount: facts.length,
210
+ format: 'json-export'
211
+ };
212
+ }
213
+ } catch (err) {
214
+ // If export fails, note it but don't block sync
215
+ console.warn('Memory export failed:', err.message);
216
+ }
217
+ }
218
+ }
219
+
220
+ return files;
221
+ }
222
+
223
+ /**
224
+ * Export facts from memory database as JSON
225
+ */
226
+ function exportMemoryFacts(dbPath) {
227
+ try {
228
+ // Use sqlite3 CLI to export (avoids native module dependency)
229
+ const { execSync } = require('child_process');
230
+
231
+ // First check what columns exist
232
+ const schema = execSync(
233
+ `sqlite3 "${dbPath}" "PRAGMA table_info(facts);"`,
234
+ { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
235
+ );
236
+
237
+ // Parse available columns
238
+ const columns = schema.split('\n')
239
+ .filter(Boolean)
240
+ .map(line => line.split('|')[1])
241
+ .filter(Boolean);
242
+
243
+ if (columns.length === 0) {
244
+ return null;
245
+ }
246
+
247
+ // Build query with available columns
248
+ const selectCols = columns.map(c => `'${c}', ${c}`).join(', ');
249
+ const result = execSync(
250
+ `sqlite3 "${dbPath}" "SELECT json_group_array(json_object(${selectCols})) FROM facts LIMIT 100"`,
251
+ { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
252
+ );
253
+ return JSON.parse(result.trim() || '[]');
254
+ } catch {
255
+ // sqlite3 CLI not available or query failed - fail silently
256
+ return null;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Hash content for change detection
262
+ */
263
+ function hashContent(content) {
264
+ return crypto.createHash('md5').update(content).digest('hex');
265
+ }
266
+
267
+ /**
268
+ * Create a sync payload for upload
269
+ */
270
+ function createSyncPayload() {
271
+ const teamConfig = getTeamConfig();
272
+ const files = getSyncableFiles();
273
+
274
+ return {
275
+ projectId: teamConfig.projectId || generateProjectId(),
276
+ teamId: teamConfig.teamId,
277
+ timestamp: new Date().toISOString(),
278
+ files,
279
+ metadata: {
280
+ version: getConfig().version || '1.0.0',
281
+ syncedBy: teamConfig.userId || 'anonymous'
282
+ }
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Apply remote changes to local files
288
+ */
289
+ function applyRemoteChanges(remotePayload, strategy = 'newest-wins') {
290
+ const projectRoot = getProjectRoot();
291
+ const localFiles = getSyncableFiles();
292
+ const changes = [];
293
+
294
+ // Apply decisions.md
295
+ if (remotePayload.files.decisions) {
296
+ const result = mergeFile(
297
+ localFiles.decisions,
298
+ remotePayload.files.decisions,
299
+ strategy
300
+ );
301
+ if (result.changed) {
302
+ fs.writeFileSync(result.path, result.content);
303
+ // Sync to .claude/rules/ for Claude Code integration
304
+ syncDecisionsToRules();
305
+ changes.push({ file: 'decisions.md', action: result.action });
306
+ }
307
+ }
308
+
309
+ // Apply app-map.md
310
+ if (remotePayload.files.appMap) {
311
+ const result = mergeFile(
312
+ localFiles.appMap,
313
+ remotePayload.files.appMap,
314
+ strategy
315
+ );
316
+ if (result.changed) {
317
+ fs.writeFileSync(result.path, result.content);
318
+ changes.push({ file: 'app-map.md', action: result.action });
319
+ }
320
+ }
321
+
322
+ // Apply skill learnings
323
+ if (remotePayload.files.skillLearnings) {
324
+ for (const [skill, remoteFile] of Object.entries(remotePayload.files.skillLearnings)) {
325
+ const localFile = localFiles.skillLearnings?.[skill];
326
+ const result = mergeFile(localFile, remoteFile, strategy);
327
+
328
+ if (result.changed) {
329
+ // Create skill directory if needed
330
+ const skillDir = path.join(projectRoot, '.claude', 'skills', skill, 'knowledge');
331
+ if (!fs.existsSync(skillDir)) {
332
+ fs.mkdirSync(skillDir, { recursive: true });
333
+ }
334
+ fs.writeFileSync(result.path, result.content);
335
+ changes.push({ file: `.claude/skills/${skill}/learnings.md`, action: result.action });
336
+ }
337
+ }
338
+ }
339
+
340
+ return changes;
341
+ }
342
+
343
+ /**
344
+ * Merge a single file based on strategy
345
+ */
346
+ function mergeFile(localFile, remoteFile, strategy) {
347
+ const projectRoot = getProjectRoot();
348
+
349
+ // Remote file path may be absolute from remote, need to localize
350
+ const localPath = localFile?.path || remoteFile.path.replace(/.*\.workflow/, path.join(projectRoot, '.workflow'));
351
+
352
+ // No local file - use remote
353
+ if (!localFile) {
354
+ return {
355
+ changed: true,
356
+ action: 'created',
357
+ path: localPath,
358
+ content: remoteFile.content
359
+ };
360
+ }
361
+
362
+ // Same content - no change
363
+ if (localFile.hash === remoteFile.hash) {
364
+ return { changed: false };
365
+ }
366
+
367
+ // Apply strategy
368
+ switch (strategy) {
369
+ case 'newest-wins': {
370
+ const localTime = new Date(localFile.lastModified).getTime();
371
+ const remoteTime = new Date(remoteFile.lastModified).getTime();
372
+
373
+ if (remoteTime > localTime) {
374
+ return {
375
+ changed: true,
376
+ action: 'updated',
377
+ path: localPath,
378
+ content: remoteFile.content
379
+ };
380
+ }
381
+ return { changed: false };
382
+ }
383
+
384
+ case 'remote-wins':
385
+ return {
386
+ changed: true,
387
+ action: 'updated',
388
+ path: localPath,
389
+ content: remoteFile.content
390
+ };
391
+
392
+ case 'local-wins':
393
+ return { changed: false };
394
+
395
+ case 'merge':
396
+ // For markdown files, we can do a simple append of unique sections
397
+ const merged = mergeMarkdownContent(localFile.content, remoteFile.content);
398
+ return {
399
+ changed: merged !== localFile.content,
400
+ action: 'merged',
401
+ path: localPath,
402
+ content: merged
403
+ };
404
+
405
+ default:
406
+ return { changed: false };
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Simple markdown merge - append unique sections
412
+ */
413
+ function mergeMarkdownContent(localContent, remoteContent) {
414
+ const localSections = parseSections(localContent);
415
+ const remoteSections = parseSections(remoteContent);
416
+
417
+ // Find sections in remote that don't exist in local
418
+ const localHeaders = new Set(localSections.map(s => s.header.toLowerCase()));
419
+ const newSections = remoteSections.filter(s =>
420
+ !localHeaders.has(s.header.toLowerCase())
421
+ );
422
+
423
+ if (newSections.length === 0) {
424
+ return localContent;
425
+ }
426
+
427
+ // Append new sections
428
+ let merged = localContent.trimEnd();
429
+
430
+ for (const section of newSections) {
431
+ merged += `\n\n${section.raw}`;
432
+ }
433
+
434
+ return merged;
435
+ }
436
+
437
+ /**
438
+ * Parse markdown into sections by headers
439
+ */
440
+ function parseSections(content) {
441
+ const sections = [];
442
+ const lines = content.split('\n');
443
+ let currentSection = null;
444
+
445
+ for (const line of lines) {
446
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)/);
447
+
448
+ if (headerMatch) {
449
+ if (currentSection) {
450
+ sections.push(currentSection);
451
+ }
452
+ currentSection = {
453
+ level: headerMatch[1].length,
454
+ header: headerMatch[2],
455
+ raw: line,
456
+ content: []
457
+ };
458
+ } else if (currentSection) {
459
+ currentSection.raw += '\n' + line;
460
+ currentSection.content.push(line);
461
+ }
462
+ }
463
+
464
+ if (currentSection) {
465
+ sections.push(currentSection);
466
+ }
467
+
468
+ return sections;
469
+ }
470
+
471
+ /**
472
+ * Get sync status
473
+ */
474
+ function getSyncStatus() {
475
+ const teamConfig = getTeamConfig();
476
+ const files = getSyncableFiles();
477
+
478
+ const status = {
479
+ enabled: teamConfig.enabled === true,
480
+ projectId: teamConfig.projectId || generateProjectId(),
481
+ teamId: teamConfig.teamId,
482
+ projectScope: teamConfig.projectScope !== false,
483
+ lastSync: teamConfig.lastSync || null,
484
+ files: {
485
+ decisions: files.decisions ? {
486
+ exists: true,
487
+ hash: files.decisions.hash,
488
+ lastModified: files.decisions.lastModified
489
+ } : { exists: false },
490
+ appMap: files.appMap ? {
491
+ exists: true,
492
+ hash: files.appMap.hash,
493
+ lastModified: files.appMap.lastModified
494
+ } : { exists: false },
495
+ componentIndex: files.componentIndex ? {
496
+ exists: true,
497
+ hash: files.componentIndex.hash,
498
+ lastModified: files.componentIndex.lastModified
499
+ } : { exists: false },
500
+ requestLog: files.requestLog ? {
501
+ exists: true,
502
+ mode: files.requestLog.mode,
503
+ lastModified: files.requestLog.lastModified
504
+ } : { exists: false },
505
+ tasks: files.tasks ? {
506
+ exists: true,
507
+ hash: files.tasks.hash,
508
+ lastModified: files.tasks.lastModified
509
+ } : { exists: false },
510
+ memory: files.memory ? {
511
+ exists: true,
512
+ factCount: files.memory.factCount,
513
+ lastModified: files.memory.lastModified
514
+ } : { exists: false },
515
+ skillLearnings: files.skillLearnings ? Object.keys(files.skillLearnings) : []
516
+ },
517
+ syncConfig: {
518
+ syncDecisions: teamConfig.syncDecisions !== false,
519
+ syncAppMap: teamConfig.syncAppMap !== false,
520
+ syncComponentIndex: teamConfig.syncComponentIndex !== false,
521
+ syncSkillLearnings: teamConfig.syncSkillLearnings !== false,
522
+ syncRequestLog: teamConfig.syncRequestLog || 'recent',
523
+ syncTasks: teamConfig.syncTasks || false,
524
+ syncMemory: teamConfig.syncMemory || false,
525
+ conflictResolution: teamConfig.conflictResolution || 'newest-wins'
526
+ }
527
+ };
528
+
529
+ return status;
530
+ }
531
+
532
+ /**
533
+ * Initialize team sync for a project
534
+ */
535
+ function initializeTeamSync(teamId, options = {}) {
536
+ const projectRoot = getProjectRoot();
537
+ const configPath = path.join(projectRoot, '.workflow', 'config.json');
538
+
539
+ const config = getConfig();
540
+
541
+ config.team = {
542
+ ...config.team,
543
+ enabled: true,
544
+ teamId,
545
+ projectId: options.projectId || generateProjectId(),
546
+ projectScope: true,
547
+ syncDecisions: options.syncDecisions !== false,
548
+ syncAppMap: options.syncAppMap !== false,
549
+ syncSkillLearnings: options.syncSkillLearnings !== false,
550
+ conflictResolution: options.conflictResolution || 'newest-wins',
551
+ lastSync: null
552
+ };
553
+
554
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
555
+
556
+ return {
557
+ success: true,
558
+ projectId: config.team.projectId,
559
+ message: `Team sync initialized for project ${config.team.projectId}`
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Sync with backend (placeholder for actual API call)
565
+ */
566
+ async function syncWithBackend() {
567
+ const teamConfig = getTeamConfig();
568
+
569
+ if (!teamConfig.enabled) {
570
+ return { success: false, message: 'Team sync not enabled' };
571
+ }
572
+
573
+ const payload = createSyncPayload();
574
+
575
+ // In a real implementation, this would call the API
576
+ // For now, save to a local sync file for testing
577
+ const projectRoot = getProjectRoot();
578
+ const syncDir = path.join(projectRoot, '.workflow', 'sync');
579
+
580
+ if (!fs.existsSync(syncDir)) {
581
+ fs.mkdirSync(syncDir, { recursive: true });
582
+ }
583
+
584
+ const syncFile = path.join(syncDir, `sync-${Date.now()}.json`);
585
+ fs.writeFileSync(syncFile, JSON.stringify(payload, null, 2));
586
+
587
+ // Update last sync time
588
+ const configPath = path.join(projectRoot, '.workflow', 'config.json');
589
+ const config = getConfig();
590
+ config.team.lastSync = new Date().toISOString();
591
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
592
+
593
+ return {
594
+ success: true,
595
+ message: 'Sync payload created',
596
+ syncFile,
597
+ filesIncluded: Object.keys(payload.files)
598
+ };
599
+ }
600
+
601
+ /**
602
+ * Generate sync status report
603
+ */
604
+ function generateStatusReport() {
605
+ const status = getSyncStatus();
606
+ const lines = [
607
+ '',
608
+ '╔══════════════════════════════════════════════════════╗',
609
+ '║ 🔄 TEAM SYNC STATUS ║',
610
+ '╠══════════════════════════════════════════════════════╣'
611
+ ];
612
+
613
+ const enabledIcon = status.enabled ? '✅' : '❌';
614
+ lines.push(`║ Status: ${enabledIcon} ${status.enabled ? 'Enabled' : 'Disabled'}`.padEnd(55) + '║');
615
+ lines.push(`║ Project ID: ${(status.projectId || 'Not set').substring(0, 35)}`.padEnd(55) + '║');
616
+ lines.push(`║ Team ID: ${status.teamId || 'Not set'}`.padEnd(55) + '║');
617
+ lines.push(`║ Scope: ${status.projectScope ? 'Project-based' : 'User-based'}`.padEnd(55) + '║');
618
+
619
+ lines.push('╠══════════════════════════════════════════════════════╣');
620
+ lines.push('║ Syncable Files:'.padEnd(55) + '║');
621
+
622
+ const dIcon = status.files.decisions?.exists ? '✅' : '❌';
623
+ const dEnabled = status.syncConfig.syncDecisions ? '' : ' (disabled)';
624
+ lines.push(`║ ${dIcon} decisions.md${dEnabled}`.padEnd(55) + '║');
625
+
626
+ const aIcon = status.files.appMap?.exists ? '✅' : '❌';
627
+ const aEnabled = status.syncConfig.syncAppMap ? '' : ' (disabled)';
628
+ lines.push(`║ ${aIcon} app-map.md${aEnabled}`.padEnd(55) + '║');
629
+
630
+ const cIcon = status.files.componentIndex?.exists ? '✅' : '❌';
631
+ const cEnabled = status.syncConfig.syncComponentIndex ? '' : ' (disabled)';
632
+ lines.push(`║ ${cIcon} component-index.json${cEnabled}`.padEnd(55) + '║');
633
+
634
+ const rIcon = status.files.requestLog?.exists ? '✅' : '❌';
635
+ const rMode = status.syncConfig.syncRequestLog === 'recent' ? ' (recent)' : '';
636
+ const rEnabled = status.syncConfig.syncRequestLog ? rMode : ' (disabled)';
637
+ lines.push(`║ ${rIcon} request-log.md${rEnabled}`.padEnd(55) + '║');
638
+
639
+ const tIcon = status.files.tasks?.exists ? '✅' : '❌';
640
+ const tEnabled = status.syncConfig.syncTasks ? '' : ' (disabled)';
641
+ lines.push(`║ ${tIcon} ready.json (tasks)${tEnabled}`.padEnd(55) + '║');
642
+
643
+ const mIcon = status.files.memory?.exists ? '✅' : '❌';
644
+ const mCount = status.files.memory?.factCount ? ` (${status.files.memory.factCount} facts)` : '';
645
+ const mEnabled = status.syncConfig.syncMemory ? mCount : ' (disabled)';
646
+ lines.push(`║ ${mIcon} memory facts${mEnabled}`.padEnd(55) + '║');
647
+
648
+ const skillCount = status.files.skillLearnings?.length || 0;
649
+ const sEnabled = status.syncConfig.syncSkillLearnings ? '' : ' (disabled)';
650
+ lines.push(`║ 📚 ${skillCount} skill learning files${sEnabled}`.padEnd(55) + '║');
651
+
652
+ lines.push('╠══════════════════════════════════════════════════════╣');
653
+ lines.push(`║ Conflict Resolution: ${status.syncConfig.conflictResolution}`.padEnd(55) + '║');
654
+ lines.push(`║ Last Sync: ${status.lastSync || 'Never'}`.padEnd(55) + '║');
655
+ lines.push('╚══════════════════════════════════════════════════════╝');
656
+
657
+ return lines.join('\n');
658
+ }
659
+
660
+ // ============================================================
661
+ // Exports
662
+ // ============================================================
663
+
664
+ module.exports = {
665
+ getTeamConfig,
666
+ generateProjectId,
667
+ getSyncableFiles,
668
+ createSyncPayload,
669
+ applyRemoteChanges,
670
+ getSyncStatus,
671
+ initializeTeamSync,
672
+ syncWithBackend,
673
+ generateStatusReport,
674
+ hashContent,
675
+ mergeMarkdownContent
676
+ };
677
+
678
+ // ============================================================
679
+ // CLI
680
+ // ============================================================
681
+
682
+ if (require.main === module) {
683
+ const args = process.argv.slice(2);
684
+ const command = args[0];
685
+
686
+ switch (command) {
687
+ case 'status': {
688
+ console.log(generateStatusReport());
689
+ break;
690
+ }
691
+
692
+ case 'init': {
693
+ const teamId = args[1];
694
+ if (!teamId) {
695
+ console.error('Usage: node flow-team-sync.js init <team-id>');
696
+ process.exit(1);
697
+ }
698
+
699
+ const result = initializeTeamSync(teamId);
700
+ console.log(`\n✅ ${result.message}`);
701
+ console.log(` Project ID: ${result.projectId}`);
702
+ break;
703
+ }
704
+
705
+ case 'sync': {
706
+ syncWithBackend().then(result => {
707
+ if (result.success) {
708
+ console.log(`\n✅ ${result.message}`);
709
+ console.log(` Files: ${result.filesIncluded.join(', ')}`);
710
+ } else {
711
+ console.error(`\n❌ ${result.message}`);
712
+ process.exit(1);
713
+ }
714
+ });
715
+ break;
716
+ }
717
+
718
+ case 'payload': {
719
+ const payload = createSyncPayload();
720
+ console.log(JSON.stringify(payload, null, 2));
721
+ break;
722
+ }
723
+
724
+ case 'project-id': {
725
+ console.log(generateProjectId());
726
+ break;
727
+ }
728
+
729
+ default:
730
+ console.log(`
731
+ Wogi Flow - Team Sync (Project-Based)
732
+
733
+ Usage:
734
+ node flow-team-sync.js <command> [args]
735
+
736
+ Commands:
737
+ status Show sync status
738
+ init <team-id> Initialize team sync
739
+ sync Sync with backend
740
+ payload Show sync payload (debug)
741
+ project-id Generate/show project ID
742
+
743
+ Configuration (config.json):
744
+ team.enabled: true
745
+ team.projectScope: true (project-based, not user-based)
746
+ team.syncDecisions: true Sync decisions.md
747
+ team.syncAppMap: true Sync app-map.md
748
+ team.syncSkillLearnings: true Sync skill learnings
749
+ team.conflictResolution: "newest-wins" | "remote-wins" | "local-wins" | "merge"
750
+ `);
751
+ }
752
+ }