@11agents/cli 0.1.2 → 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/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,6 +2,7 @@
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'
@@ -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() {
@@ -123,6 +131,16 @@ async function main() {
123
131
  return
124
132
  }
125
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
+
126
144
  if (command === 'node' && subcommand === 'run') {
127
145
  await runNode(flags)
128
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.2",
3
+ "version": "0.1.3",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,7 +408,7 @@ 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',
@@ -219,7 +428,7 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
219
428
 
220
429
  const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
221
430
  deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
222
- const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir })
431
+ const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir, env: task.execution_context?.env || process.env })
223
432
  const output = String(result.stdout || '').trim()
224
433
  const error = String(result.stderr || '').trim()
225
434
  if (result.code !== 0) {
@@ -289,6 +498,21 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
289
498
  }
290
499
 
291
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
292
516
  let completion
293
517
  try {
294
518
  completion = await handlerModule.handleRuntimeTask(runtimeTask)
@@ -297,6 +521,22 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
297
521
  comment: error instanceof Error ? error.message : String(error),
298
522
  status: 'failed',
299
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)
300
540
  }
301
541
 
302
542
  const body = normalizeTaskCompletion(runtimeTask, completion)
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(),