@lightcone-ai/daemon 0.9.79 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/mcp-servers/mysql/index.js +13 -5
  2. package/mcp-servers/mysql/manifest.json +16 -0
  3. package/mcp-servers/official/company-fundamentals/index.js +34 -0
  4. package/mcp-servers/official/company-fundamentals/manifest.json +14 -0
  5. package/mcp-servers/official/compliance-check/index.js +49 -0
  6. package/mcp-servers/official/compliance-check/manifest.json +14 -0
  7. package/mcp-servers/official/industry-report/index.js +34 -0
  8. package/mcp-servers/official/industry-report/manifest.json +14 -0
  9. package/mcp-servers/official/market-data-query/index.js +34 -0
  10. package/mcp-servers/official/market-data-query/manifest.json +14 -0
  11. package/mcp-servers/official/portfolio-analysis/index.js +74 -0
  12. package/mcp-servers/official/portfolio-analysis/manifest.json +14 -0
  13. package/mcp-servers/official/portfolio-read/index.js +34 -0
  14. package/mcp-servers/official/portfolio-read/manifest.json +14 -0
  15. package/mcp-servers/official/research-fetch/index.js +35 -0
  16. package/mcp-servers/official/research-fetch/manifest.json +14 -0
  17. package/mcp-servers/official/risk-metrics/index.js +34 -0
  18. package/mcp-servers/official/risk-metrics/manifest.json +14 -0
  19. package/mcp-servers/official-common/fixtures.js +273 -0
  20. package/mcp-servers/official-common/server.js +34 -0
  21. package/mcp-servers/platform/manifest.json +15 -0
  22. package/mcp-servers/portfolio-analysis/core.js +592 -0
  23. package/mcp-servers/portfolio-analysis/index.js +45 -0
  24. package/mcp-servers/portfolio-analysis/package-lock.json +1139 -0
  25. package/mcp-servers/portfolio-analysis/package.json +10 -0
  26. package/mcp-servers/portfolio-read/core.js +330 -0
  27. package/mcp-servers/portfolio-read/index.js +127 -0
  28. package/mcp-servers/portfolio-read/package-lock.json +1243 -0
  29. package/mcp-servers/portfolio-read/package.json +11 -0
  30. package/mcp-servers/publisher/index.js +14 -14
  31. package/mcp-servers/publisher/manifest.json +16 -0
  32. package/package.json +4 -2
  33. package/src/_vendor/mcp/registry.js +327 -0
  34. package/src/agent-manager.js +761 -188
  35. package/src/chat-bridge.js +567 -92
  36. package/src/connection.js +1 -1
  37. package/src/drivers/claude.js +48 -45
  38. package/src/drivers/codex.js +110 -8
  39. package/src/drivers/kimi.js +80 -35
  40. package/src/governance-state.js +89 -0
  41. package/src/index.js +34 -16
  42. package/src/lease-window.js +8 -0
  43. package/src/mcp-config.js +52 -23
package/src/connection.js CHANGED
@@ -58,7 +58,7 @@ export class DaemonConnection {
58
58
  try { msg = JSON.parse(raw.toString()); }
59
59
  catch { return; }
60
60
  if (msg.type !== 'pong') {
61
- console.log(`[Connection] ← ${msg.type}${msg.agentId ? ` agent=${msg.agentId.slice(0,8)}` : ''}${msg.teamId ? ` team=${msg.teamId.slice(0,8)}` : ''}${msg.seq != null ? ` seq=${msg.seq}` : ''}`);
61
+ console.log(`[Connection] ← ${msg.type}${msg.agentId ? ` agent=${msg.agentId.slice(0,8)}` : ''}${msg.workspaceId ? ` workspace=${msg.workspaceId.slice(0,8)}` : ''}${msg.seq != null ? ` seq=${msg.seq}` : ''}`);
62
62
  }
63
63
  this.onMessage(msg);
64
64
  });
@@ -1,7 +1,7 @@
1
1
  const t = (name) => `mcp__chat__${name}`;
2
2
 
3
3
  const BASE_PROMPT = (displayName, name, description, agentId, feishuBotName) => `\
4
- You are "${displayName || name}", an AI agent in Lightcone — a collaborative platform for human-AI collaboration.
4
+ You are "${displayName || name}", an AI agent in lightcone — a collaborative platform for human-AI collaboration.
5
5
  ${feishuBotName ? `You are also known as "${feishuBotName}" on Feishu — messages mentioning @${feishuBotName} are directed at you.\n` : ''}\
6
6
 
7
7
  ## Who you are
@@ -13,11 +13,11 @@ Your workspace and MEMORY.md persist across turns, so you can recover context wh
13
13
  You have MCP tools from the "chat" server. Use ONLY these for communication:
14
14
 
15
15
  1. **${t("check_messages")}** — Non-blocking check for new messages. Use freely during work — at natural breakpoints or after notifications.
16
- 2. **${t("send_message")}** — Send a message to a team or DM.
17
- 3. **${t("list_server")}** — List all teams in this server, which ones you have joined, plus all agents and humans.
16
+ 2. **${t("send_message")}** — Send a message to a workspace or DM.
17
+ 3. **${t("list_server")}** — List all workspaces in this server, which ones you have joined, plus all agents and humans.
18
18
  4. **${t("search_messages")}** — Search messages visible to you by keyword.
19
- 5. **${t("list_tasks")}** — View a team's task board.
20
- 6. **${t("create_tasks")}** — Create new task-messages in a team (supports batch; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
19
+ 5. **${t("list_tasks")}** — View a workspace's task board.
20
+ 6. **${t("create_tasks")}** — Create new task-messages in a workspace (supports batch; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
21
21
  7. **${t("claim_tasks")}** — Claim tasks by number (supports batch, handles conflicts).
22
22
  8. **${t("unclaim_task")}** — Release your claim on a task.
23
23
  9. **${t("update_task_status")}** — Change a task's status (e.g. to in_review or done).
@@ -57,16 +57,16 @@ Header fields:
57
57
 
58
58
  ### Sending messages
59
59
 
60
- - **Reply to a team**: \`send_message(target="#team-name", content="...")\`
60
+ - **Reply to a workspace**: \`send_message(target="#workspace-name", content="...")\`
61
61
  - **Reply to a DM**: \`send_message(target="dm:@peer-name", content="...")\`
62
- - **Reply in a thread**: \`send_message(target="#team:shortid", content="...")\` or \`send_message(target="dm:@peer:shortid", content="...")\`
62
+ - **Reply in a thread**: \`send_message(target="#workspace:shortid", content="...")\` or \`send_message(target="dm:@peer:shortid", content="...")\`
63
63
  - **Start a NEW DM**: \`send_message(target="dm:@person-name", content="...")\`
64
64
 
65
- **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place — whether it's a team, DM, or thread.
65
+ **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place — whether it's a workspace, DM, or thread.
66
66
 
67
67
  ### Threads
68
68
 
69
- Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main team.
69
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main workspace.
70
70
 
71
71
  - **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
72
72
  - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
@@ -74,16 +74,16 @@ Threads are sub-conversations attached to a specific message. They let you discu
74
74
  - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
75
75
  - Threads cannot be nested — you cannot start a thread inside a thread.
76
76
 
77
- ### Discovering people and teams
77
+ ### Discovering people and workspaces
78
78
 
79
- Call \`list_server\` to see all teams in this server, which ones you have joined, other agents, and humans.
79
+ Call \`list_server\` to see all workspaces in this server, which ones you have joined, other agents, and humans.
80
80
 
81
- ### Team awareness
81
+ ### Workspace awareness
82
82
 
83
- Each team has a **name** and optionally a **description** that define its purpose (visible via \`list_server\`). Respect them:
84
- - **Reply in context** — always respond in the team/thread the message came from.
85
- - **Stay on topic** — when proactively sharing results or updates, post in the team most relevant to the work. Don't scatter messages across unrelated teams.
86
- - If unsure where something belongs, call \`list_server\` to review team descriptions.
83
+ Each workspace has a **name** and optionally a **description** that define its purpose (visible via \`list_server\`). Respect them:
84
+ - **Reply in context** — always respond in the workspace/thread the message came from.
85
+ - **Stay on topic** — when proactively sharing results or updates, post in the workspace most relevant to the work. Don't scatter messages across unrelated workspaces.
86
+ - If unsure where something belongs, call \`list_server\` to review workspace descriptions.
87
87
 
88
88
  ### Tasks
89
89
 
@@ -96,7 +96,7 @@ When someone sends a message that asks you to do something — fix a bug, write
96
96
  - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
97
97
  - A system notification about task changes: \`📋 Alice converted a message to task #3 "Fix the login bug"\`
98
98
 
99
- Only top-level team / DM messages can become tasks. Messages inside threads are discussion context — reply there, but keep claims and conversions to top-level messages.
99
+ Only top-level workspace / DM messages can become tasks. Messages inside threads are discussion context — reply there, but keep claims and conversions to top-level messages.
100
100
 
101
101
  **Status flow:** \`todo\` → \`in_progress\` → \`in_review\` → \`done\`
102
102
 
@@ -105,7 +105,7 @@ Only top-level team / DM messages can become tasks. Messages inside threads are
105
105
  **Workflow:**
106
106
  1. Receive a message that requires action → claim it first (by task number if already a task, or by message ID if it's a regular message)
107
107
  2. If the claim fails, someone else is working on it — move on to another task
108
- 3. Post updates in the task's thread: \`send_message(target="#team:msgShortId", ...)\`
108
+ 3. Post updates in the task's thread: \`send_message(target="#workspace:msgShortId", ...)\`
109
109
  4. When done, set status to \`in_review\` so a human can validate
110
110
  5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
111
111
 
@@ -135,12 +135,12 @@ When you receive a notification about new tasks, check the task board and claim
135
135
 
136
136
  ## @Mentions
137
137
 
138
- In team group chats, you can @mention people by their unique name (e.g. "@alice" or "@bob").
139
- - Your stable Lightcone @mention handle is \`@${name}\`.
138
+ In workspace group chats, you can @mention people by their unique name (e.g. "@alice" or "@bob").
139
+ - Your stable lightcone @mention handle is \`@${name}\`.
140
140
  - Your display name is \`${displayName || name}\`. Treat it as presentation only — when reasoning about identity and @mentions, prefer your stable \`name\`.
141
141
  - Every human and agent has a unique \`name\` — this is their stable identifier for @mentions.
142
142
  - Mention others, not yourself — assign reviews and follow-ups to teammates.
143
- - @mentions only reach people inside the teamteams are the isolation boundary.
143
+ - @mentions only reach people inside the workspaceworkspaces are the isolation boundary.
144
144
 
145
145
  ## Communication style
146
146
 
@@ -156,14 +156,14 @@ Keep the user informed. They cannot see your internal reasoning, so:
156
156
  - **Only respond when relevant.** If a message does not @mention you and is not clearly directed at you or your expertise, do NOT respond. Let the appropriate agent handle it.
157
157
  - **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work — let them respond to questions about it.
158
158
  - **Claim before you start.** Always call \`${t("claim_tasks")}\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
159
- - **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or team before stopping.
159
+ - **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or workspace before stopping.
160
160
  - **Skip idle narration.** Only send messages when you have actionable content — avoid broadcasting that you are waiting or idle.
161
161
 
162
162
  ### Formatting — No HTML
163
163
 
164
- Use plain-text @mentions (e.g. \`@alice\`) and #team references (e.g. \`#general\`, \`#1\`) — no HTML tags.
164
+ Use plain-text @mentions (e.g. \`@alice\`) and #workspace references (e.g. \`#general\`, \`#1\`) — no HTML tags.
165
165
 
166
- When referencing a team or mentioning someone, write them as plain text without backticks. Backtick-wrapped mentions render as code instead of interactive links.
166
+ When referencing a workspace or mentioning someone, write them as plain text without backticks. Backtick-wrapped mentions render as code instead of interactive links.
167
167
 
168
168
  ### Formatting — URLs in non-English text
169
169
 
@@ -178,7 +178,7 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
178
178
  - You have filesystem access via Bash, Read, Write, Edit tools.
179
179
  - **You MUST only read/write files inside your workspace directories.** Never modify files outside these paths:
180
180
  - Your personal workspace (shown at startup)
181
- - The team shared workspace (one level up)
181
+ - The workspace shared workspace (one level up)
182
182
  - **NEVER touch other projects or directories outside your workspace roots** without explicit permission from a human in this conversation.
183
183
  - If a task requires modifying an external codebase, **ask for explicit authorization first**, stating exactly which files you intend to change.
184
184
 
@@ -186,23 +186,23 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
186
186
 
187
187
  Your workspace is organized in two layers:
188
188
 
189
- ### Personal workspace (this team only)
189
+ ### Personal workspace (this workspace only)
190
190
  Your current working directory. Contains:
191
- - \`MEMORY.md\` — your memory index for this team context (managed via \`${t("read_memory")}\` / \`${t("write_memory")}\`)
192
- - \`notes/\` — your personal notes for this team
191
+ - \`MEMORY.md\` — your memory index for this workspace context (managed via \`${t("read_memory")}\` / \`${t("write_memory")}\`)
192
+ - \`notes/\` — your personal notes for this workspace
193
193
  - \`tmp/\` — in-progress work files
194
194
  - \`.skills/\` — **read-only**, system-injected skill files. Each \`.md\` file is a skill bound to you. Read them with your Read tool — they are already on disk.
195
195
 
196
- ### Team shared workspace (shared with all agents in this team)
196
+ ### Workspace shared workspace (shared with all agents in this workspace)
197
197
  Located one level up from your personal workspace. Contains:
198
- - \`BRIEF.md\` — **read this on every startup**. Set by humans. Defines team mission, conventions, and background.
199
- - \`KNOWLEDGE.md\` — shared knowledge index. Use \`${t("write_workspace")}\` to record team-level learnings here.
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
200
  - \`notes/\` — shared research notes and decisions.
201
201
  - \`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
202
 
203
203
  **Write rule:**
204
204
  - Personal learnings → \`${t("write_memory")}\`
205
- - Team-level knowledge → \`${t("write_workspace")}({ path: "KNOWLEDGE.md", ... })\`
205
+ - Workspace-level knowledge → \`${t("write_workspace")}({ path: "KNOWLEDGE.md", ... })\`
206
206
  - **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
207
 
208
208
  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.
@@ -211,21 +211,21 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
211
211
 
212
212
  ## Memory MCP tools
213
213
 
214
- **Personal memory** (per-agent, per-team — stored server-side):
214
+ **Personal memory** (per-agent, per-workspace — stored server-side):
215
215
  - \`${t("read_memory")}({ path })\` — read a personal memory file (e.g. \`"MEMORY.md"\`)
216
216
  - \`${t("write_memory")}({ path, content })\` — save a personal memory file (full replace)
217
217
  - \`${t("list_memory")}()\` — list your personal memory files
218
218
 
219
- **Team memory** (shared filesystem — visible to all agents in the team):
220
- - \`${t("read_workspace")}({ path })\` — read a team workspace file (e.g. \`"BRIEF.md"\`, \`"KNOWLEDGE.md"\`)
221
- - \`${t("write_workspace")}({ path, content })\` — write a team workspace file
222
- - \`${t("write_workspace_file")}({ file_path, path })\` — write a local file from your workspace to a team workspace artifact without putting base64 in context
223
- - \`${t("list_workspace")}()\` — list all files in the team workspace
219
+ **Workspace memory** (shared filesystem — visible to all agents in the workspace):
220
+ - \`${t("read_workspace")}({ path })\` — read a workspace file (e.g. \`"BRIEF.md"\`, \`"KNOWLEDGE.md"\`)
221
+ - \`${t("write_workspace")}({ path, content })\` — write a workspace file
222
+ - \`${t("write_workspace_file")}({ file_path, path })\` — write a local file from your workspace to a workspace artifact without putting base64 in context
223
+ - \`${t("list_workspace")}()\` — list all files in the workspace
224
224
 
225
225
  ### Startup sequence (CRITICAL)
226
226
 
227
227
  1. Call \`${t("read_memory")}({ path: "MEMORY.md" })\` to load your personal memory index.
228
- 2. Call \`${t("read_workspace")}({ path: "BRIEF.md" })\` to read the team brief. If empty, proceed normally.
228
+ 2. Call \`${t("read_workspace")}({ path: "BRIEF.md" })\` to read the workspace brief. If empty, proceed normally.
229
229
  3. Then check messages and handle work.
230
230
 
231
231
  ### MEMORY.md — Your Personal Memory Index
@@ -236,7 +236,7 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
236
236
  # <Your Name>
237
237
 
238
238
  ## Role
239
- <your role in this team, evolved over time>
239
+ <your role in this workspace, evolved over time>
240
240
 
241
241
  ## Key Knowledge
242
242
  - Read notes/work-log.md for decisions and completed work
@@ -249,15 +249,15 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
249
249
  \`\`\`
250
250
 
251
251
  **What belongs in personal memory:**
252
- 1. Your role and how it has evolved in this team
252
+ 1. Your role and how it has evolved in this workspace
253
253
  2. User preferences and communication style
254
254
  3. Work history — decisions made, problems solved, approaches that worked or failed
255
255
  4. Pointers to your notes files
256
256
 
257
- **What belongs in team memory (KNOWLEDGE.md):**
257
+ **What belongs in workspace memory (KNOWLEDGE.md):**
258
258
  1. Facts about the project that all agents need (tech stack, domain conventions)
259
259
  2. Shared learnings — things you discovered that teammates would benefit from
260
- 3. Ongoing team context — which tasks are in progress across all agents
260
+ 3. Ongoing workspace context — which tasks are in progress across all agents
261
261
 
262
262
  ### Compaction safety
263
263
 
@@ -305,7 +305,7 @@ Use \`skill_read\` to load full content when needed.
305
305
  - Load a skill's full procedure: \`skill_read({ name: "skill-name" })\`
306
306
  - After completing a complex task (5+ tool calls), consider saving it as a skill: \`skill_create({ name, description, content })\`
307
307
  - When using a skill and finding it outdated or wrong, update it immediately: \`skill_update({ name, content })\`
308
- - Skills you create are automatically shared with other agents in the same team
308
+ - Skills you create are automatically shared with other agents in the same workspace
309
309
 
310
310
  ### Available Skills
311
311
  `;
@@ -323,12 +323,15 @@ Use \`skill_read\` to load full content when needed.
323
323
  }
324
324
 
325
325
  export function buildSystemPrompt(config, agentId, skills) {
326
+ if (typeof config?.systemPrompt === 'string' && config.systemPrompt.trim()) {
327
+ return config.systemPrompt;
328
+ }
326
329
  const { name, displayName, description, feishuBotName, rolePrompt } = config;
327
330
 
328
331
  const base = BASE_PROMPT(displayName, name, description, agentId, feishuBotName);
329
332
 
330
333
  const roleSection = rolePrompt
331
- ? `\n\n## Your role in this team\n\n${rolePrompt}`
334
+ ? `\n\n## Your role in this workspace\n\n${rolePrompt}`
332
335
  : '';
333
336
 
334
337
  const skillsPrompt = buildSkillsPrompt(skills);
@@ -4,7 +4,14 @@ import path from 'path';
4
4
  import { buildSystemPrompt as buildClaudeSystemPrompt } from './claude.js';
5
5
  import { buildSkillMcpServers } from '../mcp-config.js';
6
6
 
7
- function buildChatBridgeArgs(chatBridgePath, { agentId, teamId, serverUrl, authToken, workspaceDir }) {
7
+ function buildChatBridgeArgs(chatBridgePath, {
8
+ agentId,
9
+ workspaceId,
10
+ serverUrl,
11
+ authToken,
12
+ workspaceDir,
13
+ governanceEnv = {},
14
+ }) {
8
15
  const args = [
9
16
  chatBridgePath,
10
17
  '--agent-id', agentId,
@@ -12,7 +19,19 @@ function buildChatBridgeArgs(chatBridgePath, { agentId, teamId, serverUrl, authT
12
19
  '--auth-token', authToken,
13
20
  '--workspace-dir', workspaceDir,
14
21
  ];
15
- if (teamId) args.push('--team-id', teamId);
22
+ if (workspaceId) args.push('--workspace-id', workspaceId);
23
+ if (governanceEnv.GOVERNANCE_SPAWN_BUNDLE_ID) {
24
+ args.push('--spawn-bundle-id', governanceEnv.GOVERNANCE_SPAWN_BUNDLE_ID);
25
+ }
26
+ if (governanceEnv.GOVERNANCE_POLICY_VERSION) {
27
+ args.push('--policy-version', governanceEnv.GOVERNANCE_POLICY_VERSION);
28
+ }
29
+ if (governanceEnv.GOVERNANCE_POLICY_LEASE) {
30
+ args.push('--policy-lease', governanceEnv.GOVERNANCE_POLICY_LEASE);
31
+ }
32
+ if (governanceEnv.GOVERNANCE_MCP_CLASSIFICATION) {
33
+ args.push('--mcp-classification', governanceEnv.GOVERNANCE_MCP_CLASSIFICATION);
34
+ }
16
35
  return args;
17
36
  }
18
37
 
@@ -47,8 +66,11 @@ function ensureGitRepo(workspaceDir) {
47
66
  });
48
67
  }
49
68
 
50
- export function buildCodexSystemPrompt(config, agentId) {
51
- let prompt = buildClaudeSystemPrompt(config, agentId)
69
+ export function adaptCodexSystemPrompt(sourcePrompt) {
70
+ const basePrompt = typeof sourcePrompt === 'string' ? sourcePrompt : '';
71
+ if (!basePrompt.trim()) return '';
72
+
73
+ let prompt = basePrompt
52
74
  .replaceAll('mcp__chat__', '')
53
75
  .replace(
54
76
  '3. If there is no concrete incoming message to handle, stop and wait. New messages will be delivered to you automatically via stdin.',
@@ -59,15 +81,94 @@ export function buildCodexSystemPrompt(config, agentId) {
59
81
  '5. **Complete ALL your work before stopping.** If a task requires multi-step work (research, code changes, testing), finish everything, report results, then stop. Your process exits after each turn and will be restarted automatically for new work.'
60
82
  );
61
83
 
62
- prompt += '\n\nIMPORTANT: Do not wait for stdin notifications. Finish the current turn completely, then stop.';
84
+ const codexStopRule = 'IMPORTANT: Do not wait for stdin notifications. Finish the current turn completely, then stop.';
85
+ if (!prompt.includes(codexStopRule)) {
86
+ prompt += `\n\n${codexStopRule}`;
87
+ }
63
88
  return prompt;
64
89
  }
65
90
 
91
+ export function buildCodexSystemPrompt(config, agentId) {
92
+ const sourcePrompt = typeof config?.systemPrompt === 'string' && config.systemPrompt.trim()
93
+ ? config.systemPrompt
94
+ : buildClaudeSystemPrompt(config, agentId);
95
+ return adaptCodexSystemPrompt(sourcePrompt);
96
+ }
97
+
98
+ function normalizeDirectiveMcpServers(value) {
99
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
100
+ const normalized = {};
101
+ for (const [serverKey, rawServer] of Object.entries(value)) {
102
+ if (!rawServer || typeof rawServer !== 'object' || Array.isArray(rawServer)) continue;
103
+ const command = typeof rawServer.command === 'string' ? rawServer.command.trim() : '';
104
+ if (!command) continue;
105
+ normalized[serverKey] = {
106
+ command,
107
+ args: Array.isArray(rawServer.args) ? rawServer.args.map(item => String(item)) : [],
108
+ env: rawServer.env && typeof rawServer.env === 'object' && !Array.isArray(rawServer.env)
109
+ ? Object.fromEntries(Object.entries(rawServer.env).map(([k, v]) => [k, String(v ?? '')]))
110
+ : {},
111
+ required: rawServer.required === true,
112
+ };
113
+ }
114
+ return normalized;
115
+ }
116
+
117
+ function buildDirectiveMcpFlags(mcpServers) {
118
+ const args = [];
119
+ for (const [serverKey, mc] of Object.entries(mcpServers)) {
120
+ const envPairs = Object.entries(mc.env ?? {}).map(([k, v]) => `${k}=${v ?? ''}`);
121
+ if (envPairs.length > 0) {
122
+ args.push(
123
+ '-c', `mcp_servers.${quote(serverKey)}.command=${quote('env')}`,
124
+ '-c', `mcp_servers.${quote(serverKey)}.args=${quote([...envPairs, mc.command, ...(mc.args ?? [])])}`,
125
+ '-c', `mcp_servers.${quote(serverKey)}.enabled=true`
126
+ );
127
+ } else {
128
+ args.push(
129
+ '-c', `mcp_servers.${quote(serverKey)}.command=${quote(mc.command)}`,
130
+ '-c', `mcp_servers.${quote(serverKey)}.args=${quote(mc.args ?? [])}`,
131
+ '-c', `mcp_servers.${quote(serverKey)}.enabled=true`
132
+ );
133
+ }
134
+ if (mc.required) {
135
+ args.push('-c', `mcp_servers.${quote(serverKey)}.required=true`);
136
+ }
137
+ }
138
+ return args;
139
+ }
140
+
66
141
  export function buildCodexSpawn({
67
- config, agentId, teamId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, prompt, skills, credentialGrants,
142
+ config, agentId, workspaceId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, prompt, skills, credentialGrants, directive = null,
68
143
  }) {
69
144
  ensureGitRepo(workspaceDir);
70
145
 
146
+ if (directive && typeof directive === 'object') {
147
+ const baseArgs = Array.isArray(directive.spawn_args) && directive.spawn_args.length > 0
148
+ ? directive.spawn_args.map(item => String(item))
149
+ : ['exec', '--dangerously-bypass-approvals-and-sandbox', '--json'];
150
+ const promptText = (typeof directive.system_prompt === 'string' && directive.system_prompt.trim())
151
+ ? directive.system_prompt
152
+ : (prompt || buildCodexSystemPrompt(config, agentId));
153
+ const mcpServers = normalizeDirectiveMcpServers(directive.mcp_servers);
154
+ if (!baseArgs.some(arg => String(arg).includes('mcp_servers.')) && Object.keys(mcpServers).length > 0) {
155
+ baseArgs.push(...buildDirectiveMcpFlags(mcpServers));
156
+ }
157
+ const args = baseArgs.map(arg => String(arg).replaceAll('__SYSTEM_PROMPT__', promptText));
158
+ const hasPromptPlaceholder = baseArgs.some(arg => String(arg).includes('__SYSTEM_PROMPT__'));
159
+ if (!hasPromptPlaceholder) {
160
+ args.push(promptText);
161
+ }
162
+ const env = {
163
+ ...process.env,
164
+ FORCE_COLOR: '0',
165
+ NO_COLOR: '1',
166
+ ...(config.envVars ?? {}),
167
+ ...(directive.env_vars ?? {}),
168
+ };
169
+ return { args, env };
170
+ }
171
+
71
172
  const args = ['exec'];
72
173
  if (config.sessionId) {
73
174
  args.push('resume', config.sessionId);
@@ -75,10 +176,11 @@ export function buildCodexSpawn({
75
176
 
76
177
  const bridgeArgs = buildChatBridgeArgs(chatBridgePath, {
77
178
  agentId,
78
- teamId,
179
+ workspaceId,
79
180
  serverUrl,
80
181
  authToken: config.authToken || machineApiKey,
81
182
  workspaceDir,
183
+ governanceEnv: config.envVars ?? {},
82
184
  });
83
185
 
84
186
  args.push(
@@ -97,7 +199,7 @@ export function buildCodexSpawn({
97
199
  credentialGrants,
98
200
  config,
99
201
  agentId,
100
- teamId,
202
+ workspaceId,
101
203
  workspaceDir,
102
204
  serverUrl,
103
205
  authToken: config.authToken || machineApiKey,
@@ -9,11 +9,30 @@ const KIMI_SYSTEM_PROMPT_FILE = '.lightcone-kimi-system.md';
9
9
  const KIMI_AGENT_FILE = '.lightcone-kimi-agent.yaml';
10
10
  const KIMI_MCP_FILE = '.lightcone-kimi-mcp.json';
11
11
 
12
+ function normalizeDirectiveMcpServers(value) {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
14
+ const normalized = {};
15
+ for (const [serverKey, rawServer] of Object.entries(value)) {
16
+ if (!rawServer || typeof rawServer !== 'object' || Array.isArray(rawServer)) continue;
17
+ const command = typeof rawServer.command === 'string' ? rawServer.command.trim() : '';
18
+ if (!command) continue;
19
+ normalized[serverKey] = {
20
+ command,
21
+ args: Array.isArray(rawServer.args) ? rawServer.args.map(item => String(item)) : [],
22
+ env: rawServer.env && typeof rawServer.env === 'object' && !Array.isArray(rawServer.env)
23
+ ? Object.fromEntries(Object.entries(rawServer.env).map(([k, v]) => [k, String(v ?? '')]))
24
+ : {},
25
+ required: rawServer.required === true,
26
+ };
27
+ }
28
+ return normalized;
29
+ }
30
+
12
31
  /**
13
32
  * Build Kimi CLI spawn args and config files.
14
33
  * Returns { args, env, setupFiles() } ready for spawn('kimi', args, { env }).
15
34
  */
16
- export function buildKimiSpawn({ config, agentId, teamId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, skills, credentialGrants }) {
35
+ export function buildKimiSpawn({ config, agentId, workspaceId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, skills, credentialGrants, directive = null }) {
17
36
  const isResume = !!config.sessionId;
18
37
  const sessionId = config.sessionId || randomUUID();
19
38
 
@@ -22,7 +41,11 @@ export function buildKimiSpawn({ config, agentId, teamId, workspaceDir, chatBrid
22
41
  const mcpConfigPath = path.join(workspaceDir, KIMI_MCP_FILE);
23
42
 
24
43
  // Build system prompt (reuse claude's prompt builder)
25
- const prompt = buildClaudeSystemPrompt(config, agentId);
44
+ const prompt = (directive && typeof directive.system_prompt === 'string' && directive.system_prompt.trim())
45
+ ? directive.system_prompt
46
+ : (typeof config?.systemPrompt === 'string' && config.systemPrompt.trim())
47
+ ? config.systemPrompt
48
+ : buildClaudeSystemPrompt(config, agentId);
26
49
 
27
50
  // Write system prompt file (skip if resuming and file exists)
28
51
  if (!isResume || !existsSync(systemPromptPath)) {
@@ -39,47 +62,69 @@ export function buildKimiSpawn({ config, agentId, teamId, workspaceDir, chatBrid
39
62
  ].join('\n'), 'utf8');
40
63
 
41
64
  // Build MCP config
42
- const mcpServers = {
43
- chat: {
44
- command: 'node',
45
- args: [chatBridgePath],
46
- env: {
47
- SERVER_URL: serverUrl,
48
- MACHINE_API_KEY: config.authToken,
49
- AGENT_ID: agentId,
50
- TEAM_ID: teamId ?? '',
51
- WORKSPACE_DIR: workspaceDir,
65
+ let mcpServers = normalizeDirectiveMcpServers(directive?.mcp_servers);
66
+ if (Object.keys(mcpServers).length === 0) {
67
+ mcpServers = {
68
+ chat: {
69
+ command: 'node',
70
+ args: [chatBridgePath],
71
+ env: {
72
+ SERVER_URL: serverUrl,
73
+ MACHINE_API_KEY: config.authToken || machineApiKey,
74
+ AGENT_ID: agentId,
75
+ WORKSPACE_ID: workspaceId ?? '',
76
+ WORKSPACE_DIR: workspaceDir,
77
+ GOVERNANCE_SPAWN_BUNDLE_ID: config.envVars?.GOVERNANCE_SPAWN_BUNDLE_ID ?? '',
78
+ GOVERNANCE_POLICY_VERSION: config.envVars?.GOVERNANCE_POLICY_VERSION ?? '',
79
+ GOVERNANCE_POLICY_LEASE: config.envVars?.GOVERNANCE_POLICY_LEASE ?? '',
80
+ GOVERNANCE_MCP_CLASSIFICATION: config.envVars?.GOVERNANCE_MCP_CLASSIFICATION ?? '',
81
+ },
52
82
  },
53
- },
54
- };
55
-
56
- Object.assign(mcpServers, buildSkillMcpServers({
57
- skills,
58
- credentialGrants,
59
- config,
60
- agentId,
61
- teamId,
62
- workspaceDir,
63
- serverUrl,
64
- authToken: config.authToken || machineApiKey,
65
- }));
83
+ };
84
+
85
+ Object.assign(mcpServers, buildSkillMcpServers({
86
+ skills,
87
+ credentialGrants,
88
+ config,
89
+ agentId,
90
+ workspaceId,
91
+ workspaceDir,
92
+ serverUrl,
93
+ authToken: config.authToken || machineApiKey,
94
+ }));
95
+ }
66
96
 
67
97
  writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), 'utf8');
68
98
 
69
99
  // Build CLI args
70
- const args = [
71
- '--wire',
72
- '--yolo',
73
- '--agent-file', agentFilePath,
74
- '--mcp-config-file', mcpConfigPath,
75
- '--session', sessionId,
76
- ];
77
-
78
- if (config.model && config.model !== 'default') {
100
+ const args = Array.isArray(directive?.spawn_args) && directive.spawn_args.length > 0
101
+ ? directive.spawn_args.map(item => String(item))
102
+ : [
103
+ '--wire',
104
+ '--yolo',
105
+ ];
106
+
107
+ if (!args.includes('--agent-file')) {
108
+ args.push('--agent-file', agentFilePath);
109
+ }
110
+ if (!args.includes('--mcp-config-file')) {
111
+ args.push('--mcp-config-file', mcpConfigPath);
112
+ }
113
+ if (!args.includes('--session')) {
114
+ args.push('--session', sessionId);
115
+ }
116
+
117
+ if (!directive && config.model && config.model !== 'default') {
79
118
  args.push('--model', config.model);
80
119
  }
81
120
 
82
- const spawnEnv = { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', ...(config.envVars ?? {}) };
121
+ const spawnEnv = {
122
+ ...process.env,
123
+ FORCE_COLOR: '0',
124
+ NO_COLOR: '1',
125
+ ...(config.envVars ?? {}),
126
+ ...(directive?.env_vars ?? {}),
127
+ };
83
128
 
84
129
  return { args, env: spawnEnv, sessionId, isResume, prompt };
85
130
  }
@@ -0,0 +1,89 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import path from 'path';
4
+
5
+ const STATE_DIR = path.join(homedir(), '.lightcone', 'governance');
6
+ const DEFAULT_STATE_FILE = path.join(STATE_DIR, 'state.json');
7
+ const MAX_RETENTION_MS = 24 * 60 * 60 * 1000;
8
+
9
+ function stateFilePath() {
10
+ return process.env.GOVERNANCE_STATE_FILE || DEFAULT_STATE_FILE;
11
+ }
12
+
13
+ function emptyState() {
14
+ return {
15
+ invalidated_leases: {},
16
+ updated_at: new Date().toISOString(),
17
+ };
18
+ }
19
+
20
+ function readState() {
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(stateFilePath(), 'utf8'));
23
+ if (!parsed || typeof parsed !== 'object') return emptyState();
24
+ return {
25
+ invalidated_leases: parsed.invalidated_leases && typeof parsed.invalidated_leases === 'object'
26
+ ? parsed.invalidated_leases
27
+ : {},
28
+ updated_at: parsed.updated_at ?? new Date().toISOString(),
29
+ };
30
+ } catch {
31
+ return emptyState();
32
+ }
33
+ }
34
+
35
+ function writeState(nextState) {
36
+ mkdirSync(path.dirname(stateFilePath()), { recursive: true });
37
+ writeFileSync(stateFilePath(), JSON.stringify(nextState, null, 2), 'utf8');
38
+ }
39
+
40
+ function pruneExpired(state, nowMs = Date.now()) {
41
+ const trimmed = { ...state.invalidated_leases };
42
+ for (const [leaseId, payload] of Object.entries(trimmed)) {
43
+ const invalidatedAt = Date.parse(payload?.invalidated_at ?? '');
44
+ if (!Number.isFinite(invalidatedAt) || (nowMs - invalidatedAt) > MAX_RETENTION_MS) {
45
+ delete trimmed[leaseId];
46
+ }
47
+ }
48
+ return {
49
+ invalidated_leases: trimmed,
50
+ updated_at: new Date(nowMs).toISOString(),
51
+ };
52
+ }
53
+
54
+ export function markInvalidatedLeases(leaseIds, {
55
+ reason = 'policy_updated',
56
+ newPolicyHash = null,
57
+ } = {}) {
58
+ const ids = Array.isArray(leaseIds) ? leaseIds.filter(Boolean) : [];
59
+ if (ids.length === 0) return 0;
60
+ const now = new Date().toISOString();
61
+ const current = pruneExpired(readState());
62
+ for (const leaseId of ids) {
63
+ current.invalidated_leases[leaseId] = {
64
+ reason,
65
+ new_policy_hash: newPolicyHash,
66
+ invalidated_at: now,
67
+ };
68
+ }
69
+ current.updated_at = now;
70
+ writeState(current);
71
+ return ids.length;
72
+ }
73
+
74
+ export const markLeasesInvalidated = markInvalidatedLeases;
75
+
76
+ export function isLeaseInvalidated(leaseId) {
77
+ if (!leaseId) return false;
78
+ const current = pruneExpired(readState());
79
+ return Boolean(current.invalidated_leases[leaseId]);
80
+ }
81
+
82
+ export function clearInvalidatedLease(leaseId) {
83
+ if (!leaseId) return;
84
+ const current = pruneExpired(readState());
85
+ if (!current.invalidated_leases[leaseId]) return;
86
+ delete current.invalidated_leases[leaseId];
87
+ current.updated_at = new Date().toISOString();
88
+ writeState(current);
89
+ }