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.
Files changed (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
@@ -0,0 +1,183 @@
1
+ // Z.AI (GLM) provider - chat + image generation via Z.AI API
2
+ // API docs: https://docs.z.ai/guides/overview/quick-start
3
+ // Models: glm-5 (chat), glm-4.6v (vision), glm-image (image gen), cogvideox-3 (video)
4
+
5
+ const https = require('https');
6
+
7
+ var API_HOST = 'api.z.ai';
8
+
9
+ class ZaiProvider {
10
+ constructor(options) {
11
+ options = options || {};
12
+ this.apiKey = options.apiKey || '';
13
+ this.model = options.model || 'glm-5';
14
+ this.name = 'zai';
15
+ this.color = '#4f46e5'; // Z.AI indigo
16
+ // Use coding endpoint if specified, otherwise try coding first (most users have coding plan)
17
+ this.useCodingEndpoint = options.useCodingEndpoint !== false;
18
+ this._apiBase = this.useCodingEndpoint ? '/api/coding/paas/v4' : '/api/paas/v4';
19
+ }
20
+
21
+ async generate(prompt, options) {
22
+ options = options || {};
23
+ if (!this.apiKey) throw new Error('Z.AI API key not configured');
24
+
25
+ // Route by model type
26
+ if (this.model === 'glm-image' || this.model.indexOf('image') !== -1) {
27
+ return this._generateImage(prompt, options);
28
+ }
29
+ if (this.model === 'cogvideox-3' || this.model.indexOf('video') !== -1) {
30
+ return this._generateText(prompt, options); // video gen returns task ID, treat as text for now
31
+ }
32
+
33
+ // Default: chat completion
34
+ return this._generateText(prompt, options);
35
+ }
36
+
37
+ async _generateText(prompt, options) {
38
+ var messages = [
39
+ { role: 'system', content: 'You are a helpful AI assistant working in a multi-agent team. Be concise and actionable.' },
40
+ { role: 'user', content: prompt },
41
+ ];
42
+
43
+ // If images attached, use vision model format
44
+ if (options.images && options.images.length > 0) {
45
+ var userContent = [{ type: 'text', text: prompt }];
46
+ for (var i = 0; i < options.images.length; i++) {
47
+ userContent.push({
48
+ type: 'image_url',
49
+ image_url: { url: 'data:' + options.images[i].mimeType + ';base64,' + options.images[i].base64 }
50
+ });
51
+ }
52
+ messages = [
53
+ { role: 'system', content: 'You are a helpful AI assistant with vision capabilities.' },
54
+ { role: 'user', content: userContent },
55
+ ];
56
+ }
57
+
58
+ var body = JSON.stringify({
59
+ model: this.model,
60
+ messages: messages,
61
+ });
62
+
63
+ var self = this;
64
+ return new Promise(function(resolve, reject) {
65
+ var req = https.request({
66
+ hostname: API_HOST,
67
+ path: self._apiBase + '/chat/completions',
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ 'Accept-Language': 'en-US,en',
72
+ 'Authorization': 'Bearer ' + self.apiKey,
73
+ },
74
+ timeout: 120000,
75
+ }, function(res) {
76
+ var data = '';
77
+ res.on('data', function(chunk) { data += chunk; });
78
+ res.on('end', function() {
79
+ try {
80
+ var result = JSON.parse(data);
81
+ if (result.error) {
82
+ reject(new Error('Z.AI API error: ' + (result.error.message || JSON.stringify(result.error))));
83
+ return;
84
+ }
85
+ if (result.choices && result.choices[0] && result.choices[0].message) {
86
+ resolve({
87
+ type: 'text',
88
+ data: result.choices[0].message.content,
89
+ model: self.model,
90
+ prompt: prompt,
91
+ });
92
+ } else {
93
+ reject(new Error('Empty response from Z.AI'));
94
+ }
95
+ } catch (e) {
96
+ reject(new Error('Failed to parse Z.AI response: ' + e.message));
97
+ }
98
+ });
99
+ });
100
+ req.on('error', function(e) { reject(new Error('Z.AI connection failed: ' + e.message)); });
101
+ req.on('timeout', function() { req.destroy(); reject(new Error('Z.AI request timed out')); });
102
+ req.write(body);
103
+ req.end();
104
+ });
105
+ }
106
+
107
+ async _generateImage(prompt, options) {
108
+ var body = JSON.stringify({
109
+ model: this.model,
110
+ prompt: prompt,
111
+ });
112
+
113
+ var self = this;
114
+ return new Promise(function(resolve, reject) {
115
+ var req = https.request({
116
+ hostname: API_HOST,
117
+ path: self._apiBase + '/images/generations',
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ 'Accept-Language': 'en-US,en',
122
+ 'Authorization': 'Bearer ' + self.apiKey,
123
+ },
124
+ timeout: 120000,
125
+ }, function(res) {
126
+ var data = '';
127
+ res.on('data', function(chunk) { data += chunk; });
128
+ res.on('end', function() {
129
+ try {
130
+ var result = JSON.parse(data);
131
+ if (result.error) {
132
+ reject(new Error('Z.AI image error: ' + (result.error.message || JSON.stringify(result.error))));
133
+ return;
134
+ }
135
+ if (result.data && result.data[0]) {
136
+ var imgData = result.data[0];
137
+ if (imgData.b64_json) {
138
+ resolve({
139
+ type: 'image',
140
+ data: imgData.b64_json,
141
+ format: 'png',
142
+ model: self.model,
143
+ prompt: prompt,
144
+ });
145
+ } else if (imgData.url) {
146
+ resolve({
147
+ type: 'image',
148
+ data: imgData.url,
149
+ format: 'url',
150
+ model: self.model,
151
+ prompt: prompt,
152
+ });
153
+ }
154
+ } else {
155
+ reject(new Error('No image data from Z.AI'));
156
+ }
157
+ } catch (e) {
158
+ reject(new Error('Failed to parse Z.AI image response: ' + e.message));
159
+ }
160
+ });
161
+ });
162
+ req.on('error', function(e) { reject(new Error('Z.AI connection failed: ' + e.message)); });
163
+ req.on('timeout', function() { req.destroy(); reject(new Error('Z.AI request timed out')); });
164
+ req.write(body);
165
+ req.end();
166
+ });
167
+ }
168
+
169
+ async checkHealth() {
170
+ return !!this.apiKey;
171
+ }
172
+
173
+ async listModels() {
174
+ return [
175
+ { name: 'glm-5', size: 'cloud', description: 'Flagship chat + agentic model' },
176
+ { name: 'glm-4.6v', size: 'cloud', description: 'Multimodal vision (128K context)' },
177
+ { name: 'glm-image', size: 'cloud', description: 'Text-to-image generation' },
178
+ { name: 'cogvideox-3', size: 'cloud', description: 'Video frame generation' },
179
+ ];
180
+ }
181
+ }
182
+
183
+ module.exports = { ZaiProvider };
@@ -0,0 +1,270 @@
1
+ const PROVIDER_COLORS = Object.freeze({
2
+ ollama: '#0ea5e9',
3
+ dalle: '#10b981',
4
+ replicate: '#8b5cf6',
5
+ gemini: '#4285f4',
6
+ comfyui: '#ff6b35',
7
+ zai: '#4f46e5',
8
+ });
9
+
10
+ const VALID_RUNTIME_TYPES = Object.freeze(['api', 'cli']);
11
+ const VALID_CAPABILITIES = Object.freeze([
12
+ 'chat',
13
+ 'vision',
14
+ 'image_generation',
15
+ 'video_generation',
16
+ 'texture_generation',
17
+ ]);
18
+
19
+ const LEGACY_TO_CAPABILITY = Object.freeze({
20
+ chat: 'chat',
21
+ vision: 'vision',
22
+ image_gen: 'image_generation',
23
+ video_gen: 'video_generation',
24
+ texture_gen: 'texture_generation',
25
+ });
26
+
27
+ const CAPABILITY_TO_LEGACY = Object.freeze({
28
+ chat: 'chat',
29
+ vision: 'vision',
30
+ image_generation: 'image_gen',
31
+ video_generation: 'video_gen',
32
+ texture_generation: 'texture_gen',
33
+ });
34
+
35
+ const PRIMARY_CAPABILITY_ORDER = Object.freeze([
36
+ 'video_generation',
37
+ 'texture_generation',
38
+ 'image_generation',
39
+ 'vision',
40
+ 'chat',
41
+ ]);
42
+
43
+ const VALID_RUNTIME_TYPE_SET = new Set(VALID_RUNTIME_TYPES);
44
+ const VALID_CAPABILITY_SET = new Set(VALID_CAPABILITIES);
45
+
46
+ function normalizeText(value) {
47
+ if (typeof value !== 'string') return null;
48
+ const trimmed = value.trim();
49
+ return trimmed ? trimmed : null;
50
+ }
51
+
52
+ function normalizeProviderId(value) {
53
+ const text = normalizeText(value);
54
+ return text ? text.toLowerCase() : null;
55
+ }
56
+
57
+ function normalizeRuntimeType(value) {
58
+ const text = normalizeText(value);
59
+ if (!text) return null;
60
+ const normalized = text.toLowerCase();
61
+ return VALID_RUNTIME_TYPE_SET.has(normalized) ? normalized : null;
62
+ }
63
+
64
+ function normalizeCapabilityToken(value) {
65
+ const text = normalizeText(value);
66
+ if (!text) return null;
67
+ const normalized = text.toLowerCase();
68
+ return VALID_CAPABILITY_SET.has(normalized) ? normalized : null;
69
+ }
70
+
71
+ function normalizeCapabilities(value) {
72
+ const entries = Array.isArray(value)
73
+ ? value
74
+ : (value == null ? [] : [value]);
75
+ const normalized = [];
76
+ const seen = new Set();
77
+
78
+ for (const entry of entries) {
79
+ const token = normalizeCapabilityToken(entry);
80
+ if (!token || seen.has(token)) continue;
81
+ seen.add(token);
82
+ normalized.push(token);
83
+ }
84
+
85
+ return normalized;
86
+ }
87
+
88
+ function normalizeLegacyCapability(value) {
89
+ const text = normalizeText(value);
90
+ if (!text) return null;
91
+ return LEGACY_TO_CAPABILITY[text.toLowerCase()] || null;
92
+ }
93
+
94
+ function projectLegacyCapability(capability) {
95
+ const token = normalizeCapabilityToken(capability);
96
+ return token ? CAPABILITY_TO_LEGACY[token] : null;
97
+ }
98
+
99
+ function getPrimaryCapability(capabilities) {
100
+ const normalized = normalizeCapabilities(capabilities);
101
+ for (const token of PRIMARY_CAPABILITY_ORDER) {
102
+ if (normalized.includes(token)) return token;
103
+ }
104
+ return normalized[0] || null;
105
+ }
106
+
107
+ function getProviderColor(providerId) {
108
+ const normalizedProviderId = normalizeProviderId(providerId);
109
+ return normalizedProviderId ? (PROVIDER_COLORS[normalizedProviderId] || null) : null;
110
+ }
111
+
112
+ function inferApiAgentCapabilities(params = {}) {
113
+ const providerId = normalizeProviderId(params.provider_id || params.provider);
114
+ const modelHint = [
115
+ normalizeText(params.model_id || params.model),
116
+ normalizeText(params.name),
117
+ ].filter(Boolean).join(' ').toLowerCase();
118
+
119
+ if (!providerId) return [];
120
+
121
+ if (providerId === 'gemini' || providerId === 'dalle' || providerId === 'replicate') {
122
+ if (modelHint.indexOf('video') !== -1 || modelHint.indexOf('runway') !== -1 || modelHint.indexOf('kling') !== -1) {
123
+ return ['video_generation'];
124
+ }
125
+ if (modelHint.indexOf('texture') !== -1 || modelHint.indexOf('material') !== -1) {
126
+ return ['texture_generation'];
127
+ }
128
+ return ['image_generation'];
129
+ }
130
+
131
+ if (providerId === 'comfyui') {
132
+ if (modelHint.indexOf('video') !== -1 || modelHint.indexOf('wan') !== -1 || modelHint.indexOf('i2v') !== -1) {
133
+ return ['video_generation'];
134
+ }
135
+ if (modelHint.indexOf('3d') !== -1 || modelHint.indexOf('mesh') !== -1 || modelHint.indexOf('texture') !== -1) {
136
+ return ['texture_generation'];
137
+ }
138
+ return ['image_generation'];
139
+ }
140
+
141
+ if (providerId === 'zai') {
142
+ if (modelHint.indexOf('image') !== -1 || modelHint === 'glm-image') {
143
+ return ['image_generation'];
144
+ }
145
+ if (modelHint.indexOf('video') !== -1 || modelHint.indexOf('cogvideo') !== -1) {
146
+ return ['video_generation'];
147
+ }
148
+ if (modelHint.indexOf('4.6v') !== -1 || modelHint.indexOf('vision') !== -1) {
149
+ return ['vision', 'chat'];
150
+ }
151
+ return ['chat'];
152
+ }
153
+
154
+ if (providerId === 'ollama') {
155
+ if (modelHint.indexOf('vision') !== -1 || modelHint.indexOf('llava') !== -1) {
156
+ return ['vision', 'chat'];
157
+ }
158
+ if (modelHint.indexOf('sdxl') !== -1 || modelHint.indexOf('flux') !== -1 || modelHint.indexOf('stable') !== -1) {
159
+ return ['image_generation'];
160
+ }
161
+ return ['chat'];
162
+ }
163
+
164
+ return ['chat'];
165
+ }
166
+
167
+ function createApiAgentRuntimeDescriptor(params = {}) {
168
+ return {
169
+ runtime_type: 'api',
170
+ provider_id: normalizeProviderId(params.provider_id || params.provider),
171
+ model_id: normalizeText(params.model_id || params.model),
172
+ capabilities: normalizeCapabilities(params.capabilities).length > 0
173
+ ? normalizeCapabilities(params.capabilities)
174
+ : inferApiAgentCapabilities(params),
175
+ };
176
+ }
177
+
178
+ function collectInvalidCapabilityTokens(value) {
179
+ if (value == null) return [];
180
+ if (!Array.isArray(value)) return ['<non-array>'];
181
+
182
+ const invalid = [];
183
+ for (const entry of value) {
184
+ const text = normalizeText(entry);
185
+ if (!text) {
186
+ invalid.push(String(entry));
187
+ continue;
188
+ }
189
+ if (!VALID_CAPABILITY_SET.has(text.toLowerCase())) invalid.push(text);
190
+ }
191
+ return invalid;
192
+ }
193
+
194
+ function validateExplicitRuntimeDescriptor(descriptor = {}) {
195
+ const normalized = {
196
+ runtime_type: normalizeRuntimeType(descriptor.runtime_type),
197
+ provider_id: normalizeProviderId(descriptor.provider_id),
198
+ model_id: normalizeText(descriptor.model_id),
199
+ capabilities: normalizeCapabilities(descriptor.capabilities),
200
+ };
201
+ const errors = [];
202
+
203
+ if (!normalized.runtime_type) {
204
+ errors.push(`runtime_type must be one of: ${VALID_RUNTIME_TYPES.join(', ')}`);
205
+ }
206
+ if (!normalized.provider_id) errors.push('provider_id is required');
207
+ if (!normalized.model_id) errors.push('model_id is required');
208
+ if (!Array.isArray(descriptor.capabilities)) errors.push('capabilities must be an array');
209
+
210
+ const invalidCapabilityTokens = collectInvalidCapabilityTokens(descriptor.capabilities);
211
+ if (invalidCapabilityTokens.length > 0) {
212
+ errors.push(`capabilities contains unsupported token(s): ${invalidCapabilityTokens.join(', ')}`);
213
+ }
214
+ if (normalized.capabilities.length === 0) {
215
+ errors.push(`capabilities must include at least one supported token: ${VALID_CAPABILITIES.join(', ')}`);
216
+ }
217
+
218
+ return {
219
+ valid: errors.length === 0,
220
+ errors,
221
+ normalized,
222
+ };
223
+ }
224
+
225
+ function resolveAgentRuntimeMetadata(params = {}) {
226
+ const runtimeType = normalizeRuntimeType(params.runtime_type) || (params.is_api_agent ? 'api' : null);
227
+ const providerId = normalizeProviderId(params.provider_id || params.provider);
228
+ const modelId = normalizeText(params.model_id || params.model);
229
+
230
+ let capabilities = normalizeCapabilities(params.capabilities);
231
+ if (capabilities.length === 0) {
232
+ const legacyCapability = normalizeLegacyCapability(params.bot_capability);
233
+ if (legacyCapability) capabilities = [legacyCapability];
234
+ }
235
+ if (capabilities.length === 0 && (runtimeType === 'api' || params.is_api_agent)) {
236
+ capabilities = inferApiAgentCapabilities({
237
+ provider_id: providerId,
238
+ model_id: modelId,
239
+ name: params.name,
240
+ });
241
+ }
242
+
243
+ const primaryCapability = getPrimaryCapability(capabilities);
244
+ const projectedProvider = providerId || normalizeProviderId(params.provider) || normalizeText(params.provider);
245
+
246
+ return {
247
+ runtime_type: runtimeType,
248
+ provider_id: providerId,
249
+ model_id: modelId,
250
+ capabilities,
251
+ provider: projectedProvider || null,
252
+ provider_color: getProviderColor(projectedProvider) || params.provider_color || null,
253
+ bot_capability: primaryCapability
254
+ ? projectLegacyCapability(primaryCapability)
255
+ : (normalizeText(params.bot_capability) || null),
256
+ };
257
+ }
258
+
259
+ module.exports = {
260
+ PROVIDER_COLORS,
261
+ VALID_RUNTIME_TYPES,
262
+ VALID_CAPABILITIES,
263
+ createApiAgentRuntimeDescriptor,
264
+ inferApiAgentCapabilities,
265
+ getPrimaryCapability,
266
+ getProviderColor,
267
+ projectLegacyCapability,
268
+ resolveAgentRuntimeMetadata,
269
+ validateExplicitRuntimeDescriptor,
270
+ };
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SERVER_FILE = path.resolve(__dirname, '..', 'server.js');
7
+ const DASHBOARD_FILE = path.resolve(__dirname, '..', 'dashboard.js');
8
+ const {
9
+ analyzeContractFit,
10
+ buildGuideContractAdvisory,
11
+ buildRuntimeContractMetadata,
12
+ resolveAgentContract,
13
+ sanitizeContractProfilePatch,
14
+ } = require(path.resolve(__dirname, '..', 'agent-contracts.js'));
15
+
16
+ function fail(lines, exitCode = 1) {
17
+ process.stderr.write(lines.join('\n') + '\n');
18
+ process.exit(exitCode);
19
+ }
20
+
21
+ function assert(condition, message, problems) {
22
+ if (!condition) problems.push(message);
23
+ }
24
+
25
+ function extractBlock(source, startAnchor, endAnchor) {
26
+ const startIndex = source.indexOf(startAnchor);
27
+ if (startIndex === -1) return '';
28
+ const endIndex = endAnchor ? source.indexOf(endAnchor, startIndex + startAnchor.length) : source.length;
29
+ if (endIndex === -1) return source.slice(startIndex);
30
+ return source.slice(startIndex, endIndex);
31
+ }
32
+
33
+ function sameJson(left, right) {
34
+ return JSON.stringify(left) === JSON.stringify(right);
35
+ }
36
+
37
+ function main() {
38
+ const problems = [];
39
+ const serverSource = fs.readFileSync(SERVER_FILE, 'utf8');
40
+ const dashboardSource = fs.readFileSync(DASHBOARD_FILE, 'utf8');
41
+
42
+ const listAgentsBlock = extractBlock(serverSource, 'function toolListAgents() {', 'async function toolSendMessage(content, to = null, reply_to = null, channel = null) {');
43
+ const buildGuideBlock = extractBlock(serverSource, 'function buildGuide(level = \'standard\') {', 'const COMPLETION_EVIDENCE_INPUT_SCHEMA = {');
44
+ const updateProfileBlock = extractBlock(serverSource, 'function toolUpdateProfile(displayName, avatar, bio, role, appearance, archetype, skills, contractMode) {', '// --- Phase 2: Workspace tools ---');
45
+ const suggestTaskBlock = extractBlock(serverSource, 'function toolSuggestTask() {', '// --- Rules system: project-level rules visible in dashboard and injected into agent guides ---');
46
+ const getWorkBlock = extractBlock(serverSource, 'async function toolGetWork(params = {}) {', 'async function toolVerifyAndAdvance(params) {');
47
+ const yieldFloorBlock = extractBlock(serverSource, 'function toolYieldFloor(to, prompt = null) {', 'function toolSetPhase(phase) {');
48
+ const setPhaseBlock = extractBlock(serverSource, 'function toolSetPhase(phase) {', 'function toolRegister(name, provider = null) {');
49
+ const apiAgentsBlock = extractBlock(dashboardSource, 'function apiAgents(query) {', 'function apiStatus(query) {');
50
+ const profilesPostBlock = extractBlock(dashboardSource, "else if (url.pathname === '/api/profiles' && req.method === 'POST') {", "else if (url.pathname === '/api/workspaces' && req.method === 'GET') {");
51
+
52
+ assert(serverSource.includes("require('./agent-contracts')"), 'server.js must load the shared advisory contract module.', problems);
53
+ assert(listAgentsBlock.includes('buildRuntimeContractMetadata(contract)'), 'toolListAgents() must expose resolved contract metadata in runtime-facing agent data.', problems);
54
+ assert(buildGuideBlock.includes('buildGuideContractAdvisory(myContract)'), 'buildGuide() must derive contract-aware guide advisory data.', problems);
55
+ assert(buildGuideBlock.includes('contract_advisory'), 'buildGuide() must expose contract_advisory output.', problems);
56
+ assert(updateProfileBlock.includes('sanitizeContractProfilePatch({'), 'toolUpdateProfile() must validate advisory contract metadata through the shared sanitizer.', problems);
57
+ assert(suggestTaskBlock.includes('attachContractAdvisory('), 'toolSuggestTask() must attach contract advisory output instead of only raw task suggestions.', problems);
58
+ assert(getWorkBlock.includes('attachContractAdvisory('), 'toolGetWork() must attach contract advisory output to returned work items.', problems);
59
+
60
+ assert(yieldFloorBlock.includes('Only the manager can yield the floor.'), 'Managed-mode manager-only enforcement must remain in toolYieldFloor().', problems);
61
+ assert(setPhaseBlock.includes('Only the manager can set the phase.'), 'Managed-mode manager-only enforcement must remain in toolSetPhase().', problems);
62
+ assert(!getWorkBlock.includes('You must be the manager to yield the floor.'), 'toolGetWork() must not absorb managed-mode manager-only hard gates.', problems);
63
+
64
+ assert(apiAgentsBlock.includes('resolveAgentContract(profile)'), '/api/agents must resolve contract metadata from stored profiles.', problems);
65
+ assert(apiAgentsBlock.includes('buildRuntimeContractMetadata(contract)'), '/api/agents must expose contract metadata on dashboard agent payloads.', problems);
66
+ assert(profilesPostBlock.includes('sanitizeContractProfilePatch({'), '/api/profiles POST must validate advisory contract metadata via the shared sanitizer.', problems);
67
+
68
+ const validPatch = sanitizeContractProfilePatch({
69
+ archetype: 'Reviewer',
70
+ skills: ['Testing', 'review', 'testing'],
71
+ contract_mode: 'STRICT',
72
+ });
73
+ assert(validPatch.valid, 'sanitizeContractProfilePatch() should accept valid advisory contract metadata.', problems);
74
+ assert(sameJson(validPatch.normalized, { archetype: 'reviewer', skills: ['testing', 'review'], contract_mode: 'strict' }), 'sanitizeContractProfilePatch() should normalize archetype, skills, and contract_mode deterministically.', problems);
75
+
76
+ const invalidPatch = sanitizeContractProfilePatch({ contract_mode: 'enforced-now' });
77
+ assert(!invalidPatch.valid, 'sanitizeContractProfilePatch() should reject unsupported contract modes.', problems);
78
+
79
+ const legacyContract = resolveAgentContract({ role: 'Quality Lead' });
80
+ assert(legacyContract.role_token === 'quality', 'resolveAgentContract() should preserve legacy role compatibility by normalizing Quality Lead to quality.', problems);
81
+ assert(legacyContract.archetype === 'reviewer', 'resolveAgentContract() should infer the reviewer archetype from the legacy quality role.', problems);
82
+ assert(legacyContract.has_explicit_contract === false, 'resolveAgentContract() should keep legacy role-only profiles compatible without pretending they are explicit contracts.', problems);
83
+
84
+ const explicitContract = resolveAgentContract({
85
+ role: 'Backend',
86
+ archetype: 'implementer',
87
+ skills: ['API', 'backend'],
88
+ contract_mode: 'strict',
89
+ });
90
+ assert(explicitContract.declared_archetype === 'implementer', 'resolveAgentContract() should preserve the explicitly declared archetype.', problems);
91
+ assert(explicitContract.role_alignment === 'aligned', 'resolveAgentContract() should mark compatible explicit role/archetype combinations as aligned.', problems);
92
+ assert(explicitContract.effective_skills.includes('api') && explicitContract.effective_skills.includes('backend'), 'resolveAgentContract() should carry normalized explicit skills into effective_skills.', problems);
93
+
94
+ const mismatchGuide = buildGuideContractAdvisory(resolveAgentContract({ role: 'advisor', archetype: 'implementer' }));
95
+ assert(mismatchGuide && mismatchGuide.status === 'mismatch', 'buildGuideContractAdvisory() should flag explicit role/archetype mismatches without hard failing.', problems);
96
+
97
+ const reviewFit = analyzeContractFit(resolveAgentContract({ role: 'quality', archetype: 'reviewer', skills: ['testing'] }), {
98
+ work_type: 'review',
99
+ title: 'Review API regression fix',
100
+ description: 'Verify tests and review the patch',
101
+ });
102
+ assert(reviewFit && reviewFit.status === 'aligned', 'analyzeContractFit() should mark reviewer-aligned review work as aligned.', problems);
103
+
104
+ const assignedMismatch = analyzeContractFit(resolveAgentContract({ archetype: 'advisor', contract_mode: 'strict' }), {
105
+ work_type: 'claimed_task',
106
+ title: 'Implement dashboard drag and drop',
107
+ description: 'Write code for the new dashboard interaction',
108
+ assigned: true,
109
+ });
110
+ assert(assignedMismatch && assignedMismatch.status === 'mismatch', 'analyzeContractFit() should detect weaker-fit assigned implementation work for advisor archetypes.', problems);
111
+ assert(assignedMismatch && assignedMismatch.summary.includes('assigned work still takes precedence'), 'Assigned mismatch guidance must remain advisory by stating that assigned work still takes precedence.', problems);
112
+ assert(assignedMismatch && assignedMismatch.migration_note && assignedMismatch.migration_note.includes('advisory'), 'Strict contract mode must still surface only advisory guidance in Task 11B.', problems);
113
+
114
+ const runtimeMetadata = buildRuntimeContractMetadata(explicitContract);
115
+ assert(runtimeMetadata.archetype === 'implementer', 'buildRuntimeContractMetadata() should expose the explicit archetype in runtime-facing metadata.', problems);
116
+ assert(sameJson(runtimeMetadata.skills, ['api', 'backend']), 'buildRuntimeContractMetadata() should expose explicit skills in runtime-facing metadata.', problems);
117
+ assert(runtimeMetadata.contract && runtimeMetadata.contract.role_alignment === 'aligned', 'buildRuntimeContractMetadata() should expose resolved role alignment details for advisory consumers.', problems);
118
+
119
+ if (problems.length > 0) {
120
+ fail(['Agent contract advisory validation failed.', ...problems.map((problem) => `- ${problem}`)]);
121
+ }
122
+
123
+ console.log([
124
+ 'Agent contract advisory validation passed.',
125
+ '- Shared advisory contract metadata normalizes archetype, skills, and contract_mode deterministically.',
126
+ '- Legacy free-form role strings remain compatible through read-time resolution instead of persisted rewrites.',
127
+ '- get_guide, suggest_task, and get_work are wired to surface advisory contract guidance without broad hard blocking.',
128
+ '- Dashboard/runtime-facing agent metadata exposes the resolved contract shape while managed-mode hard gates stay separate.',
129
+ ].join('\n'));
130
+ }
131
+
132
+ main();