@agentprojectcontext/apx 1.33.1 → 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 (208) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +49 -61
  3. package/src/core/agent/a2a/reply.js +48 -0
  4. package/src/core/agent/build-agent-system.js +136 -59
  5. package/src/core/agent/channels/voice-context.js +98 -0
  6. package/src/core/agent/memory.js +2 -1
  7. package/src/core/agent/prompt-builder.js +178 -124
  8. package/src/core/agent/prompts/channels/code.md +12 -10
  9. package/src/core/agent/prompts/channels/desktop.md +5 -32
  10. package/src/core/agent/prompts/channels/telegram.md +4 -15
  11. package/src/core/agent/prompts/channels/web_code.md +11 -11
  12. package/src/core/agent/prompts/core/agent-base.md +24 -0
  13. package/src/core/agent/prompts/core/project-agent.md +11 -0
  14. package/src/core/agent/prompts/core/super-agent.md +21 -0
  15. package/src/core/agent/prompts/discipline/action.md +10 -0
  16. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  17. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  18. package/src/core/agent/prompts/modes/code-build.md +1 -0
  19. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  20. package/src/core/agent/prompts/modes/index.js +28 -0
  21. package/src/core/agent/self-memory.js +43 -1
  22. package/src/core/agent/skills/index-store.js +307 -0
  23. package/src/core/agent/skills/index.js +15 -1
  24. package/src/core/agent/skills/inspector.js +317 -0
  25. package/src/core/agent/skills/loader.js +22 -18
  26. package/src/core/agent/stream/turn-accumulator.js +73 -0
  27. package/src/core/agent/suggestions.js +37 -0
  28. package/src/core/agent/super-agent.js +7 -1
  29. package/src/core/agent/tools/handlers/_git.js +50 -0
  30. package/src/core/agent/tools/handlers/add-project.js +5 -2
  31. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  32. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  33. package/src/core/agent/tools/handlers/git-log.js +38 -0
  34. package/src/core/agent/tools/handlers/git-show.js +34 -0
  35. package/src/core/agent/tools/handlers/git-status.js +61 -0
  36. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  37. package/src/core/agent/tools/helpers.js +2 -2
  38. package/src/core/agent/tools/names.js +169 -0
  39. package/src/core/agent/tools/registry-bridge.js +6 -14
  40. package/src/core/agent/tools/registry.js +103 -69
  41. package/src/core/apc/context-copy.js +27 -0
  42. package/src/core/apc/notes.js +19 -0
  43. package/src/core/apc/parser.js +12 -5
  44. package/src/core/apc/paths.js +87 -0
  45. package/src/core/apc/scaffold.js +82 -76
  46. package/src/core/apc/skill-sync.js +10 -0
  47. package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
  48. package/src/core/config/index.js +24 -2
  49. package/src/core/config/redact.js +95 -0
  50. package/src/core/constants/channels.js +2 -0
  51. package/src/core/constants/code-modes.js +10 -0
  52. package/src/core/constants/index.js +1 -0
  53. package/src/core/deck/manifest.js +186 -0
  54. package/src/core/engines/catalog.js +83 -0
  55. package/src/core/{tools → http-tools}/browser.js +0 -1
  56. package/src/core/{tools → http-tools}/fetch.js +0 -1
  57. package/src/core/{tools → http-tools}/glob.js +0 -1
  58. package/src/core/{tools → http-tools}/grep.js +0 -1
  59. package/src/core/{tools → http-tools}/registry.js +0 -1
  60. package/src/core/{tools → http-tools}/search.js +0 -1
  61. package/src/core/i18n/en.js +9 -0
  62. package/src/core/i18n/es.js +12 -0
  63. package/src/core/i18n/index.js +54 -0
  64. package/src/core/i18n/pt.js +9 -0
  65. package/src/core/identity/telegram.js +2 -1
  66. package/src/core/mcp/runner.js +272 -14
  67. package/src/core/mcp/sources.js +3 -2
  68. package/src/core/routines/index.js +16 -0
  69. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  70. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  71. package/src/core/runtime-skills/apx/SKILL.md +83 -0
  72. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +125 -0
  73. package/src/core/runtime-skills/apx-agent/SKILL.md +97 -0
  74. package/src/core/runtime-skills/apx-mcp/SKILL.md +111 -0
  75. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +169 -0
  76. package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +20 -29
  77. package/src/core/runtime-skills/apx-routine/SKILL.md +127 -0
  78. package/src/core/runtime-skills/apx-runtime/SKILL.md +99 -0
  79. package/src/core/runtime-skills/apx-sessions/SKILL.md +232 -0
  80. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +129 -0
  81. package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +18 -21
  82. package/src/core/runtime-skills/apx-telegram/SKILL.md +120 -0
  83. package/src/core/runtime-skills/apx-voice/SKILL.md +117 -0
  84. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  85. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  86. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  87. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  88. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  89. package/src/core/stores/code-sessions.js +50 -2
  90. package/src/core/stores/routine-memory.js +1 -1
  91. package/src/core/stores/sessions-search.js +121 -0
  92. package/src/core/stores/sessions.js +38 -0
  93. package/src/core/vars/index.js +14 -0
  94. package/src/core/vars/interpolate.js +86 -0
  95. package/src/core/vars/sources.js +151 -0
  96. package/src/core/voice/audio-decode.js +38 -0
  97. package/src/core/voice/transcription.js +225 -0
  98. package/src/host/daemon/api/admin-config.js +5 -82
  99. package/src/host/daemon/api/agents.js +5 -5
  100. package/src/host/daemon/api/code.js +17 -169
  101. package/src/host/daemon/api/config.js +3 -4
  102. package/src/host/daemon/api/conversations.js +8 -29
  103. package/src/host/daemon/api/deck.js +37 -404
  104. package/src/host/daemon/api/engines.js +1 -80
  105. package/src/host/daemon/api/exec.js +1 -1
  106. package/src/host/daemon/api/mcps.js +32 -0
  107. package/src/host/daemon/api/routines.js +1 -1
  108. package/src/host/daemon/api/runtimes.js +4 -3
  109. package/src/host/daemon/api/sessions-search.js +24 -140
  110. package/src/host/daemon/api/sessions.js +12 -30
  111. package/src/host/daemon/api/shared.js +2 -1
  112. package/src/host/daemon/api/skills.js +140 -6
  113. package/src/host/daemon/api/super-agent.js +56 -1
  114. package/src/host/daemon/api/telegram.js +1 -11
  115. package/src/host/daemon/api/tools.js +6 -6
  116. package/src/host/daemon/api/transcribe.js +2 -2
  117. package/src/host/daemon/api/vars.js +137 -0
  118. package/src/host/daemon/api/voice.js +13 -290
  119. package/src/host/daemon/api.js +2 -0
  120. package/src/host/daemon/db.js +6 -6
  121. package/src/host/daemon/deck-exec.js +148 -0
  122. package/src/host/daemon/index.js +20 -3
  123. package/src/host/daemon/plugins/telegram/index.js +9 -9
  124. package/src/host/daemon/routines-scheduler.js +64 -0
  125. package/src/host/daemon/smoke.js +3 -2
  126. package/src/host/daemon/whisper-server.js +225 -0
  127. package/src/interfaces/cli/branding.js +53 -0
  128. package/src/interfaces/cli/commands/agent.js +3 -2
  129. package/src/interfaces/cli/commands/command.js +2 -3
  130. package/src/interfaces/cli/commands/messages.js +6 -2
  131. package/src/interfaces/cli/commands/pair.js +5 -4
  132. package/src/interfaces/cli/commands/search.js +1 -1
  133. package/src/interfaces/cli/commands/sessions.js +3 -2
  134. package/src/interfaces/cli/commands/skills.js +290 -55
  135. package/src/interfaces/cli/index.js +84 -2
  136. package/src/interfaces/web/dist/assets/index-C0fm31dY.js +618 -0
  137. package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +1 -0
  138. package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +1 -0
  139. package/src/interfaces/web/dist/index.html +2 -2
  140. package/src/interfaces/web/package-lock.json +182 -182
  141. package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
  142. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  143. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  144. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +37 -4
  145. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  146. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  147. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  148. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  149. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  150. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  151. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  152. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  153. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  154. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  155. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  156. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  157. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  158. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +73 -4
  159. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  160. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  161. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  162. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  163. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  164. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  165. package/src/interfaces/web/src/constants/index.ts +1 -1
  166. package/src/interfaces/web/src/hooks/useChat.ts +19 -0
  167. package/src/interfaces/web/src/i18n/en.ts +175 -7
  168. package/src/interfaces/web/src/i18n/es.ts +180 -15
  169. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  170. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  171. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  172. package/src/interfaces/web/src/lib/api.ts +1 -0
  173. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  174. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  175. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  176. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  177. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  178. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  179. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  180. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  181. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  182. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  183. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  184. package/src/interfaces/web/src/types/daemon.ts +15 -0
  185. package/skills/apx-agency-agents/SKILL.md +0 -141
  186. package/skills/apx-agent/SKILL.md +0 -100
  187. package/skills/apx-mcp-builder/SKILL.md +0 -183
  188. package/skills/apx-routine/SKILL.md +0 -140
  189. package/skills/apx-runtime/SKILL.md +0 -117
  190. package/skills/apx-sessions/SKILL.md +0 -281
  191. package/skills/apx-skill-builder/SKILL.md +0 -153
  192. package/skills/apx-telegram/SKILL.md +0 -131
  193. package/skills/apx-voice/SKILL.md +0 -137
  194. package/src/core/agent/prompts/action-discipline.md +0 -24
  195. package/src/core/agent/prompts/super-agent-base.md +0 -42
  196. package/src/host/daemon/transcription.js +0 -538
  197. package/src/host/daemon/whisper-transcribe.py +0 -73
  198. package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
  199. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
  200. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
  201. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  202. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  203. /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
  204. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  205. /package/src/core/{tools → http-tools}/index.js +0 -0
  206. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  207. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  208. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -0,0 +1,317 @@
1
+ // Skill Inspector — middleware that mutates the chat each turn so the agent
2
+ // only ever sees the skills it actually needs.
3
+ //
4
+ // Design goals (test feature, opt-in via config.skills.inspector.enabled):
5
+ // 1. NO static slug dump. The "Available skills" hint block listing every
6
+ // slug in the catalog is suppressed when the inspector is on — the agent
7
+ // reaches skills via this middleware and the existing load_skill tool.
8
+ // 2. Per-turn re-evaluation. The decision is recomputed from the current
9
+ // user prompt; a skill that matched last turn but not this one simply
10
+ // disappears from the next system prompt — natural decay.
11
+ // 3. Two tiers based on confidence:
12
+ // - LOAD (sim ≥ load_threshold): the body is inlined as contextNote.
13
+ // The agent has it right there — no extra tool call.
14
+ // - HINT (sim ≥ hint_threshold): only the slug + one-line description
15
+ // is named, and the agent is told to call load_skill if it actually
16
+ // needs the syntax.
17
+ // Below hint_threshold → nothing.
18
+ // 4. Local-first. Uses the same embeddings chain as cross-channel memory
19
+ // (ollama → gemini → openai → tf). With no provider, the offline TF
20
+ // fallback runs — works on any machine, zero API key, zero GPU.
21
+ // 5. Never block the request. Any embedding failure → empty contextNote.
22
+ //
23
+ // Returns a structured trace so the daemon can emit `skill_inspector` events
24
+ // to the stream (handy for the web debug panel and CLI inspect).
25
+
26
+ import { embedOne, cosineSim } from "#core/memory/embeddings.js";
27
+ import { listSkills, loadSkill } from "./loader.js";
28
+ import { readIndex, backgroundRefreshIfStale } from "./index-store.js";
29
+
30
+ // Defaults — exported so the CLI/web can render them.
31
+ export const INSPECTOR_DEFAULTS = Object.freeze({
32
+ enabled: false, // OPT-IN — this is a test feature.
33
+ load_threshold: 0.55, // sim ≥ this → inline body
34
+ hint_threshold: 0.40, // sim ≥ this → just hint
35
+ margin: 0.04, // top must beat runner-up by this for confident pick
36
+ max_loaded: 1, // how many bodies to inline at once
37
+ max_hints: 2, // how many additional hints to add
38
+ prompt_floor: 8, // skip super-short prompts ("ok", "hola")
39
+ body_char_cap: 6000, // hard cap on inlined skill bodies (token guard)
40
+ });
41
+
42
+ function effectiveConfig(globalConfig) {
43
+ const raw = globalConfig?.skills?.inspector || {};
44
+ return { ...INSPECTOR_DEFAULTS, ...raw };
45
+ }
46
+
47
+ /** Quick public probe so the daemon/api can decide whether to suppress the
48
+ * static hint block in the system prompt. */
49
+ export function isInspectorEnabled(globalConfig) {
50
+ return effectiveConfig(globalConfig).enabled === true;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Scoring
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function scoreAgainstIndex(promptVec, indexItems) {
58
+ const out = [];
59
+ for (const slug of Object.keys(indexItems)) {
60
+ const it = indexItems[slug];
61
+ if (!Array.isArray(it.desc_vector)) continue;
62
+ out.push({
63
+ slug,
64
+ source: it.source,
65
+ desc: it.desc || "",
66
+ file: it.file,
67
+ sim: cosineSim(promptVec, it.desc_vector),
68
+ });
69
+ }
70
+ out.sort((a, b) => b.sim - a.sim);
71
+ return out;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Context block rendering
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function renderInjectedBlock({ loaded, hinted, embedder }) {
79
+ if (loaded.length === 0 && hinted.length === 0) return "";
80
+
81
+ const lines = [
82
+ "# Skill Inspector",
83
+ `Local RAG (${embedder}) matched the user's prompt against your skill catalog.`,
84
+ "The catalog itself is NOT in your system prompt — only what's below is.",
85
+ "If none of these is right, call `list_skills` to browse and `load_skill` to fetch one.",
86
+ "",
87
+ ];
88
+
89
+ if (loaded.length) {
90
+ lines.push("## Loaded for this turn");
91
+ for (const s of loaded) {
92
+ lines.push("");
93
+ lines.push(`### \`${s.slug}\` (sim ${s.sim.toFixed(2)}, source: ${s.source})`);
94
+ lines.push(s.body);
95
+ }
96
+ lines.push("");
97
+ }
98
+
99
+ if (hinted.length) {
100
+ lines.push("## Possibly relevant — load on demand");
101
+ for (const s of hinted) {
102
+ lines.push(`- \`${s.slug}\` — sim ${s.sim.toFixed(2)}. ${s.desc}`);
103
+ }
104
+ lines.push("");
105
+ lines.push("Call `load_skill({slug:\"…\"})` for any of these BEFORE answering if you need its exact syntax.");
106
+ }
107
+
108
+ return lines.join("\n");
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Main entrypoint
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Decide what skill context (if any) to inject for this turn.
117
+ *
118
+ * @param {object} args
119
+ * @param {string} args.prompt the user's current message
120
+ * @param {string=} args.projectPath project root (project skills shadow global)
121
+ * @param {object=} args.globalConfig passed through to embedOne()
122
+ * @param {object=} args.embedOpts optional embedOne overrides
123
+ *
124
+ * @returns {{
125
+ * contextNote: string,
126
+ * trace: {
127
+ * enabled: boolean,
128
+ * reason?: string,
129
+ * embedder?: string,
130
+ * scored?: Array<{slug, sim}>,
131
+ * loaded?: string[],
132
+ * hinted?: string[],
133
+ * }
134
+ * }}
135
+ */
136
+ export async function inspectPromptForSkills({ prompt, projectPath, globalConfig, embedOpts } = {}) {
137
+ const cfg = effectiveConfig(globalConfig);
138
+ if (!cfg.enabled) {
139
+ return { contextNote: "", trace: { enabled: false, reason: "disabled" } };
140
+ }
141
+
142
+ const text = String(prompt || "").trim();
143
+ if (text.length < cfg.prompt_floor) {
144
+ return { contextNote: "", trace: { enabled: true, reason: "prompt_too_short" } };
145
+ }
146
+
147
+ // Self-heal: if a skill was added/edited/removed since the last index, kick a
148
+ // background rebuild. Non-blocking — this turn uses whatever is already on
149
+ // disk; the next turn picks up the fresh vectors. This is what lets a user
150
+ // drop a SKILL.md and have it "just work" without running `apx skills index`.
151
+ try {
152
+ backgroundRefreshIfStale({ projectPath, embedOpts: { ...(embedOpts || {}), globalConfig } });
153
+ } catch { /* best-effort */ }
154
+
155
+ // Pull the persistent index. If it's empty (no `apx skills index` ever ran),
156
+ // we don't fall back to recomputing every skill's vector here — that's the
157
+ // job of the index command and the daemon startup probe. Instead, we emit a
158
+ // trace reason so the operator sees "you forgot to index".
159
+ const idx = readIndex();
160
+ const items = idx.items || {};
161
+ const indexedCount = Object.keys(items).length;
162
+
163
+ // If the on-disk index has nothing yet, try a JIT pass over the live catalog
164
+ // using the in-process cache. Slower than a primed index but means a fresh
165
+ // install still works — the inspector is supposed to "just work" the moment
166
+ // it's flipped on.
167
+ if (indexedCount === 0) {
168
+ return await inspectFromLive({ text, projectPath, cfg, globalConfig, embedOpts });
169
+ }
170
+
171
+ const probe = await embedOne(text, { ...(embedOpts || {}), globalConfig });
172
+ if (!probe || !Array.isArray(probe.vector) || probe.vector.length === 0) {
173
+ return { contextNote: "", trace: { enabled: true, reason: "embed_failed" } };
174
+ }
175
+
176
+ // Embedder mismatch — old index was built with a different provider. Don't
177
+ // mix cosine spaces; the operator needs to re-run `apx skills index`.
178
+ if (idx.embedder && idx.embedder !== probe.embedder) {
179
+ return {
180
+ contextNote: "",
181
+ trace: {
182
+ enabled: true,
183
+ reason: "embedder_mismatch",
184
+ embedder: probe.embedder,
185
+ index_embedder: idx.embedder,
186
+ },
187
+ };
188
+ }
189
+
190
+ const scored = scoreAgainstIndex(probe.vector, items);
191
+ return await pickAndRender({ scored, projectPath, probe, cfg });
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // JIT fallback when the persistent index is empty
196
+ // ---------------------------------------------------------------------------
197
+
198
+ async function inspectFromLive({ text, projectPath, cfg, globalConfig, embedOpts }) {
199
+ const skills = listSkills({ projectPath });
200
+ if (!skills.length) {
201
+ return { contextNote: "", trace: { enabled: true, reason: "no_skills" } };
202
+ }
203
+
204
+ const probe = await embedOne(text, { ...(embedOpts || {}), globalConfig });
205
+ if (!probe || !Array.isArray(probe.vector) || probe.vector.length === 0) {
206
+ return { contextNote: "", trace: { enabled: true, reason: "embed_failed" } };
207
+ }
208
+
209
+ const scored = [];
210
+ for (const s of skills) {
211
+ const desc = (s.description || "").slice(0, 600);
212
+ if (!desc.trim()) continue;
213
+ const out = await embedOne(desc, { ...(embedOpts || {}), globalConfig });
214
+ if (!out || !Array.isArray(out.vector)) continue;
215
+ scored.push({
216
+ slug: s.slug,
217
+ source: s.source,
218
+ desc,
219
+ file: s.file,
220
+ sim: cosineSim(probe.vector, out.vector),
221
+ });
222
+ }
223
+ scored.sort((a, b) => b.sim - a.sim);
224
+ const result = await pickAndRender({ scored, projectPath, probe, cfg });
225
+ return {
226
+ contextNote: result.contextNote,
227
+ trace: { ...result.trace, jit: true },
228
+ };
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Common pick + render
233
+ // ---------------------------------------------------------------------------
234
+
235
+ async function pickAndRender({ scored, projectPath, probe, cfg }) {
236
+ if (scored.length === 0) {
237
+ return { contextNote: "", trace: { enabled: true, reason: "no_candidates", embedder: probe.embedder } };
238
+ }
239
+ const top = scored[0];
240
+ const runner = scored[1] || { sim: 0 };
241
+
242
+ if (top.sim < cfg.hint_threshold) {
243
+ return {
244
+ contextNote: "",
245
+ trace: {
246
+ enabled: true,
247
+ reason: "below_threshold",
248
+ embedder: probe.embedder,
249
+ scored: scored.slice(0, 5).map((s) => ({ slug: s.slug, sim: Number(s.sim.toFixed(3)) })),
250
+ },
251
+ };
252
+ }
253
+
254
+ const loaded = [];
255
+ const hinted = [];
256
+
257
+ // High-confidence top picks → inline body. Bounded by max_loaded and require
258
+ // a margin over the runner-up so a flat tie of weak matches doesn't bloat
259
+ // the prompt.
260
+ if (top.sim >= cfg.load_threshold && top.sim - runner.sim >= cfg.margin) {
261
+ for (let i = 0; i < scored.length && loaded.length < cfg.max_loaded; i++) {
262
+ const cand = scored[i];
263
+ if (cand.sim < cfg.load_threshold) break;
264
+ const body = readBodyCapped(cand.slug, projectPath, cfg.body_char_cap);
265
+ if (!body) continue;
266
+ loaded.push({ ...cand, body });
267
+ }
268
+ }
269
+
270
+ // Mid-confidence remainder → hint.
271
+ for (const cand of scored) {
272
+ if (loaded.some((l) => l.slug === cand.slug)) continue;
273
+ if (hinted.length >= cfg.max_hints) break;
274
+ if (cand.sim < cfg.hint_threshold) break;
275
+ hinted.push(cand);
276
+ }
277
+
278
+ const contextNote = renderInjectedBlock({ loaded, hinted, embedder: probe.embedder });
279
+ return {
280
+ contextNote,
281
+ trace: {
282
+ enabled: true,
283
+ embedder: probe.embedder,
284
+ scored: scored.slice(0, 5).map((s) => ({ slug: s.slug, sim: Number(s.sim.toFixed(3)) })),
285
+ loaded: loaded.map((l) => l.slug),
286
+ hinted: hinted.map((h) => h.slug),
287
+ },
288
+ };
289
+ }
290
+
291
+ function readBodyCapped(slug, projectPath, cap) {
292
+ try {
293
+ const { body } = loadSkill(slug, { projectPath });
294
+ if (!body) return "";
295
+ if (body.length <= cap) return body;
296
+ return body.slice(0, cap) + "\n\n…(skill body truncated — call load_skill for the full text)";
297
+ } catch {
298
+ return "";
299
+ }
300
+ }
301
+
302
+ // Small helper used by the CLI inspect command to print why something fell out.
303
+ export function summarizeTrace(trace) {
304
+ if (!trace) return "(no trace)";
305
+ if (!trace.enabled) return `inspector disabled (${trace.reason || "off"})`;
306
+ if (trace.reason && !trace.loaded && !trace.hinted) {
307
+ return `no skill injected: ${trace.reason}`;
308
+ }
309
+ const parts = [];
310
+ if (trace.loaded?.length) parts.push(`loaded: ${trace.loaded.join(", ")}`);
311
+ if (trace.hinted?.length) parts.push(`hinted: ${trace.hinted.join(", ")}`);
312
+ if (!parts.length) parts.push("nothing injected");
313
+ return parts.join(" · ");
314
+ }
315
+
316
+ // Re-exported for callers that want to introspect.
317
+ export { readIndex };
@@ -10,15 +10,19 @@
10
10
  // 1. <projectPath>/.apc/skills/<slug>.md ← project-scoped (flat)
11
11
  // 1b.<projectPath>/.apc/skills/<slug>/SKILL.md ← project-scoped (dir)
12
12
  // 2. ~/.apx/skills/<slug>/SKILL.md ← user-installed global
13
- // 3. <packageRoot>/skills/<slug>/SKILL.md ← bundled core skills
14
- // (apx, apc-context)
15
- // 4. <packageRoot>/src/core/runtime-skills/<slug>.md
16
- // (claude-code, codex-cli,
17
- // opencode-cli, openrouter)
13
+ // 3. <packageRoot>/src/core/runtime-skills/<slug>/SKILL.md
14
+ // runtime-internal set
15
+ // (rich apx-*, apc-context,
16
+ // claude-code, codex-cli,
17
+ // opencode-cli, openrouter)
18
18
  //
19
- // A slug found in a higher-priority location SHADOWS lower ones. A user can
20
- // override the bundled apc-context by dropping `~/.apx/skills/apc-context/SKILL.md`,
21
- // but the bundled copy stays in the package as a safety net.
19
+ // A slug found in a higher-priority location SHADOWS lower ones. The user can
20
+ // override any runtime skill by dropping `~/.apx/skills/<slug>/SKILL.md`; the
21
+ // in-repo copy stays as a safety net.
22
+ //
23
+ // NOTE: <packageRoot>/skills/<slug>/SKILL.md is intentionally NOT in this chain.
24
+ // That dir holds the engine-side slim set replicated to external CLIs/IDEs
25
+ // (~/.claude/skills/, ~/.codex/skills/, ...) — it's not for the super-agent.
22
26
  //
23
27
  // Note: the bundled `apc-context` skill is REFRESHED from the canonical apc
24
28
  // repo on every npm install / update (see src/interfaces/cli/postinstall.js). APC is a
@@ -28,6 +32,7 @@ import fs from "node:fs";
28
32
  import path from "node:path";
29
33
  import os from "node:os";
30
34
  import { fileURLToPath } from "node:url";
35
+ import { apcSkillsDir } from "#core/apc/paths.js";
31
36
 
32
37
  const __filename = fileURLToPath(import.meta.url);
33
38
  const __dirname = path.dirname(__filename);
@@ -35,8 +40,10 @@ const __dirname = path.dirname(__filename);
35
40
  // → repo root. Used to find the bundled skills/ folder at the repo root.
36
41
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
37
42
 
38
- const RUNTIME_SKILLS_DIR = path.join(PACKAGE_ROOT, "src", "core", "runtime-skills");
39
- const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
43
+ // Runtime-internal skills — full apx-* catalog + CLI docs. Lives outside
44
+ // <packageRoot>/skills/ so external tools that copy "skills/" from the repo
45
+ // don't accidentally pull the rich set or the runtime CLI docs.
46
+ const BUILTIN_SKILLS_DIR = path.join(PACKAGE_ROOT, "src", "core", "runtime-skills");
40
47
  const GLOBAL_DIR = path.join(os.homedir(), ".apx", "skills");
41
48
 
42
49
  // ---------------------------------------------------------------------------
@@ -119,7 +126,7 @@ export function listSkills({ projectPath } = {}) {
119
126
 
120
127
  // priority 1: project-scoped
121
128
  if (projectPath) {
122
- const apcSkills = path.join(projectPath, ".apc", "skills");
129
+ const apcSkills = apcSkillsDir(projectPath);
123
130
  found.push(...scanDirStyle(apcSkills, "project"));
124
131
  found.push(...scanFlatStyle(apcSkills, "project"));
125
132
  }
@@ -127,11 +134,9 @@ export function listSkills({ projectPath } = {}) {
127
134
  // priority 2: user-installed global
128
135
  found.push(...scanDirStyle(GLOBAL_DIR, "global"));
129
136
 
130
- // priority 3: bundled core skills (apx, apc-context)
131
- found.push(...scanDirStyle(BUNDLED_SKILLS_DIR, "builtin"));
132
-
133
- // priority 4: runtime docs (claude-code, codex-cli, opencode-cli, openrouter)
134
- found.push(...scanFlatStyle(RUNTIME_SKILLS_DIR, "builtin"));
137
+ // priority 3: runtime-internal builtin set
138
+ // (rich apx-*, apc-context, plus claude-code, codex-cli, opencode-cli, openrouter)
139
+ found.push(...scanDirStyle(BUILTIN_SKILLS_DIR, "builtin"));
135
140
 
136
141
  // dedupe by slug (first-wins = higher priority shadows lower)
137
142
  const seen = new Set();
@@ -189,7 +194,6 @@ export function loadSkill(slug, { projectPath } = {}) {
189
194
 
190
195
  // Useful for diagnostics
191
196
  export const SKILL_LOCATIONS = {
192
- runtime_skills: RUNTIME_SKILLS_DIR,
193
- bundled: BUNDLED_SKILLS_DIR,
197
+ builtin: BUILTIN_SKILLS_DIR,
194
198
  global: GLOBAL_DIR,
195
199
  };
@@ -0,0 +1,73 @@
1
+ // Accumulate super-agent stream events into the rich ChatPart shape so a
2
+ // persisted assistant turn matches exactly what the UI rendered live.
3
+ // Mirrors the front-end reducer in hooks/useChat.ts (applyStreamEvent) — keep
4
+ // the two in sync if you add a new event type.
5
+ //
6
+ // Pure: no I/O, no globals. Caller drives it event-by-event and finally calls
7
+ // build() to snapshot the resulting parts/notes/model/usage.
8
+ export function makeTurnAccumulator() {
9
+ const parts = [];
10
+ const notes = [];
11
+ let model = null;
12
+ let usage = null;
13
+ const findTool = (id) => parts.find((p) => p.kind === "tool" && p.id === id);
14
+ return {
15
+ apply(ev) {
16
+ switch (ev?.type) {
17
+ case "model_start":
18
+ if (ev.model) model = ev.model;
19
+ break;
20
+ case "model_routed":
21
+ if (ev.model) model = ev.model;
22
+ if (ev.from_fallback) notes.push(`routing fell back → ${ev.model}`);
23
+ break;
24
+ case "engine_failed":
25
+ notes.push(`engine ${ev.model || "?"} failed → ${ev.retry_with || "retry"}`);
26
+ break;
27
+ case "model_retry":
28
+ notes.push(`retry (${ev.reason || "?"})`);
29
+ break;
30
+ case "tools_suppressed":
31
+ notes.push(`tools suppressed: ${(ev.tools || []).join(", ")}`);
32
+ break;
33
+ case "assistant_text":
34
+ if (ev.text) parts.push({ kind: "text", text: ev.text });
35
+ break;
36
+ case "tool_start":
37
+ if (ev.trace)
38
+ parts.push({
39
+ kind: "tool",
40
+ id: ev.trace.id,
41
+ tool: ev.trace.tool,
42
+ args: ev.trace.args,
43
+ status: "running",
44
+ });
45
+ break;
46
+ case "tool_deduped": {
47
+ const t = ev.trace && findTool(ev.trace.id);
48
+ if (t) t.status = "deduped";
49
+ break;
50
+ }
51
+ case "tool_result": {
52
+ const t = ev.trace && findTool(ev.trace.id);
53
+ if (t) {
54
+ t.result = ev.trace.result;
55
+ const errored =
56
+ ev.trace.result && typeof ev.trace.result === "object" && ev.trace.result.error;
57
+ t.status = errored ? "error" : t.status === "deduped" ? "deduped" : "done";
58
+ }
59
+ break;
60
+ }
61
+ case "final":
62
+ usage = ev.result?.usage ?? usage;
63
+ if (!model) model = ev.result?.name || null;
64
+ break;
65
+ default:
66
+ break;
67
+ }
68
+ },
69
+ build() {
70
+ return { parts, notes, model, usage };
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,37 @@
1
+ // Parse the trailing ```suggestions JSON``` block out of an agent reply.
2
+ // Surfaces like the deck/desktop render the JSON as chips; the visible reply
3
+ // should be stripped of the fenced block so the user (and TTS) never sees it.
4
+ //
5
+ // Pure: just regex + JSON.parse. Malformed JSON drops suggestions silently
6
+ // rather than failing the turn — better UX to show the reply without chips
7
+ // than an error.
8
+ const SUGGESTIONS_BLOCK_RE = /\n*```\s*suggestions\s*\n([\s\S]*?)\n?```\s*$/i;
9
+
10
+ const MAX_SUGGESTIONS = 4;
11
+ const MAX_LABEL_LEN = 48;
12
+ const MAX_COMMAND_LEN = 96;
13
+
14
+ export function extractSuggestions(text) {
15
+ if (typeof text !== "string" || !text) {
16
+ return { cleanText: text || "", suggestions: [] };
17
+ }
18
+ const m = SUGGESTIONS_BLOCK_RE.exec(text);
19
+ if (!m) return { cleanText: text, suggestions: [] };
20
+ const cleanText = text.slice(0, m.index).trim();
21
+ let suggestions = [];
22
+ try {
23
+ const parsed = JSON.parse(m[1]);
24
+ if (Array.isArray(parsed)) {
25
+ suggestions = parsed
26
+ .filter((s) => s && typeof s === "object" && typeof s.label === "string")
27
+ .slice(0, MAX_SUGGESTIONS)
28
+ .map((s) => ({
29
+ label: String(s.label).slice(0, MAX_LABEL_LEN),
30
+ ...(typeof s.command === "string" ? { command: s.command.slice(0, MAX_COMMAND_LEN) } : {}),
31
+ }));
32
+ }
33
+ } catch {
34
+ // Malformed JSON — drop silently.
35
+ }
36
+ return { cleanText, suggestions };
37
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from "#core/agent/index.js";
11
11
  import { resolveAgentName } from "#core/identity/index.js";
12
12
  import { memoryBlockFor } from "#core/memory/index.js";
13
+ import { CHANNELS } from "#core/constants/channels.js";
13
14
 
14
15
  export {
15
16
  buildIdentityBlock,
@@ -58,6 +59,10 @@ export async function runSuperAgent({
58
59
  // Null disables human-in-the-loop (tools that need confirmation fail
59
60
  // immediately instead of waiting for user input).
60
61
  requestConfirmation = null,
62
+ // When true, suppress the static "Available skills" slug-dump hint block
63
+ // because a per-turn skill inspector already injected the right context.
64
+ // Set by the daemon's super-agent endpoint when config.skills.inspector is on.
65
+ skipSkillsHint = false,
61
66
  }) {
62
67
  if (!isSuperAgentEnabled(globalConfig)) {
63
68
  throw new Error("super-agent not enabled (set super_agent.enabled and .model in ~/.apx/config.json)");
@@ -74,7 +79,7 @@ export async function runSuperAgent({
74
79
  memoryBlock = await memoryBlockFor(prompt, { config: globalConfig, channel });
75
80
  // "Hilos activos en otros canales" — pure-recency cross-channel awareness.
76
81
  // Skipped for autonomous routines (no human to reference other threads).
77
- if (channel !== "routine") {
82
+ if (channel !== CHANNELS.ROUTINE) {
78
83
  try {
79
84
  activeThreadsBlock = buildActiveThreadsBlock(channel, { config: globalConfig });
80
85
  } catch {
@@ -106,6 +111,7 @@ export async function runSuperAgent({
106
111
  // Compact "tools you can activate" block (names only, no schemas). Empty on
107
112
  // full channels and tool-free callers, where it's omitted from the prompt.
108
113
  lazyToolsBlock: buildLazyToolsBlock(toolSession),
114
+ skipSkillsHint,
109
115
  });
110
116
 
111
117
  const toolSchemas = noTools ? [] : toolSession.initialSchemas;
@@ -0,0 +1,50 @@
1
+ // Shared git spawn helper. Resolves the working directory from the same
2
+ // project/cwd contract every other shell-aware tool uses (resolveProject),
3
+ // then runs `git <args...>` with no shell so paths with spaces are safe.
4
+ import { spawn } from "node:child_process";
5
+ import { resolveProject } from "../helpers.js";
6
+
7
+ const DEFAULT_TIMEOUT_MS = 15_000;
8
+ const MAX_OUTPUT_CHARS = 60_000; // ~15K tokens — generous for diffs, cuts runaways
9
+
10
+ export function runGit(args, { cwd, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
11
+ return new Promise((resolve) => {
12
+ const child = spawn("git", args, { cwd, env: process.env });
13
+ let stdout = "";
14
+ let stderr = "";
15
+ let timedOut = false;
16
+ const timer = setTimeout(() => {
17
+ timedOut = true;
18
+ child.kill("SIGTERM");
19
+ }, timeoutMs);
20
+ child.stdout.on("data", (d) => { stdout += d.toString(); });
21
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
22
+ child.on("error", (err) => {
23
+ clearTimeout(timer);
24
+ resolve({ ok: false, error: err.message });
25
+ });
26
+ child.on("close", (code) => {
27
+ clearTimeout(timer);
28
+ const truncated = stdout.length > MAX_OUTPUT_CHARS;
29
+ if (truncated) stdout = stdout.slice(0, MAX_OUTPUT_CHARS) + "\n…(output truncated)";
30
+ resolve({
31
+ ok: code === 0,
32
+ code,
33
+ stdout,
34
+ stderr: stderr.trim() || null,
35
+ timedOut,
36
+ truncated,
37
+ });
38
+ });
39
+ });
40
+ }
41
+
42
+ /** Resolve the working directory for a git tool from the standard tool args. */
43
+ export function resolveGitCwd(ctx, { project, cwd }) {
44
+ // 1) If `cwd` was passed, use it directly (advanced override).
45
+ if (cwd) return cwd;
46
+ // 2) Otherwise resolve `project` (id, name, or path) → project root.
47
+ const proj = resolveProject(ctx.projects, project);
48
+ if (!proj) throw new Error("git: no project resolved (pass project or cwd)");
49
+ return proj.path;
50
+ }
@@ -2,12 +2,15 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { readConfig, addProject as addProjectInConfig } from "#core/config/index.js";
4
4
  import { initApf } from "#core/apc/scaffold.js";
5
+ import { agentsMdFile, apcProjectFile } from "#core/apc/paths.js";
5
6
  import { projectMeta } from "../helpers.js";
6
7
 
8
+ // Stricter than core/apc/paths.js::isApcProject — `add_project` also requires
9
+ // AGENTS.md at the root, matching the gate that config.addProject() enforces.
7
10
  function isApcProject(absPath) {
8
11
  return (
9
- fs.existsSync(path.join(absPath, "AGENTS.md")) &&
10
- fs.existsSync(path.join(absPath, ".apc", "project.json"))
12
+ fs.existsSync(agentsMdFile(absPath)) &&
13
+ fs.existsSync(apcProjectFile(absPath))
11
14
  );
12
15
  }
13
16
 
@@ -2,13 +2,14 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { loggerFor } from "#core/logging.js";
4
4
  import { readAgents } from "#core/apc/parser.js";
5
+ import { apcProjectFile } from "#core/apc/paths.js";
5
6
  import {
6
7
  closeRuntimeSession,
7
8
  createRuntimeSession,
8
9
  extractRuntimeResult as extractApfResult,
9
10
  } from "#core/stores/runtime-sessions.js";
10
11
  import { buildRuntimeBridgeHint as buildApfHint } from "#core/agent/runtime-bridge.js";
11
- import { detectAll } from "#host/daemon/env-detect.js";
12
+ import { detectAll } from "#core/runtimes/detect.js";
12
13
  import {
13
14
  findEngineSessionById,
14
15
  readEngineSessionContext,
@@ -88,7 +89,7 @@ function resolveProjectForAgent(projects, project, slug) {
88
89
 
89
90
  function projectName(project) {
90
91
  try {
91
- const meta = JSON.parse(fs.readFileSync(path.join(project.path, ".apc", "project.json"), "utf8"));
92
+ const meta = JSON.parse(fs.readFileSync(apcProjectFile(project.path), "utf8"));
92
93
  if (meta.name) return meta.name;
93
94
  } catch {}
94
95
  return path.basename(project.path);