wogiflow 2.8.0 → 2.9.1

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.
@@ -105,7 +105,10 @@ Use `AskUserQuestion` to let user select.
105
105
  - What was the root cause?
106
106
  - What should have been done differently?
107
107
 
108
- 4. Propose a rule:
108
+ 4. **Auto-route via knowledge router** (NEW):
109
+ Run `node -e "const kr = require('wogiflow/scripts/flow-knowledge-router'); const routes = kr.detectKnowledgeRoute('[correction text]', { currentModel: process.env.CLAUDE_MODEL }); console.log(JSON.stringify(routes))"` to get routing suggestions. Use the highest-confidence route as the default option.
110
+
111
+ 5. Propose a rule with routing recommendation:
109
112
 
110
113
  ```
111
114
  Based on recent work, here's what I found:
@@ -114,19 +117,23 @@ Incident: [description from request-log/corrections]
114
117
  Root Cause: [analysis]
115
118
  Proposed Rule: "[rule statement]"
116
119
 
120
+ Knowledge Router suggests: [route type] (confidence: XX%)
121
+
117
122
  What should we do with this learning?
118
123
  1. Create a project rule (routes to /wogi-decide flow)
119
124
  2. Fix WogiFlow product behavior (edit command/script/template)
120
- 3. Add to feedback-patterns for monitoring first
121
- 4. Skip not a recurring issue
125
+ 3. Store as skill learning (auto-routes to matching skill)
126
+ 4. Add to feedback-patterns for monitoring first
127
+ 5. Skip — not a recurring issue
122
128
  ```
123
129
 
124
130
  Use `AskUserQuestion` to present options.
125
131
 
126
132
  If option 1: Invoke `/wogi-decide --from-pattern` with the proposed rule (uses streamlined path, writes to project `decisions.md`). If user cancels within the /wogi-decide sub-flow, return to wogi-learn and display "Rule creation cancelled. Pattern not promoted."
127
133
  If option 2: Apply the product-level fix directly — edit the relevant command `.md` file, script, or template. If the fix needs investigation, add to `.workflow/state/product-feedback.md`. Do NOT write to `decisions.md` (product fixes don't belong in per-project state).
128
- If option 3: Add to `feedback-patterns.md` Pending Patterns section with count 1.
129
- If option 4: No action taken.
134
+ If option 3: Route via knowledge router: `node -e "const kr = require('wogiflow/scripts/flow-knowledge-router'); kr.storeByRoute(correction, route, context)"` stores to the matching skill's `knowledge/learnings.md`.
135
+ If option 4: Add to `feedback-patterns.md` Pending Patterns section with count 1.
136
+ If option 5: No action taken.
130
137
 
131
138
  ### Mode C: Bulk Promotion
132
139
 
@@ -63,6 +63,12 @@ const WORKSPACE_GATES = [
63
63
  description: 'Verify integration map is up-to-date',
64
64
  phase: 'pre',
65
65
  severity: 'warning'
66
+ },
67
+ {
68
+ name: 'deploymentReadiness',
69
+ description: 'Verify changes are committed and pushed before handoff to downstream workers',
70
+ phase: 'post',
71
+ severity: 'error'
66
72
  }
67
73
  ];
68
74
 
@@ -434,6 +440,84 @@ function broadcastPostChange(workspaceRoot, fromRepo, context, options = {}) {
434
440
  * @param {Object} [taskMeta] — { taskId, taskTitle, changedFiles, impactAssessed }
435
441
  * @returns {{ passed: boolean, message: string, severity: string }}
436
442
  */
443
+ /**
444
+ * Deployment readiness gate — verifies changes are committed and pushed
445
+ * before allowing handoff to downstream workers.
446
+ *
447
+ * In workspace mode, when backend completes and frontend needs to start,
448
+ * the backend's changes MUST be committed and pushed first. Otherwise the
449
+ * frontend worker will build against stale code.
450
+ *
451
+ * Checks:
452
+ * 1. No uncommitted changes in the current repo (git status clean)
453
+ * 2. Local branch is not ahead of remote (changes are pushed)
454
+ *
455
+ * @param {string} workspaceRoot
456
+ * @param {Object} context
457
+ * @param {Object} taskMeta
458
+ * @returns {{ passed: boolean, message: string, severity: string }}
459
+ */
460
+ function gateDeploymentReadiness(workspaceRoot, context, taskMeta) {
461
+ const { execFileSync } = require('node:child_process');
462
+
463
+ try {
464
+ // Check 1: No uncommitted changes
465
+ const statusOutput = execFileSync('git', ['status', '--porcelain'], {
466
+ encoding: 'utf-8',
467
+ timeout: 5000,
468
+ stdio: ['pipe', 'pipe', 'pipe'],
469
+ cwd: workspaceRoot || process.cwd()
470
+ }).trim();
471
+
472
+ if (statusOutput) {
473
+ const lineCount = statusOutput.split('\n').filter(Boolean).length;
474
+ return {
475
+ passed: false,
476
+ message: `${lineCount} uncommitted change(s). Commit and push before handoff to downstream workers.`,
477
+ severity: 'error'
478
+ };
479
+ }
480
+
481
+ // Check 2: Not ahead of remote (changes pushed)
482
+ try {
483
+ const aheadOutput = execFileSync('git', ['rev-list', '--count', '@{upstream}..HEAD'], {
484
+ encoding: 'utf-8',
485
+ timeout: 5000,
486
+ stdio: ['pipe', 'pipe', 'pipe'],
487
+ cwd: workspaceRoot || process.cwd()
488
+ }).trim();
489
+
490
+ const aheadCount = parseInt(aheadOutput, 10);
491
+ if (aheadCount > 0) {
492
+ return {
493
+ passed: false,
494
+ message: `${aheadCount} commit(s) not pushed to remote. Push before handoff to downstream workers.`,
495
+ severity: 'error'
496
+ };
497
+ }
498
+ } catch (_err) {
499
+ // No upstream configured — skip push check but warn
500
+ return {
501
+ passed: true,
502
+ message: 'No upstream branch configured — push check skipped',
503
+ severity: 'warning'
504
+ };
505
+ }
506
+
507
+ return {
508
+ passed: true,
509
+ message: 'Changes committed and pushed — ready for downstream handoff',
510
+ severity: 'info'
511
+ };
512
+ } catch (err) {
513
+ return {
514
+ passed: true,
515
+ message: `Deployment readiness check failed (${err.message}) — degraded to manual`,
516
+ severity: 'warning'
517
+ };
518
+ }
519
+ }
520
+
437
521
  function runWorkspaceGate(gateName, workspaceRoot, context, taskMeta = {}) {
438
522
  switch (gateName) {
439
523
  case 'crossRepoImpactCheck':
@@ -451,6 +535,9 @@ function runWorkspaceGate(gateName, workspaceRoot, context, taskMeta = {}) {
451
535
  case 'integrationMapFreshness':
452
536
  return gateIntegrationMapFreshness(workspaceRoot);
453
537
 
538
+ case 'deploymentReadiness':
539
+ return gateDeploymentReadiness(workspaceRoot, context, taskMeta);
540
+
454
541
  default:
455
542
  return { passed: true, message: `Unknown gate: ${gateName}`, severity: 'warning' };
456
543
  }
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Workspace — Manager Session Handoff
5
+ *
6
+ * The workspace manager doesn't have WogiFlow installed locally
7
+ * (it orchestrates, doesn't code). So /wogi-session-end fails.
8
+ *
9
+ * This module provides workspace-aware session management:
10
+ * - saveManagerHandoff() — captures session state for next session
11
+ * - loadManagerHandoff() — restores state at session start
12
+ *
13
+ * The handoff document (.workspace/state/manager-session.json) includes:
14
+ * - Dispatched tasks summary (what was sent to which worker)
15
+ * - Pending/completed workspace tasks
16
+ * - Unread worker messages
17
+ * - Active locks
18
+ * - Last sync timestamp
19
+ * - Session notes (what was discussed, decisions made)
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const { WORKSPACE_CONFIG_FILE, WORKSPACE_DIR } = require('./workspace');
28
+
29
+ // ============================================================
30
+ // Manager Detection
31
+ // ============================================================
32
+
33
+ /**
34
+ * Detect if the current session is a workspace manager.
35
+ * The manager is the session running in the workspace root directory
36
+ * (where wogi-workspace.json lives), NOT in a member repo.
37
+ *
38
+ * @param {string} [cwd]
39
+ * @returns {{ isManager: boolean, workspaceRoot: string|null }}
40
+ */
41
+ function isWorkspaceManager(cwd) {
42
+ const dir = cwd || process.cwd();
43
+ const configPath = path.join(dir, WORKSPACE_CONFIG_FILE);
44
+
45
+ if (fs.existsSync(configPath)) {
46
+ // Check this is the root, not a member repo that happens to have the config
47
+ // The manager's cwd IS the workspace root
48
+ return { isManager: true, workspaceRoot: dir };
49
+ }
50
+
51
+ return { isManager: false, workspaceRoot: null };
52
+ }
53
+
54
+ // ============================================================
55
+ // Session Handoff — Save
56
+ // ============================================================
57
+
58
+ /**
59
+ * Save a manager session handoff document.
60
+ * Called when the manager session ends (or user says "wrap up").
61
+ *
62
+ * @param {string} workspaceRoot
63
+ * @param {Object} [options]
64
+ * @param {string} [options.sessionNotes] — free-text notes about what was discussed
65
+ * @param {string[]} [options.decisionsM made] — decisions made during this session
66
+ * @returns {Object} the saved handoff document
67
+ */
68
+ function saveManagerHandoff(workspaceRoot, options = {}) {
69
+ const handoff = {
70
+ savedAt: new Date().toISOString(),
71
+ workspaceName: '',
72
+ members: {},
73
+ dispatched: [],
74
+ pendingTasks: [],
75
+ completedTasks: [],
76
+ unreadMessages: [],
77
+ activeLocks: [],
78
+ contractDrifts: [],
79
+ lastSyncAt: null,
80
+ sessionNotes: options.sessionNotes || '',
81
+ decisions: options.decisions || []
82
+ };
83
+
84
+ // Read workspace config
85
+ const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
86
+ try {
87
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
88
+ handoff.workspaceName = config.name || '';
89
+
90
+ // Read each member's task status
91
+ for (const [name, memberConfig] of Object.entries(config.members || {})) {
92
+ const memberPath = path.resolve(workspaceRoot, memberConfig.path);
93
+ const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
94
+
95
+ const memberStatus = {
96
+ role: memberConfig.role,
97
+ inProgress: [],
98
+ ready: [],
99
+ recentlyCompleted: []
100
+ };
101
+
102
+ try {
103
+ if (fs.existsSync(readyPath)) {
104
+ const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
105
+ memberStatus.inProgress = (ready.inProgress || []).map(t => ({ id: t.id, title: t.title }));
106
+ memberStatus.ready = (ready.ready || []).map(t => ({ id: t.id, title: t.title }));
107
+ memberStatus.recentlyCompleted = (ready.recentlyCompleted || []).slice(0, 5).map(t => ({ id: t.id, title: t.title }));
108
+ }
109
+ } catch (_err) {
110
+ // Non-critical
111
+ }
112
+
113
+ handoff.members[name] = memberStatus;
114
+ }
115
+ } catch (_err) {
116
+ // Config read failure — save what we can
117
+ }
118
+
119
+ // Read workspace-level tasks
120
+ const wsReadyPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'ready.json');
121
+ try {
122
+ if (fs.existsSync(wsReadyPath)) {
123
+ const wsReady = JSON.parse(fs.readFileSync(wsReadyPath, 'utf-8'));
124
+ handoff.pendingTasks = (wsReady.ready || []).map(t => ({ id: t.id, title: t.title }));
125
+ handoff.completedTasks = (wsReady.recentlyCompleted || []).slice(0, 10).map(t => ({ id: t.id, title: t.title }));
126
+ }
127
+ } catch (_err) {
128
+ // Non-critical
129
+ }
130
+
131
+ // Read unread messages
132
+ try {
133
+ const messagesDir = path.join(workspaceRoot, WORKSPACE_DIR, 'messages');
134
+ if (fs.existsSync(messagesDir)) {
135
+ const files = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
136
+ for (const file of files) {
137
+ try {
138
+ const msg = JSON.parse(fs.readFileSync(path.join(messagesDir, file), 'utf-8'));
139
+ if (msg.status === 'pending') {
140
+ handoff.unreadMessages.push({
141
+ id: msg.id,
142
+ from: msg.from,
143
+ type: msg.type,
144
+ subject: msg.subject,
145
+ timestamp: msg.timestamp
146
+ });
147
+ }
148
+ } catch (_err) {
149
+ // Skip malformed
150
+ }
151
+ }
152
+ }
153
+ } catch (_err) {
154
+ // Non-critical
155
+ }
156
+
157
+ // Read active locks
158
+ try {
159
+ const locksDir = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'locks');
160
+ if (fs.existsSync(locksDir)) {
161
+ const now = Date.now();
162
+ const files = fs.readdirSync(locksDir).filter(f => f.endsWith('.json'));
163
+ for (const file of files) {
164
+ try {
165
+ const lock = JSON.parse(fs.readFileSync(path.join(locksDir, file), 'utf-8'));
166
+ if (new Date(lock.expiresAt).getTime() > now) {
167
+ handoff.activeLocks.push({
168
+ interface: lock.interface,
169
+ owner: lock.owner,
170
+ expiresAt: lock.expiresAt
171
+ });
172
+ }
173
+ } catch (_err) {
174
+ // Skip
175
+ }
176
+ }
177
+ }
178
+ } catch (_err) {
179
+ // Non-critical
180
+ }
181
+
182
+ // Check manifest freshness
183
+ const manifestPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'workspace-manifest.json');
184
+ try {
185
+ if (fs.existsSync(manifestPath)) {
186
+ const stat = fs.statSync(manifestPath);
187
+ handoff.lastSyncAt = stat.mtime.toISOString();
188
+ }
189
+ } catch (_err) {
190
+ // Non-critical
191
+ }
192
+
193
+ // Write the handoff document
194
+ const handoffPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'manager-session.json');
195
+ fs.mkdirSync(path.dirname(handoffPath), { recursive: true });
196
+ fs.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
197
+
198
+ return handoff;
199
+ }
200
+
201
+ // ============================================================
202
+ // Session Handoff — Load (for session start)
203
+ // ============================================================
204
+
205
+ /**
206
+ * Load the previous manager session handoff.
207
+ * Called at session start to restore context.
208
+ *
209
+ * @param {string} workspaceRoot
210
+ * @returns {Object|null} handoff document or null if none exists
211
+ */
212
+ function loadManagerHandoff(workspaceRoot) {
213
+ const handoffPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'manager-session.json');
214
+
215
+ try {
216
+ if (fs.existsSync(handoffPath)) {
217
+ return JSON.parse(fs.readFileSync(handoffPath, 'utf-8'));
218
+ }
219
+ } catch (_err) {
220
+ // Non-critical
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ /**
227
+ * Format the handoff document as a readable session start briefing.
228
+ *
229
+ * @param {Object} handoff — from loadManagerHandoff()
230
+ * @returns {string} formatted briefing
231
+ */
232
+ function formatHandoffBriefing(handoff) {
233
+ if (!handoff) return 'No previous session handoff found.';
234
+
235
+ const lines = [];
236
+ const age = Math.round((Date.now() - new Date(handoff.savedAt).getTime()) / (60 * 60 * 1000));
237
+
238
+ lines.push('Previous Session Handoff');
239
+ lines.push('━'.repeat(40));
240
+ lines.push(`Saved: ${handoff.savedAt} (${age}h ago)`);
241
+ lines.push(`Workspace: ${handoff.workspaceName}`);
242
+ lines.push('');
243
+
244
+ // Member status
245
+ lines.push('Member Status:');
246
+ for (const [name, status] of Object.entries(handoff.members || {})) {
247
+ const inProgress = status.inProgress?.length || 0;
248
+ const ready = status.ready?.length || 0;
249
+ lines.push(` ${name} (${status.role}): ${inProgress} in-progress, ${ready} ready`);
250
+ for (const t of status.inProgress || []) {
251
+ lines.push(` >> ${t.id}: ${t.title}`);
252
+ }
253
+ }
254
+ lines.push('');
255
+
256
+ // Unread messages
257
+ if (handoff.unreadMessages?.length > 0) {
258
+ lines.push(`Unread Messages (${handoff.unreadMessages.length}):`);
259
+ for (const msg of handoff.unreadMessages) {
260
+ lines.push(` [${msg.type}] from ${msg.from}: ${msg.subject}`);
261
+ }
262
+ lines.push('');
263
+ }
264
+
265
+ // Active locks
266
+ if (handoff.activeLocks?.length > 0) {
267
+ lines.push(`Active Locks (${handoff.activeLocks.length}):`);
268
+ for (const lock of handoff.activeLocks) {
269
+ lines.push(` ${lock.interface} — held by ${lock.owner} (expires: ${lock.expiresAt})`);
270
+ }
271
+ lines.push('');
272
+ }
273
+
274
+ // Session notes
275
+ if (handoff.sessionNotes) {
276
+ lines.push('Session Notes:');
277
+ lines.push(` ${handoff.sessionNotes}`);
278
+ lines.push('');
279
+ }
280
+
281
+ // Decisions
282
+ if (handoff.decisions?.length > 0) {
283
+ lines.push('Decisions Made:');
284
+ for (const d of handoff.decisions) {
285
+ lines.push(` - ${d}`);
286
+ }
287
+ lines.push('');
288
+ }
289
+
290
+ // Manifest freshness
291
+ if (handoff.lastSyncAt) {
292
+ const syncAge = Math.round((Date.now() - new Date(handoff.lastSyncAt).getTime()) / (60 * 60 * 1000));
293
+ lines.push(`Last sync: ${syncAge}h ago${syncAge > 24 ? ' (STALE — run flow workspace sync)' : ''}`);
294
+ }
295
+
296
+ return lines.join('\n');
297
+ }
298
+
299
+ // ============================================================
300
+ // Exports
301
+ // ============================================================
302
+
303
+ module.exports = {
304
+ isWorkspaceManager,
305
+ saveManagerHandoff,
306
+ loadManagerHandoff,
307
+ formatHandoffBriefing
308
+ };
package/lib/workspace.js CHANGED
@@ -575,9 +575,45 @@ ${memberLines.join('\n')}
575
575
  ## Session Startup Checklist
576
576
 
577
577
  When you start a session, do this FIRST:
578
- 1. Read \`.workspace/state/workspace-manifest.json\` — understand the current integration map
579
- 2. Check \`.workspace/messages/\` for unread messages (status: "pending") show them to the user
580
- 3. Read each member's \`.workflow/state/ready.json\`know what tasks are in progress
578
+ 1. **Check for previous session handoff**: Read \`.workspace/state/manager-session.json\` if it exists this tells you what happened in the last session, what's pending, and what the user was working on. Display the briefing to the user.
579
+ 2. Read \`.workspace/state/workspace-manifest.json\`understand the current integration map
580
+ 3. Check \`.workspace/messages/\` for unread messages (status: "pending") show them to the user
581
+ 4. Read each member's \`.workflow/state/ready.json\` — know what tasks are in progress
582
+ 5. If manifest is stale (>24h): suggest \`flow workspace sync\`
583
+
584
+ ## Session End (Manager Handoff)
585
+
586
+ When the user says "wrap up", "session end", "that's all":
587
+
588
+ **You are the workspace manager — you don't have /wogi-session-end.** Instead, save a handoff document:
589
+
590
+ 1. **Collect session state**:
591
+ - What tasks were dispatched to which workers (and their status)
592
+ - Current state of each member's ready.json (in-progress, ready, completed)
593
+ - Unread messages from workers
594
+ - Active locks on shared interfaces
595
+ - Manifest freshness (when was last sync?)
596
+
597
+ 2. **Ask the user**: "Any session notes or decisions to carry forward?"
598
+
599
+ 3. **Save handoff** to \`.workspace/state/manager-session.json\`:
600
+ \`\`\`bash
601
+ # Read current workspace state and save
602
+ cat .workspace/state/workspace-manifest.json | head -5 # Check freshness
603
+ ls -t .workspace/messages/*.json 2>/dev/null | head -10 # Unread messages
604
+ \`\`\`
605
+
606
+ 4. **Write the JSON handoff** with: savedAt, members (per-repo task status), unreadMessages, activeLocks, sessionNotes, decisions, lastSyncAt
607
+
608
+ 5. **Display session summary**:
609
+ \`\`\`
610
+ Session Summary:
611
+ Dispatched: N tasks to M repos
612
+ Completed: N tasks
613
+ Pending: N messages, N locks
614
+ Handoff saved to: .workspace/state/manager-session.json
615
+ Next session will resume from this point.
616
+ \`\`\`
581
617
 
582
618
  ## How to Route Tasks
583
619
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -161,6 +161,7 @@ const RESEARCH_TRIGGERS = {
161
161
  const CONFIG_DEFAULTS = {
162
162
  // --- Core ---
163
163
  version: '2.0.0',
164
+ _configVersion: 2, // Tracks config schema version for migrations (see flow-config-migrate.js)
164
165
  projectName: '',
165
166
  cli: {
166
167
  type: 'claude-code',
@@ -172,6 +173,7 @@ const CONFIG_DEFAULTS = {
172
173
  enforcement: {
173
174
  strictMode: true,
174
175
  requireTaskForImplementation: true,
176
+ requireGateLatch: true, // Gate latch: quality gates must pass before TaskCompleted allows completion
175
177
  requirePatternCitation: false,
176
178
  citationFormat: '// Pattern: {pattern}',
177
179
  blockAutoTask: true,
@@ -300,11 +302,11 @@ const CONFIG_DEFAULTS = {
300
302
  qualityGates: {
301
303
  preTaskBaseline: { enabled: false },
302
304
  feature: {
303
- require: ['loopComplete', 'tests', 'generatedTestsPass', 'uiVerification', 'apiVerification', 'registryUpdate', 'requestLogEntry', 'integrationWiring', 'standardsCompliance'],
305
+ require: ['loopComplete', 'tests', 'generatedTestsPass', 'uiVerification', 'apiVerification', 'verificationProof', 'registryUpdate', 'requestLogEntry', 'integrationWiring', 'standardsCompliance'],
304
306
  optional: ['review', 'docs', 'webmcpVerification']
305
307
  },
306
308
  bugfix: {
307
- require: ['loopComplete', 'tests', 'generatedTestsPass', 'requestLogEntry', 'standardsCompliance'],
309
+ require: ['loopComplete', 'tests', 'generatedTestsPass', 'verificationProof', 'requestLogEntry', 'standardsCompliance'],
308
310
  optional: ['learningEnforcement', 'resolutionPopulated', 'review', 'webmcpVerification']
309
311
  },
310
312
  refactor: {
@@ -327,7 +329,7 @@ const CONFIG_DEFAULTS = {
327
329
 
328
330
  // --- Standards & Compliance ---
329
331
  standardsCompliance: {
330
- enabled: false,
332
+ enabled: true,
331
333
  mode: 'block',
332
334
  scopeByTaskType: true,
333
335
  alwaysCheck: ['naming', 'security'],
@@ -337,6 +339,27 @@ const CONFIG_DEFAULTS = {
337
339
  checkpoint: { enabled: false },
338
340
  regressionTesting: { enabled: false },
339
341
 
342
+ // --- Runtime Verification (CC 2.1.89+ enforcement) ---
343
+ // Ensures agents actually test their work before marking done.
344
+ // Without this, agents claim "done" based on static evidence only.
345
+ runtimeVerification: {
346
+ enabled: true,
347
+ autoGenerateTests: true,
348
+ blockOnFailure: true,
349
+ frontend: {
350
+ method: 'webmcp',
351
+ fallback: ['playwright', 'checklist'],
352
+ devServerUrl: 'http://localhost:5173'
353
+ },
354
+ backend: {
355
+ method: 'api-test',
356
+ fallback: ['curl', 'checklist'],
357
+ baseUrl: 'http://localhost:3000'
358
+ },
359
+ testOutput: 'tests/verification',
360
+ persistTests: true
361
+ },
362
+
340
363
  // --- Detection (Project Type Awareness) ---
341
364
  detection: {
342
365
  _comment: "Weighted scoring for project type detection. Overrides take precedence over scoring.",
@@ -402,7 +425,7 @@ const CONFIG_DEFAULTS = {
402
425
  threshold: 30,
403
426
  allRegistries: true,
404
427
  aiAsJudge: true,
405
- blockOnSimilar: false,
428
+ blockOnSimilar: true,
406
429
  injectContext: true,
407
430
  preferVariants: true,
408
431
  requireAppMapEntry: true,