@a-company/paradigm 3.34.0 → 3.44.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/dist/{accept-orchestration-XXANWJVZ.js → accept-orchestration-ZUWQUHSK.js} +6 -6
- package/dist/add-VSPZ6FM4.js +81 -0
- package/dist/{aggregate-XHQ6GI3Z.js → aggregate-SV3VGEIL.js} +2 -2
- package/dist/assess-UHBDYIK7.js +68 -0
- package/dist/{beacon-BTLQMYQL.js → beacon-3SJV4DAP.js} +2 -2
- package/dist/calibration-WWHK73WU.js +135 -0
- package/dist/{chunk-C5ZE6WEX.js → chunk-2SKXFXIT.js} +91 -1
- package/dist/{chunk-S5TDFT5Q.js → chunk-7COU5S2Z.js} +2 -2
- package/dist/{chunk-H4TVBJD4.js → chunk-AKIMFN6I.js} +3 -3
- package/dist/{chunk-3DYYXGDC.js → chunk-CDMAMDSG.js} +33 -0
- package/dist/chunk-F3BCHPYT.js +143 -0
- package/dist/{chunk-R2SGQ22F.js → chunk-FKJUBQU3.js} +461 -2
- package/dist/chunk-GT5QGC2H.js +253 -0
- package/dist/{chunk-UQNTJ5VB.js → chunk-HIKKOCXY.js} +1 -1
- package/dist/{chunk-J26YQVAK.js → chunk-J4E6K5MG.js} +1 -1
- package/dist/chunk-L27I3CPZ.js +357 -0
- package/dist/{chunk-WOONGZ3C.js → chunk-P7XSBJE3.js} +1 -1
- package/dist/{chunk-Z7W7HNRG.js → chunk-QDXI2DHR.js} +1 -1
- package/dist/{chunk-BRILIG7Z.js → chunk-QIOCFXDQ.js} +42 -0
- package/dist/{chunk-3BGSDKWD.js → chunk-QWA26UNO.js} +7 -7
- package/dist/{lore-server-ILPHKWLK.js → chunk-RAB5IKPR.js} +77 -112
- package/dist/chunk-SOBTKFSP.js +616 -0
- package/dist/{chunk-BKMNLROM.js → chunk-ZDHLG5VP.js} +461 -147
- package/dist/{chunk-CTF6RHKG.js → chunk-ZGUAAVMA.js} +17 -2
- package/dist/{chunk-PFLWLC6J.js → chunk-ZMQA6SCO.js} +855 -34
- package/dist/{chunk-3BAMPB6I.js → chunk-ZSYVKSY6.js} +2 -147
- package/dist/{commands-KPT2T2OZ.js → commands-5N4ILTPH.js} +465 -1
- package/dist/config-schema-3YNIFJCJ.js +152 -0
- package/dist/{constellation-LZ6XIKDT.js → constellation-FAGT45TU.js} +2 -2
- package/dist/{context-audit-RI4R2WRH.js → context-audit-557EO6PK.js} +138 -8
- package/dist/{cost-4SZM7OUS.js → cost-UD3WPEKZ.js} +1 -1
- package/dist/{delete-YTASL4SM.js → delete-RRK4RL6Y.js} +1 -1
- package/dist/{diff-T6YJSAAC.js → diff-IP5CIARP.js} +6 -6
- package/dist/{dist-AG5JNIZU-HW2FWNTZ.js → dist-5QE2BB2B-X6DYVSUL.js} +59 -5
- package/dist/{dist-IKBGY7FQ.js → dist-CM3MVWWW.js} +3 -1
- package/dist/{dist-OH4DBV2O.js → dist-OGTSAZ55.js} +16 -1
- package/dist/{dist-RMAIFRTW.js → dist-POMVY6WP.js} +5 -3
- package/dist/{dist-QSBAGCZT.js → dist-UXWV4OKX.js} +2 -2
- package/dist/{doctor-INBOLZC7.js → doctor-GKZJU7QG.js} +1 -1
- package/dist/{edit-S7NZD7H7.js → edit-4CLNN5JG.js} +1 -1
- package/dist/{graph-ERNQQQ7C.js → graph-YYUXI3F7.js} +1 -1
- package/dist/graph-server-ZPXRSGCW.js +116 -0
- package/dist/{habits-7BORPC2F.js → habits-RG5SVKXP.js} +2 -2
- package/dist/index.js +200 -86
- package/dist/integrity-MK2OP5TA.js +194 -0
- package/dist/integrity-checker-J7YXRTBT.js +11 -0
- package/dist/{lint-MTRZB5EC.js → lint-HYWGS3JJ.js} +1 -1
- package/dist/{list-QTFWN35D.js → list-BTLFHSRC.js} +1 -1
- package/dist/list-IUCYPGMK.js +57 -0
- package/dist/{lore-loader-S5BXMH27.js → lore-loader-VTEEZDX3.js} +3 -1
- package/dist/lore-server-NOOAHKJX.js +118 -0
- package/dist/mcp.js +2591 -112
- package/dist/{migrate-HRN5TUBQ.js → migrate-FQVGQNXZ.js} +21 -3
- package/dist/{migrate-assessments-FPR6C35Z.js → migrate-assessments-JP6Q5KME.js} +1 -1
- package/dist/{orchestrate-3SI6ON33.js → orchestrate-A226N6FC.js} +6 -6
- package/dist/platform-server-KHL6ZPPN.js +900 -0
- package/dist/{probe-ABMGCXQG.js → probe-7JK7IDNI.js} +4 -4
- package/dist/{providers-YW3FG6DA.js → providers-YNFSL6HK.js} +1 -1
- package/dist/quiz-I75NU2QQ.js +99 -0
- package/dist/{record-UGN75GTB.js → record-46CLR4OG.js} +11 -2
- package/dist/{reindex-YC7LD4MN.js → reindex-WIJMCJ4A.js} +3 -2
- package/dist/{remember-WR6ZVXLT.js → remember-4EUZKIIB.js} +1 -1
- package/dist/{retag-URLJLMSK.js → retag-KC4JVRLE.js} +1 -1
- package/dist/{review-725ZKA7U.js → review-Q7M4CRB5.js} +1 -1
- package/dist/{ripple-QTXKJCEI.js → ripple-RI3LOT6R.js} +2 -2
- package/dist/{sentinel-FUR3QKCJ.js → sentinel-UOIGJWHH.js} +1 -1
- package/dist/sentinel-bridge-APDXYAZS.js +109 -0
- package/dist/sentinel-mcp.js +13 -0
- package/dist/sentinel-ui/assets/{index-Zh1YM0C9.css → index-CJ1Wx083.css} +1 -1
- package/dist/sentinel-ui/assets/index-S1VJ67dT.js +62 -0
- package/dist/sentinel-ui/assets/index-S1VJ67dT.js.map +1 -0
- package/dist/sentinel-ui/index.html +2 -2
- package/dist/sentinel.js +6 -6
- package/dist/{serve-DIALBCTU.js → serve-22A4XOIG.js} +1 -1
- package/dist/{university-A66BMZ4Z.js → serve-2YJ6D2Y6.js} +9 -8
- package/dist/serve-JVXSRSUB.js +33 -0
- package/dist/{server-2VICPDUR.js → server-JV6UFGWZ.js} +25 -2
- package/dist/{server-OWBK2WFS.js → server-RDLQ3DK7.js} +49 -4
- package/dist/{setup-ASR6OMKV.js → setup-M2ZKLKNN.js} +2 -2
- package/dist/{shift-7XLSBLDW.js → shift-LNMKFYLR.js} +63 -14
- package/dist/{show-GEVVQWWG.js → show-P7GYO43X.js} +1 -1
- package/dist/show-PKZMYKRN.js +82 -0
- package/dist/{snapshot-QZFD7YBI.js → snapshot-Y3COXK4T.js} +2 -2
- package/dist/{spawn-DIY7T4QW.js → spawn-SSXZX45U.js} +2 -2
- package/dist/status-KLHALGW4.js +71 -0
- package/dist/{summary-R4CSYNNP.js → summary-5NQNOD3F.js} +2 -2
- package/dist/{sweep-5POCF2E4.js → sweep-EZU3GU6S.js} +1 -1
- package/dist/symphony-EYRGGVNE.js +470 -0
- package/dist/symphony-QWOEKZMC.js +308 -0
- package/dist/{team-VH3HYABB.js → team-HGLJXWQG.js} +7 -7
- package/dist/{timeline-RKXNRMKF.js → timeline-ANC7LVDL.js} +1 -1
- package/dist/{triage-GJ6GK647.js → triage-IZ4MDYNB.js} +2 -2
- package/dist/university-content/courses/.purpose +7 -1
- package/dist/university-content/courses/para-501.json +166 -0
- package/dist/university-content/plsat/.purpose +6 -0
- package/dist/university-content/plsat/v3.0.json +323 -1
- package/dist/university-content/reference.json +48 -0
- package/dist/university-ui/assets/{index-TcsCEBMo.js → index-tfi5xN4Q.js} +2 -2
- package/dist/university-ui/assets/{index-TcsCEBMo.js.map → index-tfi5xN4Q.js.map} +1 -1
- package/dist/university-ui/index.html +1 -1
- package/dist/validate-GD5XWILV.js +134 -0
- package/dist/{validate-OUHUBZPO.js → validate-ZVPNN4FL.js} +1 -1
- package/dist/{workspace-5RBSALXC.js → workspace-UIUTHZTD.js} +5 -5
- package/package.json +4 -2
- package/platform-ui/dist/assets/GitSection-BD3Ze06e.js +4 -0
- package/platform-ui/dist/assets/GitSection-C-GQWHcu.css +1 -0
- package/platform-ui/dist/assets/GraphSection-BlgXTl53.css +1 -0
- package/platform-ui/dist/assets/GraphSection-SglITfSs.js +8 -0
- package/platform-ui/dist/assets/LoreSection-C3EixkjW.css +1 -0
- package/platform-ui/dist/assets/LoreSection-bR5Km4Fd.js +1 -0
- package/platform-ui/dist/assets/SentinelSection-BI-aIYKL.css +1 -0
- package/platform-ui/dist/assets/SentinelSection-QSpAZArG.js +1 -0
- package/platform-ui/dist/assets/SymphonySection-CobYJgvg.js +1 -0
- package/platform-ui/dist/assets/SymphonySection-zY0C5tFl.css +1 -0
- package/platform-ui/dist/assets/index-CfpZFjea.css +1 -0
- package/platform-ui/dist/assets/index-DbxeSMkV.js +57 -0
- package/platform-ui/dist/index.html +14 -0
- package/dist/graph-server-BZ73HTAT.js +0 -251
- package/dist/sentinel-ui/assets/index-C_Wstm64.js +0 -62
- package/dist/sentinel-ui/assets/index-C_Wstm64.js.map +0 -1
- /package/dist/{chunk-VUSCJJ4A.js → chunk-EDOAWN7J.js} +0 -0
- /package/dist/{chunk-5SXMV4SP.js → chunk-FS3WTUHY.js} +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
approveFileRequest,
|
|
4
|
+
buildMessage,
|
|
5
|
+
cleanStaleAgents,
|
|
6
|
+
createThread,
|
|
7
|
+
denyFileRequest,
|
|
8
|
+
discoverClaudeCodeSessions,
|
|
9
|
+
expireOldRequests,
|
|
10
|
+
getMyIdentity,
|
|
11
|
+
getThreadMessages,
|
|
12
|
+
isAgentAsleep,
|
|
13
|
+
listAgents,
|
|
14
|
+
listFileRequests,
|
|
15
|
+
listThreads,
|
|
16
|
+
loadThread,
|
|
17
|
+
readInbox,
|
|
18
|
+
resolveAgentIdentity,
|
|
19
|
+
resolveThread,
|
|
20
|
+
routeMessage
|
|
21
|
+
} from "./chunk-SOBTKFSP.js";
|
|
22
|
+
import "./chunk-ZXMDA7VB.js";
|
|
23
|
+
|
|
24
|
+
// src/platform-server/routes/symphony.ts
|
|
25
|
+
import { Router } from "express";
|
|
26
|
+
function createSymphonyRouter(projectDir, broadcast) {
|
|
27
|
+
const router = Router();
|
|
28
|
+
router.get("/agents", (_req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
cleanStaleAgents();
|
|
31
|
+
const agents = listAgents();
|
|
32
|
+
const discovered = discoverClaudeCodeSessions();
|
|
33
|
+
const agentIds = new Set(agents.map((a) => a.id));
|
|
34
|
+
for (const d of discovered) {
|
|
35
|
+
if (!agentIds.has(d.id)) agents.push(d);
|
|
36
|
+
}
|
|
37
|
+
const result = agents.map((a) => ({
|
|
38
|
+
id: a.id,
|
|
39
|
+
name: a.name,
|
|
40
|
+
project: a.project,
|
|
41
|
+
role: a.role,
|
|
42
|
+
status: isAgentAsleep(a) ? "asleep" : "awake",
|
|
43
|
+
lastPoll: a.lastPoll,
|
|
44
|
+
startedAt: a.startedAt,
|
|
45
|
+
statusBlurb: a.statusBlurb
|
|
46
|
+
}));
|
|
47
|
+
res.json({ agents: result });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
res.status(500).json({ error: "Failed to list agents", detail: String(err) });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
router.get("/agents/me", (_req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const identity = getMyIdentity(projectDir);
|
|
55
|
+
res.json({ identity: identity || null });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
res.status(500).json({ error: "Failed to get identity", detail: String(err) });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
router.get("/threads", (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const statusParam = req.query.status;
|
|
63
|
+
let status;
|
|
64
|
+
if (statusParam === "active" || statusParam === "resolved") {
|
|
65
|
+
status = statusParam;
|
|
66
|
+
}
|
|
67
|
+
const threads = listThreads(status);
|
|
68
|
+
const result = threads.map((t) => ({
|
|
69
|
+
id: t.id,
|
|
70
|
+
topic: t.topic,
|
|
71
|
+
status: t.status,
|
|
72
|
+
participants: t.participants.map((p) => ({ id: p.id, name: p.name, type: p.type })),
|
|
73
|
+
messageCount: t.messageCount,
|
|
74
|
+
lastActivity: t.lastActivity,
|
|
75
|
+
decision: t.decision
|
|
76
|
+
}));
|
|
77
|
+
res.json({ threads: result });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
res.status(500).json({ error: "Failed to list threads", detail: String(err) });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
router.get("/threads/:threadId", (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const { threadId } = req.params;
|
|
85
|
+
const thread = loadThread(threadId);
|
|
86
|
+
if (!thread) {
|
|
87
|
+
res.status(404).json({ error: `Thread not found: ${threadId}` });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const messages = getThreadMessages(threadId);
|
|
91
|
+
const symbolsDiscussed = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const msg of messages) {
|
|
93
|
+
for (const sym of msg.symbols) symbolsDiscussed.add(sym);
|
|
94
|
+
}
|
|
95
|
+
res.json({
|
|
96
|
+
thread: {
|
|
97
|
+
id: thread.id,
|
|
98
|
+
topic: thread.topic,
|
|
99
|
+
status: thread.status,
|
|
100
|
+
participants: thread.participants.map((p) => ({ id: p.id, name: p.name, type: p.type })),
|
|
101
|
+
messageCount: thread.messageCount,
|
|
102
|
+
lastActivity: thread.lastActivity,
|
|
103
|
+
decision: thread.decision
|
|
104
|
+
},
|
|
105
|
+
messages: messages.map((m) => ({
|
|
106
|
+
id: m.id,
|
|
107
|
+
sender: { id: m.sender.id, name: m.sender.name, type: m.sender.type },
|
|
108
|
+
intent: m.intent,
|
|
109
|
+
text: m.content.text,
|
|
110
|
+
timestamp: m.timestamp,
|
|
111
|
+
symbols: m.symbols,
|
|
112
|
+
diff: m.content.diff,
|
|
113
|
+
decision: m.content.decision,
|
|
114
|
+
recipients: m.recipients?.map((r) => ({ id: r.id, name: r.name }))
|
|
115
|
+
})),
|
|
116
|
+
symbolsDiscussed: [...symbolsDiscussed]
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
res.status(500).json({ error: "Failed to load thread", detail: String(err) });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
router.post("/threads/:threadId/resolve", (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const { threadId } = req.params;
|
|
125
|
+
const { decision } = req.body;
|
|
126
|
+
const success = resolveThread(threadId, decision);
|
|
127
|
+
if (!success) {
|
|
128
|
+
res.status(404).json({ error: `Thread not found: ${threadId}` });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (broadcast) {
|
|
132
|
+
broadcast({ type: "symphony:thread_resolved", threadId, decision });
|
|
133
|
+
}
|
|
134
|
+
res.json({ resolved: true, threadId, decision });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
res.status(500).json({ error: "Failed to resolve thread", detail: String(err) });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
router.post("/messages", (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
const { intent, text, threadRoot, recipients, symbols, diff, decision } = req.body;
|
|
142
|
+
if (!intent || !text) {
|
|
143
|
+
res.status(400).json({ error: "intent and text are required" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const agentId = resolveAgentIdentity(projectDir);
|
|
147
|
+
const sender = {
|
|
148
|
+
id: `human/${agentId}`,
|
|
149
|
+
name: "Human (Platform UI)",
|
|
150
|
+
type: "human"
|
|
151
|
+
};
|
|
152
|
+
let effectiveThreadRoot = threadRoot;
|
|
153
|
+
let threadCreated = false;
|
|
154
|
+
if (!threadRoot) {
|
|
155
|
+
const topic = text.length > 60 ? text.slice(0, 60) + "..." : text;
|
|
156
|
+
const thread = createThread(topic, sender);
|
|
157
|
+
effectiveThreadRoot = thread.id;
|
|
158
|
+
threadCreated = true;
|
|
159
|
+
}
|
|
160
|
+
let resolvedRecipients;
|
|
161
|
+
if (recipients && recipients.length > 0) {
|
|
162
|
+
const allAgents = listAgents();
|
|
163
|
+
resolvedRecipients = recipients.map((id) => {
|
|
164
|
+
const agent = allAgents.find((a) => a.id === id);
|
|
165
|
+
if (agent) return { id: agent.id, name: agent.name, type: "agent" };
|
|
166
|
+
return { id, name: id, type: "agent" };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const message = buildMessage({
|
|
170
|
+
sender,
|
|
171
|
+
recipients: resolvedRecipients,
|
|
172
|
+
intent,
|
|
173
|
+
text,
|
|
174
|
+
threadRoot: effectiveThreadRoot,
|
|
175
|
+
symbols,
|
|
176
|
+
diff,
|
|
177
|
+
decision
|
|
178
|
+
});
|
|
179
|
+
const deliveryCount = routeMessage(message);
|
|
180
|
+
if (broadcast) {
|
|
181
|
+
broadcast({
|
|
182
|
+
type: "symphony:message",
|
|
183
|
+
message: {
|
|
184
|
+
id: message.id,
|
|
185
|
+
sender: { id: sender.id, name: sender.name, type: sender.type },
|
|
186
|
+
intent: message.intent,
|
|
187
|
+
text: message.content.text,
|
|
188
|
+
timestamp: message.timestamp,
|
|
189
|
+
symbols: message.symbols,
|
|
190
|
+
diff: message.content.diff,
|
|
191
|
+
decision: message.content.decision
|
|
192
|
+
},
|
|
193
|
+
threadId: effectiveThreadRoot
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
res.json({
|
|
197
|
+
sent: true,
|
|
198
|
+
messageId: message.id,
|
|
199
|
+
threadId: effectiveThreadRoot,
|
|
200
|
+
threadCreated,
|
|
201
|
+
deliveredTo: deliveryCount
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
res.status(500).json({ error: "Failed to send message", detail: String(err) });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
router.get("/inbox", (_req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const agentId = resolveAgentIdentity(projectDir);
|
|
210
|
+
const messages = readInbox(agentId);
|
|
211
|
+
res.json({
|
|
212
|
+
agentId,
|
|
213
|
+
messages: messages.map((m) => ({
|
|
214
|
+
id: m.id,
|
|
215
|
+
sender: { id: m.sender.id, name: m.sender.name, type: m.sender.type },
|
|
216
|
+
intent: m.intent,
|
|
217
|
+
text: m.content.text,
|
|
218
|
+
timestamp: m.timestamp,
|
|
219
|
+
threadRoot: m.threadRoot,
|
|
220
|
+
symbols: m.symbols
|
|
221
|
+
}))
|
|
222
|
+
});
|
|
223
|
+
} catch (err) {
|
|
224
|
+
res.status(500).json({ error: "Failed to read inbox", detail: String(err) });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
router.get("/file-requests", (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
expireOldRequests();
|
|
230
|
+
const statusParam = req.query.status;
|
|
231
|
+
let status;
|
|
232
|
+
if (statusParam === "pending" || statusParam === "approved" || statusParam === "denied" || statusParam === "expired") {
|
|
233
|
+
status = statusParam;
|
|
234
|
+
}
|
|
235
|
+
const requests = listFileRequests(status);
|
|
236
|
+
const result = requests.map((r) => ({
|
|
237
|
+
requestId: r.request.requestId,
|
|
238
|
+
filePath: r.request.filePath,
|
|
239
|
+
reason: r.request.reason,
|
|
240
|
+
requester: { id: r.request.requester.id, name: r.request.requester.name },
|
|
241
|
+
urgency: r.request.urgency,
|
|
242
|
+
snippet: r.request.snippet,
|
|
243
|
+
status: r.status,
|
|
244
|
+
createdAt: r.createdAt,
|
|
245
|
+
resolvedAt: r.resolvedAt,
|
|
246
|
+
denyReason: r.denyReason
|
|
247
|
+
}));
|
|
248
|
+
res.json({ fileRequests: result });
|
|
249
|
+
} catch (err) {
|
|
250
|
+
res.status(500).json({ error: "Failed to list file requests", detail: String(err) });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
router.post("/file-requests/:requestId/action", (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const { requestId } = req.params;
|
|
256
|
+
const { action, reason } = req.body;
|
|
257
|
+
if (!action) {
|
|
258
|
+
res.status(400).json({ error: "action is required" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (action === "deny") {
|
|
262
|
+
const success = denyFileRequest(requestId, reason);
|
|
263
|
+
res.json({ success, requestId, action: "denied", reason });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const redact = action === "approve-redacted";
|
|
267
|
+
const result = approveFileRequest(requestId, projectDir, redact);
|
|
268
|
+
if (!result.success) {
|
|
269
|
+
res.status(400).json({ success: false, requestId, error: result.error });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
res.json({
|
|
273
|
+
success: true,
|
|
274
|
+
requestId,
|
|
275
|
+
action: redact ? "approved-redacted" : "approved",
|
|
276
|
+
filePath: result.delivery?.filePath,
|
|
277
|
+
size: result.delivery?.size
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
res.status(500).json({ error: "Failed to handle file request", detail: String(err) });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
router.get("/status", (_req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
cleanStaleAgents();
|
|
286
|
+
const agents = listAgents();
|
|
287
|
+
const awake = agents.filter((a) => !isAgentAsleep(a)).length;
|
|
288
|
+
const threads = listThreads("active");
|
|
289
|
+
const agentId = resolveAgentIdentity(projectDir);
|
|
290
|
+
const unread = readInbox(agentId);
|
|
291
|
+
const pendingRequests = listFileRequests("pending");
|
|
292
|
+
res.json({
|
|
293
|
+
agentCount: agents.length,
|
|
294
|
+
awakeCount: awake,
|
|
295
|
+
asleepCount: agents.length - awake,
|
|
296
|
+
activeThreadCount: threads.length,
|
|
297
|
+
unreadCount: unread.length,
|
|
298
|
+
pendingFileRequests: pendingRequests.length
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
res.status(500).json({ error: "Failed to get status", detail: String(err) });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return router;
|
|
305
|
+
}
|
|
306
|
+
export {
|
|
307
|
+
createSymphonyRouter
|
|
308
|
+
};
|
|
@@ -8,17 +8,17 @@ import {
|
|
|
8
8
|
teamModelsCommand,
|
|
9
9
|
teamResetCommand,
|
|
10
10
|
teamStatusCommand
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-J26YQVAK.js";
|
|
11
|
+
} from "./chunk-HIKKOCXY.js";
|
|
12
|
+
import "./chunk-QWA26UNO.js";
|
|
13
|
+
import "./chunk-J4E6K5MG.js";
|
|
15
14
|
import "./chunk-PBHIFAL4.js";
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-FS3WTUHY.js";
|
|
16
|
+
import "./chunk-6QC3YGB6.js";
|
|
17
17
|
import "./chunk-PMXRGPRQ.js";
|
|
18
18
|
import "./chunk-MW5DMGBB.js";
|
|
19
19
|
import "./chunk-5JGJACDU.js";
|
|
20
|
-
import "./chunk-
|
|
21
|
-
import "./chunk-
|
|
20
|
+
import "./chunk-ZGUAAVMA.js";
|
|
21
|
+
import "./chunk-EDOAWN7J.js";
|
|
22
22
|
import "./chunk-IRKUEJVW.js";
|
|
23
23
|
import "./chunk-ZXMDA7VB.js";
|
|
24
24
|
export {
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
StatsCalculator,
|
|
7
7
|
TimelineBuilder,
|
|
8
8
|
loadAllSeedPatterns
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ZSYVKSY6.js";
|
|
10
10
|
import {
|
|
11
11
|
SentinelStorage
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FKJUBQU3.js";
|
|
13
13
|
import "./chunk-ZXMDA7VB.js";
|
|
14
14
|
|
|
15
15
|
// src/commands/triage/index.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: university-courses
|
|
2
|
-
description: Course content for Paradigm University — 5 courses,
|
|
2
|
+
description: Course content for Paradigm University — 5 courses, 50 lessons. Each lesson is mapped to the Paradigm concepts it teaches so that ripple analysis can identify which lessons need updating when a concept changes.
|
|
3
3
|
|
|
4
4
|
components:
|
|
5
5
|
# PARA 101: Foundations (8 lessons)
|
|
@@ -305,3 +305,9 @@ components:
|
|
|
305
305
|
file: para-501.json
|
|
306
306
|
tags: [course-content, para-501]
|
|
307
307
|
references: ["#lore-system"]
|
|
308
|
+
|
|
309
|
+
para-501-platform-agent-ui:
|
|
310
|
+
description: "Platform & Agent-Driven UI — paradigm serve, MCP→HTTP→WS→browser pipeline, 5 agent tools, conflict resolution, presence, visual treatment"
|
|
311
|
+
file: para-501.json
|
|
312
|
+
tags: [course-content, para-501]
|
|
313
|
+
references: ["#PlatformServer", "#PlatformWebSocket", "#AgentPresenceManager", "#UserStateTracker", "#AgentCommandRoute", "#PlatformTools", "#AgentStore", "#AgentToast", "#AgentCallout"]
|
|
@@ -789,6 +789,172 @@
|
|
|
789
789
|
"explanation": "`paradigm_lore_search` supports combining filters: `tag` for arc prefix matching, `type` for entry type, and `symbol` for symbol references. These filters combine (AND logic), so you get only retro entries in the auth-hardening arc that touch the payment service. The assessment tools (B) are deprecated. Using `tags` array (C) uses OR logic, not AND. General search (D) searches the symbol index, not lore content."
|
|
790
790
|
}
|
|
791
791
|
]
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
"id": "symphony-a-mail",
|
|
795
|
+
"title": "Symphony: Multi-Agent Messaging with The Score",
|
|
796
|
+
"content": "## Agents Need to Talk\n\nUntil now, every Paradigm agent has worked in isolation. A Claude Code session modifying the backend has no awareness of what the session working on the frontend is doing. Two developers on the same team, each with their own AI assistant, have no way for those assistants to coordinate — even when they are working on the same project at the same time.\n\nSymphony changes this. It is Paradigm's multi-agent, multi-human collaborative intelligence layer. And its foundation is The Score: a lightweight, file-based messaging protocol that gives every Claude Code session its own mailbox.\n\n## The Metaphor: Email for AI Agents\n\nThe Score works exactly like email. Each agent has an identity, an inbox, and an outbox. Messages are delivered as JSONL files on the filesystem. Agents poll for new messages on a timer. There is no persistent server, no WebSocket connection, and no cloud dependency. If two agents are running on the same machine, they can message each other through nothing more than file reads and writes.\n\nThis simplicity is deliberate. The Score is the CLI-only foundation of Symphony — it works with zero dependencies beyond the Paradigm CLI. No Conductor, no Sentinel, no network configuration. The only requirement is that agents are running on the same machine (or connected via a lightweight TCP relay for cross-machine scenarios).\n\n## Agent Identity and Discovery\n\nEvery Claude Code session that participates in The Score has a stable identity. The identity is derived from the project directory and the agent's role — for example, `a-paradigm/backend` or `a-kamiki/frontend`. This deterministic naming means the same project opened in the same context always gets the same identity, even across session restarts.\n\nWhen you run `paradigm symphony join`, the CLI discovers all Claude Code sessions on the current machine and connects them into a mail network. Each session gets a mailbox directory at `~/.paradigm/score/agents/{agent-id}/` containing four files:\n\n- **`inbox.jsonl`** — Messages waiting for this agent, one per line, append-only\n- **`outbox.jsonl`** — Replies from this agent, append-only\n- **`ack.json`** — The ID of the last acknowledged message (for garbage collection)\n- **`identity.json`** — Agent ID, project, role, PID, and session start time\n\nThe JSONL format — one JSON object per line — makes appending atomic and parsing trivial. No file locking, no corruption risk from concurrent writes, no binary format to decode.\n\n## Messaging and Threading\n\nMessages in The Score carry structured metadata beyond plain text. Every message has an **intent** that classifies its purpose:\n\n| Intent | Meaning |\n|---|---|\n| `question` | Asking for information from other agents |\n| `context` | Providing background or context |\n| `proposal` | Proposing an action or fix |\n| `action` | Announcing an action the agent took |\n| `decision` | Recording a decision |\n| `alert` | Forwarding a Sentinel alert |\n| `approval` / `rejection` | Responding to a proposal |\n| `handoff` | Transferring responsibility to another agent |\n| `fileRequest` | Requesting a file from another agent |\n| `fileDelivery` | Delivering a requested file |\n\nIntents serve two purposes. First, they give the receiving agent structured context about what kind of response is expected — a question needs an answer, a proposal needs approval or rejection, a decision needs acknowledgment. Second, they feed into Lore: when a message has `intent: decision`, Symphony can automatically record it as a lore entry.\n\nMessages belong to **threads**. A thread starts when the first message on a topic is sent (with no `parentId`). Subsequent replies reference the thread root, building a conversation tree. Thread state is tracked in `~/.paradigm/score/threads/{thread-id}.json`, which records the topic, participants, message count, and last activity timestamp.\n\n## The File Pipeline\n\nAgents often need to share files — a type definition, an API contract, a configuration file. The Score's file pipeline enables this with a critical security constraint: **every file transfer requires explicit human approval**.\n\nThe flow works like this: Agent A sends a `fileRequest` message specifying the file path, a reason, and the target agent. The request appears in the owning human's terminal (via `paradigm symphony requests`). The human reviews and either approves, denies, or approves with redaction (stripping sensitive lines). Only after human approval does the file content get written to the requester's inbox.\n\nTrust configuration lives in `~/.paradigm/score/trust.yaml`. You can define auto-approve patterns for trusted users (`docs/**`, `*.md`) and never-approve patterns for sensitive files (`.env*`, `*.key`, `*.pem`, `**/secrets/**`). The never-approve list is enforced absolutely — even clicking approve on a `.env` file will be denied by the system. File requests expire after one hour without action, and all transfers are logged.\n\n## /loop: The Agent Heartbeat\n\nThe glue that makes The Score work is `/loop`. Each Claude Code session runs `/loop 10s paradigm_symphony_poll`, which polls the inbox every 10 seconds for new messages. The `paradigm_symphony_poll` MCP tool reads `inbox.jsonl`, formats messages as structured prompts the agent can reason about, and suggests actions.\n\nWithout `/loop`, messages would accumulate in the inbox with nobody reading them. The loop is the heartbeat — it keeps agents responsive. When an agent processes a message and replies via `paradigm_symphony_send`, the reply goes to `outbox.jsonl`. A mail router (or Conductor, in later phases) picks up outbox messages and delivers them to the appropriate inbox files.\n\nThe convenience command `paradigm symphony join` combines registration and loop setup in one step — it registers the session's identity and starts the polling loop automatically.\n\n## Thread Resolution and Lore Integration\n\nConversation threads are not meant to live forever. When a thread reaches a conclusion, any participant (human or agent) can resolve it with `paradigm symphony resolve <thread-id>`. Resolution triggers an automatic lore entry that captures the full conversation: topic, participants, decisions made, actions taken, and symbols discussed.\n\nThis is the bridge between ephemeral conversation and permanent project memory. A 15-minute exchange between three agents about a serialization bug becomes a searchable lore entry tagged with the relevant symbols and arc. The next developer encountering a similar issue can find the conversation, the decision, and the fix — all linked together.\n\n## CLI Commands\n\nThe `paradigm symphony` command group provides the complete human interface:\n\n- `paradigm symphony whoami` — Show this agent's identity and linked peers\n- `paradigm symphony list` — List all known agents with status (awake/asleep) and location\n- `paradigm symphony join` — Discover and connect Claude Code sessions on this machine\n- `paradigm symphony join --remote <ip>` — Connect to a remote machine's mail server\n- `paradigm symphony send \"message\"` — Broadcast to all linked agents\n- `paradigm symphony send --to <agent> \"message\"` — Direct message to a specific agent\n- `paradigm symphony send --thread <id> \"message\"` — Reply to an existing thread\n- `paradigm symphony read` — Show unread messages\n- `paradigm symphony threads` — List active threads\n- `paradigm symphony resolve <id>` — Resolve a thread, creating a lore entry\n- `paradigm symphony status` — Network overview (agents, threads, unread count)\n\nFor the file pipeline: `paradigm symphony request`, `paradigm symphony requests`, `paradigm symphony approve`, and `paradigm symphony deny`.\n\n## MCP Tools for Agent Participation\n\nSix MCP tools power agent-side Symphony participation:\n\n- **`paradigm_symphony_poll`** — The heartbeat. Reads inbox, returns formatted messages and thread summaries. Called by `/loop`.\n- **`paradigm_symphony_send`** — Send a message with intent, text, optional symbols, diff, or decision. Writes to outbox.\n- **`paradigm_symphony_status`** — Overview of the local network: agents, threads, Sentinel endpoint.\n- **`paradigm_symphony_thread`** — Get full context of a conversation thread with messages, participants, and extracted decisions.\n- **`paradigm_symphony_request_file`** — Request a file from another agent. Returns immediately with `pending` status; delivery arrives via future poll.\n- **`paradigm_symphony_approve_file`** — Approve or deny a pending file request after human confirmation.\n\nThese tools compose naturally with existing Paradigm workflows. An agent can poll for messages, discover a question about `#payment-serializer`, call `paradigm_ripple` to check impact, and respond with full context — all within a single `/loop` cycle.",
|
|
797
|
+
"keyConcepts": [
|
|
798
|
+
"The Score is Symphony's CLI-only foundation — file-based messaging between agents with zero dependencies beyond the Paradigm CLI",
|
|
799
|
+
"Agent identity is derived from project directory + role (e.g., a-paradigm/backend), stable across session restarts",
|
|
800
|
+
"Mailbox protocol uses JSONL files: inbox.jsonl (incoming), outbox.jsonl (replies), ack.json (last acknowledged), identity.json (agent metadata)",
|
|
801
|
+
"Messages carry structured intents (question, proposal, decision, action, alert, etc.) that classify purpose and drive Lore integration",
|
|
802
|
+
"Threads group related messages into conversations, tracked in ~/.paradigm/score/threads/{thread-id}.json",
|
|
803
|
+
"The file pipeline requires explicit human approval for every file transfer, with configurable trust levels and a hard deny list for sensitive files",
|
|
804
|
+
"/loop is the agent heartbeat — each session runs /loop 10s paradigm_symphony_poll to stay responsive to incoming messages",
|
|
805
|
+
"Thread resolution via paradigm symphony resolve triggers automatic lore entry creation, bridging ephemeral conversation to permanent project memory",
|
|
806
|
+
"Six MCP tools: paradigm_symphony_poll, paradigm_symphony_send, paradigm_symphony_status, paradigm_symphony_thread, paradigm_symphony_request_file, paradigm_symphony_approve_file",
|
|
807
|
+
"paradigm symphony join discovers Claude Code sessions on the local machine; paradigm symphony join --remote <ip> extends to remote machines via TCP"
|
|
808
|
+
],
|
|
809
|
+
"quiz": [
|
|
810
|
+
{
|
|
811
|
+
"id": "q1",
|
|
812
|
+
"question": "You have three Claude Code sessions open on your machine: one working on the core library, one on the backend API, and one on the frontend. You want them to communicate. What is the correct setup sequence?",
|
|
813
|
+
"choices": {
|
|
814
|
+
"A": "Start a Sentinel hub, then connect each session to the WebSocket endpoint",
|
|
815
|
+
"B": "Install Conductor, which automatically links all sessions on the machine",
|
|
816
|
+
"C": "Run `paradigm symphony join` to discover and connect the sessions, then run `/loop 10s paradigm_symphony_poll` in each session",
|
|
817
|
+
"D": "Create a `.paradigm-workspace` file listing all three projects — workspace linking enables messaging",
|
|
818
|
+
"E": "Run `paradigm team orchestrate` which automatically sets up inter-agent communication"
|
|
819
|
+
},
|
|
820
|
+
"correct": "C",
|
|
821
|
+
"explanation": "The Score is the CLI-only foundation that requires no Conductor, no Sentinel, and no workspace setup. `paradigm symphony join` discovers Claude Code sessions on the machine and creates mailboxes. Then each session needs `/loop 10s paradigm_symphony_poll` to poll for incoming messages. Conductor (B) auto-links sessions but is not required — The Score works standalone."
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
"id": "q2",
|
|
825
|
+
"question": "An agent sends a message with `intent: 'decision'` and the text 'Use Redis for session storage instead of in-memory.' What happens beyond normal message delivery?",
|
|
826
|
+
"choices": {
|
|
827
|
+
"A": "The message is blocked — only humans can make decisions",
|
|
828
|
+
"B": "Symphony automatically records a lore entry of type `decision` with the message text, linking it to the conversation thread and referenced symbols",
|
|
829
|
+
"C": "The message is flagged for human review before delivery",
|
|
830
|
+
"D": "Nothing special — intent is informational only and has no side effects",
|
|
831
|
+
"E": "The decision is written to `.paradigm/config.yaml` as a project setting"
|
|
832
|
+
},
|
|
833
|
+
"correct": "B",
|
|
834
|
+
"explanation": "Messages with `intent: decision` trigger automatic Lore integration. Symphony records a lore entry with the decision text, links it to the conversation thread, and tags it with referenced symbols. This bridges ephemeral conversation to permanent project memory without manual recording."
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
"id": "q3",
|
|
838
|
+
"question": "Agent A (on your machine) requests `src/types.ts` from Agent B (on a teammate's machine). What must happen before Agent A receives the file?",
|
|
839
|
+
"choices": {
|
|
840
|
+
"A": "Agent B must call `paradigm_symphony_approve_file` — agents can approve their own file requests",
|
|
841
|
+
"B": "The file is sent automatically since both agents are linked in the same Symphony network",
|
|
842
|
+
"C": "The teammate (human) must explicitly approve the file transfer — every cross-agent file transfer requires human approval from the file owner's side",
|
|
843
|
+
"D": "Agent A must have the `^file-access` gate declared in portal.yaml",
|
|
844
|
+
"E": "The file is sent if it matches the auto-approve glob patterns, but there is no human gate"
|
|
845
|
+
},
|
|
846
|
+
"correct": "C",
|
|
847
|
+
"explanation": "The file pipeline's core security constraint is that every file transfer requires explicit human approval from the owner's side. When Agent A requests a file, the teammate sees a prompt (via Conductor or `paradigm symphony requests`) and must approve, deny, or approve with redaction. Even if auto-approve patterns exist in trust.yaml, the initial setup still requires human-configured trust levels."
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
"id": "q4",
|
|
851
|
+
"question": "An agent's `paradigm_symphony_poll` returns 3 new messages in a thread about a failing test. The agent reads them, investigates the code, and finds the bug. How should the agent communicate its findings?",
|
|
852
|
+
"choices": {
|
|
853
|
+
"A": "Call `paradigm_lore_record` with the findings — lore is the communication channel",
|
|
854
|
+
"B": "Call `paradigm_symphony_send` with `intent: 'context'` providing the investigation results, then `intent: 'proposal'` with the fix, writing to `outbox.jsonl` for delivery to the thread",
|
|
855
|
+
"C": "Write directly to the other agents' `inbox.jsonl` files for immediate delivery",
|
|
856
|
+
"D": "Call `paradigm_sentinel_record` to log the bug as an incident",
|
|
857
|
+
"E": "Call `paradigm symphony send` from the CLI — agents cannot use MCP tools to reply"
|
|
858
|
+
},
|
|
859
|
+
"correct": "B",
|
|
860
|
+
"explanation": "Agents communicate via `paradigm_symphony_send`, which writes structured messages to `outbox.jsonl`. Using multiple messages with appropriate intents (context for the investigation, proposal for the fix) gives other agents structured context. Writing directly to inbox files (C) bypasses the routing protocol. Lore (A) and Sentinel (D) are recording systems, not communication channels."
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
"id": "q5",
|
|
864
|
+
"question": "A thread about a serialization bug has been active for 20 minutes across 4 agents. The team agrees on a fix. What should happen next?",
|
|
865
|
+
"choices": {
|
|
866
|
+
"A": "Delete the thread files from `~/.paradigm/score/threads/` to clean up",
|
|
867
|
+
"B": "Let the thread expire naturally — threads auto-resolve after 1 hour of inactivity",
|
|
868
|
+
"C": "Run `paradigm symphony resolve <thread-id>` which marks the thread as resolved and automatically creates a lore entry capturing the full conversation, decisions, and actions",
|
|
869
|
+
"D": "Each agent independently records a lore entry about their contribution",
|
|
870
|
+
"E": "Run `paradigm_reindex` to incorporate the thread into the project index"
|
|
871
|
+
},
|
|
872
|
+
"correct": "C",
|
|
873
|
+
"explanation": "Thread resolution via `paradigm symphony resolve` is the bridge between ephemeral conversation and permanent project memory. It marks the thread as resolved and automatically creates a comprehensive lore entry with the topic, participants, decisions, actions, and referenced symbols. This single action preserves the entire collaborative context for future reference."
|
|
874
|
+
}
|
|
875
|
+
]
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
"id": "platform-agent-ui",
|
|
879
|
+
"title": "Platform & Agent-Driven UI",
|
|
880
|
+
"content": "## The Unified Platform\n\n`paradigm serve` launches the Paradigm Platform — a unified development management interface on port 3850 that absorbs every Paradigm tool (Lore, Graph, Sentinel, University, Symphony) into one browser tab.\n\nThe Platform is built on Express + WebSocket on the server, React 18 + Zustand on the client. Sections are lazy-loaded. A shared design system provides consistent theming and symbol colors.\n\n### Architecture\n\n```\nlocalhost:3850 (Express + WebSocket)\n├── /api/lore/* ← LoreRouter\n├── /api/symbols/* ← SymbolsRouter\n├── /api/graphs/* ← GraphsRouter\n├── /api/platform/* ← PlatformRouter (health, sections, agent-command)\n├── /ws ← WebSocket (agent commands + user activity)\n└── / ← Platform UI SPA\n```\n\n## Agent-Driven UI\n\nThe breakthrough: **the AI agent can drive the browser in real-time.** Five MCP tools let the agent navigate, highlight, annotate, observe, and clear — turning the Platform from a passive viewer into a shared workspace.\n\n### The Pipeline: MCP → HTTP → WebSocket → Browser\n\n```\nAgent (Claude Code) Platform Server Browser\n │ │ │\n │ paradigm_platform_* │ │\n │ POST /api/platform/cmd │ │\n │ ─────────────────────────►│ │\n │ ◄── { ok: true } ──────│ │\n │ │ ws: agent:* │\n │ │──────────────────►│\n │ │ │ UI updates\n```\n\nWhy HTTP not file-based: the <500ms latency requirement rules out file-watching. Why not direct WebSocket from MCP: MCP tools are stdio-based with no event loop for persistent WS connections.\n\n### The Five Tools\n\n| Tool | Purpose |\n|------|---------|\n| `paradigm_platform_navigate` | Switch sections, select symbols, open lore entries |\n| `paradigm_platform_highlight` | Pulsing glow on symbols with color + label, auto-expires |\n| `paradigm_platform_annotate` | Toasts (notifications), callouts (on graph nodes), badges |\n| `paradigm_platform_observe` | Read user's current section, selected symbol, theme, mute state |\n| `paradigm_platform_clear` | Remove all agent highlights and annotations |\n\n### Conflict Resolution: User Always Wins\n\nThe agent must never hijack the user's attention:\n\n- **User idle (>5s):** Agent navigation executes immediately\n- **User active (<5s):** A prompt appears: \"Agent wants to show you #X — [Go there] [Dismiss]\"\n- **User muted:** All agent effects are silently discarded; `observe` returns `{ muted: true }`\n\n### Agent Presence\n\nThe `#AgentPresenceManager` tracks connected agents by their Symphony identity (`{project}/{role}`). Each agent gets a deterministic color from its ID hash. Presence dots appear in the Platform header with a mute toggle.\n\nStale agents are auto-pruned after 2 minutes of inactivity.\n\n### User State Tracking\n\nThe `#UserStateTracker` accumulates user activity — what section they're viewing, what symbol is selected, theme preference. This state is served to `paradigm_platform_observe` so the agent can reason about what the user is looking at.\n\nBrowser clients report activity via WebSocket messages: `user:navigate`, `user:select`, `user:theme`, `user:mute`.\n\n### Visual Treatment\n\n| Element | Human | Agent |\n|---------|-------|-------|\n| Selection ring | Solid 2px blue | Dashed 2px agent-color |\n| Highlight | N/A | Pulsing glow animation |\n| Toast | N/A | Left border + robot icon |\n| Navigation | Instant | 300ms ease + toast notification |\n\n### Browser Architecture\n\nThe agent UI layer sits alongside existing stores:\n\n- `agentStore.ts` — Zustand store managing presence, highlights, annotations, toasts, mute, pending navigation\n- `useAgentEffects` — Hook connecting WebSocket `agent:*` messages to store actions, with auto-reconnect\n- `useActivityReporter` — Hook reporting section/theme changes back to server\n- `AgentToast` — Severity-colored toast component\n- `AgentCallout` — Floating callout overlay + navigation conflict prompt",
|
|
881
|
+
"keyConcepts": [
|
|
882
|
+
"paradigm serve unifies all tools on port 3850 in one browser tab",
|
|
883
|
+
"Agent-Driven UI: 5 MCP tools (navigate, highlight, annotate, observe, clear) control the browser in real-time",
|
|
884
|
+
"Pipeline: MCP → HTTP POST → Platform server → WebSocket broadcast → browser Zustand store → visual effect",
|
|
885
|
+
"Conflict resolution: user idle → auto-navigate, user active → prompt, user muted → silently discard",
|
|
886
|
+
"Agent identity reuses Symphony pattern: {project}/{role} with deterministic color from hash",
|
|
887
|
+
"AgentPresenceManager tracks agents, auto-prunes after 2min idle",
|
|
888
|
+
"UserStateTracker accumulates section/symbol/theme for observe tool",
|
|
889
|
+
"Visual distinction: agent effects use dashed rings, pulsing glow, robot-icon toasts vs. human solid styles"
|
|
890
|
+
],
|
|
891
|
+
"quiz": [
|
|
892
|
+
{
|
|
893
|
+
"id": "q1",
|
|
894
|
+
"question": "An AI agent calls `paradigm_platform_navigate({ section: 'graph', symbol: '#payment-service' })` while the user is actively typing in the lore section. What happens in the browser?",
|
|
895
|
+
"choices": {
|
|
896
|
+
"A": "The browser immediately switches to the graph section and selects the node",
|
|
897
|
+
"B": "The command fails with an error because the user is in a different section",
|
|
898
|
+
"C": "A prompt appears: 'Agent wants to show you #payment-service — [Go there] [Dismiss]' — the user decides",
|
|
899
|
+
"D": "The agent's command is queued and executes when the user next switches sections",
|
|
900
|
+
"E": "The browser switches to graph but keeps the lore section visible in a split view"
|
|
901
|
+
},
|
|
902
|
+
"correct": "C",
|
|
903
|
+
"explanation": "When the user is active (last interaction <5s ago), the agent's navigation creates a pending navigation prompt instead of auto-navigating. The user sees 'Agent wants to show you #payment-service — [Go there] [Dismiss]' and chooses whether to follow. This is the conflict resolution model: user always wins."
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"id": "q2",
|
|
907
|
+
"question": "What is the communication pipeline when an MCP tool like `paradigm_platform_highlight` sends a command to the browser?",
|
|
908
|
+
"choices": {
|
|
909
|
+
"A": "MCP tool → direct WebSocket connection to browser → UI update",
|
|
910
|
+
"B": "MCP tool → writes to file → browser polls file every 500ms → UI update",
|
|
911
|
+
"C": "MCP tool → HTTP POST to Platform server → server broadcasts via WebSocket → browser Zustand store → UI update",
|
|
912
|
+
"D": "MCP tool → writes to scan-index.json → browser watches index file → UI update",
|
|
913
|
+
"E": "MCP tool → sends message via Symphony mailbox → browser reads mailbox → UI update"
|
|
914
|
+
},
|
|
915
|
+
"correct": "C",
|
|
916
|
+
"explanation": "The pipeline is MCP → HTTP POST → WebSocket broadcast → browser. The MCP tool calls the platform-bridge helper which POSTs to /api/platform/agent-command. The server validates the command, updates server-side state (presence, highlights), and broadcasts a typed WebSocket message (e.g., agent:highlight) to all connected browsers. The browser's useAgentEffects hook receives the message and dispatches to the Zustand agentStore."
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
"id": "q3",
|
|
920
|
+
"question": "The user clicks the 'Mute' button in the Platform header. An agent then calls `paradigm_platform_annotate({ type: 'toast', message: 'Found a bug in #auth' })`. What happens?",
|
|
921
|
+
"choices": {
|
|
922
|
+
"A": "The toast appears but with reduced opacity",
|
|
923
|
+
"B": "The command returns `{ annotated: false, reason: 'Agent actions are muted by user' }` and no toast appears",
|
|
924
|
+
"C": "The toast is queued and shown when the user unmutes",
|
|
925
|
+
"D": "The command throws an error that the agent must handle",
|
|
926
|
+
"E": "The toast appears regardless — mute only affects navigation, not annotations"
|
|
927
|
+
},
|
|
928
|
+
"correct": "B",
|
|
929
|
+
"explanation": "When the user mutes agent actions, ALL agent effects are silently discarded — navigate, highlight, annotate, and clear commands all return a response with `reason: 'Agent actions are muted by user'`. The server checks UserStateTracker.isMuted() before broadcasting. The agent can detect this via `paradigm_platform_observe` which returns `{ muted: true }`."
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
"id": "q4",
|
|
933
|
+
"question": "How does the Platform determine an agent's display color in the header presence indicator?",
|
|
934
|
+
"choices": {
|
|
935
|
+
"A": "Each agent chooses its color when connecting via WebSocket",
|
|
936
|
+
"B": "Colors are assigned sequentially from a fixed palette (first agent = blue, second = green, etc.)",
|
|
937
|
+
"C": "The color is deterministically computed from a hash of the agent's Symphony identity string ({project}/{role})",
|
|
938
|
+
"D": "Colors are stored in .paradigm/config.yaml under platform.agentColors",
|
|
939
|
+
"E": "All agents share the same color — they're distinguished by name only"
|
|
940
|
+
},
|
|
941
|
+
"correct": "C",
|
|
942
|
+
"explanation": "Agent colors are deterministic: the AgentPresenceManager computes a hash of the agentId string (e.g., 'a-paradigm/core') and maps it to one of 8 predefined colors. This means the same agent always gets the same color across sessions, making it recognizable. The identity reuses Symphony's {project}/{role} pattern."
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
"id": "q5",
|
|
946
|
+
"question": "An agent wants to understand what the user is currently looking at before deciding what to highlight. Which approach is correct?",
|
|
947
|
+
"choices": {
|
|
948
|
+
"A": "Read the platformStore.ts file to check the activeSection variable",
|
|
949
|
+
"B": "Call `paradigm_platform_observe()` which returns the current section, selected symbol, theme, mute state, and connected agents",
|
|
950
|
+
"C": "Call `paradigm_status` which includes the Platform UI state in its output",
|
|
951
|
+
"D": "Check the browser's localStorage via a Bash command",
|
|
952
|
+
"E": "Call `paradigm_navigate({ intent: 'context' })` which includes Platform UI state"
|
|
953
|
+
},
|
|
954
|
+
"correct": "B",
|
|
955
|
+
"explanation": "paradigm_platform_observe is the dedicated tool for reading UI state. It sends an 'observe' command to the Platform server, which returns the UserStateTracker's accumulated state: current section, selected symbol, theme, mute status, connected agents, and optionally active highlights/annotations. This is real-time data from the server, not a file read."
|
|
956
|
+
}
|
|
957
|
+
]
|
|
792
958
|
}
|
|
793
959
|
]
|
|
794
960
|
}
|
|
@@ -147,3 +147,9 @@ components:
|
|
|
147
147
|
file: v3.0.json
|
|
148
148
|
tags: [plsat, para-101]
|
|
149
149
|
references: ["#cold-start", "#disciplines"]
|
|
150
|
+
|
|
151
|
+
plsat-questions-platform-agent-ui:
|
|
152
|
+
description: "PLSAT questions testing Platform agent-driven UI — MCP→HTTP→WS pipeline, conflict resolution, observe, highlights, presence pruning, useAgentEffects (slots 109-112, para-501)"
|
|
153
|
+
file: v3.0.json
|
|
154
|
+
tags: [plsat, para-501]
|
|
155
|
+
references: ["#PlatformWebSocket", "#AgentPresenceManager", "#UserStateTracker", "#PlatformTools", "#AgentStore"]
|