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,654 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Background Sync Daemon
5
+ *
6
+ * Keeps workflow state in sync when multiple agents work on different branches.
7
+ * Watches .workflow/state/ for changes and handles branch switching.
8
+ *
9
+ * Part of Phase 6: Team & Integrations
10
+ *
11
+ * Usage:
12
+ * flow sync-daemon start Start the daemon
13
+ * flow sync-daemon stop Stop the daemon
14
+ * flow sync-daemon status Check daemon status
15
+ * flow sync-daemon restart Restart the daemon
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execSync, spawn } = require('child_process');
21
+ const {
22
+ PROJECT_ROOT,
23
+ STATE_DIR,
24
+ parseFlags,
25
+ color,
26
+ info,
27
+ warn,
28
+ error,
29
+ success,
30
+ fileExists,
31
+ safeJsonParse,
32
+ getConfig,
33
+ printHeader
34
+ } = require('./flow-utils');
35
+
36
+ // ============================================================
37
+ // Constants
38
+ // ============================================================
39
+
40
+ const PID_FILE = path.join(STATE_DIR, 'sync-daemon.pid');
41
+ const LOG_FILE = path.join(STATE_DIR, 'sync-daemon.log');
42
+ const HEARTBEAT_FILE = path.join(STATE_DIR, 'sync-daemon.heartbeat');
43
+ const SYNC_STATE_FILE = path.join(STATE_DIR, 'sync-state.json');
44
+
45
+ const DEFAULT_CONFIG = {
46
+ enabled: false,
47
+ watchPaths: ['.workflow/state/'],
48
+ syncOnBranchSwitch: true,
49
+ heartbeatIntervalMs: 30000,
50
+ debounceMs: 1000,
51
+ maxLogSizeBytes: 1024 * 1024 // 1MB
52
+ };
53
+
54
+ // ============================================================
55
+ // Configuration
56
+ // ============================================================
57
+
58
+ /**
59
+ * Get sync daemon configuration
60
+ */
61
+ function getSyncConfig() {
62
+ const config = getConfig();
63
+ return {
64
+ ...DEFAULT_CONFIG,
65
+ ...(config?.syncDaemon || {})
66
+ };
67
+ }
68
+
69
+ // ============================================================
70
+ // Daemon Management
71
+ // ============================================================
72
+
73
+ /**
74
+ * Check if daemon is running
75
+ */
76
+ function isDaemonRunning() {
77
+ if (!fileExists(PID_FILE)) {
78
+ return false;
79
+ }
80
+
81
+ try {
82
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
83
+ // Check if process exists
84
+ process.kill(pid, 0);
85
+
86
+ // Check heartbeat freshness (2.5x interval for clearer threshold)
87
+ if (fileExists(HEARTBEAT_FILE)) {
88
+ try {
89
+ const heartbeat = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf-8'));
90
+ if (heartbeat?.timestamp) {
91
+ const age = Date.now() - new Date(heartbeat.timestamp).getTime();
92
+ const config = getSyncConfig();
93
+ const threshold = config.heartbeatIntervalMs * 2.5;
94
+
95
+ if (!isNaN(age) && age > threshold) {
96
+ warn('Daemon heartbeat stale, may be unresponsive');
97
+ return false;
98
+ }
99
+ }
100
+ } catch (parseError) {
101
+ warn('Failed to parse heartbeat file: ' + parseError.message);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ return true;
107
+ } catch (err) {
108
+ // Process doesn't exist
109
+ cleanupPidFile();
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get daemon status
116
+ */
117
+ function getDaemonStatus() {
118
+ const running = isDaemonRunning();
119
+ let pid = null;
120
+ let heartbeat = null;
121
+ let currentBranch = null;
122
+
123
+ if (fileExists(PID_FILE)) {
124
+ pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
125
+ }
126
+
127
+ if (fileExists(HEARTBEAT_FILE)) {
128
+ try {
129
+ heartbeat = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf-8'));
130
+ } catch {
131
+ heartbeat = null; // Invalid JSON, treat as no heartbeat
132
+ }
133
+ }
134
+
135
+ try {
136
+ currentBranch = execSync('git branch --show-current', {
137
+ encoding: 'utf-8',
138
+ cwd: PROJECT_ROOT
139
+ }).trim();
140
+ } catch (err) {
141
+ currentBranch = 'unknown';
142
+ }
143
+
144
+ const syncState = safeJsonParse(SYNC_STATE_FILE) || {};
145
+
146
+ return {
147
+ running,
148
+ pid,
149
+ heartbeat,
150
+ currentBranch,
151
+ lastSync: syncState.lastSync,
152
+ syncedBranches: syncState.branches || {},
153
+ config: getSyncConfig()
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Cleanup stale PID file
159
+ */
160
+ function cleanupPidFile() {
161
+ if (fileExists(PID_FILE)) {
162
+ fs.unlinkSync(PID_FILE);
163
+ }
164
+ if (fileExists(HEARTBEAT_FILE)) {
165
+ fs.unlinkSync(HEARTBEAT_FILE);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Filter environment variables for daemon (security: only pass necessary vars)
171
+ */
172
+ function getSafeEnv() {
173
+ const safeVars = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', 'NODE_ENV'];
174
+ const env = { WOGI_DAEMON: '1' };
175
+ for (const key of safeVars) {
176
+ if (process.env[key]) {
177
+ env[key] = process.env[key];
178
+ }
179
+ }
180
+ return env;
181
+ }
182
+
183
+ /**
184
+ * Start the daemon
185
+ */
186
+ function startDaemon() {
187
+ if (isDaemonRunning()) {
188
+ warn('Daemon is already running');
189
+ return false;
190
+ }
191
+
192
+ const config = getSyncConfig();
193
+ if (!config.enabled) {
194
+ warn('Sync daemon is disabled in config');
195
+ info('Enable with: flow config set syncDaemon.enabled true');
196
+ return false;
197
+ }
198
+
199
+ // Start daemon as detached process with filtered environment
200
+ const daemon = spawn('node', [__filename, '--daemon'], {
201
+ detached: true,
202
+ stdio: ['ignore', 'ignore', 'ignore'],
203
+ cwd: PROJECT_ROOT,
204
+ env: getSafeEnv()
205
+ });
206
+
207
+ daemon.unref();
208
+
209
+ // Write PID file
210
+ fs.writeFileSync(PID_FILE, daemon.pid.toString());
211
+
212
+ // Verify daemon started by waiting briefly and checking heartbeat
213
+ setTimeout(() => {
214
+ if (!fileExists(HEARTBEAT_FILE)) {
215
+ warn('Daemon may have failed to start - no heartbeat file yet');
216
+ warn('Check log file for errors: ' + LOG_FILE);
217
+ }
218
+ }, 500);
219
+
220
+ success(`Daemon started (PID: ${daemon.pid})`);
221
+ info(`Log file: ${LOG_FILE}`);
222
+
223
+ return true;
224
+ }
225
+
226
+ /**
227
+ * Stop the daemon
228
+ */
229
+ function stopDaemon() {
230
+ if (!fileExists(PID_FILE)) {
231
+ info('Daemon is not running');
232
+ return false;
233
+ }
234
+
235
+ try {
236
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
237
+ process.kill(pid, 'SIGTERM');
238
+ cleanupPidFile();
239
+ success('Daemon stopped');
240
+ return true;
241
+ } catch (err) {
242
+ cleanupPidFile();
243
+ info('Daemon was not running (cleaned up stale PID)');
244
+ return false;
245
+ }
246
+ }
247
+
248
+ // ============================================================
249
+ // Daemon Process
250
+ // ============================================================
251
+
252
+ /**
253
+ * Log message to file with proper error handling
254
+ */
255
+ function log(level, message) {
256
+ const timestamp = new Date().toISOString();
257
+ const line = `${timestamp} [${level}] ${message}\n`;
258
+
259
+ try {
260
+ // Rotate log if too large
261
+ const config = getSyncConfig();
262
+ if (fileExists(LOG_FILE)) {
263
+ try {
264
+ const stats = fs.statSync(LOG_FILE);
265
+ if (stats.size > config.maxLogSizeBytes) {
266
+ const backupPath = LOG_FILE + '.old';
267
+ try {
268
+ if (fileExists(backupPath)) fs.unlinkSync(backupPath);
269
+ fs.renameSync(LOG_FILE, backupPath);
270
+ } catch (rotateError) {
271
+ // Log rotation failed, continue anyway
272
+ console.error(console.error('Log rotation failed:', rotateError.message));
273
+ }
274
+ }
275
+ } catch (statError) {
276
+ // Stat failed, continue anyway
277
+ }
278
+ }
279
+
280
+ fs.appendFileSync(LOG_FILE, line);
281
+ } catch (err) {
282
+ // Silently fail to avoid infinite loops if logging fails
283
+ console.error(console.error('Failed to write log:', err.message));
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Update heartbeat
289
+ */
290
+ function updateHeartbeat() {
291
+ const status = {
292
+ timestamp: new Date().toISOString(),
293
+ pid: process.pid,
294
+ branch: getCurrentBranch(),
295
+ uptime: process.uptime()
296
+ };
297
+
298
+ fs.writeFileSync(HEARTBEAT_FILE, JSON.stringify(status, null, 2));
299
+ }
300
+
301
+ /**
302
+ * Get current git branch
303
+ */
304
+ function getCurrentBranch() {
305
+ try {
306
+ return execSync('git branch --show-current', {
307
+ encoding: 'utf-8',
308
+ cwd: PROJECT_ROOT
309
+ }).trim();
310
+ } catch (err) {
311
+ return 'unknown';
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Detect branch switch
317
+ */
318
+ let lastBranch = null;
319
+ function detectBranchSwitch() {
320
+ const currentBranch = getCurrentBranch();
321
+
322
+ if (lastBranch && lastBranch !== currentBranch) {
323
+ log('INFO', `Branch switched: ${lastBranch} -> ${currentBranch}`);
324
+ handleBranchSwitch(lastBranch, currentBranch);
325
+ }
326
+
327
+ lastBranch = currentBranch;
328
+ }
329
+
330
+ /**
331
+ * Handle branch switch
332
+ */
333
+ function handleBranchSwitch(fromBranch, toBranch) {
334
+ const config = getSyncConfig();
335
+
336
+ if (!config.syncOnBranchSwitch) {
337
+ return;
338
+ }
339
+
340
+ // Save state for current branch
341
+ saveBranchState(fromBranch);
342
+
343
+ // Load state for new branch
344
+ loadBranchState(toBranch);
345
+
346
+ // Invalidate caches
347
+ invalidateCaches();
348
+ }
349
+
350
+ /**
351
+ * Validate branch name format (security: prevent JSON key injection)
352
+ */
353
+ function isValidBranchName(branch) {
354
+ // Allow alphanumeric, dots, dashes, underscores, slashes (max 255 chars)
355
+ return branch &&
356
+ typeof branch === 'string' &&
357
+ branch.length <= 255 &&
358
+ /^[a-zA-Z0-9._/-]+$/.test(branch);
359
+ }
360
+
361
+ /**
362
+ * Save state for a branch
363
+ */
364
+ function saveBranchState(branch) {
365
+ // Validate branch name before using as JSON key
366
+ if (!isValidBranchName(branch)) {
367
+ log('WARN', `Invalid branch name format, skipping save: ${branch?.slice(0, 50)}`);
368
+ return;
369
+ }
370
+
371
+ const syncState = safeJsonParse(SYNC_STATE_FILE) || { branches: {} };
372
+
373
+ syncState.branches[branch] = {
374
+ savedAt: new Date().toISOString(),
375
+ ready: safeJsonParse(path.join(STATE_DIR, 'ready.json')),
376
+ progress: fileExists(path.join(STATE_DIR, 'progress.md'))
377
+ ? fs.readFileSync(path.join(STATE_DIR, 'progress.md'), 'utf-8').slice(0, 1000)
378
+ : null
379
+ };
380
+
381
+ fs.writeFileSync(SYNC_STATE_FILE, JSON.stringify(syncState, null, 2));
382
+ log('INFO', `Saved state for branch: ${branch}`);
383
+ }
384
+
385
+ /**
386
+ * Load state for a branch
387
+ */
388
+ function loadBranchState(branch) {
389
+ const syncState = safeJsonParse(SYNC_STATE_FILE);
390
+
391
+ if (!syncState?.branches?.[branch]) {
392
+ log('INFO', `No saved state for branch: ${branch}`);
393
+ return;
394
+ }
395
+
396
+ const branchState = syncState.branches[branch];
397
+
398
+ // Optionally restore ready.json (be careful not to overwrite work)
399
+ // For now, just log that we could restore
400
+ log('INFO', `Found saved state for branch: ${branch} (from ${branchState.savedAt})`);
401
+ }
402
+
403
+ /**
404
+ * Invalidate caches
405
+ */
406
+ function invalidateCaches() {
407
+ const cacheFiles = [
408
+ 'jira-cache.json',
409
+ 'linear-cache.json',
410
+ 'component-index.json'
411
+ ];
412
+
413
+ for (const file of cacheFiles) {
414
+ const cachePath = path.join(STATE_DIR, file);
415
+ if (fileExists(cachePath)) {
416
+ fs.unlinkSync(cachePath);
417
+ log('DEBUG', `Invalidated cache: ${file}`);
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * File watcher callback (debounced)
424
+ */
425
+ let debounceTimer = null;
426
+ function onFileChange(eventType, filename) {
427
+ const config = getSyncConfig();
428
+
429
+ // Debounce rapid changes
430
+ if (debounceTimer) {
431
+ clearTimeout(debounceTimer);
432
+ }
433
+
434
+ debounceTimer = setTimeout(() => {
435
+ log('DEBUG', `File changed: ${filename} (${eventType})`);
436
+ updateSyncState(filename);
437
+ }, config.debounceMs);
438
+ }
439
+
440
+ /**
441
+ * Update sync state after file change
442
+ */
443
+ function updateSyncState(filename) {
444
+ const syncState = safeJsonParse(SYNC_STATE_FILE) || { branches: {} };
445
+
446
+ syncState.lastSync = new Date().toISOString();
447
+ syncState.lastFile = filename;
448
+
449
+ fs.writeFileSync(SYNC_STATE_FILE, JSON.stringify(syncState, null, 2));
450
+ }
451
+
452
+ /**
453
+ * Run the daemon
454
+ */
455
+ function runDaemon() {
456
+ const config = getSyncConfig();
457
+
458
+ log('INFO', `Daemon started (PID: ${process.pid})`);
459
+ log('INFO', `Watch paths: ${config.watchPaths.join(', ')}`);
460
+ log('INFO', `Heartbeat interval: ${config.heartbeatIntervalMs}ms`);
461
+
462
+ // Initial state
463
+ lastBranch = getCurrentBranch();
464
+ log('INFO', `Current branch: ${lastBranch}`);
465
+
466
+ // Set up file watcher
467
+ const watchers = [];
468
+ for (const watchPath of config.watchPaths) {
469
+ const fullPath = path.join(PROJECT_ROOT, watchPath);
470
+
471
+ if (!fs.existsSync(fullPath)) {
472
+ log('WARN', `Watch path does not exist: ${watchPath}`);
473
+ continue;
474
+ }
475
+
476
+ try {
477
+ const watcher = fs.watch(fullPath, { recursive: true }, onFileChange);
478
+ watchers.push(watcher);
479
+ log('INFO', `Watching: ${watchPath}`);
480
+ } catch (err) {
481
+ log('ERROR', `Failed to watch ${watchPath}: ${err.message}`);
482
+ }
483
+ }
484
+
485
+ // Heartbeat interval
486
+ const heartbeatInterval = setInterval(() => {
487
+ updateHeartbeat();
488
+ detectBranchSwitch();
489
+ }, config.heartbeatIntervalMs);
490
+
491
+ // Initial heartbeat
492
+ updateHeartbeat();
493
+
494
+ // Handle signals
495
+ const cleanup = () => {
496
+ log('INFO', 'Daemon stopping...');
497
+ clearInterval(heartbeatInterval);
498
+ for (const watcher of watchers) {
499
+ watcher.close();
500
+ }
501
+ cleanupPidFile();
502
+ log('INFO', 'Daemon stopped');
503
+ process.exit(0);
504
+ };
505
+
506
+ process.on('SIGTERM', cleanup);
507
+ process.on('SIGINT', cleanup);
508
+
509
+ log('INFO', 'Daemon running. Send SIGTERM to stop.');
510
+ }
511
+
512
+ // ============================================================
513
+ // CLI Output
514
+ // ============================================================
515
+
516
+ function printStatus(status) {
517
+ printHeader('SYNC DAEMON STATUS');
518
+
519
+ console.log(` ${color('dim', 'Running:')} ${status.running ? color('green', 'Yes') : color('red', 'No')}`);
520
+ console.log(` ${color('dim', 'PID:')} ${status.pid || 'N/A'}`);
521
+ console.log(` ${color('dim', 'Current branch:')} ${color('cyan', status.currentBranch)}`);
522
+
523
+ if (status.heartbeat) {
524
+ const age = Date.now() - new Date(status.heartbeat.timestamp).getTime();
525
+ const ageStr = age < 60000 ? `${Math.floor(age / 1000)}s ago` : `${Math.floor(age / 60000)}m ago`;
526
+ console.log(` ${color('dim', 'Last heartbeat:')} ${ageStr}`);
527
+ console.log(` ${color('dim', 'Uptime:')} ${Math.floor(status.heartbeat.uptime / 60)}m`);
528
+ }
529
+
530
+ if (status.lastSync) {
531
+ console.log(` ${color('dim', 'Last sync:')} ${status.lastSync}`);
532
+ }
533
+
534
+ console.log(`\n ${color('dim', 'Configuration:')}`);
535
+ console.log(` ${color('dim', ' Enabled:')} ${status.config.enabled ? 'Yes' : 'No'}`);
536
+ console.log(` ${color('dim', ' Watch paths:')} ${status.config.watchPaths.join(', ')}`);
537
+ console.log(` ${color('dim', ' Branch sync:')} ${status.config.syncOnBranchSwitch ? 'Yes' : 'No'}`);
538
+
539
+ const branchCount = Object.keys(status.syncedBranches).length;
540
+ if (branchCount > 0) {
541
+ console.log(`\n ${color('dim', `Synced branches: ${branchCount}`)}`);
542
+ for (const [branch, data] of Object.entries(status.syncedBranches)) {
543
+ console.log(` - ${branch} (${data.savedAt})`);
544
+ }
545
+ }
546
+
547
+ console.log('');
548
+ }
549
+
550
+ // ============================================================
551
+ // CLI Entry Point
552
+ // ============================================================
553
+
554
+ function showHelp() {
555
+ console.log(`
556
+ Wogi Flow - Background Sync Daemon
557
+
558
+ Keep workflow state in sync across branches and agents.
559
+
560
+ Usage:
561
+ flow sync-daemon start Start the daemon
562
+ flow sync-daemon stop Stop the daemon
563
+ flow sync-daemon status Check daemon status
564
+ flow sync-daemon restart Restart the daemon
565
+
566
+ Options:
567
+ --json Output as JSON
568
+ --help, -h Show this help
569
+
570
+ Configuration:
571
+ Add to .workflow/config.json:
572
+ {
573
+ "syncDaemon": {
574
+ "enabled": true,
575
+ "watchPaths": [".workflow/state/"],
576
+ "syncOnBranchSwitch": true,
577
+ "heartbeatIntervalMs": 30000
578
+ }
579
+ }
580
+
581
+ Features:
582
+ - Watches .workflow/state/ for file changes
583
+ - Detects branch switches and saves/restores state
584
+ - Invalidates caches on branch switch
585
+ - Logs activity to .workflow/state/sync-daemon.log
586
+ `);
587
+ }
588
+
589
+ async function main() {
590
+ const args = process.argv.slice(2);
591
+ const { flags, positional } = parseFlags(args);
592
+
593
+ // Check if running as daemon
594
+ if (flags.daemon || process.env.WOGI_DAEMON === '1') {
595
+ runDaemon();
596
+ return;
597
+ }
598
+
599
+ if (flags.help || flags.h) {
600
+ showHelp();
601
+ process.exit(0);
602
+ }
603
+
604
+ const command = positional[0] || 'status';
605
+
606
+ switch (command) {
607
+ case 'start':
608
+ startDaemon();
609
+ break;
610
+
611
+ case 'stop':
612
+ stopDaemon();
613
+ break;
614
+
615
+ case 'restart':
616
+ stopDaemon();
617
+ setTimeout(() => startDaemon(), 500);
618
+ break;
619
+
620
+ case 'status': {
621
+ const status = getDaemonStatus();
622
+ if (flags.json) {
623
+ console.log(JSON.stringify(status, null, 2));
624
+ } else {
625
+ printStatus(status);
626
+ }
627
+ break;
628
+ }
629
+
630
+ default:
631
+ error(`Unknown command: ${command}`);
632
+ showHelp();
633
+ process.exit(1);
634
+ }
635
+ }
636
+
637
+ // ============================================================
638
+ // Exports
639
+ // ============================================================
640
+
641
+ module.exports = {
642
+ getSyncConfig,
643
+ isDaemonRunning,
644
+ getDaemonStatus,
645
+ startDaemon,
646
+ stopDaemon
647
+ };
648
+
649
+ if (require.main === module) {
650
+ main().catch(err => {
651
+ error(err.message);
652
+ process.exit(1);
653
+ });
654
+ }