let-them-talk 5.3.0 → 5.4.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/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7216
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- package/templates/team.json +3 -3
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
createCanonicalState,
|
|
9
|
+
createBranchPathResolvers,
|
|
10
|
+
} = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
11
|
+
|
|
12
|
+
const FEATURE_BRANCH = 'feature_task4c';
|
|
13
|
+
const TRACKED_AGENT = 'beta';
|
|
14
|
+
const CHANNEL_NAME = 'ops';
|
|
15
|
+
const SUPPORTED_LEAKS = ['messages', 'delivery', 'control', 'channels', 'tasks-workflows', 'workspaces'];
|
|
16
|
+
const USAGE = `Usage: node agent-bridge/scripts/check-branch-isolation.js [--simulate-cross-branch-leak ${SUPPORTED_LEAKS.join('|')}]`;
|
|
17
|
+
|
|
18
|
+
function fail(lines, exitCode = 1) {
|
|
19
|
+
fs.writeSync(2, lines.join('\n') + '\n');
|
|
20
|
+
process.exit(exitCode);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
if (argv.length === 0) {
|
|
25
|
+
return { simulateCrossBranchLeak: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (argv.length === 2 && argv[0] === '--simulate-cross-branch-leak') {
|
|
29
|
+
const simulateCrossBranchLeak = argv[1];
|
|
30
|
+
if (!SUPPORTED_LEAKS.includes(simulateCrossBranchLeak)) {
|
|
31
|
+
fail([
|
|
32
|
+
`Unknown leak domain: ${simulateCrossBranchLeak}`,
|
|
33
|
+
`Supported leak domains: ${SUPPORTED_LEAKS.join(', ')}`,
|
|
34
|
+
USAGE,
|
|
35
|
+
], 2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { simulateCrossBranchLeak };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fail([USAGE], 2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stableSerialize(value) {
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return `[${value.map((entry) => stableSerialize(entry)).join(',')}]`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (value && typeof value === 'object') {
|
|
50
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`).join(',')}}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function deepEqual(actual, expected) {
|
|
57
|
+
return stableSerialize(actual) === stableSerialize(expected);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toJsonl(rows) {
|
|
61
|
+
return rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readJson(filePath, fallback) {
|
|
65
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readJsonl(filePath) {
|
|
74
|
+
if (!fs.existsSync(filePath)) return [];
|
|
75
|
+
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
76
|
+
if (!raw) return [];
|
|
77
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function displayScopedPath(dataDir, filePath) {
|
|
81
|
+
const relative = path.relative(dataDir, filePath).split(path.sep).join('/');
|
|
82
|
+
return `.agent-bridge/${relative}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function uniqueSorted(values) {
|
|
86
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectCompressedMessageIds(compressed) {
|
|
90
|
+
const segments = Array.isArray(compressed && compressed.segments) ? compressed.segments : [];
|
|
91
|
+
return uniqueSorted(
|
|
92
|
+
segments.flatMap((segment) => [segment.first_msg_id, segment.last_msg_id])
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expectEqual(problems, label, filePath, actual, expected, dataDir) {
|
|
97
|
+
if (!deepEqual(actual, expected)) {
|
|
98
|
+
problems.push(`${label} in ${displayScopedPath(dataDir, filePath)} did not match the healthy branch-local fixture.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function expectBasename(problems, label, filePath, expectedBasename, dataDir) {
|
|
103
|
+
if (path.basename(filePath) !== expectedBasename) {
|
|
104
|
+
problems.push(`${label} resolved to ${displayScopedPath(dataDir, filePath)} instead of .agent-bridge/${expectedBasename}.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function reportForeignIds(problems, label, filePath, ids, foreignIds, dataDir) {
|
|
109
|
+
const leakedIds = uniqueSorted(ids.filter((id) => foreignIds.has(id)));
|
|
110
|
+
if (leakedIds.length > 0) {
|
|
111
|
+
problems.push(`${label} leaked foreign branch message ids via ${displayScopedPath(dataDir, filePath)}: ${leakedIds.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createFixture() {
|
|
116
|
+
const generalChannel = {
|
|
117
|
+
description: 'General channel — all agents',
|
|
118
|
+
members: ['*'],
|
|
119
|
+
created_by: 'system',
|
|
120
|
+
created_at: '2026-04-15T00:00:00.000Z',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const mainMessages = [
|
|
124
|
+
{
|
|
125
|
+
id: 'msg-task4c-main-1',
|
|
126
|
+
from: 'alpha',
|
|
127
|
+
to: 'beta',
|
|
128
|
+
content: 'Main branch hello',
|
|
129
|
+
timestamp: '2026-04-15T23:40:00.000Z',
|
|
130
|
+
reply_to: null,
|
|
131
|
+
system: false,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'msg-task4c-main-2',
|
|
135
|
+
from: 'beta',
|
|
136
|
+
to: 'alpha',
|
|
137
|
+
content: 'Main branch reply',
|
|
138
|
+
timestamp: '2026-04-15T23:40:05.000Z',
|
|
139
|
+
reply_to: 'msg-task4c-main-1',
|
|
140
|
+
system: false,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const featureMessages = [
|
|
145
|
+
{
|
|
146
|
+
id: 'msg-task4c-feature-1',
|
|
147
|
+
from: 'alpha',
|
|
148
|
+
to: 'beta',
|
|
149
|
+
content: 'Feature branch hello',
|
|
150
|
+
timestamp: '2026-04-15T23:41:00.000Z',
|
|
151
|
+
reply_to: null,
|
|
152
|
+
system: false,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'msg-task4c-feature-2',
|
|
156
|
+
from: 'beta',
|
|
157
|
+
to: 'alpha',
|
|
158
|
+
content: 'Feature branch reply',
|
|
159
|
+
timestamp: '2026-04-15T23:41:05.000Z',
|
|
160
|
+
reply_to: 'msg-task4c-feature-1',
|
|
161
|
+
system: false,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const mainChannelMessages = [
|
|
166
|
+
{
|
|
167
|
+
id: 'msg-task4c-main-ops-1',
|
|
168
|
+
from: 'alpha',
|
|
169
|
+
to: '__group__',
|
|
170
|
+
channel: CHANNEL_NAME,
|
|
171
|
+
content: 'Main branch ops note',
|
|
172
|
+
timestamp: '2026-04-15T23:40:10.000Z',
|
|
173
|
+
system: false,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const featureChannelMessages = [
|
|
178
|
+
{
|
|
179
|
+
id: 'msg-task4c-feature-ops-1',
|
|
180
|
+
from: 'beta',
|
|
181
|
+
to: '__group__',
|
|
182
|
+
channel: CHANNEL_NAME,
|
|
183
|
+
content: 'Feature branch ops note',
|
|
184
|
+
timestamp: '2026-04-15T23:41:10.000Z',
|
|
185
|
+
system: false,
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
featureBranch: FEATURE_BRANCH,
|
|
191
|
+
trackedAgent: TRACKED_AGENT,
|
|
192
|
+
channelName: CHANNEL_NAME,
|
|
193
|
+
branches: {
|
|
194
|
+
main: {
|
|
195
|
+
tasks: [
|
|
196
|
+
{
|
|
197
|
+
id: 'task-task4c-main',
|
|
198
|
+
title: 'Main branch task fixture',
|
|
199
|
+
description: 'Main branch task branch-locality fixture',
|
|
200
|
+
status: 'pending',
|
|
201
|
+
assignee: 'alpha',
|
|
202
|
+
created_by: 'alpha',
|
|
203
|
+
created_at: '2026-04-15T23:39:00.000Z',
|
|
204
|
+
updated_at: '2026-04-15T23:39:00.000Z',
|
|
205
|
+
notes: [],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
workflows: [
|
|
209
|
+
{
|
|
210
|
+
id: 'wf-task4c-main',
|
|
211
|
+
name: 'Main branch workflow fixture',
|
|
212
|
+
branch_id: 'main',
|
|
213
|
+
status: 'active',
|
|
214
|
+
autonomous: false,
|
|
215
|
+
parallel: false,
|
|
216
|
+
created_by: 'alpha',
|
|
217
|
+
created_at: '2026-04-15T23:39:00.000Z',
|
|
218
|
+
updated_at: '2026-04-15T23:39:00.000Z',
|
|
219
|
+
steps: [
|
|
220
|
+
{
|
|
221
|
+
id: 1,
|
|
222
|
+
description: 'Main branch workflow step',
|
|
223
|
+
assignee: 'alpha',
|
|
224
|
+
depends_on: [],
|
|
225
|
+
status: 'in_progress',
|
|
226
|
+
started_at: '2026-04-15T23:39:00.000Z',
|
|
227
|
+
completed_at: null,
|
|
228
|
+
notes: '',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
messages: mainMessages,
|
|
234
|
+
history: mainMessages,
|
|
235
|
+
acks: {
|
|
236
|
+
'msg-task4c-main-2': {
|
|
237
|
+
acked_by: 'alpha',
|
|
238
|
+
acked_at: '2026-04-15T23:40:07.000Z',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
readReceipts: {
|
|
242
|
+
'msg-task4c-main-1': {
|
|
243
|
+
beta: '2026-04-15T23:40:01.000Z',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
consumedByAgent: {
|
|
247
|
+
[TRACKED_AGENT]: ['msg-task4c-main-1'],
|
|
248
|
+
},
|
|
249
|
+
compressed: {
|
|
250
|
+
segments: [
|
|
251
|
+
{
|
|
252
|
+
id: 'seg-task4c-main-1',
|
|
253
|
+
from_time: '2026-04-15T23:40:00.000Z',
|
|
254
|
+
to_time: '2026-04-15T23:40:05.000Z',
|
|
255
|
+
message_count: 2,
|
|
256
|
+
speakers: ['alpha', 'beta'],
|
|
257
|
+
summary: 'alpha: Main branch hello | beta: Main branch reply',
|
|
258
|
+
first_msg_id: 'msg-task4c-main-1',
|
|
259
|
+
last_msg_id: 'msg-task4c-main-2',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
last_compressed_at: '2026-04-15T23:40:06.000Z',
|
|
263
|
+
total_original_messages: 2,
|
|
264
|
+
},
|
|
265
|
+
config: {
|
|
266
|
+
conversation_mode: 'managed',
|
|
267
|
+
group_cooldown: 1000,
|
|
268
|
+
managed: {
|
|
269
|
+
manager: 'alpha',
|
|
270
|
+
phase: 'review',
|
|
271
|
+
floor: 'alpha',
|
|
272
|
+
turn_queue: ['beta'],
|
|
273
|
+
turn_current: 'alpha',
|
|
274
|
+
phase_history: [
|
|
275
|
+
{ phase: 'discussion', at: '2026-04-15T23:39:00.000Z' },
|
|
276
|
+
{ phase: 'review', at: '2026-04-15T23:40:06.000Z' },
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
channels: {
|
|
281
|
+
general: generalChannel,
|
|
282
|
+
[CHANNEL_NAME]: {
|
|
283
|
+
description: 'Main branch ops',
|
|
284
|
+
members: ['alpha', 'beta'],
|
|
285
|
+
created_by: 'alpha',
|
|
286
|
+
created_at: '2026-04-15T23:40:10.000Z',
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
channelMessages: mainChannelMessages,
|
|
290
|
+
channelHistory: mainChannelMessages,
|
|
291
|
+
workspaces: {
|
|
292
|
+
alpha: {
|
|
293
|
+
draft: { content: 'Main branch alpha workspace' },
|
|
294
|
+
},
|
|
295
|
+
beta: {
|
|
296
|
+
_status: 'Main branch workspace status',
|
|
297
|
+
retry_history: [
|
|
298
|
+
{ attempt: 1, task: 'main-task', timestamp: '2026-04-15T23:40:20.000Z' },
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
[FEATURE_BRANCH]: {
|
|
304
|
+
tasks: [
|
|
305
|
+
{
|
|
306
|
+
id: 'task-task4c-feature',
|
|
307
|
+
title: 'Feature branch task fixture',
|
|
308
|
+
description: 'Feature branch task branch-locality fixture',
|
|
309
|
+
status: 'in_progress',
|
|
310
|
+
assignee: 'beta',
|
|
311
|
+
created_by: 'beta',
|
|
312
|
+
created_at: '2026-04-15T23:40:30.000Z',
|
|
313
|
+
updated_at: '2026-04-15T23:40:30.000Z',
|
|
314
|
+
notes: [
|
|
315
|
+
{
|
|
316
|
+
by: 'beta',
|
|
317
|
+
text: 'Feature branch task note',
|
|
318
|
+
at: '2026-04-15T23:40:31.000Z',
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
workflows: [
|
|
324
|
+
{
|
|
325
|
+
id: 'wf-task4c-feature',
|
|
326
|
+
name: 'Feature branch workflow fixture',
|
|
327
|
+
branch_id: FEATURE_BRANCH,
|
|
328
|
+
status: 'active',
|
|
329
|
+
autonomous: true,
|
|
330
|
+
parallel: false,
|
|
331
|
+
created_by: 'beta',
|
|
332
|
+
created_at: '2026-04-15T23:40:30.000Z',
|
|
333
|
+
updated_at: '2026-04-15T23:40:30.000Z',
|
|
334
|
+
steps: [
|
|
335
|
+
{
|
|
336
|
+
id: 1,
|
|
337
|
+
description: 'Feature branch workflow step',
|
|
338
|
+
assignee: 'beta',
|
|
339
|
+
depends_on: [],
|
|
340
|
+
status: 'in_progress',
|
|
341
|
+
started_at: '2026-04-15T23:40:30.000Z',
|
|
342
|
+
completed_at: null,
|
|
343
|
+
notes: '',
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
messages: featureMessages,
|
|
349
|
+
history: featureMessages,
|
|
350
|
+
acks: {
|
|
351
|
+
'msg-task4c-feature-2': {
|
|
352
|
+
acked_by: 'alpha',
|
|
353
|
+
acked_at: '2026-04-15T23:41:07.000Z',
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
readReceipts: {
|
|
357
|
+
'msg-task4c-feature-1': {
|
|
358
|
+
beta: '2026-04-15T23:41:01.000Z',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
consumedByAgent: {
|
|
362
|
+
[TRACKED_AGENT]: ['msg-task4c-feature-1'],
|
|
363
|
+
},
|
|
364
|
+
compressed: {
|
|
365
|
+
segments: [
|
|
366
|
+
{
|
|
367
|
+
id: 'seg-task4c-feature-1',
|
|
368
|
+
from_time: '2026-04-15T23:41:00.000Z',
|
|
369
|
+
to_time: '2026-04-15T23:41:05.000Z',
|
|
370
|
+
message_count: 2,
|
|
371
|
+
speakers: ['alpha', 'beta'],
|
|
372
|
+
summary: 'alpha: Feature branch hello | beta: Feature branch reply',
|
|
373
|
+
first_msg_id: 'msg-task4c-feature-1',
|
|
374
|
+
last_msg_id: 'msg-task4c-feature-2',
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
last_compressed_at: '2026-04-15T23:41:06.000Z',
|
|
378
|
+
total_original_messages: 2,
|
|
379
|
+
},
|
|
380
|
+
config: {
|
|
381
|
+
conversation_mode: 'managed',
|
|
382
|
+
group_cooldown: 1750,
|
|
383
|
+
managed: {
|
|
384
|
+
manager: 'beta',
|
|
385
|
+
phase: 'execution',
|
|
386
|
+
floor: 'beta',
|
|
387
|
+
turn_queue: ['alpha'],
|
|
388
|
+
turn_current: 'beta',
|
|
389
|
+
phase_history: [
|
|
390
|
+
{ phase: 'planning', at: '2026-04-15T23:40:40.000Z' },
|
|
391
|
+
{ phase: 'execution', at: '2026-04-15T23:41:06.000Z' },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
channels: {
|
|
396
|
+
general: generalChannel,
|
|
397
|
+
[CHANNEL_NAME]: {
|
|
398
|
+
description: 'Feature branch ops',
|
|
399
|
+
members: ['beta'],
|
|
400
|
+
created_by: 'beta',
|
|
401
|
+
created_at: '2026-04-15T23:41:10.000Z',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
channelMessages: featureChannelMessages,
|
|
405
|
+
channelHistory: featureChannelMessages,
|
|
406
|
+
workspaces: {
|
|
407
|
+
alpha: {
|
|
408
|
+
draft: { content: 'Feature branch alpha workspace' },
|
|
409
|
+
},
|
|
410
|
+
beta: {
|
|
411
|
+
_status: 'Feature branch workspace status',
|
|
412
|
+
retry_history: [
|
|
413
|
+
{ attempt: 2, task: 'feature-task', timestamp: '2026-04-15T23:41:20.000Z' },
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function writeScenarioFixtures(dataDir, fixture) {
|
|
423
|
+
const branchPaths = createBranchPathResolvers(dataDir);
|
|
424
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: 4545 });
|
|
425
|
+
|
|
426
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
427
|
+
|
|
428
|
+
for (const message of fixture.branches.main.messages) {
|
|
429
|
+
canonicalState.appendMessage(message);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (const message of fixture.branches[fixture.featureBranch].messages) {
|
|
433
|
+
canonicalState.appendMessage(message, { branch: fixture.featureBranch });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (const [branchName, spec] of Object.entries(fixture.branches)) {
|
|
437
|
+
fs.writeFileSync(branchPaths.getTasksFile(branchName), JSON.stringify(spec.tasks));
|
|
438
|
+
fs.writeFileSync(branchPaths.getWorkflowsFile(branchName), JSON.stringify(spec.workflows));
|
|
439
|
+
fs.writeFileSync(branchPaths.getAcksFile(branchName), JSON.stringify(spec.acks));
|
|
440
|
+
fs.writeFileSync(branchPaths.getReadReceiptsFile(branchName), JSON.stringify(spec.readReceipts));
|
|
441
|
+
fs.writeFileSync(branchPaths.getConsumedFile(fixture.trackedAgent, branchName), JSON.stringify(spec.consumedByAgent[fixture.trackedAgent] || []));
|
|
442
|
+
fs.writeFileSync(branchPaths.getCompressedFile(branchName), JSON.stringify(spec.compressed));
|
|
443
|
+
fs.writeFileSync(branchPaths.getConfigFile(branchName), JSON.stringify(spec.config));
|
|
444
|
+
fs.writeFileSync(branchPaths.getChannelsFile(branchName), JSON.stringify(spec.channels));
|
|
445
|
+
fs.writeFileSync(branchPaths.getChannelMessagesFile(fixture.channelName, branchName), toJsonl(spec.channelMessages));
|
|
446
|
+
fs.writeFileSync(branchPaths.getChannelHistoryFile(fixture.channelName, branchName), toJsonl(spec.channelHistory));
|
|
447
|
+
fs.mkdirSync(branchPaths.getWorkspacesDir(branchName), { recursive: true });
|
|
448
|
+
for (const [agentName, workspace] of Object.entries(spec.workspaces || {})) {
|
|
449
|
+
fs.writeFileSync(branchPaths.getWorkspaceFile(agentName, branchName), JSON.stringify(workspace));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return branchPaths;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function applySimulatedCrossBranchLeak(dataDir, branchPaths, fixture, domain) {
|
|
457
|
+
const mainSpec = fixture.branches.main;
|
|
458
|
+
const featureBranch = fixture.featureBranch;
|
|
459
|
+
|
|
460
|
+
if (domain === 'messages') {
|
|
461
|
+
fs.writeFileSync(branchPaths.getMessagesFile(featureBranch), toJsonl(mainSpec.messages));
|
|
462
|
+
fs.writeFileSync(branchPaths.getHistoryFile(featureBranch), toJsonl(mainSpec.history));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (domain === 'delivery') {
|
|
467
|
+
fs.writeFileSync(branchPaths.getAcksFile(featureBranch), JSON.stringify(mainSpec.acks));
|
|
468
|
+
fs.writeFileSync(branchPaths.getReadReceiptsFile(featureBranch), JSON.stringify(mainSpec.readReceipts));
|
|
469
|
+
fs.writeFileSync(branchPaths.getConsumedFile(fixture.trackedAgent, featureBranch), JSON.stringify(mainSpec.consumedByAgent[fixture.trackedAgent] || []));
|
|
470
|
+
fs.writeFileSync(branchPaths.getCompressedFile(featureBranch), JSON.stringify(mainSpec.compressed));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (domain === 'control') {
|
|
475
|
+
fs.writeFileSync(branchPaths.getConfigFile(featureBranch), JSON.stringify(mainSpec.config));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (domain === 'channels') {
|
|
480
|
+
fs.writeFileSync(branchPaths.getChannelsFile(featureBranch), JSON.stringify(mainSpec.channels));
|
|
481
|
+
fs.writeFileSync(branchPaths.getChannelMessagesFile(fixture.channelName, featureBranch), toJsonl(mainSpec.channelMessages));
|
|
482
|
+
fs.writeFileSync(branchPaths.getChannelHistoryFile(fixture.channelName, featureBranch), toJsonl(mainSpec.channelHistory));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (domain === 'tasks-workflows') {
|
|
487
|
+
fs.writeFileSync(branchPaths.getTasksFile(featureBranch), JSON.stringify(mainSpec.tasks));
|
|
488
|
+
fs.writeFileSync(branchPaths.getWorkflowsFile(featureBranch), JSON.stringify(mainSpec.workflows));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (domain === 'workspaces') {
|
|
493
|
+
fs.rmSync(branchPaths.getWorkspacesDir(featureBranch), { recursive: true, force: true });
|
|
494
|
+
fs.mkdirSync(branchPaths.getWorkspacesDir(featureBranch), { recursive: true });
|
|
495
|
+
for (const [agentName, workspace] of Object.entries(mainSpec.workspaces || {})) {
|
|
496
|
+
fs.writeFileSync(branchPaths.getWorkspaceFile(agentName, featureBranch), JSON.stringify(workspace));
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
fail([
|
|
502
|
+
`Internal error: unsupported leak domain ${domain}`,
|
|
503
|
+
USAGE,
|
|
504
|
+
], 2);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function getBranchIsolationChecks(dataDir, branchPaths, fixture) {
|
|
508
|
+
const branchNames = ['main', fixture.featureBranch];
|
|
509
|
+
const readWorkspaces = (branchName) => {
|
|
510
|
+
const directory = branchPaths.getWorkspacesDir(branchName);
|
|
511
|
+
if (!fs.existsSync(directory)) return {};
|
|
512
|
+
|
|
513
|
+
return Object.fromEntries(
|
|
514
|
+
fs.readdirSync(directory)
|
|
515
|
+
.filter((fileName) => fileName.endsWith('.json'))
|
|
516
|
+
.sort()
|
|
517
|
+
.map((fileName) => [fileName.replace(/\.json$/i, ''), readJson(path.join(directory, fileName), {})])
|
|
518
|
+
);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
{
|
|
523
|
+
key: 'path_resolution',
|
|
524
|
+
success: 'Branch path resolvers still isolate legacy main files from branch-prefixed P0 projections.',
|
|
525
|
+
run() {
|
|
526
|
+
const problems = [];
|
|
527
|
+
const featureBranch = fixture.featureBranch;
|
|
528
|
+
|
|
529
|
+
expectBasename(problems, 'Main messages file', branchPaths.getMessagesFile('main'), 'messages.jsonl', dataDir);
|
|
530
|
+
expectBasename(problems, 'Feature messages file', branchPaths.getMessagesFile(featureBranch), `branch-${featureBranch}-messages.jsonl`, dataDir);
|
|
531
|
+
expectBasename(problems, 'Main history file', branchPaths.getHistoryFile('main'), 'history.jsonl', dataDir);
|
|
532
|
+
expectBasename(problems, 'Feature history file', branchPaths.getHistoryFile(featureBranch), `branch-${featureBranch}-history.jsonl`, dataDir);
|
|
533
|
+
expectBasename(problems, 'Main acks file', branchPaths.getAcksFile('main'), 'acks.json', dataDir);
|
|
534
|
+
expectBasename(problems, 'Main tasks file', branchPaths.getTasksFile('main'), 'tasks.json', dataDir);
|
|
535
|
+
expectBasename(problems, 'Feature tasks file', branchPaths.getTasksFile(featureBranch), `branch-${featureBranch}-tasks.json`, dataDir);
|
|
536
|
+
expectBasename(problems, 'Main workflows file', branchPaths.getWorkflowsFile('main'), 'workflows.json', dataDir);
|
|
537
|
+
expectBasename(problems, 'Feature workflows file', branchPaths.getWorkflowsFile(featureBranch), `branch-${featureBranch}-workflows.json`, dataDir);
|
|
538
|
+
expectBasename(problems, 'Feature acks file', branchPaths.getAcksFile(featureBranch), `branch-${featureBranch}-acks.json`, dataDir);
|
|
539
|
+
expectBasename(problems, 'Main read receipts file', branchPaths.getReadReceiptsFile('main'), 'read_receipts.json', dataDir);
|
|
540
|
+
expectBasename(problems, 'Feature read receipts file', branchPaths.getReadReceiptsFile(featureBranch), `branch-${featureBranch}-read_receipts.json`, dataDir);
|
|
541
|
+
expectBasename(problems, 'Main config file', branchPaths.getConfigFile('main'), 'config.json', dataDir);
|
|
542
|
+
expectBasename(problems, 'Feature config file', branchPaths.getConfigFile(featureBranch), `branch-${featureBranch}-config.json`, dataDir);
|
|
543
|
+
expectBasename(problems, 'Main channels file', branchPaths.getChannelsFile('main'), 'channels.json', dataDir);
|
|
544
|
+
expectBasename(problems, 'Feature channels file', branchPaths.getChannelsFile(featureBranch), `branch-${featureBranch}-channels.json`, dataDir);
|
|
545
|
+
expectBasename(problems, 'Main compressed file', branchPaths.getCompressedFile('main'), 'compressed.json', dataDir);
|
|
546
|
+
expectBasename(problems, 'Feature compressed file', branchPaths.getCompressedFile(featureBranch), `branch-${featureBranch}-compressed.json`, dataDir);
|
|
547
|
+
expectBasename(problems, 'Main consumed file', branchPaths.getConsumedFile(fixture.trackedAgent, 'main'), `consumed-${fixture.trackedAgent}.json`, dataDir);
|
|
548
|
+
expectBasename(problems, 'Feature consumed file', branchPaths.getConsumedFile(fixture.trackedAgent, featureBranch), `branch-${featureBranch}-consumed-${fixture.trackedAgent}.json`, dataDir);
|
|
549
|
+
expectBasename(problems, 'Main channel messages file', branchPaths.getChannelMessagesFile(fixture.channelName, 'main'), `channel-${fixture.channelName}-messages.jsonl`, dataDir);
|
|
550
|
+
expectBasename(problems, 'Feature channel messages file', branchPaths.getChannelMessagesFile(fixture.channelName, featureBranch), `branch-${featureBranch}-channel-${fixture.channelName}-messages.jsonl`, dataDir);
|
|
551
|
+
expectBasename(problems, 'Main channel history file', branchPaths.getChannelHistoryFile(fixture.channelName, 'main'), `channel-${fixture.channelName}-history.jsonl`, dataDir);
|
|
552
|
+
expectBasename(problems, 'Feature channel history file', branchPaths.getChannelHistoryFile(fixture.channelName, featureBranch), `branch-${featureBranch}-channel-${fixture.channelName}-history.jsonl`, dataDir);
|
|
553
|
+
expectBasename(problems, 'Main workspaces dir', branchPaths.getWorkspacesDir('main'), 'workspaces', dataDir);
|
|
554
|
+
expectBasename(problems, 'Feature workspaces dir', branchPaths.getWorkspacesDir(featureBranch), `branch-${featureBranch}-workspaces`, dataDir);
|
|
555
|
+
expectBasename(problems, 'Main tracked workspace file', branchPaths.getWorkspaceFile(fixture.trackedAgent, 'main'), `${fixture.trackedAgent}.json`, dataDir);
|
|
556
|
+
expectBasename(problems, 'Feature tracked workspace file', branchPaths.getWorkspaceFile(fixture.trackedAgent, featureBranch), `${fixture.trackedAgent}.json`, dataDir);
|
|
557
|
+
|
|
558
|
+
return problems;
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
key: 'messages_history',
|
|
563
|
+
success: 'Messages/history remain isolated between main and feature_task4c branches.',
|
|
564
|
+
run() {
|
|
565
|
+
const problems = [];
|
|
566
|
+
|
|
567
|
+
for (const branchName of branchNames) {
|
|
568
|
+
const spec = fixture.branches[branchName];
|
|
569
|
+
const otherBranch = branchName === 'main' ? fixture.featureBranch : 'main';
|
|
570
|
+
const foreignMessageIds = new Set(fixture.branches[otherBranch].messages.map((message) => message.id));
|
|
571
|
+
const messagesFile = branchPaths.getMessagesFile(branchName);
|
|
572
|
+
const historyFile = branchPaths.getHistoryFile(branchName);
|
|
573
|
+
const actualMessages = readJsonl(messagesFile);
|
|
574
|
+
const actualHistory = readJsonl(historyFile);
|
|
575
|
+
|
|
576
|
+
expectEqual(problems, `${branchName} messages`, messagesFile, actualMessages, spec.messages, dataDir);
|
|
577
|
+
expectEqual(problems, `${branchName} history`, historyFile, actualHistory, spec.history, dataDir);
|
|
578
|
+
reportForeignIds(problems, `${branchName} messages`, messagesFile, actualMessages.map((message) => message.id), foreignMessageIds, dataDir);
|
|
579
|
+
reportForeignIds(problems, `${branchName} history`, historyFile, actualHistory.map((message) => message.id), foreignMessageIds, dataDir);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return problems;
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
key: 'tasks_workflows',
|
|
587
|
+
success: 'Tasks and workflows remain isolated between main and feature_task4c branches.',
|
|
588
|
+
run() {
|
|
589
|
+
const problems = [];
|
|
590
|
+
|
|
591
|
+
for (const branchName of branchNames) {
|
|
592
|
+
const spec = fixture.branches[branchName];
|
|
593
|
+
const otherBranch = branchName === 'main' ? fixture.featureBranch : 'main';
|
|
594
|
+
const foreignTaskIds = new Set(fixture.branches[otherBranch].tasks.map((task) => task.id));
|
|
595
|
+
const foreignWorkflowIds = new Set(fixture.branches[otherBranch].workflows.map((workflow) => workflow.id));
|
|
596
|
+
const tasksFile = branchPaths.getTasksFile(branchName);
|
|
597
|
+
const workflowsFile = branchPaths.getWorkflowsFile(branchName);
|
|
598
|
+
const actualTasks = readJson(tasksFile, []);
|
|
599
|
+
const actualWorkflows = readJson(workflowsFile, []);
|
|
600
|
+
|
|
601
|
+
expectEqual(problems, `${branchName} tasks`, tasksFile, actualTasks, spec.tasks, dataDir);
|
|
602
|
+
expectEqual(problems, `${branchName} workflows`, workflowsFile, actualWorkflows, spec.workflows, dataDir);
|
|
603
|
+
reportForeignIds(problems, `${branchName} tasks`, tasksFile, Array.isArray(actualTasks) ? actualTasks.map((task) => task.id) : [], foreignTaskIds, dataDir);
|
|
604
|
+
reportForeignIds(problems, `${branchName} workflows`, workflowsFile, Array.isArray(actualWorkflows) ? actualWorkflows.map((workflow) => workflow.id) : [], foreignWorkflowIds, dataDir);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return problems;
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
key: 'workspaces',
|
|
612
|
+
success: 'Workspaces remain isolated between main and feature_task4c branches.',
|
|
613
|
+
run() {
|
|
614
|
+
const problems = [];
|
|
615
|
+
|
|
616
|
+
for (const branchName of branchNames) {
|
|
617
|
+
const spec = fixture.branches[branchName];
|
|
618
|
+
const otherBranch = branchName === 'main' ? fixture.featureBranch : 'main';
|
|
619
|
+
const actualWorkspaces = readWorkspaces(branchName);
|
|
620
|
+
const workspacesDir = branchPaths.getWorkspacesDir(branchName);
|
|
621
|
+
|
|
622
|
+
expectEqual(problems, `${branchName} workspaces`, workspacesDir, actualWorkspaces, spec.workspaces, dataDir);
|
|
623
|
+
if (deepEqual(actualWorkspaces, fixture.branches[otherBranch].workspaces)) {
|
|
624
|
+
problems.push(`${branchName} workspaces in ${displayScopedPath(dataDir, workspacesDir)} matched ${otherBranch} workspace state exactly.`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return problems;
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
key: 'delivery_read_state',
|
|
633
|
+
success: 'Delivery/read projections remain branch-local for consumed ids, acknowledgements, read receipts, and compressed history.',
|
|
634
|
+
run() {
|
|
635
|
+
const problems = [];
|
|
636
|
+
|
|
637
|
+
for (const branchName of branchNames) {
|
|
638
|
+
const spec = fixture.branches[branchName];
|
|
639
|
+
const otherBranch = branchName === 'main' ? fixture.featureBranch : 'main';
|
|
640
|
+
const foreignMessageIds = new Set(fixture.branches[otherBranch].messages.map((message) => message.id));
|
|
641
|
+
const acksFile = branchPaths.getAcksFile(branchName);
|
|
642
|
+
const readReceiptsFile = branchPaths.getReadReceiptsFile(branchName);
|
|
643
|
+
const consumedFile = branchPaths.getConsumedFile(fixture.trackedAgent, branchName);
|
|
644
|
+
const compressedFile = branchPaths.getCompressedFile(branchName);
|
|
645
|
+
const actualAcks = readJson(acksFile, {});
|
|
646
|
+
const actualReadReceipts = readJson(readReceiptsFile, {});
|
|
647
|
+
const actualConsumed = readJson(consumedFile, []);
|
|
648
|
+
const actualCompressed = readJson(compressedFile, {});
|
|
649
|
+
|
|
650
|
+
expectEqual(problems, `${branchName} acks`, acksFile, actualAcks, spec.acks, dataDir);
|
|
651
|
+
expectEqual(problems, `${branchName} read receipts`, readReceiptsFile, actualReadReceipts, spec.readReceipts, dataDir);
|
|
652
|
+
expectEqual(problems, `${branchName} consumed ids`, consumedFile, actualConsumed, spec.consumedByAgent[fixture.trackedAgent] || [], dataDir);
|
|
653
|
+
expectEqual(problems, `${branchName} compressed history`, compressedFile, actualCompressed, spec.compressed, dataDir);
|
|
654
|
+
|
|
655
|
+
reportForeignIds(problems, `${branchName} acks`, acksFile, Object.keys(actualAcks), foreignMessageIds, dataDir);
|
|
656
|
+
reportForeignIds(problems, `${branchName} read receipts`, readReceiptsFile, Object.keys(actualReadReceipts), foreignMessageIds, dataDir);
|
|
657
|
+
reportForeignIds(problems, `${branchName} consumed ids`, consumedFile, Array.isArray(actualConsumed) ? actualConsumed : [], foreignMessageIds, dataDir);
|
|
658
|
+
reportForeignIds(problems, `${branchName} compressed history`, compressedFile, collectCompressedMessageIds(actualCompressed), foreignMessageIds, dataDir);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return problems;
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
key: 'conversation_control',
|
|
666
|
+
success: 'Conversation mode, managed floor, and phase state remain branch-local.',
|
|
667
|
+
run() {
|
|
668
|
+
const problems = [];
|
|
669
|
+
|
|
670
|
+
for (const branchName of branchNames) {
|
|
671
|
+
const spec = fixture.branches[branchName];
|
|
672
|
+
const otherBranch = branchName === 'main' ? fixture.featureBranch : 'main';
|
|
673
|
+
const configFile = branchPaths.getConfigFile(branchName);
|
|
674
|
+
const actualConfig = readJson(configFile, {});
|
|
675
|
+
|
|
676
|
+
expectEqual(problems, `${branchName} config`, configFile, actualConfig, spec.config, dataDir);
|
|
677
|
+
if (deepEqual(actualConfig, fixture.branches[otherBranch].config)) {
|
|
678
|
+
problems.push(`${branchName} config in ${displayScopedPath(dataDir, configFile)} matched ${otherBranch} control state exactly.`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return problems;
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
key: 'non_general_channels',
|
|
687
|
+
success: 'Non-general channel metadata plus channel message/history files remain branch-local.',
|
|
688
|
+
run() {
|
|
689
|
+
const problems = [];
|
|
690
|
+
|
|
691
|
+
for (const branchName of branchNames) {
|
|
692
|
+
const spec = fixture.branches[branchName];
|
|
693
|
+
const otherBranch = branchName === 'main' ? fixture.featureBranch : 'main';
|
|
694
|
+
const foreignChannelIds = new Set(fixture.branches[otherBranch].channelHistory.map((message) => message.id));
|
|
695
|
+
const channelsFile = branchPaths.getChannelsFile(branchName);
|
|
696
|
+
const channelMessagesFile = branchPaths.getChannelMessagesFile(fixture.channelName, branchName);
|
|
697
|
+
const channelHistoryFile = branchPaths.getChannelHistoryFile(fixture.channelName, branchName);
|
|
698
|
+
const actualChannels = readJson(channelsFile, {});
|
|
699
|
+
const actualChannelMessages = readJsonl(channelMessagesFile);
|
|
700
|
+
const actualChannelHistory = readJsonl(channelHistoryFile);
|
|
701
|
+
|
|
702
|
+
expectEqual(problems, `${branchName} channels metadata`, channelsFile, actualChannels, spec.channels, dataDir);
|
|
703
|
+
expectEqual(problems, `${branchName} channel messages`, channelMessagesFile, actualChannelMessages, spec.channelMessages, dataDir);
|
|
704
|
+
expectEqual(problems, `${branchName} channel history`, channelHistoryFile, actualChannelHistory, spec.channelHistory, dataDir);
|
|
705
|
+
|
|
706
|
+
if (deepEqual(actualChannels, fixture.branches[otherBranch].channels)) {
|
|
707
|
+
problems.push(`${branchName} channel metadata in ${displayScopedPath(dataDir, channelsFile)} matched ${otherBranch} channel state exactly.`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
reportForeignIds(problems, `${branchName} channel messages`, channelMessagesFile, actualChannelMessages.map((message) => message.id), foreignChannelIds, dataDir);
|
|
711
|
+
reportForeignIds(problems, `${branchName} channel history`, channelHistoryFile, actualChannelHistory.map((message) => message.id), foreignChannelIds, dataDir);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return problems;
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function runValidation(simulateCrossBranchLeak) {
|
|
721
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-task4c-'));
|
|
722
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
723
|
+
const fixture = createFixture();
|
|
724
|
+
let result = null;
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
const branchPaths = writeScenarioFixtures(dataDir, fixture);
|
|
728
|
+
if (simulateCrossBranchLeak) {
|
|
729
|
+
applySimulatedCrossBranchLeak(dataDir, branchPaths, fixture, simulateCrossBranchLeak);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const checks = getBranchIsolationChecks(dataDir, branchPaths, fixture);
|
|
733
|
+
const failures = [];
|
|
734
|
+
const successes = [];
|
|
735
|
+
|
|
736
|
+
for (const check of checks) {
|
|
737
|
+
const problems = check.run();
|
|
738
|
+
if (problems.length > 0) {
|
|
739
|
+
failures.push({ key: check.key, problems });
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
successes.push({ key: check.key, message: check.success });
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
result = { failures, successes };
|
|
746
|
+
} finally {
|
|
747
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function main() {
|
|
754
|
+
const { simulateCrossBranchLeak } = parseArgs(process.argv.slice(2));
|
|
755
|
+
const result = runValidation(simulateCrossBranchLeak);
|
|
756
|
+
|
|
757
|
+
if (result.failures.length > 0) {
|
|
758
|
+
const lines = [
|
|
759
|
+
'Branch isolation validation failed.',
|
|
760
|
+
`Simulated cross-branch leak: ${simulateCrossBranchLeak || 'none'}`,
|
|
761
|
+
'Violations:',
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
for (const failure of result.failures) {
|
|
765
|
+
lines.push(`- ${failure.key}`);
|
|
766
|
+
for (const problem of failure.problems) {
|
|
767
|
+
lines.push(` - ${problem}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
fail(lines, 1);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const lines = [
|
|
775
|
+
'Branch isolation validation passed.',
|
|
776
|
+
`Simulated cross-branch leak: ${simulateCrossBranchLeak || 'none'}`,
|
|
777
|
+
`Validated ${result.successes.length} branch-isolation checks for Task 4 P0 domains.`,
|
|
778
|
+
];
|
|
779
|
+
|
|
780
|
+
for (const success of result.successes) {
|
|
781
|
+
lines.push(`- ${success.key}: ${success.message}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
console.log(lines.join('\n'));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
main();
|