@agentprojectcontext/apx 1.31.0 → 1.31.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.31.0",
3
+ "version": "1.31.1",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -59,7 +59,7 @@ apx agent vault list --all
59
59
  # Create a new template (writes ~/.apx/agents/<slug>.md)
60
60
  apx agent vault add reviewer \
61
61
  --role "Code reviewer" \
62
- --model claude-haiku-4-5 \
62
+ --model ollama:llama3.2:3b \
63
63
  --language es \
64
64
  --skills code-review,git \
65
65
  --description "Reviews PRs and pushes back on hand-wavy diffs."
@@ -5,7 +5,7 @@ description: How to create, configure, and use project agents in APX. Load when
5
5
 
6
6
  # apx-agent
7
7
 
8
- A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data: `.apc/agents/<slug>/memory.md` (and optional `sessions/` only when using external runtimes that write APC session stubs APX-native sessions live under `~/.apx/projects/<id>/`).
8
+ A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data (memory, conversations, sessions) lives under `~/.apx/projects/<apx_id>/agents/<slug>/` and is never committed. APX still reads legacy `.apc/agents/<slug>/memory.md` as a migration fallback only.
9
9
 
10
10
  ## Concrete CLI calls
11
11
 
@@ -13,10 +13,10 @@ A project agent is a named persona inside an APC project. Canonical definition:
13
13
  # List agents in a project
14
14
  apx agent list --project iacrmar
15
15
 
16
- # Create a new agent (writes .apc/agents/<slug>.md + regenerates AGENTS.md)
16
+ # Create a new agent (writes .apc/agents/<slug>.md, creates runtime dir, regenerates AGENTS.md)
17
17
  apx agent add reviewer \
18
18
  --role "Code reviewer" \
19
- --model claude-haiku-4-5 \
19
+ --model ollama:llama3.2:3b \
20
20
  --language es \
21
21
  --description "Reviews PRs and pushes back on hand-wavy diffs." \
22
22
  --tools read,write,run \
@@ -45,7 +45,7 @@ apx memory <slug> --project iacrmar --replace < file.md # full replace fro
45
45
  2. Description (from AGENTS.md).
46
46
  3. Role + Language fields.
47
47
  4. Invocation context: `engine | telegram | routine | runtime` — the channel calling the agent.
48
- 5. Memory: `.apc/agents/<slug>/memory.md` if it exists.
48
+ 5. Memory: `~/.apx/projects/<apx_id>/agents/<slug>/memory.md` if it exists, with legacy `.apc/agents/<slug>/memory.md` as a migration fallback.
49
49
  6. Skills declared in the agent's `Skills:` field, each loaded from `.apc/skills/<slug>.md` or the bundled set.
50
50
  7. The `apx` meta-skill (so the agent knows how to operate APX).
51
51
  8. ACTION_DISCIPLINE_RULES (fixed footer — anti-ghost, anti-disclaimer, action-first).
@@ -54,13 +54,13 @@ That's the prompt the engine sees on every `apx exec <agent>` or `apx chat <agen
54
54
 
55
55
  ## Models per agent
56
56
 
57
- Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model. Useful when a particular agent should use a cheaper / smaller / specialized model.
57
+ Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model. Leave it empty when the agent should follow the project/global default.
58
58
 
59
59
  ```markdown
60
60
  # .apc/agents/reviewer.md
61
61
  ---
62
62
  Role: Code reviewer
63
- Model: claude-haiku-4-5 ← this agent always uses Haiku, independent of super_agent.model
63
+ Model: ollama:llama3.2:3b ← this agent uses this model, independent of super_agent.model
64
64
  Language: es
65
65
  ---
66
66
  ```
@@ -57,7 +57,6 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
57
57
  └── .apc/
58
58
  ├── project.json ← { apxId, name, ... }
59
59
  ├── agents/<slug>.md
60
- ├── agents/<slug>/memory.md
61
60
  ├── skills/<slug>.md or <slug>/SKILL.md
62
61
  ├── mcps.json ← shared MCPs (committed)
63
62
  ├── commands/ ← custom slash-commands
@@ -65,7 +64,7 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
65
64
 
66
65
  ~/.apx/projects/<apxId>/ ← runtime state (never committed)
67
66
  ├── messages/YYYY-MM-DD.jsonl
68
- ├── agents/<slug>/{sessions/, conversations/}
67
+ ├── agents/<slug>/{memory.md, sessions/, conversations/}
69
68
  ├── routines.json
70
69
  ├── tasks/YYYY-MM.jsonl
71
70
  ├── artifacts/
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // This is distinct from:
4
4
  // - identity.json → who Roby is (name, personality, owner)
5
- // - project agents' .apc/agents/<slug>/memory.md → per-agent, per-project
5
+ // - project agents' ~/.apx/projects/<apx_id>/agents/<slug>/memory.md → per-agent, per-project
6
6
  // - sessions → raw transcripts of past work (search_sessions)
7
7
  //
8
8
  // It is a single free-form markdown file at ~/.apx/memory.md that Roby keeps
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { projectStorageRoot } from "./config.js";
4
+ import { getOrCreateApxId } from "./scaffold.js";
5
+
6
+ const EMPTY_MEMORY = (slug) =>
7
+ `# Memory — ${slug}\n\n` +
8
+ `## Identity\n- \n\n` +
9
+ `## Long-term facts\n- \n\n` +
10
+ `## Recent context\n- \n`;
11
+
12
+ export function agentRuntimeDir(projectOrRoot, slug) {
13
+ const storagePath =
14
+ typeof projectOrRoot === "object" && projectOrRoot?.storagePath
15
+ ? projectOrRoot.storagePath
16
+ : null;
17
+ const root =
18
+ typeof projectOrRoot === "string"
19
+ ? projectOrRoot
20
+ : projectOrRoot?.path;
21
+ const base = storagePath || projectStorageRoot(getOrCreateApxId(root));
22
+ return path.join(base, "agents", slug);
23
+ }
24
+
25
+ export function agentMemoryPath(projectOrRoot, slug) {
26
+ return path.join(agentRuntimeDir(projectOrRoot, slug), "memory.md");
27
+ }
28
+
29
+ export function legacyAgentMemoryPath(projectRoot, slug) {
30
+ return path.join(projectRoot, ".apc", "agents", slug, "memory.md");
31
+ }
32
+
33
+ export function ensureAgentRuntimeDir(projectOrRoot, slug, { createMemory = false } = {}) {
34
+ const dir = agentRuntimeDir(projectOrRoot, slug);
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ if (createMemory) {
37
+ const memory = path.join(dir, "memory.md");
38
+ if (!fs.existsSync(memory)) fs.writeFileSync(memory, EMPTY_MEMORY(slug));
39
+ }
40
+ return dir;
41
+ }
42
+
43
+ export function readAgentMemory(projectOrRoot, slug) {
44
+ const primary = agentMemoryPath(projectOrRoot, slug);
45
+ if (fs.existsSync(primary)) return fs.readFileSync(primary, "utf8");
46
+
47
+ const root =
48
+ typeof projectOrRoot === "string"
49
+ ? projectOrRoot
50
+ : projectOrRoot?.path;
51
+ if (root) {
52
+ const legacy = legacyAgentMemoryPath(root, slug);
53
+ if (fs.existsSync(legacy)) return fs.readFileSync(legacy, "utf8");
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ export function writeAgentMemory(projectOrRoot, slug, body) {
60
+ ensureAgentRuntimeDir(projectOrRoot, slug);
61
+ const memory = agentMemoryPath(projectOrRoot, slug);
62
+ fs.writeFileSync(memory, body);
63
+ return memory;
64
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { readAgentMemory } from "./agent-memory.js";
3
4
 
4
5
  // ---------------------------------------------------------------------------
5
6
  // Anti-ghost-response rules injected into every agent system prompt.
@@ -62,8 +63,8 @@ export function buildAgentSystem(project, agent, {
62
63
 
63
64
  parts.push(buildInvocationContext({ invocation, runtime, channel, caller, routine }));
64
65
 
65
- const memPath = path.join(project.path, ".apc", "agents", agent.slug, "memory.md");
66
- if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
66
+ const memory = readAgentMemory(project, agent.slug);
67
+ if (memory) parts.push("## Memory\n" + memory);
67
68
 
68
69
  const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
69
70
  if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
@@ -325,24 +325,60 @@ const AGENTS_MD_TEMPLATE = `# AGENTS.md
325
325
  <!-- Hard constraints: what agents must always / never do here. -->
326
326
  `;
327
327
 
328
- const APC_GITIGNORE = `# APC runtime data — never in the repository
329
- # Chat conversations and runtime sessions belong in ~/.apx/projects/<id>/
330
- agents/*/sessions/
331
- agents/*/conversations/
328
+ const APC_GITIGNORE = `# APC repository-safe context only.
329
+ # Runtime state belongs in ~/.apx/projects/<id>/, not in .apc/.
330
+
331
+ # Legacy per-agent runtime dirs (agent definitions are flat: agents/<slug>.md)
332
+ agents/*/
333
+
334
+ # Runtime sessions / conversations / messages
332
335
  sessions/
333
336
  conversations/
334
337
  messages/
335
338
  chats/
339
+ threads/
340
+ transcripts/
341
+ runs/
342
+
343
+ # Runtime memory, indexes, databases, and caches
344
+ memory.local.md
345
+ auto-memory.md
336
346
  cache/
337
347
  tmp/
338
348
  private/
339
349
  secrets/
350
+ *.db
351
+ *.db-*
352
+ *.sqlite
353
+ *.sqlite3
354
+ project.db
355
+ memory.db
356
+ memory-index.jsonl
357
+ memory-cursor.json
358
+
359
+ # Local config and secrets
360
+ .env
340
361
  *.local.json
341
362
  *.secret.json
342
363
  *.env
343
364
  *.env.*
344
- project.db
365
+ *.key
366
+ *.pem
367
+ *.p12
368
+ *.crt
369
+ credentials.json
370
+ service-account*.json
371
+ token*.json
372
+ mcps.local.json
373
+ config.local.json
374
+
375
+ # Scratch planning state
376
+ plans/scratch/
377
+ plans/*.local.md
378
+
379
+ # Migration marker and OS noise
345
380
  migrate.md
381
+ .DS_Store
346
382
  `;
347
383
 
348
384
  function nowIso() {
@@ -448,19 +484,8 @@ export function initApf(directory, { name } = {}) {
448
484
  }
449
485
 
450
486
  export function ensureAgentDir(root, slug) {
451
- const dir = path.join(root, ".apc", "agents", slug);
452
- fs.mkdirSync(dir, { recursive: true });
453
- const memory = path.join(dir, "memory.md");
454
- if (!fs.existsSync(memory)) {
455
- fs.writeFileSync(
456
- memory,
457
- `# Memory — ${slug}\n\n` +
458
- `## Identity\n- \n\n` +
459
- `## Long-term facts\n- \n\n` +
460
- `## Recent context\n- \n`
461
- );
462
- }
463
- return dir;
487
+ fs.mkdirSync(path.join(root, ".apc", "agents"), { recursive: true });
488
+ return path.join(root, ".apc", "agents");
464
489
  }
465
490
 
466
491
  // Write .apc/agents/<slug>.md — the canonical agent definition file.
@@ -619,6 +619,8 @@ function makeInlineHandlers({ projects, registries }) {
619
619
  memory_list: async (body) => {
620
620
  const { default: fs } = await import("node:fs");
621
621
  const { default: path } = await import("node:path");
622
+ const { readAgents } = await import("../parser.js");
623
+ const { agentMemoryPath } = await import("../agent-memory.js");
622
624
  // Find the project
623
625
  const all = projects.list();
624
626
  let p = null;
@@ -629,15 +631,13 @@ function makeInlineHandlers({ projects, registries }) {
629
631
  }
630
632
  if (!p) p = projects.get(all.filter((x) => x.id !== 0)[0]?.id) || projects.get(0);
631
633
  if (!p) throw new Error("no project registered");
632
- const agentsDir = path.join(p.path, ".apc", "agents");
633
- if (!fs.existsSync(agentsDir)) return { agents_with_memory: [] };
634
- const result = fs.readdirSync(agentsDir).filter((slug) => {
635
- return fs.existsSync(path.join(agentsDir, slug, "memory.md"));
636
- }).map((slug) => {
637
- const memPath = path.join(agentsDir, slug, "memory.md");
634
+ const result = readAgents(p.path).map((agent) => {
635
+ const slug = agent.slug;
636
+ const memPath = agentMemoryPath(p, slug);
637
+ if (!fs.existsSync(memPath)) return null;
638
638
  const stat = fs.statSync(memPath);
639
639
  return { agent: slug, path: memPath, size: stat.size, mtime: stat.mtime };
640
- });
640
+ }).filter(Boolean);
641
641
  return { project: p.path, agents_with_memory: result };
642
642
  },
643
643
  };
@@ -14,6 +14,13 @@ import {
14
14
  restoreVaultAgent,
15
15
  ensureAgentDir,
16
16
  } from "../../../core/scaffold.js";
17
+ import {
18
+ ensureAgentRuntimeDir,
19
+ readAgentMemory,
20
+ writeAgentMemory,
21
+ agentMemoryPath,
22
+ legacyAgentMemoryPath,
23
+ } from "../../../core/agent-memory.js";
17
24
  import { agentToResponse } from "./shared.js";
18
25
 
19
26
  // Lowercase the patch keys we accept on the vault and turn skills/tools into
@@ -114,6 +121,7 @@ export function register(app, { projects, project }) {
114
121
  try {
115
122
  writeAgentFile(p.path, slug, vault.fields || {}, vault.body || "");
116
123
  ensureAgentDir(p.path, slug);
124
+ ensureAgentRuntimeDir(p, slug);
117
125
  projects.rebuild(p.id);
118
126
  res.status(201).json(agentToResponse(readAgents(p.path).find((a) => a.slug === slug)));
119
127
  } catch (e) {
@@ -133,10 +141,7 @@ export function register(app, { projects, project }) {
133
141
  const agents = readAgents(p.path);
134
142
  const a = agents.find((x) => x.slug === req.params.slug);
135
143
  if (!a) return res.status(404).json({ error: "agent not found" });
136
- const memPath = path.join(p.path, ".apc", "agents", a.slug, "memory.md");
137
- const memory = fs.existsSync(memPath)
138
- ? fs.readFileSync(memPath, "utf8")
139
- : "";
144
+ const memory = readAgentMemory(p, a.slug);
140
145
  res.json({ ...agentToResponse(a), memory, system: a.body || "" });
141
146
  });
142
147
 
@@ -163,6 +168,7 @@ export function register(app, { projects, project }) {
163
168
  Parent: parent || null,
164
169
  });
165
170
  ensureAgentDir(p.path, slug);
171
+ ensureAgentRuntimeDir(p, slug);
166
172
  projects.rebuild(p.id);
167
173
  const created = readAgents(p.path).find((a) => a.slug === slug);
168
174
  res.status(201).json(agentToResponse(created));
@@ -203,6 +209,7 @@ export function register(app, { projects, project }) {
203
209
  try {
204
210
  writeAgentFile(p.path, slug, fields, body);
205
211
  ensureAgentDir(p.path, slug);
212
+ ensureAgentRuntimeDir(p, slug);
206
213
  projects.rebuild(p.id);
207
214
  const updated = readAgents(p.path).find((a) => a.slug === slug);
208
215
  res.json(agentToResponse(updated));
@@ -211,18 +218,20 @@ export function register(app, { projects, project }) {
211
218
  }
212
219
  });
213
220
 
214
- // Delete an agent: removes .apc/agents/<slug>.md and its data dir.
221
+ // Delete an agent: removes .apc/agents/<slug>.md and runtime data dir.
215
222
  app.delete("/projects/:pid/agents/:slug", (req, res) => {
216
223
  const p = project(req, res);
217
224
  if (!p) return;
218
225
  const slug = req.params.slug;
219
226
  const file = path.join(p.path, ".apc", "agents", `${slug}.md`);
220
- const dir = path.join(p.path, ".apc", "agents", slug);
221
- if (!fs.existsSync(file) && !fs.existsSync(dir))
227
+ const runtimeDir = path.dirname(agentMemoryPath(p, slug));
228
+ const legacyDir = path.dirname(legacyAgentMemoryPath(p.path, slug));
229
+ if (!fs.existsSync(file) && !fs.existsSync(runtimeDir) && !fs.existsSync(legacyDir))
222
230
  return res.status(404).json({ error: "agent not found" });
223
231
  try {
224
232
  if (fs.existsSync(file)) fs.rmSync(file);
225
- if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
233
+ if (fs.existsSync(runtimeDir)) fs.rmSync(runtimeDir, { recursive: true, force: true });
234
+ if (fs.existsSync(legacyDir)) fs.rmSync(legacyDir, { recursive: true, force: true });
226
235
  projects.rebuild(p.id);
227
236
  res.json({ ok: true });
228
237
  } catch (e) {
@@ -257,15 +266,7 @@ export function register(app, { projects, project }) {
257
266
  app.get("/projects/:pid/agents/:slug/memory", (req, res) => {
258
267
  const p = project(req, res);
259
268
  if (!p) return;
260
- const memPath = path.join(
261
- p.path,
262
- ".apc",
263
- "agents",
264
- req.params.slug,
265
- "memory.md"
266
- );
267
- if (!fs.existsSync(memPath)) return res.json({ body: "" });
268
- res.json({ body: fs.readFileSync(memPath, "utf8") });
269
+ res.json({ body: readAgentMemory(p, req.params.slug) });
269
270
  });
270
271
 
271
272
  app.put("/projects/:pid/agents/:slug/memory", (req, res) => {
@@ -274,10 +275,7 @@ export function register(app, { projects, project }) {
274
275
  const { body } = req.body || {};
275
276
  if (typeof body !== "string")
276
277
  return res.status(400).json({ error: "body must be string" });
277
- const dir = path.join(p.path, ".apc", "agents", req.params.slug);
278
- fs.mkdirSync(path.join(dir, "sessions"), { recursive: true });
279
- const memPath = path.join(dir, "memory.md");
280
- fs.writeFileSync(memPath, body);
278
+ writeAgentMemory(p, req.params.slug, body);
281
279
  projects.rebuild(p.id);
282
280
  res.json({ ok: true, bytes: Buffer.byteLength(body, "utf8") });
283
281
  });
@@ -31,7 +31,7 @@ export function register(app, { projects, config }) {
31
31
  for (const p of targetProjects) {
32
32
  if (!p) continue;
33
33
 
34
- // 1) Session files in the repo (.apc/agents/<slug>/sessions/)
34
+ // 1) Legacy session files in the repo (.apc/agents/<slug>/sessions/)
35
35
  const sessionAgentsDir = path.join(p.path, ".apc", "agents");
36
36
  if (fs.existsSync(sessionAgentsDir)) {
37
37
  for (const slug of fs.readdirSync(sessionAgentsDir)) {
@@ -6,6 +6,8 @@ import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { appendErrorTrace, previewText } from "../../../core/logging.js";
9
+ import { readAgents } from "../../../core/parser.js";
10
+ import { agentMemoryPath } from "../../../core/agent-memory.js";
9
11
 
10
12
  export const nowIso = () =>
11
13
  new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -110,15 +112,10 @@ export function makeTopProjectResolver(projects) {
110
112
  }
111
113
 
112
114
  // Pick the memory.md to use when /memory is called without an agent ref.
113
- // Prefer the first .apc/agents/<slug>/memory.md; else .apc/memory.md.
115
+ // Prefer the first agent's runtime-local memory; else project-level .apc/memory.md.
114
116
  export function resolveMemoryPath(p) {
115
- const agentsDir = path.join(p.path, ".apc", "agents");
116
- if (fs.existsSync(agentsDir)) {
117
- const slugs = fs.readdirSync(agentsDir).filter((s) =>
118
- fs.statSync(path.join(agentsDir, s)).isDirectory()
119
- );
120
- if (slugs.length) return path.join(agentsDir, slugs[0], "memory.md");
121
- }
117
+ const firstAgent = readAgents(p.path)[0];
118
+ if (firstAgent) return agentMemoryPath(p, firstAgent.slug);
122
119
  return path.join(p.path, ".apc", "memory.md");
123
120
  }
124
121
 
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { readVaultAgents, VAULT_DIR } from "../../../../core/parser.js";
4
4
  import { addImportedAgent, ensureAgentDir } from "../../../../core/scaffold.js";
5
+ import { ensureAgentRuntimeDir } from "../../../../core/agent-memory.js";
5
6
  import { confirmedProperty, projectMeta, resolveProject } from "../helpers.js";
6
7
 
7
8
  export default {
@@ -35,6 +36,7 @@ export default {
35
36
  const p = resolveProject(projects, project || "default");
36
37
  addImportedAgent(p.path, slug);
37
38
  ensureAgentDir(p.path, slug);
39
+ ensureAgentRuntimeDir(p, slug);
38
40
  projects.rebuild(p.id);
39
41
 
40
42
  return {
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import { resolveProject } from "../helpers.js";
3
+ import { agentMemoryPath, readAgentMemory } from "../../../../core/agent-memory.js";
4
4
 
5
5
  export default {
6
6
  name: "read_agent_memory",
@@ -21,8 +21,9 @@ export default {
21
21
  },
22
22
  makeHandler: ({ projects }) => ({ project, agent }) => {
23
23
  const p = resolveProject(projects, project);
24
- const file = path.join(p.path, ".apc", "agents", agent, "memory.md");
25
- if (!fs.existsSync(file)) return { error: `no memory.md for agent ${agent}` };
26
- return { body: fs.readFileSync(file, "utf8") };
24
+ const file = agentMemoryPath(p, agent);
25
+ const body = readAgentMemory(p, agent);
26
+ if (!body && !fs.existsSync(file)) return { error: `no memory.md for agent ${agent}` };
27
+ return { body, path: file };
27
28
  },
28
29
  };
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { findApfRoot, readAgents, readVaultAgents, readVaultAgent, VAULT_DIR, SLUG_RE } from "../../../core/parser.js";
4
4
  import { writeAgentFile, writeVaultAgentFile, removeVaultAgent, restoreVaultAgent, addImportedAgent, ensureAgentDir } from "../../../core/scaffold.js";
5
+ import { ensureAgentRuntimeDir, agentMemoryPath } from "../../../core/agent-memory.js";
5
6
  import { http } from "../http.js";
6
7
 
7
8
  // ── ANSI ──────────────────────────────────────────────────────────────────────
@@ -49,6 +50,7 @@ export async function cmdAgentAdd(args) {
49
50
 
50
51
  writeAgentFile(root, slug, fields);
51
52
  ensureAgentDir(root, slug);
53
+ ensureAgentRuntimeDir(root, slug);
52
54
  await nudgeDaemon(root);
53
55
 
54
56
  console.log(`Added agent ${slug}`);
@@ -207,10 +209,11 @@ export async function cmdAgentImport(args) {
207
209
  addImportedAgent(root, slug);
208
210
  console.log(`\n ${bold(slug)} imported from vault ${tag("↑ vault")}\n`);
209
211
  console.log(gray(` definition: ${vaultPath}`));
210
- console.log(gray(` memory: ${path.join(root, ".apc", "agents", slug, "memory.md")} (project-local)`));
212
+ console.log(gray(` memory: ${agentMemoryPath(root, slug)} (runtime-local)`));
211
213
  console.log();
212
214
  }
213
215
 
214
216
  ensureAgentDir(root, slug);
217
+ ensureAgentRuntimeDir(root, slug);
215
218
  await nudgeDaemon(root);
216
219
  }
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import { findApfRoot } from "../../../core/parser.js";
4
- import { ensureAgentDir } from "../../../core/scaffold.js";
3
+ import { agentMemoryPath, readAgentMemory, writeAgentMemory, ensureAgentRuntimeDir } from "../../../core/agent-memory.js";
5
4
  import { http } from "../http.js";
6
5
 
7
6
  function requireRoot() {
@@ -23,12 +22,11 @@ export async function cmdMemory(args) {
23
22
  const slug = args._[0];
24
23
  if (!slug) throw new Error("apx memory: missing <agent-slug>");
25
24
  const root = requireRoot();
26
- const memPath = path.join(root, ".apc", "agents", slug, "memory.md");
25
+ const memPath = agentMemoryPath(root, slug);
27
26
 
28
27
  if (args.flags.replace) {
29
28
  const newBody = readStdinSync();
30
- ensureAgentDir(root, slug);
31
- fs.writeFileSync(memPath, newBody);
29
+ writeAgentMemory(root, slug, newBody);
32
30
  await nudgeDaemon(root);
33
31
  console.log(`replaced memory for ${slug} (${Buffer.byteLength(newBody)} bytes)`);
34
32
  return;
@@ -36,23 +34,24 @@ export async function cmdMemory(args) {
36
34
 
37
35
  if (args.flags.append && args.flags.append !== true) {
38
36
  const note = String(args.flags.append);
39
- ensureAgentDir(root, slug);
40
- let body = fs.existsSync(memPath) ? fs.readFileSync(memPath, "utf8") : "";
37
+ ensureAgentRuntimeDir(root, slug);
38
+ let body = readAgentMemory(root, slug);
41
39
  if (!/##\s+Recent context/i.test(body)) {
42
40
  body += body.endsWith("\n") ? "\n## Recent context\n" : "\n\n## Recent context\n";
43
41
  }
44
42
  const today = new Date().toISOString().slice(0, 10);
45
43
  body = body.replace(/(##\s+Recent context\s*\n)/i, `$1- ${today}: ${note}\n`);
46
- fs.writeFileSync(memPath, body);
44
+ writeAgentMemory(root, slug, body);
47
45
  await nudgeDaemon(root);
48
46
  console.log(`appended to ${slug} memory: ${note}`);
49
47
  return;
50
48
  }
51
49
 
52
- if (!fs.existsSync(memPath)) {
50
+ const body = readAgentMemory(root, slug);
51
+ if (!body && !fs.existsSync(memPath)) {
53
52
  throw new Error(`no memory for "${slug}" — agent dir not yet created`);
54
53
  }
55
- process.stdout.write(fs.readFileSync(memPath, "utf8"));
54
+ process.stdout.write(body);
56
55
  }
57
56
 
58
57
  function readStdinSync() {