let-them-talk 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7216
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- package/templates/team.json +3 -3
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
|
|
8
|
+
const { createCanonicalState } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
9
|
+
|
|
10
|
+
const SERVER_FILE = path.resolve(__dirname, '..', 'server.js');
|
|
11
|
+
const CANONICAL_FILE = path.resolve(__dirname, '..', 'state', 'canonical.js');
|
|
12
|
+
const LOG_FILE = path.resolve(__dirname, '..', 'events', 'log.js');
|
|
13
|
+
const HOOKS_FILE = path.resolve(__dirname, '..', 'events', 'hooks.js');
|
|
14
|
+
|
|
15
|
+
function fail(lines, exitCode = 1) {
|
|
16
|
+
fs.writeSync(2, lines.join('\n') + '\n');
|
|
17
|
+
process.exit(exitCode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assert(condition, message, problems) {
|
|
21
|
+
if (!condition) problems.push(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractBlock(source, startAnchor, endAnchor) {
|
|
25
|
+
const startIndex = source.indexOf(startAnchor);
|
|
26
|
+
if (startIndex === -1) return '';
|
|
27
|
+
const endIndex = endAnchor ? source.indexOf(endAnchor, startIndex + startAnchor.length) : source.length;
|
|
28
|
+
if (endIndex === -1) return source.slice(startIndex);
|
|
29
|
+
return source.slice(startIndex, endIndex);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildTask(id, title) {
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
title,
|
|
36
|
+
description: `${title} description`,
|
|
37
|
+
status: 'pending',
|
|
38
|
+
assignee: null,
|
|
39
|
+
created_by: 'alpha',
|
|
40
|
+
created_at: '2026-04-16T18:00:10.000Z',
|
|
41
|
+
updated_at: '2026-04-16T18:00:10.000Z',
|
|
42
|
+
notes: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildWorkflow(id, description) {
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
name: `Workflow ${id}`,
|
|
50
|
+
status: 'active',
|
|
51
|
+
autonomous: true,
|
|
52
|
+
parallel: false,
|
|
53
|
+
created_by: 'alpha',
|
|
54
|
+
created_at: '2026-04-16T18:00:20.000Z',
|
|
55
|
+
updated_at: '2026-04-16T18:00:20.000Z',
|
|
56
|
+
steps: [
|
|
57
|
+
{
|
|
58
|
+
id: 1,
|
|
59
|
+
description,
|
|
60
|
+
assignee: 'alpha',
|
|
61
|
+
depends_on: [],
|
|
62
|
+
status: 'in_progress',
|
|
63
|
+
started_at: '2026-04-16T18:00:20.000Z',
|
|
64
|
+
completed_at: null,
|
|
65
|
+
notes: '',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 2,
|
|
69
|
+
description: `Follow-up for ${id}`,
|
|
70
|
+
assignee: 'beta',
|
|
71
|
+
depends_on: [1],
|
|
72
|
+
status: 'pending',
|
|
73
|
+
started_at: null,
|
|
74
|
+
completed_at: null,
|
|
75
|
+
notes: '',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertHookTopics(events, hooks, topic, problems, label) {
|
|
82
|
+
const expectedEventIds = events.filter((event) => event.type === topic).map((event) => event.event_id);
|
|
83
|
+
const actualHookEventIds = hooks.filter((hook) => hook.topic === topic).map((hook) => hook.event_id);
|
|
84
|
+
assert(
|
|
85
|
+
JSON.stringify(actualHookEventIds) === JSON.stringify(expectedEventIds),
|
|
86
|
+
`${label} hooks for ${topic} must mirror committed canonical events 1:1.`,
|
|
87
|
+
problems
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function main() {
|
|
92
|
+
const problems = [];
|
|
93
|
+
const serverSource = fs.readFileSync(SERVER_FILE, 'utf8');
|
|
94
|
+
const canonicalSource = fs.readFileSync(CANONICAL_FILE, 'utf8');
|
|
95
|
+
const logSource = fs.readFileSync(LOG_FILE, 'utf8');
|
|
96
|
+
const hooksSource = fs.readFileSync(HOOKS_FILE, 'utf8');
|
|
97
|
+
|
|
98
|
+
const registerBlock = extractBlock(serverSource, 'function toolRegister(name, provider = null) {', '// Update last_activity timestamp for this agent');
|
|
99
|
+
const conversationModeBlock = extractBlock(serverSource, 'function toolSetConversationMode(mode) {', '// --- Managed mode tools ---');
|
|
100
|
+
const claimManagerBlock = extractBlock(serverSource, 'function toolClaimManager() {', 'function toolYieldFloor(to, prompt = null) {');
|
|
101
|
+
const yieldFloorBlock = extractBlock(serverSource, 'function toolYieldFloor(to, prompt = null) {', 'function toolSetPhase(phase) {');
|
|
102
|
+
const setPhaseBlock = extractBlock(serverSource, 'function toolSetPhase(phase) {', '// Deterministic stagger delay based on agent name');
|
|
103
|
+
const createTaskBlock = extractBlock(serverSource, 'function toolCreateTask(title, description = \'\', assignee = null) {', 'function toolUpdateTask(taskId, status, notes = null, evidence = null) {');
|
|
104
|
+
const updateTaskBlock = extractBlock(serverSource, 'function toolUpdateTask(taskId, status, notes = null, evidence = null) {', 'function toolListTasks(status = null, assignee = null) {');
|
|
105
|
+
const workspaceWriteBlock = extractBlock(serverSource, 'function toolWorkspaceWrite(key, content) {', 'function toolWorkspaceRead(key, agent) {');
|
|
106
|
+
const createWorkflowBlock = extractBlock(serverSource, 'function toolCreateWorkflow(name, steps, autonomous = false, parallel = false) {', 'function toolAdvanceWorkflow(workflowId, notes, evidence = null) {');
|
|
107
|
+
const logDecisionBlock = extractBlock(serverSource, 'function toolLogDecision(decision, reasoning, topic) {', 'function toolGetDecisions(topic) {');
|
|
108
|
+
const kbWriteBlock = extractBlock(serverSource, 'function toolKBWrite(key, content) {', 'function toolKBRead(key) {');
|
|
109
|
+
const progressBlock = extractBlock(serverSource, 'function toolUpdateProgress(feature, percent, notes) {', 'function toolGetProgress() {');
|
|
110
|
+
const callVoteBlock = extractBlock(serverSource, 'function toolCallVote(question, options) {', 'function toolCastVote(voteId, choice) {');
|
|
111
|
+
const castVoteBlock = extractBlock(serverSource, 'function toolCastVote(voteId, choice) {', 'function toolVoteStatus(voteId) {');
|
|
112
|
+
const requestReviewBlock = extractBlock(serverSource, 'function toolRequestReview(filePath, description) {', 'function toolSubmitReview(reviewId, status, feedback) {');
|
|
113
|
+
const submitReviewBlock = extractBlock(serverSource, 'function toolSubmitReview(reviewId, status, feedback) {', 'function toolDeclareDependency(taskId, dependsOnTaskId) {');
|
|
114
|
+
const declareDependencyBlock = extractBlock(serverSource, 'function toolDeclareDependency(taskId, dependsOnTaskId) {', 'function toolCheckDependencies(taskId) {');
|
|
115
|
+
const addRuleBlock = extractBlock(serverSource, 'function toolAddRule(text, category = \'custom\') {', 'function toolListRules() {');
|
|
116
|
+
const removeRuleBlock = extractBlock(serverSource, 'function toolRemoveRule(ruleId) {', 'function toolToggleRule(ruleId) {');
|
|
117
|
+
const toggleRuleBlock = extractBlock(serverSource, 'function toolToggleRule(ruleId) {', '// --- MCP Server setup ---');
|
|
118
|
+
const exitBlock = extractBlock(serverSource, "process.on('exit', () => {", "process.on('SIGTERM', () => process.exit(0));");
|
|
119
|
+
|
|
120
|
+
assert(hooksSource.includes('topic: event.type'), 'Hook projection must key topic directly from canonical event type.', problems);
|
|
121
|
+
assert(!hooksSource.includes('appendEvent('), 'Hook projection must not append canonical events or create a second event store.', problems);
|
|
122
|
+
assert(logSource.includes('onCommitted(cloneJsonValue(event));'), 'Canonical event log must feed hook projection only after append succeeds.', problems);
|
|
123
|
+
assert(canonicalSource.includes('function appendCanonicalEvent(params = {}) {'), 'canonical state must expose a shared canonical append helper.', problems);
|
|
124
|
+
assert(canonicalSource.includes('readHooks,'), 'canonical state must expose the hook subscription surface.', problems);
|
|
125
|
+
assert(canonicalSource.includes("type: 'workflow.step_started'"), 'canonical workflow helpers must emit workflow.step_started where steps enter progress.', problems);
|
|
126
|
+
assert(canonicalSource.includes("type: 'workflow.step_reassigned'"), 'canonical workflow helpers must emit workflow.step_reassigned.', problems);
|
|
127
|
+
assert(canonicalSource.includes('function saveWorkspace(agentName, workspace, params = {}) {'), 'canonical state must expose saveWorkspace().', problems);
|
|
128
|
+
assert(canonicalSource.includes("type: 'workspace.written'"), 'canonical state must emit workspace.written.', problems);
|
|
129
|
+
assert(canonicalSource.includes('function logDecision(params = {}) {'), 'canonical state must expose logDecision().', problems);
|
|
130
|
+
assert(canonicalSource.includes("type: 'decision.logged'"), 'canonical state must emit decision.logged.', problems);
|
|
131
|
+
assert(canonicalSource.includes('function writeKnowledgeBaseEntry(params = {}) {'), 'canonical state must expose writeKnowledgeBaseEntry().', problems);
|
|
132
|
+
assert(canonicalSource.includes("type: 'kb.written'"), 'canonical state must emit kb.written.', problems);
|
|
133
|
+
assert(canonicalSource.includes('function updateProgressRecord(params = {}) {'), 'canonical state must expose updateProgressRecord().', problems);
|
|
134
|
+
assert(canonicalSource.includes("type: 'progress.updated'"), 'canonical state must emit progress.updated.', problems);
|
|
135
|
+
assert(canonicalSource.includes('function createVote(params = {}) {'), 'canonical state must expose createVote().', problems);
|
|
136
|
+
assert(canonicalSource.includes('function castVote(params = {}) {'), 'canonical state must expose castVote().', problems);
|
|
137
|
+
assert(canonicalSource.includes("type: 'vote.called'"), 'canonical state must emit vote.called.', problems);
|
|
138
|
+
assert(canonicalSource.includes("type: 'vote.cast'"), 'canonical state must emit vote.cast.', problems);
|
|
139
|
+
assert(canonicalSource.includes("type: 'vote.resolved'"), 'canonical state must emit vote.resolved.', problems);
|
|
140
|
+
assert(canonicalSource.includes('function addRule(params = {}) {'), 'canonical state must expose addRule().', problems);
|
|
141
|
+
assert(canonicalSource.includes('function toggleRule(params = {}) {'), 'canonical state must expose toggleRule().', problems);
|
|
142
|
+
assert(canonicalSource.includes('function removeRule(params = {}) {'), 'canonical state must expose removeRule().', problems);
|
|
143
|
+
assert(canonicalSource.includes("type: 'rule.added'"), 'canonical state must emit rule.added.', problems);
|
|
144
|
+
assert(canonicalSource.includes("type: 'rule.toggled'"), 'canonical state must emit rule.toggled.', problems);
|
|
145
|
+
assert(canonicalSource.includes("type: 'rule.removed'"), 'canonical state must emit rule.removed.', problems);
|
|
146
|
+
|
|
147
|
+
assert(registerBlock.includes("type: 'agent.registered'"), 'toolRegister() must emit agent.registered.', problems);
|
|
148
|
+
assert(serverSource.includes('canonicalState.recordAgentHeartbeat(name,'), 'Server heartbeat helper must route through canonicalState.recordAgentHeartbeat(...).', problems);
|
|
149
|
+
assert(serverSource.includes('canonicalState.setAgentListeningState(registeredName, isListening'), 'Listening transitions must route through canonicalState.setAgentListeningState(...).', problems);
|
|
150
|
+
assert(serverSource.includes('canonicalState.updateAgentBranch(registeredName, branchName'), 'Branch activation must route through canonicalState.updateAgentBranch(...).', problems);
|
|
151
|
+
assert(exitBlock.includes("type: 'agent.unregistered'"), 'Graceful exit cleanup must emit agent.unregistered.', problems);
|
|
152
|
+
assert(conversationModeBlock.includes("type: 'conversation.mode_updated'"), 'set_conversation_mode must emit conversation.mode_updated.', problems);
|
|
153
|
+
assert(claimManagerBlock.includes("type: 'conversation.manager_claimed'"), 'claim_manager must emit conversation.manager_claimed.', problems);
|
|
154
|
+
assert(yieldFloorBlock.includes("type: 'conversation.floor_yielded'"), 'yield_floor must emit conversation.floor_yielded.', problems);
|
|
155
|
+
assert(setPhaseBlock.includes("type: 'conversation.phase_updated'"), 'set_phase must emit conversation.phase_updated.', problems);
|
|
156
|
+
assert(createTaskBlock.includes('canonicalState.createTask({'), 'create_task must route through canonicalState.createTask(...).', problems);
|
|
157
|
+
assert(updateTaskBlock.includes('canonicalState.updateTaskStatus({'), 'update_task must route status transitions through canonicalState.updateTaskStatus(...).', problems);
|
|
158
|
+
assert(workspaceWriteBlock.includes('saveWorkspace(registeredName, ws, { key, keys: [key] });'), 'workspace_write must persist through saveWorkspace(...).', problems);
|
|
159
|
+
assert(serverSource.includes('const result = canonicalState.saveWorkspace(agentName, data, {'), 'workspace save helper must route through canonicalState.saveWorkspace(...).', problems);
|
|
160
|
+
assert(createWorkflowBlock.includes('canonicalState.createWorkflow({'), 'create_workflow must route through canonicalState.createWorkflow(...).', problems);
|
|
161
|
+
assert(logDecisionBlock.includes('canonicalState.logDecision({'), 'log_decision must route through canonicalState.logDecision(...).', problems);
|
|
162
|
+
assert(kbWriteBlock.includes('canonicalState.writeKnowledgeBaseEntry({'), 'kb_write must route through canonicalState.writeKnowledgeBaseEntry(...).', problems);
|
|
163
|
+
assert(progressBlock.includes('canonicalState.updateProgressRecord({'), 'update_progress must route through canonicalState.updateProgressRecord(...).', problems);
|
|
164
|
+
assert(callVoteBlock.includes('canonicalState.createVote({'), 'call_vote must route through canonicalState.createVote(...).', problems);
|
|
165
|
+
assert(castVoteBlock.includes('canonicalState.castVote({'), 'cast_vote must route through canonicalState.castVote(...).', problems);
|
|
166
|
+
assert(requestReviewBlock.includes("type: 'review.requested'"), 'request_review must emit review.requested.', problems);
|
|
167
|
+
assert(submitReviewBlock.includes("type: 'review.submitted'"), 'submit_review must emit review.submitted.', problems);
|
|
168
|
+
assert(declareDependencyBlock.includes("type: 'dependency.declared'"), 'declare_dependency must emit dependency.declared.', problems);
|
|
169
|
+
assert(updateTaskBlock.includes("type: 'dependency.resolved'"), 'Task completion dependency resolution must emit dependency.resolved.', problems);
|
|
170
|
+
assert(addRuleBlock.includes('canonicalState.addRule({'), 'add_rule must route through canonicalState.addRule(...).', problems);
|
|
171
|
+
assert(removeRuleBlock.includes('canonicalState.removeRule({'), 'remove_rule must route through canonicalState.removeRule(...).', problems);
|
|
172
|
+
assert(toggleRuleBlock.includes('canonicalState.toggleRule({'), 'toggle_rule must route through canonicalState.toggleRule(...).', problems);
|
|
173
|
+
|
|
174
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-lifecycle-hooks-'));
|
|
175
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
176
|
+
const branchName = 'feature_hooks';
|
|
177
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: 5150 });
|
|
178
|
+
const eventLog = createCanonicalEventLog({ dataDir });
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
182
|
+
|
|
183
|
+
canonicalState.registerApiAgent({
|
|
184
|
+
name: 'alpha',
|
|
185
|
+
sessionId: 'session_alpha',
|
|
186
|
+
reason: 'api_register',
|
|
187
|
+
agent: {
|
|
188
|
+
pid: 5150,
|
|
189
|
+
timestamp: '2026-04-16T18:00:00.000Z',
|
|
190
|
+
last_activity: '2026-04-16T18:00:00.000Z',
|
|
191
|
+
provider: 'claude',
|
|
192
|
+
branch: 'main',
|
|
193
|
+
started_at: '2026-04-16T18:00:00.000Z',
|
|
194
|
+
status: 'active',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
canonicalState.updateAgentStatus('alpha', 'sleeping', { actorAgent: 'alpha', sessionId: 'session_alpha', reason: 'idle' });
|
|
198
|
+
canonicalState.updateAgentHeartbeat('alpha', { actorAgent: 'alpha', sessionId: 'session_alpha', reason: 'api_heartbeat' });
|
|
199
|
+
canonicalState.updateAgentBranch('alpha', branchName, { actorAgent: 'alpha', sessionId: 'session_alpha', reason: 'branch_activate' });
|
|
200
|
+
canonicalState.setAgentListeningState('alpha', true, { actorAgent: 'alpha', sessionId: 'session_alpha', reason: 'listen_start' });
|
|
201
|
+
|
|
202
|
+
const task = buildTask('task_hooks', 'Hook lifecycle task');
|
|
203
|
+
const createdTask = canonicalState.createTask({ task, actor: 'alpha', branch: branchName, sessionId: 'session_alpha', correlationId: task.id });
|
|
204
|
+
assert(createdTask.success, 'Fixture task creation should succeed.', problems);
|
|
205
|
+
|
|
206
|
+
const claimedTask = canonicalState.updateTaskStatus({
|
|
207
|
+
taskId: task.id,
|
|
208
|
+
status: 'in_progress',
|
|
209
|
+
actor: 'alpha',
|
|
210
|
+
branch: branchName,
|
|
211
|
+
sessionId: 'session_alpha',
|
|
212
|
+
correlationId: task.id,
|
|
213
|
+
assignee: 'alpha',
|
|
214
|
+
trackAttemptAgent: true,
|
|
215
|
+
expectedStatuses: ['pending'],
|
|
216
|
+
});
|
|
217
|
+
assert(claimedTask.success, 'Fixture task claim should succeed.', problems);
|
|
218
|
+
|
|
219
|
+
const workflow = buildWorkflow('wf_hooks', 'Hook lifecycle workflow');
|
|
220
|
+
const createdWorkflow = canonicalState.createWorkflow({ workflow, actor: 'alpha', branch: branchName, sessionId: 'session_alpha', correlationId: workflow.id });
|
|
221
|
+
assert(createdWorkflow.success, 'Fixture workflow creation should succeed.', problems);
|
|
222
|
+
|
|
223
|
+
const reassigned = canonicalState.reassignWorkflowStep({
|
|
224
|
+
workflowId: workflow.id,
|
|
225
|
+
stepId: 1,
|
|
226
|
+
newAssignee: 'beta',
|
|
227
|
+
actor: 'alpha',
|
|
228
|
+
branch: branchName,
|
|
229
|
+
sessionId: 'session_alpha',
|
|
230
|
+
correlationId: workflow.id,
|
|
231
|
+
});
|
|
232
|
+
assert(reassigned.success, 'Fixture workflow reassignment should succeed.', problems);
|
|
233
|
+
|
|
234
|
+
const paused = canonicalState.pausePlan({ actor: 'alpha', branch: branchName, sessionId: 'session_alpha', correlationId: workflow.id });
|
|
235
|
+
const resumed = canonicalState.resumePlan({ actor: 'alpha', branch: branchName, sessionId: 'session_alpha', correlationId: workflow.id });
|
|
236
|
+
const stopped = canonicalState.stopPlan({ actor: 'alpha', branch: branchName, sessionId: 'session_alpha', correlationId: workflow.id });
|
|
237
|
+
assert(paused.success && resumed.success && stopped.success, 'Fixture pause/resume/stop workflow transitions should succeed.', problems);
|
|
238
|
+
|
|
239
|
+
const modeEvent = canonicalState.appendCanonicalEvent({
|
|
240
|
+
type: 'conversation.mode_updated',
|
|
241
|
+
branchId: branchName,
|
|
242
|
+
actorAgent: 'alpha',
|
|
243
|
+
sessionId: 'session_alpha',
|
|
244
|
+
correlationId: branchName,
|
|
245
|
+
payload: {
|
|
246
|
+
mode: 'managed',
|
|
247
|
+
previous_mode: 'direct',
|
|
248
|
+
updated_at: '2026-04-16T18:01:00.000Z',
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const reviewRequested = canonicalState.appendCanonicalEvent({
|
|
252
|
+
type: 'review.requested',
|
|
253
|
+
branchId: branchName,
|
|
254
|
+
actorAgent: 'alpha',
|
|
255
|
+
sessionId: 'session_alpha',
|
|
256
|
+
correlationId: 'rev_hooks',
|
|
257
|
+
payload: {
|
|
258
|
+
review_id: 'rev_hooks',
|
|
259
|
+
file: 'agent-bridge/server.js',
|
|
260
|
+
requested_by: 'alpha',
|
|
261
|
+
status: 'pending',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
canonicalState.appendCanonicalEvent({
|
|
265
|
+
type: 'review.submitted',
|
|
266
|
+
branchId: branchName,
|
|
267
|
+
actorAgent: 'beta',
|
|
268
|
+
sessionId: 'session_beta',
|
|
269
|
+
causationId: reviewRequested.event_id,
|
|
270
|
+
correlationId: 'rev_hooks',
|
|
271
|
+
payload: {
|
|
272
|
+
review_id: 'rev_hooks',
|
|
273
|
+
file: 'agent-bridge/server.js',
|
|
274
|
+
reviewer: 'beta',
|
|
275
|
+
requested_by: 'alpha',
|
|
276
|
+
status: 'approved',
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const dependencyDeclared = canonicalState.appendCanonicalEvent({
|
|
280
|
+
type: 'dependency.declared',
|
|
281
|
+
branchId: branchName,
|
|
282
|
+
actorAgent: 'alpha',
|
|
283
|
+
sessionId: 'session_alpha',
|
|
284
|
+
correlationId: 'dep_hooks',
|
|
285
|
+
payload: {
|
|
286
|
+
dependency_id: 'dep_hooks',
|
|
287
|
+
task_id: task.id,
|
|
288
|
+
depends_on: 'task_root',
|
|
289
|
+
declared_by: 'alpha',
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
canonicalState.appendCanonicalEvent({
|
|
293
|
+
type: 'dependency.resolved',
|
|
294
|
+
branchId: branchName,
|
|
295
|
+
actorAgent: 'alpha',
|
|
296
|
+
sessionId: 'session_alpha',
|
|
297
|
+
causationId: dependencyDeclared.event_id,
|
|
298
|
+
correlationId: 'dep_hooks',
|
|
299
|
+
payload: {
|
|
300
|
+
dependency_id: 'dep_hooks',
|
|
301
|
+
task_id: task.id,
|
|
302
|
+
depends_on: 'task_root',
|
|
303
|
+
resolved_by_task_id: 'task_root',
|
|
304
|
+
reason: 'fixture_resolution',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const workspaceWrite = canonicalState.saveWorkspace('alpha', {
|
|
309
|
+
note: {
|
|
310
|
+
content: 'Workspace event fixture',
|
|
311
|
+
updated_at: '2026-04-16T18:02:00.000Z',
|
|
312
|
+
},
|
|
313
|
+
}, {
|
|
314
|
+
actor: 'alpha',
|
|
315
|
+
branch: branchName,
|
|
316
|
+
sessionId: 'session_alpha',
|
|
317
|
+
key: 'note',
|
|
318
|
+
keys: ['note'],
|
|
319
|
+
correlationId: 'workspace_hooks',
|
|
320
|
+
updatedAt: '2026-04-16T18:02:00.000Z',
|
|
321
|
+
});
|
|
322
|
+
const loggedDecision = canonicalState.logDecision({
|
|
323
|
+
entry: {
|
|
324
|
+
id: 'dec_hooks',
|
|
325
|
+
decision: 'Route governance writes through canonical helpers',
|
|
326
|
+
topic: 'architecture',
|
|
327
|
+
decided_by: 'alpha',
|
|
328
|
+
decided_at: '2026-04-16T18:02:10.000Z',
|
|
329
|
+
},
|
|
330
|
+
actor: 'alpha',
|
|
331
|
+
branch: branchName,
|
|
332
|
+
sessionId: 'session_alpha',
|
|
333
|
+
correlationId: 'dec_hooks',
|
|
334
|
+
});
|
|
335
|
+
const kbWrite = canonicalState.writeKnowledgeBaseEntry({
|
|
336
|
+
key: 'lesson_hooks',
|
|
337
|
+
value: {
|
|
338
|
+
content: 'Canonical governance writes emit real events now.',
|
|
339
|
+
updated_by: 'alpha',
|
|
340
|
+
updated_at: '2026-04-16T18:02:20.000Z',
|
|
341
|
+
},
|
|
342
|
+
actor: 'alpha',
|
|
343
|
+
branch: branchName,
|
|
344
|
+
sessionId: 'session_alpha',
|
|
345
|
+
correlationId: 'lesson_hooks',
|
|
346
|
+
maxEntries: 100,
|
|
347
|
+
});
|
|
348
|
+
const progressUpdate = canonicalState.updateProgressRecord({
|
|
349
|
+
feature: 'governance-hooks',
|
|
350
|
+
value: {
|
|
351
|
+
percent: 42,
|
|
352
|
+
notes: 'Hook fixture update',
|
|
353
|
+
updated_by: 'alpha',
|
|
354
|
+
updated_at: '2026-04-16T18:02:30.000Z',
|
|
355
|
+
},
|
|
356
|
+
actor: 'alpha',
|
|
357
|
+
branch: branchName,
|
|
358
|
+
sessionId: 'session_alpha',
|
|
359
|
+
correlationId: 'governance-hooks',
|
|
360
|
+
});
|
|
361
|
+
const createdVote = canonicalState.createVote({
|
|
362
|
+
vote: {
|
|
363
|
+
id: 'vote_hooks',
|
|
364
|
+
question: 'Use canonical governance helpers?',
|
|
365
|
+
options: ['yes', 'no'],
|
|
366
|
+
votes: {},
|
|
367
|
+
status: 'open',
|
|
368
|
+
created_by: 'alpha',
|
|
369
|
+
created_at: '2026-04-16T18:02:40.000Z',
|
|
370
|
+
},
|
|
371
|
+
actor: 'alpha',
|
|
372
|
+
branch: branchName,
|
|
373
|
+
sessionId: 'session_alpha',
|
|
374
|
+
correlationId: 'vote_hooks',
|
|
375
|
+
});
|
|
376
|
+
const castVoteAlpha = canonicalState.castVote({
|
|
377
|
+
voteId: 'vote_hooks',
|
|
378
|
+
voter: 'alpha',
|
|
379
|
+
choice: 'yes',
|
|
380
|
+
actor: 'alpha',
|
|
381
|
+
branch: branchName,
|
|
382
|
+
sessionId: 'session_alpha',
|
|
383
|
+
correlationId: 'vote_hooks',
|
|
384
|
+
onlineAgents: ['alpha', 'beta'],
|
|
385
|
+
});
|
|
386
|
+
const castVoteBeta = canonicalState.castVote({
|
|
387
|
+
voteId: 'vote_hooks',
|
|
388
|
+
voter: 'beta',
|
|
389
|
+
choice: 'yes',
|
|
390
|
+
actor: 'beta',
|
|
391
|
+
branch: branchName,
|
|
392
|
+
sessionId: 'session_beta',
|
|
393
|
+
correlationId: 'vote_hooks',
|
|
394
|
+
onlineAgents: ['alpha', 'beta'],
|
|
395
|
+
});
|
|
396
|
+
const addedRule = canonicalState.addRule({
|
|
397
|
+
rule: {
|
|
398
|
+
id: 'rule_hooks',
|
|
399
|
+
text: 'Use canonical state helpers',
|
|
400
|
+
category: 'workflow',
|
|
401
|
+
priority: 'high',
|
|
402
|
+
created_by: 'alpha',
|
|
403
|
+
created_at: '2026-04-16T18:02:50.000Z',
|
|
404
|
+
active: true,
|
|
405
|
+
},
|
|
406
|
+
actor: 'alpha',
|
|
407
|
+
branch: branchName,
|
|
408
|
+
sessionId: 'session_alpha',
|
|
409
|
+
correlationId: 'rule_hooks',
|
|
410
|
+
});
|
|
411
|
+
const toggledRule = canonicalState.toggleRule({
|
|
412
|
+
ruleId: 'rule_hooks',
|
|
413
|
+
actor: 'alpha',
|
|
414
|
+
branch: branchName,
|
|
415
|
+
sessionId: 'session_alpha',
|
|
416
|
+
correlationId: 'rule_hooks',
|
|
417
|
+
});
|
|
418
|
+
const removedRule = canonicalState.removeRule({
|
|
419
|
+
ruleId: 'rule_hooks',
|
|
420
|
+
actor: 'alpha',
|
|
421
|
+
branch: branchName,
|
|
422
|
+
sessionId: 'session_alpha',
|
|
423
|
+
correlationId: 'rule_hooks',
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
assert(workspaceWrite && workspaceWrite.event && workspaceWrite.event.type === 'workspace.written', 'Fixture workspace write should emit workspace.written.', problems);
|
|
427
|
+
assert(loggedDecision && loggedDecision.success, 'Fixture decision log should succeed.', problems);
|
|
428
|
+
assert(kbWrite && kbWrite.success, 'Fixture KB write should succeed.', problems);
|
|
429
|
+
assert(progressUpdate && progressUpdate.success, 'Fixture progress update should succeed.', problems);
|
|
430
|
+
assert(createdVote && createdVote.success, 'Fixture vote creation should succeed.', problems);
|
|
431
|
+
assert(castVoteAlpha && castVoteAlpha.success, 'Fixture first vote cast should succeed.', problems);
|
|
432
|
+
assert(castVoteBeta && castVoteBeta.success, 'Fixture second vote cast should succeed.', problems);
|
|
433
|
+
assert(addedRule && addedRule.success, 'Fixture rule add should succeed.', problems);
|
|
434
|
+
assert(toggledRule && toggledRule.success, 'Fixture rule toggle should succeed.', problems);
|
|
435
|
+
assert(removedRule && removedRule.success, 'Fixture rule removal should succeed.', problems);
|
|
436
|
+
|
|
437
|
+
canonicalState.unregisterApiAgent('alpha', { sessionId: 'session_alpha', reason: 'api_unregister' });
|
|
438
|
+
|
|
439
|
+
const runtimeEvents = eventLog.readEvents({ stream: 'runtime' });
|
|
440
|
+
const branchEvents = eventLog.readBranchEvents(branchName);
|
|
441
|
+
const runtimeHooks = canonicalState.readRuntimeHooks();
|
|
442
|
+
const branchHooks = canonicalState.readBranchHooks(branchName);
|
|
443
|
+
|
|
444
|
+
const runtimeEventTypes = runtimeEvents.map((event) => event.type);
|
|
445
|
+
const branchEventTypes = branchEvents.map((event) => event.type);
|
|
446
|
+
|
|
447
|
+
assert(runtimeEventTypes.includes('agent.registered'), 'Runtime canonical events must include agent.registered.', problems);
|
|
448
|
+
assert(runtimeEventTypes.includes('agent.status_updated'), 'Runtime canonical events must include agent.status_updated.', problems);
|
|
449
|
+
assert(runtimeEventTypes.includes('agent.heartbeat_recorded'), 'Runtime canonical events must include agent.heartbeat_recorded.', problems);
|
|
450
|
+
assert(runtimeEventTypes.includes('agent.branch_assigned'), 'Runtime canonical events must include agent.branch_assigned.', problems);
|
|
451
|
+
assert(runtimeEventTypes.includes('agent.listening_updated'), 'Runtime canonical events must include agent.listening_updated.', problems);
|
|
452
|
+
assert(runtimeEventTypes.includes('agent.unregistered'), 'Runtime canonical events must include agent.unregistered.', problems);
|
|
453
|
+
|
|
454
|
+
assert(branchEventTypes.includes('task.created'), 'Branch canonical events must include task.created.', problems);
|
|
455
|
+
assert(branchEventTypes.includes('task.claimed'), 'Branch canonical events must include task.claimed.', problems);
|
|
456
|
+
assert(branchEventTypes.includes('workflow.created'), 'Branch canonical events must include workflow.created.', problems);
|
|
457
|
+
assert(branchEventTypes.includes('workflow.step_started'), 'Branch canonical events must include workflow.step_started.', problems);
|
|
458
|
+
assert(branchEventTypes.includes('workflow.step_reassigned'), 'Branch canonical events must include workflow.step_reassigned.', problems);
|
|
459
|
+
assert(branchEventTypes.includes('workflow.paused'), 'Branch canonical events must include workflow.paused.', problems);
|
|
460
|
+
assert(branchEventTypes.includes('workflow.resumed'), 'Branch canonical events must include workflow.resumed.', problems);
|
|
461
|
+
assert(branchEventTypes.includes('workflow.stopped'), 'Branch canonical events must include workflow.stopped.', problems);
|
|
462
|
+
assert(branchEventTypes.includes('conversation.mode_updated'), 'Branch canonical events must include conversation.mode_updated.', problems);
|
|
463
|
+
assert(branchEventTypes.includes('workspace.written'), 'Branch canonical events must include workspace.written.', problems);
|
|
464
|
+
assert(branchEventTypes.includes('decision.logged'), 'Branch canonical events must include decision.logged.', problems);
|
|
465
|
+
assert(branchEventTypes.includes('kb.written'), 'Branch canonical events must include kb.written.', problems);
|
|
466
|
+
assert(branchEventTypes.includes('progress.updated'), 'Branch canonical events must include progress.updated.', problems);
|
|
467
|
+
assert(branchEventTypes.includes('vote.called'), 'Branch canonical events must include vote.called.', problems);
|
|
468
|
+
assert(branchEventTypes.includes('vote.cast'), 'Branch canonical events must include vote.cast.', problems);
|
|
469
|
+
assert(branchEventTypes.includes('vote.resolved'), 'Branch canonical events must include vote.resolved.', problems);
|
|
470
|
+
assert(branchEventTypes.includes('rule.added'), 'Branch canonical events must include rule.added.', problems);
|
|
471
|
+
assert(branchEventTypes.includes('rule.toggled'), 'Branch canonical events must include rule.toggled.', problems);
|
|
472
|
+
assert(branchEventTypes.includes('rule.removed'), 'Branch canonical events must include rule.removed.', problems);
|
|
473
|
+
assert(branchEventTypes.includes('review.requested'), 'Branch canonical events must include review.requested.', problems);
|
|
474
|
+
assert(branchEventTypes.includes('review.submitted'), 'Branch canonical events must include review.submitted.', problems);
|
|
475
|
+
assert(branchEventTypes.includes('dependency.declared'), 'Branch canonical events must include dependency.declared.', problems);
|
|
476
|
+
assert(branchEventTypes.includes('dependency.resolved'), 'Branch canonical events must include dependency.resolved.', problems);
|
|
477
|
+
|
|
478
|
+
assert(runtimeHooks.length === runtimeEvents.length, 'Runtime hook projection must mirror the runtime canonical event count.', problems);
|
|
479
|
+
assert(branchHooks.length === branchEvents.length, 'Branch hook projection must mirror the branch canonical event count.', problems);
|
|
480
|
+
assert(runtimeHooks.every((hook) => hook.topic && hook.event_id && hook.payload), 'Runtime hooks must expose topic, event_id, and payload.', problems);
|
|
481
|
+
assert(branchHooks.every((hook) => hook.topic && hook.event_id && hook.payload), 'Branch hooks must expose topic, event_id, and payload.', problems);
|
|
482
|
+
|
|
483
|
+
assertHookTopics(runtimeEvents, runtimeHooks, 'agent.registered', problems, 'Runtime');
|
|
484
|
+
assertHookTopics(runtimeEvents, runtimeHooks, 'agent.heartbeat_recorded', problems, 'Runtime');
|
|
485
|
+
assertHookTopics(runtimeEvents, runtimeHooks, 'agent.listening_updated', problems, 'Runtime');
|
|
486
|
+
assertHookTopics(branchEvents, branchHooks, 'task.created', problems, 'Branch');
|
|
487
|
+
assertHookTopics(branchEvents, branchHooks, 'workflow.step_started', problems, 'Branch');
|
|
488
|
+
assertHookTopics(branchEvents, branchHooks, 'workspace.written', problems, 'Branch');
|
|
489
|
+
assertHookTopics(branchEvents, branchHooks, 'decision.logged', problems, 'Branch');
|
|
490
|
+
assertHookTopics(branchEvents, branchHooks, 'kb.written', problems, 'Branch');
|
|
491
|
+
assertHookTopics(branchEvents, branchHooks, 'progress.updated', problems, 'Branch');
|
|
492
|
+
assertHookTopics(branchEvents, branchHooks, 'vote.called', problems, 'Branch');
|
|
493
|
+
assertHookTopics(branchEvents, branchHooks, 'vote.cast', problems, 'Branch');
|
|
494
|
+
assertHookTopics(branchEvents, branchHooks, 'vote.resolved', problems, 'Branch');
|
|
495
|
+
assertHookTopics(branchEvents, branchHooks, 'rule.added', problems, 'Branch');
|
|
496
|
+
assertHookTopics(branchEvents, branchHooks, 'rule.toggled', problems, 'Branch');
|
|
497
|
+
assertHookTopics(branchEvents, branchHooks, 'rule.removed', problems, 'Branch');
|
|
498
|
+
assertHookTopics(branchEvents, branchHooks, 'review.requested', problems, 'Branch');
|
|
499
|
+
assertHookTopics(branchEvents, branchHooks, 'dependency.resolved', problems, 'Branch');
|
|
500
|
+
assertHookTopics(branchEvents, branchHooks, 'conversation.mode_updated', problems, 'Branch');
|
|
501
|
+
|
|
502
|
+
const managedHooks = canonicalState.readBranchHooks(branchName, { topic: 'conversation.mode_updated' });
|
|
503
|
+
const reviewHooks = canonicalState.readBranchHooks(branchName, { topics: ['review.requested', 'review.submitted'] });
|
|
504
|
+
const heartbeatHooks = canonicalState.readRuntimeHooks({ topic: 'agent.heartbeat_recorded' });
|
|
505
|
+
|
|
506
|
+
assert(managedHooks.length === 1 && managedHooks[0].topic === 'conversation.mode_updated', 'Hook subscription surface must filter by one canonical topic.', problems);
|
|
507
|
+
assert(reviewHooks.length === 2 && reviewHooks.every((hook) => hook.topic.startsWith('review.')), 'Hook subscription surface must filter by canonical topic sets.', problems);
|
|
508
|
+
assert(heartbeatHooks.length === 1 && heartbeatHooks[0].topic === 'agent.heartbeat_recorded', 'Runtime hook subscription surface must filter by canonical agent topic.', problems);
|
|
509
|
+
assert(modeEvent && managedHooks[0].event_id === modeEvent.event_id, 'Branch hook records must retain the originating canonical event id.', problems);
|
|
510
|
+
} finally {
|
|
511
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (problems.length > 0) {
|
|
515
|
+
fail(['Lifecycle hook validation failed.', ...problems.map((problem) => `- ${problem}`)], 1);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log([
|
|
519
|
+
'Lifecycle hook validation passed.',
|
|
520
|
+
'Validated runtime, governance, and branch lifecycle seams now emit canonical events for the targeted slices.',
|
|
521
|
+
'Validated the derived hook surface is projected post-commit from canonical events and remains keyed by canonical event type.',
|
|
522
|
+
].join('\n'));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
main();
|