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,992 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
7
+ const AGENT_BRIDGE_ROOT = path.resolve(__dirname, '..');
8
+ const USAGE = 'Usage: node agent-bridge/scripts/check-invariants.js --suite authority [--simulate-legacy-write]';
9
+
10
+ const FILES = {
11
+ io: {
12
+ path: path.join(AGENT_BRIDGE_ROOT, 'state', 'io.js'),
13
+ display: 'agent-bridge/state/io.js',
14
+ },
15
+ messages: {
16
+ path: path.join(AGENT_BRIDGE_ROOT, 'state', 'messages.js'),
17
+ display: 'agent-bridge/state/messages.js',
18
+ },
19
+ agents: {
20
+ path: path.join(AGENT_BRIDGE_ROOT, 'state', 'agents.js'),
21
+ display: 'agent-bridge/state/agents.js',
22
+ },
23
+ tasksWorkflows: {
24
+ path: path.join(AGENT_BRIDGE_ROOT, 'state', 'tasks-workflows.js'),
25
+ display: 'agent-bridge/state/tasks-workflows.js',
26
+ },
27
+ server: {
28
+ path: path.join(AGENT_BRIDGE_ROOT, 'server.js'),
29
+ display: 'agent-bridge/server.js',
30
+ },
31
+ canonical: {
32
+ path: path.join(AGENT_BRIDGE_ROOT, 'state', 'canonical.js'),
33
+ display: 'agent-bridge/state/canonical.js',
34
+ },
35
+ dashboard: {
36
+ path: path.join(AGENT_BRIDGE_ROOT, 'dashboard.js'),
37
+ display: 'agent-bridge/dashboard.js',
38
+ },
39
+ cli: {
40
+ path: path.join(AGENT_BRIDGE_ROOT, 'cli.js'),
41
+ display: 'agent-bridge/cli.js',
42
+ },
43
+ apiAgents: {
44
+ path: path.join(AGENT_BRIDGE_ROOT, 'api-agents.js'),
45
+ display: 'agent-bridge/api-agents.js',
46
+ },
47
+ runtimeContract: {
48
+ path: path.join(REPO_ROOT, 'docs', 'architecture', 'runtime-contract.md'),
49
+ display: 'docs/architecture/runtime-contract.md',
50
+ },
51
+ };
52
+
53
+ function fail(message, exitCode) {
54
+ console.error(message);
55
+ process.exit(exitCode);
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ let suite = null;
60
+ let simulateLegacyWrite = false;
61
+
62
+ for (let index = 0; index < argv.length; index++) {
63
+ const arg = argv[index];
64
+
65
+ if (arg === '--suite') {
66
+ if (suite || index + 1 >= argv.length) fail(USAGE, 2);
67
+ suite = argv[index + 1];
68
+ index += 1;
69
+ continue;
70
+ }
71
+
72
+ if (arg === '--simulate-legacy-write') {
73
+ simulateLegacyWrite = true;
74
+ continue;
75
+ }
76
+
77
+ fail(USAGE, 2);
78
+ }
79
+
80
+ if (suite !== 'authority') {
81
+ if (!suite) fail(USAGE, 2);
82
+ fail([`Unknown suite: ${suite}`, 'Supported suites: authority', USAGE].join('\n'), 2);
83
+ }
84
+
85
+ return { suite, simulateLegacyWrite };
86
+ }
87
+
88
+ function readFile(spec) {
89
+ if (!fs.existsSync(spec.path)) {
90
+ fail(`Authority invariant check failed.\nMissing file: ${spec.display}`, 1);
91
+ }
92
+
93
+ return fs.readFileSync(spec.path, 'utf8');
94
+ }
95
+
96
+ function extractBlock(source, fileDisplayPath, label, startAnchor, endAnchor) {
97
+ const startIndex = source.indexOf(startAnchor);
98
+ if (startIndex === -1) {
99
+ return {
100
+ text: '',
101
+ problems: [`${fileDisplayPath} is missing the expected start anchor for ${label}: ${startAnchor}`],
102
+ };
103
+ }
104
+
105
+ let endIndex = source.length;
106
+ if (endAnchor) {
107
+ endIndex = source.indexOf(endAnchor, startIndex + startAnchor.length);
108
+ if (endIndex === -1) {
109
+ return {
110
+ text: '',
111
+ problems: [`${fileDisplayPath} is missing the expected end anchor for ${label}: ${endAnchor}`],
112
+ };
113
+ }
114
+ }
115
+
116
+ return {
117
+ text: source.slice(startIndex, endIndex),
118
+ problems: [],
119
+ };
120
+ }
121
+
122
+ function normalizeSnippet(snippet) {
123
+ return snippet.replace(/\s+/g, ' ').trim().slice(0, 180);
124
+ }
125
+
126
+ function requireSnippet(problems, source, message, snippet) {
127
+ if (!source.includes(snippet)) {
128
+ problems.push(`${message} Missing snippet: ${snippet}`);
129
+ }
130
+ }
131
+
132
+ function forbidPattern(problems, source, message, pattern) {
133
+ const match = source.match(pattern);
134
+ if (match) {
135
+ problems.push(`${message} Matched: ${normalizeSnippet(match[0])}`);
136
+ }
137
+ }
138
+
139
+ function addBlockProblems(problems, block) {
140
+ if (block && Array.isArray(block.problems) && block.problems.length > 0) {
141
+ problems.push(...block.problems);
142
+ }
143
+ }
144
+
145
+ function buildContext(simulateLegacyWrite) {
146
+ const sources = {
147
+ io: readFile(FILES.io),
148
+ messages: readFile(FILES.messages),
149
+ agents: readFile(FILES.agents),
150
+ tasksWorkflows: readFile(FILES.tasksWorkflows),
151
+ server: readFile(FILES.server),
152
+ canonical: readFile(FILES.canonical),
153
+ dashboard: readFile(FILES.dashboard),
154
+ cli: readFile(FILES.cli),
155
+ apiAgents: readFile(FILES.apiAgents),
156
+ runtimeContract: readFile(FILES.runtimeContract),
157
+ };
158
+
159
+ const blocks = {
160
+ cliMsg: extractBlock(
161
+ sources.cli,
162
+ FILES.cli.display,
163
+ 'cli message send path',
164
+ 'function cliMsg() {',
165
+ 'function cliStatus() {'
166
+ ),
167
+ dashboardInject: extractBlock(
168
+ sources.dashboard,
169
+ FILES.dashboard.display,
170
+ 'dashboard inject path',
171
+ 'function apiInjectMessage(body, query) {',
172
+ '// Multi-project management'
173
+ ),
174
+ dashboardAgents: extractBlock(
175
+ sources.dashboard,
176
+ FILES.dashboard.display,
177
+ 'dashboard agents query path',
178
+ 'function apiAgents(query) {',
179
+ 'function apiStatus(query) {'
180
+ ),
181
+ dashboardTasksGet: extractBlock(
182
+ sources.dashboard,
183
+ FILES.dashboard.display,
184
+ 'dashboard tasks query path',
185
+ 'function apiTasks(query) {',
186
+ 'function apiSearch(query) {'
187
+ ),
188
+ dashboardDecisionsRoute: extractBlock(
189
+ sources.dashboard,
190
+ FILES.dashboard.display,
191
+ 'dashboard decisions route',
192
+ "else if (url.pathname === '/api/decisions' && req.method === 'GET') {",
193
+ "else if (url.pathname === '/api/agents' && req.method === 'DELETE') {"
194
+ ),
195
+ dashboardUpdateTask: extractBlock(
196
+ sources.dashboard,
197
+ FILES.dashboard.display,
198
+ 'dashboard task mutation path',
199
+ 'function apiUpdateTask(body, query) {',
200
+ '// Rules API'
201
+ ),
202
+ dashboardRulesApi: extractBlock(
203
+ sources.dashboard,
204
+ FILES.dashboard.display,
205
+ 'dashboard rules api helpers',
206
+ 'function apiRules(query) {',
207
+ '// Auto-discover .agent-bridge directories nearby'
208
+ ),
209
+ dashboardEditMessage: extractBlock(
210
+ sources.dashboard,
211
+ FILES.dashboard.display,
212
+ 'dashboard message edit path',
213
+ 'async function apiEditMessage(body, query) {',
214
+ '// --- v3.4: Message Delete ---'
215
+ ),
216
+ dashboardDeleteMessage: extractBlock(
217
+ sources.dashboard,
218
+ FILES.dashboard.display,
219
+ 'dashboard message delete path',
220
+ 'async function apiDeleteMessage(body, query) {',
221
+ '// --- v3.4: Conversation Templates ---'
222
+ ),
223
+ dashboardWorkflowsPost: extractBlock(
224
+ sources.dashboard,
225
+ FILES.dashboard.display,
226
+ 'dashboard workflow mutation route',
227
+ "else if (url.pathname === '/api/workflows' && req.method === 'POST') {",
228
+ '// ========== Plan Control API (v5.0 Autonomy Engine) =========='
229
+ ),
230
+ dashboardPlanPause: extractBlock(
231
+ sources.dashboard,
232
+ FILES.dashboard.display,
233
+ 'dashboard plan pause route',
234
+ "else if (url.pathname === '/api/plan/pause' && req.method === 'POST') {",
235
+ "else if (url.pathname === '/api/plan/resume' && req.method === 'POST') {"
236
+ ),
237
+ dashboardPlanResume: extractBlock(
238
+ sources.dashboard,
239
+ FILES.dashboard.display,
240
+ 'dashboard plan resume route',
241
+ "else if (url.pathname === '/api/plan/resume' && req.method === 'POST') {",
242
+ "else if (url.pathname === '/api/plan/stop' && req.method === 'POST') {"
243
+ ),
244
+ dashboardPlanStop: extractBlock(
245
+ sources.dashboard,
246
+ FILES.dashboard.display,
247
+ 'dashboard plan stop route',
248
+ "else if (url.pathname === '/api/plan/stop' && req.method === 'POST') {",
249
+ "else if (url.pathname.startsWith('/api/plan/skip/') && req.method === 'POST') {"
250
+ ),
251
+ dashboardPlanSkip: extractBlock(
252
+ sources.dashboard,
253
+ FILES.dashboard.display,
254
+ 'dashboard plan step skip route',
255
+ "else if (url.pathname.startsWith('/api/plan/skip/') && req.method === 'POST') {",
256
+ "else if (url.pathname.startsWith('/api/plan/reassign/') && req.method === 'POST') {"
257
+ ),
258
+ dashboardPlanReassign: extractBlock(
259
+ sources.dashboard,
260
+ FILES.dashboard.display,
261
+ 'dashboard plan step reassign route',
262
+ "else if (url.pathname.startsWith('/api/plan/reassign/') && req.method === 'POST') {",
263
+ "else if (url.pathname === '/api/plan/inject' && req.method === 'POST') {"
264
+ ),
265
+ dashboardRulesRoutes: extractBlock(
266
+ sources.dashboard,
267
+ FILES.dashboard.display,
268
+ 'dashboard rules runtime routes',
269
+ '// ========== Rules API ==========',
270
+ '// ========== End Rules API =========='
271
+ ),
272
+ dashboardExportJson: extractBlock(
273
+ sources.dashboard,
274
+ FILES.dashboard.display,
275
+ 'dashboard export-json route',
276
+ "else if (url.pathname === '/api/export-json' && req.method === 'GET') {",
277
+ "else if (url.pathname === '/api/export' && req.method === 'GET') {"
278
+ ),
279
+ dashboardPlanSkillsRoute: extractBlock(
280
+ sources.dashboard,
281
+ FILES.dashboard.display,
282
+ 'dashboard plan skills route',
283
+ "else if (url.pathname === '/api/plan/skills' && req.method === 'GET') {",
284
+ "else if (url.pathname === '/api/plan/retries' && req.method === 'GET') {"
285
+ ),
286
+ dashboardProfilesGet: extractBlock(
287
+ sources.dashboard,
288
+ FILES.dashboard.display,
289
+ 'dashboard profiles GET route',
290
+ "else if (url.pathname === '/api/profiles' && req.method === 'GET') {",
291
+ "else if (url.pathname === '/api/profiles' && req.method === 'POST') {"
292
+ ),
293
+ dashboardProfilesPost: extractBlock(
294
+ sources.dashboard,
295
+ FILES.dashboard.display,
296
+ 'dashboard profiles POST route',
297
+ "else if (url.pathname === '/api/profiles' && req.method === 'POST') {",
298
+ "else if (url.pathname === '/api/workspaces' && req.method === 'GET') {"
299
+ ),
300
+ dashboardWorkflowsGet: extractBlock(
301
+ sources.dashboard,
302
+ FILES.dashboard.display,
303
+ 'dashboard workflows GET route',
304
+ "else if (url.pathname === '/api/workflows' && req.method === 'GET') {",
305
+ "else if (url.pathname === '/api/workflows' && req.method === 'POST') {"
306
+ ),
307
+ apiAgentsConstructor: extractBlock(
308
+ sources.apiAgents,
309
+ FILES.apiAgents.display,
310
+ 'api-agent canonical state setup',
311
+ ' constructor(dataDir) {',
312
+ ' _loadConfigs() {'
313
+ ),
314
+ apiRegister: extractBlock(
315
+ sources.apiAgents,
316
+ FILES.apiAgents.display,
317
+ 'api-agent register path',
318
+ ' _registerInAgentsJson(name, provider) {',
319
+ ' // Remove API agent from agents.json'
320
+ ),
321
+ apiUnregister: extractBlock(
322
+ sources.apiAgents,
323
+ FILES.apiAgents.display,
324
+ 'api-agent unregister path',
325
+ ' _unregisterFromAgentsJson(name) {',
326
+ ' // Delete an API agent'
327
+ ),
328
+ apiSendMessage: extractBlock(
329
+ sources.apiAgents,
330
+ FILES.apiAgents.display,
331
+ 'api-agent send message path',
332
+ ' _sendMessage(from, to, content, replyTo) {',
333
+ ' // Update heartbeat in agents.json'
334
+ ),
335
+ apiHeartbeat: extractBlock(
336
+ sources.apiAgents,
337
+ FILES.apiAgents.display,
338
+ 'api-agent heartbeat path',
339
+ ' _updateHeartbeat(name) {',
340
+ ' _updateAgentStatus(name, status) {'
341
+ ),
342
+ apiStatusUpdate: extractBlock(
343
+ sources.apiAgents,
344
+ FILES.apiAgents.display,
345
+ 'api-agent status path',
346
+ ' _updateAgentStatus(name, status) {',
347
+ ' _getMessageCount() {'
348
+ ),
349
+ workspaceSaveHelper: extractBlock(
350
+ sources.server,
351
+ 'agent-bridge/server.js',
352
+ 'server workspace save helper',
353
+ 'function saveWorkspace(agentName, data, options = {}) {',
354
+ '// --- Workflow helpers ---'
355
+ ),
356
+ toolLogDecision: extractBlock(
357
+ sources.server,
358
+ 'agent-bridge/server.js',
359
+ 'decision logging tool',
360
+ 'function toolLogDecision(decision, reasoning, topic) {',
361
+ 'function toolGetDecisions(topic) {'
362
+ ),
363
+ toolKBWrite: extractBlock(
364
+ sources.server,
365
+ 'agent-bridge/server.js',
366
+ 'knowledge base write tool',
367
+ 'function toolKBWrite(key, content) {',
368
+ 'function toolKBRead(key) {'
369
+ ),
370
+ toolUpdateProgress: extractBlock(
371
+ sources.server,
372
+ 'agent-bridge/server.js',
373
+ 'progress update tool',
374
+ 'function toolUpdateProgress(feature, percent, notes) {',
375
+ 'function toolGetProgress() {'
376
+ ),
377
+ toolCallVote: extractBlock(
378
+ sources.server,
379
+ 'agent-bridge/server.js',
380
+ 'vote creation tool',
381
+ 'function toolCallVote(question, options) {',
382
+ 'function toolCastVote(voteId, choice) {'
383
+ ),
384
+ toolCastVote: extractBlock(
385
+ sources.server,
386
+ 'agent-bridge/server.js',
387
+ 'vote cast tool',
388
+ 'function toolCastVote(voteId, choice) {',
389
+ 'function toolVoteStatus(voteId) {'
390
+ ),
391
+ toolAddRule: extractBlock(
392
+ sources.server,
393
+ 'agent-bridge/server.js',
394
+ 'rule add tool',
395
+ 'function toolAddRule(text, category = \'custom\') {',
396
+ 'function toolListRules() {'
397
+ ),
398
+ toolRemoveRule: extractBlock(
399
+ sources.server,
400
+ 'agent-bridge/server.js',
401
+ 'rule remove tool',
402
+ 'function toolRemoveRule(ruleId) {',
403
+ 'function toolToggleRule(ruleId) {'
404
+ ),
405
+ toolToggleRule: extractBlock(
406
+ sources.server,
407
+ 'agent-bridge/server.js',
408
+ 'rule toggle tool',
409
+ 'function toolToggleRule(ruleId) {',
410
+ '// --- MCP Server setup ---'
411
+ ),
412
+ toolRequestReview: extractBlock(
413
+ sources.server,
414
+ 'agent-bridge/server.js',
415
+ 'review request tool',
416
+ 'function toolRequestReview(filePath, description) {',
417
+ 'function toolSubmitReview(reviewId, status, feedback) {'
418
+ ),
419
+ toolSubmitReview: extractBlock(
420
+ sources.server,
421
+ 'agent-bridge/server.js',
422
+ 'review submit tool',
423
+ 'function toolSubmitReview(reviewId, status, feedback) {',
424
+ 'function toolDeclareDependency(taskId, dependsOnTaskId) {'
425
+ ),
426
+ toolDeclareDependency: extractBlock(
427
+ sources.server,
428
+ 'agent-bridge/server.js',
429
+ 'dependency declare tool',
430
+ 'function toolDeclareDependency(taskId, dependsOnTaskId) {',
431
+ 'function toolCheckDependencies(taskId) {'
432
+ ),
433
+ };
434
+
435
+ if (simulateLegacyWrite) {
436
+ blocks.cliMsg.text += [
437
+ '',
438
+ "fs.appendFileSync(path.join(resolveDataDirCli(), 'messages.jsonl'), JSON.stringify(msg) + '\\n');",
439
+ "fs.appendFileSync(path.join(resolveDataDirCli(), 'history.jsonl'), JSON.stringify(msg) + '\\n');",
440
+ ].join('\n');
441
+
442
+ blocks.dashboardInject.text += [
443
+ '',
444
+ "fs.appendFileSync(path.join(resolveDataDir(projectPath), 'messages.jsonl'), JSON.stringify(msg) + '\\n');",
445
+ "fs.appendFileSync(path.join(resolveDataDir(projectPath), 'history.jsonl'), JSON.stringify(msg) + '\\n');",
446
+ ].join('\n');
447
+
448
+ blocks.dashboardAgents.text += [
449
+ '',
450
+ "const agents = readJson(filePath('agents.json', projectPath));",
451
+ "const profiles = readJson(filePath('profiles.json', projectPath));",
452
+ ].join('\n');
453
+
454
+ blocks.dashboardTasksGet.text += [
455
+ '',
456
+ "const tasks = readJson(filePath('tasks.json', projectPath));",
457
+ ].join('\n');
458
+
459
+ blocks.dashboardDeleteMessage.text += [
460
+ '',
461
+ "fs.writeFileSync(path.join(resolveDataDir(projectPath), 'messages.jsonl'), rewrittenMessages.join('\\n') + '\\n');",
462
+ "fs.writeFileSync(path.join(resolveDataDir(projectPath), 'history.jsonl'), rewrittenHistory.join('\\n') + '\\n');",
463
+ ].join('\n');
464
+
465
+ blocks.dashboardUpdateTask.text += [
466
+ '',
467
+ "fs.writeFileSync(filePath('tasks.json', projectPath), JSON.stringify(tasks, null, 2));",
468
+ ].join('\n');
469
+
470
+ blocks.dashboardPlanPause.text += [
471
+ '',
472
+ "fs.writeFileSync(filePath('workflows.json', projectPath), JSON.stringify(workflows, null, 2));",
473
+ ].join('\n');
474
+
475
+ blocks.dashboardExportJson.text += [
476
+ '',
477
+ "const tasksRaw = readJson(filePath('tasks.json', projectPath));",
478
+ ].join('\n');
479
+
480
+ blocks.dashboardProfilesGet.text += [
481
+ '',
482
+ "res.end(JSON.stringify(readJson(filePath('profiles.json', projectPath))));",
483
+ ].join('\n');
484
+
485
+ blocks.dashboardProfilesPost.text += [
486
+ '',
487
+ "const profilesFile = filePath('profiles.json', projectPath);",
488
+ "const profiles = readJson(profilesFile);",
489
+ "fs.writeFileSync(profilesFile, JSON.stringify(profiles, null, 2));",
490
+ ].join('\n');
491
+
492
+ blocks.dashboardWorkflowsGet.text += [
493
+ '',
494
+ "const workflowData = readJson(filePath('workflows.json', projectPath), []);",
495
+ ].join('\n');
496
+
497
+ blocks.dashboardPlanSkip.text += [
498
+ '',
499
+ "const workflowData = readJson(filePath('workflows.json', projectPath), []);",
500
+ ].join('\n');
501
+
502
+ blocks.dashboardPlanReassign.text += [
503
+ '',
504
+ "const workflowData = readJson(filePath('workflows.json', projectPath), []);",
505
+ ].join('\n');
506
+
507
+ blocks.apiRegister.text += [
508
+ '',
509
+ "fs.writeFileSync(path.join(this.dataDir, 'agents.json'), JSON.stringify({ [name]: this.agents[name] }, null, 2));",
510
+ "fs.writeFileSync(path.join(this.dataDir, 'profiles.json'), JSON.stringify({ [name]: { display_name: name } }, null, 2));",
511
+ ].join('\n');
512
+
513
+ blocks.apiHeartbeat.text += [
514
+ '',
515
+ "fs.writeFileSync(path.join(this.dataDir, 'heartbeat-' + name + '.json'), JSON.stringify({ last_activity: new Date().toISOString(), pid: process.pid }));",
516
+ ].join('\n');
517
+
518
+ blocks.dashboardRulesApi.text += [
519
+ '',
520
+ "fs.writeFileSync(rulesFile, JSON.stringify(rules, null, 2));",
521
+ ].join('\n');
522
+
523
+ blocks.dashboardRulesRoutes.text += [
524
+ '',
525
+ "fs.writeFileSync(rulesFile, JSON.stringify(rules));",
526
+ ].join('\n');
527
+
528
+ blocks.workspaceSaveHelper.text += [
529
+ '',
530
+ "fs.writeFileSync(path.join(WORKSPACES_DIR, `${sanitizeName(agentName)}.json`), JSON.stringify(data));",
531
+ ].join('\n');
532
+
533
+ blocks.toolLogDecision.text += [
534
+ '',
535
+ 'writeJsonFile(DECISIONS_FILE, decisions);',
536
+ ].join('\n');
537
+
538
+ blocks.toolKBWrite.text += [
539
+ '',
540
+ 'writeJsonFile(KB_FILE, kb);',
541
+ ].join('\n');
542
+
543
+ blocks.toolUpdateProgress.text += [
544
+ '',
545
+ 'writeJsonFile(PROGRESS_FILE, progress);',
546
+ ].join('\n');
547
+
548
+ blocks.toolCallVote.text += [
549
+ '',
550
+ 'writeJsonFile(VOTES_FILE, votes);',
551
+ ].join('\n');
552
+
553
+ blocks.toolCastVote.text += [
554
+ '',
555
+ 'writeJsonFile(VOTES_FILE, votes);',
556
+ ].join('\n');
557
+
558
+ blocks.toolAddRule.text += [
559
+ '',
560
+ 'writeJsonFile(RULES_FILE, rules);',
561
+ ].join('\n');
562
+
563
+ blocks.toolRemoveRule.text += [
564
+ '',
565
+ 'writeJsonFile(RULES_FILE, rules);',
566
+ ].join('\n');
567
+
568
+ blocks.toolToggleRule.text += [
569
+ '',
570
+ 'writeJsonFile(RULES_FILE, rules);',
571
+ ].join('\n');
572
+ }
573
+
574
+ return { sources, blocks, simulateLegacyWrite };
575
+ }
576
+
577
+ function getAuthorityChecks() {
578
+ const directMessageWrite = /fs\.(?:appendFileSync|writeFileSync)\(\s*(?:path\.join\([^)]*['"]messages\.jsonl['"]|messagesFile\b)/;
579
+ const directHistoryWrite = /fs\.(?:appendFileSync|writeFileSync)\(\s*(?:path\.join\([^)]*['"]history\.jsonl['"]|historyFile\b)/;
580
+ const directAgentsRead = /readJson\(\s*(?:filePath\(['"]agents\.json['"]|path\.join\([^)]*['"]agents\.json['"]|agentsFile\b)|JSON\.parse\(fs\.readFileSync\(\s*(?:filePath\(['"]agents\.json['"]|path\.join\([^)]*['"]agents\.json['"]|agentsFile\b)/;
581
+ const directProfilesRead = /readJson\(\s*(?:filePath\(['"]profiles\.json['"]|path\.join\([^)]*['"]profiles\.json['"]|profilesFile\b)|JSON\.parse\(fs\.readFileSync\(\s*(?:filePath\(['"]profiles\.json['"]|path\.join\([^)]*['"]profiles\.json['"]|profilesFile\b)/;
582
+ const directTasksRead = /readJson\(\s*(?:filePath\(['"]tasks\.json['"]|path\.join\([^)]*['"]tasks\.json['"]|tasksFile\b)|JSON\.parse\(fs\.readFileSync\(\s*(?:filePath\(['"]tasks\.json['"]|path\.join\([^)]*['"]tasks\.json['"]|tasksFile\b)/;
583
+ const directWorkflowsRead = /readJson\(\s*(?:filePath\(['"]workflows\.json['"]|path\.join\([^)]*['"]workflows\.json['"]|workflowsFile\b|wfFile\b)|JSON\.parse\(fs\.readFileSync\(\s*(?:filePath\(['"]workflows\.json['"]|path\.join\([^)]*['"]workflows\.json['"]|workflowsFile\b|wfFile\b)/;
584
+ const directAgentsWrite = /fs\.writeFileSync\(\s*(?:path\.join\([^)]*['"]agents\.json['"]|agentsFile\b)/;
585
+ const directProfilesWrite = /fs\.writeFileSync\(\s*(?:path\.join\([^)]*['"]profiles\.json['"]|profilesFile\b)/;
586
+ const directHeartbeatWrite = /fs\.writeFileSync\(\s*(?:path\.join\([^)]*heartbeat-|heartbeatFile\b)/;
587
+ const directTasksWrite = /fs\.writeFileSync\(\s*(?:filePath\(['"]tasks\.json['"]|tasksFile\b)/;
588
+ const directWorkflowsWrite = /fs\.writeFileSync\(\s*(?:filePath\(['"]workflows\.json['"]|workflowsFile\b)/;
589
+ const directRulesWrite = /fs\.writeFileSync\(\s*(?:rulesFile\b|RULES_FILE\b|filePath\(['"]rules\.json['"])/;
590
+ const directWorkspaceWrite = /fs\.writeFileSync\(\s*path\.join\(WORKSPACES_DIR/;
591
+ const directGovernanceWrite = /writeJsonFile\((?:DECISIONS_FILE|KB_FILE|PROGRESS_FILE|VOTES_FILE|RULES_FILE|REVIEWS_FILE|DEPS_FILE)\b/;
592
+ const directDecisionsRead = /readJson\(\s*filePath\(['"]decisions\.json['"]|JSON\.parse\(fs\.readFileSync\(\s*filePath\(['"]decisions\.json['"]/;
593
+ const directKnowledgeRead = /readJson\(\s*filePath\(['"]kb\.json['"]|JSON\.parse\(fs\.readFileSync\(\s*filePath\(['"]kb\.json['"]/;
594
+
595
+ return [
596
+ {
597
+ key: 'broker_surface',
598
+ success: 'Broker-owned mutators still exist in the extracted state layer and canonical composition helper.',
599
+ run(context) {
600
+ const problems = [];
601
+ requireSnippet(problems, context.sources.io, `${FILES.io.display} must expose appendJsonl().`, 'function appendJsonl(filePath, value) {');
602
+ requireSnippet(problems, context.sources.io, `${FILES.io.display} must expose writeJson().`, 'function writeJson(filePath, data, options = {}) {');
603
+ requireSnippet(problems, context.sources.io, `${FILES.io.display} must expose withLock().`, 'function withLock(filePath, fn) {');
604
+
605
+ requireSnippet(problems, context.sources.messages, `${FILES.messages.display} must expose appendConversationMessage().`, 'function appendConversationMessage(message, targets) {');
606
+ requireSnippet(problems, context.sources.messages, `${FILES.messages.display} must append to message projections via io.appendJsonl().`, 'io.appendJsonl(targets.messageFile, message);');
607
+ requireSnippet(problems, context.sources.messages, `${FILES.messages.display} must append to history projections via io.appendJsonl().`, 'io.appendJsonl(targets.historyFile, message);');
608
+ requireSnippet(problems, context.sources.messages, `${FILES.messages.display} must lock history rewrites through io.withLock().`, 'io.withLock(targets.historyFile, () => {');
609
+ requireSnippet(problems, context.sources.messages, `${FILES.messages.display} must lock message rewrites through io.withLock().`, 'io.withLock(targets.messageFile, () => {');
610
+
611
+ requireSnippet(problems, context.sources.agents, `${FILES.agents.display} must persist agents through io.writeJson().`, "io.writeJson(agentsFile, agents, { cacheKey: 'agents' });");
612
+ requireSnippet(problems, context.sources.agents, `${FILES.agents.display} must persist profiles through io.writeJson().`, "io.writeJson(profilesFile, profiles, { cacheKey: 'profiles', space: 2 });");
613
+ requireSnippet(problems, context.sources.agents, `${FILES.agents.display} must persist heartbeat overlays through io.writeJson().`, 'io.writeJson(heartbeatFile(name), {');
614
+ requireSnippet(problems, context.sources.agents, `${FILES.agents.display} must guard agent mutations behind withAgentsLock().`, 'function withAgentsLock(fn) {');
615
+
616
+ requireSnippet(problems, context.sources.tasksWorkflows, `${FILES.tasksWorkflows.display} must expose mutateTasks().`, 'function mutateTasks(mutator, writeOptions = {}) {');
617
+ requireSnippet(problems, context.sources.tasksWorkflows, `${FILES.tasksWorkflows.display} must expose mutateWorkflows().`, 'function mutateWorkflows(mutator, writeOptions = {}) {');
618
+ requireSnippet(problems, context.sources.tasksWorkflows, `${FILES.tasksWorkflows.display} must resolve branch-aware task files.`, 'function resolveTasksFile(branchName = \'main\') {');
619
+ requireSnippet(problems, context.sources.tasksWorkflows, `${FILES.tasksWorkflows.display} must resolve branch-aware workflow files.`, 'function resolveWorkflowsFile(branchName = \'main\') {');
620
+ requireSnippet(problems, context.sources.tasksWorkflows, `${FILES.tasksWorkflows.display} must persist tasks through io.writeJson().`, 'io.writeJson(filePath, tasks, {');
621
+ requireSnippet(problems, context.sources.tasksWorkflows, `${FILES.tasksWorkflows.display} must persist workflows through io.writeJson().`, 'io.writeJson(filePath, workflows, {');
622
+
623
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must compose createMessagesState().`, "const { createMessagesState } = require('./messages');");
624
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must compose createAgentsState().`, "const { createAgentsState } = require('./agents');");
625
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must compose createTasksWorkflowsState().`, "const { createTasksWorkflowsState } = require('./tasks-workflows');");
626
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must build the message mutator.`, 'const messagesState = createMessagesState({ io });');
627
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must build the agent mutator.`, 'const agentsState = createAgentsState({');
628
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must build the task/workflow mutator.`, 'const tasksWorkflowsState = createTasksWorkflowsState({');
629
+
630
+ return problems;
631
+ },
632
+ },
633
+ {
634
+ key: 'message_history_authority',
635
+ success: 'Dashboard, CLI, and API-agent message/history mutations still route through the canonical broker path.',
636
+ run(context) {
637
+ const problems = [];
638
+ addBlockProblems(problems, context.blocks.cliMsg);
639
+ addBlockProblems(problems, context.blocks.dashboardInject);
640
+ addBlockProblems(problems, context.blocks.dashboardEditMessage);
641
+ addBlockProblems(problems, context.blocks.dashboardDeleteMessage);
642
+ addBlockProblems(problems, context.blocks.apiSendMessage);
643
+
644
+ requireSnippet(problems, context.sources.dashboard, `${FILES.dashboard.display} must import the canonical helper for message authority.`, "const { createCanonicalState } = require('./state/canonical');");
645
+ requireSnippet(problems, context.sources.cli, `${FILES.cli.display} must import the canonical helper for CLI message authority.`, "const { createCanonicalState } = require('./state/canonical');");
646
+ requireSnippet(problems, context.sources.apiAgents, `${FILES.apiAgents.display} must import the canonical helper for API-agent message authority.`, "const { createCanonicalState } = require('./state/canonical');");
647
+
648
+ requireSnippet(problems, context.blocks.dashboardInject.text, `${FILES.dashboard.display} apiInjectMessage must use canonicalState.appendMessage(...).`, 'canonicalState.appendMessage(msg, { branch });');
649
+ requireSnippet(problems, context.blocks.dashboardEditMessage.text, `${FILES.dashboard.display} apiEditMessage must use getCanonicalState(projectPath).editMessage(...).`, 'getCanonicalState(projectPath).editMessage({');
650
+ requireSnippet(problems, context.blocks.dashboardDeleteMessage.text, `${FILES.dashboard.display} apiDeleteMessage must use getCanonicalState(projectPath).deleteMessage(...).`, 'getCanonicalState(projectPath).deleteMessage({');
651
+ requireSnippet(problems, context.blocks.cliMsg.text, `${FILES.cli.display} cliMsg must use getCanonicalStateCli().appendMessage(msg).`, 'getCanonicalStateCli().appendMessage(msg);');
652
+ requireSnippet(problems, context.blocks.apiSendMessage.text, `${FILES.apiAgents.display} _sendMessage must use this._canonicalState.appendMessage(msg).`, 'this._canonicalState.appendMessage(msg);');
653
+
654
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose appendMessage().`, 'function appendMessage(message, options = {}) {');
655
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} appendMessage() must delegate to messagesState.appendConversationMessage().`, 'return messagesState.appendConversationMessage(');
656
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose editMessage().`, 'function editMessage(params) {');
657
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} editMessage() must resolve the current message from canonical events.`, 'messagesState.getConversationMessageFromEvents(readCanonicalMessageEvents(branch), params.id)');
658
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} editMessage() must append canonical correction events.`, 'appendCanonicalMessageCorrectedEvent({');
659
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} editMessage() must rebuild projections from canonical events after correction.`, 'rebuildMessageProjections({ branch });');
660
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose deleteMessage().`, 'function deleteMessage(params) {');
661
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} deleteMessage() must resolve the current message from canonical events.`, 'messagesState.getConversationMessageFromEvents(readCanonicalMessageEvents(branch), params.id)');
662
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} deleteMessage() must append canonical redaction events.`, 'appendCanonicalMessageRedactedEvent({');
663
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} deleteMessage() must rebuild projections from canonical events after redaction.`, 'rebuildMessageProjections({ branch });');
664
+
665
+ for (const target of [
666
+ { label: `${FILES.cli.display} cliMsg`, text: context.blocks.cliMsg.text },
667
+ { label: `${FILES.dashboard.display} apiInjectMessage`, text: context.blocks.dashboardInject.text },
668
+ { label: `${FILES.dashboard.display} apiEditMessage`, text: context.blocks.dashboardEditMessage.text },
669
+ { label: `${FILES.dashboard.display} apiDeleteMessage`, text: context.blocks.dashboardDeleteMessage.text },
670
+ { label: `${FILES.apiAgents.display} _sendMessage`, text: context.blocks.apiSendMessage.text },
671
+ ]) {
672
+ forbidPattern(problems, target.text, `${target.label} must not directly append or rewrite messages.jsonl.`, directMessageWrite);
673
+ forbidPattern(problems, target.text, `${target.label} must not directly append or rewrite history.jsonl.`, directHistoryWrite);
674
+ }
675
+
676
+ return problems;
677
+ },
678
+ },
679
+ {
680
+ key: 'dashboard_read_authority',
681
+ success: 'Dashboard read-side task/workflow/profile/agent paths now route through canonical or shared query helpers instead of raw authority files.',
682
+ run(context) {
683
+ const problems = [];
684
+ addBlockProblems(problems, context.blocks.dashboardAgents);
685
+ addBlockProblems(problems, context.blocks.dashboardTasksGet);
686
+ addBlockProblems(problems, context.blocks.dashboardExportJson);
687
+ addBlockProblems(problems, context.blocks.dashboardProfilesGet);
688
+ addBlockProblems(problems, context.blocks.dashboardProfilesPost);
689
+ addBlockProblems(problems, context.blocks.dashboardWorkflowsGet);
690
+ addBlockProblems(problems, context.blocks.dashboardPlanSkip);
691
+ addBlockProblems(problems, context.blocks.dashboardPlanReassign);
692
+
693
+ requireSnippet(problems, context.blocks.dashboardAgents.text, `${FILES.dashboard.display} apiAgents must fetch the canonical state helper once.`, 'const canonicalState = getCanonicalState(projectPath);');
694
+ requireSnippet(problems, context.blocks.dashboardAgents.text, `${FILES.dashboard.display} apiAgents must read agents through canonicalState.listAgents().`, 'const agents = canonicalState.listAgents();');
695
+ requireSnippet(problems, context.blocks.dashboardAgents.text, `${FILES.dashboard.display} apiAgents must read profiles through canonicalState.listProfiles().`, 'const profiles = canonicalState.listProfiles();');
696
+ requireSnippet(problems, context.blocks.dashboardTasksGet.text, `${FILES.dashboard.display} apiTasks must validate and pass branch-aware task reads through getCanonicalState(projectPath).listTasks(...).`, 'return getCanonicalState(projectPath).listTasks({ branch: branchResult.branch });');
697
+ requireSnippet(problems, context.blocks.dashboardExportJson.text, `${FILES.dashboard.display} /api/export-json must fetch the canonical state helper once.`, 'const canonicalState = getCanonicalState(projectPath);');
698
+ requireSnippet(problems, context.blocks.dashboardExportJson.text, `${FILES.dashboard.display} /api/export-json must read tasks through canonicalState.listTasks(...) with the requested branch.`, 'const tasks = canonicalState.listTasks({ branch: branchResult.branch });');
699
+ requireSnippet(problems, context.blocks.dashboardProfilesGet.text, `${FILES.dashboard.display} /api/profiles GET must read profiles through getCanonicalState(projectPath).listProfiles().`, 'res.end(JSON.stringify(getCanonicalState(projectPath).listProfiles()));');
700
+ requireSnippet(problems, context.blocks.dashboardProfilesPost.text, `${FILES.dashboard.display} /api/profiles POST must still validate advisory contract metadata via sanitizeContractProfilePatch().`, 'const contractPatch = sanitizeContractProfilePatch({');
701
+ requireSnippet(problems, context.blocks.dashboardProfilesPost.text, `${FILES.dashboard.display} /api/profiles POST must fetch the canonical state helper once.`, 'const canonicalState = getCanonicalState(projectPath);');
702
+ requireSnippet(problems, context.blocks.dashboardProfilesPost.text, `${FILES.dashboard.display} /api/profiles POST must persist through canonicalState.upsertProfile(...).`, 'canonicalState.upsertProfile({');
703
+ requireSnippet(problems, context.blocks.dashboardWorkflowsGet.text, `${FILES.dashboard.display} /api/workflows GET must read workflows through getCanonicalState(projectPath).listWorkflows(...) with the requested branch.`, 'res.end(JSON.stringify(getCanonicalState(projectPath).listWorkflows({ branch: branchResult.branch })));');
704
+ requireSnippet(problems, context.blocks.dashboardPlanSkip.text, `${FILES.dashboard.display} /api/plan/skip/:id must derive workflow selection through canonicalState.listWorkflows(...) with the requested branch.`, 'const workflows = canonicalState.listWorkflows({ branch: branchResult.branch });');
705
+ requireSnippet(problems, context.blocks.dashboardPlanReassign.text, `${FILES.dashboard.display} /api/plan/reassign/:id must derive workflow selection through canonicalState.listWorkflows(...) with the requested branch.`, 'const workflows = canonicalState.listWorkflows({ branch: branchResult.branch });');
706
+
707
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listAgents().`, 'function listAgents() {');
708
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listProfiles().`, 'function listProfiles() {');
709
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose branch-aware listTasks().`, 'function listTasks(params = {}) {');
710
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose branch-aware listWorkflows().`, 'function listWorkflows(params = {}) {');
711
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose upsertProfile().`, 'function upsertProfile(params = {}) {');
712
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} upsertProfile() must persist via agentsState.updateProfile().`, 'const profile = agentsState.updateProfile(name, (currentProfile) => {');
713
+
714
+ forbidPattern(problems, context.blocks.dashboardAgents.text, `${FILES.dashboard.display} apiAgents must not directly read agents.json.`, /const\s+agents\s*=\s*readJson\(\s*filePath\(['"]agents\.json['"]/);
715
+ forbidPattern(problems, context.blocks.dashboardAgents.text, `${FILES.dashboard.display} apiAgents must not directly read profiles.json.`, /const\s+profiles\s*=\s*readJson\(\s*filePath\(['"]profiles\.json['"]/);
716
+
717
+ for (const target of [
718
+ { label: `${FILES.dashboard.display} apiTasks`, text: context.blocks.dashboardTasksGet.text },
719
+ { label: `${FILES.dashboard.display} /api/export-json`, text: context.blocks.dashboardExportJson.text },
720
+ { label: `${FILES.dashboard.display} /api/profiles GET`, text: context.blocks.dashboardProfilesGet.text },
721
+ { label: `${FILES.dashboard.display} /api/profiles POST`, text: context.blocks.dashboardProfilesPost.text },
722
+ { label: `${FILES.dashboard.display} /api/workflows GET`, text: context.blocks.dashboardWorkflowsGet.text },
723
+ { label: `${FILES.dashboard.display} /api/plan/skip/:id`, text: context.blocks.dashboardPlanSkip.text },
724
+ { label: `${FILES.dashboard.display} /api/plan/reassign/:id`, text: context.blocks.dashboardPlanReassign.text },
725
+ ]) {
726
+ forbidPattern(problems, target.text, `${target.label} must not directly read agents.json.`, directAgentsRead);
727
+ forbidPattern(problems, target.text, `${target.label} must not directly read profiles.json.`, directProfilesRead);
728
+ forbidPattern(problems, target.text, `${target.label} must not directly read tasks.json.`, directTasksRead);
729
+ forbidPattern(problems, target.text, `${target.label} must not directly read workflows.json.`, directWorkflowsRead);
730
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite profiles.json.`, directProfilesWrite);
731
+ }
732
+
733
+ return problems;
734
+ },
735
+ },
736
+ {
737
+ key: 'api_agent_authority',
738
+ success: 'API-agent registration, profile seeding, heartbeat, and status updates still route through canonical agent helpers.',
739
+ run(context) {
740
+ const problems = [];
741
+ addBlockProblems(problems, context.blocks.apiAgentsConstructor);
742
+ addBlockProblems(problems, context.blocks.apiRegister);
743
+ addBlockProblems(problems, context.blocks.apiUnregister);
744
+ addBlockProblems(problems, context.blocks.apiHeartbeat);
745
+ addBlockProblems(problems, context.blocks.apiStatusUpdate);
746
+
747
+ requireSnippet(problems, context.blocks.apiAgentsConstructor.text, `${FILES.apiAgents.display} constructor must create the canonical state helper.`, 'this._canonicalState = createCanonicalState({ dataDir, processPid: process.pid });');
748
+ requireSnippet(problems, context.blocks.apiRegister.text, `${FILES.apiAgents.display} _registerInAgentsJson must use this._canonicalState.registerApiAgent(...).`, 'this._canonicalState.registerApiAgent({');
749
+ requireSnippet(problems, context.blocks.apiUnregister.text, `${FILES.apiAgents.display} _unregisterFromAgentsJson must use this._canonicalState.unregisterApiAgent(name).`, 'this._canonicalState.unregisterApiAgent(name);');
750
+ requireSnippet(problems, context.blocks.apiHeartbeat.text, `${FILES.apiAgents.display} _updateHeartbeat must use this._canonicalState.updateAgentHeartbeat(name).`, 'this._canonicalState.updateAgentHeartbeat(name);');
751
+ requireSnippet(problems, context.blocks.apiStatusUpdate.text, `${FILES.apiAgents.display} _updateAgentStatus must use this._canonicalState.updateAgentStatus(name, status).`, 'this._canonicalState.updateAgentStatus(name, status);');
752
+
753
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose registerApiAgent().`, 'function registerApiAgent(params) {');
754
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} registerApiAgent() must write agents through agentsState.setAgent().`, 'const savedAgent = agentsState.setAgent(name, agent);');
755
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} registerApiAgent() must seed profiles through agentsState.updateProfile().`, 'savedProfile = agentsState.updateProfile(name, (currentProfile) => {');
756
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose unregisterApiAgent().`, 'function unregisterApiAgent(name, options = {}) {');
757
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} unregisterApiAgent() must remove agents through agentsState.removeAgent().`, 'const removed = agentsState.removeAgent(name);');
758
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} unregisterApiAgent() must remove profiles through agentsState.deleteProfile().`, 'if (removeProfile) agentsState.deleteProfile(name);');
759
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose updateAgentHeartbeat().`, 'function updateAgentHeartbeat(name) {');
760
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} updateAgentHeartbeat() must touch heartbeat overlays through agentsState.touchHeartbeat().`, 'if (updated) agentsState.touchHeartbeat(name);');
761
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose updateAgentStatus().`, 'function updateAgentStatus(name, status) {');
762
+
763
+ for (const target of [
764
+ { label: `${FILES.apiAgents.display} _registerInAgentsJson`, text: context.blocks.apiRegister.text },
765
+ { label: `${FILES.apiAgents.display} _unregisterFromAgentsJson`, text: context.blocks.apiUnregister.text },
766
+ { label: `${FILES.apiAgents.display} _updateHeartbeat`, text: context.blocks.apiHeartbeat.text },
767
+ { label: `${FILES.apiAgents.display} _updateAgentStatus`, text: context.blocks.apiStatusUpdate.text },
768
+ ]) {
769
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite agents.json.`, directAgentsWrite);
770
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite profiles.json.`, directProfilesWrite);
771
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite heartbeat overlays.`, directHeartbeatWrite);
772
+ }
773
+
774
+ return problems;
775
+ },
776
+ },
777
+ {
778
+ key: 'dashboard_task_workflow_authority',
779
+ success: 'Dashboard task, workflow, and plan-control mutations still route through canonical task/workflow helpers.',
780
+ run(context) {
781
+ const problems = [];
782
+ addBlockProblems(problems, context.blocks.dashboardUpdateTask);
783
+ addBlockProblems(problems, context.blocks.dashboardWorkflowsPost);
784
+ addBlockProblems(problems, context.blocks.dashboardPlanPause);
785
+ addBlockProblems(problems, context.blocks.dashboardPlanResume);
786
+ addBlockProblems(problems, context.blocks.dashboardPlanStop);
787
+ addBlockProblems(problems, context.blocks.dashboardPlanSkip);
788
+ addBlockProblems(problems, context.blocks.dashboardPlanReassign);
789
+
790
+ requireSnippet(problems, context.blocks.dashboardUpdateTask.text, `${FILES.dashboard.display} apiUpdateTask must use getCanonicalState(projectPath).updateTaskStatus(...) with the requested branch.`, 'return getCanonicalState(projectPath).updateTaskStatus({');
791
+ requireSnippet(problems, context.blocks.dashboardUpdateTask.text, `${FILES.dashboard.display} apiUpdateTask must pass branch through to canonicalState.updateTaskStatus(...).`, 'branch: branchResult.branch,');
792
+ requireSnippet(problems, context.blocks.dashboardWorkflowsPost.text, `${FILES.dashboard.display} /api/workflows POST must fetch the canonical state helper once.`, 'const canonicalState = getCanonicalState(projectPath);');
793
+ requireSnippet(problems, context.blocks.dashboardWorkflowsPost.text, `${FILES.dashboard.display} /api/workflows POST must use canonicalState.advanceWorkflow(...) with the requested branch.`, 'const result = canonicalState.advanceWorkflow({ workflowId: body.workflow_id, notes: body.notes, branch: branchResult.branch });');
794
+ requireSnippet(problems, context.blocks.dashboardWorkflowsPost.text, `${FILES.dashboard.display} /api/workflows POST must use canonicalState.skipWorkflowStep(...).`, 'const result = canonicalState.skipWorkflowStep({');
795
+ requireSnippet(problems, context.blocks.dashboardWorkflowsPost.text, `${FILES.dashboard.display} /api/workflows POST skip must pass branch through to canonicalState.skipWorkflowStep(...).`, 'branch: branchResult.branch,');
796
+ requireSnippet(problems, context.blocks.dashboardPlanPause.text, `${FILES.dashboard.display} /api/plan/pause must use getCanonicalState(projectPath).pausePlan(...) with the requested branch.`, 'const result = getCanonicalState(projectPath).pausePlan({ branch: branchResult.branch });');
797
+ requireSnippet(problems, context.blocks.dashboardPlanResume.text, `${FILES.dashboard.display} /api/plan/resume must use getCanonicalState(projectPath).resumePlan(...) with the requested branch.`, 'const result = getCanonicalState(projectPath).resumePlan({ branch: branchResult.branch });');
798
+ requireSnippet(problems, context.blocks.dashboardPlanStop.text, `${FILES.dashboard.display} /api/plan/stop must use getCanonicalState(projectPath).stopPlan(...) with the requested branch.`, 'const result = getCanonicalState(projectPath).stopPlan({ branch: branchResult.branch });');
799
+ requireSnippet(problems, context.blocks.dashboardPlanSkip.text, `${FILES.dashboard.display} /api/plan/skip/:id must use canonicalState.skipWorkflowStep(...).`, 'const result = canonicalState.skipWorkflowStep({');
800
+ requireSnippet(problems, context.blocks.dashboardPlanSkip.text, `${FILES.dashboard.display} /api/plan/skip/:id must pass branch through to canonicalState.skipWorkflowStep(...).`, 'branch: branchResult.branch,');
801
+ requireSnippet(problems, context.blocks.dashboardPlanReassign.text, `${FILES.dashboard.display} /api/plan/reassign/:id must use canonicalState.reassignWorkflowStep(...).`, 'const result = canonicalState.reassignWorkflowStep({');
802
+ requireSnippet(problems, context.blocks.dashboardPlanReassign.text, `${FILES.dashboard.display} /api/plan/reassign/:id must pass branch through to canonicalState.reassignWorkflowStep(...).`, 'branch: branchResult.branch,');
803
+
804
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose updateTaskStatus().`, 'function updateTaskStatus(params) {');
805
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} updateTaskStatus() must delegate to tasksWorkflowsState.mutateTasks(...).`, 'tasksWorkflowsState.mutateTasks((tasks) => {');
806
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose advanceWorkflow().`, 'function advanceWorkflow(params) {');
807
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} advanceWorkflow() must delegate to tasksWorkflowsState.mutateWorkflows(...).`, 'tasksWorkflowsState.mutateWorkflows((workflows) => {');
808
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose skipWorkflowStep().`, 'function skipWorkflowStep(params) {');
809
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose reassignWorkflowStep().`, 'function reassignWorkflowStep(params) {');
810
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose pausePlan().`, 'function pausePlan() {');
811
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose resumePlan().`, 'function resumePlan() {');
812
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose stopPlan().`, 'function stopPlan() {');
813
+
814
+ for (const target of [
815
+ { label: `${FILES.dashboard.display} apiUpdateTask`, text: context.blocks.dashboardUpdateTask.text },
816
+ { label: `${FILES.dashboard.display} /api/workflows POST`, text: context.blocks.dashboardWorkflowsPost.text },
817
+ { label: `${FILES.dashboard.display} /api/plan/pause`, text: context.blocks.dashboardPlanPause.text },
818
+ { label: `${FILES.dashboard.display} /api/plan/resume`, text: context.blocks.dashboardPlanResume.text },
819
+ { label: `${FILES.dashboard.display} /api/plan/stop`, text: context.blocks.dashboardPlanStop.text },
820
+ { label: `${FILES.dashboard.display} /api/plan/skip/:id`, text: context.blocks.dashboardPlanSkip.text },
821
+ { label: `${FILES.dashboard.display} /api/plan/reassign/:id`, text: context.blocks.dashboardPlanReassign.text },
822
+ ]) {
823
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite tasks.json.`, directTasksWrite);
824
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite workflows.json.`, directWorkflowsWrite);
825
+ }
826
+
827
+ return problems;
828
+ },
829
+ },
830
+ {
831
+ key: 'collaboration_state_authority',
832
+ success: 'Rules, workspace, decision, knowledge, review, dependency, progress, and vote governance surfaces route through canonical helpers instead of raw projection writes.',
833
+ run(context) {
834
+ const problems = [];
835
+ addBlockProblems(problems, context.blocks.dashboardRulesApi);
836
+ addBlockProblems(problems, context.blocks.dashboardRulesRoutes);
837
+ addBlockProblems(problems, context.blocks.workspaceSaveHelper);
838
+ addBlockProblems(problems, context.blocks.toolLogDecision);
839
+ addBlockProblems(problems, context.blocks.toolKBWrite);
840
+ addBlockProblems(problems, context.blocks.toolUpdateProgress);
841
+ addBlockProblems(problems, context.blocks.toolCallVote);
842
+ addBlockProblems(problems, context.blocks.toolCastVote);
843
+ addBlockProblems(problems, context.blocks.toolAddRule);
844
+ addBlockProblems(problems, context.blocks.toolRemoveRule);
845
+ addBlockProblems(problems, context.blocks.toolToggleRule);
846
+ addBlockProblems(problems, context.blocks.dashboardDecisionsRoute);
847
+ addBlockProblems(problems, context.blocks.dashboardPlanSkillsRoute);
848
+ addBlockProblems(problems, context.blocks.toolRequestReview);
849
+ addBlockProblems(problems, context.blocks.toolSubmitReview);
850
+ addBlockProblems(problems, context.blocks.toolDeclareDependency);
851
+
852
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listDecisions().`, 'function listDecisions(params = {}) {');
853
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose readKnowledgeBase().`, 'function readKnowledgeBase(params = {}) {');
854
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listReviews().`, 'function listReviews(params = {}) {');
855
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listDependencies().`, 'function listDependencies(params = {}) {');
856
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listVotes().`, 'function listVotes(params = {}) {');
857
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose listRules().`, 'function listRules(params = {}) {');
858
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose readProgress().`, 'function readProgress(params = {}) {');
859
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose mutateReviews().`, 'function mutateReviews(mutator, params = {}) {');
860
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose mutateDependencies().`, 'function mutateDependencies(mutator, params = {}) {');
861
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose addRule().`, 'function addRule(params = {}) {');
862
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose updateRule().`, 'function updateRule(params = {}) {');
863
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose toggleRule().`, 'function toggleRule(params = {}) {');
864
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose removeRule().`, 'function removeRule(params = {}) {');
865
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose saveWorkspace().`, 'function saveWorkspace(agentName, workspace, params = {}) {');
866
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose logDecision().`, 'function logDecision(params = {}) {');
867
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose writeKnowledgeBaseEntry().`, 'function writeKnowledgeBaseEntry(params = {}) {');
868
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose updateProgressRecord().`, 'function updateProgressRecord(params = {}) {');
869
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose createVote().`, 'function createVote(params = {}) {');
870
+ requireSnippet(problems, context.sources.canonical, `${FILES.canonical.display} must expose castVote().`, 'function castVote(params = {}) {');
871
+
872
+ requireSnippet(problems, context.blocks.dashboardRulesApi.text, `${FILES.dashboard.display} apiRules must read rules through canonicalState.listRules(...).`, 'return getCanonicalState(projectPath).listRules({ branch: branchResult.branch });');
873
+ requireSnippet(problems, context.blocks.dashboardRulesApi.text, `${FILES.dashboard.display} apiAddRule must use canonical addRule().`, 'const created = getCanonicalState(projectPath).addRule({');
874
+ requireSnippet(problems, context.blocks.dashboardRulesApi.text, `${FILES.dashboard.display} apiUpdateRule must use canonical updateRule().`, 'const updated = getCanonicalState(projectPath).updateRule({');
875
+ requireSnippet(problems, context.blocks.dashboardRulesApi.text, `${FILES.dashboard.display} apiDeleteRule must use canonical removeRule().`, 'const removed = getCanonicalState(projectPath).removeRule({');
876
+ requireSnippet(problems, context.blocks.dashboardRulesRoutes.text, `${FILES.dashboard.display} rules routes must delegate GET to apiRules().`, 'const result = apiRules(url.searchParams);');
877
+ requireSnippet(problems, context.blocks.dashboardRulesRoutes.text, `${FILES.dashboard.display} rules routes must delegate POST to apiAddRule().`, 'const result = apiAddRule(body, url.searchParams);');
878
+ requireSnippet(problems, context.blocks.dashboardRulesRoutes.text, `${FILES.dashboard.display} rules routes must delegate DELETE to apiDeleteRule().`, 'const result = apiDeleteRule({ rule_id: ruleId }, url.searchParams);');
879
+ requireSnippet(problems, context.blocks.dashboardRulesRoutes.text, `${FILES.dashboard.display} rules toggle route must use canonical toggleRule().`, 'const toggled = getCanonicalState(projectPath).toggleRule({');
880
+ requireSnippet(problems, context.blocks.dashboardDecisionsRoute.text, `${FILES.dashboard.display} /api/decisions must read decisions through canonicalState.listDecisions(...).`, 'getCanonicalState(projectPath).listDecisions({ branch: branchResult.branch });');
881
+ requireSnippet(problems, context.blocks.dashboardExportJson.text, `${FILES.dashboard.display} /api/export-json must read decisions through canonicalState.listDecisions(...).`, 'const decisions = canonicalState.listDecisions({ branch: branchResult.branch });');
882
+ requireSnippet(problems, context.blocks.dashboardPlanSkillsRoute.text, `${FILES.dashboard.display} /api/plan/skills must read the KB through canonicalState.readKnowledgeBase(...).`, 'const kb = getCanonicalState(projectPath).readKnowledgeBase({ branch: branchResult.branch });');
883
+
884
+ requireSnippet(problems, context.blocks.workspaceSaveHelper.text, 'agent-bridge/server.js workspace helper must use canonical saveWorkspace().', 'const result = canonicalState.saveWorkspace(agentName, data, {');
885
+ requireSnippet(problems, context.blocks.toolLogDecision.text, 'toolLogDecision must use canonical logDecision().', 'const logged = canonicalState.logDecision({');
886
+ requireSnippet(problems, context.blocks.toolKBWrite.text, 'toolKBWrite must use canonical writeKnowledgeBaseEntry().', 'const written = canonicalState.writeKnowledgeBaseEntry({');
887
+ requireSnippet(problems, context.blocks.toolUpdateProgress.text, 'toolUpdateProgress must use canonical updateProgressRecord().', 'const updated = canonicalState.updateProgressRecord({');
888
+ requireSnippet(problems, context.blocks.toolCallVote.text, 'toolCallVote must use canonical createVote().', 'const created = canonicalState.createVote({');
889
+ requireSnippet(problems, context.blocks.toolCastVote.text, 'toolCastVote must use canonical castVote().', 'const cast = canonicalState.castVote({');
890
+ requireSnippet(problems, context.blocks.toolAddRule.text, 'toolAddRule must use canonical addRule().', 'const created = canonicalState.addRule({');
891
+ requireSnippet(problems, context.blocks.toolRemoveRule.text, 'toolRemoveRule must use canonical removeRule().', 'const removed = canonicalState.removeRule({');
892
+ requireSnippet(problems, context.blocks.toolToggleRule.text, 'toolToggleRule must use canonical toggleRule().', 'const toggled = canonicalState.toggleRule({');
893
+ requireSnippet(problems, context.blocks.toolRequestReview.text, 'toolRequestReview must use canonical mutateReviews().', 'const reviewWrite = canonicalState.mutateReviews((reviews) => {');
894
+ requireSnippet(problems, context.blocks.toolSubmitReview.text, 'toolSubmitReview must use canonical mutateReviews().', 'const reviewUpdate = canonicalState.mutateReviews((reviews) => {');
895
+ requireSnippet(problems, context.blocks.toolDeclareDependency.text, 'toolDeclareDependency must use canonical mutateDependencies().', 'const dependencyWrite = canonicalState.mutateDependencies((deps) => {');
896
+ requireSnippet(problems, context.sources.server, 'Dependency resolution on task completion must use canonical mutateDependencies().', 'const resolvedDependencies = canonicalState.mutateDependencies((deps) => {');
897
+
898
+ for (const target of [
899
+ { label: `${FILES.dashboard.display} rules api helpers`, text: context.blocks.dashboardRulesApi.text },
900
+ { label: `${FILES.dashboard.display} rules routes`, text: context.blocks.dashboardRulesRoutes.text },
901
+ { label: 'toolAddRule', text: context.blocks.toolAddRule.text },
902
+ { label: 'toolRemoveRule', text: context.blocks.toolRemoveRule.text },
903
+ { label: 'toolToggleRule', text: context.blocks.toolToggleRule.text },
904
+ ]) {
905
+ forbidPattern(problems, target.text, `${target.label} must not directly rewrite rules.json.`, directRulesWrite);
906
+ forbidPattern(problems, target.text, `${target.label} must not use legacy writeJsonFile(RULES_FILE, ...).`, /writeJsonFile\(RULES_FILE/);
907
+ }
908
+
909
+ for (const target of [
910
+ { label: `${FILES.dashboard.display} /api/decisions`, text: context.blocks.dashboardDecisionsRoute.text },
911
+ { label: `${FILES.dashboard.display} /api/export-json`, text: context.blocks.dashboardExportJson.text },
912
+ ]) {
913
+ forbidPattern(problems, target.text, `${target.label} must not directly read decisions.json.`, directDecisionsRead);
914
+ }
915
+ forbidPattern(problems, context.blocks.dashboardPlanSkillsRoute.text, `${FILES.dashboard.display} /api/plan/skills must not directly read kb.json.`, directKnowledgeRead);
916
+
917
+ forbidPattern(problems, context.blocks.workspaceSaveHelper.text, 'saveWorkspace() must not directly write workspace files with fs.writeFileSync.', directWorkspaceWrite);
918
+
919
+ for (const target of [
920
+ { label: 'toolLogDecision', text: context.blocks.toolLogDecision.text },
921
+ { label: 'toolKBWrite', text: context.blocks.toolKBWrite.text },
922
+ { label: 'toolUpdateProgress', text: context.blocks.toolUpdateProgress.text },
923
+ { label: 'toolCallVote', text: context.blocks.toolCallVote.text },
924
+ { label: 'toolCastVote', text: context.blocks.toolCastVote.text },
925
+ { label: 'toolRequestReview', text: context.blocks.toolRequestReview.text },
926
+ { label: 'toolSubmitReview', text: context.blocks.toolSubmitReview.text },
927
+ { label: 'toolDeclareDependency', text: context.blocks.toolDeclareDependency.text },
928
+ ]) {
929
+ forbidPattern(problems, target.text, `${target.label} must not directly write legacy governance projections.`, directGovernanceWrite);
930
+ }
931
+
932
+ return problems;
933
+ },
934
+ },
935
+ ];
936
+ }
937
+
938
+ function runAuthoritySuite(context) {
939
+ const checks = getAuthorityChecks();
940
+ const failures = [];
941
+ const successes = [];
942
+
943
+ for (const check of checks) {
944
+ const problems = check.run(context);
945
+ if (problems.length > 0) {
946
+ failures.push({ key: check.key, problems });
947
+ continue;
948
+ }
949
+ successes.push({ key: check.key, message: check.success });
950
+ }
951
+
952
+ return { failures, successes };
953
+ }
954
+
955
+ function main() {
956
+ const { simulateLegacyWrite } = parseArgs(process.argv.slice(2));
957
+ const context = buildContext(simulateLegacyWrite);
958
+ const result = runAuthoritySuite(context);
959
+
960
+ if (result.failures.length > 0) {
961
+ const lines = [
962
+ 'Authority invariant suite failed.',
963
+ 'Suite: authority',
964
+ `Simulated legacy write: ${simulateLegacyWrite ? 'enabled' : 'disabled'}`,
965
+ 'Violations:',
966
+ ];
967
+
968
+ for (const failure of result.failures) {
969
+ lines.push(`- ${failure.key}`);
970
+ for (const problem of failure.problems) {
971
+ lines.push(` - ${problem}`);
972
+ }
973
+ }
974
+
975
+ fail(lines.join('\n'), 1);
976
+ }
977
+
978
+ const lines = [
979
+ 'Authority invariant suite passed.',
980
+ 'Suite: authority',
981
+ `Simulated legacy write: ${simulateLegacyWrite ? 'enabled' : 'disabled'}`,
982
+ `Validated ${result.successes.length} authority invariants across ${Object.keys(FILES).length} checked files.`,
983
+ ];
984
+
985
+ for (const success of result.successes) {
986
+ lines.push(`- ${success.key}: ${success.message}`);
987
+ }
988
+
989
+ console.log(lines.join('\n'));
990
+ }
991
+
992
+ main();