@rigour-labs/core 6.6.5 → 6.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 (154) hide show
  1. package/README.md +16 -92
  2. package/dist/environment.test.js +0 -4
  3. package/dist/gates/agent-team.d.ts +0 -2
  4. package/dist/gates/agent-team.js +0 -8
  5. package/dist/gates/checkpoint.d.ts +0 -1
  6. package/dist/gates/checkpoint.js +0 -1
  7. package/dist/gates/context.d.ts +0 -4
  8. package/dist/gates/context.js +7 -17
  9. package/dist/gates/deep-analysis.d.ts +0 -1
  10. package/dist/gates/deep-runner.js +0 -1
  11. package/dist/gates/frontend-secret-exposure.d.ts +1 -1
  12. package/dist/gates/frontend-secret-exposure.js +9 -3
  13. package/dist/gates/frontend-secret-exposure.test.js +6 -3
  14. package/dist/gates/hallucinated-imports/index.d.ts +0 -2
  15. package/dist/gates/hallucinated-imports/index.js +2 -2
  16. package/dist/gates/hallucinated-imports/js-resolver.js +1 -0
  17. package/dist/gates/hallucinated-imports.test.js +10 -0
  18. package/dist/gates/runner.js +3 -4
  19. package/dist/gates/test-quality-matchers.d.ts +0 -4
  20. package/dist/gates/test-quality-matchers.js +0 -7
  21. package/dist/gates/test-quality.d.ts +0 -2
  22. package/dist/gates/test-quality.js +1 -10
  23. package/dist/hooks/checker.js +1 -1
  24. package/dist/hooks/index.d.ts +2 -3
  25. package/dist/hooks/index.js +1 -2
  26. package/dist/hooks/stop-review.d.ts +6 -9
  27. package/dist/hooks/stop-review.js +5 -10
  28. package/dist/hooks/types.d.ts +0 -14
  29. package/dist/hooks/types.js +1 -15
  30. package/dist/index.d.ts +11 -3
  31. package/dist/index.js +11 -3
  32. package/dist/inference/cloud-provider.d.ts +2 -0
  33. package/dist/inference/cloud-provider.js +16 -6
  34. package/dist/inference/cloud-provider.test.js +21 -0
  35. package/dist/pattern-index/embeddings.d.ts +0 -5
  36. package/dist/pattern-index/embeddings.js +11 -4
  37. package/dist/pattern-index/semantic-runtime.d.ts +10 -0
  38. package/dist/pattern-index/semantic-runtime.js +46 -0
  39. package/dist/pattern-index/semantic-runtime.test.d.ts +1 -0
  40. package/dist/pattern-index/semantic-runtime.test.js +31 -0
  41. package/dist/review/backtest-init.d.ts +8 -0
  42. package/dist/review/backtest-init.js +87 -0
  43. package/dist/review/backtest-init.test.d.ts +1 -0
  44. package/dist/review/backtest-init.test.js +74 -0
  45. package/dist/review/backtest-judges.d.ts +38 -0
  46. package/dist/review/backtest-judges.js +63 -0
  47. package/dist/review/backtest-judges.test.d.ts +1 -0
  48. package/dist/review/backtest-judges.test.js +32 -0
  49. package/dist/review/backtest.d.ts +320 -0
  50. package/dist/review/backtest.js +207 -0
  51. package/dist/review/backtest.test.d.ts +1 -0
  52. package/dist/review/backtest.test.js +129 -0
  53. package/dist/review/baseline.js +2 -1
  54. package/dist/review/dead-code.test.js +15 -0
  55. package/dist/review/fix-scope.d.ts +21 -0
  56. package/dist/review/fix-scope.js +94 -0
  57. package/dist/review/fix-scope.test.d.ts +1 -0
  58. package/dist/review/fix-scope.test.js +104 -0
  59. package/dist/review/lint-overlay.d.ts +21 -0
  60. package/dist/review/lint-overlay.js +89 -0
  61. package/dist/review/orphan-files.js +3 -2
  62. package/dist/review/partial-wiring.js +5 -3
  63. package/dist/review/query-patterns.js +3 -2
  64. package/dist/review/quiet.d.ts +6 -0
  65. package/dist/review/quiet.js +12 -2
  66. package/dist/review/quiet.test.js +11 -0
  67. package/dist/review/review.d.ts +6 -0
  68. package/dist/review/review.js +10 -2
  69. package/dist/review/reviewer/adapters.d.ts +51 -0
  70. package/dist/review/reviewer/adapters.js +181 -0
  71. package/dist/review/reviewer/adapters.test.d.ts +1 -0
  72. package/dist/review/reviewer/adapters.test.js +23 -0
  73. package/dist/review/reviewer/background.d.ts +41 -0
  74. package/dist/review/reviewer/background.js +153 -0
  75. package/dist/review/reviewer/background.test.d.ts +1 -0
  76. package/dist/review/reviewer/background.test.js +118 -0
  77. package/dist/review/reviewer/consensus.d.ts +36 -0
  78. package/dist/review/reviewer/consensus.js +144 -0
  79. package/dist/review/reviewer/context.d.ts +75 -0
  80. package/dist/review/reviewer/context.js +140 -0
  81. package/dist/review/reviewer/context.test.d.ts +1 -0
  82. package/dist/review/reviewer/context.test.js +18 -0
  83. package/dist/review/reviewer/exec.d.ts +17 -0
  84. package/dist/review/reviewer/exec.js +27 -0
  85. package/dist/review/reviewer/exec.test.d.ts +1 -0
  86. package/dist/review/reviewer/exec.test.js +23 -0
  87. package/dist/review/reviewer/inputs.d.ts +39 -0
  88. package/dist/review/reviewer/inputs.js +107 -0
  89. package/dist/review/reviewer/merge-impact.d.ts +2 -0
  90. package/dist/review/reviewer/merge-impact.js +64 -0
  91. package/dist/review/reviewer/panel.d.ts +67 -0
  92. package/dist/review/reviewer/panel.js +101 -0
  93. package/dist/review/reviewer/panel.test.d.ts +1 -0
  94. package/dist/review/reviewer/panel.test.js +110 -0
  95. package/dist/review/reviewer/prompt.d.ts +32 -0
  96. package/dist/review/reviewer/prompt.js +145 -0
  97. package/dist/review/reviewer/settings.d.ts +58 -0
  98. package/dist/review/reviewer/settings.js +152 -0
  99. package/dist/review/reviewer/settings.test.d.ts +1 -0
  100. package/dist/review/reviewer/settings.test.js +57 -0
  101. package/dist/review/reviewer/store.d.ts +49 -0
  102. package/dist/review/reviewer/store.js +99 -0
  103. package/dist/review/reviewer/usage.d.ts +2 -0
  104. package/dist/review/reviewer/usage.js +28 -0
  105. package/dist/review/reviewer/usage.test.d.ts +1 -0
  106. package/dist/review/reviewer/usage.test.js +14 -0
  107. package/dist/review/reviewer/verdict.d.ts +141 -0
  108. package/dist/review/reviewer/verdict.js +166 -0
  109. package/dist/review/reviewer.d.ts +84 -49
  110. package/dist/review/reviewer.js +364 -195
  111. package/dist/review/reviewer.test.js +316 -50
  112. package/dist/review/test-files.d.ts +5 -1
  113. package/dist/review/test-files.js +6 -2
  114. package/dist/review/toolchain.d.ts +5 -2
  115. package/dist/review/toolchain.js +89 -7
  116. package/dist/review/toolchain.test.js +45 -4
  117. package/dist/review/typed/program.d.ts +26 -0
  118. package/dist/review/typed/program.js +90 -0
  119. package/dist/review/typed/redundancy.d.ts +10 -0
  120. package/dist/review/typed/redundancy.js +378 -0
  121. package/dist/review/typed/redundancy.test.d.ts +1 -0
  122. package/dist/review/typed/redundancy.test.js +179 -0
  123. package/dist/review/typed/schema-nullability.d.ts +6 -0
  124. package/dist/review/typed/schema-nullability.js +218 -0
  125. package/dist/review/typed/schema-nullability.test.d.ts +1 -0
  126. package/dist/review/typed/schema-nullability.test.js +63 -0
  127. package/dist/review/unused-exports.js +10 -7
  128. package/dist/semantic/learn/types.d.ts +2 -2
  129. package/dist/services/adaptive-thresholds.d.ts +2 -52
  130. package/dist/services/adaptive-thresholds.js +2 -147
  131. package/dist/services/adaptive-thresholds.test.js +1 -81
  132. package/dist/settings.d.ts +14 -6
  133. package/dist/settings.js +0 -12
  134. package/dist/storage/team-diagnostics.js +1 -1
  135. package/dist/storage/team-schema.d.ts +1 -1
  136. package/dist/storage/team-schema.js +2 -0
  137. package/dist/storage/team-store.js +26 -3
  138. package/dist/storage/team-sync-scope.test.js +35 -2
  139. package/dist/telemetry/telemetry.d.ts +5 -0
  140. package/dist/telemetry/telemetry.js +11 -1
  141. package/dist/templates/paradigms.js +0 -8
  142. package/dist/templates/presets.d.ts +0 -1
  143. package/dist/templates/presets.js +0 -23
  144. package/dist/templates/universal-config.js +16 -46
  145. package/dist/types/index.d.ts +202 -574
  146. package/dist/types/index.js +64 -67
  147. package/dist/types/index.test.d.ts +1 -0
  148. package/dist/types/index.test.js +33 -0
  149. package/dist/utils/command-line.d.ts +8 -0
  150. package/dist/utils/command-line.js +8 -0
  151. package/dist/utils/command-line.test.d.ts +1 -0
  152. package/dist/utils/command-line.test.js +8 -0
  153. package/dist/utils/profile.js +1 -1
  154. package/package.json +6 -7
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { detectComplexityTier, calculateAdaptiveThresholds, recordGateRun, getQualityTrend, clearAdaptiveHistory, getAdaptiveSummary, } from './adaptive-thresholds.js';
2
+ import { recordGateRun, getQualityTrend, clearAdaptiveHistory, } from './adaptive-thresholds.js';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
@@ -12,49 +12,6 @@ describe('AdaptiveThresholds', () => {
12
12
  clearAdaptiveHistory(testDir);
13
13
  fs.rmSync(testDir, { recursive: true, force: true });
14
14
  });
15
- describe('detectComplexityTier', () => {
16
- it('should detect hobby tier for small projects', () => {
17
- const tier = detectComplexityTier({ fileCount: 20 });
18
- expect(tier).toBe('hobby');
19
- });
20
- it('should detect startup tier for medium projects', () => {
21
- const tier = detectComplexityTier({ fileCount: 100 });
22
- expect(tier).toBe('startup');
23
- });
24
- it('should detect enterprise tier for large projects', () => {
25
- const tier = detectComplexityTier({ fileCount: 600 });
26
- expect(tier).toBe('enterprise');
27
- });
28
- it('should consider commit count for tier detection', () => {
29
- const tier = detectComplexityTier({ fileCount: 50, commitCount: 1500 });
30
- expect(tier).toBe('enterprise');
31
- });
32
- });
33
- describe('calculateAdaptiveThresholds', () => {
34
- it('should return lenient thresholds for hobby tier', () => {
35
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 20 });
36
- expect(adjustments.tier).toBe('hobby');
37
- expect(adjustments.coverageThreshold).toBeLessThan(80);
38
- expect(adjustments.securityBlockLevel).toBe('critical');
39
- expect(adjustments.leniencyFactor).toBeGreaterThan(0.5);
40
- });
41
- it('should return strict thresholds for enterprise tier', () => {
42
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 600 });
43
- expect(adjustments.tier).toBe('enterprise');
44
- expect(adjustments.coverageThreshold).toBe(80);
45
- expect(adjustments.securityBlockLevel).toBe('medium');
46
- expect(adjustments.leniencyFactor).toBeLessThan(0.5);
47
- });
48
- it('should respect forced_tier config', () => {
49
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 20 }, { forced_tier: 'enterprise' });
50
- expect(adjustments.tier).toBe('enterprise');
51
- });
52
- it('should include reasoning for adjustments', () => {
53
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
54
- expect(adjustments.reasoning.length).toBeGreaterThan(0);
55
- expect(adjustments.reasoning.some(r => r.includes('tier'))).toBe(true);
56
- });
57
- });
58
15
  describe('historical tracking', () => {
59
16
  it('should record gate runs', () => {
60
17
  recordGateRun(testDir, 5, 2, 10);
@@ -93,41 +50,4 @@ describe('AdaptiveThresholds', () => {
93
50
  expect(trend).toBe('degrading');
94
51
  });
95
52
  });
96
- describe('trend-based adjustments', () => {
97
- it('should relax thresholds for improving trend', () => {
98
- // Baseline: 15 runs with high failures (with variance)
99
- for (let i = 0; i < 15; i++) {
100
- recordGateRun(testDir, 3, 5, 15 + (i % 5) * 3);
101
- }
102
- // Recent: 5 runs with very low failures
103
- for (let i = 0; i < 5; i++) {
104
- recordGateRun(testDir, 7, 0, 1);
105
- }
106
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
107
- expect(adjustments.trend).toBe('improving');
108
- expect(adjustments.reasoning.some(r => r.includes('bonus'))).toBe(true);
109
- });
110
- it('should tighten thresholds for degrading trend', () => {
111
- // Baseline: 15 runs with low failures (with variance)
112
- for (let i = 0; i < 15; i++) {
113
- recordGateRun(testDir, 7, 1, 1 + (i % 3));
114
- }
115
- // Recent: 5 runs with high failures
116
- for (let i = 0; i < 5; i++) {
117
- recordGateRun(testDir, 3, 5, 25);
118
- }
119
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
120
- expect(adjustments.trend).toBe('degrading');
121
- expect(adjustments.reasoning.some(r => r.includes('tightened'))).toBe(true);
122
- });
123
- });
124
- describe('getAdaptiveSummary', () => {
125
- it('should return formatted summary string', () => {
126
- const adjustments = calculateAdaptiveThresholds(testDir, { fileCount: 100 });
127
- const summary = getAdaptiveSummary(adjustments);
128
- expect(summary).toContain('STARTUP');
129
- expect(summary).toContain('Coverage:');
130
- expect(summary).toContain('Quality:');
131
- });
132
- });
133
53
  });
@@ -18,8 +18,6 @@ export interface RigourSettings {
18
18
  defaultProvider?: string;
19
19
  defaultModel?: string;
20
20
  apiBaseUrl?: string;
21
- maxTokens?: number;
22
- temperature?: number;
23
21
  };
24
22
  agents?: {
25
23
  [agentName: string]: {
@@ -33,10 +31,24 @@ export interface RigourSettings {
33
31
  colorOutput?: boolean;
34
32
  verboseOutput?: boolean;
35
33
  };
34
+ reviewer?: UserReviewerSettings;
36
35
  cursor?: {
37
36
  apiKey?: string;
38
37
  };
39
38
  }
39
+ /** What a person may set for their own reviewer runs. They cannot set a team floor (`required`). */
40
+ export interface UserReviewerSettings {
41
+ enabled?: boolean;
42
+ mode?: 'single' | 'cross' | 'full';
43
+ panel?: boolean;
44
+ judges?: 2 | 3;
45
+ escalate?: 'always' | 'risk';
46
+ /** A lower cap for your own runs; never above the team's. */
47
+ max_runs_per_day?: number;
48
+ max_usd_per_day?: number;
49
+ reviewers?: string[];
50
+ models?: Record<string, string>;
51
+ }
40
52
  /**
41
53
  * Resolved deep options from CLI flags merged with settings.json
42
54
  */
@@ -45,8 +57,6 @@ export interface ResolvedDeepOptions {
45
57
  provider?: string;
46
58
  apiBaseUrl?: string;
47
59
  modelName?: string;
48
- maxTokens?: number;
49
- temperature?: number;
50
60
  }
51
61
  /**
52
62
  * CLI options that may override settings
@@ -56,8 +66,6 @@ export interface CLIDeepOptions {
56
66
  provider?: string;
57
67
  apiBaseUrl?: string;
58
68
  modelName?: string;
59
- maxTokens?: number;
60
- temperature?: number;
61
69
  }
62
70
  /**
63
71
  * Get the settings file path: ~/.rigour/settings.json
package/dist/settings.js CHANGED
@@ -102,12 +102,6 @@ export function resolveDeepOptions(cliOptions) {
102
102
  if (settings.deep?.apiBaseUrl) {
103
103
  result.apiBaseUrl = settings.deep.apiBaseUrl;
104
104
  }
105
- if (settings.deep?.maxTokens) {
106
- result.maxTokens = settings.deep.maxTokens;
107
- }
108
- if (settings.deep?.temperature) {
109
- result.temperature = settings.deep.temperature;
110
- }
111
105
  // 2. Apply provider selection (settings or default)
112
106
  let selectedProvider = settings.deep?.defaultProvider || 'anthropic';
113
107
  if (cliOptions.provider) {
@@ -141,12 +135,6 @@ export function resolveDeepOptions(cliOptions) {
141
135
  if (cliOptions.apiBaseUrl) {
142
136
  result.apiBaseUrl = cliOptions.apiBaseUrl;
143
137
  }
144
- if (cliOptions.maxTokens) {
145
- result.maxTokens = cliOptions.maxTokens;
146
- }
147
- if (cliOptions.temperature) {
148
- result.temperature = cliOptions.temperature;
149
- }
150
138
  return result;
151
139
  }
152
140
  /**
@@ -54,5 +54,5 @@ export async function diagnoseMissingMembership(pool) {
54
54
  + `cannot see its membership. Run \`rigour team init-schema\` with the administrator URL to add the read policies.`;
55
55
  }
56
56
  return 'This database role has no membership for the configured organization, team and actor. '
57
- + 'Ask the administrator to add it (see docs/ENTERPRISE.md).';
57
+ + 'Ask the administrator to add it (see docs/TEAM_DATABASE.md).';
58
58
  }
@@ -3,4 +3,4 @@
3
3
  * (`rigour team init-schema`) adds anything missing, including the RLS
4
4
  * policies, without touching data.
5
5
  */
6
- export declare const TEAM_SCHEMA = "\nCREATE SCHEMA IF NOT EXISTS rigour;\nCREATE TABLE IF NOT EXISTS rigour.meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS rigour.memberships (\n db_role NAME NOT NULL,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n role TEXT NOT NULL CHECK (role IN ('member', 'sme', 'owner')),\n PRIMARY KEY (db_role, team_id)\n);\nCREATE TABLE IF NOT EXISTS rigour.lessons (\n id TEXT PRIMARY KEY,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n repository_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n visibility TEXT NOT NULL CHECK (visibility IN ('personal', 'team')),\n state TEXT NOT NULL CHECK (state IN ('candidate', 'validated', 'promoted', 'rejected', 'superseded')),\n kind TEXT NOT NULL,\n subject TEXT NOT NULL,\n evidence_json JSONB NOT NULL,\n confidence DOUBLE PRECISION NOT NULL,\n source TEXT NOT NULL,\n supersedes_id TEXT,\n created_at BIGINT NOT NULL,\n updated_at BIGINT NOT NULL\n);\nALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS lessons_read ON rigour.lessons;\nCREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND ((lessons.visibility = 'team' AND lessons.state = 'promoted')\n OR membership.actor_id = lessons.actor_id)\n )\n);\nDROP POLICY IF EXISTS lessons_write ON rigour.lessons;\nCREATE POLICY lessons_write ON rigour.lessons FOR ALL USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n) WITH CHECK (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n);\n-- Row-level security on every team table. A host (for example Supabase's\n-- security advisor) may enable RLS on its own; without a policy each member\n-- login would then see no rows at all. Members read only their own\n-- membership rows (the lessons policies read memberships the same way), and\n-- meta holds only the schema version.\nALTER TABLE rigour.memberships ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS memberships_read_own ON rigour.memberships;\nCREATE POLICY memberships_read_own ON rigour.memberships FOR SELECT USING (db_role = current_user);\nALTER TABLE rigour.meta ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS meta_read ON rigour.meta;\nCREATE POLICY meta_read ON rigour.meta FOR SELECT USING (true);\nINSERT INTO rigour.meta (key, value) VALUES ('schema_version', '1')\nON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;\n";
6
+ export declare const TEAM_SCHEMA = "\nCREATE SCHEMA IF NOT EXISTS rigour;\nCREATE TABLE IF NOT EXISTS rigour.meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS rigour.memberships (\n db_role NAME NOT NULL,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n role TEXT NOT NULL CHECK (role IN ('member', 'sme', 'owner')),\n PRIMARY KEY (db_role, team_id)\n);\nCREATE TABLE IF NOT EXISTS rigour.lessons (\n id TEXT PRIMARY KEY,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n repository_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n visibility TEXT NOT NULL CHECK (visibility IN ('personal', 'team')),\n state TEXT NOT NULL CHECK (state IN ('candidate', 'validated', 'promoted', 'rejected', 'superseded')),\n kind TEXT NOT NULL,\n subject TEXT NOT NULL,\n evidence_json JSONB NOT NULL,\n confidence DOUBLE PRECISION NOT NULL,\n source TEXT NOT NULL,\n supersedes_id TEXT,\n created_at BIGINT NOT NULL,\n updated_at BIGINT NOT NULL\n);\n-- Every sync reads a team's lessons changed since its last read.\nCREATE INDEX IF NOT EXISTS lessons_team_updated ON rigour.lessons (organization_id, team_id, updated_at);\nALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS lessons_read ON rigour.lessons;\nCREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND ((lessons.visibility = 'team' AND lessons.state = 'promoted')\n OR membership.actor_id = lessons.actor_id)\n )\n);\nDROP POLICY IF EXISTS lessons_write ON rigour.lessons;\nCREATE POLICY lessons_write ON rigour.lessons FOR ALL USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n) WITH CHECK (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lessons.organization_id\n AND membership.team_id = lessons.team_id\n AND membership.actor_id = lessons.actor_id\n AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))\n )\n);\n-- Row-level security on every team table. A host (for example Supabase's\n-- security advisor) may enable RLS on its own; without a policy each member\n-- login would then see no rows at all. Members read only their own\n-- membership rows (the lessons policies read memberships the same way), and\n-- meta holds only the schema version.\nALTER TABLE rigour.memberships ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS memberships_read_own ON rigour.memberships;\nCREATE POLICY memberships_read_own ON rigour.memberships FOR SELECT USING (db_role = current_user);\nALTER TABLE rigour.meta ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS meta_read ON rigour.meta;\nCREATE POLICY meta_read ON rigour.meta FOR SELECT USING (true);\nINSERT INTO rigour.meta (key, value) VALUES ('schema_version', '1')\nON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;\n";
@@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS rigour.lessons (
34
34
  created_at BIGINT NOT NULL,
35
35
  updated_at BIGINT NOT NULL
36
36
  );
37
+ -- Every sync reads a team's lessons changed since its last read.
38
+ CREATE INDEX IF NOT EXISTS lessons_team_updated ON rigour.lessons (organization_id, team_id, updated_at);
37
39
  ALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;
38
40
  DROP POLICY IF EXISTS lessons_read ON rigour.lessons;
39
41
  CREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (
@@ -208,6 +208,13 @@ async function splitOutbox(db, queued, config) {
208
208
  }
209
209
  return { sendable, held };
210
210
  }
211
+ /** How far before the last pull a sync reads again, for members whose clocks run behind. */
212
+ const PULL_OVERLAP_MS = 15 * 60_000;
213
+ /** Postgres refused the row itself: insufficient privilege (row-level security) or an integrity constraint (class 23). */
214
+ function refusedRow(error) {
215
+ const code = error?.code;
216
+ return typeof code === 'string' && (code === '42501' || code.startsWith('23'));
217
+ }
211
218
  export async function syncTeamOutbox(options = {}) {
212
219
  const config = await loadTeamConfiguration();
213
220
  if (!config?.databaseUrl)
@@ -263,14 +270,27 @@ export async function syncTeamOutbox(options = {}) {
263
270
  synced++;
264
271
  }
265
272
  catch (error) {
266
- await db.run('UPDATE sync_outbox SET attempts = attempts + 1, last_error = ? WHERE id = ?', String(error instanceof Error ? error.message : error).slice(0, 1_000), item.id);
273
+ const message = String(error instanceof Error ? error.message : error).slice(0, 1_000);
274
+ // The database refused this one row (row-level security, a constraint): it will refuse it every time,
275
+ // so it is set aside with the reason and the rest of the queue goes on. Anything else (the
276
+ // connection, the server) stops the sync and the item is tried again next time.
277
+ if (refusedRow(error)) {
278
+ await db.run('UPDATE sync_outbox SET synced_at = ?, last_error = ? WHERE id = ?', Date.now(), `refused by the team database: ${message}`, item.id);
279
+ continue;
280
+ }
281
+ await db.run('UPDATE sync_outbox SET attempts = attempts + 1, last_error = ? WHERE id = ?', message, item.id);
267
282
  throw error;
268
283
  }
269
284
  }
285
+ // Only what changed since the last pull, less a margin: updated_at comes from each member's clock, and
286
+ // re-reading a few minutes is harmless (the local write is an upsert) where a skewed clock would lose a lesson.
287
+ const mark = `team_pulled:${config.organizationId}/${config.teamId}/${config.actorId}`;
288
+ const last = Number((await db.get('SELECT value FROM meta WHERE key = ?', mark))?.value ?? 0);
270
289
  const remote = await pool.query(`SELECT * FROM rigour.lessons
271
290
  WHERE team_id = $1 AND organization_id = $3
272
291
  AND ((actor_id = $2) OR (visibility = 'team' AND state = 'promoted'))
273
- ORDER BY updated_at ASC`, [config.teamId, config.actorId, config.organizationId]);
292
+ AND updated_at > $4
293
+ ORDER BY updated_at ASC`, [config.teamId, config.actorId, config.organizationId, Math.max(0, last - PULL_OVERLAP_MS)]);
274
294
  for (const row of remote.rows) {
275
295
  const evidence = typeof row.evidence_json === 'string' ? JSON.parse(row.evidence_json) : row.evidence_json;
276
296
  await db.run(`INSERT INTO lessons (
@@ -283,8 +303,11 @@ export async function syncTeamOutbox(options = {}) {
283
303
  updated_at=excluded.updated_at`, row.id, row.repository_id, row.actor_id, row.team_id, row.visibility, row.state, row.kind, row.subject, await encryptLocalPayload(evidence), row.confidence, row.source, row.supersedes_id, Number(row.created_at), Number(row.updated_at));
284
304
  pulled++;
285
305
  }
286
- if (pulled > 0)
306
+ if (pulled > 0) {
307
+ const newest = Math.max(last, ...remote.rows.map((row) => Number(row.updated_at)));
308
+ await db.run('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', mark, String(newest));
287
309
  await db.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
310
+ }
288
311
  }
289
312
  finally {
290
313
  await pool.end();
@@ -1,11 +1,15 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  /** The outbox and repositories a machine holds, and what reached the team database. */
3
- const state = vi.hoisted(() => ({ outbox: [], repos: new Map(), sent: [] }));
3
+ const state = vi.hoisted(() => ({ outbox: [], repos: new Map(), sent: [], refuse: new Set(), meta: new Map(), remote: [], since: [] }));
4
4
  vi.mock('./db.js', () => ({
5
5
  openDatabase: async () => ({
6
6
  all: async () => state.outbox.filter(o => o.synced_at == null),
7
- get: async (_sql, id) => (state.repos.has(id) ? { canonical_uri: state.repos.get(id) } : undefined),
7
+ get: async (sql, id) => sql.includes('FROM meta')
8
+ ? (state.meta.has(id) ? { value: state.meta.get(id) } : undefined)
9
+ : (state.repos.has(id) ? { canonical_uri: state.repos.get(id) } : undefined),
8
10
  run: async (sql, ...p) => {
11
+ if (sql.startsWith('INSERT INTO meta'))
12
+ state.meta.set(p[0], p[1]);
9
13
  if (sql.startsWith('UPDATE sync_outbox SET synced_at')) {
10
14
  const id = p.at(-1);
11
15
  Object.assign(state.outbox.find(o => o.id === id) ?? {}, { synced_at: p[0], last_error: p.length === 3 ? p[1] : null });
@@ -20,9 +24,15 @@ vi.mock('pg', () => ({
20
24
  Pool: class {
21
25
  async query(sql, params) {
22
26
  if (sql.includes('INSERT INTO rigour.lessons')) {
27
+ if (state.refuse.has(params[0]))
28
+ throw Object.assign(new Error('new row violates row-level security policy for table "lessons"'), { code: '42501' });
23
29
  state.sent.push(params[0]);
24
30
  return { rowCount: 1, rows: [] };
25
31
  }
32
+ if (sql.includes('FROM rigour.lessons')) {
33
+ state.since.push(params[3]);
34
+ return { rowCount: 0, rows: state.remote.filter(row => row.updated_at > params[3]) };
35
+ }
26
36
  return { rowCount: 1, rows: [] };
27
37
  }
28
38
  async end() { }
@@ -40,6 +50,10 @@ beforeEach(() => {
40
50
  delete process.env.RIGOUR_TEAM_SYNC_PERSONAL;
41
51
  state.outbox = [];
42
52
  state.sent = [];
53
+ state.refuse = new Set();
54
+ state.meta = new Map();
55
+ state.remote = [];
56
+ state.since = [];
43
57
  state.repos = new Map([['r-team', 'https://github.com/acme/web'], ['r-other', 'https://github.com/other-org/app']]);
44
58
  queue('l-team', 'r-team', 'team');
45
59
  queue('l-other', 'r-other', 'team');
@@ -60,6 +74,25 @@ describe('team sync scope', () => {
60
74
  expect(reasons['outbox-l-unknown']).toContain('repository unknown');
61
75
  expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0, withheld: 0 }); // never retried
62
76
  });
77
+ it('sets aside a lesson the database refuses and sends the rest of the queue', async () => {
78
+ state.outbox = [];
79
+ queue('l-refused', 'r-team', 'team'); // queued first: it must not hold back what comes after
80
+ queue('l-team', 'r-team', 'team');
81
+ state.refuse.add('l-refused');
82
+ expect(await syncTeamOutbox()).toMatchObject({ synced: 1 });
83
+ expect(state.sent).toEqual(['l-team']);
84
+ expect(state.outbox.find(o => o.id === 'outbox-l-refused').last_error).toContain('refused by the team database: new row violates row-level security');
85
+ expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0 }); // not tried again
86
+ });
87
+ it('pulls only what changed since the last pull, re-reading a margin for slow clocks', async () => {
88
+ const lesson = (id, updated) => ({ id, repository_id: 'r-team', actor_id: 'dev-2', team_id: 'web', visibility: 'team', state: 'promoted', kind: 'k', subject: 's', evidence_json: {}, confidence: 1, source: 'x', supersedes_id: null, created_at: updated, updated_at: updated });
89
+ const HOUR = 3_600_000;
90
+ state.remote = [lesson('a', 10 * HOUR), lesson('b', 20 * HOUR)];
91
+ expect(await syncTeamOutbox()).toMatchObject({ pulled: 2 });
92
+ expect(state.since).toEqual([0]); // the first pull reads everything
93
+ expect(await syncTeamOutbox()).toMatchObject({ pulled: 1 }); // only b, inside the 15-minute margin
94
+ expect(state.since[1]).toBe(20 * HOUR - 15 * 60_000);
95
+ });
63
96
  it('sends nothing when the team lists no repositories', async () => {
64
97
  delete process.env.RIGOUR_TEAM_REPOSITORIES;
65
98
  expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0, withheld: 4 });
@@ -15,6 +15,9 @@ export interface TelemetryDeps {
15
15
  export declare function telemetryToken(env?: Env): string;
16
16
  export declare function readTelemetryState(home?: string): TelemetryState;
17
17
  export declare function setTelemetryEnabled(enabled: boolean, home?: string): TelemetryState;
18
+ /** Opted out by the environment, whatever the stored answer. */
19
+ /** The cross-tool opt-out (consoledonottrack.com): any value but "0" means no telemetry and no update check. */
20
+ export declare function doNotTrack(env?: Env): boolean;
18
21
  export declare function isTelemetryEnabled(deps?: TelemetryDeps): boolean;
19
22
  /** Ask only once, only a person at a terminal, only when sending would be possible at all. */
20
23
  export declare function shouldAskTelemetry(isTty: boolean, deps?: TelemetryDeps): boolean;
@@ -24,5 +27,7 @@ export declare function countUsage(name: string, by?: number, deps?: TelemetryDe
24
27
  /** Send the day's counters as one `daily_usage` event once they are a day old, then start a new day. */
25
28
  export declare function flushDailyUsage(deps?: TelemetryDeps): Promise<boolean>;
26
29
  /** A duration as a coarse bucket, so timing never identifies a run. */
30
+ /** A model run's cost as a coarse bucket: never the amount. */
31
+ export declare function costBucket(usd: number | undefined): string | undefined;
27
32
  export declare function durationBucket(ms: number): string;
28
33
  export {};
@@ -50,8 +50,12 @@ function isCi(env) {
50
50
  return CI_MARKERS.some(name => !!env[name]);
51
51
  }
52
52
  /** Opted out by the environment, whatever the stored answer. */
53
+ /** The cross-tool opt-out (consoledonottrack.com): any value but "0" means no telemetry and no update check. */
54
+ export function doNotTrack(env = process.env) {
55
+ return !!env.DO_NOT_TRACK && env.DO_NOT_TRACK !== '0';
56
+ }
53
57
  function vetoed(env) {
54
- return (!!env.DO_NOT_TRACK && env.DO_NOT_TRACK !== '0') || env.RIGOUR_TELEMETRY === '0' || !telemetryToken(env);
58
+ return doNotTrack(env) || env.RIGOUR_TELEMETRY === '0' || !telemetryToken(env);
55
59
  }
56
60
  export function isTelemetryEnabled(deps = {}) {
57
61
  const env = deps.env ?? process.env;
@@ -141,6 +145,12 @@ function writeJson(target, value) {
141
145
  fs.writeFileSync(target, JSON.stringify(value, null, 2) + '\n');
142
146
  }
143
147
  /** A duration as a coarse bucket, so timing never identifies a run. */
148
+ /** A model run's cost as a coarse bucket: never the amount. */
149
+ export function costBucket(usd) {
150
+ if (typeof usd !== 'number')
151
+ return undefined;
152
+ return usd < 0.1 ? '<$0.10' : usd < 0.5 ? '$0.10-0.50' : usd < 2 ? '$0.50-2' : '>$2';
153
+ }
144
154
  export function durationBucket(ms) {
145
155
  if (ms < 1000)
146
156
  return '<1s';
@@ -13,10 +13,6 @@ export const PARADIGM_TEMPLATES = [
13
13
  complexity: 10,
14
14
  max_methods: 10,
15
15
  max_params: 5,
16
- max_nesting: 4,
17
- max_inheritance_depth: 3,
18
- max_class_dependencies: 5,
19
- max_function_lines: 60,
20
16
  },
21
17
  },
22
18
  },
@@ -35,10 +31,6 @@ export const PARADIGM_TEMPLATES = [
35
31
  complexity: 8,
36
32
  max_methods: 15, // Functions, not classes
37
33
  max_params: 4,
38
- max_nesting: 3,
39
- max_inheritance_depth: 3,
40
- max_class_dependencies: 5,
41
- max_function_lines: 40,
42
34
  },
43
35
  },
44
36
  },
@@ -7,7 +7,6 @@ export interface Template {
7
7
  paradigm?: string;
8
8
  commands?: Record<string, unknown>;
9
9
  gates?: Partial<Gates>;
10
- planned?: string[];
11
10
  ignore?: string[];
12
11
  };
13
12
  }
@@ -16,10 +16,6 @@ export const TEMPLATES = [
16
16
  max_file_lines: 300,
17
17
  required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
18
18
  },
19
- planned: [
20
- 'Layer Boundary: Components cannot import from DB',
21
- 'Prop-Drilling Detection: Max depth 5',
22
- ],
23
19
  },
24
20
  },
25
21
  {
@@ -41,10 +37,6 @@ export const TEMPLATES = [
41
37
  max_file_lines: 400,
42
38
  required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
43
39
  },
44
- planned: [
45
- 'Service Layer Enforcement: Controllers -> Services only',
46
- 'Repo Pattern: Databases access isolated to repositories/',
47
- ],
48
40
  },
49
41
  },
50
42
  {
@@ -75,10 +67,6 @@ export const TEMPLATES = [
75
67
  max_file_lines: 500,
76
68
  required_files: ['docs/DATA_DICTIONARY.md', 'docs/PIPELINE.md', 'README.md'],
77
69
  },
78
- planned: [
79
- 'Stochastic Determinism: Seed setting enforcement',
80
- 'Data Leaks: Detecting PII in notebook outputs',
81
- ],
82
70
  },
83
71
  },
84
72
  // --- Regulated Industry Presets ---
@@ -132,8 +120,6 @@ export const TEMPLATES = [
132
120
  agent_team: {
133
121
  enabled: true,
134
122
  max_concurrent_agents: 3,
135
- cross_agent_pattern_check: true,
136
- handoff_verification: true,
137
123
  task_ownership: 'strict',
138
124
  },
139
125
  },
@@ -153,8 +139,6 @@ export const TEMPLATES = [
153
139
  required_files: ['docs/SECURITY.md', 'docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
154
140
  ast: {
155
141
  complexity: 8, max_methods: 10, max_params: 4,
156
- max_nesting: 3, max_inheritance_depth: 3,
157
- max_class_dependencies: 5, max_function_lines: 40,
158
142
  },
159
143
  security: {
160
144
  enabled: true,
@@ -169,8 +153,6 @@ export const TEMPLATES = [
169
153
  agent_team: {
170
154
  enabled: true,
171
155
  max_concurrent_agents: 3,
172
- cross_agent_pattern_check: true,
173
- handoff_verification: true,
174
156
  task_ownership: 'strict',
175
157
  },
176
158
  checkpoint: {
@@ -178,7 +160,6 @@ export const TEMPLATES = [
178
160
  interval_minutes: 10,
179
161
  quality_threshold: 85,
180
162
  drift_detection: true,
181
- auto_save_on_failure: true,
182
163
  },
183
164
  },
184
165
  },
@@ -201,8 +182,6 @@ export const TEMPLATES = [
201
182
  required_files: ['docs/SECURITY.md', 'docs/RUNBOOK.md', 'README.md'],
202
183
  ast: {
203
184
  complexity: 10, max_methods: 10, max_params: 5,
204
- max_nesting: 3, max_inheritance_depth: 3,
205
- max_class_dependencies: 5, max_function_lines: 50,
206
185
  },
207
186
  security: {
208
187
  enabled: true,
@@ -217,8 +196,6 @@ export const TEMPLATES = [
217
196
  agent_team: {
218
197
  enabled: true,
219
198
  max_concurrent_agents: 3,
220
- cross_agent_pattern_check: true,
221
- handoff_verification: true,
222
199
  task_ownership: 'strict',
223
200
  },
224
201
  },
@@ -1,20 +1,14 @@
1
1
  export const UNIVERSAL_CONFIG = {
2
- version: 1,
3
2
  commands: {},
4
3
  gates: {
5
4
  max_file_lines: 500,
6
5
  forbid_todos: true,
7
6
  forbid_fixme: true,
8
- forbid_paths: [],
9
7
  required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'docs/DECISIONS.md', 'docs/TASKS.md'],
10
8
  ast: {
11
9
  complexity: 10,
12
10
  max_methods: 10,
13
11
  max_params: 5,
14
- max_nesting: 4,
15
- max_inheritance_depth: 3,
16
- max_class_dependencies: 5,
17
- max_function_lines: 50,
18
12
  },
19
13
  dependencies: {
20
14
  forbid: [],
@@ -34,11 +28,6 @@ export const UNIVERSAL_CONFIG = {
34
28
  enabled: true,
35
29
  sensitivity: 0.8,
36
30
  mining_depth: 100,
37
- ignored_patterns: [],
38
- cross_file_patterns: true,
39
- naming_consistency: true,
40
- import_relationships: true,
41
- max_cross_file_depth: 50,
42
31
  },
43
32
  environment: {
44
33
  enabled: true,
@@ -49,14 +38,11 @@ export const UNIVERSAL_CONFIG = {
49
38
  retry_loop_breaker: {
50
39
  enabled: true,
51
40
  max_retries: 3,
52
- auto_classify: true,
53
41
  doc_sources: {},
54
42
  },
55
43
  agent_team: {
56
44
  enabled: false,
57
45
  max_concurrent_agents: 3,
58
- cross_agent_pattern_check: true,
59
- handoff_verification: true,
60
46
  task_ownership: 'strict',
61
47
  },
62
48
  checkpoint: {
@@ -64,7 +50,6 @@ export const UNIVERSAL_CONFIG = {
64
50
  interval_minutes: 15,
65
51
  quality_threshold: 80,
66
52
  drift_detection: true,
67
- auto_save_on_failure: true,
68
53
  },
69
54
  security: {
70
55
  enabled: true,
@@ -106,12 +91,6 @@ export const UNIVERSAL_CONFIG = {
106
91
  ],
107
92
  allowlist_env_names: [],
108
93
  },
109
- adaptive: {
110
- enabled: false,
111
- base_coverage_threshold: 80,
112
- base_quality_threshold: 80,
113
- auto_detect_tier: true,
114
- },
115
94
  staleness: {
116
95
  enabled: false,
117
96
  rules: {
@@ -132,8 +111,6 @@ export const UNIVERSAL_CONFIG = {
132
111
  },
133
112
  hallucinated_imports: {
134
113
  enabled: true,
135
- check_relative: true,
136
- check_packages: true,
137
114
  ignore_patterns: [
138
115
  '\\.css$', '\\.scss$', '\\.less$', '\\.svg$', '\\.png$', '\\.jpg$',
139
116
  '\\.json$', '\\.wasm$', '\\.graphql$', '\\.gql$',
@@ -184,10 +161,8 @@ export const UNIVERSAL_CONFIG = {
184
161
  check_empty_tests: true,
185
162
  check_tautological: true,
186
163
  check_mock_heavy: true,
187
- check_snapshot_abuse: true,
188
164
  check_assertion_free_async: true,
189
165
  max_mocks_per_test: 5,
190
- ignore_patterns: [],
191
166
  },
192
167
  governance: {
193
168
  enabled: true,
@@ -213,14 +188,6 @@ export const UNIVERSAL_CONFIG = {
213
188
  '.windsurf/hooks.json',
214
189
  ],
215
190
  },
216
- input_validation: {
217
- enabled: true,
218
- block_on_detection: false,
219
- min_secret_length: 8,
220
- custom_patterns: [],
221
- ignore_patterns: [],
222
- audit_log: true,
223
- },
224
191
  style_drift: {
225
192
  enabled: true,
226
193
  deviation_threshold: 0.25,
@@ -258,6 +225,11 @@ export const UNIVERSAL_CONFIG = {
258
225
  change_sweep: {
259
226
  enabled: true,
260
227
  },
228
+ redundancy: {
229
+ enabled: true,
230
+ wire_contracts: [],
231
+ schema_migrations: ['supabase/migrations'],
232
+ },
261
233
  duplicate_functions: {
262
234
  enabled: true,
263
235
  },
@@ -281,11 +253,6 @@ export const UNIVERSAL_CONFIG = {
281
253
  ignore_patterns: [],
282
254
  },
283
255
  deep: {
284
- enabled: false,
285
- pro: false,
286
- max: false,
287
- provider: 'local',
288
- threads: 4,
289
256
  temperature: 0.1,
290
257
  intent_checks: false,
291
258
  checks: {
@@ -301,13 +268,7 @@ export const UNIVERSAL_CONFIG = {
301
268
  },
302
269
  },
303
270
  hooks: {
304
- enabled: false,
305
- tools: [],
306
- fast_gates: ['hallucinated-imports', 'phantom-apis', 'deprecated-apis', 'promise-safety', 'security-patterns', 'file-size'],
307
- timeout_ms: 5000,
308
- block_on_failure: false,
309
271
  require_review_ack: false,
310
- dlp: true,
311
272
  },
312
273
  output: {
313
274
  report_path: 'rigour-report.json',
@@ -317,10 +278,19 @@ export const UNIVERSAL_CONFIG = {
317
278
  show_preexisting: false,
318
279
  reviewer: {
319
280
  enabled: false,
320
- command: 'claude',
281
+ on_push: 'background',
282
+ reviewers: ['claude'],
283
+ mode: 'single',
284
+ models: {},
321
285
  timeout_ms: 900000,
286
+ panel: 'off',
287
+ mode_required: false,
288
+ panel_max_items: 20,
289
+ dismissals: false,
290
+ judges: 2,
291
+ escalate: 'always',
292
+ cross_models: {},
322
293
  },
323
294
  },
324
- planned: [],
325
295
  ignore: [],
326
296
  };