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,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { DEFAULT_MARKDOWN_WORKSPACE_DIR_NAME } = require(path.resolve(__dirname, '..', 'data-dir.js'));
|
|
8
|
+
const { createCanonicalState, createBranchPathResolvers } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
9
|
+
const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
|
|
10
|
+
const { createStateIo } = require(path.resolve(__dirname, '..', 'state', 'io.js'));
|
|
11
|
+
const { createSessionsState } = require(path.resolve(__dirname, '..', 'state', 'sessions.js'));
|
|
12
|
+
|
|
13
|
+
const DASHBOARD_SOURCE = path.resolve(__dirname, '..', 'dashboard.js');
|
|
14
|
+
const CANONICAL_SOURCE = path.resolve(__dirname, '..', 'state', 'canonical.js');
|
|
15
|
+
const MARKDOWN_SOURCE = path.resolve(__dirname, '..', 'state', 'markdown-workspace.js');
|
|
16
|
+
|
|
17
|
+
function fail(lines, exitCode = 1) {
|
|
18
|
+
fs.writeSync(2, lines.join('\n') + '\n');
|
|
19
|
+
process.exit(exitCode);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assert(condition, message, problems) {
|
|
23
|
+
if (!condition) problems.push(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeJson(filePath, value) {
|
|
27
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
28
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeJsonl(filePath, entries) {
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
const lines = entries.map((entry) => JSON.stringify(entry));
|
|
34
|
+
fs.writeFileSync(filePath, lines.length > 0 ? `${lines.join('\n')}\n` : '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function captureSnapshot(canonicalState, eventLog) {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
branches: canonicalState.listMarkdownBranches(),
|
|
40
|
+
branch_sequences: {
|
|
41
|
+
main: canonicalState.getBranchEventSequence('main'),
|
|
42
|
+
feature_docs: canonicalState.getBranchEventSequence('feature_docs'),
|
|
43
|
+
},
|
|
44
|
+
events: {
|
|
45
|
+
main: eventLog.readBranchEvents('main'),
|
|
46
|
+
feature_docs: eventLog.readBranchEvents('feature_docs'),
|
|
47
|
+
},
|
|
48
|
+
conversations: {
|
|
49
|
+
main: canonicalState.getConversationMessages({ branch: 'main' }),
|
|
50
|
+
feature_docs: canonicalState.getConversationMessages({ branch: 'feature_docs' }),
|
|
51
|
+
},
|
|
52
|
+
channels: {
|
|
53
|
+
main: canonicalState.getChannelsView({ branch: 'main' }),
|
|
54
|
+
feature_docs: canonicalState.getChannelsView({ branch: 'feature_docs' }),
|
|
55
|
+
},
|
|
56
|
+
sessions: {
|
|
57
|
+
main: canonicalState.listBranchSessions('main'),
|
|
58
|
+
feature_docs: canonicalState.listBranchSessions('feature_docs'),
|
|
59
|
+
},
|
|
60
|
+
evidence: {
|
|
61
|
+
main: canonicalState.readEvidence('main'),
|
|
62
|
+
feature_docs: canonicalState.readEvidence('feature_docs'),
|
|
63
|
+
},
|
|
64
|
+
decisions: {
|
|
65
|
+
main: canonicalState.listDecisions({ branch: 'main' }),
|
|
66
|
+
feature_docs: canonicalState.listDecisions({ branch: 'feature_docs' }),
|
|
67
|
+
},
|
|
68
|
+
workspaces: {
|
|
69
|
+
main: canonicalState.listWorkspaces({ branch: 'main' }),
|
|
70
|
+
feature_docs: canonicalState.listWorkspaces({ branch: 'feature_docs' }),
|
|
71
|
+
},
|
|
72
|
+
}, null, 2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function expectExportFailure(params) {
|
|
76
|
+
const {
|
|
77
|
+
canonicalState,
|
|
78
|
+
projectRoot,
|
|
79
|
+
outputRoot,
|
|
80
|
+
expectedMessage,
|
|
81
|
+
label,
|
|
82
|
+
problems,
|
|
83
|
+
} = params;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
canonicalState.exportMarkdownWorkspace({ projectRoot, outputRoot });
|
|
87
|
+
problems.push(`${label} should be rejected by the markdown safety guard.`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message = error && error.message ? error.message : String(error);
|
|
90
|
+
if (!message.includes(expectedMessage)) {
|
|
91
|
+
problems.push(`${label} should fail with message containing "${expectedMessage}", got: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function main() {
|
|
97
|
+
const problems = [];
|
|
98
|
+
const dashboardSource = fs.readFileSync(DASHBOARD_SOURCE, 'utf8');
|
|
99
|
+
const canonicalSource = fs.readFileSync(CANONICAL_SOURCE, 'utf8');
|
|
100
|
+
const markdownSource = fs.readFileSync(MARKDOWN_SOURCE, 'utf8');
|
|
101
|
+
|
|
102
|
+
assert(
|
|
103
|
+
dashboardSource.includes('entry.name === DEFAULT_MARKDOWN_WORKSPACE_DIR_NAME'),
|
|
104
|
+
'Dashboard project discovery should explicitly skip the markdown workspace directory.',
|
|
105
|
+
problems
|
|
106
|
+
);
|
|
107
|
+
assert(
|
|
108
|
+
canonicalSource.includes('runtimeDataDir: dataDir'),
|
|
109
|
+
'Canonical markdown export should pass the runtime data dir into the markdown safety guard.',
|
|
110
|
+
problems
|
|
111
|
+
);
|
|
112
|
+
assert(
|
|
113
|
+
markdownSource.includes('Markdown workspace output root must stay outside the canonical runtime data directory.'),
|
|
114
|
+
'Markdown export should reject output roots inside the canonical runtime data directory.',
|
|
115
|
+
problems
|
|
116
|
+
);
|
|
117
|
+
assert(
|
|
118
|
+
markdownSource.includes('Markdown workspace output root must not contain the canonical runtime data directory.'),
|
|
119
|
+
'Markdown export should reject output roots that would contain the canonical runtime data directory.',
|
|
120
|
+
problems
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const tempBase = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-markdown-safety-'));
|
|
124
|
+
const projectRoot = path.join(tempBase, 'project');
|
|
125
|
+
const dataDir = path.join(projectRoot, '.agent-bridge');
|
|
126
|
+
const outputRoot = path.join(projectRoot, DEFAULT_MARKDOWN_WORKSPACE_DIR_NAME);
|
|
127
|
+
const branchPaths = createBranchPathResolvers(dataDir);
|
|
128
|
+
const io = createStateIo({ dataDir });
|
|
129
|
+
const canonicalEventLog = createCanonicalEventLog({ dataDir });
|
|
130
|
+
const sessionsState = createSessionsState({ io, branchPaths, canonicalEventLog });
|
|
131
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: process.pid });
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
135
|
+
|
|
136
|
+
writeJson(path.join(dataDir, 'branches.json'), {
|
|
137
|
+
main: {
|
|
138
|
+
created_at: '2026-04-16T16:00:00.000Z',
|
|
139
|
+
created_by: 'alpha',
|
|
140
|
+
message_count: 2,
|
|
141
|
+
},
|
|
142
|
+
feature_docs: {
|
|
143
|
+
created_at: '2026-04-16T16:00:30.000Z',
|
|
144
|
+
created_by: 'beta',
|
|
145
|
+
forked_from: 'main',
|
|
146
|
+
fork_point: 'msg_main_1',
|
|
147
|
+
message_count: 1,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
canonicalState.appendMessage({
|
|
152
|
+
id: 'msg_main_1',
|
|
153
|
+
from: 'alpha',
|
|
154
|
+
to: 'beta',
|
|
155
|
+
content: 'Main branch hello',
|
|
156
|
+
timestamp: '2026-04-16T16:01:00.000Z',
|
|
157
|
+
}, { branch: 'main', actorAgent: 'alpha', sessionId: 'session_alpha_main' });
|
|
158
|
+
|
|
159
|
+
canonicalState.appendMessage({
|
|
160
|
+
id: 'msg_feature_1',
|
|
161
|
+
from: 'beta',
|
|
162
|
+
to: 'alpha',
|
|
163
|
+
content: 'Feature branch hello',
|
|
164
|
+
timestamp: '2026-04-16T16:02:00.000Z',
|
|
165
|
+
}, { branch: 'feature_docs', actorAgent: 'beta', sessionId: 'session_beta_feature' });
|
|
166
|
+
|
|
167
|
+
writeJson(branchPaths.getChannelsFile('main'), {
|
|
168
|
+
general: { description: 'General channel', members: ['*'] },
|
|
169
|
+
ops: { description: 'Ops sync', members: ['alpha'] },
|
|
170
|
+
});
|
|
171
|
+
writeJson(branchPaths.getChannelsFile('feature_docs'), {
|
|
172
|
+
general: { description: 'General channel', members: ['*'] },
|
|
173
|
+
dev: { description: 'Feature development', members: ['alpha', 'beta'] },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
writeJsonl(branchPaths.getChannelHistoryFile('ops', 'main'), [{
|
|
177
|
+
id: 'msg_main_ops_1',
|
|
178
|
+
from: 'alpha',
|
|
179
|
+
to: 'alpha',
|
|
180
|
+
channel: 'ops',
|
|
181
|
+
content: 'Ops-only update',
|
|
182
|
+
timestamp: '2026-04-16T16:01:30.000Z',
|
|
183
|
+
}]);
|
|
184
|
+
writeJsonl(branchPaths.getChannelHistoryFile('dev', 'feature_docs'), [{
|
|
185
|
+
id: 'msg_feature_dev_1',
|
|
186
|
+
from: 'beta',
|
|
187
|
+
to: 'alpha',
|
|
188
|
+
channel: 'dev',
|
|
189
|
+
content: 'Feature dev note',
|
|
190
|
+
timestamp: '2026-04-16T16:02:30.000Z',
|
|
191
|
+
}]);
|
|
192
|
+
|
|
193
|
+
writeJson(branchPaths.getAcksFile('main'), {
|
|
194
|
+
msg_main_1: true,
|
|
195
|
+
msg_main_ops_1: false,
|
|
196
|
+
});
|
|
197
|
+
writeJson(branchPaths.getAcksFile('feature_docs'), {
|
|
198
|
+
msg_feature_1: true,
|
|
199
|
+
msg_feature_dev_1: false,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const mainSession = sessionsState.activateSession({
|
|
203
|
+
agentName: 'alpha',
|
|
204
|
+
branchName: 'main',
|
|
205
|
+
provider: 'claude',
|
|
206
|
+
reason: 'register',
|
|
207
|
+
at: '2026-04-16T16:03:00.000Z',
|
|
208
|
+
});
|
|
209
|
+
sessionsState.transitionSession({
|
|
210
|
+
sessionId: mainSession.session.session_id,
|
|
211
|
+
branchName: 'main',
|
|
212
|
+
state: 'completed',
|
|
213
|
+
reason: 'graceful_exit',
|
|
214
|
+
at: '2026-04-16T16:03:30.000Z',
|
|
215
|
+
});
|
|
216
|
+
const featureSession = sessionsState.activateSession({
|
|
217
|
+
agentName: 'beta',
|
|
218
|
+
branchName: 'feature_docs',
|
|
219
|
+
provider: 'codex',
|
|
220
|
+
reason: 'register',
|
|
221
|
+
at: '2026-04-16T16:04:00.000Z',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
writeJson(branchPaths.getEvidenceFile('main'), {
|
|
225
|
+
schema_version: 1,
|
|
226
|
+
updated_at: '2026-04-16T16:05:00.000Z',
|
|
227
|
+
records: [{
|
|
228
|
+
evidence_id: 'evidence_main_1',
|
|
229
|
+
branch_id: 'main',
|
|
230
|
+
subject_kind: 'task',
|
|
231
|
+
task_id: 'task_main',
|
|
232
|
+
summary: 'Validated the main branch fixture',
|
|
233
|
+
verification: 'Ran the markdown safety fixture.',
|
|
234
|
+
files_changed: ['agent-bridge/state/markdown-workspace.js'],
|
|
235
|
+
confidence: 93,
|
|
236
|
+
learnings: 'Markdown stays projection-only.',
|
|
237
|
+
recorded_at: '2026-04-16T16:05:00.000Z',
|
|
238
|
+
recorded_by: 'alpha',
|
|
239
|
+
recorded_by_session: mainSession.session.session_id,
|
|
240
|
+
source_tool: 'fixture',
|
|
241
|
+
}],
|
|
242
|
+
});
|
|
243
|
+
writeJson(branchPaths.getEvidenceFile('feature_docs'), {
|
|
244
|
+
schema_version: 1,
|
|
245
|
+
updated_at: '2026-04-16T16:05:30.000Z',
|
|
246
|
+
records: [{
|
|
247
|
+
evidence_id: 'evidence_feature_1',
|
|
248
|
+
branch_id: 'feature_docs',
|
|
249
|
+
subject_kind: 'workflow',
|
|
250
|
+
workflow_id: 'wf_feature',
|
|
251
|
+
summary: 'Validated the feature branch fixture',
|
|
252
|
+
verification: 'Confirmed markdown edits stay non-authoritative.',
|
|
253
|
+
files_changed: ['agent-bridge/scripts/check-markdown-workspace-safety.js'],
|
|
254
|
+
confidence: 89,
|
|
255
|
+
learnings: 'Keep markdown outside runtime storage.',
|
|
256
|
+
recorded_at: '2026-04-16T16:05:30.000Z',
|
|
257
|
+
recorded_by: 'beta',
|
|
258
|
+
recorded_by_session: featureSession.session.session_id,
|
|
259
|
+
source_tool: 'fixture',
|
|
260
|
+
}],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
writeJson(branchPaths.getDecisionsFile('main'), [{
|
|
264
|
+
id: 'dec_markdown_1',
|
|
265
|
+
decision: 'Keep markdown export projection-only.',
|
|
266
|
+
topic: 'markdown',
|
|
267
|
+
decided_at: '2026-04-16T16:06:00.000Z',
|
|
268
|
+
}]);
|
|
269
|
+
writeJson(branchPaths.getDecisionsFile('feature_docs'), [{
|
|
270
|
+
id: 'dec_feature_1',
|
|
271
|
+
decision: 'Keep feature export isolated.',
|
|
272
|
+
topic: 'branching',
|
|
273
|
+
decided_at: '2026-04-16T16:06:10.000Z',
|
|
274
|
+
}]);
|
|
275
|
+
writeJson(branchPaths.getWorkspaceFile('alpha', 'main'), {
|
|
276
|
+
draft: { content: 'Summarize export findings.' },
|
|
277
|
+
retry_history: [],
|
|
278
|
+
});
|
|
279
|
+
writeJson(branchPaths.getWorkspaceFile('beta', 'main'), {
|
|
280
|
+
notes: 'Main branch workspace note',
|
|
281
|
+
});
|
|
282
|
+
writeJson(branchPaths.getWorkspaceFile('alpha', 'feature_docs'), {
|
|
283
|
+
notes: 'Feature branch alpha workspace note',
|
|
284
|
+
});
|
|
285
|
+
writeJson(branchPaths.getWorkspaceFile('beta', 'feature_docs'), {
|
|
286
|
+
notes: 'Feature branch workspace note',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const snapshotBeforeExport = captureSnapshot(canonicalState, canonicalEventLog);
|
|
290
|
+
const exportResult = canonicalState.exportMarkdownWorkspace({ projectRoot, outputRoot });
|
|
291
|
+
const snapshotAfterExport = captureSnapshot(canonicalState, canonicalEventLog);
|
|
292
|
+
|
|
293
|
+
assert(exportResult && exportResult.success === true, 'Markdown workspace export should succeed in the safety fixture.', problems);
|
|
294
|
+
assert(snapshotAfterExport === snapshotBeforeExport, 'Exporting the markdown workspace must not mutate canonical runtime state.', problems);
|
|
295
|
+
|
|
296
|
+
fs.appendFileSync(path.join(outputRoot, 'README.md'), '\n\nManual edits should stay non-authoritative.\n');
|
|
297
|
+
fs.writeFileSync(
|
|
298
|
+
path.join(outputRoot, 'branches', 'main', 'conversations', 'channels', 'general.md'),
|
|
299
|
+
'---\nauthoritative: true\nbranch: "main"\n---\nThis markdown was edited manually and must not change runtime state.\n'
|
|
300
|
+
);
|
|
301
|
+
fs.mkdirSync(path.join(outputRoot, 'branches', 'rogue'), { recursive: true });
|
|
302
|
+
fs.writeFileSync(path.join(outputRoot, 'branches', 'rogue', 'metadata.md'), '# Rogue markdown branch\n');
|
|
303
|
+
|
|
304
|
+
const snapshotAfterMarkdownEdits = captureSnapshot(canonicalState, canonicalEventLog);
|
|
305
|
+
assert(snapshotAfterMarkdownEdits === snapshotBeforeExport, 'Editing exported markdown files must not alter canonical runtime projections or event streams.', problems);
|
|
306
|
+
assert(!canonicalState.listMarkdownBranches().some((entry) => entry.branch === 'rogue'), 'Markdown-only branch folders must not appear in the canonical branch registry.', problems);
|
|
307
|
+
|
|
308
|
+
expectExportFailure({
|
|
309
|
+
canonicalState,
|
|
310
|
+
projectRoot,
|
|
311
|
+
outputRoot: projectRoot,
|
|
312
|
+
expectedMessage: 'must not be the project root',
|
|
313
|
+
label: 'Project-root markdown export',
|
|
314
|
+
problems,
|
|
315
|
+
});
|
|
316
|
+
expectExportFailure({
|
|
317
|
+
canonicalState,
|
|
318
|
+
projectRoot,
|
|
319
|
+
outputRoot: tempBase,
|
|
320
|
+
expectedMessage: 'must not contain the project root',
|
|
321
|
+
label: 'Ancestor markdown export',
|
|
322
|
+
problems,
|
|
323
|
+
});
|
|
324
|
+
expectExportFailure({
|
|
325
|
+
canonicalState,
|
|
326
|
+
projectRoot,
|
|
327
|
+
outputRoot: path.join(dataDir, DEFAULT_MARKDOWN_WORKSPACE_DIR_NAME),
|
|
328
|
+
expectedMessage: 'must stay outside the canonical runtime data directory',
|
|
329
|
+
label: 'Data-dir markdown export',
|
|
330
|
+
problems,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (problems.length > 0) {
|
|
334
|
+
fail(['Markdown workspace safety validation failed.', ...problems], 1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
console.log([
|
|
338
|
+
'Markdown workspace safety validation passed.',
|
|
339
|
+
'Validated explicit markdown discovery/output guards plus fixture-backed proof that markdown export and manual markdown edits do not change canonical runtime state.',
|
|
340
|
+
`Output root: ${outputRoot}`,
|
|
341
|
+
].join('\n'));
|
|
342
|
+
} finally {
|
|
343
|
+
fs.rmSync(tempBase, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
main();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DOC_PATH = path.resolve(__dirname, '..', '..', 'docs', 'architecture', 'markdown-workspace.md');
|
|
7
|
+
const DOC_DISPLAY_PATH = 'docs/architecture/markdown-workspace.md';
|
|
8
|
+
const USAGE = 'Usage: node agent-bridge/scripts/check-markdown-workspace.js [--simulate-missing <key>]';
|
|
9
|
+
|
|
10
|
+
const REQUIRED_HEADINGS = [
|
|
11
|
+
{ key: 'non_authoritative_authority_model', heading: '## Non-authoritative authority model' },
|
|
12
|
+
{ key: 'workspace_root_layout', heading: '## Workspace root / layout' },
|
|
13
|
+
{ key: 'required_generated_frontmatter', heading: '## Required generated frontmatter' },
|
|
14
|
+
{ key: 'common_frontmatter', heading: '### Common frontmatter' },
|
|
15
|
+
{ key: 'kind_specific_frontmatter', heading: '### Kind-specific frontmatter' },
|
|
16
|
+
{ key: 'export_source_mapping', heading: '## Export-source mapping' },
|
|
17
|
+
{ key: 'branch_compatibility_safety_rules', heading: '## Branch and compatibility safety rules' },
|
|
18
|
+
{ key: 'task_9b_implementation_seam', heading: '## Task 9B implementation seam' },
|
|
19
|
+
{ key: 'validation_path', heading: '## Validation path' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const REQUIRED_SNIPPETS = [
|
|
23
|
+
{ key: 'non_authoritative_rule', snippet: 'Markdown files are rebuilt from canonical and legacy-compat read models and never treated as runtime inputs.' },
|
|
24
|
+
{ key: 'frontmatter_authority_rule', snippet: 'Every exported markdown file MUST set `authoritative: false` in frontmatter.' },
|
|
25
|
+
{ key: 'source_sequence_frontmatter', snippet: 'source_sequence: <stream-seq-or-null>' },
|
|
26
|
+
{ key: 'no_write_back_rule', snippet: 'Task 9B MUST NOT implement write-back or live sync loops.' },
|
|
27
|
+
{ key: 'explicit_import_rule', snippet: 'Any future import must be explicit and broker-mediated.' },
|
|
28
|
+
{ key: 'default_export_root', snippet: 'Default export root: `<repo>/.agent-bridge-markdown/`' },
|
|
29
|
+
{ key: 'project_note_path', snippet: 'project/notes/project-notes.md' },
|
|
30
|
+
{ key: 'team_note_path', snippet: 'project/notes/team-notes.md' },
|
|
31
|
+
{ key: 'branch_conversation_path', snippet: 'branches/<branch>/conversations/index.md' },
|
|
32
|
+
{ key: 'session_path', snippet: 'branches/<branch>/sessions/<session-id>.md' },
|
|
33
|
+
{ key: 'workspace_agent_path', snippet: 'branches/<branch>/workspaces/agents/<agent>.md' },
|
|
34
|
+
{ key: 'plan_status_path', snippet: 'branches/<branch>/plans/status.md' },
|
|
35
|
+
{ key: 'governance_branch_local_rule', snippet: 'Governance pages and cross-branch note summaries MUST read branch-local governance views; they MUST NOT fall back to shared legacy governance files.' },
|
|
36
|
+
{ key: 'canonical_assembly_point', snippet: 'The best low-churn export assembly point is `agent-bridge/state/canonical.js`.' },
|
|
37
|
+
{ key: 'dashboard_export_seam', snippet: 'The best outward-facing seam is the existing dashboard export family (`/api/export-json`, `/api/export`, and `/api/export-replay`), not a watcher loop.' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function fail(message, exitCode) {
|
|
41
|
+
console.error(message);
|
|
42
|
+
process.exit(exitCode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
if (argv.length === 0) {
|
|
47
|
+
return { simulateMissingKey: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (argv.length === 2 && argv[0] === '--simulate-missing') {
|
|
51
|
+
const simulateMissingKey = argv[1];
|
|
52
|
+
const supportedKeys = [
|
|
53
|
+
...REQUIRED_HEADINGS.map((item) => item.key),
|
|
54
|
+
...REQUIRED_SNIPPETS.map((item) => item.key),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (!supportedKeys.includes(simulateMissingKey)) {
|
|
58
|
+
fail(
|
|
59
|
+
[
|
|
60
|
+
`Unknown key for --simulate-missing: ${simulateMissingKey}`,
|
|
61
|
+
`Supported keys: ${supportedKeys.join(', ')}`,
|
|
62
|
+
USAGE,
|
|
63
|
+
].join('\n'),
|
|
64
|
+
2
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { simulateMissingKey };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fail(USAGE, 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectHeadings(markdown) {
|
|
75
|
+
return new Set(
|
|
76
|
+
markdown
|
|
77
|
+
.split(/\r?\n/)
|
|
78
|
+
.map((line) => line.trimEnd())
|
|
79
|
+
.filter((line) => /^(##|###)\s+/.test(line))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
const { simulateMissingKey } = parseArgs(process.argv.slice(2));
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(DOC_PATH)) {
|
|
87
|
+
fail(`Markdown workspace validation failed.\nMissing file: ${DOC_DISPLAY_PATH}`, 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const markdown = fs.readFileSync(DOC_PATH, 'utf8');
|
|
91
|
+
const headings = collectHeadings(markdown);
|
|
92
|
+
const problems = [];
|
|
93
|
+
|
|
94
|
+
for (const item of REQUIRED_HEADINGS) {
|
|
95
|
+
if (item.key === simulateMissingKey) {
|
|
96
|
+
problems.push(`- ${item.key}: ${item.heading}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!headings.has(item.heading)) {
|
|
101
|
+
problems.push(`- ${item.key}: ${item.heading}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const item of REQUIRED_SNIPPETS) {
|
|
106
|
+
if (item.key === simulateMissingKey) {
|
|
107
|
+
problems.push(`- ${item.key}: ${item.snippet}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!markdown.includes(item.snippet)) {
|
|
112
|
+
problems.push(`- ${item.key}: ${item.snippet}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (problems.length > 0) {
|
|
117
|
+
const lines = ['Markdown workspace validation failed.', `Checked file: ${DOC_DISPLAY_PATH}`];
|
|
118
|
+
|
|
119
|
+
if (simulateMissingKey) {
|
|
120
|
+
lines.push(`Simulated missing key: ${simulateMissingKey}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
lines.push('Missing required markdown workspace contract markers:');
|
|
124
|
+
lines.push(...problems);
|
|
125
|
+
fail(lines.join('\n'), 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log([
|
|
129
|
+
'Markdown workspace validation passed.',
|
|
130
|
+
`Checked file: ${DOC_DISPLAY_PATH}`,
|
|
131
|
+
`Validated ${REQUIRED_HEADINGS.length} required headings.`,
|
|
132
|
+
`Validated ${REQUIRED_SNIPPETS.length} required markdown workspace markers.`,
|
|
133
|
+
].join('\n'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main();
|