wogiflow 2.6.3 → 2.7.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 (30) hide show
  1. package/.claude/settings.json +0 -1
  2. package/lib/workspace-changelog.js +182 -0
  3. package/lib/workspace-channel-server.js +75 -2
  4. package/lib/workspace-contracts.js +151 -1
  5. package/lib/workspace-events.js +383 -0
  6. package/lib/workspace-gates.js +740 -0
  7. package/lib/workspace-integration-tests.js +299 -0
  8. package/lib/workspace-intelligence.js +486 -1
  9. package/lib/workspace-locks.js +371 -0
  10. package/lib/workspace-messages.js +203 -3
  11. package/lib/workspace-routing.js +144 -0
  12. package/lib/workspace.js +18 -3
  13. package/package.json +1 -1
  14. package/scripts/flow-done-gates.js +70 -0
  15. package/.claude/rules/_internal/README.md +0 -64
  16. package/.claude/rules/_internal/document-structure.md +0 -77
  17. package/.claude/rules/_internal/dual-repo-management.md +0 -174
  18. package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
  19. package/.claude/rules/_internal/github-releases.md +0 -71
  20. package/.claude/rules/_internal/model-management.md +0 -35
  21. package/.claude/rules/_internal/self-maintenance.md +0 -87
  22. package/.claude/rules/architecture/component-reuse.md +0 -38
  23. package/.claude/rules/code-style/naming-conventions.md +0 -107
  24. package/.claude/rules/operations/git-workflows.md +0 -92
  25. package/.claude/rules/operations/scratch-directory.md +0 -54
  26. package/.claude/rules/security/security-patterns.md +0 -176
  27. package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
  28. package/.workflow/specs/architecture.md.template +0 -24
  29. package/.workflow/specs/stack.md.template +0 -33
  30. package/.workflow/specs/testing.md.template +0 -36
@@ -246,6 +246,81 @@ function addSharedDecision(workspaceRoot, title, content) {
246
246
  fs.writeFileSync(decisionsPath, existing + entry);
247
247
  }
248
248
 
249
+ // ============================================================
250
+ // S5: Decision Propagation (Active Broadcast)
251
+ // ============================================================
252
+
253
+ /**
254
+ * Add a shared decision AND broadcast it to all workspace members.
255
+ * Extends addSharedDecision with active notification.
256
+ *
257
+ * @param {string} workspaceRoot
258
+ * @param {string} fromRepo — repo that created the decision
259
+ * @param {string} title
260
+ * @param {string} content
261
+ * @param {Object} manifest — workspace manifest for member discovery
262
+ * @returns {{ saved: boolean, broadcastCount: number }}
263
+ */
264
+ function propagateDecision(workspaceRoot, fromRepo, title, content, manifest) {
265
+ // 1. Save the decision
266
+ addSharedDecision(workspaceRoot, title, content);
267
+
268
+ // 2. Broadcast to all members
269
+ let broadcastCount = 0;
270
+ if (manifest && manifest.members) {
271
+ const { broadcastDecision, saveMessage } = require('./workspace-messages');
272
+ const targetRepos = Object.keys(manifest.members).filter(n => n !== fromRepo);
273
+ const messages = broadcastDecision(fromRepo, title, content, targetRepos);
274
+
275
+ for (const msg of messages) {
276
+ try {
277
+ saveMessage(workspaceRoot, msg);
278
+ broadcastCount++;
279
+ } catch (_err) {
280
+ // Best effort
281
+ }
282
+ }
283
+ }
284
+
285
+ return { saved: true, broadcastCount };
286
+ }
287
+
288
+ /**
289
+ * Check if there are new shared decisions since a given timestamp.
290
+ * Used by session-start hooks to inject new decisions into worker context.
291
+ *
292
+ * @param {string} workspaceRoot
293
+ * @param {string} sinceDate — ISO date string
294
+ * @returns {Array<{ title: string, content: string, date: string }>}
295
+ */
296
+ function getNewDecisionsSince(workspaceRoot, sinceDate) {
297
+ const decisions = getSharedDecisions(workspaceRoot);
298
+ if (!decisions) return [];
299
+
300
+ const sinceTime = new Date(sinceDate).getTime();
301
+ const results = [];
302
+
303
+ // Parse decisions.md for entries with *Added: YYYY-MM-DD* markers
304
+ const sections = decisions.split(/^### /m).filter(Boolean);
305
+ for (const section of sections) {
306
+ const lines = section.trim().split('\n');
307
+ const title = lines[0]?.trim() || '';
308
+ const dateMatch = section.match(/\*Added:\s*(\d{4}-\d{2}-\d{2})\*/);
309
+ if (dateMatch) {
310
+ const entryDate = new Date(dateMatch[1]).getTime();
311
+ if (entryDate > sinceTime) {
312
+ results.push({
313
+ title,
314
+ content: lines.slice(1).join('\n').replace(/\*Added:.*\*/, '').trim(),
315
+ date: dateMatch[1]
316
+ });
317
+ }
318
+ }
319
+ }
320
+
321
+ return results;
322
+ }
323
+
249
324
  // ============================================================
250
325
  // S5: API Changelog (Criterion 4) — delegates to workspace-contracts
251
326
  // S5: Cross-Repo Ready Queue (Criterion 5) — workspace state/ready.json
@@ -580,6 +655,402 @@ function getMultiUserSchema() {
580
655
  // Exports
581
656
  // ============================================================
582
657
 
658
+ // ============================================================
659
+ // Workspace Review Mode (Cross-Repo Contract Impact)
660
+ // ============================================================
661
+
662
+ /**
663
+ * Analyze code review findings for cross-repo impact.
664
+ * Called during /wogi-review when workspace is active.
665
+ *
666
+ * @param {string} workspaceRoot
667
+ * @param {Object} manifest
668
+ * @param {string[]} changedFiles — files changed in the review
669
+ * @param {string} repoName — repo being reviewed
670
+ * @returns {Object} review impact analysis
671
+ */
672
+ function analyzeReviewForCrossRepoImpact(workspaceRoot, manifest, changedFiles, repoName) {
673
+ const result = {
674
+ hasContractImpact: false,
675
+ endpointChanges: [],
676
+ missingContractUpdates: [],
677
+ affectedConsumers: [],
678
+ recommendations: []
679
+ };
680
+
681
+ const member = manifest.members?.[repoName];
682
+ if (!member) return result;
683
+
684
+ // Check if any changed files are in API/route directories
685
+ const apiPatterns = ['route', 'controller', 'endpoint', 'api', 'handler'];
686
+ const changedApiFiles = changedFiles.filter(f => {
687
+ const lower = f.toLowerCase();
688
+ return apiPatterns.some(p => lower.includes(p));
689
+ });
690
+
691
+ if (changedApiFiles.length > 0) {
692
+ result.hasContractImpact = true;
693
+ result.endpointChanges = changedApiFiles;
694
+
695
+ // Check if contract was also updated
696
+ const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
697
+ const contractFiles = changedFiles.filter(f => f.includes('.workspace/contracts'));
698
+
699
+ if (contractFiles.length === 0 && changedApiFiles.length > 0) {
700
+ result.missingContractUpdates.push(
701
+ `API files changed (${changedApiFiles.join(', ')}) but no contract was updated. ` +
702
+ `Run \`flow workspace sync\` and regenerate contracts.`
703
+ );
704
+ }
705
+
706
+ // Find affected consumers
707
+ const { buildIntegrationMap } = require('./workspace-contracts');
708
+ const integrationMap = buildIntegrationMap(manifest);
709
+ const consumerSet = new Set();
710
+
711
+ for (const matched of integrationMap.matched || []) {
712
+ if ((matched.providers || []).includes(repoName)) {
713
+ for (const consumer of matched.consumers || []) {
714
+ consumerSet.add(consumer);
715
+ }
716
+ }
717
+ }
718
+
719
+ result.affectedConsumers = [...consumerSet];
720
+
721
+ if (result.affectedConsumers.length > 0) {
722
+ result.recommendations.push(
723
+ `Notify consumers: ${result.affectedConsumers.join(', ')}`,
724
+ 'Update shared contracts if endpoint signatures changed',
725
+ 'Consider creating verification tasks in consumer repos'
726
+ );
727
+ }
728
+ }
729
+
730
+ return result;
731
+ }
732
+
733
+ // ============================================================
734
+ // Workspace Morning Briefing
735
+ // ============================================================
736
+
737
+ /**
738
+ * Generate a workspace-aware morning briefing.
739
+ *
740
+ * @param {string} workspaceRoot
741
+ * @param {Object} manifest
742
+ * @param {string} [repoName] — current repo (for filtering)
743
+ * @returns {Object} briefing data
744
+ */
745
+ function generateWorkspaceBriefing(workspaceRoot, manifest, repoName) {
746
+ const briefing = {
747
+ unreadMessages: [],
748
+ contractChanges: [],
749
+ blockedTasks: [],
750
+ healthIssues: [],
751
+ activeLocks: [],
752
+ recentEvents: [],
753
+ summary: ''
754
+ };
755
+
756
+ // 1. Unread messages
757
+ try {
758
+ const { getUnreadMessages } = require('./workspace-messages');
759
+ briefing.unreadMessages = getUnreadMessages(workspaceRoot, repoName || 'all');
760
+ } catch (_err) {
761
+ // Non-critical
762
+ }
763
+
764
+ // 2. Contract changes (drift detection)
765
+ try {
766
+ briefing.contractChanges = detectContractDrift(workspaceRoot, manifest);
767
+ } catch (_err) {
768
+ // Non-critical
769
+ }
770
+
771
+ // 3. Blocked tasks
772
+ try {
773
+ const { updateCrossRepoBlocking } = require('./workspace-routing');
774
+ const blocking = updateCrossRepoBlocking(workspaceRoot, manifest);
775
+ briefing.blockedTasks = blocking.blockedTasks;
776
+ } catch (_err) {
777
+ // Non-critical
778
+ }
779
+
780
+ // 4. Health issues
781
+ try {
782
+ const health = checkWorkspaceHealth(workspaceRoot, manifest);
783
+ briefing.healthIssues = health.issues || [];
784
+ } catch (_err) {
785
+ // Non-critical
786
+ }
787
+
788
+ // 5. Active locks
789
+ try {
790
+ const { listActiveLocks } = require('./workspace-locks');
791
+ briefing.activeLocks = listActiveLocks(workspaceRoot);
792
+ } catch (_err) {
793
+ // Non-critical
794
+ }
795
+
796
+ // 6. Recent events (last 24h)
797
+ try {
798
+ const { readEvents } = require('./workspace-events');
799
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
800
+ briefing.recentEvents = readEvents(workspaceRoot, { since: yesterday, limit: 20 });
801
+ } catch (_err) {
802
+ // Non-critical
803
+ }
804
+
805
+ // 7. Summary
806
+ const parts = [];
807
+ if (briefing.unreadMessages.length > 0) parts.push(`${briefing.unreadMessages.length} unread message(s)`);
808
+ if (briefing.contractChanges.length > 0) parts.push(`${briefing.contractChanges.length} contract drift(s)`);
809
+ if (briefing.blockedTasks.length > 0) parts.push(`${briefing.blockedTasks.length} blocked task(s)`);
810
+ if (briefing.healthIssues.length > 0) parts.push(`${briefing.healthIssues.length} health issue(s)`);
811
+ if (briefing.activeLocks.length > 0) parts.push(`${briefing.activeLocks.length} active lock(s)`);
812
+ briefing.summary = parts.length > 0 ? parts.join(', ') : 'All clear';
813
+
814
+ return briefing;
815
+ }
816
+
817
+ /**
818
+ * Format workspace briefing as readable text.
819
+ *
820
+ * @param {Object} briefing — from generateWorkspaceBriefing()
821
+ * @returns {string}
822
+ */
823
+ function formatWorkspaceBriefing(briefing) {
824
+ const lines = ['Workspace Briefing', '━'.repeat(40), ''];
825
+
826
+ if (briefing.unreadMessages.length > 0) {
827
+ lines.push(`Messages (${briefing.unreadMessages.length} unread):`);
828
+ for (const msg of briefing.unreadMessages.slice(0, 5)) {
829
+ lines.push(` ${msg.from}: ${msg.subject}`);
830
+ }
831
+ lines.push('');
832
+ }
833
+
834
+ if (briefing.contractChanges.length > 0) {
835
+ lines.push(`Contract Drifts (${briefing.contractChanges.length}):`);
836
+ for (const drift of briefing.contractChanges.slice(0, 5)) {
837
+ lines.push(` ${drift.severity}: ${drift.endpoint || drift.type}`);
838
+ }
839
+ lines.push('');
840
+ }
841
+
842
+ if (briefing.blockedTasks.length > 0) {
843
+ lines.push(`Blocked Tasks (${briefing.blockedTasks.length}):`);
844
+ for (const bt of briefing.blockedTasks.slice(0, 5)) {
845
+ lines.push(` ${bt.repo}: ${bt.task.title} [blocked by: ${bt.blockedBy?.join(', ')}]`);
846
+ }
847
+ lines.push('');
848
+ }
849
+
850
+ if (briefing.activeLocks.length > 0) {
851
+ lines.push(`Active Locks (${briefing.activeLocks.length}):`);
852
+ try {
853
+ const { formatLocksForDisplay } = require('./workspace-locks');
854
+ lines.push(formatLocksForDisplay(briefing.activeLocks));
855
+ } catch (_err) {
856
+ for (const lock of briefing.activeLocks) {
857
+ lines.push(` ${lock.interface} — held by ${lock.owner}`);
858
+ }
859
+ }
860
+ lines.push('');
861
+ }
862
+
863
+ if (briefing.healthIssues.length > 0) {
864
+ lines.push(`Health Issues (${briefing.healthIssues.length}):`);
865
+ for (const issue of briefing.healthIssues.slice(0, 5)) {
866
+ lines.push(` ${issue.severity || 'warning'}: ${issue.message || issue}`);
867
+ }
868
+ lines.push('');
869
+ }
870
+
871
+ lines.push(`Summary: ${briefing.summary}`);
872
+
873
+ return lines.join('\n');
874
+ }
875
+
876
+ // ============================================================
877
+ // Workspace Audit Dimension
878
+ // ============================================================
879
+
880
+ /**
881
+ * Run workspace-specific audit checks.
882
+ * Returns findings for the workspace dimension of /wogi-audit.
883
+ *
884
+ * @param {string} workspaceRoot
885
+ * @param {Object} manifest
886
+ * @returns {Object} audit results
887
+ */
888
+ function auditWorkspaceDimension(workspaceRoot, manifest) {
889
+ const audit = {
890
+ dimension: 'workspace',
891
+ score: 100,
892
+ findings: [],
893
+ metrics: {}
894
+ };
895
+
896
+ if (!manifest || !manifest.members) {
897
+ audit.score = 0;
898
+ audit.findings.push({ severity: 'error', message: 'No workspace manifest found' });
899
+ return audit;
900
+ }
901
+
902
+ const memberCount = Object.keys(manifest.members).length;
903
+ audit.metrics.memberCount = memberCount;
904
+
905
+ // 1. Type consistency — check for type drift
906
+ try {
907
+ const memberMetadata = {};
908
+ const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
909
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
910
+
911
+ for (const [name, memberConfig] of Object.entries(config.members || {})) {
912
+ const memberPath = path.resolve(workspaceRoot, memberConfig.path);
913
+ const workflowPath = path.join(memberPath, '.workflow');
914
+ if (fs.existsSync(workflowPath)) {
915
+ const { readMemberMetadata } = require('./workspace');
916
+ memberMetadata[name] = readMemberMetadata(workflowPath);
917
+ }
918
+ }
919
+
920
+ const { detectTypeDrift } = require('./workspace-contracts');
921
+ const drifts = detectTypeDrift(manifest, memberMetadata);
922
+ audit.metrics.typeDrifts = drifts.length;
923
+
924
+ if (drifts.length > 0) {
925
+ audit.score -= drifts.length * 5;
926
+ audit.findings.push({
927
+ severity: 'high',
928
+ message: `${drifts.length} type drift(s) detected across repos`,
929
+ details: drifts.map(d => `${d.type}: ${d.entries?.length || 0} conflicting definitions`)
930
+ });
931
+ }
932
+ } catch (_err) {
933
+ // Non-critical
934
+ }
935
+
936
+ // 2. Contract coverage — what % of integrations have contracts
937
+ try {
938
+ const { buildIntegrationMap } = require('./workspace-contracts');
939
+ const map = buildIntegrationMap(manifest);
940
+ const totalIntegrations = (map.matched || []).length;
941
+ const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
942
+ const contractCount = fs.existsSync(contractsDir)
943
+ ? fs.readdirSync(contractsDir).filter(f => !f.startsWith('.')).length
944
+ : 0;
945
+
946
+ audit.metrics.totalIntegrations = totalIntegrations;
947
+ audit.metrics.contractCount = contractCount;
948
+ audit.metrics.contractCoverage = totalIntegrations > 0
949
+ ? Math.round((contractCount / totalIntegrations) * 100)
950
+ : 100;
951
+
952
+ if (audit.metrics.contractCoverage < 50) {
953
+ audit.score -= 15;
954
+ audit.findings.push({
955
+ severity: 'high',
956
+ message: `Contract coverage is ${audit.metrics.contractCoverage}% (${contractCount}/${totalIntegrations})`
957
+ });
958
+ } else if (audit.metrics.contractCoverage < 80) {
959
+ audit.score -= 5;
960
+ audit.findings.push({
961
+ severity: 'medium',
962
+ message: `Contract coverage is ${audit.metrics.contractCoverage}% — consider adding contracts for uncovered integrations`
963
+ });
964
+ }
965
+
966
+ // Orphaned endpoints
967
+ audit.metrics.orphanedConsumers = (map.orphanedConsumers || []).length;
968
+ audit.metrics.orphanedProviders = (map.orphanedProviders || []).length;
969
+
970
+ if (audit.metrics.orphanedConsumers > 0) {
971
+ audit.score -= audit.metrics.orphanedConsumers * 3;
972
+ audit.findings.push({
973
+ severity: 'high',
974
+ message: `${audit.metrics.orphanedConsumers} consumer(s) calling endpoints with no provider`
975
+ });
976
+ }
977
+ } catch (_err) {
978
+ // Non-critical
979
+ }
980
+
981
+ // 3. Communication health — message acknowledgment rate
982
+ try {
983
+ const { readMessages } = require('./workspace-messages');
984
+ const allMessages = readMessages(workspaceRoot);
985
+ const actionRequired = allMessages.filter(m => m.actionRequired);
986
+ const acknowledged = actionRequired.filter(m => m.status !== 'pending');
987
+
988
+ audit.metrics.totalMessages = allMessages.length;
989
+ audit.metrics.pendingActionRequired = actionRequired.filter(m => m.status === 'pending').length;
990
+ audit.metrics.acknowledgmentRate = actionRequired.length > 0
991
+ ? Math.round((acknowledged.length / actionRequired.length) * 100)
992
+ : 100;
993
+
994
+ if (audit.metrics.pendingActionRequired > 5) {
995
+ audit.score -= 10;
996
+ audit.findings.push({
997
+ severity: 'medium',
998
+ message: `${audit.metrics.pendingActionRequired} action-required message(s) still pending`
999
+ });
1000
+ }
1001
+ } catch (_err) {
1002
+ // Non-critical
1003
+ }
1004
+
1005
+ // 4. Dependency freshness — is the manifest up to date?
1006
+ try {
1007
+ const manifestPath = path.join(workspaceRoot, '.workspace', 'state', 'workspace-manifest.json');
1008
+ if (fs.existsSync(manifestPath)) {
1009
+ const stat = fs.statSync(manifestPath);
1010
+ const ageHours = (Date.now() - stat.mtime.getTime()) / (60 * 60 * 1000);
1011
+ audit.metrics.manifestAgeHours = Math.round(ageHours);
1012
+
1013
+ if (ageHours > 48) {
1014
+ audit.score -= 10;
1015
+ audit.findings.push({
1016
+ severity: 'medium',
1017
+ message: `Workspace manifest is ${Math.round(ageHours)}h old — run \`flow workspace sync\``
1018
+ });
1019
+ }
1020
+ }
1021
+ } catch (_err) {
1022
+ // Non-critical
1023
+ }
1024
+
1025
+ // 5. Contract drift
1026
+ try {
1027
+ const contractDrifts = detectContractDrift(workspaceRoot, manifest);
1028
+ audit.metrics.contractDrifts = contractDrifts.length;
1029
+
1030
+ if (contractDrifts.length > 0) {
1031
+ const highSeverity = contractDrifts.filter(d => d.severity === 'high');
1032
+ if (highSeverity.length > 0) {
1033
+ audit.score -= highSeverity.length * 10;
1034
+ audit.findings.push({
1035
+ severity: 'critical',
1036
+ message: `${highSeverity.length} high-severity contract drift(s) — implementation doesn't match spec`
1037
+ });
1038
+ }
1039
+ }
1040
+ } catch (_err) {
1041
+ // Non-critical
1042
+ }
1043
+
1044
+ // Clamp score
1045
+ audit.score = Math.max(0, Math.min(100, audit.score));
1046
+
1047
+ return audit;
1048
+ }
1049
+
1050
+ // ============================================================
1051
+ // Exports
1052
+ // ============================================================
1053
+
583
1054
  module.exports = {
584
1055
  // S5: Cross-Repo Intelligence
585
1056
  detectContractDrift,
@@ -587,6 +1058,10 @@ module.exports = {
587
1058
  getSharedDecisions,
588
1059
  addSharedDecision,
589
1060
 
1061
+ // Decision propagation (active broadcast)
1062
+ propagateDecision,
1063
+ getNewDecisionsSince,
1064
+
590
1065
  // S7: N-Repo Scaling
591
1066
  buildDependencyGraph,
592
1067
  getLibraryConsumers,
@@ -596,5 +1071,15 @@ module.exports = {
596
1071
  // S8: Cloud Preparation
597
1072
  exportForDashboard,
598
1073
  getMessageTransport,
599
- getMultiUserSchema
1074
+ getMultiUserSchema,
1075
+
1076
+ // Review mode
1077
+ analyzeReviewForCrossRepoImpact,
1078
+
1079
+ // Morning briefing
1080
+ generateWorkspaceBriefing,
1081
+ formatWorkspaceBriefing,
1082
+
1083
+ // Audit dimension
1084
+ auditWorkspaceDimension
600
1085
  };