@agentprojectcontext/apx 1.8.0 → 1.8.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.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -19,7 +19,7 @@ function askYN(rl, question, defaultYes = false) {
19
19
  rl.question(`${question} [${hint}]: `, (ans) => {
20
20
  const a = ans.trim().toLowerCase();
21
21
  if (!a) return resolve(defaultYes);
22
- resolve(a === "y" || a === "yes" || a === "s" || a === "si" || a === "sí");
22
+ resolve(a === "y" || a === "yes");
23
23
  });
24
24
  });
25
25
  }
@@ -120,7 +120,7 @@ export async function runWizard() {
120
120
  const personality = await ask(rl, " Personality (comma-separated traits)", existing.personality || "direct, curious, helpful");
121
121
  const owner_name = await ask(rl, " Your name", existing.owner_name || "");
122
122
  const owner_context = await ask(rl, " What are you building / working on", existing.owner_context || "");
123
- const language = await ask(rl, " Language for agent messages (e.g. Spanish, English)", existing.language || "Spanish (Español)");
123
+ const language = await ask(rl, " Language for agent messages (e.g. Spanish, English)", existing.language || "English");
124
124
 
125
125
  console.log("\n Claude Code permissions");
126
126
  console.log(" APX can configure Claude Code to allow terminal commands without prompts.");
@@ -104,9 +104,9 @@ function findSessionById(root, id) {
104
104
  }
105
105
 
106
106
  function statusEmoji(status) {
107
- if (/Completada|complete/i.test(status)) return "✅";
108
- if (/En progreso|in.progress/i.test(status)) return "🔄";
109
- if (/stale|Cerrada/i.test(status)) return "⚠️";
107
+ if (/complete/i.test(status)) return "✅";
108
+ if (/in.progress/i.test(status)) return "🔄";
109
+ if (/stale|closed/i.test(status)) return "⚠️";
110
110
  return "❓";
111
111
  }
112
112
 
@@ -242,7 +242,7 @@ export function cmdSessionClose(args) {
242
242
  if (!s) throw new Error(`session "${id}" not found`);
243
243
 
244
244
  let text = fs.readFileSync(s.path, "utf8");
245
- text = setFrontmatterField(text, "status", "✅ Completada");
245
+ text = setFrontmatterField(text, "status", "✅ Completed");
246
246
  text = setFrontmatterField(text, "completed", nowIso());
247
247
  if (args.flags.result && args.flags.result !== true) {
248
248
  text = setFrontmatterField(text, "result", String(args.flags.result));
@@ -254,7 +254,7 @@ export function cmdSessionClose(args) {
254
254
  export function cmdSessionCheck() {
255
255
  const root = requireRoot();
256
256
  const sessions = listAllSessions(root).filter((s) =>
257
- /En progreso/i.test(s.status)
257
+ /in.progress/i.test(s.status)
258
258
  );
259
259
 
260
260
  if (sessions.length === 0) {
@@ -338,7 +338,7 @@ export async function cmdSessionResume(args) {
338
338
  export function cmdSessionCloseStale() {
339
339
  const root = requireRoot();
340
340
  const sessions = listAllSessions(root).filter((s) =>
341
- /En progreso/i.test(s.status)
341
+ /in.progress/i.test(s.status)
342
342
  );
343
343
  let closed = 0;
344
344
  for (const s of sessions) {
@@ -348,7 +348,7 @@ export function cmdSessionCloseStale() {
348
348
  text = setFrontmatterField(
349
349
  text,
350
350
  "status",
351
- `⚠️ Cerrada automáticamente (stale >${STALE_HOURS}h)`
351
+ `⚠️ Automatically closed (stale >${STALE_HOURS}h)`
352
352
  );
353
353
  text = setFrontmatterField(text, "completed", nowIso());
354
354
  text = setFrontmatterField(
@@ -194,7 +194,7 @@ export async function cmdSetup() {
194
194
  console.log(b(" Language:"));
195
195
  console.log(di(" The super-agent will always respond in your language."));
196
196
  console.log();
197
- const language = await ask(" Your language (e.g. English, Español, Português): ") || "English";
197
+ const language = await ask(" Your language (e.g. English, Spanish, Portuguese): ") || "English";
198
198
 
199
199
  // ── Summary ─────────────────────────────────────────────────────────────────
200
200
  console.log();
@@ -324,15 +324,15 @@ async function sendTelegramWakeup({ botToken, chatId, language, model }) {
324
324
  // Minimal fallback messages per common language (used only if daemon can't respond)
325
325
  function languageFallback(lang) {
326
326
  const l = lang.toLowerCase();
327
- if (/espa[ñn]|spanish|arg|lat/i.test(l))
328
- return "⚡ ¡Despierto y listo para trabajar! APX online.\nAún no tengo nombre, ¿cómo te gustaría llamarme?\nY vos, ¿cómo te llamas o cómo puedo llamarte?";
327
+ if (/spanish|arg|lat/i.test(l))
328
+ return "⚡ APX is online and ready to work.\nI do not have a name yet. What would you like to call me?\nAnd what should I call you?";
329
329
  if (/portugu|brasil/i.test(l))
330
- return "⚡ Acordei e pronto para trabalhar! APX online.\nAinda não tenho nome, como você gostaria de me chamar?\nE você, como posso te chamar?";
330
+ return "⚡ APX is online and ready to work.\nI do not have a name yet. What would you like to call me?\nAnd what should I call you?";
331
331
  if (/franc|french/i.test(l))
332
- return "⚡ Réveillé et prêt à travailler ! APX en ligne.\nJe n'ai pas encore de nom, comment souhaitez-vous m'appeler ?\nEt vous, comment puis-je vous appeler ?";
332
+ return "⚡ APX is online and ready to work.\nI do not have a name yet. What would you like to call me?\nAnd what should I call you?";
333
333
  if (/deutsch|german/i.test(l))
334
- return "⚡ Aufgewacht und bereit! APX ist online.\nIch habe noch keinen Namen wie möchtest du mich nennen?\nUnd du, wie kann ich dich nennen?";
334
+ return "⚡ APX is online and ready to work.\nI do not have a name yet. What would you like to call me?\nAnd what should I call you?";
335
335
  if (/ital/i.test(l))
336
- return "⚡ Sveglio e pronto a lavorare! APX online.\nNon ho ancora un nome, come vorresti chiamarmi?\nE tu, come posso chiamarti?";
336
+ return "⚡ APX is online and ready to work.\nI do not have a name yet. What would you like to call me?\nAnd what should I call you?";
337
337
  return "⚡ I'm awake and ready to go! APX is online.\nI don't have a name yet — what would you like to call me?\nAnd you, what's your name or what should I call you?";
338
338
  }
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function listField(value) {
5
+ if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
6
+ return String(value || "").split(",").map((s) => s.trim()).filter(Boolean);
7
+ }
8
+
9
+ function projectName(project) {
10
+ if (project?.name) return project.name;
11
+ try {
12
+ const meta = JSON.parse(fs.readFileSync(path.join(project.path, ".apc", "project.json"), "utf8"));
13
+ return meta.name || path.basename(project.path);
14
+ } catch {
15
+ return path.basename(project?.path || "");
16
+ }
17
+ }
18
+
19
+ export function agentSkills(agent) {
20
+ return listField(agent?.fields?.Skills);
21
+ }
22
+
23
+ export function buildAgentSystem(project, agent, {
24
+ invocation = "engine",
25
+ runtime = null,
26
+ channel = null,
27
+ caller = null,
28
+ routine = null,
29
+ extraParts = [],
30
+ } = {}) {
31
+ const fields = agent.fields || {};
32
+ const parts = [
33
+ `You are APC agent "${agent.slug}".`,
34
+ `Project: ${projectName(project)} (${project.path}).`,
35
+ ];
36
+
37
+ if (fields.Description) parts.push(String(fields.Description));
38
+ if (fields.Role) parts.push(`Role: ${fields.Role}`);
39
+ if (fields.Language) parts.push(`Default language: ${fields.Language}`);
40
+
41
+ const declaredTools = listField(fields.Tools);
42
+ if (declaredTools.length) {
43
+ parts.push(
44
+ [
45
+ "## Declared Tool Hints",
46
+ declaredTools.join(", "),
47
+ "These are agent-level tool expectations, not a guarantee. Actual callable tools depend on invocation surface.",
48
+ ].join("\n")
49
+ );
50
+ }
51
+
52
+ parts.push(buildInvocationContext({ invocation, runtime, channel, caller, routine }));
53
+
54
+ const memPath = path.join(project.path, ".apc", "agents", agent.slug, "memory.md");
55
+ if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
56
+
57
+ const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
58
+ if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
59
+
60
+ for (const skill of agentSkills(agent)) {
61
+ const skillPath = path.join(project.path, ".apc", "skills", `${skill}.md`);
62
+ if (fs.existsSync(skillPath)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(skillPath, "utf8"));
63
+ }
64
+
65
+ for (const ep of extraParts) {
66
+ if (ep) parts.push(ep);
67
+ }
68
+
69
+ return parts.join("\n\n");
70
+ }
71
+
72
+ function buildInvocationContext({ invocation, runtime, channel, caller, routine }) {
73
+ const lines = [
74
+ "## Invocation Context",
75
+ `invocation: ${invocation}`,
76
+ ];
77
+ if (runtime) lines.push(`runtime: ${runtime}`);
78
+ if (channel) lines.push(`channel: ${channel}`);
79
+ if (caller) lines.push(`caller: ${caller}`);
80
+ if (routine) lines.push(`routine: ${routine}`);
81
+
82
+ if (runtime) {
83
+ lines.push(
84
+ "You are running inside the named external runtime. Use only tools and permissions that runtime actually exposes."
85
+ );
86
+ } else if (invocation === "engine") {
87
+ lines.push("You are a direct LLM call through APX. Do not claim shell, file, MCP, or Telegram tools unless APX explicitly provided them.");
88
+ } else if (invocation === "telegram") {
89
+ lines.push("You are replying through Telegram. Keep responses brief, plain text, and matched to the user's language.");
90
+ } else if (invocation === "routine") {
91
+ lines.push("You were invoked by an APX routine. Complete the requested work now; do not say you will do it later.");
92
+ }
93
+
94
+ return lines.join("\n");
95
+ }
@@ -297,9 +297,9 @@ export function getRecentTelegramTurns(
297
297
  // answer that mixes fragments of the old one with hallucinations. The
298
298
  // failure observed with qwen2.5:14b was:
299
299
  //
300
- // prev assistant: "agente sandbox con modelo ollama:llama3.2:3b"
301
- // user: "y en el otro proyecto qué agente tiene?"
302
- // assistant (alucinated): "agente assistant con modelo ollama:llama3.2:3b"
300
+ // prev assistant: "sandbox agent with model ollama:llama3.2:3b"
301
+ // user: "and what agent does the other project have?"
302
+ // assistant (hallucinated): "assistant agent with model ollama:llama3.2:3b"
303
303
  // (sofia exists, not "assistant", and her model is
304
304
  // claude-haiku-4-5, not the carry-over from above)
305
305
  //
@@ -66,7 +66,7 @@ export function createRuntimeSession({ projectRoot, storageRoot = projectRoot, a
66
66
  `agent: ${agentSlug}\n` +
67
67
  `title: ${sessionTitle}\n` +
68
68
  `task_ref: ${taskRef}\n` +
69
- `status: 🔄 En progreso\n` +
69
+ `status: 🔄 In progress\n` +
70
70
  `started: ${started}\n` +
71
71
  `completed: \n` +
72
72
  `result: \n` +
@@ -91,7 +91,7 @@ export function closeRuntimeSession({ filePath, externalSessionPath, exitCode, r
91
91
  } else if (result) {
92
92
  text = setField(text, "result", result.slice(0, 300));
93
93
  }
94
- text = setField(text, "status", exitCode === 0 ? "✅ Completada" : "⚠️ Cerrada con error");
94
+ text = setField(text, "status", exitCode === 0 ? "✅ Completed" : "⚠️ Closed with error");
95
95
  fs.writeFileSync(filePath, text);
96
96
  }
97
97
 
package/src/daemon/api.js CHANGED
@@ -42,6 +42,7 @@ import { readGlobalMessages, readProjectMessages, searchProjectMessages } from "
42
42
  import { readAgents } from "../core/parser.js";
43
43
  import { parseSessionFrontmatter } from "../core/parser.js";
44
44
  import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
45
+ import { buildAgentSystem } from "../core/agent-system.js";
45
46
 
46
47
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
47
48
 
@@ -421,7 +422,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
421
422
  if (!modelId) return res.status(400).json({ error: "agent has no model and none provided" });
422
423
 
423
424
  try {
424
- const system = buildAgentSystem(p, agent);
425
+ const system = buildAgentSystem(p, agent, { invocation: "engine" });
425
426
  const conv = startConversation({ storagePath: p.storagePath, agentSlug: agent.slug, engine: modelId, system });
426
427
  appendTurn({ filePath: conv.path, role: "user", content: prompt });
427
428
 
@@ -488,9 +489,9 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
488
489
 
489
490
  // Build system prompt — inject compact summary if this conversation was compacted.
490
491
  const extraParts = compactSummary
491
- ? [`## Contexto de conversación anterior (compactado)\n${compactSummary}`]
492
+ ? [`## Previous Conversation Context (Compacted)\n${compactSummary}`]
492
493
  : [];
493
- const system = buildAgentSystem(p, agent, { extraParts });
494
+ const system = buildAgentSystem(p, agent, { invocation: "engine", extraParts });
494
495
 
495
496
  if (!conversation_id) {
496
497
  const conv = startConversation({ storagePath: p.storagePath, agentSlug: agent.slug, engine: modelId, system });
@@ -688,6 +689,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
688
689
  });
689
690
 
690
691
  const system = buildAgentSystem(p, agent, {
692
+ invocation: "runtime",
693
+ runtime,
691
694
  extraParts: [
692
695
  buildApfHint({
693
696
  projectName,
@@ -775,11 +778,11 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
775
778
  if (req.query.summarize === "true" && isSuperAgentEnabled(config)) {
776
779
  try {
777
780
  const prompt =
778
- `Resumí qué pasó en esta sesión APC en 4 bullets concretos.\n\n` +
781
+ `Summarize what happened in this APC session in 4 concrete bullets.\n\n` +
779
782
  `Frontmatter:\n${JSON.stringify(out.frontmatter, null, 2)}\n\n` +
780
783
  (out.external_transcript
781
- ? `Transcript externo (últimos ${out.external_transcript.tail.length} chars):\n${out.external_transcript.tail}`
782
- : `(sin transcript externo)`);
784
+ ? `External transcript (last ${out.external_transcript.tail.length} chars):\n${out.external_transcript.tail}`
785
+ : `(no external transcript)`);
783
786
  const sa = await runSuperAgent({ globalConfig: config, projects, plugins, registries, prompt, contextNote: `Resume request for session ${id}.` });
784
787
  out.summary = sa.text;
785
788
  } catch (e) {
@@ -1084,26 +1087,3 @@ function agentToResponse(a) {
1084
1087
  extra,
1085
1088
  };
1086
1089
  }
1087
-
1088
- // Build system prompt from an agent's fields + memory + skills.
1089
- // Optional `extraParts` are appended at the end.
1090
- function buildAgentSystem(p, agent, { extraParts = [] } = {}) {
1091
- const f = agent.fields || {};
1092
- const parts = [];
1093
- if (f.Description) parts.push(f.Description);
1094
- if (f.Role) parts.push(`Role: ${f.Role}`);
1095
- if (f.Language) parts.push(`Default language: ${f.Language}`);
1096
- const memPath = path.join(p.path, ".apc", "agents", agent.slug, "memory.md");
1097
- if (fs.existsSync(memPath)) {
1098
- parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
1099
- }
1100
- const apxSkill = path.join(p.path, ".apc", "skills", "apx.md");
1101
- if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
1102
- const skills = Array.isArray(f.Skills) ? f.Skills : [];
1103
- for (const skill of skills) {
1104
- const sp = path.join(p.path, ".apc", "skills", `${skill}.md`);
1105
- if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
1106
- }
1107
- for (const ep of extraParts) parts.push(ep);
1108
- return parts.join("\n\n");
1109
- }
@@ -28,13 +28,13 @@
28
28
  // }
29
29
 
30
30
  import fs from "node:fs";
31
- import path from "node:path";
32
31
  import { TELEGRAM_STATE_PATH } from "../../core/config.js";
33
32
  import { callEngine } from "../engines/index.js";
34
33
  import { runSuperAgent, isSuperAgentEnabled } from "../super-agent.js";
35
34
  import { stripThinking } from "../thinking.js";
36
35
  import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "../../core/messages-store.js";
37
36
  import { readAgents } from "../../core/parser.js";
37
+ import { buildAgentSystem } from "../../core/agent-system.js";
38
38
 
39
39
  const API_BASE = "https://api.telegram.org";
40
40
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -297,7 +297,7 @@ class ChannelPoller {
297
297
  // honor it for future messages.
298
298
  if (isReset) {
299
299
  try {
300
- const ack = "Listo, contexto limpiado. Empezamos de cero ¿qué necesitás?";
300
+ const ack = "Done, context cleared. Starting fresh. What do you need?";
301
301
  await this._send({ chat_id, text: ack });
302
302
  appendGlobalMessage({
303
303
  channel: "telegram",
@@ -327,7 +327,11 @@ class ChannelPoller {
327
327
  const agent = readAgents(target.path).find((a) => a.slug === routeSlug);
328
328
  if (agent && agent.fields.Model) {
329
329
  try {
330
- const system = buildAgentSystem(target, agent, author);
330
+ const system = buildAgentSystem(target, agent, {
331
+ invocation: "telegram",
332
+ channel: this.channel.name,
333
+ caller: author,
334
+ });
331
335
  const result = await callEngine({
332
336
  modelId: agent.fields.Model,
333
337
  system,
@@ -486,26 +490,6 @@ class ChannelPoller {
486
490
  }
487
491
  }
488
492
 
489
- // ---------- system-prompt builder (same as /exec) ---------------------------
490
-
491
- function buildAgentSystem(target, agent, author) {
492
- const parts = [];
493
- if (agent.fields.Description) parts.push(agent.fields.Description);
494
- if (agent.fields.Role) parts.push(`Role: ${agent.fields.Role}`);
495
- if (agent.fields.Language) parts.push(`Default language: ${agent.fields.Language}`);
496
- parts.push(
497
- `You are speaking via Telegram with ${author}. Keep responses brief — ideally under 4 sentences. Mirror their language.`
498
- );
499
- const memPath = path.join(target.path, ".apc", "agents", agent.slug, "memory.md");
500
- if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
501
- const skills = (agent.fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean);
502
- for (const skill of skills) {
503
- const sp = path.join(target.path, ".apc", "skills", `${skill}.md`);
504
- if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
505
- }
506
- return parts.join("\n\n");
507
- }
508
-
509
493
  function sleep(ms) {
510
494
  return new Promise((r) => setTimeout(r, ms));
511
495
  }
@@ -15,7 +15,7 @@
15
15
  // },
16
16
  // "routines": [
17
17
  // { "name": "morning-report", "schedule": "0 9 * * *", "agent": "sofia",
18
- // "prompt": "Resumen del día anterior", "channel": "telegram" }
18
+ // "prompt": "Previous day summary", "channel": "telegram" }
19
19
  // ]
20
20
  // }
21
21
  //
@@ -11,11 +11,10 @@
11
11
  // shell — run a shell command. spec: { command, timeout_ms? }
12
12
 
13
13
  import { spawn } from "node:child_process";
14
- import path from "node:path";
15
- import fs from "node:fs";
16
14
  import { callEngine } from "./engines/index.js";
17
15
  import { runSuperAgent } from "./super-agent.js";
18
16
  import { readAgents } from "../core/parser.js";
17
+ import { buildAgentSystem } from "../core/agent-system.js";
19
18
  import {
20
19
  listRoutines,
21
20
  getRoutine,
@@ -70,17 +69,13 @@ async function handleExecAgent(ctx, routine) {
70
69
  const model = agent.fields.Model;
71
70
  if (!model) throw new Error(`agent ${slug} has no model`);
72
71
 
73
- const f = agent.fields;
74
- const parts = [];
75
- if (f.Description) parts.push(f.Description);
76
- if (f.Role) parts.push(`Role: ${f.Role}`);
77
- const memPath = path.join(project.path, ".apc", "agents", slug, "memory.md");
78
- if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
79
- parts.push(`You were invoked by routine "${routine.name}". Reply briefly, max 4 sentences.`);
80
-
81
72
  const result = await callEngine({
82
73
  modelId: model,
83
- system: parts.join("\n\n"),
74
+ system: buildAgentSystem(project, agent, {
75
+ invocation: "routine",
76
+ routine: routine.name,
77
+ extraParts: [`Reply briefly, max 4 sentences.`],
78
+ }),
84
79
  messages: [{ role: "user", content: prompt }],
85
80
  config: project.config || globalConfig,
86
81
  });
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
2
+ import { agentSkills, buildAgentSystem as buildCoreAgentSystem } from "../../core/agent-system.js";
3
3
 
4
4
  export function projectMeta(projects, entry) {
5
5
  const meta = projects.list().find((p) => p.id === entry.id);
@@ -61,8 +61,7 @@ export function safePathJoin(root, sub = ".") {
61
61
  }
62
62
 
63
63
  export function skillsFromFields(fields = {}) {
64
- if (Array.isArray(fields.Skills)) return fields.Skills;
65
- return (fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean);
64
+ return agentSkills({ fields });
66
65
  }
67
66
 
68
67
  export function agentRow(agent) {
@@ -76,24 +75,8 @@ export function agentRow(agent) {
76
75
  };
77
76
  }
78
77
 
79
- export function buildAgentSystem(project, agent) {
80
- const parts = [];
81
- if (agent.fields.Description) parts.push(agent.fields.Description);
82
- if (agent.fields.Role) parts.push(`Role: ${agent.fields.Role}`);
83
- if (agent.fields.Language) parts.push(`Default language: ${agent.fields.Language}`);
84
-
85
- const memPath = path.join(project.path, ".apc", "agents", agent.slug, "memory.md");
86
- if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
87
-
88
- const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
89
- if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
90
-
91
- for (const skill of skillsFromFields(agent.fields)) {
92
- const skillPath = path.join(project.path, ".apc", "skills", `${skill}.md`);
93
- if (fs.existsSync(skillPath)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(skillPath, "utf8"));
94
- }
95
-
96
- return parts.join("\n\n");
78
+ export function buildAgentSystem(project, agent, opts = {}) {
79
+ return buildCoreAgentSystem(project, agent, opts);
97
80
  }
98
81
 
99
82
  export function createPermissionGuard(globalConfig = {}, { implicitConfirmation = false } = {}) {
@@ -28,7 +28,10 @@ export default {
28
28
 
29
29
  const result = await callEngine({
30
30
  modelId: agent.fields.Model,
31
- system: buildAgentSystem(p, agent),
31
+ system: buildAgentSystem(p, agent, {
32
+ invocation: "engine",
33
+ caller: "super_agent_tool",
34
+ }),
32
35
  messages: [{ role: "user", content: prompt }],
33
36
  config: p.config || globalConfig,
34
37
  });
@@ -68,7 +68,11 @@ export default {
68
68
  }
69
69
 
70
70
  const r = await rt.run({
71
- system: buildAgentSystem(p, agent),
71
+ system: buildAgentSystem(p, agent, {
72
+ invocation: "runtime",
73
+ runtime,
74
+ caller: "super_agent_tool",
75
+ }),
72
76
  prompt,
73
77
  cwd: p.path,
74
78
  timeoutMs: timeout_s * 1000,
@@ -33,6 +33,7 @@ APC projects are filesystem projects anywhere on disk with AGENTS.md and .apc/pr
33
33
  Useful CLI facts:
34
34
  - Permission mode: apx permission show; apx permission set total|automatico|permiso.
35
35
  - Routines: apx routine list|get|history|run|add. Autonomous super-agent routines use kind super_agent.
36
+ - Routine design: if the user asks for an agent to think, decide, write, or reply, create an exec_agent routine with spec.agent and spec.prompt. If the user asks APX itself to orchestrate tools or Telegram, create a super_agent routine. If the request is only a deterministic command, create a shell routine. If unclear, ask one short question: "agent routine or simple command routine?"
36
37
  - Safe read-only shell checks such as apx --help, apx routine list, docker ps, find, ls, rg, grep can run in automatico without asking.
37
38
 
38
39
  Channel context:
@@ -57,30 +58,30 @@ Available tools:
57
58
 
58
59
  HARD RULES (do not deviate):
59
60
  1. NEVER invent project names, agent slugs, model ids, MCP names or paths. ALWAYS look them up via list_* first.
60
- 2. If the user says "los agentes" / "lista" / "qué hay" without specifying a project, that means **all of them** — call the tool WITHOUT a project argument and the result will include every project.
61
+ 2. If the user asks for agents, lists, inventory, or "what exists" without specifying a project, that means **all of them** — call the tool WITHOUT a project argument and the result will include every project.
61
62
  3. NEVER answer "specify a project" — instead, just call the tool with no argument and you'll get the full picture.
62
63
  4. If a tool result has an error, retry with different arguments before falling back to asking the user.
63
64
  5. Respect permission mode. total = execute requested actions without confirmation. automatico = read/list/safe shell actions run directly; destructive, external, runtime, MCP calls, outbound messages, config, and filesystem mutations need explicit user confirmation. permiso = only allowed tools run directly; everything else needs confirmation.
64
- 6. Default language: es-AR. Plain text, no markdown formatting (Telegram doesn't render it).
65
+ 6. Write in the user's language unless they request another language. The system prompt stays English. Plain text, no markdown formatting for Telegram.
65
66
  7. Stay brief: under 6 sentences unless asked for detail.
66
67
  8. You DO see recent prior turns of this chat as previous messages when applicable. **Use them ONLY to disambiguate references** (e.g. "el primero" → first project mentioned earlier). For ANY factual data — agent details, MCP details, file contents, memory — RE-CALL the tool. Past turns are context, not a cache. Models change, agents change, files change.
67
68
  9. /reset or /new from the user means "forget previous turns and answer this one fresh" — if you see those prefixes the operator already cleared the context for you.
68
69
  10. ACTION RULE: use direct tools for direct work. run_shell executes commands; write_file/edit_file modify files. call_runtime is only for spawning a separate external runtime/chat. call_mcp is only for an MCP server/tool.
69
- 11. DISPATCH RULE: when the user says things like "que <agente> haga X", "iniciá una sesión con Claude/Codex", "que <agente> arranque <runtime>", "andá a <runtime> y hacé X" — that is a call_runtime request. Look up the agent slug with list_agents if needed, then call call_runtime({agent: <slug>, runtime: 'claude-code'|'codex'|'opencode'|'aider', prompt: <user's request>}). The agent's declared model (in AGENTS.md) is IGNORED in this case; the runtime supplies the model. Memory + skills of the agent become the system prompt of the runtime.
70
+ 11. DISPATCH RULE: when the user asks a named agent to work inside Claude, Codex, OpenCode, or Aider, that is a call_runtime request. Look up the agent slug with list_agents if needed, then call call_runtime({agent: <slug>, runtime: 'claude-code'|'codex'|'opencode'|'aider', prompt: <user's request>}). The agent's declared model (in AGENTS.md) is IGNORED in this case; the runtime supplies the model. Memory + skills of the agent become the system prompt of the runtime.
70
71
  12. PROJECT RULE: when the user gives no project, use project "default". Do not infer a non-default project from old chat history unless the user references it. If they mention a path or project name, look it up or add it with add_project.
71
72
  13. VAULT RULE: when the user wants a new existing agent/template, call list_vault_agents first. If a suitable vault agent exists, import_agent into the chosen project. If none fits, say briefly what is missing.
72
- 14. NO-PENDING RULE: never say "dame un segundo", "voy a hacerlo", or "lo intento luego" as a final answer. Either call the tool in this same turn or say what blocks you.
73
- 15. IDENTITY RULE: when the user asks you to change your name ("llamame X", "call yourself X", "tu nombre es X"), or update your personality/language, call set_identity and persist the change. Then confirm with your new name.`;
73
+ 14. NO-PENDING RULE: never say "give me a second", "I will do it", or "I will try later" as a final answer. Either call the tool in this same turn or say what blocks you.
74
+ 15. IDENTITY RULE: when the user asks you to change your name, call yourself something, or update your personality/language, call set_identity and persist the change. Then confirm with your new name.`;
74
75
 
75
76
  function isShortConfirmation(text) {
76
- return /^(s[ií]|si dale| dale|dale|ok|oka|okey|confirmo|confirmado|mandale|hacelo|proced[eé]|vamos)\b/i
77
+ return /^(yes|y|si|si dale|dale|ok|okay|confirm|confirmed|go|proceed|do it)\b/i
77
78
  .test(String(text || "").trim());
78
79
  }
79
80
 
80
81
  function lastAssistantAskedForConfirmation(messages) {
81
82
  for (let i = messages.length - 1; i >= 0; i--) {
82
83
  if (messages[i]?.role !== "assistant") continue;
83
- return /\b(confirm[aá]s?|confirmame|ok|permiso|puedo|dale|proced[ao])\b/i.test(messages[i].content || "");
84
+ return /\b(confirm|confirmation|ok|okay|permission|allowed|proceed|do it|dale)\b/i.test(messages[i].content || "");
84
85
  }
85
86
  return false;
86
87
  }
@@ -11,20 +11,20 @@ function detectLanguage(identity) {
11
11
  const lang = process.env.LANG || process.env.LC_MESSAGES || process.env.LC_ALL || "";
12
12
  const code = lang.split(/[_\.]/)[0].toLowerCase();
13
13
  const map = {
14
- es: "Spanish (Español)",
14
+ es: "Spanish",
15
15
  en: "English",
16
- fr: "French (Français)",
17
- pt: "Portuguese (Português)",
18
- de: "German (Deutsch)",
19
- it: "Italian (Italiano)",
20
- nl: "Dutch (Nederlands)",
21
- ru: "Russian (Русский)",
22
- ja: "Japanese (日本語)",
23
- zh: "Chinese (中文)",
24
- ko: "Korean (한국어)",
25
- ar: "Arabic (العربية)",
16
+ fr: "French",
17
+ pt: "Portuguese",
18
+ de: "German",
19
+ it: "Italian",
20
+ nl: "Dutch",
21
+ ru: "Russian",
22
+ ja: "Japanese",
23
+ zh: "Chinese",
24
+ ko: "Korean",
25
+ ar: "Arabic",
26
26
  };
27
- return map[code] || "Spanish (Español)"; // default es-AR like the super-agent
27
+ return map[code] || "English";
28
28
  }
29
29
 
30
30
  async function generateMessage(identity, engineConfig) {