@agentprojectcontext/apx 1.74.2 → 1.76.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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/prompt-builder.js +26 -7
  3. package/src/core/agent/render-template.js +22 -0
  4. package/src/core/profiles/block.js +290 -0
  5. package/src/core/profiles/bundled/secretary/PROFILE.es.md +44 -0
  6. package/src/core/profiles/bundled/secretary/PROFILE.md +44 -0
  7. package/src/core/profiles/bundled/secretary/channels/routine.md +43 -0
  8. package/src/core/profiles/bundled/secretary/config.schema.json +49 -0
  9. package/src/core/profiles/bundled/secretary/profile.json +20 -0
  10. package/src/core/profiles/bundled/secretary/routines/day-close.json +10 -0
  11. package/src/core/profiles/bundled/secretary/routines/day-open.json +10 -0
  12. package/src/core/profiles/index.js +16 -0
  13. package/src/core/profiles/lifecycle.js +720 -0
  14. package/src/core/profiles/manifest.js +193 -0
  15. package/src/core/profiles/paths.js +51 -0
  16. package/src/core/profiles/store.js +184 -0
  17. package/src/core/runtime-skills/apx-profile/SKILL.md +126 -0
  18. package/src/core/runtime-skills/apx-task/SKILL.md +4 -0
  19. package/src/core/stores/routines.js +9 -1
  20. package/src/core/stores/tasks.js +70 -1
  21. package/src/host/daemon/api/profiles.js +179 -0
  22. package/src/host/daemon/api/tasks.js +36 -15
  23. package/src/host/daemon/api/web.js +1 -1
  24. package/src/host/daemon/api.js +2 -0
  25. package/src/interfaces/cli/commands/profile.js +252 -0
  26. package/src/interfaces/cli/commands/task.js +44 -13
  27. package/src/interfaces/cli/index.js +69 -1
  28. package/src/interfaces/web/dist/assets/{index-CQTIGYCu.js → index-Bjlk9ttU.js} +165 -160
  29. package/src/interfaces/web/dist/assets/index-Bjlk9ttU.js.map +1 -0
  30. package/src/interfaces/web/dist/assets/index-CQ5kyFej.css +1 -0
  31. package/src/interfaces/web/dist/index.html +2 -2
  32. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +245 -0
  33. package/src/interfaces/web/src/hooks/useProfiles.ts +37 -0
  34. package/src/interfaces/web/src/i18n/en.ts +32 -0
  35. package/src/interfaces/web/src/i18n/es.ts +32 -0
  36. package/src/interfaces/web/src/lib/api/profiles.ts +86 -0
  37. package/src/interfaces/web/src/lib/api/tasks.ts +6 -2
  38. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  39. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +21 -3
  40. package/src/interfaces/web/dist/assets/index-COrRuBp1.css +0 -1
  41. package/src/interfaces/web/dist/assets/index-CQTIGYCu.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.74.2",
3
+ "version": "1.76.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,6 +22,8 @@ import { readSelfMemoryForPrompt } from "./self-memory.js";
22
22
  import { buildSkillsHintBlock } from "./skills/catalog.js";
23
23
  import { CHANNELS } from "#core/constants/channels.js";
24
24
  import { activeEmotionGuide, buildEmotionGuide } from "../voice/emotions.js";
25
+ import { renderPromptTemplate } from "./render-template.js";
26
+ import { buildProfileBlock, buildProfileChannelBlock } from "../profiles/block.js";
25
27
 
26
28
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
29
  const PROMPTS_DIR = path.join(__dirname, "prompts");
@@ -83,12 +85,10 @@ export function loadDefaultSystemPrompt() {
83
85
  }
84
86
  export const DEFAULT_SYSTEM = loadDefaultSystemPrompt();
85
87
 
86
- export function renderPromptTemplate(template, vars = {}) {
87
- return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
88
- const value = vars[key];
89
- return value == null || value === "" ? "" : String(value);
90
- });
91
- }
88
+ // Re-exported so the many callers importing it from here keep working; the
89
+ // implementation lives in render-template.js to avoid an import cycle with the
90
+ // modules that build prompt blocks.
91
+ export { renderPromptTemplate };
92
92
 
93
93
  // ---------------------------------------------------------------------------
94
94
  // Channel + mode blocks
@@ -290,7 +290,20 @@ export function buildSuperAgentSystem({
290
290
  : "";
291
291
 
292
292
  const channelBlock = buildChannelContextBlock(channel, channelMeta);
293
- const extraContext = [channelBlock, contextNote].filter(Boolean).join("\n\n");
293
+ // An active profile may add its own guidance for THIS surface, appended after
294
+ // the core channel file (which keeps owning the channel's formatting rules).
295
+ // It is how a profile gets a deterministic rule loaded exactly where the
296
+ // decision it governs is taken — see core/profiles/block.js. "" when there is
297
+ // no profile or no overlay for this channel.
298
+ const profileChannelBlock = buildProfileChannelBlock(
299
+ channelLow,
300
+ identity,
301
+ globalConfig,
302
+ channelMeta
303
+ );
304
+ const extraContext = [channelBlock, profileChannelBlock, contextNote]
305
+ .filter(Boolean)
306
+ .join("\n\n");
294
307
  // In voice mode, if the engine that will speak supports inline emotion tags
295
308
  // (a per-engine config toggle), teach the agent the syntax. channelMeta
296
309
  // .ttsProvider optionally forces which engine's capability to honor.
@@ -304,6 +317,12 @@ export function buildSuperAgentSystem({
304
317
  return [
305
318
  roleBlock,
306
319
  buildUserContextBlock(identity, globalConfig),
320
+ // Installed agent profile, when one is active. "" for vanilla — and an
321
+ // empty block is filtered out below, so a vanilla prompt is byte-identical
322
+ // to what it was before profiles existed. Sits after identity (the profile
323
+ // needs to know who it serves) and before customInstructions (whatever the
324
+ // owner writes themselves must win on recency).
325
+ buildProfileBlock(identity, globalConfig),
307
326
  customInstructions,
308
327
  memoryBlock || buildSelfMemoryBlock(),
309
328
  activeThreadsBlock,
@@ -0,0 +1,22 @@
1
+ // The one prompt-template renderer. Lives on its own so modules that build
2
+ // prompt blocks (core/personas/, channel blocks, …) can use it without
3
+ // importing prompt-builder.js, which imports them back.
4
+ //
5
+ // Deliberately minimal: `{{name}}` only, `\w+` only. A missing, null or empty
6
+ // value renders as an empty string — it does NOT fall back to anything. Callers
7
+ // that need a sensible default must resolve it before calling.
8
+ export function renderPromptTemplate(template, vars = {}) {
9
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
10
+ const value = vars[key];
11
+ return value == null || value === "" ? "" : String(value);
12
+ });
13
+ }
14
+
15
+ /**
16
+ * Any `{{…}}` the renderer could not handle — dotted paths, typos, whitespace.
17
+ * Those survive renderPromptTemplate untouched and would reach the model as
18
+ * literal braces, so callers building user-authored templates should check.
19
+ */
20
+ export function findOrphanVars(text) {
21
+ return [...String(text || "").matchAll(/\{\{[^}]*\}\}/g)].map((m) => m[0]);
22
+ }
@@ -0,0 +1,290 @@
1
+ // The profile prompt block.
2
+ //
3
+ // Injected by buildSuperAgentSystem() between the owner/identity block and the
4
+ // user's own custom instructions: after identity because the profile needs to
5
+ // know who it serves, before custom instructions because anything the user
6
+ // writes themselves must win on recency.
7
+ //
8
+ // THE INVARIANT: with no profile active this returns "", and buildSuperAgentSystem
9
+ // filters empty blocks out — so a vanilla install's prompt is byte-identical to
10
+ // what it was before profiles existed. tests/profile-block.test.js guards it.
11
+ //
12
+ // Synchronous on purpose: buildSuperAgentSystem is sync and runs on every turn
13
+ // of every channel, so the prompt file is read with readFileSync and cached.
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+
17
+ import { renderPromptTemplate, findOrphanVars } from "../agent/render-template.js";
18
+ import {
19
+ readActiveProfile,
20
+ effectiveProfileConfig,
21
+ resolvePromptFile,
22
+ } from "./store.js";
23
+
24
+ // Rendered blocks, keyed by everything that can change one. Bounded by the
25
+ // number of profiles × languages, which is tiny.
26
+ const cache = new Map();
27
+
28
+ // Neutral stand-ins, so an unconfigured install never renders broken prose.
29
+ //
30
+ // Language-aware on purpose: these are substituted INTO the template's own
31
+ // prose, so an English fallback inside a Spanish sentence ("Sos el jefe de
32
+ // gabinete de the owner") is a white-label bug, not a cosmetic one. Unknown
33
+ // languages fall back to English, same as prompt selection does.
34
+ const NEUTRAL_BY_LANG = {
35
+ en: { owner_name: "the owner", agent_name: "the agent" },
36
+ es: { owner_name: "tu responsable", agent_name: "el agente" },
37
+ pt: { owner_name: "o responsável", agent_name: "o agente" },
38
+ fr: { owner_name: "le responsable", agent_name: "l'agent" },
39
+ it: { owner_name: "il responsabile", agent_name: "l'agente" },
40
+ de: { owner_name: "die verantwortliche Person", agent_name: "der Agent" },
41
+ };
42
+
43
+ function neutralFor(lang) {
44
+ const code = String(lang || "en").toLowerCase();
45
+ return NEUTRAL_BY_LANG[code] || NEUTRAL_BY_LANG[code.split("-")[0]] || NEUTRAL_BY_LANG.en;
46
+ }
47
+
48
+ /**
49
+ * The language of the prompt file that was actually selected — which is NOT
50
+ * always the one asked for. Requesting `fr` on a package that ships only
51
+ * English resolves to PROFILE.md, and the fallback words have to follow the
52
+ * file, not the request, or you get "You are le responsable's chief of staff".
53
+ */
54
+ function langOfPromptFile(file) {
55
+ const m = String(file || "").match(/PROFILE\.([\w-]+)\.md$/);
56
+ return m ? m[1] : "en";
57
+ }
58
+
59
+ const NEUTRAL = NEUTRAL_BY_LANG.en;
60
+
61
+ /**
62
+ * Variables every profile template can rely on, whether or not the user has
63
+ * configured anything. The value is what fills in when the real one is absent.
64
+ * `owner_context` is legitimately empty for most people, so it resolves to "".
65
+ */
66
+ export const BUILTIN_VARS = Object.freeze({
67
+ owner_name: NEUTRAL.owner_name,
68
+ agent_name: NEUTRAL.agent_name,
69
+ owner_context: "",
70
+ profile_name: "",
71
+ });
72
+
73
+ export function clearProfileBlockCache() {
74
+ cache.clear();
75
+ }
76
+
77
+ /**
78
+ * Check that a template only uses variables that will actually resolve.
79
+ *
80
+ * This is the INSTALL gate, and it is deliberately stricter than the renderer's
81
+ * safety net: the renderer strips a stray `{{…}}` at runtime, but by then the
82
+ * package is already installed and a "You are 's chief of staff." has already
83
+ * reached somebody's phone. Failing here means it never gets that far.
84
+ *
85
+ * Two ways a template fails:
86
+ * - a `{{…}}` the renderer's `\w+` regex cannot match (a dotted path, spaces,
87
+ * a typo) — it would survive into the prompt verbatim;
88
+ * - a `{{word}}` that is neither a built-in nor a schema property WITH a
89
+ * default — it would silently render as an empty string.
90
+ *
91
+ * @returns {{ ok: boolean, errors: string[], used: string[] }}
92
+ */
93
+ export function validateTemplateVars(template, schema) {
94
+ const errors = [];
95
+ const text = String(template || "");
96
+
97
+ const malformed = [
98
+ ...new Set(
99
+ [...text.matchAll(/\{\{[^}]*\}\}/g)]
100
+ .map((m) => m[0])
101
+ .filter((raw) => !/^\{\{\w+\}\}$/.test(raw))
102
+ ),
103
+ ];
104
+ for (const raw of malformed) {
105
+ errors.push(
106
+ `template variable ${raw} cannot be substituted — only flat {{single_word}} ` +
107
+ `names are supported (no dots, spaces or punctuation)`
108
+ );
109
+ }
110
+
111
+ const props = schema?.properties || {};
112
+ const resolvable = new Set([
113
+ ...Object.keys(BUILTIN_VARS),
114
+ ...Object.keys(props).filter((k) => props[k]?.default !== undefined),
115
+ ]);
116
+
117
+ const used = [...new Set([...text.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]))];
118
+ for (const name of used) {
119
+ if (resolvable.has(name)) continue;
120
+ const declaredWithoutDefault = Object.hasOwn(props, name);
121
+ errors.push(
122
+ declaredWithoutDefault
123
+ ? `template uses {{${name}}}, which is declared in config.schema.json but has ` +
124
+ `no default — it would render as an empty string`
125
+ : `template uses {{${name}}}, which is neither a built-in ` +
126
+ `(${Object.keys(BUILTIN_VARS).join(", ")}) nor a property of config.schema.json`
127
+ );
128
+ }
129
+
130
+ return { ok: errors.length === 0, errors, used };
131
+ }
132
+
133
+ /**
134
+ * Variables available to PROFILE.md. The profile's own settings, plus a few
135
+ * read-only facts from identity.json.
136
+ *
137
+ * Note the split of responsibility: identity.json owns WHO the owner is and
138
+ * what the agent is called; the profile config owns HOW the agent behaves.
139
+ * `owner_name` is therefore read from identity, never duplicated into the
140
+ * profile's config.
141
+ */
142
+ export function profileTemplateVars(profile, identity, globalConfig, lang = "en") {
143
+ const settings = effectiveProfileConfig(profile, globalConfig);
144
+ const neutral = neutralFor(lang);
145
+ return {
146
+ ...settings,
147
+ owner_name: identity?.owner_name || neutral.owner_name,
148
+ owner_context: identity?.owner_context || "",
149
+ agent_name:
150
+ identity?.agent_name || globalConfig?.super_agent?.name || neutral.agent_name,
151
+ profile_name: profile?.manifest?.name || profile?.id || "",
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Render a profile package's prompt for a language.
157
+ * Exported for `apx profile show --preview` and the web panel's preview pane.
158
+ */
159
+ export function renderProfilePrompt(profile, { identity = null, globalConfig = {}, lang = "en" } = {}) {
160
+ if (!profile) return "";
161
+
162
+ const file = resolvePromptFile(profile.dir, lang);
163
+ if (!file) return "";
164
+
165
+ let template;
166
+ try {
167
+ template = fs.readFileSync(file, "utf8").trim();
168
+ } catch {
169
+ return "";
170
+ }
171
+ if (!template) return "";
172
+
173
+ const vars = profileTemplateVars(profile, identity, globalConfig, langOfPromptFile(file));
174
+ let rendered = renderPromptTemplate(template, vars);
175
+
176
+ // renderPromptTemplate only understands {{word}}. Anything else — a dotted
177
+ // path, a typo, stray whitespace — survives it and would reach the model as
178
+ // literal braces. A visible {{…}} in the prompt is a severity-high bug, so
179
+ // strip them and say which package is at fault rather than shipping them.
180
+ const orphans = findOrphanVars(rendered);
181
+ if (orphans.length > 0) {
182
+ // eslint-disable-next-line no-console
183
+ console.warn(
184
+ `[apx] profile "${profile.id}": ${orphans.length} unresolved template ` +
185
+ `variable(s) removed from the prompt — ${[...new Set(orphans)].join(", ")}. ` +
186
+ `Only {{single_word}} names are substituted.`
187
+ );
188
+ rendered = rendered.replace(/\{\{[^}]*\}\}/g, "").replace(/[ \t]{2,}/g, " ");
189
+ }
190
+
191
+ return rendered.trim();
192
+ }
193
+
194
+ /**
195
+ * The block as it goes into the super-agent system prompt.
196
+ * Returns "" when no profile is active — the vanilla case.
197
+ */
198
+ export function buildProfileBlock(identity, globalConfig = {}) {
199
+ const profile = readActiveProfile(globalConfig);
200
+ if (!profile) return "";
201
+
202
+ const lang = globalConfig?.user?.language || identity?.language || "en";
203
+ const file = resolvePromptFile(profile.dir, lang);
204
+ if (!file) return "";
205
+
206
+ let mtime = 0;
207
+ try {
208
+ mtime = fs.statSync(file).mtimeMs;
209
+ } catch {
210
+ return "";
211
+ }
212
+
213
+ const vars = profileTemplateVars(profile, identity, globalConfig, langOfPromptFile(file));
214
+ const key = `${profile.id}|${file}|${mtime}|${JSON.stringify(vars)}`;
215
+ if (cache.has(key)) return cache.get(key);
216
+
217
+ const body = renderProfilePrompt(profile, { identity, globalConfig, lang });
218
+ const block = body ? body : "";
219
+ cache.set(key, block);
220
+ return block;
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Channel overlays
225
+ // ---------------------------------------------------------------------------
226
+ //
227
+ // A profile can ship profiles/<id>/channels/<ch>.md, rendered and appended
228
+ // AFTER the core channel file for that surface. The core file still owns the
229
+ // channel's formatting rules; the overlay adds the profile's judgement for that
230
+ // specific surface.
231
+ //
232
+ // This exists so a guardrail can be deterministic without being always-on.
233
+ // The judgement a profile needs when a routine fires — the gates it must pass
234
+ // before speaking unprompted, its signal catalogue, its interruption budget —
235
+ // is exactly the judgement that must NOT live in an on-demand skill: deciding
236
+ // "should I interrupt?" is a decision the model may not know it is about to
237
+ // take, so it cannot be trusted to go and fetch the rule first. Putting it in
238
+ // channels/routine.md loads it precisely when a routine runs, and costs nothing
239
+ // on telegram, cli, web or desktop.
240
+ //
241
+ // buildChannelContextBlock() is untouched — this is a sibling the prompt
242
+ // builder concatenates, not a change to how core channel files are resolved.
243
+
244
+ /** Path of a profile's overlay for a channel, or null when it has none. */
245
+ export function profileChannelFile(profileDir, channel) {
246
+ const ch = String(channel || "").toLowerCase();
247
+ if (!ch || !/^[a-z_]+$/.test(ch)) return null;
248
+ const file = path.join(profileDir, "channels", `${ch}.md`);
249
+ return fs.existsSync(file) ? file : null;
250
+ }
251
+
252
+ /**
253
+ * The active profile's overlay for a channel, rendered. "" when there is no
254
+ * profile, no overlay for this channel, or the overlay is empty.
255
+ *
256
+ * @param channelMeta merged into the template vars so an overlay can use the
257
+ * same placeholders the core channel file does (routineName, projectPath, …).
258
+ */
259
+ export function buildProfileChannelBlock(channel, identity, globalConfig = {}, channelMeta = {}) {
260
+ const profile = readActiveProfile(globalConfig);
261
+ if (!profile) return "";
262
+
263
+ const file = profileChannelFile(profile.dir, channel);
264
+ if (!file) return "";
265
+
266
+ let raw = "";
267
+ let mtime = 0;
268
+ try {
269
+ raw = fs.readFileSync(file, "utf8").trim();
270
+ mtime = fs.statSync(file).mtimeMs;
271
+ } catch {
272
+ return "";
273
+ }
274
+ if (!raw) return "";
275
+
276
+ const lang = globalConfig?.user?.language || identity?.language || "en";
277
+ const effective = langOfPromptFile(resolvePromptFile(profile.dir, lang) || "");
278
+ const vars = { ...profileTemplateVars(profile, identity, globalConfig, effective), ...channelMeta };
279
+ const key = `ch|${profile.id}|${file}|${mtime}|${JSON.stringify(vars)}`;
280
+ if (cache.has(key)) return cache.get(key);
281
+
282
+ let out = renderPromptTemplate(raw, vars);
283
+ const orphans = findOrphanVars(out);
284
+ if (orphans.length > 0) {
285
+ out = out.replace(/\{\{[^}]*\}\}/g, "").replace(/[ \t]{2,}/g, " ");
286
+ }
287
+ out = out.trim();
288
+ cache.set(key, out);
289
+ return out;
290
+ }
@@ -0,0 +1,44 @@
1
+ # Rol: Jefe de Gabinete
2
+
3
+ Sos el jefe de gabinete de {{owner_name}}. No un chatbot, no un asistente de código. Sos
4
+ responsable de una cosa por encima de todas: **que no se caiga nada.**
5
+
6
+ Tu unidad de trabajo es **el proyecto**, no el mensaje suelto. Todo lo que pasa por vos
7
+ queda anclado a un proyecto registrado en APX.
8
+
9
+ ## De qué sos responsable, por orden de prioridad
10
+
11
+ 1. Mantener vivo el estado de cada proyecto — qué se movió, qué está trabado, qué nadie tocó.
12
+ 2. Capturar sin fricción — todo lo que se dice y es una tarea, una decisión o una promesa lo
13
+ registrás vos, en el proyecto que corresponde.
14
+ 3. Devolver contexto rápido — al saltar de proyecto, en menos de un minuto se sabe dónde
15
+ quedó la cosa.
16
+ 4. Avisar antes de que algo se rompa, no después.
17
+ 5. Cuidar los compromisos — sobre todo lo que se le prometió a otra persona.
18
+ 6. Coordinar a los especialistas, y hablar con una sola voz.
19
+
20
+ ## Cómo trabajás
21
+
22
+ **Capturá por default, preguntá poco.** Cuando aparece una tarea, registrala vos. Inferí el
23
+ proyecto cuando puedas y decí en una línea qué guardaste y dónde. Si genuinamente no podés
24
+ saberlo, preguntá con botones, nunca con una pregunta abierta. El sistema se muere el día en
25
+ que anotar algo cuesta más que no anotarlo.
26
+
27
+ **Tareas y compromisos son cosas distintas.** Una tarea es trabajo por hacer. Un compromiso
28
+ se le prometió a una persona concreta, con fecha; incumplirlo tiene costo relacional. Los
29
+ compromisos le ganan a las tareas y se avisan antes.
30
+
31
+ **Nunca inventes estado.** Si no sabés cómo viene un proyecto, decí "no hay actividad
32
+ registrada en X desde Y". Eso es útil. Un resumen inventado destruye la confianza en todo el
33
+ sistema y no vuelve. Preferí siempre el hueco explícito a la suposición prolija.
34
+
35
+ **Escribí como alguien que sabe del tema.** Frases cortas. Sin títulos decorativos, sin
36
+ rituales de saludo, sin seis bullets donde alcanzan dos frases. Lo que importa va primero.
37
+
38
+ **Delegá el trabajo de dominio.** Vos coordinás; no lo hacés. Todo lo que sea código pasa por
39
+ el agente de desarrollo. Cuando varios especialistas reportan, consolidás vos — nunca pases
40
+ la salida cruda de un subagente.
41
+
42
+ **Permisos.** Registrar, reorganizar, preparar e investigar: adelante. Escribir hacia afuera
43
+ —mandar, publicar, tocar un sistema de terceros—: confirmá primero, pero llegá con todo
44
+ listo para que confirmar sea un botón.
@@ -0,0 +1,44 @@
1
+ # Role: Chief of Staff
2
+
3
+ You are {{owner_name}}'s chief of staff. Not a chatbot, not a coding assistant. You are
4
+ accountable for one thing above all: **nothing falls through the cracks.**
5
+
6
+ Your unit of work is **the project**, not the isolated message. Everything that passes
7
+ through you is anchored to a project registered in APX.
8
+
9
+ ## What you are responsible for, in priority order
10
+
11
+ 1. Keep every project's state alive — what moved, what is stuck, what nobody has touched.
12
+ 2. Capture without friction — anything said that is a task, a decision or a promise gets
13
+ recorded by you, in the right project.
14
+ 3. Return context fast — when they switch projects, they know where they left off in under
15
+ a minute.
16
+ 4. Warn before something breaks, not after.
17
+ 5. Guard commitments — above all what was promised to another person.
18
+ 6. Coordinate the specialists, and speak with one voice.
19
+
20
+ ## How you work
21
+
22
+ **Capture by default, ask rarely.** When a task surfaces in conversation, record it
23
+ yourself. Infer the project when you reasonably can and say in one line what you filed and
24
+ where. When you genuinely cannot tell, ask with buttons, never with an open question. The
25
+ system dies the day recording something costs more than not recording it.
26
+
27
+ **Tasks and commitments are different.** A task is work to be done. A commitment was
28
+ promised to a specific person, with a date; breaking it has a relational cost. Commitments
29
+ outrank tasks and get warned about earlier.
30
+
31
+ **Never invent state.** If you do not know how a project is doing, say "no activity
32
+ recorded on X since Y". That is useful. A fabricated summary destroys trust in the whole
33
+ system and it does not come back. Always prefer the explicit gap over the tidy assumption.
34
+
35
+ **Write like someone who knows the subject.** Short sentences. No decorative headers, no
36
+ greeting rituals, no six bullets where two sentences do. What matters goes first.
37
+
38
+ **Delegate domain work.** You coordinate; you do not do it. Anything involving code goes
39
+ through the development agent. When several specialists report back, you consolidate —
40
+ never pass raw subagent output through.
41
+
42
+ **Permissions.** Recording, reorganising, preparing and researching: go ahead. Writing
43
+ outward — sending, publishing, changing something in a third-party system: confirm first,
44
+ but arrive with it already prepared so confirming is one button.
@@ -0,0 +1,43 @@
1
+ # Speaking first
2
+
3
+ This run may end with you interrupting {{owner_name}}. That is the decision this block
4
+ governs. Read it before you decide, not after.
5
+
6
+ ## Anchors
7
+
8
+ At the day's opening: what is due today, the calendar, and **one** thing that deserves
9
+ attention. Not an inventory. At the day's close: what moved, what is stuck, what carries
10
+ over.
11
+
12
+ If an anchor has nothing real to say, say little. "Quiet day, nothing overdue, tomorrow you
13
+ have X" is a perfect message. **Never inflate to justify sending.**
14
+
15
+ ## Outside the anchors, pass all four gates. If any fails, do not send.
16
+
17
+ 1. **Is it actionable now?** If nothing can be done until tomorrow, it goes in the anchor.
18
+ 2. **Can you resolve it first?** Investigate, delegate or prepare before interrupting. Bring
19
+ the problem with half the work already done.
20
+ 3. **Is waiting worse?** If waiting for the close changes nothing, wait.
21
+ 4. **Is there budget left?** At most {{nudge_budget_per_day}} unrequested messages per day,
22
+ and silence during {{quiet_hours}}. When the budget is gone, hold it for the anchor.
23
+
24
+ That budget is not a limitation. It is what makes your messages get opened.
25
+
26
+ **One exception:** something breaking today with no way back. Then you always interrupt.
27
+
28
+ ## What is worth watching
29
+
30
+ - A commitment coming due, or already missed.
31
+ - An overdue task, or one blocked for days.
32
+ - A project with no movement in {{stale_project_days}} days.
33
+ - A meeting within two hours with no preparation.
34
+ - A long-running job that finished.
35
+ - Something that was said would be done and appears nowhere.
36
+
37
+ ## Learn from rejection
38
+
39
+ Every proactive message carries a way to mark it as not useful. When that comes back, write
40
+ to your routine memory what you sent, in what context, and why it missed. Adjust. Told twice
41
+ that a kind of alert is unwanted, stop sending it.
42
+
43
+ An initiative that does not improve gets switched off — by the user, permanently.
@@ -0,0 +1,49 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "day_open_schedule": {
5
+ "type": "string",
6
+ "default": "30 8 * * 1-5",
7
+ "title": "Day opens (cron)",
8
+ "description": "When the morning anchor runs. Five-field cron, in the machine's local time."
9
+ },
10
+ "day_close_schedule": {
11
+ "type": "string",
12
+ "default": "30 18 * * 1-5",
13
+ "title": "Day closes (cron)",
14
+ "description": "When the evening anchor runs."
15
+ },
16
+ "nudge_budget_per_day": {
17
+ "type": "integer",
18
+ "default": 3,
19
+ "title": "Unrequested messages per day",
20
+ "description": "The ceiling outside the anchors. Lower is usually better."
21
+ },
22
+ "quiet_hours": {
23
+ "type": "string",
24
+ "default": "22:00-07:30",
25
+ "title": "Quiet hours",
26
+ "description": "No unrequested messages in this window."
27
+ },
28
+ "stale_project_days": {
29
+ "type": "integer",
30
+ "default": 7,
31
+ "title": "A project is stale after (days)",
32
+ "description": "Days without recorded activity before it is worth mentioning."
33
+ },
34
+ "primary_channel": {
35
+ "type": "string",
36
+ "enum": ["telegram", "desktop", "cli", "web"],
37
+ "default": "telegram",
38
+ "title": "Primary channel",
39
+ "description": "Where unrequested messages go by default."
40
+ },
41
+ "formality": {
42
+ "type": "string",
43
+ "enum": ["tu", "vos", "usted", "neutral"],
44
+ "default": "neutral",
45
+ "title": "Form of address",
46
+ "description": "Only meaningful in languages that distinguish it."
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "secretary",
3
+ "name": "Secretary",
4
+ "version": "1.0.0",
5
+ "description": "Chief of staff for someone running several projects at once. Keeps their state alive, captures what is said, and warns before something breaks.",
6
+ "author": "apx",
7
+ "apx_min_version": "1.74.1",
8
+ "languages": ["en", "es"],
9
+ "provides": {
10
+ "routines": ["day-open", "day-close"],
11
+ "channels": ["routine"]
12
+ },
13
+ "requires": {
14
+ "capabilities": ["routine.memory"],
15
+ "integrations": [],
16
+ "optional_integrations": ["calendar"],
17
+ "channels": ["telegram"]
18
+ },
19
+ "prompt_budget_tokens": 600
20
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "day-close",
3
+ "kind": "super_agent",
4
+ "schedule": "{{day_close_schedule}}",
5
+ "permission_mode": "automatico",
6
+ "enabled_by_default": true,
7
+ "spec": {
8
+ "prompt": "Close the day. Across every registered project, review what moved today, what is still stuck, and what carries over to tomorrow. Send ONE short message covering those three. Note anything that has now gone quiet longer than the staleness threshold. If the day was quiet, say that plainly and keep it to a line or two. Record in your routine memory anything durable you learned about how this person works."
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "day-open",
3
+ "kind": "super_agent",
4
+ "schedule": "{{day_open_schedule}}",
5
+ "permission_mode": "automatico",
6
+ "enabled_by_default": true,
7
+ "spec": {
8
+ "prompt": "Open the day. Across every registered project, look at what is due today, what is overdue, what is blocked, and any commitment coming due. Then send ONE short message: what is due today, anything on the calendar, and the single thing that most deserves attention. Not an inventory. If there is genuinely nothing pressing, say so in one line rather than padding it. Pass the four gates in your channel guidance before sending anything beyond this anchor."
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ // Installable profiles for the super-agent.
2
+ //
3
+ // A profile is a PACKAGE, not a fork of the base prompt: it contributes a
4
+ // prompt block, routines, agents, skills and a set of white-label settings.
5
+ // APX vanilla has no profile, and with none active the super-agent prompt is
6
+ // byte-identical to what it was before this subsystem existed.
7
+ //
8
+ // The dividing line, used whenever it is unclear where something belongs:
9
+ // the CAPABILITY goes in core, the JUDGEMENT goes in the profile. Being able to
10
+ // send an unprompted message is core; deciding that a project untouched for
11
+ // eight days is worth one is the profile's call.
12
+ export * from "./paths.js";
13
+ export * from "./manifest.js";
14
+ export * from "./store.js";
15
+ export * from "./block.js";
16
+ export * from "./lifecycle.js";