wogiflow 2.4.2 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/wogi-start.md +124 -0
- package/.claude/docs/claude-code-compatibility.md +51 -0
- package/.claude/docs/explore-agents.md +11 -0
- package/.claude/settings.json +12 -1
- package/.workflow/models/registry.json +1 -1
- package/bin/flow +11 -1
- package/lib/workspace-contracts.js +599 -0
- package/lib/workspace-intelligence.js +600 -0
- package/lib/workspace-messages.js +441 -0
- package/lib/workspace-routing.js +485 -0
- package/lib/workspace-sync.js +339 -0
- package/lib/workspace.js +1073 -0
- package/package.json +4 -4
- package/scripts/MEMORY-ARCHITECTURE.md +1 -1
- package/scripts/base-workflow-step.js +136 -0
- package/scripts/flow-adaptive-learning.js +8 -9
- package/scripts/flow-aggregate.js +11 -6
- package/scripts/flow-api-index.js +4 -6
- package/scripts/flow-assumption-detector.js +0 -2
- package/scripts/flow-audit.js +15 -2
- package/scripts/flow-auto-context.js +8 -12
- package/scripts/flow-auto-learn.js +49 -49
- package/scripts/flow-background.js +5 -6
- package/scripts/flow-bridge-state.js +8 -10
- package/scripts/flow-bulk-loop.js +1 -3
- package/scripts/flow-bulk-orchestrator.js +1 -3
- package/scripts/flow-cascade-completion.js +0 -2
- package/scripts/flow-cascade.js +4 -4
- package/scripts/flow-checkpoint.js +10 -13
- package/scripts/flow-code-intelligence.js +10 -12
- package/scripts/flow-community-sync.js +4 -4
- package/scripts/flow-community.js +12 -20
- package/scripts/flow-config-defaults.js +28 -2
- package/scripts/flow-config-interactive.js +9 -5
- package/scripts/flow-config-loader.js +49 -92
- package/scripts/flow-config-substitution.js +0 -2
- package/scripts/flow-context-estimator.js +4 -4
- package/scripts/flow-context-init.js +10 -12
- package/scripts/flow-context-manager.js +0 -2
- package/scripts/flow-context-scoring.js +2 -2
- package/scripts/flow-contract-scan.js +6 -9
- package/scripts/flow-correct.js +29 -27
- package/scripts/flow-correction-detector.js +5 -1
- package/scripts/flow-damage-control.js +47 -54
- package/scripts/flow-decisions-merge.js +4 -14
- package/scripts/flow-diff.js +5 -8
- package/scripts/flow-done-gates.js +786 -0
- package/scripts/flow-done-report.js +123 -0
- package/scripts/flow-done.js +71 -717
- package/scripts/flow-entropy-monitor.js +1 -3
- package/scripts/flow-eval-calibration.js +257 -0
- package/scripts/flow-eval-judge.js +10 -1
- package/scripts/flow-eval.js +14 -5
- package/scripts/flow-extraction-review.js +1 -0
- package/scripts/flow-failure-categories.js +0 -2
- package/scripts/flow-figma-confirm.js +5 -9
- package/scripts/flow-figma-generate.js +8 -10
- package/scripts/flow-figma-index.js +8 -10
- package/scripts/flow-figma-match.js +3 -5
- package/scripts/flow-figma-mcp-server.js +2 -4
- package/scripts/flow-figma-orchestrator.js +2 -3
- package/scripts/flow-figma-registry.js +2 -3
- package/scripts/flow-framework-resolver.js +0 -2
- package/scripts/flow-function-index.js +4 -6
- package/scripts/flow-gate-confidence.js +2 -2
- package/scripts/flow-gitignore.js +0 -2
- package/scripts/flow-guided-edit.js +5 -6
- package/scripts/flow-health.js +5 -6
- package/scripts/flow-hook-errors.js +6 -0
- package/scripts/flow-hook-status.js +263 -0
- package/scripts/flow-hooks.js +17 -29
- package/scripts/flow-http-client.js +9 -8
- package/scripts/flow-hybrid-interactive.js +7 -12
- package/scripts/flow-hybrid-test.js +12 -13
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-io.js +21 -4
- package/scripts/flow-knowledge-router.js +9 -3
- package/scripts/flow-learning-orchestrator.js +318 -13
- package/scripts/flow-links.js +5 -7
- package/scripts/flow-long-input-association.js +275 -0
- package/scripts/flow-long-input-chunking.js +1 -0
- package/scripts/flow-long-input-cli.js +0 -2
- package/scripts/flow-long-input-complexity.js +0 -2
- package/scripts/flow-long-input-constants.js +0 -2
- package/scripts/flow-long-input-contradictions.js +351 -0
- package/scripts/flow-long-input-detection.js +0 -2
- package/scripts/flow-long-input-passes.js +885 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-long-input-voice.js +0 -2
- package/scripts/flow-long-input.js +425 -3005
- package/scripts/flow-loop-retry-learning.js +2 -3
- package/scripts/flow-lsp.js +3 -3
- package/scripts/flow-mcp-docs.js +3 -4
- package/scripts/flow-memory-db.js +6 -8
- package/scripts/flow-memory-sync.js +18 -11
- package/scripts/flow-metrics.js +1 -2
- package/scripts/flow-model-adapter.js +2 -3
- package/scripts/flow-model-config.js +72 -104
- package/scripts/flow-model-router.js +2 -2
- package/scripts/flow-model-types.js +0 -2
- package/scripts/flow-multi-approach.js +5 -6
- package/scripts/flow-orchestrate-context.js +3 -7
- package/scripts/flow-orchestrate-rollback.js +3 -8
- package/scripts/flow-orchestrate-state.js +8 -14
- package/scripts/flow-orchestrate-templates.js +2 -6
- package/scripts/flow-orchestrate-validator.js +5 -9
- package/scripts/flow-orchestrate.js +126 -103
- package/scripts/flow-output.js +0 -2
- package/scripts/flow-parallel.js +1 -1
- package/scripts/flow-paths.js +23 -2
- package/scripts/flow-pattern-enforcer.js +30 -28
- package/scripts/flow-pattern-extractor.js +3 -4
- package/scripts/flow-pending.js +0 -2
- package/scripts/flow-permissions.js +2 -3
- package/scripts/flow-plugin-registry.js +10 -12
- package/scripts/flow-prd-manager.js +1 -1
- package/scripts/flow-progress.js +7 -9
- package/scripts/flow-prompt-composer.js +3 -3
- package/scripts/flow-prompt-template.js +2 -2
- package/scripts/flow-providers.js +7 -4
- package/scripts/flow-registry-manager.js +7 -12
- package/scripts/flow-regression.js +9 -11
- package/scripts/flow-roadmap.js +2 -2
- package/scripts/flow-run-trace.js +16 -15
- package/scripts/flow-safety.js +2 -5
- package/scripts/flow-scanner-base.js +5 -7
- package/scripts/flow-scenario-engine.js +1 -5
- package/scripts/flow-security.js +29 -0
- package/scripts/flow-session-end.js +32 -41
- package/scripts/flow-session-learning.js +53 -49
- package/scripts/flow-setup-hooks.js +2 -3
- package/scripts/flow-skill-create.js +7 -12
- package/scripts/flow-skill-generator.js +12 -16
- package/scripts/flow-skill-learn.js +17 -8
- package/scripts/flow-skill-matcher.js +1 -2
- package/scripts/flow-spec-generator.js +2 -4
- package/scripts/flow-stack-wizard.js +5 -7
- package/scripts/flow-standards-learner.js +35 -16
- package/scripts/flow-start.js +2 -0
- package/scripts/flow-stats-collector.js +2 -2
- package/scripts/flow-status.js +10 -10
- package/scripts/flow-statusline-setup.js +2 -2
- package/scripts/flow-step-changelog.js +2 -3
- package/scripts/flow-step-comments.js +66 -81
- package/scripts/flow-step-complexity.js +50 -70
- package/scripts/flow-step-coverage.js +3 -5
- package/scripts/flow-step-knowledge.js +2 -3
- package/scripts/flow-step-pr-tests.js +64 -74
- package/scripts/flow-step-regression.js +3 -5
- package/scripts/flow-step-review.js +86 -103
- package/scripts/flow-step-security.js +111 -121
- package/scripts/flow-step-silent-failures.js +56 -83
- package/scripts/flow-step-simplifier.js +52 -70
- package/scripts/flow-story.js +4 -7
- package/scripts/flow-strict-adherence.js +3 -4
- package/scripts/flow-task-checkpoint.js +36 -5
- package/scripts/flow-task-enforcer.js +2 -24
- package/scripts/flow-tech-debt.js +1 -1
- package/scripts/flow-template-extractor.js +1 -0
- package/scripts/flow-templates.js +11 -13
- package/scripts/flow-test-api.js +9 -13
- package/scripts/flow-test-discovery.js +1 -1
- package/scripts/flow-test-generate.js +5 -9
- package/scripts/flow-test-integrity.js +3 -7
- package/scripts/flow-test-ui.js +5 -9
- package/scripts/flow-testing-deps.js +1 -3
- package/scripts/flow-tiered-learning.js +4 -4
- package/scripts/flow-todowrite-sync.js +1 -1
- package/scripts/flow-tokens.js +0 -2
- package/scripts/flow-verification-profile.js +6 -10
- package/scripts/flow-verify.js +12 -16
- package/scripts/flow-version-check.js +4 -12
- package/scripts/flow-webmcp-generator.js +3 -5
- package/scripts/flow-workflow-steps.js +0 -2
- package/scripts/flow-workflow.js +9 -11
- package/scripts/hooks/adapters/claude-code.js +31 -0
- package/scripts/hooks/core/config-change.js +1 -0
- package/scripts/hooks/core/extension-registry.js +0 -2
- package/scripts/hooks/core/instructions-loaded.js +1 -1
- package/scripts/hooks/core/observation-capture.js +5 -5
- package/scripts/hooks/core/phase-gate.js +5 -0
- package/scripts/hooks/core/post-compact.js +1 -12
- package/scripts/hooks/core/research-gate.js +2 -12
- package/scripts/hooks/core/routing-gate.js +6 -0
- package/scripts/hooks/core/task-completed.js +12 -0
- package/scripts/hooks/core/task-created.js +83 -0
- package/scripts/hooks/core/worktree-lifecycle.js +1 -1
- package/scripts/hooks/entry/claude-code/config-change.js +6 -29
- package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
- package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
- package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
- package/scripts/hooks/entry/claude-code/session-end.js +4 -28
- package/scripts/hooks/entry/claude-code/session-start.js +205 -243
- package/scripts/hooks/entry/claude-code/setup.js +8 -49
- package/scripts/hooks/entry/claude-code/stop.js +40 -72
- package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
- package/scripts/hooks/entry/claude-code/task-created.js +15 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
- package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
- package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
- package/scripts/hooks/entry/shared/hook-runner.js +99 -0
- package/scripts/hooks/entry/shared/read-stdin.js +0 -2
- package/scripts/postinstall.js +2 -0
- package/scripts/registries/api-registry.js +0 -2
- package/scripts/registries/component-registry.js +5 -9
- package/scripts/registries/contract-scanner.js +2 -9
- package/scripts/registries/function-registry.js +0 -2
- package/scripts/registries/schema-registry.js +14 -18
- package/scripts/registries/service-registry.js +23 -27
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Workspace — Cross-Repo Intelligence + N-Repo Scaling + Cloud Prep
|
|
5
|
+
*
|
|
6
|
+
* Stories 5-8 (wf-bbc47dc1, wf-d2e01566, wf-868ba5a5):
|
|
7
|
+
* Contract drift detection, blame router, shared decisions, API changelog,
|
|
8
|
+
* cross-repo ready queue, integration testing gate, graph-based integration map,
|
|
9
|
+
* library support, cascading propagation, workspace health, and cloud interfaces.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// S5: Contract Drift Detection (Criterion 1)
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compare actual implementation against the contract spec.
|
|
21
|
+
* Scans provider api-maps and compares with contracts/.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} workspaceRoot
|
|
24
|
+
* @param {Object} manifest
|
|
25
|
+
* @returns {Array<Object>} drift entries
|
|
26
|
+
*/
|
|
27
|
+
function detectContractDrift(workspaceRoot, manifest) {
|
|
28
|
+
const drifts = [];
|
|
29
|
+
const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
|
|
30
|
+
if (!fs.existsSync(contractsDir)) return drifts;
|
|
31
|
+
|
|
32
|
+
// Load all contracts
|
|
33
|
+
const contractEndpoints = new Set();
|
|
34
|
+
const files = fs.readdirSync(contractsDir).filter(f => f.endsWith('.json'));
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
try {
|
|
38
|
+
const spec = JSON.parse(fs.readFileSync(path.join(contractsDir, file), 'utf-8'));
|
|
39
|
+
if (spec.paths) {
|
|
40
|
+
for (const [routePath, methods] of Object.entries(spec.paths)) {
|
|
41
|
+
for (const method of Object.keys(methods)) {
|
|
42
|
+
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
|
|
43
|
+
contractEndpoints.add(`${method.toUpperCase()} ${routePath}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (_err) {
|
|
49
|
+
// Skip malformed contracts
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (contractEndpoints.size === 0) return drifts;
|
|
54
|
+
|
|
55
|
+
// Compare each provider's actual endpoints against contract
|
|
56
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
57
|
+
if (member.role !== 'provider' && member.role !== 'both') continue;
|
|
58
|
+
|
|
59
|
+
const actualEndpoints = new Set(member.provides || []);
|
|
60
|
+
|
|
61
|
+
// Endpoints in contract but not in actual implementation
|
|
62
|
+
for (const contractEp of contractEndpoints) {
|
|
63
|
+
const hasMatch = [...actualEndpoints].some(actual => {
|
|
64
|
+
const normActual = normalizeForDrift(actual);
|
|
65
|
+
const normContract = normalizeForDrift(contractEp);
|
|
66
|
+
return normActual === normContract;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!hasMatch) {
|
|
70
|
+
drifts.push({
|
|
71
|
+
type: 'missing-implementation',
|
|
72
|
+
member: name,
|
|
73
|
+
endpoint: contractEp,
|
|
74
|
+
severity: 'high',
|
|
75
|
+
message: `Contract defines ${contractEp} but ${name} does not implement it`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Endpoints in implementation but not in contract
|
|
81
|
+
for (const actualEp of actualEndpoints) {
|
|
82
|
+
const hasMatch = [...contractEndpoints].some(contractEp => {
|
|
83
|
+
return normalizeForDrift(actualEp) === normalizeForDrift(contractEp);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!hasMatch) {
|
|
87
|
+
drifts.push({
|
|
88
|
+
type: 'undocumented-endpoint',
|
|
89
|
+
member: name,
|
|
90
|
+
endpoint: actualEp,
|
|
91
|
+
severity: 'medium',
|
|
92
|
+
message: `${name} implements ${actualEp} but it's not in any contract`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return drifts;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeForDrift(ep) {
|
|
102
|
+
const parts = ep.trim().split(/\s+/);
|
|
103
|
+
const method = (parts[0] || 'GET').toUpperCase();
|
|
104
|
+
let urlPath = parts.slice(1).join(' ');
|
|
105
|
+
urlPath = urlPath.replace(/\{[^}]+\}/g, ':param');
|
|
106
|
+
urlPath = urlPath.replace(/:\w+/g, ':param');
|
|
107
|
+
urlPath = urlPath.replace(/\/\d+/g, '/:param');
|
|
108
|
+
return `${method} ${urlPath}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================
|
|
112
|
+
// S5: Blame Router (Criterion 2)
|
|
113
|
+
// ============================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Analyze a bug report and determine the most likely repo to blame.
|
|
117
|
+
* Uses recent git changes + contract + error analysis.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} workspaceRoot
|
|
120
|
+
* @param {string} bugDescription
|
|
121
|
+
* @param {Object} manifest
|
|
122
|
+
* @returns {Object} blame analysis
|
|
123
|
+
*/
|
|
124
|
+
function routeBlame(workspaceRoot, bugDescription, manifest) {
|
|
125
|
+
const desc = bugDescription.toLowerCase();
|
|
126
|
+
const scores = {};
|
|
127
|
+
const evidence = {};
|
|
128
|
+
|
|
129
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
130
|
+
scores[name] = 0;
|
|
131
|
+
evidence[name] = [];
|
|
132
|
+
|
|
133
|
+
// Check if bug mentions endpoints this repo owns
|
|
134
|
+
for (const ep of (member.provides || [])) {
|
|
135
|
+
const epPath = ep.split(' ').slice(1).join(' ').toLowerCase();
|
|
136
|
+
if (desc.includes(epPath)) {
|
|
137
|
+
scores[name] += 3;
|
|
138
|
+
evidence[name].push(`Bug mentions endpoint ${ep} which ${name} provides`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if bug mentions components/pages (consumer keywords)
|
|
143
|
+
if (member.role === 'consumer' || member.role === 'both') {
|
|
144
|
+
const uiKeywords = ['page', 'screen', 'component', 'form', 'button', 'blank', 'not showing', 'not loading', 'ui'];
|
|
145
|
+
for (const kw of uiKeywords) {
|
|
146
|
+
if (desc.includes(kw)) {
|
|
147
|
+
scores[name] += 1;
|
|
148
|
+
evidence[name].push(`Bug mentions UI keyword: "${kw}"`);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check for error codes (5xx = likely backend, 4xx = could be either)
|
|
155
|
+
if (member.role === 'provider' || member.role === 'both') {
|
|
156
|
+
if (desc.includes('500') || desc.includes('502') || desc.includes('503') || desc.includes('server error')) {
|
|
157
|
+
scores[name] += 3;
|
|
158
|
+
evidence[name].push('Server error (5xx) suggests backend issue');
|
|
159
|
+
}
|
|
160
|
+
if (desc.includes('database') || desc.includes('query') || desc.includes('migration')) {
|
|
161
|
+
scores[name] += 2;
|
|
162
|
+
evidence[name].push('Database-related keywords');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check recent git changes (if accessible)
|
|
167
|
+
try {
|
|
168
|
+
const memberPath = path.resolve(workspaceRoot, member.path || `./${name}`);
|
|
169
|
+
const { execFileSync } = require('node:child_process');
|
|
170
|
+
const recentChanges = execFileSync('git', ['log', '--oneline', '-5', '--since=24 hours ago'], {
|
|
171
|
+
cwd: memberPath, encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
172
|
+
}).trim();
|
|
173
|
+
|
|
174
|
+
if (recentChanges) {
|
|
175
|
+
const changeCount = recentChanges.split('\n').filter(Boolean).length;
|
|
176
|
+
scores[name] += changeCount; // More recent changes = more likely to have introduced a bug
|
|
177
|
+
evidence[name].push(`${changeCount} commit(s) in the last 24 hours`);
|
|
178
|
+
}
|
|
179
|
+
} catch (_err) {
|
|
180
|
+
// Git not available or timeout — skip
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sort by score
|
|
185
|
+
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
186
|
+
const primarySuspect = sorted[0]?.[0];
|
|
187
|
+
const confidence = sorted[0]?.[1] > 0
|
|
188
|
+
? sorted[0][1] > (sorted[1]?.[1] || 0) * 2 ? 'high' : 'medium'
|
|
189
|
+
: 'low';
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
primarySuspect,
|
|
193
|
+
confidence,
|
|
194
|
+
scores,
|
|
195
|
+
evidence,
|
|
196
|
+
recommendation: confidence === 'high'
|
|
197
|
+
? `Start investigation in ${primarySuspect}`
|
|
198
|
+
: `Investigate both repos in parallel — evidence is inconclusive`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================
|
|
203
|
+
// S5: Shared Decision Layer (Criterion 3)
|
|
204
|
+
// ============================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Read workspace-level shared decisions
|
|
208
|
+
* @param {string} workspaceRoot
|
|
209
|
+
* @returns {string} decisions content
|
|
210
|
+
*/
|
|
211
|
+
function getSharedDecisions(workspaceRoot) {
|
|
212
|
+
const decisionsPath = path.join(workspaceRoot, '.workspace', 'state', 'decisions.md');
|
|
213
|
+
try {
|
|
214
|
+
if (fs.existsSync(decisionsPath)) {
|
|
215
|
+
return fs.readFileSync(decisionsPath, 'utf-8');
|
|
216
|
+
}
|
|
217
|
+
} catch (_err) {
|
|
218
|
+
// Non-critical
|
|
219
|
+
}
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Add a shared decision that applies across all repos
|
|
225
|
+
* @param {string} workspaceRoot
|
|
226
|
+
* @param {string} title
|
|
227
|
+
* @param {string} content
|
|
228
|
+
*/
|
|
229
|
+
function addSharedDecision(workspaceRoot, title, content) {
|
|
230
|
+
const decisionsPath = path.join(workspaceRoot, '.workspace', 'state', 'decisions.md');
|
|
231
|
+
let existing = '';
|
|
232
|
+
try {
|
|
233
|
+
if (fs.existsSync(decisionsPath)) {
|
|
234
|
+
existing = fs.readFileSync(decisionsPath, 'utf-8');
|
|
235
|
+
}
|
|
236
|
+
} catch (_err) {
|
|
237
|
+
if (!fs.existsSync(decisionsPath)) {
|
|
238
|
+
existing = '# Workspace Decisions\n\nCross-repo rules that apply to all member repositories.\n';
|
|
239
|
+
} else {
|
|
240
|
+
// File exists but can't be read — don't risk overwriting
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const entry = `\n### ${title}\n\n${content}\n\n*Added: ${new Date().toISOString().split('T')[0]}*\n`;
|
|
246
|
+
fs.writeFileSync(decisionsPath, existing + entry);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================
|
|
250
|
+
// S5: API Changelog (Criterion 4) — delegates to workspace-contracts
|
|
251
|
+
// S5: Cross-Repo Ready Queue (Criterion 5) — workspace state/ready.json
|
|
252
|
+
// S5: Integration Testing Gate (Criterion 6) — part of routing verify step
|
|
253
|
+
// ============================================================
|
|
254
|
+
|
|
255
|
+
// ============================================================
|
|
256
|
+
// S7: Graph-Based Integration Map (Criterion 1)
|
|
257
|
+
// ============================================================
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Build a dependency graph across N repos.
|
|
261
|
+
* Nodes = repos, edges = endpoint dependencies.
|
|
262
|
+
*
|
|
263
|
+
* @param {Object} manifest
|
|
264
|
+
* @returns {Object} graph { nodes, edges, adjacency }
|
|
265
|
+
*/
|
|
266
|
+
function buildDependencyGraph(manifest) {
|
|
267
|
+
const graph = {
|
|
268
|
+
nodes: [],
|
|
269
|
+
edges: [],
|
|
270
|
+
adjacency: {} // memberName → [dependsOn]
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
274
|
+
graph.nodes.push({
|
|
275
|
+
name,
|
|
276
|
+
role: member.role,
|
|
277
|
+
endpointCount: (member.provides || []).length + (member.consumes || []).length
|
|
278
|
+
});
|
|
279
|
+
graph.adjacency[name] = [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Build edges from matched integrations
|
|
283
|
+
const matched = manifest.integrations?.matched || [];
|
|
284
|
+
for (const m of matched) {
|
|
285
|
+
for (const consumer of (m.consumers || [])) {
|
|
286
|
+
for (const provider of (m.providers || [])) {
|
|
287
|
+
if (consumer !== provider) {
|
|
288
|
+
graph.edges.push({
|
|
289
|
+
from: consumer,
|
|
290
|
+
to: provider,
|
|
291
|
+
endpoint: m.endpoint,
|
|
292
|
+
type: 'consumes'
|
|
293
|
+
});
|
|
294
|
+
if (!graph.adjacency[consumer].includes(provider)) {
|
|
295
|
+
graph.adjacency[consumer].push(provider);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return graph;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================================================
|
|
306
|
+
// S7: Library Repo Support (Criterion 3)
|
|
307
|
+
// ============================================================
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Find all repos that depend on a library repo.
|
|
311
|
+
* Used for cascading change propagation.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} libraryName
|
|
314
|
+
* @param {Object} manifest
|
|
315
|
+
* @returns {string[]} consumer repo names
|
|
316
|
+
*/
|
|
317
|
+
function getLibraryConsumers(libraryName, manifest) {
|
|
318
|
+
const consumers = [];
|
|
319
|
+
|
|
320
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
321
|
+
if (name === libraryName) continue;
|
|
322
|
+
|
|
323
|
+
// Check if this repo imports from the library
|
|
324
|
+
// (Would need to check package.json dependencies in real implementation)
|
|
325
|
+
// For now, all non-library repos are potential consumers
|
|
326
|
+
if (member.role !== 'library') {
|
|
327
|
+
consumers.push(name);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return consumers;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================
|
|
335
|
+
// S7: Cascading Change Propagation (Criterion 4)
|
|
336
|
+
// ============================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* When a repo changes, determine which other repos need notification.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} changedRepo
|
|
342
|
+
* @param {Object} manifest
|
|
343
|
+
* @param {Object} graph — from buildDependencyGraph()
|
|
344
|
+
* @returns {string[]} repos that need notification
|
|
345
|
+
*/
|
|
346
|
+
function getCascadeTargets(changedRepo, manifest, graph) {
|
|
347
|
+
const targets = new Set();
|
|
348
|
+
|
|
349
|
+
// Direct dependents (repos that consume from changedRepo)
|
|
350
|
+
for (const edge of graph.edges) {
|
|
351
|
+
if (edge.to === changedRepo) {
|
|
352
|
+
targets.add(edge.from);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// If changedRepo is a library, all non-library repos are potential targets
|
|
357
|
+
const member = manifest.members[changedRepo];
|
|
358
|
+
if (member?.role === 'library') {
|
|
359
|
+
for (const name of getLibraryConsumers(changedRepo, manifest)) {
|
|
360
|
+
targets.add(name);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return [...targets];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================================
|
|
368
|
+
// S7: Workspace Health Check (Criterion 5)
|
|
369
|
+
// ============================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check workspace health — stale manifests, broken contracts, unsynced repos.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} workspaceRoot
|
|
375
|
+
* @param {Object} manifest
|
|
376
|
+
* @returns {Object} health report
|
|
377
|
+
*/
|
|
378
|
+
function checkWorkspaceHealth(workspaceRoot, manifest) {
|
|
379
|
+
const issues = [];
|
|
380
|
+
const checks = { total: 0, passed: 0, failed: 0, warnings: 0 };
|
|
381
|
+
|
|
382
|
+
// Check 1: All member repos still exist
|
|
383
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
384
|
+
checks.total++;
|
|
385
|
+
const memberPath = path.resolve(workspaceRoot, member.path || `./${name}`);
|
|
386
|
+
if (!fs.existsSync(memberPath)) {
|
|
387
|
+
issues.push({ severity: 'error', check: 'member-exists', message: `Member '${name}' path does not exist: ${path.relative(workspaceRoot, memberPath)}` });
|
|
388
|
+
checks.failed++;
|
|
389
|
+
} else {
|
|
390
|
+
checks.passed++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check 2: All members have .workflow/
|
|
395
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
396
|
+
checks.total++;
|
|
397
|
+
const workflowPath = path.join(path.resolve(workspaceRoot, member.path || `./${name}`), '.workflow');
|
|
398
|
+
if (!fs.existsSync(workflowPath)) {
|
|
399
|
+
issues.push({ severity: 'warning', check: 'workflow-exists', message: `Member '${name}' has no .workflow/ directory` });
|
|
400
|
+
checks.warnings++;
|
|
401
|
+
} else {
|
|
402
|
+
checks.passed++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Check 3: Manifest freshness
|
|
407
|
+
checks.total++;
|
|
408
|
+
const manifestPath = path.join(workspaceRoot, '.workspace', 'state', 'workspace-manifest.json');
|
|
409
|
+
if (fs.existsSync(manifestPath)) {
|
|
410
|
+
const stat = fs.statSync(manifestPath);
|
|
411
|
+
const ageHours = (Date.now() - stat.mtime.getTime()) / (1000 * 60 * 60);
|
|
412
|
+
if (ageHours > 24) {
|
|
413
|
+
issues.push({ severity: 'warning', check: 'manifest-fresh', message: `Manifest is ${Math.floor(ageHours)}h old — run 'flow workspace sync'` });
|
|
414
|
+
checks.warnings++;
|
|
415
|
+
} else {
|
|
416
|
+
checks.passed++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check 4: Contract drift
|
|
421
|
+
checks.total++;
|
|
422
|
+
const drifts = detectContractDrift(workspaceRoot, manifest);
|
|
423
|
+
if (drifts.length > 0) {
|
|
424
|
+
const highDrifts = drifts.filter(d => d.severity === 'high');
|
|
425
|
+
if (highDrifts.length > 0) {
|
|
426
|
+
issues.push({ severity: 'error', check: 'contract-drift', message: `${highDrifts.length} high-severity contract drift(s) detected` });
|
|
427
|
+
checks.failed++;
|
|
428
|
+
} else {
|
|
429
|
+
issues.push({ severity: 'warning', check: 'contract-drift', message: `${drifts.length} contract drift(s) detected` });
|
|
430
|
+
checks.warnings++;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
checks.passed++;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check 5: Unread messages
|
|
437
|
+
checks.total++;
|
|
438
|
+
try {
|
|
439
|
+
const { readMessages } = require('./workspace-messages');
|
|
440
|
+
const pending = readMessages(workspaceRoot, { status: 'pending' });
|
|
441
|
+
if (pending.length > 5) {
|
|
442
|
+
issues.push({ severity: 'warning', check: 'pending-messages', message: `${pending.length} unread messages — review with 'show messages'` });
|
|
443
|
+
checks.warnings++;
|
|
444
|
+
} else {
|
|
445
|
+
checks.passed++;
|
|
446
|
+
}
|
|
447
|
+
} catch (_err) {
|
|
448
|
+
checks.passed++; // No messages module = no issue
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check 6: Orphaned consumers
|
|
452
|
+
checks.total++;
|
|
453
|
+
const orphanedC = manifest.integrations?.orphanedConsumers?.length || 0;
|
|
454
|
+
if (orphanedC > 0) {
|
|
455
|
+
issues.push({ severity: 'warning', check: 'orphaned-consumers', message: `${orphanedC} consumer(s) calling endpoints with no provider` });
|
|
456
|
+
checks.warnings++;
|
|
457
|
+
} else {
|
|
458
|
+
checks.passed++;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
healthy: checks.failed === 0,
|
|
463
|
+
checks,
|
|
464
|
+
issues,
|
|
465
|
+
summary: checks.failed > 0
|
|
466
|
+
? `Unhealthy: ${checks.failed} error(s), ${checks.warnings} warning(s)`
|
|
467
|
+
: checks.warnings > 0
|
|
468
|
+
? `OK with ${checks.warnings} warning(s)`
|
|
469
|
+
: 'All checks passed'
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ============================================================
|
|
474
|
+
// S8: Cloud Preparation — Extension Points & Interfaces
|
|
475
|
+
// ============================================================
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Export workspace state in a format suitable for cloud dashboard consumption.
|
|
479
|
+
* This is the interface contract between OSS and cloud.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} workspaceRoot
|
|
482
|
+
* @param {Object} manifest
|
|
483
|
+
* @returns {Object} dashboard-ready workspace state
|
|
484
|
+
*/
|
|
485
|
+
function exportForDashboard(workspaceRoot, manifest) {
|
|
486
|
+
const { readMessages } = require('./workspace-messages');
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
version: '1.0.0',
|
|
490
|
+
exportedAt: new Date().toISOString(),
|
|
491
|
+
workspace: {
|
|
492
|
+
name: manifest.workspace,
|
|
493
|
+
memberCount: Object.keys(manifest.members).length,
|
|
494
|
+
members: Object.entries(manifest.members).map(([name, m]) => ({
|
|
495
|
+
name,
|
|
496
|
+
role: m.role,
|
|
497
|
+
stack: m.stack,
|
|
498
|
+
endpointsProvided: (m.provides || []).length,
|
|
499
|
+
endpointsConsumed: (m.consumes || []).length
|
|
500
|
+
}))
|
|
501
|
+
},
|
|
502
|
+
integrations: {
|
|
503
|
+
matchedCount: manifest.integrations?.matched?.length || 0,
|
|
504
|
+
orphanedConsumers: manifest.integrations?.orphanedConsumers?.length || 0,
|
|
505
|
+
orphanedProviders: manifest.integrations?.orphanedProviders?.length || 0,
|
|
506
|
+
typeDrifts: manifest.integrations?.typeDrift?.length || 0,
|
|
507
|
+
contractDrifts: detectContractDrift(workspaceRoot, manifest).length
|
|
508
|
+
},
|
|
509
|
+
messages: (() => {
|
|
510
|
+
const allMessages = readMessages(workspaceRoot);
|
|
511
|
+
return {
|
|
512
|
+
total: allMessages.length,
|
|
513
|
+
pending: allMessages.filter(m => m.status === 'pending').length
|
|
514
|
+
};
|
|
515
|
+
})(),
|
|
516
|
+
health: checkWorkspaceHealth(workspaceRoot, manifest)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Message bus abstraction — provides the interface that cloud can swap for HTTP/WebSocket.
|
|
522
|
+
* OSS uses file-based transport. Cloud overrides with network transport.
|
|
523
|
+
*
|
|
524
|
+
* @returns {Object} transport interface
|
|
525
|
+
*/
|
|
526
|
+
function getMessageTransport() {
|
|
527
|
+
return {
|
|
528
|
+
type: 'file',
|
|
529
|
+
send: (workspaceRoot, message) => {
|
|
530
|
+
const { saveMessage } = require('./workspace-messages');
|
|
531
|
+
return saveMessage(workspaceRoot, message);
|
|
532
|
+
},
|
|
533
|
+
receive: (workspaceRoot, filter) => {
|
|
534
|
+
const { readMessages } = require('./workspace-messages');
|
|
535
|
+
return readMessages(workspaceRoot, filter);
|
|
536
|
+
},
|
|
537
|
+
acknowledge: (workspaceRoot, messageId) => {
|
|
538
|
+
const { updateMessageStatus } = require('./workspace-messages');
|
|
539
|
+
return updateMessageStatus(workspaceRoot, messageId, 'acknowledged');
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Multi-user data model schema — defines the structure for when cloud adds
|
|
546
|
+
* multi-user support. No implementation — just the interface contract.
|
|
547
|
+
*
|
|
548
|
+
* @returns {Object} schema definition
|
|
549
|
+
*/
|
|
550
|
+
function getMultiUserSchema() {
|
|
551
|
+
return {
|
|
552
|
+
workspace: {
|
|
553
|
+
id: 'string (UUID)',
|
|
554
|
+
name: 'string',
|
|
555
|
+
createdBy: 'string (userId)',
|
|
556
|
+
members: 'WorkspaceMember[]',
|
|
557
|
+
teamId: 'string (from wogiflow-cloud)',
|
|
558
|
+
createdAt: 'ISO 8601',
|
|
559
|
+
updatedAt: 'ISO 8601'
|
|
560
|
+
},
|
|
561
|
+
workspaceMember: {
|
|
562
|
+
userId: 'string',
|
|
563
|
+
role: 'owner | admin | member | viewer',
|
|
564
|
+
repos: 'string[] (which repos they can access)',
|
|
565
|
+
lastActiveAt: 'ISO 8601'
|
|
566
|
+
},
|
|
567
|
+
agentSession: {
|
|
568
|
+
id: 'string (UUID)',
|
|
569
|
+
workspaceId: 'string',
|
|
570
|
+
userId: 'string',
|
|
571
|
+
repoName: 'string',
|
|
572
|
+
status: 'active | idle | disconnected',
|
|
573
|
+
startedAt: 'ISO 8601',
|
|
574
|
+
lastHeartbeat: 'ISO 8601'
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ============================================================
|
|
580
|
+
// Exports
|
|
581
|
+
// ============================================================
|
|
582
|
+
|
|
583
|
+
module.exports = {
|
|
584
|
+
// S5: Cross-Repo Intelligence
|
|
585
|
+
detectContractDrift,
|
|
586
|
+
routeBlame,
|
|
587
|
+
getSharedDecisions,
|
|
588
|
+
addSharedDecision,
|
|
589
|
+
|
|
590
|
+
// S7: N-Repo Scaling
|
|
591
|
+
buildDependencyGraph,
|
|
592
|
+
getLibraryConsumers,
|
|
593
|
+
getCascadeTargets,
|
|
594
|
+
checkWorkspaceHealth,
|
|
595
|
+
|
|
596
|
+
// S8: Cloud Preparation
|
|
597
|
+
exportForDashboard,
|
|
598
|
+
getMessageTransport,
|
|
599
|
+
getMultiUserSchema
|
|
600
|
+
};
|