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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Knowledge Sync
|
|
5
|
+
*
|
|
6
|
+
* Detects drift in knowledge files (stack.md, architecture.md, testing.md)
|
|
7
|
+
* by tracking hashes of project indicator files.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* flow knowledge-sync status Check sync status
|
|
11
|
+
* flow knowledge-sync check Check and report drift
|
|
12
|
+
* flow knowledge-sync regenerate Regenerate stale knowledge files
|
|
13
|
+
* flow knowledge-sync --json JSON output
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
PATHS,
|
|
22
|
+
PROJECT_ROOT,
|
|
23
|
+
fileExists,
|
|
24
|
+
dirExists,
|
|
25
|
+
parseFlags,
|
|
26
|
+
outputJson,
|
|
27
|
+
printHeader,
|
|
28
|
+
printSection,
|
|
29
|
+
color,
|
|
30
|
+
success,
|
|
31
|
+
warn,
|
|
32
|
+
error,
|
|
33
|
+
info,
|
|
34
|
+
getConfig,
|
|
35
|
+
isPathWithinProject,
|
|
36
|
+
safeJsonParse
|
|
37
|
+
} = require('./flow-utils');
|
|
38
|
+
|
|
39
|
+
// Files that indicate stack/architecture changes
|
|
40
|
+
const STACK_INDICATORS = [
|
|
41
|
+
'package.json',
|
|
42
|
+
'package-lock.json',
|
|
43
|
+
'yarn.lock',
|
|
44
|
+
'pnpm-lock.yaml',
|
|
45
|
+
'requirements.txt',
|
|
46
|
+
'Pipfile',
|
|
47
|
+
'Gemfile',
|
|
48
|
+
'go.mod',
|
|
49
|
+
'Cargo.toml',
|
|
50
|
+
'build.gradle',
|
|
51
|
+
'pom.xml',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Files that indicate architecture changes
|
|
55
|
+
const ARCHITECTURE_INDICATORS = [
|
|
56
|
+
'tsconfig.json',
|
|
57
|
+
'tsconfig.*.json',
|
|
58
|
+
'jsconfig.json',
|
|
59
|
+
'.eslintrc*',
|
|
60
|
+
'.prettierrc*',
|
|
61
|
+
'webpack.config.*',
|
|
62
|
+
'vite.config.*',
|
|
63
|
+
'next.config.*',
|
|
64
|
+
'nuxt.config.*',
|
|
65
|
+
'angular.json',
|
|
66
|
+
'nest-cli.json',
|
|
67
|
+
'.babelrc*',
|
|
68
|
+
'rollup.config.*',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Files that indicate testing changes
|
|
72
|
+
const TESTING_INDICATORS = [
|
|
73
|
+
'jest.config.*',
|
|
74
|
+
'vitest.config.*',
|
|
75
|
+
'cypress.config.*',
|
|
76
|
+
'playwright.config.*',
|
|
77
|
+
'.mocharc*',
|
|
78
|
+
'karma.conf.*',
|
|
79
|
+
'pytest.ini',
|
|
80
|
+
'setup.py',
|
|
81
|
+
'tox.ini',
|
|
82
|
+
'phpunit.xml',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compute MD5 hash of file content
|
|
87
|
+
* @param {string} filePath - Path to file
|
|
88
|
+
* @returns {{hash: string|null, error: string|null}} Hash result with error context
|
|
89
|
+
*/
|
|
90
|
+
function hashFile(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(filePath)) {
|
|
93
|
+
return { hash: null, error: 'not_found' };
|
|
94
|
+
}
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
96
|
+
return { hash: crypto.createHash('md5').update(content).digest('hex'), error: null };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// Provide error context for debugging
|
|
99
|
+
const errorType = err.code === 'EACCES' ? 'permission_denied' :
|
|
100
|
+
err.code === 'EISDIR' ? 'is_directory' :
|
|
101
|
+
'read_error';
|
|
102
|
+
return { hash: null, error: errorType };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Escape all regex special characters except * which becomes .*
|
|
108
|
+
* @param {string} pattern - Glob pattern
|
|
109
|
+
* @returns {string} Regex-safe string
|
|
110
|
+
*/
|
|
111
|
+
function escapeGlobToRegex(pattern) {
|
|
112
|
+
// Escape all regex special chars except *
|
|
113
|
+
return pattern
|
|
114
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
115
|
+
.replace(/\*/g, '[^/]*'); // Convert * to non-path-separator match
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate pattern contains only safe characters
|
|
120
|
+
* @param {string} pattern - Pattern to validate
|
|
121
|
+
* @returns {boolean} True if safe
|
|
122
|
+
*/
|
|
123
|
+
function isSafePattern(pattern) {
|
|
124
|
+
// Only allow alphanumeric, -, _, ., and *
|
|
125
|
+
return /^[a-zA-Z0-9._*-]+$/.test(pattern);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Find files matching glob patterns in project root
|
|
130
|
+
*/
|
|
131
|
+
function findIndicatorFiles(patterns) {
|
|
132
|
+
const found = [];
|
|
133
|
+
|
|
134
|
+
for (const pattern of patterns) {
|
|
135
|
+
// Validate pattern is safe
|
|
136
|
+
if (!isSafePattern(pattern)) {
|
|
137
|
+
warn(`Skipping unsafe pattern: ${pattern}`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Simple glob matching - supports * wildcard
|
|
142
|
+
if (pattern.includes('*')) {
|
|
143
|
+
const regex = new RegExp('^' + escapeGlobToRegex(pattern) + '$');
|
|
144
|
+
try {
|
|
145
|
+
const files = fs.readdirSync(PROJECT_ROOT);
|
|
146
|
+
for (const file of files) {
|
|
147
|
+
// Skip hidden files and symlinks for safety
|
|
148
|
+
const fullPath = path.join(PROJECT_ROOT, file);
|
|
149
|
+
try {
|
|
150
|
+
const stat = fs.lstatSync(fullPath);
|
|
151
|
+
if (stat.isSymbolicLink()) continue;
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (regex.test(file)) {
|
|
157
|
+
found.push(file);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Directory read error - silently skip
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// Exact match
|
|
165
|
+
const fullPath = path.join(PROJECT_ROOT, pattern);
|
|
166
|
+
|
|
167
|
+
// Defense in depth: verify resolved path is within project
|
|
168
|
+
if (!isPathWithinProject(fullPath)) {
|
|
169
|
+
warn(`Path traversal blocked: ${pattern}`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// Check it exists and is not a symlink
|
|
175
|
+
const stat = fs.lstatSync(fullPath);
|
|
176
|
+
if (!stat.isSymbolicLink() && fs.existsSync(fullPath)) {
|
|
177
|
+
found.push(pattern);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// File doesn't exist - skip
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compute hashes for a category of indicator files
|
|
190
|
+
*/
|
|
191
|
+
function computeCategoryHashes(patterns) {
|
|
192
|
+
const files = findIndicatorFiles(patterns);
|
|
193
|
+
const hashes = {};
|
|
194
|
+
const errors = {};
|
|
195
|
+
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
const fullPath = path.join(PROJECT_ROOT, file);
|
|
198
|
+
|
|
199
|
+
// Defense in depth: double-check path is within project
|
|
200
|
+
if (!isPathWithinProject(fullPath)) {
|
|
201
|
+
errors[file] = 'path_traversal';
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = hashFile(fullPath);
|
|
206
|
+
if (result.hash) {
|
|
207
|
+
hashes[file] = result.hash;
|
|
208
|
+
} else if (result.error && result.error !== 'not_found') {
|
|
209
|
+
// Track non-trivial errors for debugging
|
|
210
|
+
errors[file] = result.error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Return combined hash of all files
|
|
215
|
+
const combined = Object.entries(hashes)
|
|
216
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
217
|
+
.map(([file, hash]) => `${file}:${hash}`)
|
|
218
|
+
.join('|');
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
files: Object.keys(hashes),
|
|
222
|
+
combinedHash: combined ? crypto.createHash('md5').update(combined).digest('hex') : null,
|
|
223
|
+
individualHashes: hashes,
|
|
224
|
+
errors: Object.keys(errors).length > 0 ? errors : null
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validate sync state structure
|
|
230
|
+
* @param {Object} state - Parsed state object
|
|
231
|
+
* @returns {boolean} True if valid structure
|
|
232
|
+
*/
|
|
233
|
+
function isValidSyncState(state) {
|
|
234
|
+
if (!state || typeof state !== 'object') {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// lastSync is optional but must be string if present
|
|
239
|
+
if (state.lastSync !== undefined && typeof state.lastSync !== 'string') {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Validate each category if present
|
|
244
|
+
const categories = ['stack', 'architecture', 'testing'];
|
|
245
|
+
for (const cat of categories) {
|
|
246
|
+
if (state[cat] !== undefined) {
|
|
247
|
+
const catData = state[cat];
|
|
248
|
+
if (typeof catData !== 'object' || catData === null) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
// combinedHash should be string or null
|
|
252
|
+
if (catData.combinedHash !== undefined &&
|
|
253
|
+
catData.combinedHash !== null &&
|
|
254
|
+
typeof catData.combinedHash !== 'string') {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
// individualHashes should be object if present
|
|
258
|
+
if (catData.individualHashes !== undefined &&
|
|
259
|
+
(typeof catData.individualHashes !== 'object' || catData.individualHashes === null)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Load current sync state
|
|
270
|
+
*/
|
|
271
|
+
function loadSyncState() {
|
|
272
|
+
if (!fileExists(PATHS.knowledgeSync)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const content = fs.readFileSync(PATHS.knowledgeSync, 'utf-8');
|
|
278
|
+
const state = safeJsonParse(content, null);
|
|
279
|
+
|
|
280
|
+
// Validate structure before returning
|
|
281
|
+
if (!isValidSyncState(state)) {
|
|
282
|
+
warn('Invalid sync state structure in knowledge-sync.json');
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return state;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
warn(`Failed to load sync state: ${err.message}`);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Save sync state
|
|
295
|
+
*/
|
|
296
|
+
function saveSyncState(state) {
|
|
297
|
+
fs.writeFileSync(PATHS.knowledgeSync, JSON.stringify(state, null, 2));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Check drift for a specific knowledge file category
|
|
302
|
+
*/
|
|
303
|
+
function checkCategoryDrift(category, indicators, syncState) {
|
|
304
|
+
const current = computeCategoryHashes(indicators);
|
|
305
|
+
const stored = syncState?.[category];
|
|
306
|
+
|
|
307
|
+
if (!stored) {
|
|
308
|
+
return {
|
|
309
|
+
category,
|
|
310
|
+
status: 'missing',
|
|
311
|
+
reason: 'No sync state recorded',
|
|
312
|
+
needsRegeneration: true,
|
|
313
|
+
currentHash: current.combinedHash,
|
|
314
|
+
storedHash: null,
|
|
315
|
+
files: current.files
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (current.combinedHash !== stored.combinedHash) {
|
|
320
|
+
// Find which files changed
|
|
321
|
+
const changedFiles = [];
|
|
322
|
+
for (const [file, hash] of Object.entries(current.individualHashes)) {
|
|
323
|
+
if (stored.individualHashes?.[file] !== hash) {
|
|
324
|
+
changedFiles.push(file);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Check for removed files
|
|
328
|
+
for (const file of Object.keys(stored.individualHashes || {})) {
|
|
329
|
+
if (!current.individualHashes[file]) {
|
|
330
|
+
changedFiles.push(`${file} (removed)`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
category,
|
|
336
|
+
status: 'drifted',
|
|
337
|
+
reason: `Files changed: ${changedFiles.join(', ')}`,
|
|
338
|
+
needsRegeneration: true,
|
|
339
|
+
currentHash: current.combinedHash,
|
|
340
|
+
storedHash: stored.combinedHash,
|
|
341
|
+
changedFiles,
|
|
342
|
+
files: current.files
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
category,
|
|
348
|
+
status: 'synced',
|
|
349
|
+
reason: 'Hashes match',
|
|
350
|
+
needsRegeneration: false,
|
|
351
|
+
currentHash: current.combinedHash,
|
|
352
|
+
storedHash: stored.combinedHash,
|
|
353
|
+
files: current.files
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Check all knowledge file categories for drift
|
|
359
|
+
*/
|
|
360
|
+
function checkAllDrift() {
|
|
361
|
+
const syncState = loadSyncState();
|
|
362
|
+
|
|
363
|
+
const results = {
|
|
364
|
+
stack: checkCategoryDrift('stack', STACK_INDICATORS, syncState),
|
|
365
|
+
architecture: checkCategoryDrift('architecture', ARCHITECTURE_INDICATORS, syncState),
|
|
366
|
+
testing: checkCategoryDrift('testing', TESTING_INDICATORS, syncState)
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Check if knowledge files exist
|
|
370
|
+
results.stack.fileExists = fileExists(PATHS.stackMd);
|
|
371
|
+
results.architecture.fileExists = fileExists(PATHS.architectureMd);
|
|
372
|
+
results.testing.fileExists = fileExists(PATHS.testingMd);
|
|
373
|
+
|
|
374
|
+
// Overall status
|
|
375
|
+
const anyDrift = Object.values(results).some(r => r.needsRegeneration);
|
|
376
|
+
const anyMissing = !results.stack.fileExists || !results.architecture.fileExists || !results.testing.fileExists;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
overall: anyDrift || anyMissing ? 'stale' : 'synced',
|
|
380
|
+
lastSync: syncState?.lastSync || null,
|
|
381
|
+
categories: results,
|
|
382
|
+
anyDrift,
|
|
383
|
+
anyMissing
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Update sync state after regeneration
|
|
389
|
+
*/
|
|
390
|
+
function markAsSynced() {
|
|
391
|
+
const state = {
|
|
392
|
+
lastSync: new Date().toISOString(),
|
|
393
|
+
stack: computeCategoryHashes(STACK_INDICATORS),
|
|
394
|
+
architecture: computeCategoryHashes(ARCHITECTURE_INDICATORS),
|
|
395
|
+
testing: computeCategoryHashes(TESTING_INDICATORS)
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
saveSyncState(state);
|
|
399
|
+
return state;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Print human-readable status
|
|
404
|
+
*/
|
|
405
|
+
function printStatus(driftStatus) {
|
|
406
|
+
printHeader('KNOWLEDGE FILES SYNC STATUS');
|
|
407
|
+
|
|
408
|
+
if (driftStatus.lastSync) {
|
|
409
|
+
info(`Last synced: ${driftStatus.lastSync}`);
|
|
410
|
+
} else {
|
|
411
|
+
warn('Never synced - run "flow onboard" or "flow knowledge-sync regenerate"');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log('');
|
|
415
|
+
|
|
416
|
+
const categories = [
|
|
417
|
+
{ key: 'stack', name: 'Stack (stack.md)', file: PATHS.stackMd },
|
|
418
|
+
{ key: 'architecture', name: 'Architecture (architecture.md)', file: PATHS.architectureMd },
|
|
419
|
+
{ key: 'testing', name: 'Testing (testing.md)', file: PATHS.testingMd }
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
for (const { key, name, file } of categories) {
|
|
423
|
+
const status = driftStatus.categories[key];
|
|
424
|
+
printSection(name);
|
|
425
|
+
|
|
426
|
+
// File existence
|
|
427
|
+
if (status.fileExists) {
|
|
428
|
+
console.log(` ${color('green', '✓')} File exists`);
|
|
429
|
+
} else {
|
|
430
|
+
console.log(` ${color('red', '✗')} File missing`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Sync status
|
|
434
|
+
if (status.status === 'synced') {
|
|
435
|
+
console.log(` ${color('green', '✓')} In sync`);
|
|
436
|
+
} else if (status.status === 'drifted') {
|
|
437
|
+
console.log(` ${color('yellow', '⚠')} Drifted: ${status.reason}`);
|
|
438
|
+
} else {
|
|
439
|
+
console.log(` ${color('yellow', '○')} ${status.reason}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Indicator files
|
|
443
|
+
if (status.files.length > 0) {
|
|
444
|
+
console.log(` Tracked files: ${status.files.join(', ')}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log('');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Overall recommendation
|
|
451
|
+
printSection('📌 Recommendation');
|
|
452
|
+
if (driftStatus.overall === 'synced') {
|
|
453
|
+
console.log(` ${color('green', '✓')} All knowledge files are up to date`);
|
|
454
|
+
} else if (driftStatus.anyMissing) {
|
|
455
|
+
console.log(` Run: ${color('cyan', 'flow onboard')} to generate missing files`);
|
|
456
|
+
} else if (driftStatus.anyDrift) {
|
|
457
|
+
console.log(` Run: ${color('cyan', 'flow knowledge-sync regenerate')} to update drifted files`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.log('');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Regenerate knowledge files using onboard generators
|
|
465
|
+
* @param {string[]} categories - Categories to regenerate
|
|
466
|
+
* @returns {Promise<Object>} New sync state or null if failed
|
|
467
|
+
*/
|
|
468
|
+
async function regenerateKnowledgeFiles(categories = ['stack', 'architecture', 'testing']) {
|
|
469
|
+
info('Regenerating knowledge files...');
|
|
470
|
+
|
|
471
|
+
const { spawn } = require('child_process');
|
|
472
|
+
|
|
473
|
+
return new Promise((resolve, reject) => {
|
|
474
|
+
// Use spawn with explicit args array and absolute path to prevent command injection
|
|
475
|
+
const scriptPath = path.join(PROJECT_ROOT, 'scripts', 'flow-onboard');
|
|
476
|
+
const child = spawn('node', [scriptPath, '--update-knowledge'], {
|
|
477
|
+
cwd: PROJECT_ROOT,
|
|
478
|
+
stdio: 'inherit'
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
child.on('error', (err) => {
|
|
482
|
+
// Spawn failed (e.g., node not found)
|
|
483
|
+
error(`Failed to spawn process: ${err.message}`);
|
|
484
|
+
warn('Run "flow onboard" manually to regenerate knowledge files');
|
|
485
|
+
resolve(null);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
child.on('close', (code) => {
|
|
489
|
+
if (code === 0) {
|
|
490
|
+
// Success - update sync state
|
|
491
|
+
const state = markAsSynced();
|
|
492
|
+
success('Knowledge files regenerated and sync state updated');
|
|
493
|
+
resolve(state);
|
|
494
|
+
} else if (code === null) {
|
|
495
|
+
// Process was killed
|
|
496
|
+
warn('Regeneration process was terminated');
|
|
497
|
+
resolve(null);
|
|
498
|
+
} else {
|
|
499
|
+
// Non-zero exit - onboard command may not support --update-knowledge
|
|
500
|
+
warn(`Onboard exited with code ${code}`);
|
|
501
|
+
info('The --update-knowledge flag may not be supported yet.');
|
|
502
|
+
info('Options:');
|
|
503
|
+
info(' 1. Run "flow onboard" to regenerate all knowledge files');
|
|
504
|
+
info(' 2. Run "flow knowledge-sync mark-synced" to accept current state');
|
|
505
|
+
// Do NOT mark as synced - this would be misleading
|
|
506
|
+
resolve(null);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Main entry point
|
|
514
|
+
*/
|
|
515
|
+
async function main() {
|
|
516
|
+
const { positional, flags } = parseFlags(process.argv.slice(2));
|
|
517
|
+
const command = positional[0] || 'status';
|
|
518
|
+
|
|
519
|
+
const driftStatus = checkAllDrift();
|
|
520
|
+
|
|
521
|
+
// JSON output
|
|
522
|
+
if (flags.json) {
|
|
523
|
+
outputJson({
|
|
524
|
+
success: true,
|
|
525
|
+
command,
|
|
526
|
+
...driftStatus
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
switch (command) {
|
|
532
|
+
case 'status':
|
|
533
|
+
case 'check':
|
|
534
|
+
printStatus(driftStatus);
|
|
535
|
+
// Exit with code 1 if stale (useful for CI)
|
|
536
|
+
process.exit(driftStatus.overall === 'stale' ? 1 : 0);
|
|
537
|
+
break;
|
|
538
|
+
|
|
539
|
+
case 'regenerate':
|
|
540
|
+
case 'sync':
|
|
541
|
+
case 'update':
|
|
542
|
+
if (driftStatus.overall === 'synced' && !flags.force) {
|
|
543
|
+
success('Knowledge files are already in sync');
|
|
544
|
+
info('Use --force to regenerate anyway');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
await regenerateKnowledgeFiles();
|
|
548
|
+
break;
|
|
549
|
+
|
|
550
|
+
case 'mark-synced':
|
|
551
|
+
// Manual mark as synced (for testing or after manual edits)
|
|
552
|
+
markAsSynced();
|
|
553
|
+
success('Sync state updated');
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
default:
|
|
557
|
+
error(`Unknown command: ${command}`);
|
|
558
|
+
console.log('');
|
|
559
|
+
console.log('Usage:');
|
|
560
|
+
console.log(' flow knowledge-sync status Check sync status');
|
|
561
|
+
console.log(' flow knowledge-sync check Check and report drift');
|
|
562
|
+
console.log(' flow knowledge-sync regenerate Regenerate stale files');
|
|
563
|
+
console.log(' flow knowledge-sync mark-synced Mark current state as synced');
|
|
564
|
+
console.log('');
|
|
565
|
+
console.log('Options:');
|
|
566
|
+
console.log(' --json Output in JSON format');
|
|
567
|
+
console.log(' --force Force regeneration even if synced');
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Export for use by other scripts
|
|
573
|
+
module.exports = {
|
|
574
|
+
checkAllDrift,
|
|
575
|
+
checkCategoryDrift,
|
|
576
|
+
markAsSynced,
|
|
577
|
+
loadSyncState,
|
|
578
|
+
computeCategoryHashes,
|
|
579
|
+
STACK_INDICATORS,
|
|
580
|
+
ARCHITECTURE_INDICATORS,
|
|
581
|
+
TESTING_INDICATORS
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
if (require.main === module) {
|
|
585
|
+
main().catch(err => {
|
|
586
|
+
error(err.message);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
});
|
|
589
|
+
}
|