wogiflow 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/workspace-gates.js +87 -0
- package/lib/workspace-session.js +308 -0
- package/lib/workspace.js +39 -3
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +27 -4
- package/scripts/flow-config-migrate.js +270 -0
- package/scripts/flow-context-manifest.js +322 -0
- package/scripts/flow-done-gates.js +76 -0
- package/scripts/flow-done.js +14 -0
- package/scripts/flow-gate-latch.js +119 -0
- package/scripts/hooks/core/post-compact.js +11 -1
- package/scripts/hooks/core/session-context.js +51 -7
- package/scripts/hooks/core/task-completed.js +26 -0
- package/scripts/postinstall.js +20 -0
package/lib/workspace-gates.js
CHANGED
|
@@ -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/
|
|
579
|
-
2.
|
|
580
|
-
3.
|
|
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
|
@@ -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:
|
|
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:
|
|
428
|
+
blockOnSimilar: true,
|
|
406
429
|
injectContext: true,
|
|
407
430
|
preferVariants: true,
|
|
408
431
|
requireAppMapEntry: true,
|