@agentprojectcontext/apx 1.34.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +1 -1
  3. package/src/core/agent/build-agent-system.js +134 -58
  4. package/src/core/agent/channels/voice-context.js +4 -4
  5. package/src/core/agent/prompt-builder.js +176 -123
  6. package/src/core/agent/prompts/channels/code.md +12 -10
  7. package/src/core/agent/prompts/channels/desktop.md +5 -32
  8. package/src/core/agent/prompts/channels/telegram.md +4 -15
  9. package/src/core/agent/prompts/channels/web_code.md +11 -11
  10. package/src/core/agent/prompts/core/agent-base.md +24 -0
  11. package/src/core/agent/prompts/core/project-agent.md +11 -0
  12. package/src/core/agent/prompts/core/super-agent.md +21 -0
  13. package/src/core/agent/prompts/discipline/action.md +10 -0
  14. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  15. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  16. package/src/core/agent/self-memory.js +43 -1
  17. package/src/core/agent/skills/index-store.js +307 -0
  18. package/src/core/agent/skills/index.js +15 -1
  19. package/src/core/agent/skills/inspector.js +317 -0
  20. package/src/core/agent/super-agent.js +7 -1
  21. package/src/core/agent/tools/handlers/_git.js +50 -0
  22. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  23. package/src/core/agent/tools/handlers/git-log.js +38 -0
  24. package/src/core/agent/tools/handlers/git-show.js +34 -0
  25. package/src/core/agent/tools/handlers/git-status.js +61 -0
  26. package/src/core/agent/tools/names.js +31 -0
  27. package/src/core/agent/tools/registry.js +36 -5
  28. package/src/core/config/index.js +21 -0
  29. package/src/core/runtime-skills/apx/SKILL.md +27 -39
  30. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +40 -56
  31. package/src/core/runtime-skills/apx-agent/SKILL.md +27 -30
  32. package/src/core/runtime-skills/apx-mcp/SKILL.md +31 -36
  33. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +37 -51
  34. package/src/core/runtime-skills/apx-project/SKILL.md +20 -29
  35. package/src/core/runtime-skills/apx-routine/SKILL.md +34 -47
  36. package/src/core/runtime-skills/apx-runtime/SKILL.md +32 -50
  37. package/src/core/runtime-skills/apx-sessions/SKILL.md +96 -145
  38. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +53 -77
  39. package/src/core/runtime-skills/apx-task/SKILL.md +18 -21
  40. package/src/core/runtime-skills/apx-telegram/SKILL.md +43 -54
  41. package/src/core/runtime-skills/apx-voice/SKILL.md +36 -56
  42. package/src/host/daemon/api/skills.js +140 -6
  43. package/src/host/daemon/api/super-agent.js +56 -1
  44. package/src/host/daemon/index.js +17 -0
  45. package/src/interfaces/cli/branding.js +53 -0
  46. package/src/interfaces/cli/commands/skills.js +254 -0
  47. package/src/interfaces/cli/index.js +84 -2
  48. package/src/interfaces/web/dist/assets/index-C0fm31dY.js +618 -0
  49. package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +1 -0
  50. package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +1 -0
  51. package/src/interfaces/web/dist/index.html +2 -2
  52. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +21 -1
  53. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +68 -0
  54. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  55. package/src/interfaces/web/src/hooks/useChat.ts +19 -0
  56. package/src/interfaces/web/src/i18n/en.ts +1 -0
  57. package/src/interfaces/web/src/i18n/es.ts +1 -0
  58. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  59. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  60. package/src/interfaces/web/src/types/daemon.ts +10 -0
  61. package/src/core/agent/prompts/action-discipline.md +0 -24
  62. package/src/core/agent/prompts/super-agent-base.md +0 -42
  63. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +0 -1
  64. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +0 -613
  65. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +0 -1
@@ -0,0 +1,10 @@
1
+ # Action discipline (mandatory)
2
+ - NEVER acknowledge an action you will not execute in the same turn. If you say you will do something, the tool call must be in the same response.
3
+ - Empty acknowledgments ("Ok", "On it", "Give me a moment", "I'll do that now") are not valid standalone replies when a tool call is expected. Either call the tool in this turn, or explain WHY you can't (missing permission, unclear params, tool unavailable).
4
+ - If the user asks for multiple things, do them all in this turn using sequential tool calls.
5
+ - If a tool errors, retry with different arguments before asking the user.
6
+
7
+ # Chit-chat
8
+ - A pure greeting / thanks / "ok" with no actionable request → reply with `finish` only, no other tool call. Tools exist so you can act when needed, not so you must use one.
9
+ - A greeting that piggybacks a real request ("hola, listame las rutinas") is NOT chit-chat — handle the request normally.
10
+ - When in doubt, ask ONE short clarifying question via `finish` — never invent a topic to "be useful".
@@ -0,0 +1,6 @@
1
+ # One reply per turn (voice / desktop / deck-voice)
2
+ Your reply will be spoken aloud, or shown briefly in a small surface. Produce ONE clean message per turn — no intro filler, no "ahora ejecuto X" before the tool, no restating after.
3
+
4
+ - Call any tools you need silently, then write the ONE answer with the result.
5
+ - Lead with the outcome. Keep it to 1–2 short sentences unless the user asks for detail.
6
+ - Greet at most once per conversation; if you already greeted, skip it.
@@ -0,0 +1,11 @@
1
+ # Two-segment turns (text channels with visible history)
2
+ When you call a tool, the user sees two text segments — the intro before the tool runs, and the answer after it returns.
3
+
4
+ 1. **Intro** — a short natural filler in the user's language BEFORE the tool runs. 2–8 words. NEVER contains the answer. Examples: "Reviso eso", "Dale, lo anoto", "Un momento, busco".
5
+ 2. **Answer** — the substantive result AFTER the tool returns. Carries the data, the confirmation, or the next question.
6
+
7
+ Rules:
8
+ - The intro NEVER includes the substantive content. The tool hasn't run yet — you don't know the result.
9
+ - The answer NEVER restates the intro. They're complementary: filler + result.
10
+ - Greet at most ONCE per turn. If the intro greeted, the answer starts with the result.
11
+ - A turn with NO tool calls produces ONE segment — go straight to the answer.
@@ -44,7 +44,49 @@ export function readSelfMemoryForPrompt(limit = SELF_MEMORY_PROMPT_LIMIT) {
44
44
  const body = readSelfMemory().trim();
45
45
  if (!body) return "";
46
46
  if (body.length <= limit) return body;
47
- return body.slice(0, limit).trimEnd() + "\n… (truncated — call read_self_memory for the full notebook)";
47
+
48
+ // The notebook grows chronologically (oldest day first), so a naive head
49
+ // slice injects the OLDEST notes and truncates the most recent — exactly
50
+ // backwards for "what's relevant now". Keep the file header + the NEWEST
51
+ // entries that fit `limit`, re-grouped under their date headings. The full
52
+ // file is always available via read_self_memory.
53
+ const firstLine = body.split("\n", 1)[0];
54
+ const header = firstLine.startsWith("# ") ? firstLine : notebookHeader();
55
+ const notice =
56
+ "_(most recent notes — older history truncated; call read_self_memory for the full notebook)_";
57
+
58
+ const entries = parseSelfMemoryEntries(body); // oldest → newest
59
+ if (!entries.length) {
60
+ // No structured bullets (free-form prose notebook) — fall back to the tail.
61
+ const tail = body.slice(-(limit - notice.length - 2)).replace(/^\S*\n/, "");
62
+ return `${notice}\n${tail.trim()}`;
63
+ }
64
+
65
+ let budget = limit - header.length - notice.length - 4;
66
+ const picked = [];
67
+ for (let i = entries.length - 1; i >= 0; i--) {
68
+ const e = entries[i];
69
+ const tag =
70
+ (e.time ? `[${e.time}]` : "") +
71
+ (e.channel && e.channel !== "memory" ? `[${e.channel}]` : "");
72
+ const bullet = `- ${tag ? tag + " " : ""}${e.text}`.replace(/\s+/g, " ").trim();
73
+ const cost = bullet.length + 14; // headroom for an occasional date heading
74
+ if (budget - cost < 0 && picked.length) break;
75
+ picked.push({ date: e.date, bullet });
76
+ budget -= cost;
77
+ }
78
+ picked.reverse(); // back to chronological order (newest at the bottom)
79
+
80
+ const out = [header, notice];
81
+ let lastDate = "";
82
+ for (const p of picked) {
83
+ if (p.date && p.date !== lastDate) {
84
+ out.push("", `## ${p.date}`);
85
+ lastDate = p.date;
86
+ }
87
+ out.push(p.bullet);
88
+ }
89
+ return out.join("\n").trim();
48
90
  }
49
91
 
50
92
  // HH:MM (UTC) for the current time — used to tag notes per the cross-channel
@@ -0,0 +1,307 @@
1
+ // Persistent vector index for the skill inspector.
2
+ //
3
+ // Why a store instead of re-embedding every turn:
4
+ // - The inspector scores the user prompt against every known skill on every
5
+ // turn. Even with the in-process cache in rag.js, a daemon restart pays the
6
+ // cold cost. A JSON-backed store survives restarts and makes `apx skills
7
+ // index` a real, observable operation (progress bar, totals).
8
+ // - It also unlocks "chunked" descriptions later: today we embed just the
9
+ // condensed description; tomorrow we can index the SKILL.md body itself.
10
+ //
11
+ // Format (~/.apx/skills/.index.json):
12
+ // {
13
+ // embedder: "tf" | "ollama:nomic-embed-text" | "openai:text-embedding-3-small" | ...,
14
+ // dim: 256 | 768 | ...,
15
+ // updated_at: "2026-06-13T...",
16
+ // items: {
17
+ // "<slug>": {
18
+ // slug, source, file, mtime_ms,
19
+ // desc_hash, desc, desc_vector: [..],
20
+ // // future: chunks: [{ text, vector }]
21
+ // }
22
+ // }
23
+ // }
24
+ //
25
+ // Invariants:
26
+ // - All vectors in the file share the same embedder tag and dim. Switching
27
+ // embedder invalidates the entire index — we rebuild from scratch.
28
+ // - A skill whose source file has a different mtime than what's recorded is
29
+ // re-embedded the next time `ensureIndex` runs. A skill that disappeared
30
+ // is dropped.
31
+ // - Reads NEVER throw into the daemon: a corrupted file is treated as empty.
32
+ //
33
+ // Concurrency: the daemon is single-process for now; we use a simple write-
34
+ // then-rename to avoid half-written files, no advisory lock. If a future
35
+ // multi-process arrangement is added, swap in proper file locking here.
36
+
37
+ import fs from "node:fs";
38
+ import path from "node:path";
39
+ import os from "node:os";
40
+
41
+ import { embedOne } from "#core/memory/embeddings.js";
42
+ import { listSkills } from "./loader.js";
43
+ import { condenseSkillDescription } from "./catalog.js";
44
+
45
+ const INDEX_PATH = path.join(os.homedir(), ".apx", "skills", ".index.json");
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Disk I/O
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function emptyIndex() {
52
+ return { embedder: null, dim: null, updated_at: null, items: {} };
53
+ }
54
+
55
+ export function indexPath() {
56
+ return INDEX_PATH;
57
+ }
58
+
59
+ export function readIndex() {
60
+ try {
61
+ if (!fs.existsSync(INDEX_PATH)) return emptyIndex();
62
+ const raw = fs.readFileSync(INDEX_PATH, "utf8");
63
+ const parsed = JSON.parse(raw);
64
+ if (!parsed || typeof parsed !== "object" || !parsed.items) return emptyIndex();
65
+ return parsed;
66
+ } catch {
67
+ return emptyIndex();
68
+ }
69
+ }
70
+
71
+ function writeIndex(idx) {
72
+ fs.mkdirSync(path.dirname(INDEX_PATH), { recursive: true });
73
+ const tmp = INDEX_PATH + ".tmp";
74
+ fs.writeFileSync(tmp, JSON.stringify(idx, null, 2));
75
+ fs.renameSync(tmp, INDEX_PATH);
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Hashing
80
+ // ---------------------------------------------------------------------------
81
+
82
+ function descHashOf(text) {
83
+ let h = 0;
84
+ const s = String(text || "");
85
+ for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
86
+ return h;
87
+ }
88
+
89
+ function fileMtimeMs(file) {
90
+ try { return fs.statSync(file).mtimeMs; } catch { return 0; }
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Build / refresh
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Decide what work the next ensureIndex() call would do — without actually
99
+ * embedding anything. Used by `apx skills index` to render the progress bar
100
+ * and by the inspector startup probe to decide whether to bother rebuilding.
101
+ *
102
+ * Returns: { existing, missing, stale, gone, total } — `existing` are the
103
+ * slugs that already have an up-to-date vector, `missing` need a first embed,
104
+ * `stale` had their description rewritten, `gone` are slugs in the index but
105
+ * no longer on disk.
106
+ */
107
+ export function planIndex({ projectPath, currentEmbedder } = {}) {
108
+ const skills = listSkills({ projectPath });
109
+ const idx = readIndex();
110
+ const embedderChanged = currentEmbedder && idx.embedder && idx.embedder !== currentEmbedder;
111
+
112
+ const existing = [];
113
+ const missing = [];
114
+ const stale = [];
115
+ const slugsSeen = new Set();
116
+
117
+ for (const s of skills) {
118
+ slugsSeen.add(s.slug);
119
+ const desc = condenseSkillDescription(s.description);
120
+ const hash = descHashOf(desc + "|" + s.file);
121
+ const mtime = fileMtimeMs(s.file);
122
+ const hit = idx.items?.[s.slug];
123
+
124
+ if (embedderChanged || !hit || !Array.isArray(hit.desc_vector)) {
125
+ missing.push(s.slug);
126
+ } else if (hit.desc_hash !== hash || hit.mtime_ms !== mtime) {
127
+ stale.push(s.slug);
128
+ } else {
129
+ existing.push(s.slug);
130
+ }
131
+ }
132
+
133
+ const gone = Object.keys(idx.items || {}).filter((slug) => !slugsSeen.has(slug));
134
+
135
+ return {
136
+ existing,
137
+ missing,
138
+ stale,
139
+ gone,
140
+ total: skills.length,
141
+ embedderChanged: !!embedderChanged,
142
+ embedder: idx.embedder,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Bring the on-disk index up to date with the current skill catalog. Skills
148
+ * with unchanged file+desc are skipped (cheap). Skills with new/changed
149
+ * content are re-embedded. Skills that disappeared are dropped. When the
150
+ * embedder differs from the file's tag, the whole index is rebuilt.
151
+ *
152
+ * @param opts.projectPath also scan this project's .apc/skills
153
+ * @param opts.embedOpts forwarded to embedOne (globalConfig, provider, ...)
154
+ * @param opts.onProgress called as ({ done, total, slug, action })
155
+ * @param opts.force rebuild every slug from scratch
156
+ * @returns { embedder, dim, items, changed: { added, refreshed, removed, kept } }
157
+ */
158
+ export async function ensureIndex({ projectPath, embedOpts = {}, onProgress, force = false } = {}) {
159
+ const skills = listSkills({ projectPath });
160
+ const idxBefore = readIndex();
161
+
162
+ // Probe the embedder once. If TF fallback wins, every skill embeds offline —
163
+ // no per-skill provider timeout cost. Tag is "<provider>:<model>" or "tf".
164
+ const probe = await embedOne("probe", embedOpts);
165
+ const embedder = embedderTag(probe);
166
+ const dim = probe.vector.length;
167
+
168
+ const embedderChanged = !force && idxBefore.embedder && idxBefore.embedder !== embedder;
169
+ const items = embedderChanged || force ? {} : structuredClone(idxBefore.items || {});
170
+
171
+ const added = [];
172
+ const refreshed = [];
173
+ const kept = [];
174
+
175
+ const seen = new Set();
176
+ let done = 0;
177
+ for (const s of skills) {
178
+ seen.add(s.slug);
179
+ const desc = condenseSkillDescription(s.description);
180
+ const hash = descHashOf(desc + "|" + s.file);
181
+ const mtime = fileMtimeMs(s.file);
182
+
183
+ const prev = items[s.slug];
184
+ const upToDate = prev
185
+ && Array.isArray(prev.desc_vector)
186
+ && prev.desc_hash === hash
187
+ && prev.mtime_ms === mtime;
188
+
189
+ let action;
190
+ if (upToDate) {
191
+ kept.push(s.slug);
192
+ action = "kept";
193
+ } else {
194
+ const out = await embedOne(desc, embedOpts);
195
+ const vector = Array.isArray(out?.vector) ? out.vector : [];
196
+ items[s.slug] = {
197
+ slug: s.slug,
198
+ source: s.source,
199
+ file: s.file,
200
+ mtime_ms: mtime,
201
+ desc_hash: hash,
202
+ desc,
203
+ desc_vector: vector,
204
+ };
205
+ if (prev) {
206
+ refreshed.push(s.slug);
207
+ action = "refreshed";
208
+ } else {
209
+ added.push(s.slug);
210
+ action = "added";
211
+ }
212
+ }
213
+
214
+ done += 1;
215
+ try { onProgress?.({ done, total: skills.length, slug: s.slug, action }); }
216
+ catch { /* progress callback errors must not break indexing */ }
217
+ }
218
+
219
+ const removed = Object.keys(items).filter((slug) => !seen.has(slug));
220
+ for (const slug of removed) delete items[slug];
221
+
222
+ const next = {
223
+ embedder,
224
+ dim,
225
+ updated_at: new Date().toISOString(),
226
+ items,
227
+ };
228
+ writeIndex(next);
229
+
230
+ return {
231
+ embedder,
232
+ dim,
233
+ items,
234
+ changed: { added, refreshed, removed, kept },
235
+ };
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Self-healing background refresh
240
+ // ---------------------------------------------------------------------------
241
+
242
+ // Single in-flight guard so concurrent turns don't stack reindexes. Module-
243
+ // scoped: one daemon process = one refresh at a time.
244
+ let refreshInFlight = null;
245
+
246
+ /**
247
+ * If the on-disk index is out of date relative to the live catalog (a skill
248
+ * was added/edited/removed, or the embedder changed), kick a background
249
+ * rebuild — WITHOUT blocking the caller. The current turn keeps using whatever
250
+ * is already indexed; the next turn sees the fresh vectors.
251
+ *
252
+ * This is what makes "drop a SKILL.md and it just works" true: the inspector
253
+ * calls this every turn (fire-and-forget), and the daemon calls it on startup.
254
+ *
255
+ * Returns a small descriptor of what it decided to do (handy for logging/tests).
256
+ */
257
+ export function backgroundRefreshIfStale({ projectPath, embedOpts = {}, currentEmbedder, onDone } = {}) {
258
+ if (refreshInFlight) return { started: false, reason: "in_flight" };
259
+
260
+ let plan;
261
+ try {
262
+ plan = planIndex({ projectPath, currentEmbedder });
263
+ } catch {
264
+ return { started: false, reason: "plan_failed" };
265
+ }
266
+
267
+ const work = plan.missing.length + plan.stale.length + plan.gone.length;
268
+ if (work === 0 && !plan.embedderChanged) {
269
+ return { started: false, reason: "fresh" };
270
+ }
271
+
272
+ refreshInFlight = ensureIndex({ projectPath, embedOpts, force: plan.embedderChanged })
273
+ .then((out) => {
274
+ try { onDone?.(out); } catch { /* best-effort */ }
275
+ return out;
276
+ })
277
+ .catch(() => null)
278
+ .finally(() => { refreshInFlight = null; });
279
+
280
+ return {
281
+ started: true,
282
+ missing: plan.missing.length,
283
+ stale: plan.stale.length,
284
+ gone: plan.gone.length,
285
+ embedderChanged: plan.embedderChanged,
286
+ };
287
+ }
288
+
289
+ /** Await any in-flight background refresh (used by tests / graceful shutdown). */
290
+ export async function awaitRefresh() {
291
+ if (refreshInFlight) { try { await refreshInFlight; } catch { /* ignore */ } }
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Helpers
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export function embedderTag(probe) {
299
+ if (!probe) return "tf";
300
+ if (probe.embedder) return probe.embedder;
301
+ return "tf";
302
+ }
303
+
304
+ /** Delete the on-disk index. Used by `apx skills index --reset`. */
305
+ export function clearIndex() {
306
+ try { fs.unlinkSync(INDEX_PATH); } catch { /* missing is fine */ }
307
+ }
@@ -3,4 +3,18 @@ export { condenseSkillDescription, buildSkillsHintBlock } from "./catalog.js";
3
3
  export { tryResolveSkillCommand } from "./trigger.js";
4
4
  export { suggestSkillForPrompt, clearSkillVectorCache } from "./rag.js";
5
5
  export { listSkills, loadSkill, SKILL_LOCATIONS } from "./loader.js";
6
-
6
+ export {
7
+ inspectPromptForSkills,
8
+ isInspectorEnabled,
9
+ INSPECTOR_DEFAULTS,
10
+ summarizeTrace,
11
+ } from "./inspector.js";
12
+ export {
13
+ ensureIndex,
14
+ planIndex,
15
+ readIndex,
16
+ clearIndex,
17
+ indexPath,
18
+ backgroundRefreshIfStale,
19
+ awaitRefresh,
20
+ } from "./index-store.js";