@agentprojectcontext/apx 1.34.0 → 1.36.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.
- package/package.json +1 -1
- package/skills/apx/SKILL.md +1 -1
- package/src/core/agent/build-agent-system.js +134 -58
- package/src/core/agent/channels/voice-context.js +4 -4
- package/src/core/agent/prompt-builder.js +176 -123
- package/src/core/agent/prompts/channels/code.md +12 -10
- package/src/core/agent/prompts/channels/desktop.md +5 -32
- package/src/core/agent/prompts/channels/telegram.md +4 -15
- package/src/core/agent/prompts/channels/web_code.md +11 -11
- package/src/core/agent/prompts/core/agent-base.md +24 -0
- package/src/core/agent/prompts/core/project-agent.md +11 -0
- package/src/core/agent/prompts/core/super-agent.md +21 -0
- package/src/core/agent/prompts/discipline/action.md +10 -0
- package/src/core/agent/prompts/discipline/single-segment.md +6 -0
- package/src/core/agent/prompts/discipline/two-segment.md +11 -0
- package/src/core/agent/self-memory.js +43 -1
- package/src/core/agent/skills/index-store.js +307 -0
- package/src/core/agent/skills/index.js +15 -1
- package/src/core/agent/skills/inspector.js +317 -0
- package/src/core/agent/super-agent.js +7 -1
- package/src/core/agent/tools/handlers/_git.js +50 -0
- package/src/core/agent/tools/handlers/git-diff.js +44 -0
- package/src/core/agent/tools/handlers/git-log.js +38 -0
- package/src/core/agent/tools/handlers/git-show.js +34 -0
- package/src/core/agent/tools/handlers/git-status.js +61 -0
- package/src/core/agent/tools/names.js +31 -0
- package/src/core/agent/tools/registry.js +36 -5
- package/src/core/config/index.js +21 -0
- package/src/core/runtime-skills/apx/SKILL.md +27 -39
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +40 -56
- package/src/core/runtime-skills/apx-agent/SKILL.md +27 -30
- package/src/core/runtime-skills/apx-mcp/SKILL.md +31 -36
- package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +37 -51
- package/src/core/runtime-skills/apx-project/SKILL.md +20 -29
- package/src/core/runtime-skills/apx-routine/SKILL.md +34 -47
- package/src/core/runtime-skills/apx-runtime/SKILL.md +32 -50
- package/src/core/runtime-skills/apx-sessions/SKILL.md +96 -145
- package/src/core/runtime-skills/apx-skill-builder/SKILL.md +53 -77
- package/src/core/runtime-skills/apx-task/SKILL.md +18 -21
- package/src/core/runtime-skills/apx-telegram/SKILL.md +43 -54
- package/src/core/runtime-skills/apx-voice/SKILL.md +36 -56
- package/src/core/stores/conversations.js +27 -2
- package/src/host/daemon/api/exec.js +2 -0
- package/src/host/daemon/api/skills.js +140 -6
- package/src/host/daemon/api/super-agent.js +56 -1
- package/src/host/daemon/index.js +17 -0
- package/src/interfaces/cli/branding.js +53 -0
- package/src/interfaces/cli/commands/skills.js +254 -0
- package/src/interfaces/cli/index.js +84 -2
- package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +1 -0
- package/src/interfaces/web/dist/assets/index-DJKA763h.js +628 -0
- package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +0 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +412 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +21 -1
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +69 -1
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
- package/src/interfaces/web/src/hooks/useChat.ts +54 -2
- package/src/interfaces/web/src/i18n/en.ts +12 -1
- package/src/interfaces/web/src/i18n/es.ts +12 -1
- package/src/interfaces/web/src/lib/api/agents.ts +1 -1
- package/src/interfaces/web/src/lib/api/skills.ts +70 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +3 -5
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +12 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +120 -87
- package/src/interfaces/web/src/types/daemon.ts +10 -0
- package/src/core/agent/prompts/action-discipline.md +0 -24
- package/src/core/agent/prompts/super-agent-base.md +0 -42
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +0 -1
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +0 -613
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +0 -1
- package/src/interfaces/web/src/screens/project/ThreadsTab.tsx +0 -100
|
@@ -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,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 !==
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// git_diff — show the diff for a project. Defaults to unstaged working-tree
|
|
2
|
+
// changes; pass staged=true for the index, or `ref` to diff against a commit.
|
|
3
|
+
import { runGit, resolveGitCwd } from "./_git.js";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "git_diff",
|
|
7
|
+
category: "code",
|
|
8
|
+
schema: {
|
|
9
|
+
type: "function",
|
|
10
|
+
function: {
|
|
11
|
+
name: "git_diff",
|
|
12
|
+
description:
|
|
13
|
+
"Show the git diff for a project. Defaults to UNSTAGED changes (working tree vs index). Set staged=true for the index vs HEAD, or pass ref (a commit, branch, or 'HEAD~1') to diff the working tree against that ref. Optional path argument limits the diff to a file/directory. Output is capped — use git_status first to choose what to diff if the change is large.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
project: { type: "string", description: "project id, name, or path" },
|
|
18
|
+
cwd: { type: "string", description: "explicit working directory (overrides project)" },
|
|
19
|
+
staged: { type: "boolean", description: "diff the index vs HEAD instead of the working tree" },
|
|
20
|
+
ref: { type: "string", description: "ref to diff against (commit / branch / HEAD~N)" },
|
|
21
|
+
path: { type: "string", description: "limit the diff to this path (file or directory)" },
|
|
22
|
+
stat: { type: "boolean", description: "summary only (--stat) instead of full diff" },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
makeHandler: (ctx) => async ({ project, cwd, staged, ref, path: subPath, stat } = {}) => {
|
|
28
|
+
const root = resolveGitCwd(ctx, { project, cwd });
|
|
29
|
+
const args = ["diff", "--no-color"];
|
|
30
|
+
if (stat) args.push("--stat");
|
|
31
|
+
if (staged) args.push("--staged");
|
|
32
|
+
if (ref) args.push(ref);
|
|
33
|
+
if (subPath) args.push("--", subPath);
|
|
34
|
+
const r = await runGit(args, { cwd: root });
|
|
35
|
+
if (!r.ok) return { ok: false, error: r.stderr || `git diff exited ${r.code}` };
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
cwd: root,
|
|
39
|
+
diff: r.stdout,
|
|
40
|
+
truncated: r.truncated,
|
|
41
|
+
args: args.slice(1),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// git_log — recent commits for a project. One-line format by default.
|
|
2
|
+
import { runGit, resolveGitCwd } from "./_git.js";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: "git_log",
|
|
6
|
+
category: "code",
|
|
7
|
+
schema: {
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "git_log",
|
|
11
|
+
description:
|
|
12
|
+
"List recent commits for a project. Defaults to the last 20 commits in one-line format. Pass path to limit to a file/directory, ref to start from a different commit/branch, or full=true for the full message + stats.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
project: { type: "string", description: "project id, name, or path" },
|
|
17
|
+
cwd: { type: "string", description: "explicit working directory (overrides project)" },
|
|
18
|
+
limit: { type: "integer", description: "max commits to return (default 20, capped at 200)" },
|
|
19
|
+
ref: { type: "string", description: "ref to start from (branch / commit / HEAD~N); defaults to HEAD" },
|
|
20
|
+
path: { type: "string", description: "limit to commits touching this path" },
|
|
21
|
+
full: { type: "boolean", description: "show full subject + body + stat instead of oneline" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
makeHandler: (ctx) => async ({ project, cwd, limit = 20, ref, path: subPath, full } = {}) => {
|
|
27
|
+
const root = resolveGitCwd(ctx, { project, cwd });
|
|
28
|
+
const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 200));
|
|
29
|
+
const args = ["log", `-n${safeLimit}`, "--no-color"];
|
|
30
|
+
if (full) args.push("--format=fuller", "--stat");
|
|
31
|
+
else args.push("--oneline", "--decorate=short");
|
|
32
|
+
if (ref) args.push(ref);
|
|
33
|
+
if (subPath) args.push("--", subPath);
|
|
34
|
+
const r = await runGit(args, { cwd: root });
|
|
35
|
+
if (!r.ok) return { ok: false, error: r.stderr || `git log exited ${r.code}` };
|
|
36
|
+
return { ok: true, cwd: root, log: r.stdout, truncated: r.truncated };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// git_show — inspect a single commit (or branch tip): subject, author, files
|
|
2
|
+
// changed, full diff.
|
|
3
|
+
import { runGit, resolveGitCwd } from "./_git.js";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "git_show",
|
|
7
|
+
category: "code",
|
|
8
|
+
schema: {
|
|
9
|
+
type: "function",
|
|
10
|
+
function: {
|
|
11
|
+
name: "git_show",
|
|
12
|
+
description:
|
|
13
|
+
"Show a single git commit (or any ref) — message, author, files changed, and full diff. Use `ref` to point at a commit hash, branch, or HEAD~N. Defaults to HEAD.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
project: { type: "string", description: "project id, name, or path" },
|
|
18
|
+
cwd: { type: "string", description: "explicit working directory (overrides project)" },
|
|
19
|
+
ref: { type: "string", description: "ref to show (commit / branch / HEAD~N); defaults to HEAD" },
|
|
20
|
+
stat: { type: "boolean", description: "show stat summary instead of full diff" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
makeHandler: (ctx) => async ({ project, cwd, ref = "HEAD", stat } = {}) => {
|
|
26
|
+
const root = resolveGitCwd(ctx, { project, cwd });
|
|
27
|
+
const args = ["show", "--no-color"];
|
|
28
|
+
if (stat) args.push("--stat");
|
|
29
|
+
args.push(ref);
|
|
30
|
+
const r = await runGit(args, { cwd: root });
|
|
31
|
+
if (!r.ok) return { ok: false, error: r.stderr || `git show exited ${r.code}` };
|
|
32
|
+
return { ok: true, cwd: root, ref, output: r.stdout, truncated: r.truncated };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// git_status — porcelain working-tree status for a project. Returns the raw
|
|
2
|
+
// porcelain text plus a structured list of files so the model doesn't have to
|
|
3
|
+
// re-parse it.
|
|
4
|
+
import { runGit, resolveGitCwd } from "./_git.js";
|
|
5
|
+
|
|
6
|
+
function parsePorcelain(text) {
|
|
7
|
+
const files = [];
|
|
8
|
+
for (const line of String(text).split("\n")) {
|
|
9
|
+
if (!line) continue;
|
|
10
|
+
// Format: XY <path> (optionally `XY <orig> -> <renamed>` for renames)
|
|
11
|
+
const xy = line.slice(0, 2);
|
|
12
|
+
const rest = line.slice(3);
|
|
13
|
+
const renameIdx = rest.indexOf(" -> ");
|
|
14
|
+
let pathStr = rest;
|
|
15
|
+
let origPath = null;
|
|
16
|
+
if (renameIdx >= 0) {
|
|
17
|
+
origPath = rest.slice(0, renameIdx);
|
|
18
|
+
pathStr = rest.slice(renameIdx + 4);
|
|
19
|
+
}
|
|
20
|
+
files.push({
|
|
21
|
+
staged: xy[0] !== " " && xy[0] !== "?",
|
|
22
|
+
unstaged: xy[1] !== " ",
|
|
23
|
+
untracked: xy === "??",
|
|
24
|
+
status: xy.trim(),
|
|
25
|
+
path: pathStr,
|
|
26
|
+
...(origPath ? { original_path: origPath } : {}),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
name: "git_status",
|
|
34
|
+
category: "code",
|
|
35
|
+
schema: {
|
|
36
|
+
type: "function",
|
|
37
|
+
function: {
|
|
38
|
+
name: "git_status",
|
|
39
|
+
description:
|
|
40
|
+
"Show the git working-tree status (staged + unstaged + untracked) for a project. Returns porcelain output plus a parsed list of files. Pass project (id/name/path) OR cwd. Use this BEFORE summarizing changes or BEFORE staging.",
|
|
41
|
+
parameters: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
project: { type: "string", description: "project id, name, or path; falls back to the active project if omitted" },
|
|
45
|
+
cwd: { type: "string", description: "explicit working directory (overrides project)" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
makeHandler: (ctx) => async ({ project, cwd } = {}) => {
|
|
51
|
+
const root = resolveGitCwd(ctx, { project, cwd });
|
|
52
|
+
const r = await runGit(["status", "--porcelain=v1", "-uall"], { cwd: root });
|
|
53
|
+
if (!r.ok) return { ok: false, error: r.stderr || `git status exited ${r.code}` };
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
cwd: root,
|
|
57
|
+
files: parsePorcelain(r.stdout),
|
|
58
|
+
raw: r.stdout,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -56,6 +56,12 @@ export const TOOLS = Object.freeze({
|
|
|
56
56
|
SET_PERMISSION_MODE: "set_permission_mode",
|
|
57
57
|
TRANSCRIBE_AUDIO: "transcribe_audio",
|
|
58
58
|
|
|
59
|
+
// Git — code-channel tools, lazy on chat
|
|
60
|
+
GIT_STATUS: "git_status",
|
|
61
|
+
GIT_DIFF: "git_diff",
|
|
62
|
+
GIT_LOG: "git_log",
|
|
63
|
+
GIT_SHOW: "git_show",
|
|
64
|
+
|
|
59
65
|
// HTTP-bridged registry tools (not native handlers; served via
|
|
60
66
|
// core/tools/registry.js so the regular generic tools work the same way).
|
|
61
67
|
GREP: "grep",
|
|
@@ -101,6 +107,25 @@ export const NATIVE_TOOL_NAMES = new Set([
|
|
|
101
107
|
TOOLS.SEARCH_SESSIONS,
|
|
102
108
|
TOOLS.TRANSCRIBE_AUDIO,
|
|
103
109
|
TOOLS.DISCOVER_TOOLS,
|
|
110
|
+
TOOLS.GIT_STATUS,
|
|
111
|
+
TOOLS.GIT_DIFF,
|
|
112
|
+
TOOLS.GIT_LOG,
|
|
113
|
+
TOOLS.GIT_SHOW,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Tools that belong in code-shaped channels (apx code, web_code) but should
|
|
118
|
+
* stay lazy on chat surfaces (telegram, web_sidebar, deck, desktop) — there's
|
|
119
|
+
* no point loading `git_diff` schemas in a Telegram chat.
|
|
120
|
+
*
|
|
121
|
+
* Listed separately so registry.js can promote them into the base set when
|
|
122
|
+
* the channel is a coding surface, without touching the chat base.
|
|
123
|
+
*/
|
|
124
|
+
export const CODE_CHANNEL_TOOLS = Object.freeze([
|
|
125
|
+
TOOLS.GIT_STATUS,
|
|
126
|
+
TOOLS.GIT_DIFF,
|
|
127
|
+
TOOLS.GIT_LOG,
|
|
128
|
+
TOOLS.GIT_SHOW,
|
|
104
129
|
]);
|
|
105
130
|
|
|
106
131
|
/**
|
|
@@ -128,6 +153,12 @@ export const CODE_PLAN_TOOLS = Object.freeze([
|
|
|
128
153
|
TOOLS.ASK_QUESTIONS,
|
|
129
154
|
TOOLS.FETCH,
|
|
130
155
|
TOOLS.SEARCH,
|
|
156
|
+
// Git tools are read-only on plan mode and let the agent inspect the
|
|
157
|
+
// working state before proposing edits.
|
|
158
|
+
TOOLS.GIT_STATUS,
|
|
159
|
+
TOOLS.GIT_DIFF,
|
|
160
|
+
TOOLS.GIT_LOG,
|
|
161
|
+
TOOLS.GIT_SHOW,
|
|
131
162
|
]);
|
|
132
163
|
|
|
133
164
|
/**
|