@lightcone-ai/daemon 0.10.3 → 0.12.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/mcp-servers/official/ai-image-gen/index.js +277 -0
- package/mcp-servers/official/ai-image-gen/manifest.json +19 -0
- package/mcp-servers/official/audience-research/index.js +383 -0
- package/mcp-servers/official/audience-research/manifest.json +18 -0
- package/mcp-servers/official/hook-pattern-library/index.js +417 -0
- package/mcp-servers/official/hook-pattern-library/manifest.json +18 -0
- package/mcp-servers/official/image-search-licensed/index.js +309 -0
- package/mcp-servers/official/image-search-licensed/manifest.json +18 -0
- package/mcp-servers/official/keyword-research/index.js +292 -0
- package/mcp-servers/official/keyword-research/manifest.json +19 -0
- package/mcp-servers/official/platform-policy-db/index.js +316 -0
- package/mcp-servers/official/platform-policy-db/manifest.json +18 -0
- package/mcp-servers/official/publish-window-optimizer/index.js +296 -0
- package/mcp-servers/official/publish-window-optimizer/manifest.json +17 -0
- package/mcp-servers/publisher/adapters/publisher-adapter.js +37 -0
- package/mcp-servers/publisher/adapters/xhs.js +103 -17
- package/mcp-servers/publisher/index.js +123 -25
- package/mcp-servers/publisher/official-tool-client.js +171 -0
- package/mcp-servers/publisher/precheck.js +240 -0
- package/package.json +1 -1
- package/src/agent-manager.js +41 -1
- package/src/chat-bridge.js +163 -3
- package/src/drivers/claude.js +66 -10
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
function pickField(source, keys = []) {
|
|
2
|
+
for (const key of keys) {
|
|
3
|
+
const value = source?.[key];
|
|
4
|
+
if (value === undefined || value === null) continue;
|
|
5
|
+
const asText = typeof value === 'string' ? value.trim() : value;
|
|
6
|
+
if (asText === '') continue;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeBoolean(value, fallback = false) {
|
|
13
|
+
if (value === undefined || value === null) return fallback;
|
|
14
|
+
if (typeof value === 'boolean') return value;
|
|
15
|
+
const normalized = String(value).trim().toLowerCase();
|
|
16
|
+
if (!normalized) return fallback;
|
|
17
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
18
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeRole(value) {
|
|
23
|
+
return String(value ?? '').trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeStringArray(value) {
|
|
27
|
+
if (!Array.isArray(value)) return [];
|
|
28
|
+
return value
|
|
29
|
+
.map(item => String(item ?? '').trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function composeDraftText({ title, text, tags = [] } = {}) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
if (title && String(title).trim()) lines.push(String(title).trim());
|
|
36
|
+
if (text && String(text).trim()) lines.push(String(text).trim());
|
|
37
|
+
const normalizedTags = normalizeStringArray(tags);
|
|
38
|
+
if (normalizedTags.length > 0) {
|
|
39
|
+
lines.push(normalizedTags.map(tag => (tag.startsWith('#') ? tag : `#${tag}`)).join(' '));
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectPayloadFlags(payload = {}) {
|
|
45
|
+
const accountId = pickField(payload, [
|
|
46
|
+
'account_id',
|
|
47
|
+
'accountId',
|
|
48
|
+
'target_account_id',
|
|
49
|
+
'targetAccountId',
|
|
50
|
+
]);
|
|
51
|
+
const accountRole = pickField(payload, [
|
|
52
|
+
'account_role',
|
|
53
|
+
'accountRole',
|
|
54
|
+
'target_account_role',
|
|
55
|
+
'targetAccountRole',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const autoPublishViaDelegation = normalizeBoolean(
|
|
59
|
+
pickField(payload, [
|
|
60
|
+
'auto_publish_via_delegation',
|
|
61
|
+
'autoPublishViaDelegation',
|
|
62
|
+
'auto_publish',
|
|
63
|
+
'autoPublish',
|
|
64
|
+
]),
|
|
65
|
+
false
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const contentIndicatesCollaboration = normalizeBoolean(
|
|
69
|
+
pickField(payload, [
|
|
70
|
+
'content_indicates_collaboration',
|
|
71
|
+
'contentIndicatesCollaboration',
|
|
72
|
+
'is_ad_content',
|
|
73
|
+
'isAdContent',
|
|
74
|
+
'ad_content',
|
|
75
|
+
'adContent',
|
|
76
|
+
]),
|
|
77
|
+
false
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const hasAdDisclosure = normalizeBoolean(
|
|
81
|
+
pickField(payload, [
|
|
82
|
+
'has_ad_disclosure',
|
|
83
|
+
'hasAdDisclosure',
|
|
84
|
+
'ad_disclosure',
|
|
85
|
+
'adDisclosure',
|
|
86
|
+
]),
|
|
87
|
+
false
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const contentContainsAiGenerated = normalizeBoolean(
|
|
91
|
+
pickField(payload, [
|
|
92
|
+
'content_contains_ai_generated',
|
|
93
|
+
'contentContainsAiGenerated',
|
|
94
|
+
'ai_generated',
|
|
95
|
+
'aiGenerated',
|
|
96
|
+
]),
|
|
97
|
+
false
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const hasAiLabel = normalizeBoolean(
|
|
101
|
+
pickField(payload, [
|
|
102
|
+
'has_ai_label',
|
|
103
|
+
'hasAiLabel',
|
|
104
|
+
'ai_labeled',
|
|
105
|
+
'aiLabeled',
|
|
106
|
+
]),
|
|
107
|
+
false
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const publishHistory = Array.isArray(payload?.publish_history)
|
|
111
|
+
? payload.publish_history
|
|
112
|
+
: (Array.isArray(payload?.publishHistory) ? payload.publishHistory : []);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
accountId: accountId ? String(accountId).trim() : null,
|
|
116
|
+
accountRole: normalizeRole(accountRole),
|
|
117
|
+
autoPublishViaDelegation,
|
|
118
|
+
contentIndicatesCollaboration,
|
|
119
|
+
hasAdDisclosure,
|
|
120
|
+
contentContainsAiGenerated,
|
|
121
|
+
hasAiLabel,
|
|
122
|
+
publishHistory,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function summarizePolicyViolations(policyScan = {}) {
|
|
127
|
+
const violations = Array.isArray(policyScan.violations) ? policyScan.violations : [];
|
|
128
|
+
const blockers = [];
|
|
129
|
+
const warnings = [];
|
|
130
|
+
for (const violation of violations) {
|
|
131
|
+
const severity = String(violation?.severity ?? '').trim().toLowerCase();
|
|
132
|
+
const item = {
|
|
133
|
+
code: String(violation?.rule_id ?? 'policy_violation'),
|
|
134
|
+
message: String(violation?.reason ?? violation?.term ?? 'policy violation').trim(),
|
|
135
|
+
snippet: violation?.snippet ?? null,
|
|
136
|
+
action: violation?.action ?? null,
|
|
137
|
+
term: violation?.term ?? null,
|
|
138
|
+
};
|
|
139
|
+
if (severity === 'blocker') blockers.push(item);
|
|
140
|
+
else warnings.push(item);
|
|
141
|
+
}
|
|
142
|
+
return { blockers, warnings };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function evaluatePrimaryAccountProtection(flags) {
|
|
146
|
+
if (flags.accountRole !== 'primary') return null;
|
|
147
|
+
if (!flags.autoPublishViaDelegation) return null;
|
|
148
|
+
return {
|
|
149
|
+
code: 'primary_account_protection',
|
|
150
|
+
message: 'primary account cannot use auto-publish via delegation; manual confirmation is required',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function evaluateLabelBlockers(flags, policyScan = {}) {
|
|
155
|
+
const blockers = [];
|
|
156
|
+
if (policyScan.ai_label_required && flags.contentContainsAiGenerated && !flags.hasAiLabel) {
|
|
157
|
+
blockers.push({
|
|
158
|
+
code: 'policy_ai_label_required',
|
|
159
|
+
message: 'AI-generated content is detected but AI label is missing',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (policyScan.ad_disclosure_required && flags.contentIndicatesCollaboration && !flags.hasAdDisclosure) {
|
|
163
|
+
blockers.push({
|
|
164
|
+
code: 'policy_ad_disclosure_required',
|
|
165
|
+
message: 'ad/collaboration content requires platform disclosure label',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return blockers;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function runPublishPrecheck({
|
|
172
|
+
platform,
|
|
173
|
+
title,
|
|
174
|
+
text,
|
|
175
|
+
tags = [],
|
|
176
|
+
payload = {},
|
|
177
|
+
callTool,
|
|
178
|
+
}) {
|
|
179
|
+
if (typeof callTool !== 'function') {
|
|
180
|
+
throw new Error('callTool function is required');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const draftText = composeDraftText({ title, text, tags });
|
|
184
|
+
const flags = collectPayloadFlags(payload ?? {});
|
|
185
|
+
const blockers = [];
|
|
186
|
+
const warnings = [];
|
|
187
|
+
|
|
188
|
+
const policyScan = await callTool({
|
|
189
|
+
serverId: 'platform-policy-db',
|
|
190
|
+
toolName: 'platform_policy_db',
|
|
191
|
+
argumentsPayload: {
|
|
192
|
+
operation: 'scan_text',
|
|
193
|
+
platform,
|
|
194
|
+
text: draftText,
|
|
195
|
+
max_matches: 20,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const policySummary = summarizePolicyViolations(policyScan);
|
|
199
|
+
blockers.push(...policySummary.blockers);
|
|
200
|
+
warnings.push(...policySummary.warnings);
|
|
201
|
+
|
|
202
|
+
const primaryProtection = evaluatePrimaryAccountProtection(flags);
|
|
203
|
+
if (primaryProtection) blockers.push(primaryProtection);
|
|
204
|
+
|
|
205
|
+
blockers.push(...evaluateLabelBlockers(flags, policyScan));
|
|
206
|
+
|
|
207
|
+
let advisory = null;
|
|
208
|
+
try {
|
|
209
|
+
advisory = await callTool({
|
|
210
|
+
serverId: 'publish-window-optimizer',
|
|
211
|
+
toolName: 'publish_window_optimizer',
|
|
212
|
+
argumentsPayload: {
|
|
213
|
+
platform,
|
|
214
|
+
account_id: flags.accountId ?? undefined,
|
|
215
|
+
history: flags.publishHistory,
|
|
216
|
+
limit: 3,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
if (advisory?.advisory?.level === 'suggest_delay') {
|
|
220
|
+
warnings.push({
|
|
221
|
+
code: 'publish_window_suggest_delay',
|
|
222
|
+
message: String(advisory?.advisory?.message ?? 'recommended publish window suggests delay').trim(),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
warnings.push({
|
|
227
|
+
code: 'publish_window_optimizer_unavailable',
|
|
228
|
+
message: `publish-window-optimizer unavailable: ${error.message}`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
ok: blockers.length === 0,
|
|
234
|
+
blockers,
|
|
235
|
+
warnings,
|
|
236
|
+
policyScan,
|
|
237
|
+
advisory,
|
|
238
|
+
payloadFlags: flags,
|
|
239
|
+
};
|
|
240
|
+
}
|
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -12,6 +12,7 @@ import { homedir } from 'os';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { parseCodexLine, adaptCodexSystemPrompt } from './drivers/codex.js';
|
|
15
|
+
import { injectWorkspaceContext } from './drivers/claude.js';
|
|
15
16
|
import { parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
|
|
16
17
|
import { startSession, stopSession, stopAllSessions } from './browser-login.js';
|
|
17
18
|
import { markInvalidatedLeases } from './governance-state.js';
|
|
@@ -427,6 +428,32 @@ export class AgentManager {
|
|
|
427
428
|
};
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
// Fetches the per-spawn workspace context bundle (Goal State + active
|
|
432
|
+
// context items) so the daemon can inject it into the agent's system
|
|
433
|
+
// prompt. Returns '' when there is nothing to inject; never throws — a
|
|
434
|
+
// network error or absent workspaceId just degrades to empty context, so
|
|
435
|
+
// spawn proceeds without the bundle rather than failing closed.
|
|
436
|
+
async _fetchWorkspaceContextPrompt({ agentId, workspaceId }) {
|
|
437
|
+
if (!workspaceId) return '';
|
|
438
|
+
if (!this.serverUrl || !this.machineApiKey) return '';
|
|
439
|
+
try {
|
|
440
|
+
const url = `${this.serverUrl}/internal/agent/${encodeURIComponent(agentId)}/context?workspaceId=${encodeURIComponent(workspaceId)}`;
|
|
441
|
+
const res = await fetch(url, {
|
|
442
|
+
headers: { 'Authorization': `Bearer ${this.machineApiKey}` },
|
|
443
|
+
});
|
|
444
|
+
if (!res.ok) {
|
|
445
|
+
const text = await res.text();
|
|
446
|
+
console.log(`[AgentManager] Workspace context fetch failed for ${agentId} (non-fatal): ${res.status} ${text.slice(0, 200)}`);
|
|
447
|
+
return '';
|
|
448
|
+
}
|
|
449
|
+
const payload = await res.json();
|
|
450
|
+
return typeof payload?.renderedPrompt === 'string' ? payload.renderedPrompt : '';
|
|
451
|
+
} catch (err) {
|
|
452
|
+
console.log(`[AgentManager] Workspace context fetch error for ${agentId} (non-fatal): ${err.message}`);
|
|
453
|
+
return '';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
430
457
|
async _fetchSpawnDirective({
|
|
431
458
|
agentId,
|
|
432
459
|
workspaceId,
|
|
@@ -642,10 +669,23 @@ export class AgentManager {
|
|
|
642
669
|
...credentialEnvVars,
|
|
643
670
|
};
|
|
644
671
|
|
|
672
|
+
// Inject the assembled workspace context (Goal State + active context
|
|
673
|
+
// items) directly into the system prompt, replacing the legacy
|
|
674
|
+
// "read BRIEF.md on startup" handshake. Empty workspaces leave the prompt
|
|
675
|
+
// unchanged.
|
|
676
|
+
const renderedWorkspaceContext = await this._fetchWorkspaceContextPrompt({
|
|
677
|
+
agentId,
|
|
678
|
+
workspaceId,
|
|
679
|
+
});
|
|
680
|
+
const baseSystemPrompt = typeof directive?.system_prompt === 'string'
|
|
681
|
+
? directive.system_prompt
|
|
682
|
+
: '';
|
|
683
|
+
directive.system_prompt = injectWorkspaceContext(baseSystemPrompt, renderedWorkspaceContext);
|
|
684
|
+
|
|
645
685
|
const runtimeConfig = {
|
|
646
686
|
...config,
|
|
647
687
|
authToken: this.machineApiKey,
|
|
648
|
-
systemPrompt:
|
|
688
|
+
systemPrompt: directive.system_prompt,
|
|
649
689
|
envVars: normalizeObject(directive?.env_vars),
|
|
650
690
|
};
|
|
651
691
|
|
package/src/chat-bridge.js
CHANGED
|
@@ -84,6 +84,10 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
84
84
|
read_memory: 'local',
|
|
85
85
|
upload_image: 'local',
|
|
86
86
|
read_file_base64: 'local',
|
|
87
|
+
write_governance_decision: 'local',
|
|
88
|
+
write_governance_correction: 'local',
|
|
89
|
+
get_orchestrate_context: 'local',
|
|
90
|
+
complete_orchestrate_trigger: 'local',
|
|
87
91
|
|
|
88
92
|
send_message: 'mandatory',
|
|
89
93
|
create_tasks: 'mandatory',
|
|
@@ -97,6 +101,7 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
97
101
|
skill_update: 'mandatory',
|
|
98
102
|
request_approval: 'mandatory',
|
|
99
103
|
execute_approved_action: 'mandatory',
|
|
104
|
+
promote_context: 'mandatory',
|
|
100
105
|
|
|
101
106
|
search_messages: 'cacheable',
|
|
102
107
|
list_server: 'cacheable',
|
|
@@ -126,6 +131,7 @@ const CACHE_INVALIDATION_TOOLS = new Set([
|
|
|
126
131
|
'skill_update',
|
|
127
132
|
'request_approval',
|
|
128
133
|
'execute_approved_action',
|
|
134
|
+
'promote_context',
|
|
129
135
|
]);
|
|
130
136
|
|
|
131
137
|
const governanceContext = {
|
|
@@ -190,6 +196,11 @@ function inferToolForApi(method, apiPath, body) {
|
|
|
190
196
|
if (method === 'PATCH' && cleanPath.startsWith('/skills/')) return 'skill_update';
|
|
191
197
|
if (method === 'POST' && cleanPath === '/actions/request') return 'request_approval';
|
|
192
198
|
if (method === 'POST' && /^\/actions\/[^/]+\/execute$/.test(cleanPath)) return 'execute_approved_action';
|
|
199
|
+
if (method === 'POST' && cleanPath === '/orchestrate/decision') return 'write_governance_decision';
|
|
200
|
+
if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
|
|
201
|
+
if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
|
|
202
|
+
if (method === 'POST' && cleanPath === '/orchestrate/complete') return 'complete_orchestrate_trigger';
|
|
203
|
+
if (method === 'POST' && cleanPath === '/context-proposals') return 'promote_context';
|
|
193
204
|
return null;
|
|
194
205
|
}
|
|
195
206
|
|
|
@@ -767,7 +778,7 @@ server.tool('write_memory', 'Write or update a memory file (full content replace
|
|
|
767
778
|
});
|
|
768
779
|
|
|
769
780
|
// ── list_workspace ────────────────────────────────────────────────────────────
|
|
770
|
-
server.tool('list_workspace', 'List all files in the shared workspace (
|
|
781
|
+
server.tool('list_workspace', 'List all files in the shared workspace (typically artifacts/ and notes/). Use promote_context — not a workspace file — to share workspace-level knowledge with future agents.', {}, async () => {
|
|
771
782
|
if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
|
|
772
783
|
const data = await api('GET', `/workspace-memory?workspaceId=${encodeURIComponent(currentWorkspaceId)}`);
|
|
773
784
|
const files = data.files ?? [];
|
|
@@ -776,7 +787,7 @@ server.tool('list_workspace', 'List all files in the shared workspace (BRIEF.md,
|
|
|
776
787
|
});
|
|
777
788
|
|
|
778
789
|
// ── read_workspace ────────────────────────────────────────────────────────────
|
|
779
|
-
server.tool('read_workspace', 'Read a file from the shared workspace (e.g. "
|
|
790
|
+
server.tool('read_workspace', 'Read a file from the shared workspace (e.g. "artifacts/report.html", "notes/work-log.md"). The active workspace context (Goal State, decisions, knowledge) is already injected into your system prompt — do not use this tool to recover it.', {
|
|
780
791
|
path: z.string().describe('File path relative to workspace root'),
|
|
781
792
|
}, async ({ path }) => {
|
|
782
793
|
if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
|
|
@@ -799,7 +810,7 @@ server.tool('read_workspace', 'Read a file from the shared workspace (e.g. "BRIE
|
|
|
799
810
|
});
|
|
800
811
|
|
|
801
812
|
// ── write_workspace ───────────────────────────────────────────────────────────
|
|
802
|
-
server.tool('write_workspace', 'Write a file to the shared workspace. Use this to save ALL deliverables: code, HTML, scripts, reports, data files, images — everything goes under artifacts/.
|
|
813
|
+
server.tool('write_workspace', 'Write a file to the shared workspace. Use this to save ALL task deliverables: code, HTML, scripts, reports, data files, images — everything goes under artifacts/. Do NOT use this to share workspace-level knowledge across agents — call promote_context instead so the candidate is reviewed and injected into every future agent\'s context. For binary files (images/PNG/JPG), encode as base64 data URL: read the file with fs.readFileSync, then format as "data:image/png;base64," + buf.toString("base64"). The server will decode and serve them correctly.', {
|
|
803
814
|
path: z.string().describe('File path relative to workspace root, e.g. "artifacts/result.html" or "artifacts/cover.png"'),
|
|
804
815
|
content: z.string().describe('File content. For images: base64 data URL "data:image/png;base64,<base64data>"'),
|
|
805
816
|
}, async ({ path, content }) => {
|
|
@@ -962,6 +973,155 @@ server.tool('execute_approved_action',
|
|
|
962
973
|
}
|
|
963
974
|
);
|
|
964
975
|
|
|
976
|
+
// ── promote_context ───────────────────────────────────────────────────────────
|
|
977
|
+
server.tool('promote_context',
|
|
978
|
+
'Submit a workspace-level knowledge candidate for human review. Use this after finishing a task when you discover a stable fact, convention, or learning that future agents in this workspace should know. The candidate appears in the workspace owner\'s "My Context → Pending Proposals" panel; once a human (or the workspace Orchestrator) confirms it, it is auto-injected into every future agent\'s system prompt under "## Workspace context". This is the only sanctioned path for sharing workspace-level knowledge — do NOT write a "knowledge index" file via write_workspace.',
|
|
979
|
+
{
|
|
980
|
+
workspace_id: z.string().optional().describe('Target workspace id. Defaults to your current workspace if omitted.'),
|
|
981
|
+
type: z.enum(['knowledge', 'workspace_norm', 'memory']).optional().describe('Candidate type. Default "knowledge" — use "workspace_norm" for standing rules, "memory" for durable facts.'),
|
|
982
|
+
summary: z.string().describe('One-line title that reviewers will see in the Pending Proposals list.'),
|
|
983
|
+
content: z.string().describe('Full candidate text that future agents will read. Be concrete, citable, and self-contained.'),
|
|
984
|
+
source_message_id: z.string().optional().describe('Optional message id that motivated this candidate, for audit trail.'),
|
|
985
|
+
tags: z.array(z.string()).optional().describe('Optional tags for categorization.'),
|
|
986
|
+
reason: z.string().optional().describe('Optional human-readable rationale stored on the lifecycle event.'),
|
|
987
|
+
},
|
|
988
|
+
async ({ workspace_id, type, summary, content, source_message_id, tags, reason }) => {
|
|
989
|
+
const targetWorkspaceId = (workspace_id ?? currentWorkspaceId ?? WORKSPACE_ID ?? '').trim();
|
|
990
|
+
if (!targetWorkspaceId) {
|
|
991
|
+
return { isError: true, content: [{ type: 'text', text: 'workspace_id is required (no current workspace context).' }] };
|
|
992
|
+
}
|
|
993
|
+
if (!summary?.trim()) {
|
|
994
|
+
return { isError: true, content: [{ type: 'text', text: 'summary is required.' }] };
|
|
995
|
+
}
|
|
996
|
+
if (!content?.trim()) {
|
|
997
|
+
return { isError: true, content: [{ type: 'text', text: 'content is required.' }] };
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
const data = await api('POST', '/context-proposals', {
|
|
1001
|
+
workspaceId: targetWorkspaceId,
|
|
1002
|
+
type: type ?? 'knowledge',
|
|
1003
|
+
summary,
|
|
1004
|
+
content,
|
|
1005
|
+
sourceMessageId: source_message_id,
|
|
1006
|
+
tags,
|
|
1007
|
+
reason,
|
|
1008
|
+
});
|
|
1009
|
+
const proposal = data?.proposal ?? {};
|
|
1010
|
+
return {
|
|
1011
|
+
content: [{
|
|
1012
|
+
type: 'text',
|
|
1013
|
+
text:
|
|
1014
|
+
`Knowledge candidate submitted.\n` +
|
|
1015
|
+
`proposal_id=${proposal.id ?? 'unknown'} workspace=${proposal.workspaceId ?? targetWorkspaceId} status=${proposal.status ?? 'candidate'}\n` +
|
|
1016
|
+
`It is now visible in the workspace owner's "My Context → Pending Proposals" panel; once confirmed, it will be injected into every future agent's "## Workspace context".`,
|
|
1017
|
+
}],
|
|
1018
|
+
};
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
// ── write_governance_decision ─────────────────────────────────────────────────
|
|
1026
|
+
server.tool('write_governance_decision',
|
|
1027
|
+
'Orchestrator-only: write a `decision` context_item (authority=orchestrator) into the workspace Goal State. Use this when adjudicating multi-agent conflicts, codifying long-term routing rules, or recording any decision that should be visible to all workers in this workspace.',
|
|
1028
|
+
{
|
|
1029
|
+
workspace_id: z.string().describe('Target workspace id; you must be the registered orchestrator for it.'),
|
|
1030
|
+
content: z.string().describe('Decision text. Be concrete, auditable, and reusable; avoid hedging language.'),
|
|
1031
|
+
title: z.string().optional().describe('Short title (optional).'),
|
|
1032
|
+
summary: z.string().optional().describe('One-line summary (optional).'),
|
|
1033
|
+
source_risk: z.enum(['trusted', 'normal', 'untrusted', 'hostile']).optional().describe('Source risk classification (default normal).'),
|
|
1034
|
+
tags: z.array(z.string()).optional().describe('Optional tags.'),
|
|
1035
|
+
item_id: z.string().optional().describe('Override the generated context_item id (otherwise a uuid is allocated).'),
|
|
1036
|
+
reason: z.string().optional().describe('Human-readable rationale stored on the lifecycle event.'),
|
|
1037
|
+
},
|
|
1038
|
+
async ({ workspace_id, content, title, summary, source_risk, tags, item_id, reason }) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const data = await api('POST', '/orchestrate/decision', {
|
|
1041
|
+
workspaceId: workspace_id,
|
|
1042
|
+
content,
|
|
1043
|
+
title,
|
|
1044
|
+
summary,
|
|
1045
|
+
sourceRisk: source_risk,
|
|
1046
|
+
tags,
|
|
1047
|
+
itemId: item_id,
|
|
1048
|
+
reason,
|
|
1049
|
+
});
|
|
1050
|
+
return { content: [{ type: 'text', text: `Decision written. context_item_id=${data?.item?.id} version=${data?.item?.version} status=${data?.item?.derivedStatus}` }] };
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
// ── write_governance_correction ───────────────────────────────────────────────
|
|
1058
|
+
server.tool('write_governance_correction',
|
|
1059
|
+
'Orchestrator-only: write a `correction` context_item (authority=orchestrator) into the workspace Goal State. Use this when correcting worker drift, blocker timeouts, or other governance-level course corrections that must propagate to all workers.',
|
|
1060
|
+
{
|
|
1061
|
+
workspace_id: z.string().describe('Target workspace id; you must be the registered orchestrator for it.'),
|
|
1062
|
+
content: z.string().describe('Correction text. Be concrete and actionable; record the deviation observed and the corrective direction.'),
|
|
1063
|
+
title: z.string().optional().describe('Short title (optional).'),
|
|
1064
|
+
summary: z.string().optional().describe('One-line summary (optional).'),
|
|
1065
|
+
source_risk: z.enum(['trusted', 'normal', 'untrusted', 'hostile']).optional().describe('Source risk classification (default normal).'),
|
|
1066
|
+
tags: z.array(z.string()).optional().describe('Optional tags.'),
|
|
1067
|
+
item_id: z.string().optional().describe('Override the generated context_item id (otherwise a uuid is allocated).'),
|
|
1068
|
+
reason: z.string().optional().describe('Human-readable rationale stored on the lifecycle event.'),
|
|
1069
|
+
},
|
|
1070
|
+
async ({ workspace_id, content, title, summary, source_risk, tags, item_id, reason }) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const data = await api('POST', '/orchestrate/correction', {
|
|
1073
|
+
workspaceId: workspace_id,
|
|
1074
|
+
content,
|
|
1075
|
+
title,
|
|
1076
|
+
summary,
|
|
1077
|
+
sourceRisk: source_risk,
|
|
1078
|
+
tags,
|
|
1079
|
+
itemId: item_id,
|
|
1080
|
+
reason,
|
|
1081
|
+
});
|
|
1082
|
+
return { content: [{ type: 'text', text: `Correction written. context_item_id=${data?.item?.id} version=${data?.item?.version} status=${data?.item?.derivedStatus}` }] };
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
// ── get_orchestrate_context ───────────────────────────────────────────────────
|
|
1090
|
+
server.tool('get_orchestrate_context',
|
|
1091
|
+
'Orchestrator-only: fetch the active orchestrator trigger payload bound to this agent (the `processing` row from orchestrator_trigger_queue). Returns null when no trigger is currently bound. Use this at the start of every wake-up turn to recover trigger context, then call complete_orchestrate_trigger when the action is finished.',
|
|
1092
|
+
{
|
|
1093
|
+
workspace_id: z.string().optional().describe('Optional workspace filter; defaults to any workspace this agent is the orchestrator for.'),
|
|
1094
|
+
},
|
|
1095
|
+
async ({ workspace_id }) => {
|
|
1096
|
+
try {
|
|
1097
|
+
const path = workspace_id
|
|
1098
|
+
? `/orchestrate/context?workspaceId=${encodeURIComponent(workspace_id)}`
|
|
1099
|
+
: `/orchestrate/context`;
|
|
1100
|
+
const data = await api('GET', path);
|
|
1101
|
+
if (!data?.trigger) return { content: [{ type: 'text', text: 'No active orchestrator trigger bound to this agent.' }] };
|
|
1102
|
+
return { content: [{ type: 'text', text: JSON.stringify(data.trigger, null, 2) }] };
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
// ── complete_orchestrate_trigger ──────────────────────────────────────────────
|
|
1110
|
+
server.tool('complete_orchestrate_trigger',
|
|
1111
|
+
'Orchestrator-only: mark a processing orchestrator trigger as completed (status processing→completed) and reset its circuit breaker. Call this once you have written all governance actions for the current trigger.',
|
|
1112
|
+
{
|
|
1113
|
+
queue_id: z.number().describe('orchestrator_trigger_queue.id of the trigger to complete (returned by get_orchestrate_context as queueId).'),
|
|
1114
|
+
},
|
|
1115
|
+
async ({ queue_id }) => {
|
|
1116
|
+
try {
|
|
1117
|
+
const data = await api('POST', '/orchestrate/complete', { queueId: queue_id });
|
|
1118
|
+
return { content: [{ type: 'text', text: `Trigger completed. queueId=${data?.queueId} triggerId=${data?.triggerId} status=${data?.status}` }] };
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
|
|
965
1125
|
// ── start ─────────────────────────────────────────────────────────────────────
|
|
966
1126
|
const transport = new StdioServerTransport();
|
|
967
1127
|
await server.connect(transport);
|
package/src/drivers/claude.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
const t = (name) => `mcp__chat__${name}`;
|
|
2
2
|
|
|
3
|
+
// Marker that `injectWorkspaceContext` rewrites into the agent's runtime
|
|
4
|
+
// workspace context block (Goal State + active decisions + knowledge). Kept
|
|
5
|
+
// stable so server-built and daemon-built system prompts can both target it.
|
|
6
|
+
export const WORKSPACE_CONTEXT_PLACEHOLDER = '__WORKSPACE_CONTEXT_BLOCK__';
|
|
7
|
+
|
|
3
8
|
const BASE_PROMPT = (displayName, name, description, agentId, feishuBotName) => `\
|
|
4
9
|
You are "${displayName || name}", an AI agent in lightcone — a collaborative platform for human-AI collaboration.
|
|
5
10
|
${feishuBotName ? `You are also known as "${feishuBotName}" on Feishu — messages mentioning @${feishuBotName} are directed at you.\n` : ''}\
|
|
@@ -8,6 +13,8 @@ ${feishuBotName ? `You are also known as "${feishuBotName}" on Feishu — messag
|
|
|
8
13
|
|
|
9
14
|
Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.
|
|
10
15
|
|
|
16
|
+
${WORKSPACE_CONTEXT_PLACEHOLDER}
|
|
17
|
+
|
|
11
18
|
## Communication — MCP tools ONLY
|
|
12
19
|
|
|
13
20
|
You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
@@ -195,14 +202,14 @@ Your current working directory. Contains:
|
|
|
195
202
|
|
|
196
203
|
### Workspace shared workspace (shared with all agents in this workspace)
|
|
197
204
|
Located one level up from your personal workspace. Contains:
|
|
198
|
-
- \`BRIEF.md\` — **read this on every startup**. Set by humans. Defines workspace mission, conventions, and background.
|
|
199
|
-
- \`KNOWLEDGE.md\` — shared knowledge index. Use \`${t("write_workspace")}\` to record workspace-level learnings here.
|
|
200
205
|
- \`notes/\` — shared research notes and decisions.
|
|
201
206
|
- \`artifacts/\` — **ALL deliverables go here without exception**: code, scripts, HTML pages, data files, reports, images — everything you produce for a task. Use \`${t("write_workspace")}({ path: "artifacts/filename.ext", content: "..." })\` for text files and \`${t("write_workspace_file")}({ file_path: "tmp/local-file.png", path: "artifacts/file.png" })\` for local binary files. Never create deliverable files anywhere else.
|
|
202
207
|
|
|
208
|
+
The active workspace context (Goal State, constraints, decisions, knowledge) is already injected into your system prompt above under "## Workspace context" — you do not need to read or write any standing workspace document to recover it.
|
|
209
|
+
|
|
203
210
|
**Write rule:**
|
|
204
211
|
- Personal learnings → \`${t("write_memory")}\`
|
|
205
|
-
- Workspace-level knowledge → \`${t("
|
|
212
|
+
- Workspace-level knowledge worth sharing across all future agents → \`${t("promote_context")}\` (submits a knowledge candidate; see below). Do **not** dump shared knowledge into ad-hoc files inside the workspace shared workspace.
|
|
206
213
|
- **Any file you produce for a task** → \`${t("write_workspace")}({ path: "artifacts/your-file.ext", ... })\` or \`${t("write_workspace_file")}({ file_path, path: "artifacts/your-file.ext" })\`
|
|
207
214
|
|
|
208
215
|
Temporary local files belong under \`tmp/\` in your personal workspace. If you need to show an image in chat, first save the durable copy to \`artifacts/\`, then optionally call \`${t("upload_image")}\` for a temporary public preview URL.
|
|
@@ -217,15 +224,18 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
|
|
|
217
224
|
- \`${t("list_memory")}()\` — list your personal memory files
|
|
218
225
|
|
|
219
226
|
**Workspace memory** (shared filesystem — visible to all agents in the workspace):
|
|
220
|
-
- \`${t("read_workspace")}({ path })\` — read a workspace file (e.g. \`"
|
|
221
|
-
- \`${t("write_workspace")}({ path, content })\` — write a workspace file
|
|
227
|
+
- \`${t("read_workspace")}({ path })\` — read a workspace file (e.g. \`"artifacts/report.html"\`, \`"notes/work-log.md"\`)
|
|
228
|
+
- \`${t("write_workspace")}({ path, content })\` — write a workspace file (use this for task deliverables under \`artifacts/\`, never for shared "knowledge index" files)
|
|
222
229
|
- \`${t("write_workspace_file")}({ file_path, path })\` — write a local file from your workspace to a workspace artifact without putting base64 in context
|
|
223
230
|
- \`${t("list_workspace")}()\` — list all files in the workspace
|
|
224
231
|
|
|
232
|
+
**Workspace knowledge governance:**
|
|
233
|
+
- \`${t("promote_context")}({ workspace_id, type, summary, content })\` — submit a workspace-level knowledge candidate. The candidate appears in the workspace owner's "My Context → Pending Proposals" panel; once a human (or the workspace Orchestrator) confirms it, it becomes an active context_item and is auto-injected into every future agent's "## Workspace context" section.
|
|
234
|
+
|
|
225
235
|
### Startup sequence (CRITICAL)
|
|
226
236
|
|
|
227
237
|
1. Call \`${t("read_memory")}({ path: "MEMORY.md" })\` to load your personal memory index.
|
|
228
|
-
2.
|
|
238
|
+
2. The active workspace context (Goal State, constraints, decisions, knowledge) is already injected above under "## Workspace context" — read it first; do not fetch any standing workspace document to recover it.
|
|
229
239
|
3. Then check messages and handle work.
|
|
230
240
|
|
|
231
241
|
### MEMORY.md — Your Personal Memory Index
|
|
@@ -254,10 +264,24 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
|
|
|
254
264
|
3. Work history — decisions made, problems solved, approaches that worked or failed
|
|
255
265
|
4. Pointers to your notes files
|
|
256
266
|
|
|
257
|
-
**What belongs in workspace
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
267
|
+
**What belongs in promote_context (workspace-level knowledge candidates):**
|
|
268
|
+
|
|
269
|
+
Use \`${t("promote_context")}\` after finishing a task when you discover something that future agents in this workspace will need to know. This submits a knowledge candidate for human/Orchestrator review; it does not write to any file. Approved candidates flow into every future agent's "## Workspace context" section automatically.
|
|
270
|
+
|
|
271
|
+
Good promote_context content:
|
|
272
|
+
1. Stable facts about the project that all agents need (tech stack, domain conventions, where things live)
|
|
273
|
+
2. Hard-won learnings — non-obvious gotchas, working procedures, conventions you had to discover
|
|
274
|
+
3. Standing workspace norms — "in this workspace we always X" / "never touch Y"
|
|
275
|
+
|
|
276
|
+
Bad promote_context content:
|
|
277
|
+
- Per-task progress, in-flight status, or one-off observations (use messages or your own MEMORY.md)
|
|
278
|
+
- Personal preferences specific to one human (use chat instead so a human can confirm)
|
|
279
|
+
- Untrusted outputs from web search / scraped pages (cite the source in \`content\`; expect the candidate to be reviewed as untrusted)
|
|
280
|
+
|
|
281
|
+
How to call it:
|
|
282
|
+
- \`${t("promote_context")}({ workspace_id: "<this-workspace-id>", type: "knowledge", summary: "one-line title", content: "<full text future agents will read>" })\`
|
|
283
|
+
- Optional: \`source_message_id\` to link the proposal back to the conversation that motivated it; \`tags\` for categorization.
|
|
284
|
+
- The call returns a proposal id; the candidate sits in "My Context → Pending Proposals" until a human confirms or rejects it. Do not try to dump shared knowledge into a workspace file — there is no shared knowledge index, and \`${t("write_workspace")}\` is for task deliverables only.
|
|
261
285
|
|
|
262
286
|
### Compaction safety
|
|
263
287
|
|
|
@@ -338,3 +362,35 @@ export function buildSystemPrompt(config, agentId, skills) {
|
|
|
338
362
|
|
|
339
363
|
return base + skillsPrompt + roleSection;
|
|
340
364
|
}
|
|
365
|
+
|
|
366
|
+
// Replaces the WORKSPACE_CONTEXT_PLACEHOLDER in `systemPrompt` with the
|
|
367
|
+
// rendered workspace context (Goal State + active context items) produced by
|
|
368
|
+
// `assembleAndPersistContextBundle`. When `renderedPrompt` is empty/undefined,
|
|
369
|
+
// the placeholder line is removed entirely so the marker never leaks into the
|
|
370
|
+
// agent's prompt. If the placeholder is missing (e.g. user-supplied
|
|
371
|
+
// systemPrompt override), the rendered context is appended as a trailing
|
|
372
|
+
// `## Workspace context` section so it still reaches the agent.
|
|
373
|
+
export function injectWorkspaceContext(systemPrompt, renderedPrompt) {
|
|
374
|
+
const prompt = typeof systemPrompt === 'string' ? systemPrompt : '';
|
|
375
|
+
const context = typeof renderedPrompt === 'string' ? renderedPrompt.trim() : '';
|
|
376
|
+
const hasPlaceholder = prompt.includes(WORKSPACE_CONTEXT_PLACEHOLDER);
|
|
377
|
+
|
|
378
|
+
if (!context) {
|
|
379
|
+
if (!hasPlaceholder) return prompt;
|
|
380
|
+
// Drop the marker plus the surrounding blank lines that wrap it in
|
|
381
|
+
// BASE_PROMPT so the prompt stays cleanly formatted.
|
|
382
|
+
return prompt.replace(
|
|
383
|
+
new RegExp(`\\n*${WORKSPACE_CONTEXT_PLACEHOLDER}\\n*`),
|
|
384
|
+
'\n\n',
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const block = `## Workspace context\n\n${context}`;
|
|
389
|
+
if (hasPlaceholder) {
|
|
390
|
+
return prompt.split(WORKSPACE_CONTEXT_PLACEHOLDER).join(block);
|
|
391
|
+
}
|
|
392
|
+
// Fall-through: placeholder was stripped by an upstream override. Append
|
|
393
|
+
// the context block so the agent still receives it.
|
|
394
|
+
const sep = prompt.endsWith('\n') ? '\n' : '\n\n';
|
|
395
|
+
return `${prompt}${sep}${block}\n`;
|
|
396
|
+
}
|