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,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { ApiAgentEngine } = require(path.resolve(__dirname, '..', 'api-agents.js'));
|
|
8
|
+
const {
|
|
9
|
+
inferApiAgentCapabilities,
|
|
10
|
+
resolveAgentRuntimeMetadata,
|
|
11
|
+
validateExplicitRuntimeDescriptor,
|
|
12
|
+
} = require(path.resolve(__dirname, '..', 'runtime-descriptor.js'));
|
|
13
|
+
|
|
14
|
+
function fail(lines, exitCode = 1) {
|
|
15
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
16
|
+
process.exit(exitCode);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assert(condition, message, problems) {
|
|
20
|
+
if (!condition) problems.push(message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJson(filePath, fallback) {
|
|
24
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
25
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sameJson(left, right) {
|
|
29
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getExplicitDescriptor(record = {}) {
|
|
33
|
+
return {
|
|
34
|
+
runtime_type: record.runtime_type,
|
|
35
|
+
provider_id: record.provider_id,
|
|
36
|
+
model_id: record.model_id,
|
|
37
|
+
capabilities: record.capabilities,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function routeAgentsByExplicitCapability(agentRecords, requiredCapability) {
|
|
42
|
+
const matched = [];
|
|
43
|
+
const rejected = [];
|
|
44
|
+
|
|
45
|
+
for (const record of agentRecords) {
|
|
46
|
+
const validation = validateExplicitRuntimeDescriptor(getExplicitDescriptor(record));
|
|
47
|
+
if (!validation.valid) {
|
|
48
|
+
rejected.push({
|
|
49
|
+
name: record && record.name ? record.name : '<unknown>',
|
|
50
|
+
errors: validation.errors,
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (validation.normalized.capabilities.includes(requiredCapability)) {
|
|
56
|
+
matched.push(record.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
matched.sort();
|
|
61
|
+
rejected.sort((left, right) => String(left.name).localeCompare(String(right.name)));
|
|
62
|
+
return { matched, rejected };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createFixtureDataDir() {
|
|
66
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ltt-provider-capabilities-'));
|
|
67
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
68
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
69
|
+
return { tempRoot, dataDir };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeFixture(tempRoot) {
|
|
73
|
+
try {
|
|
74
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main() {
|
|
79
|
+
const problems = [];
|
|
80
|
+
const fixture = createFixtureDataDir();
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const engine = new ApiAgentEngine(fixture.dataDir);
|
|
84
|
+
|
|
85
|
+
const createResults = [
|
|
86
|
+
engine.create('vision_bot', 'zai', { model: 'glm-4.6v' }),
|
|
87
|
+
engine.create('video_bot', 'comfyui', { model: 'wan_i2v' }),
|
|
88
|
+
engine.create('image_bot', 'gemini', { model: 'gemini-3-pro-image-preview' }),
|
|
89
|
+
engine.create('gemini_vid_override', 'gemini', {
|
|
90
|
+
model: 'gemini-3-pro-image-preview',
|
|
91
|
+
capabilities: ['video_generation'],
|
|
92
|
+
}),
|
|
93
|
+
engine.create('ollama_tex_override', 'ollama', {
|
|
94
|
+
model: 'llava:34b',
|
|
95
|
+
capabilities: ['texture_generation'],
|
|
96
|
+
}),
|
|
97
|
+
engine.create('zai_chat_override', 'zai', {
|
|
98
|
+
model: 'glm-image',
|
|
99
|
+
capabilities: ['chat'],
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
createResults.forEach((result, index) => {
|
|
104
|
+
assert(result && result.ok, `API agent create() should succeed for fixture agent ${index + 1}.`, problems);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const agentsFile = path.join(fixture.dataDir, 'agents.json');
|
|
108
|
+
const agents = readJson(agentsFile, {});
|
|
109
|
+
const listedAgents = engine.list();
|
|
110
|
+
|
|
111
|
+
const expectedAgents = [
|
|
112
|
+
{
|
|
113
|
+
name: 'vision_bot',
|
|
114
|
+
provider_id: 'zai',
|
|
115
|
+
model_id: 'glm-4.6v',
|
|
116
|
+
capabilities: ['vision', 'chat'],
|
|
117
|
+
bot_capability: 'vision',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'video_bot',
|
|
121
|
+
provider_id: 'comfyui',
|
|
122
|
+
model_id: 'wan_i2v',
|
|
123
|
+
capabilities: ['video_generation'],
|
|
124
|
+
bot_capability: 'video_gen',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'image_bot',
|
|
128
|
+
provider_id: 'gemini',
|
|
129
|
+
model_id: 'gemini-3-pro-image-preview',
|
|
130
|
+
capabilities: ['image_generation'],
|
|
131
|
+
bot_capability: 'image_gen',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'gemini_vid_override',
|
|
135
|
+
provider_id: 'gemini',
|
|
136
|
+
model_id: 'gemini-3-pro-image-preview',
|
|
137
|
+
capabilities: ['video_generation'],
|
|
138
|
+
bot_capability: 'video_gen',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'ollama_tex_override',
|
|
142
|
+
provider_id: 'ollama',
|
|
143
|
+
model_id: 'llava:34b',
|
|
144
|
+
capabilities: ['texture_generation'],
|
|
145
|
+
bot_capability: 'texture_gen',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'zai_chat_override',
|
|
149
|
+
provider_id: 'zai',
|
|
150
|
+
model_id: 'glm-image',
|
|
151
|
+
capabilities: ['chat'],
|
|
152
|
+
bot_capability: 'chat',
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const expected of expectedAgents) {
|
|
157
|
+
const stored = agents[expected.name] || {};
|
|
158
|
+
const listed = listedAgents.find((entry) => entry.name === expected.name) || {};
|
|
159
|
+
const validation = validateExplicitRuntimeDescriptor({
|
|
160
|
+
runtime_type: stored.runtime_type,
|
|
161
|
+
provider_id: stored.provider_id,
|
|
162
|
+
model_id: stored.model_id,
|
|
163
|
+
capabilities: stored.capabilities,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assert(validation.valid, `Stored descriptor for ${expected.name} should validate: ${validation.errors.join('; ')}`, problems);
|
|
167
|
+
assert(stored.runtime_type === 'api', `${expected.name} should store runtime_type="api".`, problems);
|
|
168
|
+
assert(stored.provider_id === expected.provider_id, `${expected.name} should store provider_id="${expected.provider_id}".`, problems);
|
|
169
|
+
assert(stored.model_id === expected.model_id, `${expected.name} should store model_id="${expected.model_id}".`, problems);
|
|
170
|
+
assert(JSON.stringify(stored.capabilities || []) === JSON.stringify(expected.capabilities), `${expected.name} should store capabilities ${JSON.stringify(expected.capabilities)}.`, problems);
|
|
171
|
+
assert(stored.provider === expected.provider_id, `${expected.name} should project legacy provider from provider_id.`, problems);
|
|
172
|
+
assert(typeof stored.provider_color === 'string' && stored.provider_color.length > 0, `${expected.name} should project a legacy provider_color.`, problems);
|
|
173
|
+
assert(stored.bot_capability === expected.bot_capability, `${expected.name} should project legacy bot_capability="${expected.bot_capability}".`, problems);
|
|
174
|
+
|
|
175
|
+
assert(listed.runtime_type === 'api', `engine.list() should expose runtime_type for ${expected.name}.`, problems);
|
|
176
|
+
assert(listed.provider_id === expected.provider_id, `engine.list() should expose provider_id for ${expected.name}.`, problems);
|
|
177
|
+
assert(listed.model_id === expected.model_id, `engine.list() should expose model_id for ${expected.name}.`, problems);
|
|
178
|
+
assert(JSON.stringify(listed.capabilities || []) === JSON.stringify(expected.capabilities), `engine.list() should expose capabilities for ${expected.name}.`, problems);
|
|
179
|
+
assert(listed.bot_capability === expected.bot_capability, `engine.list() should expose projected bot_capability for ${expected.name}.`, problems);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const heuristicOverrideFixtures = expectedAgents.filter((expected) => (
|
|
183
|
+
expected.name === 'gemini_vid_override'
|
|
184
|
+
|| expected.name === 'ollama_tex_override'
|
|
185
|
+
|| expected.name === 'zai_chat_override'
|
|
186
|
+
));
|
|
187
|
+
|
|
188
|
+
for (const expected of heuristicOverrideFixtures) {
|
|
189
|
+
const heuristicCapabilities = inferApiAgentCapabilities({
|
|
190
|
+
name: expected.name,
|
|
191
|
+
provider_id: expected.provider_id,
|
|
192
|
+
model_id: expected.model_id,
|
|
193
|
+
});
|
|
194
|
+
assert(
|
|
195
|
+
!sameJson(heuristicCapabilities, expected.capabilities),
|
|
196
|
+
`${expected.name} fixture should differ from provider/model heuristics so the validator proves explicit capability routing.`,
|
|
197
|
+
problems
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const videoRoute = routeAgentsByExplicitCapability(listedAgents, 'video_generation');
|
|
202
|
+
const textureRoute = routeAgentsByExplicitCapability(listedAgents, 'texture_generation');
|
|
203
|
+
const imageRoute = routeAgentsByExplicitCapability(listedAgents, 'image_generation');
|
|
204
|
+
const chatRoute = routeAgentsByExplicitCapability(listedAgents, 'chat');
|
|
205
|
+
const visionRoute = routeAgentsByExplicitCapability(listedAgents, 'vision');
|
|
206
|
+
|
|
207
|
+
assert(sameJson(videoRoute.matched, ['gemini_vid_override', 'video_bot']), 'Explicit video-generation routing should select only the mixed-provider agents whose explicit capabilities include video_generation.', problems);
|
|
208
|
+
assert(sameJson(textureRoute.matched, ['ollama_tex_override']), 'Explicit texture-generation routing should select the capability override even when provider/model heuristics would not.', problems);
|
|
209
|
+
assert(sameJson(imageRoute.matched, ['image_bot']), 'Explicit image-generation routing should exclude agents whose providers/models look image-capable but whose explicit capabilities say otherwise.', problems);
|
|
210
|
+
assert(sameJson(chatRoute.matched, ['vision_bot', 'zai_chat_override']), 'Explicit chat routing should include only agents whose explicit capabilities expose chat.', problems);
|
|
211
|
+
assert(sameJson(visionRoute.matched, ['vision_bot']), 'Explicit vision routing should not silently route to provider/model heuristic matches without explicit vision capability metadata.', problems);
|
|
212
|
+
assert(videoRoute.rejected.length === 0 && textureRoute.rejected.length === 0 && imageRoute.rejected.length === 0 && chatRoute.rejected.length === 0 && visionRoute.rejected.length === 0, 'Valid API-agent rows should not be rejected by strict explicit-capability routing.', problems);
|
|
213
|
+
|
|
214
|
+
const explicitPreferred = resolveAgentRuntimeMetadata({
|
|
215
|
+
name: 'explicit_preferred',
|
|
216
|
+
is_api_agent: true,
|
|
217
|
+
runtime_type: 'api',
|
|
218
|
+
provider_id: 'zai',
|
|
219
|
+
model_id: 'glm-image',
|
|
220
|
+
capabilities: ['image_generation'],
|
|
221
|
+
provider: 'legacy-provider-should-not-win',
|
|
222
|
+
bot_capability: 'chat',
|
|
223
|
+
});
|
|
224
|
+
assert(explicitPreferred.provider === 'zai', 'Explicit provider_id should override stale legacy provider projection.', problems);
|
|
225
|
+
assert(explicitPreferred.bot_capability === 'image_gen', 'Explicit capabilities should override stale legacy bot_capability.', problems);
|
|
226
|
+
|
|
227
|
+
const legacyFallback = resolveAgentRuntimeMetadata({
|
|
228
|
+
name: 'legacy_wan_worker',
|
|
229
|
+
is_api_agent: true,
|
|
230
|
+
provider: 'comfyui',
|
|
231
|
+
});
|
|
232
|
+
assert(JSON.stringify(legacyFallback.capabilities || []) === JSON.stringify(['video_generation']), 'Legacy API-agent fallback should infer video_generation from provider/name hints.', problems);
|
|
233
|
+
assert(legacyFallback.bot_capability === 'video_gen', 'Legacy API-agent fallback should still project legacy bot_capability.', problems);
|
|
234
|
+
|
|
235
|
+
const missingCapabilityMetadata = validateExplicitRuntimeDescriptor({
|
|
236
|
+
runtime_type: 'api',
|
|
237
|
+
provider_id: 'zai',
|
|
238
|
+
model_id: 'glm-image',
|
|
239
|
+
});
|
|
240
|
+
assert(!missingCapabilityMetadata.valid, 'Descriptor validation should fail closed when capabilities metadata is absent entirely.', problems);
|
|
241
|
+
|
|
242
|
+
const missingCapabilities = validateExplicitRuntimeDescriptor({
|
|
243
|
+
runtime_type: 'api',
|
|
244
|
+
provider_id: 'zai',
|
|
245
|
+
model_id: 'glm-5',
|
|
246
|
+
capabilities: [],
|
|
247
|
+
});
|
|
248
|
+
assert(!missingCapabilities.valid, 'Descriptor validation should fail closed when capabilities are missing.', problems);
|
|
249
|
+
|
|
250
|
+
const invalidCapability = validateExplicitRuntimeDescriptor({
|
|
251
|
+
runtime_type: 'api',
|
|
252
|
+
provider_id: 'zai',
|
|
253
|
+
model_id: 'glm-5',
|
|
254
|
+
capabilities: ['telepathy'],
|
|
255
|
+
});
|
|
256
|
+
assert(!invalidCapability.valid, 'Descriptor validation should reject unsupported capability tokens.', problems);
|
|
257
|
+
|
|
258
|
+
const staleCompatibilityFixtures = [
|
|
259
|
+
{
|
|
260
|
+
name: 'missing_caps_fixture',
|
|
261
|
+
runtime_type: 'api',
|
|
262
|
+
provider_id: 'zai',
|
|
263
|
+
model_id: 'glm-image',
|
|
264
|
+
capabilities: null,
|
|
265
|
+
provider: 'zai',
|
|
266
|
+
bot_capability: 'image_gen',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'invalid_caps_fixture',
|
|
270
|
+
runtime_type: 'api',
|
|
271
|
+
provider_id: 'gemini',
|
|
272
|
+
model_id: 'gemini-3-pro-image-preview',
|
|
273
|
+
capabilities: ['telepathy'],
|
|
274
|
+
provider: 'gemini',
|
|
275
|
+
bot_capability: 'image_gen',
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
for (const fixtureAgent of staleCompatibilityFixtures) {
|
|
280
|
+
const compatibilityProjection = resolveAgentRuntimeMetadata(fixtureAgent);
|
|
281
|
+
assert(
|
|
282
|
+
Array.isArray(compatibilityProjection.capabilities) && compatibilityProjection.capabilities.includes('image_generation'),
|
|
283
|
+
`${fixtureAgent.name} should remain inferable through the compatibility resolver so the strict router proves it is failing closed instead of using fallback heuristics.`,
|
|
284
|
+
problems
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const strictImageRouteWithStaleRows = routeAgentsByExplicitCapability(
|
|
289
|
+
listedAgents.concat(staleCompatibilityFixtures),
|
|
290
|
+
'image_generation'
|
|
291
|
+
);
|
|
292
|
+
assert(sameJson(strictImageRouteWithStaleRows.matched, ['image_bot']), 'Strict explicit-capability routing should still select only explicitly valid image agents when stale fallback-only rows are present.', problems);
|
|
293
|
+
assert(strictImageRouteWithStaleRows.rejected.some((entry) => entry.name === 'missing_caps_fixture'), 'Strict explicit-capability routing should reject rows with missing capability metadata instead of inferring from legacy fields.', problems);
|
|
294
|
+
assert(strictImageRouteWithStaleRows.rejected.some((entry) => entry.name === 'invalid_caps_fixture'), 'Strict explicit-capability routing should reject rows with invalid capability metadata instead of inferring from provider/model hints.', problems);
|
|
295
|
+
} finally {
|
|
296
|
+
removeFixture(fixture.tempRoot);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (problems.length > 0) {
|
|
300
|
+
fail([
|
|
301
|
+
'Provider capability validation failed.',
|
|
302
|
+
...problems.map((problem) => `- ${problem}`),
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log([
|
|
307
|
+
'Provider capability validation passed.',
|
|
308
|
+
'- API agent descriptors are stored explicitly with runtime_type/provider_id/model_id/capabilities.',
|
|
309
|
+
'- Mixed-provider routing fixtures prove explicit capabilities win even when provider/model heuristics disagree.',
|
|
310
|
+
'- Strict explicit-capability routing rejects missing/invalid metadata instead of silently using compatibility fallbacks.',
|
|
311
|
+
'- Legacy provider/provider_color/bot_capability projections still resolve from the shared descriptor helper.',
|
|
312
|
+
'- Older API-agent rows still fall back through the centralized compatibility inference path.',
|
|
313
|
+
].join('\n'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const CONTRACT_PATH = path.resolve(__dirname, '..', '..', 'docs', 'architecture', 'runtime-contract.md');
|
|
7
|
+
const CONTRACT_DISPLAY_PATH = 'docs/architecture/runtime-contract.md';
|
|
8
|
+
const USAGE = 'Usage: node agent-bridge/scripts/check-runtime-contract.js [--simulate-missing <section-key>]';
|
|
9
|
+
|
|
10
|
+
const REQUIRED_SECTIONS = [
|
|
11
|
+
{ key: 'authority_boundaries', heading: '## Authority boundaries' },
|
|
12
|
+
{ key: 'canonical_writer_rule', heading: '### 1. Canonical writer rule' },
|
|
13
|
+
{ key: 'storage_model', heading: '## Storage model' },
|
|
14
|
+
{ key: 'event_command_model', heading: '## Event / command model' },
|
|
15
|
+
{ key: 'required_event_envelope', heading: '### 3. Required event envelope' },
|
|
16
|
+
{ key: 'evidence_model', heading: '### 5. Evidence-backed completion semantics' },
|
|
17
|
+
{ key: 'branch_semantics', heading: '## Branching / isolation semantics' },
|
|
18
|
+
{ key: 'session_scope', heading: '### 5. Session scope / resumption semantics' },
|
|
19
|
+
{ key: 'versioning_migration', heading: '## Versioning / migration / compatibility' },
|
|
20
|
+
{ key: 'migration_policy', heading: '### 4. Migration policy' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function fail(message, exitCode) {
|
|
24
|
+
console.error(message);
|
|
25
|
+
process.exit(exitCode);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
if (argv.length === 0) {
|
|
30
|
+
return { simulateMissingKey: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (argv.length === 2 && argv[0] === '--simulate-missing') {
|
|
34
|
+
const simulateMissingKey = argv[1];
|
|
35
|
+
const supportedKeys = REQUIRED_SECTIONS.map((section) => section.key);
|
|
36
|
+
|
|
37
|
+
if (!supportedKeys.includes(simulateMissingKey)) {
|
|
38
|
+
fail(
|
|
39
|
+
[
|
|
40
|
+
`Unknown section key for --simulate-missing: ${simulateMissingKey}`,
|
|
41
|
+
`Supported keys: ${supportedKeys.join(', ')}`,
|
|
42
|
+
USAGE,
|
|
43
|
+
].join('\n'),
|
|
44
|
+
2
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { simulateMissingKey };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fail(USAGE, 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectHeadings(markdown) {
|
|
55
|
+
return new Set(
|
|
56
|
+
markdown
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((line) => line.trimEnd())
|
|
59
|
+
.filter((line) => /^(##|###)\s+/.test(line))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function main() {
|
|
64
|
+
const { simulateMissingKey } = parseArgs(process.argv.slice(2));
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(CONTRACT_PATH)) {
|
|
67
|
+
fail(`Runtime contract validation failed.\nMissing file: ${CONTRACT_DISPLAY_PATH}`, 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const markdown = fs.readFileSync(CONTRACT_PATH, 'utf8');
|
|
71
|
+
const headings = collectHeadings(markdown);
|
|
72
|
+
const missingSections = REQUIRED_SECTIONS.filter((section) => {
|
|
73
|
+
if (section.key === simulateMissingKey) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return !headings.has(section.heading);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (missingSections.length > 0) {
|
|
81
|
+
const lines = ['Runtime contract validation failed.', `Checked file: ${CONTRACT_DISPLAY_PATH}`];
|
|
82
|
+
|
|
83
|
+
if (simulateMissingKey) {
|
|
84
|
+
lines.push(`Simulated missing section key: ${simulateMissingKey}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
lines.push('Missing required contract sections:');
|
|
88
|
+
|
|
89
|
+
for (const section of missingSections) {
|
|
90
|
+
lines.push(`- ${section.key}: ${section.heading}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fail(lines.join('\n'), 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const successLines = [
|
|
97
|
+
'Runtime contract validation passed.',
|
|
98
|
+
`Checked file: ${CONTRACT_DISPLAY_PATH}`,
|
|
99
|
+
`Validated ${REQUIRED_SECTIONS.length} required contract sections.`,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
103
|
+
successLines.push(`- ${section.key}: ${section.heading}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(successLines.join('\n'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
|
|
8
|
+
const { createCanonicalState, createBranchPathResolvers } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
9
|
+
const { createStateIo } = require(path.resolve(__dirname, '..', 'state', 'io.js'));
|
|
10
|
+
const { createSessionsState } = require(path.resolve(__dirname, '..', 'state', 'sessions.js'));
|
|
11
|
+
|
|
12
|
+
const SERVER_FILE = path.resolve(__dirname, '..', 'server.js');
|
|
13
|
+
|
|
14
|
+
function fail(lines, exitCode = 1) {
|
|
15
|
+
fs.writeSync(2, lines.join('\n') + '\n');
|
|
16
|
+
process.exit(exitCode);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assert(condition, message, problems) {
|
|
20
|
+
if (!condition) problems.push(message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJson(filePath) {
|
|
24
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractBlock(source, startAnchor, endAnchor) {
|
|
28
|
+
const startIndex = source.indexOf(startAnchor);
|
|
29
|
+
if (startIndex === -1) return '';
|
|
30
|
+
const endIndex = endAnchor ? source.indexOf(endAnchor, startIndex + startAnchor.length) : source.length;
|
|
31
|
+
if (endIndex === -1) return source.slice(startIndex);
|
|
32
|
+
return source.slice(startIndex, endIndex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function countOccurrences(source, needle) {
|
|
36
|
+
if (!needle) return 0;
|
|
37
|
+
return source.split(needle).length - 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildWorkflow() {
|
|
41
|
+
return {
|
|
42
|
+
id: 'wf_context',
|
|
43
|
+
name: 'Session-aware context workflow',
|
|
44
|
+
status: 'active',
|
|
45
|
+
created_by: 'alpha',
|
|
46
|
+
created_at: '2026-04-16T03:20:00.000Z',
|
|
47
|
+
updated_at: '2026-04-16T03:20:00.000Z',
|
|
48
|
+
steps: [
|
|
49
|
+
{
|
|
50
|
+
id: 1,
|
|
51
|
+
description: 'Complete the upstream verified step',
|
|
52
|
+
assignee: 'alpha',
|
|
53
|
+
depends_on: [],
|
|
54
|
+
status: 'in_progress',
|
|
55
|
+
started_at: '2026-04-16T03:20:00.000Z',
|
|
56
|
+
completed_at: null,
|
|
57
|
+
notes: '',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 2,
|
|
61
|
+
description: 'Resume from upstream evidence',
|
|
62
|
+
assignee: 'alpha',
|
|
63
|
+
depends_on: [1],
|
|
64
|
+
status: 'pending',
|
|
65
|
+
started_at: null,
|
|
66
|
+
completed_at: null,
|
|
67
|
+
notes: '',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main() {
|
|
74
|
+
const problems = [];
|
|
75
|
+
const serverSource = fs.readFileSync(SERVER_FILE, 'utf8');
|
|
76
|
+
const registerBlock = extractBlock(serverSource, 'function toolRegister(name, provider = null) {', '// Update last_activity timestamp for this agent');
|
|
77
|
+
const getWorkBlock = extractBlock(serverSource, 'async function toolGetWork(params = {}) {', 'async function toolVerifyAndAdvance(params) {');
|
|
78
|
+
const getBriefingBlock = extractBlock(serverSource, 'function toolGetBriefing() {', 'function toolLockFile(filePath) {');
|
|
79
|
+
|
|
80
|
+
assert(serverSource.includes('function getAuthoritativeSessionSummary(agentName = registeredName, branchName = currentBranch, sessionId = currentSessionId) {'), 'server.js must define getAuthoritativeSessionSummary() for manifest/index-backed resume lookup.', problems);
|
|
81
|
+
assert(serverSource.includes('function buildAuthoritativeResumeContext(options = {}) {'), 'server.js must define buildAuthoritativeResumeContext() for session/evidence-first context assembly.', problems);
|
|
82
|
+
assert(serverSource.includes('function collectMessageHandoffContext(messages, branchName = currentBranch) {'), 'server.js must define collectMessageHandoffContext() for evidence-backed message resume context.', problems);
|
|
83
|
+
|
|
84
|
+
assert(registerBlock.includes('const recoveryContext = buildAuthoritativeResumeContext({'), 'toolRegister() must assemble recovery context from authoritative session/evidence helpers first.', problems);
|
|
85
|
+
assert(registerBlock.includes('result.recovery.session_summary = recoveryContext.session_summary;'), 'toolRegister() recovery payload must expose the authoritative session summary.', problems);
|
|
86
|
+
assert(registerBlock.includes('result.recovery.checkpoint_fallbacks = checkpointFallbacks;'), 'toolRegister() must keep workspace checkpoints as fallback recovery state.', problems);
|
|
87
|
+
assert(registerBlock.includes('result.recovery.compatibility_hint = compatibilityHint;'), 'toolRegister() must downgrade recovery snapshots to compatibility fallback context instead of replacing the authoritative hint.', problems);
|
|
88
|
+
|
|
89
|
+
assert(countOccurrences(getWorkBlock, 'buildAuthoritativeResumeContext({') >= 2, 'toolGetWork() must use authoritative session/evidence context in both the active-step and upcoming-step branches.', problems);
|
|
90
|
+
assert(countOccurrences(getWorkBlock, 'resume_context: { message_handoffs: messageContext }') >= 2, 'toolGetWork() must enrich both message-return branches with evidence-backed handoff context.', problems);
|
|
91
|
+
assert(getWorkBlock.includes('Fallback checkpoint (saved ${checkpoint.saved_at})'), 'toolGetWork() active-step branch must keep checkpoints as fallback after authoritative context.', problems);
|
|
92
|
+
assert(getWorkBlock.includes('checkpoint_fallbacks contains older workspace WIP notes for this workflow if you need compatibility context.'), 'toolGetWork() upcoming-step branch must preserve checkpoint fallback wording after authoritative evidence context.', problems);
|
|
93
|
+
|
|
94
|
+
assert(getBriefingBlock.includes('const briefingContext = buildAuthoritativeResumeContext({'), 'toolGetBriefing() must start from authoritative session/evidence context.', problems);
|
|
95
|
+
assert(getBriefingBlock.includes('...(briefingContext.session_summary ? { session_summary: briefingContext.session_summary } : {}),'), 'toolGetBriefing() must surface the authoritative session summary in its response.', problems);
|
|
96
|
+
assert(getBriefingBlock.includes('...(Object.keys(resumeContext).length > 0 ? { resume_context: resumeContext } : {}),'), 'toolGetBriefing() must expose resume_context before the heuristic message/task summary fields.', problems);
|
|
97
|
+
assert(getBriefingBlock.indexOf('session_summary') !== -1 && getBriefingBlock.indexOf('session_summary') < getBriefingBlock.indexOf('recent_messages'), 'toolGetBriefing() must place session_summary before recent_messages in the response assembly.', problems);
|
|
98
|
+
|
|
99
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-session-aware-context-'));
|
|
100
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
101
|
+
const branchName = 'feature_task5c';
|
|
102
|
+
const branchPaths = createBranchPathResolvers(dataDir);
|
|
103
|
+
const io = createStateIo({ dataDir });
|
|
104
|
+
const eventLog = createCanonicalEventLog({ dataDir });
|
|
105
|
+
const sessionsState = createSessionsState({ io, branchPaths, canonicalEventLog: eventLog });
|
|
106
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: 5150 });
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
110
|
+
fs.writeFileSync(branchPaths.getWorkflowsFile(branchName), JSON.stringify([buildWorkflow()], null, 2));
|
|
111
|
+
|
|
112
|
+
const activation = sessionsState.activateSession({
|
|
113
|
+
agentName: 'alpha',
|
|
114
|
+
branchName,
|
|
115
|
+
provider: 'claude',
|
|
116
|
+
reason: 'register',
|
|
117
|
+
at: '2026-04-16T03:20:00.000Z',
|
|
118
|
+
});
|
|
119
|
+
const sessionSummary = sessionsState.getLatestSessionSummaryForAgent(branchName, 'alpha', {
|
|
120
|
+
indexedAt: '2026-04-16T03:20:05.000Z',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert(!!sessionSummary, 'sessionsState must be able to project an authoritative session summary for the active branch session.', problems);
|
|
124
|
+
assert(sessionSummary && sessionSummary.session_id === activation.session.session_id, 'Projected session summary must track the active session manifest id.', problems);
|
|
125
|
+
assert(sessionSummary && sessionSummary.branch_id === branchName, 'Projected session summary must remain branch-scoped.', problems);
|
|
126
|
+
assert(sessionSummary && sessionSummary.state === 'active', 'Projected session summary must reflect the active session state.', problems);
|
|
127
|
+
|
|
128
|
+
const completion = canonicalState.advanceWorkflow({
|
|
129
|
+
workflowId: 'wf_context',
|
|
130
|
+
actor: 'alpha',
|
|
131
|
+
branch: branchName,
|
|
132
|
+
sessionId: activation.session.session_id,
|
|
133
|
+
expectedAssignee: 'alpha',
|
|
134
|
+
sourceTool: 'verify_and_advance',
|
|
135
|
+
evidence: {
|
|
136
|
+
summary: 'Completed the upstream step with evidence',
|
|
137
|
+
verification: 'Ran the Task 5C deterministic fixture',
|
|
138
|
+
files_changed: ['agent-bridge/server.js'],
|
|
139
|
+
confidence: 92,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert(completion.success, 'canonicalState.advanceWorkflow() must produce evidence-backed workflow context for the Task 5C fixture.', problems);
|
|
144
|
+
assert(!!completion.evidence_ref, 'advanceWorkflow() must return an evidence reference for the completed upstream step.', problems);
|
|
145
|
+
|
|
146
|
+
const projectedEvidence = canonicalState.projectEvidence(branchName, completion.evidence_ref);
|
|
147
|
+
const workflows = readJson(branchPaths.getWorkflowsFile(branchName));
|
|
148
|
+
const workflow = workflows.find((entry) => entry.id === 'wf_context');
|
|
149
|
+
const completedStep = workflow.steps.find((step) => step.id === 1);
|
|
150
|
+
const activeStep = workflow.steps.find((step) => step.status === 'in_progress');
|
|
151
|
+
|
|
152
|
+
assert(!!projectedEvidence, 'canonicalState.projectEvidence() must resolve an evidence reference into the existing verification projection shape.', problems);
|
|
153
|
+
assert(projectedEvidence && projectedEvidence.evidence_ref && projectedEvidence.evidence_ref.evidence_id === completion.evidence_ref.evidence_id, 'Projected evidence must preserve the evidence reference identity.', problems);
|
|
154
|
+
assert(projectedEvidence && projectedEvidence.recorded_by_session === activation.session.session_id, 'Projected evidence must retain the recorded_by_session field for resume context.', problems);
|
|
155
|
+
assert(projectedEvidence && projectedEvidence.summary === 'Completed the upstream step with evidence', 'Projected evidence must preserve the summary text for downstream briefing/work context.', problems);
|
|
156
|
+
assert(completedStep && completedStep.verification && completedStep.verification.summary === projectedEvidence.summary, 'Workflow step verification projection must stay aligned with projectEvidence() output.', problems);
|
|
157
|
+
assert(activeStep && activeStep.id === 2, 'Evidence-backed completion fixture must activate the dependency step for downstream prep/resume context.', problems);
|
|
158
|
+
} finally {
|
|
159
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (problems.length > 0) {
|
|
163
|
+
fail(['Session-aware context validation failed.', ...problems.map((problem) => `- ${problem}`)], 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log([
|
|
167
|
+
'Session-aware context validation passed.',
|
|
168
|
+
'Validated session summary and evidence projection helpers plus the targeted server seams for get_briefing(), get_work(), and register recovery.',
|
|
169
|
+
].join('\n'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main();
|