@11agents/cli 0.1.1 → 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 11agents.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -64,6 +64,15 @@ Useful daemon options:
64
64
  11agents daemon start --handler ./worker.js
65
65
  ```
66
66
 
67
+ Before a claimed task is handed to the built-in Codex worker, the daemon prepares a local read-only project cache:
68
+
69
+ - Cloud database snapshot: `~/.11agents/projects/<project>/database/snapshot.json`
70
+ - Project knowledge base: `~/.11agents/projects/<project>/knowledge_base/`
71
+ - Skill sync state: `~/.11agents/projects/<project>/skills-state.json`
72
+ - Task scratch directory: `~/.11agents/projects/<project>/tmp/<taskId>/`
73
+
74
+ Codex runs from `~/.11agents/projects/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk.
75
+
67
76
  The built-in task runner currently supports Codex tasks. A custom handler may export:
68
77
 
69
78
  ```js
package/bin/11agents.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { parseArgs } from '../src/args.js'
5
+ import { knowledgeStatus, syncKnowledge } from '../src/commands/knowledge.js'
5
6
  import { runNode } from '../src/commands/node.js'
6
7
  import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
7
8
  import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/commands/runtime.js'
8
9
  import { startBackgroundDaemon, statusBackgroundDaemon, stopBackgroundDaemon } from '../src/daemon-process.js'
9
10
  import { getControlConfig } from '../src/client.js'
10
- import { printStartupInfo } from '../src/info.js'
11
+ import { CLI_VERSION, printStartupInfo } from '../src/info.js'
11
12
  import { validateTelemetryBatch } from '../src/schema.js'
12
13
 
13
14
  function usage() {
@@ -25,6 +26,8 @@ Usage:
25
26
  11agents push batch <file>
26
27
  11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
27
28
  11agents push observation --workspace <slug> --agent <key> --platform x --type post --external-id <id> --metric views=123
29
+ 11agents knowledge sync --project <slug> [--mode pull|push]
30
+ 11agents knowledge status --project <slug>
28
31
  11agents node run --workspace <slug> --agent <key> --node <node-id> --handler ./collect-x.js [--once]
29
32
 
30
33
  Environment:
@@ -32,7 +35,12 @@ Environment:
32
35
  GTM_SWARM_SERVER compatibility fallback for server URL
33
36
  GTM_WRITES_TOKEN control-plane token for runtime registration
34
37
  ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
35
- GTM_SWARM_TOKEN project swarm token for push/node commands`)
38
+ GTM_SWARM_TOKEN project swarm token for push/node commands
39
+
40
+ Runtime task workspace:
41
+ Built-in Codex tasks run from ~/.11agents/projects/<project>/ by default.
42
+ That directory is read-only project context. Temporary writes belong under
43
+ ./tmp/<taskId>/ and are cleaned by the daemon after the task finishes.`)
36
44
  }
37
45
 
38
46
  async function main() {
@@ -65,9 +73,12 @@ async function main() {
65
73
  const result = await startBackgroundDaemon({
66
74
  argv,
67
75
  scriptPath: fileURLToPath(import.meta.url),
76
+ version: CLI_VERSION,
68
77
  })
69
78
  if (result.alreadyRunning) {
70
79
  console.log(`11agents daemon already running with pid ${result.pid}`)
80
+ } else if (result.restarted) {
81
+ console.log(`11agents daemon restarted pid ${result.previousPid} -> ${result.pid}`)
71
82
  } else {
72
83
  console.log(`11agents daemon started with pid ${result.pid}`)
73
84
  }
@@ -120,6 +131,16 @@ async function main() {
120
131
  return
121
132
  }
122
133
 
134
+ if (command === 'knowledge' && subcommand === 'sync') {
135
+ await syncKnowledge(flags)
136
+ return
137
+ }
138
+
139
+ if (command === 'knowledge' && subcommand === 'status') {
140
+ await knowledgeStatus(flags)
141
+ return
142
+ }
143
+
123
144
  if (command === 'node' && subcommand === 'run') {
124
145
  await runNode(flags)
125
146
  return
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "schema_version": "swarm.telemetry.v1",
3
3
  "workspace": "voc-ai",
4
+ "agent_id": "voc-amazon-reviews-mcp-runtime",
4
5
  "agent_key": "voc-amazon-reviews-mcp",
5
6
  "node_id": "vercel-prod",
6
7
  "sent_at": "2026-05-25T10:00:02Z",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "schema_version": "swarm.telemetry.v1",
3
3
  "workspace": "flatkey",
4
+ "agent_id": "agent-runtime-123",
4
5
  "agent_key": "x-growth-agent",
5
6
  "node_id": "mac-mini-01",
6
7
  "sent_at": "2026-05-25T09:30:00Z",
@@ -4,6 +4,7 @@
4
4
  "batch": {
5
5
  "schema_version": "swarm.telemetry.v1",
6
6
  "workspace": "flatkey",
7
+ "agent_id": "agent-runtime-123",
7
8
  "agent_key": "x-growth-agent",
8
9
  "node_id": "mac-mini-01",
9
10
  "sent_at": "2026-05-25T09:34:00Z",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@11agents/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "bugs": {
31
31
  "url": "https://github.com/11Agents/11agents-ai/issues"
32
32
  },
33
- "license": "UNLICENSED",
33
+ "license": "MIT",
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  }
@@ -0,0 +1,157 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { flag } from '../args.js'
5
+ import { requestJson } from '../client.js'
6
+
7
+ function slugify(value) {
8
+ return String(value || 'project')
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/^-|-$/g, '') || 'project'
13
+ }
14
+
15
+ function safeLocalPath(value, id) {
16
+ const raw = String(value || '').trim()
17
+ if (raw && !raw.startsWith('/') && !raw.split('/').includes('..')) return raw
18
+ return `nodes/${slugify(id)}.md`
19
+ }
20
+
21
+ function normalizeSnapshot(snapshot = {}) {
22
+ return {
23
+ cloud_revision: Number(snapshot.cloud_revision || snapshot.knowledge_base?.cloud_revision || 0),
24
+ local_revision: String(snapshot.local_revision || ''),
25
+ nodes: Array.isArray(snapshot.nodes) ? snapshot.nodes.map((node, index) => {
26
+ const id = slugify(node.id || node.title || `node-${index + 1}`)
27
+ return {
28
+ id,
29
+ kind: String(node.kind || 'document'),
30
+ title: String(node.title || id),
31
+ body: String(node.body || ''),
32
+ local_path: safeLocalPath(node.local_path || node.localPath, id),
33
+ metadata: node.metadata && typeof node.metadata === 'object' ? node.metadata : {},
34
+ }
35
+ }).sort((a, b) => a.id.localeCompare(b.id)) : [],
36
+ edges: Array.isArray(snapshot.edges) ? snapshot.edges.map(edge => ({
37
+ from: slugify(edge.from || edge.from_node_id),
38
+ to: slugify(edge.to || edge.to_node_id),
39
+ relation: slugify(edge.relation || 'related'),
40
+ metadata: edge.metadata && typeof edge.metadata === 'object' ? edge.metadata : {},
41
+ })).filter(edge => edge.from && edge.to).sort((a, b) => (
42
+ `${a.from}:${a.to}:${a.relation}`.localeCompare(`${b.from}:${b.to}:${b.relation}`)
43
+ )) : [],
44
+ }
45
+ }
46
+
47
+ function markdownForNode(node) {
48
+ return [
49
+ `# ${node.title}`,
50
+ '',
51
+ `kind: ${node.kind}`,
52
+ `id: ${node.id}`,
53
+ '',
54
+ node.body || '',
55
+ '',
56
+ ].join('\n')
57
+ }
58
+
59
+ function bodyFromMarkdownNode(markdown, fallback = '') {
60
+ const lines = String(markdown || '').split('\n')
61
+ let index = 0
62
+ if (lines[index]?.startsWith('# ')) index += 1
63
+ if (lines[index] === '') index += 1
64
+ if (/^kind:\s*/.test(lines[index] || '')) index += 1
65
+ if (/^id:\s*/.test(lines[index] || '')) index += 1
66
+ if (lines[index] === '') index += 1
67
+ const body = lines.slice(index).join('\n').replace(/\n+$/, '')
68
+ return body || fallback
69
+ }
70
+
71
+ export function buildKnowledgeBaseDir(project, { homeDir = os.homedir() } = {}) {
72
+ return path.join(homeDir, '.11agents', 'projects', slugify(project), 'knowledge_base')
73
+ }
74
+
75
+ export async function writeKnowledgeSnapshot(project, snapshot, { homeDir = os.homedir() } = {}) {
76
+ const normalized = normalizeSnapshot(snapshot)
77
+ const baseDir = buildKnowledgeBaseDir(project, { homeDir })
78
+ const localRevision = normalized.local_revision || `${Date.now()}`
79
+ const index = {
80
+ cloud_revision: normalized.cloud_revision,
81
+ local_revision: localRevision,
82
+ nodes: normalized.nodes,
83
+ edges: normalized.edges,
84
+ }
85
+ await mkdir(path.join(baseDir, 'nodes'), { recursive: true })
86
+ await writeFile(path.join(baseDir, 'index.json'), JSON.stringify(index, null, 2))
87
+ await writeFile(path.join(baseDir, 'sync-state.json'), JSON.stringify({
88
+ cloud_revision: normalized.cloud_revision,
89
+ local_revision: localRevision,
90
+ synced_at: new Date().toISOString(),
91
+ }, null, 2))
92
+ for (const node of normalized.nodes) {
93
+ const target = path.join(baseDir, node.local_path)
94
+ await mkdir(path.dirname(target), { recursive: true })
95
+ await writeFile(target, markdownForNode(node))
96
+ }
97
+ return index
98
+ }
99
+
100
+ export async function readKnowledgeSnapshot(project, { homeDir = os.homedir() } = {}) {
101
+ const baseDir = buildKnowledgeBaseDir(project, { homeDir })
102
+ const raw = await readFile(path.join(baseDir, 'index.json'), 'utf-8').catch(() => '{}')
103
+ const snapshot = normalizeSnapshot(JSON.parse(raw))
104
+ const nodes = await Promise.all(snapshot.nodes.map(async node => {
105
+ const markdown = await readFile(path.join(baseDir, node.local_path), 'utf-8').catch(() => null)
106
+ if (markdown === null) return node
107
+ return { ...node, body: bodyFromMarkdownNode(markdown, node.body) }
108
+ }))
109
+ return { ...snapshot, nodes }
110
+ }
111
+
112
+ export async function syncKnowledge(flags = {}, deps = {}) {
113
+ const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
114
+ if (!project) throw new Error('knowledge sync requires --project <slug>')
115
+ const mode = flag(flags, 'mode', flags.push ? 'push' : 'pull')
116
+ const request = deps.requestJson || requestJson
117
+ const log = deps.log || (value => console.log(value))
118
+ const homeDir = deps.homeDir || os.homedir()
119
+ const localSnapshot = await readKnowledgeSnapshot(project, { homeDir })
120
+ const body = mode === 'push'
121
+ ? { ...localSnapshot, mode: 'push', actor: 'cli' }
122
+ : { mode: 'pull', local_revision: localSnapshot.local_revision, cloud_revision: localSnapshot.cloud_revision, actor: 'cli' }
123
+ const result = await request(`/api/projects/${project}/knowledge/sync`, {
124
+ method: 'POST',
125
+ body,
126
+ })
127
+ const written = await writeKnowledgeSnapshot(project, result, { homeDir })
128
+ const payload = {
129
+ ok: true,
130
+ project,
131
+ mode,
132
+ path: buildKnowledgeBaseDir(project, { homeDir }),
133
+ cloud_revision: written.cloud_revision,
134
+ local_revision: written.local_revision,
135
+ nodes: written.nodes.length,
136
+ edges: written.edges.length,
137
+ }
138
+ log(JSON.stringify(payload, null, 2))
139
+ return { ...written, path: payload.path }
140
+ }
141
+
142
+ export async function knowledgeStatus(flags = {}, deps = {}) {
143
+ const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
144
+ if (!project) throw new Error('knowledge status requires --project <slug>')
145
+ const homeDir = deps.homeDir || os.homedir()
146
+ const snapshot = await readKnowledgeSnapshot(project, { homeDir })
147
+ const payload = {
148
+ project,
149
+ path: buildKnowledgeBaseDir(project, { homeDir }),
150
+ cloud_revision: snapshot.cloud_revision,
151
+ local_revision: snapshot.local_revision,
152
+ nodes: snapshot.nodes.length,
153
+ edges: snapshot.edges.length,
154
+ }
155
+ ;(deps.log || (value => console.log(value)))(JSON.stringify(payload, null, 2))
156
+ return payload
157
+ }
@@ -6,6 +6,7 @@ import { buildTelemetryBatch, validateTelemetryBatch } from '../schema.js'
6
6
  function defaults(flags) {
7
7
  return {
8
8
  workspace: flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''),
9
+ agent_id: flag(flags, 'agent-id', process.env.GTM_SWARM_AGENT_ID || ''),
9
10
  agent_key: flag(flags, 'agent', process.env.GTM_SWARM_AGENT || ''),
10
11
  node_id: flag(flags, 'node', process.env.GTM_SWARM_NODE || 'local'),
11
12
  }
@@ -1,10 +1,15 @@
1
1
  import { spawn } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
2
3
  import { readFileSync } from 'node:fs'
4
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
5
+ import os from 'node:os'
3
6
  import { dirname, resolve } from 'node:path'
7
+ import path from 'node:path'
4
8
  import { pathToFileURL } from 'node:url'
5
9
  import { fileURLToPath } from 'node:url'
6
10
  import { flag } from '../args.js'
7
11
  import { getControlConfig, requestJson } from '../client.js'
12
+ import { syncKnowledge } from './knowledge.js'
8
13
  import { buildRuntimeScan } from '../runtime-scan.js'
9
14
 
10
15
  const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
@@ -55,10 +60,12 @@ function runtimeDeps(overrides = {}) {
55
60
  buildRuntimeScan: overrides.buildRuntimeScan || buildRuntimeScan,
56
61
  importHandler: overrides.importHandler || (async handlerPath => import(pathToFileURL(resolve(handlerPath)).href)),
57
62
  log: overrides.log || (value => console.log(value)),
63
+ homeDir: overrides.homeDir || os.homedir(),
58
64
  runCodex: overrides.runCodex || runCodex,
59
65
  runProcess: overrides.runProcess || runProcess,
60
66
  requestJson: overrides.requestJson || requestJson,
61
67
  sleep: overrides.sleep || sleep,
68
+ syncKnowledge: overrides.syncKnowledge || syncKnowledge,
62
69
  }
63
70
  }
64
71
 
@@ -165,10 +172,211 @@ function compactJson(value) {
165
172
  return JSON.stringify(value ?? null, null, 2)
166
173
  }
167
174
 
175
+ const nonAlphaNum = /[^a-z0-9]+/g
176
+
177
+ function slugify(value, fallback = 'project') {
178
+ return String(value || fallback)
179
+ .trim()
180
+ .toLowerCase()
181
+ .replace(nonAlphaNum, '-')
182
+ .replace(/^-|-$/g, '') || fallback
183
+ }
184
+
185
+ function sanitizeTaskId(value) {
186
+ return slugify(value, 'task')
187
+ }
188
+
189
+ function projectSlugForTask(task, flags = {}) {
190
+ return slugify(task.workspace?.slug || task.workspace_slug || flag(flags, 'project') || flag(flags, 'workspace') || 'project')
191
+ }
192
+
193
+ function projectDirForTask(task, flags = {}, deps) {
194
+ return path.join(deps.homeDir, '.11agents', 'projects', projectSlugForTask(task, flags))
195
+ }
196
+
197
+ async function readJsonFile(filePath, fallback = {}) {
198
+ try {
199
+ return JSON.parse(await readFile(filePath, 'utf8'))
200
+ } catch (error) {
201
+ if (error?.code === 'ENOENT') return fallback
202
+ return fallback
203
+ }
204
+ }
205
+
206
+ function stableHash(value) {
207
+ return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex')
208
+ }
209
+
210
+ function assertSafeRelativePath(filePath) {
211
+ const clean = path.normalize(String(filePath || ''))
212
+ if (!clean || path.isAbsolute(clean) || clean === '..' || clean.startsWith(`..${path.sep}`)) {
213
+ throw new Error(`invalid skill file path: ${filePath}`)
214
+ }
215
+ return clean
216
+ }
217
+
218
+ function decodeFileContent(file) {
219
+ if (file?.encoding === 'base64') return Buffer.from(String(file.content || ''), 'base64')
220
+ return Buffer.from(String(file?.content || ''), 'utf8')
221
+ }
222
+
223
+ function skillDirForProvider({ workdir, provider, codexHome }) {
224
+ if (provider === 'codex') return path.join(codexHome || path.join(os.homedir(), '.codex'), 'skills')
225
+ if (provider === 'claude') return path.join(workdir, '.claude', 'skills')
226
+ if (provider === 'copilot') return path.join(workdir, '.github', 'skills')
227
+ if (provider === 'opencode') return path.join(workdir, '.opencode', 'skills')
228
+ if (provider === 'cursor') return path.join(workdir, '.cursor', 'skills')
229
+ return path.join(workdir, '.agent_context', 'skills')
230
+ }
231
+
232
+ function normalizeSkillBundle(skill = {}) {
233
+ return {
234
+ id: String(skill.id || ''),
235
+ name: String(skill.name || 'skill'),
236
+ content: String(skill.content || ''),
237
+ files: Array.isArray(skill.files)
238
+ ? skill.files.map(file => ({
239
+ path: String(file.path || ''),
240
+ encoding: file.encoding === 'base64' ? 'base64' : 'utf8',
241
+ content: String(file.content || ''),
242
+ })).sort((a, b) => a.path.localeCompare(b.path))
243
+ : [],
244
+ }
245
+ }
246
+
247
+ async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
248
+ const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
249
+ if (!skills.length) return { changed: false, count: 0 }
250
+
251
+ const statePath = path.join(workdir, 'skills-state.json')
252
+ const nextHash = stableHash(skills)
253
+ const current = await readJsonFile(statePath, {})
254
+ if (current.hash === nextHash) return { changed: false, count: skills.length }
255
+
256
+ const provider = task.runtime?.provider || ''
257
+ const skillsDir = skillDirForProvider({
258
+ workdir,
259
+ provider,
260
+ codexHome: flag(flags, 'codex-home', process.env.CODEX_HOME || path.join(deps.homeDir, '.codex')),
261
+ })
262
+ await mkdir(skillsDir, { recursive: true })
263
+ for (const skill of skills) {
264
+ const skillDir = path.join(skillsDir, slugify(skill.name, 'skill'))
265
+ await mkdir(skillDir, { recursive: true })
266
+ await writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf8')
267
+ for (const file of skill.files) {
268
+ const relativePath = assertSafeRelativePath(file.path)
269
+ const target = path.join(skillDir, relativePath)
270
+ await mkdir(path.dirname(target), { recursive: true })
271
+ await writeFile(target, decodeFileContent(file))
272
+ }
273
+ }
274
+ await writeFile(statePath, JSON.stringify({
275
+ hash: nextHash,
276
+ count: skills.length,
277
+ skills_dir: skillsDir,
278
+ synced_at: new Date().toISOString(),
279
+ }, null, 2))
280
+ return { changed: true, count: skills.length, skills_dir: skillsDir }
281
+ }
282
+
283
+ function databaseSyncSpec(task) {
284
+ const spec = task.database || task.cloud_database || task.workspace?.database || null
285
+ if (!spec || typeof spec !== 'object') return null
286
+ const cloudRevision = Number(spec.cloud_revision ?? spec.cloudRevision ?? spec.revision ?? 0)
287
+ const syncRequired = spec.sync_required === true || spec.syncRequired === true
288
+ if (!syncRequired && !cloudRevision) return null
289
+ return { cloudRevision, syncRequired }
290
+ }
291
+
292
+ async function syncDatabaseIfNeeded({ task, workdir, config, deps }) {
293
+ const spec = databaseSyncSpec(task)
294
+ if (!spec) return { synced: false }
295
+ const statePath = path.join(workdir, 'database', 'sync-state.json')
296
+ const current = await readJsonFile(statePath, {})
297
+ const localRevision = Number(current.cloud_revision || 0)
298
+ if (!spec.syncRequired && localRevision === spec.cloudRevision) return { synced: false, cloud_revision: localRevision }
299
+
300
+ const project = projectSlugForTask(task)
301
+ const result = await deps.requestJson(`/api/projects/${project}/database/sync`, {
302
+ method: 'POST',
303
+ body: {
304
+ task_id: task.id,
305
+ local_revision: localRevision,
306
+ cloud_revision: spec.cloudRevision,
307
+ },
308
+ config,
309
+ })
310
+ await mkdir(path.dirname(statePath), { recursive: true })
311
+ if (result?.snapshot !== undefined) {
312
+ await writeFile(path.join(path.dirname(statePath), 'snapshot.json'), JSON.stringify(result.snapshot, null, 2))
313
+ }
314
+ await writeFile(statePath, JSON.stringify({
315
+ cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision),
316
+ synced_at: result?.synced_at || new Date().toISOString(),
317
+ }, null, 2))
318
+ return { synced: true, cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision) }
319
+ }
320
+
321
+ function agentEnvironment(task) {
322
+ const values = task.agent?.environment_variables || task.agent?.environment || task.agent?.custom_env || {}
323
+ const env = {}
324
+ if (Array.isArray(values)) {
325
+ for (const item of values) {
326
+ const key = String(item?.key || '').trim()
327
+ if (key) env[key] = String(item?.value || '')
328
+ }
329
+ return env
330
+ }
331
+ if (values && typeof values === 'object') {
332
+ for (const [key, value] of Object.entries(values)) {
333
+ const cleanKey = String(key || '').trim()
334
+ if (cleanKey) env[cleanKey] = String(value ?? '')
335
+ }
336
+ }
337
+ return env
338
+ }
339
+
340
+ async function prepareRuntimeTask(task, flags, deps, config) {
341
+ const workdir = flag(flags, 'codex-workdir') || projectDirForTask(task, flags, deps)
342
+ const tmpDir = path.join(workdir, 'tmp', sanitizeTaskId(task.id))
343
+ await mkdir(tmpDir, { recursive: true })
344
+
345
+ const database = await syncDatabaseIfNeeded({ task, workdir, config, deps })
346
+ const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
347
+ const env = {
348
+ ...process.env,
349
+ ...agentEnvironment(task),
350
+ ELEVENAGENTS_PROJECT_DIR: workdir,
351
+ ELEVENAGENTS_TASK_TMP: tmpDir,
352
+ ELEVENAGENTS_TASK_ID: String(task.id || ''),
353
+ }
354
+
355
+ return {
356
+ workdir,
357
+ tmp_dir: tmpDir,
358
+ project_slug: projectSlugForTask(task, flags),
359
+ readonly: true,
360
+ env,
361
+ database,
362
+ skills,
363
+ }
364
+ }
365
+
168
366
  function buildCodexPrompt(task) {
169
367
  return [
170
368
  'You are executing an 11agents task as the assigned agent.',
171
369
  '',
370
+ 'Execution workspace:',
371
+ compactJson({
372
+ workdir: task.execution_context?.workdir,
373
+ tmp_dir: task.execution_context?.tmp_dir,
374
+ project_slug: task.execution_context?.project_slug,
375
+ knowledge_base: './knowledge_base/',
376
+ rule: 'Treat the project directory as read-only project context except ./knowledge_base/ for durable project knowledge updates and ./tmp/<taskId>/ for temporary scratch files.',
377
+ cleanup: 'Temporary files under ./tmp/<taskId>/ are removed by the CLI after the task finishes.',
378
+ }),
379
+ '',
172
380
  'Task context:',
173
381
  compactJson({
174
382
  queue_event: task.queue_event,
@@ -176,6 +384,7 @@ function buildCodexPrompt(task) {
176
384
  issue: task.issue,
177
385
  trigger_summary: task.trigger_summary,
178
386
  thread_memory: task.thread_memory,
387
+ project_knowledge: task.project_knowledge,
179
388
  }),
180
389
  '',
181
390
  'Assigned agent context:',
@@ -199,11 +408,12 @@ function buildCodexPrompt(task) {
199
408
 
200
409
  async function runCodex({ task, prompt, flags = {}, deps }) {
201
410
  const codexBin = flag(flags, 'codex-bin', 'codex')
202
- const workdir = flag(flags, 'codex-workdir', process.cwd())
411
+ const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
203
412
  const args = [
204
413
  '--ask-for-approval',
205
414
  'never',
206
415
  'exec',
416
+ '--skip-git-repo-check',
207
417
  '--sandbox',
208
418
  flag(flags, 'codex-sandbox', 'workspace-write'),
209
419
  '-C',
@@ -216,12 +426,18 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
216
426
  if (model) args.splice(execIndex + 1, 0, '--model', model)
217
427
  if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
218
428
 
219
- const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir })
429
+ const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
430
+ deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
431
+ const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir, env: task.execution_context?.env || process.env })
220
432
  const output = String(result.stdout || '').trim()
221
433
  const error = String(result.stderr || '').trim()
222
434
  if (result.code !== 0) {
435
+ const body = error || output || `codex exited with status ${result.code}`
436
+ const trustHint = body.includes('--skip-git-repo-check')
437
+ ? '\n\nCodex was invoked with --skip-git-repo-check. If this message persists, the background daemon may still be running an older CLI; run `11agents daemon start --background` again or restart it.'
438
+ : ''
223
439
  return {
224
- comment: error || output || `codex exited with status ${result.code}`,
440
+ comment: `${body}\n\nCodex command: ${commandLine}${trustHint}`,
225
441
  status: 'failed',
226
442
  }
227
443
  }
@@ -282,6 +498,21 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
282
498
  }
283
499
 
284
500
  deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
501
+ if (runtimeTask.workspace?.slug) {
502
+ await runWithDaemonRetry('sync knowledge base', () => (
503
+ deps.syncKnowledge({
504
+ project: runtimeTask.workspace.slug,
505
+ mode: 'pull',
506
+ server: flags.server,
507
+ token: flags.token,
508
+ }, {
509
+ requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
510
+ log: () => {},
511
+ })
512
+ ), deps, retryState)
513
+ }
514
+ const executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
515
+ runtimeTask.execution_context = executionContext
285
516
  let completion
286
517
  try {
287
518
  completion = await handlerModule.handleRuntimeTask(runtimeTask)
@@ -290,6 +521,22 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
290
521
  comment: error instanceof Error ? error.message : String(error),
291
522
  status: 'failed',
292
523
  }
524
+ } finally {
525
+ await rm(executionContext.tmp_dir, { recursive: true, force: true })
526
+ }
527
+
528
+ if (runtimeTask.workspace?.slug) {
529
+ await runWithDaemonRetry('sync knowledge base back to cloud', () => (
530
+ deps.syncKnowledge({
531
+ project: runtimeTask.workspace.slug,
532
+ mode: 'push',
533
+ server: flags.server,
534
+ token: flags.token,
535
+ }, {
536
+ requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
537
+ log: () => {},
538
+ })
539
+ ), deps, retryState)
293
540
  }
294
541
 
295
542
  const body = normalizeTaskCompletion(runtimeTask, completion)
@@ -9,6 +9,7 @@ export function backgroundPaths(homeDir = homedir()) {
9
9
  return {
10
10
  dir,
11
11
  pidPath: join(dir, 'daemon.pid'),
12
+ metaPath: join(dir, 'daemon.json'),
12
13
  logPath: join(dir, 'daemon.log'),
13
14
  }
14
15
  }
@@ -31,6 +32,15 @@ async function readPid(pidPath) {
31
32
  }
32
33
  }
33
34
 
35
+ async function readMeta(metaPath) {
36
+ try {
37
+ return JSON.parse(await readFile(metaPath, 'utf-8'))
38
+ } catch (error) {
39
+ if (error?.code === 'ENOENT') return null
40
+ return null
41
+ }
42
+ }
43
+
34
44
  function isRunning(pid, killFn = process.kill) {
35
45
  if (!pid) return false
36
46
  try {
@@ -46,13 +56,24 @@ export async function startBackgroundDaemon({
46
56
  homeDir = homedir(),
47
57
  nodePath = process.execPath,
48
58
  scriptPath,
59
+ version = '',
49
60
  spawnFn = spawn,
61
+ killFn = process.kill,
50
62
  } = {}) {
51
63
  const paths = backgroundPaths(homeDir)
52
64
  await mkdir(paths.dir, { recursive: true })
53
65
  const existingPid = await readPid(paths.pidPath)
54
- if (isRunning(existingPid)) {
55
- return { alreadyRunning: true, pid: existingPid, logPath: paths.logPath }
66
+ let restarted = false
67
+ let previousPid = null
68
+ if (isRunning(existingPid, killFn)) {
69
+ const meta = await readMeta(paths.metaPath)
70
+ if (meta?.version === version && meta?.scriptPath === scriptPath) {
71
+ return { alreadyRunning: true, pid: existingPid, logPath: paths.logPath }
72
+ }
73
+ killFn(existingPid, 'SIGTERM')
74
+ previousPid = existingPid
75
+ restarted = true
76
+ await rm(paths.pidPath, { force: true })
56
77
  }
57
78
 
58
79
  const log = await open(paths.logPath, constants.O_CREAT | constants.O_APPEND | constants.O_WRONLY, 0o600)
@@ -67,8 +88,13 @@ export async function startBackgroundDaemon({
67
88
  })
68
89
  if (!child?.pid) throw new Error('failed to start daemon')
69
90
  await writeFile(paths.pidPath, String(child.pid))
91
+ await writeFile(paths.metaPath, JSON.stringify({
92
+ version,
93
+ scriptPath,
94
+ startedAt: new Date().toISOString(),
95
+ }))
70
96
  child.unref?.()
71
- return { pid: child.pid, logPath: paths.logPath }
97
+ return { pid: child.pid, logPath: paths.logPath, restarted, previousPid }
72
98
  } finally {
73
99
  await log.close()
74
100
  }
package/src/schema.js CHANGED
@@ -19,6 +19,8 @@ function fail(error) {
19
19
  export function validateTelemetryBatch(input) {
20
20
  if (!isObject(input)) return fail('batch must be an object')
21
21
  if (input.schema_version !== TELEMETRY_SCHEMA_VERSION) return fail(`schema_version must be ${TELEMETRY_SCHEMA_VERSION}`)
22
+ const agentId = input.agent_id ?? input.agentId
23
+ if (!isNonEmptyString(agentId)) return fail('agent_id is required')
22
24
  for (const field of ['workspace', 'agent_key', 'node_id']) {
23
25
  if (!isNonEmptyString(input[field])) return fail(`${field} is required`)
24
26
  }
@@ -43,13 +45,14 @@ export function validateTelemetryBatch(input) {
43
45
  if (typeof value !== 'number' || !Number.isFinite(value)) return fail(`observations[${i}].metrics.${key} must be a finite number`)
44
46
  }
45
47
  }
46
- return { ok: true, batch: input }
48
+ return { ok: true, batch: { ...input, agent_id: agentId.trim() } }
47
49
  }
48
50
 
49
- export function buildTelemetryBatch({ workspace, agent_key, node_id, artifacts = [], observations = [] }) {
51
+ export function buildTelemetryBatch({ workspace, agent_id, agent_key, node_id, artifacts = [], observations = [] }) {
50
52
  return {
51
53
  schema_version: TELEMETRY_SCHEMA_VERSION,
52
54
  workspace,
55
+ agent_id,
53
56
  agent_key,
54
57
  node_id,
55
58
  sent_at: new Date().toISOString(),