@agentprojectcontext/apx 1.77.3 → 1.79.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/README.md +23 -9
- package/package.json +12 -12
- package/src/core/agent/build-agent-system.js +3 -2
- package/src/core/agent/constants.js +3 -2
- package/src/core/agent/loop/greeting-guard.js +55 -0
- package/src/core/agent/loop/side-effects.js +52 -0
- package/src/core/agent/prompt-builder.js +49 -8
- package/src/core/agent/run-agent.js +43 -53
- package/src/core/agent/security.js +1 -1
- package/src/core/agent/self-memory.js +4 -2
- package/src/core/agent/skills/index-store.js +2 -2
- package/src/core/agent/skills/loader.js +3 -22
- package/src/core/agent/super-agent.js +0 -2
- package/src/core/agent/tool-summary.js +65 -0
- package/src/core/agent/tools/handlers/call-runtime.js +4 -4
- package/src/core/agent/tools/handlers/list-commitments.js +80 -0
- package/src/core/agent/tools/handlers/list-tasks.js +66 -27
- package/src/core/agent/tools/handlers/record-commitment.js +68 -0
- package/src/core/agent/tools/handlers/search-sessions.js +1 -1
- package/src/core/agent/tools/handlers/send-telegram.js +68 -2
- package/src/core/agent/tools/handlers/set-permission-mode.js +11 -4
- package/src/core/agent/tools/names.js +64 -0
- package/src/core/agent/tools/registry.js +9 -0
- package/src/core/agent/tools/tool-call-parser.js +70 -1
- package/src/core/apc/context-copy.js +1 -1
- package/src/core/apc/frontmatter.js +85 -0
- package/src/core/apc/parser.js +9 -21
- package/src/core/apc/paths.js +8 -8
- package/src/core/apc/scaffold.js +5 -4
- package/src/core/channels/telegram/ask-callbacks.js +35 -0
- package/src/core/channels/telegram/config.js +201 -0
- package/src/core/channels/telegram/dispatch.js +3 -0
- package/src/core/channels/telegram/reply.js +30 -5
- package/src/core/config/index.js +25 -183
- package/src/core/config/paths.js +57 -1
- package/src/core/config/redact.js +22 -0
- package/src/core/confirmation/adapters/web.js +1 -1
- package/src/core/daemon/service.js +238 -0
- package/src/core/deck/manifest.js +12 -12
- package/src/core/desktop/autostart.js +3 -2
- package/src/core/desktop/process.js +4 -4
- package/src/core/engines/gemini.js +322 -60
- package/src/core/engines/openai-compatible.js +21 -2
- package/src/core/engines/presets.js +2 -2
- package/src/core/http-tools/browser.js +1 -1
- package/src/core/http-tools/catalog.js +551 -0
- package/src/core/http-tools/fetch.js +1 -1
- package/src/core/http-tools/glob.js +3 -3
- package/src/core/http-tools/grep.js +2 -9
- package/src/core/http-tools/inline-handlers.js +59 -0
- package/src/core/http-tools/registry.js +31 -615
- package/src/core/http-tools/search.js +2 -2
- package/src/core/identity/self.js +6 -7
- package/src/core/identity/telegram.js +3 -3
- package/src/core/logging.js +6 -5
- package/src/core/mcp/sources.js +12 -16
- package/src/core/memory/consolidate.js +225 -0
- package/src/core/memory/indexer.js +2 -5
- package/src/core/nudge/index.js +192 -0
- package/src/core/nudge/policy.js +143 -0
- package/src/core/nudge/store.js +141 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
- package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
- package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
- package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
- package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
- package/src/core/profiles/lifecycle.js +0 -2
- package/src/core/profiles/store.js +2 -7
- package/src/core/routines/runner.js +103 -4
- package/src/core/routines/signals.js +270 -0
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
- package/src/core/runtime-skills/apx-profile/SKILL.md +3 -3
- package/src/core/runtime-skills/apx-project/SKILL.md +1 -1
- package/src/core/runtime-skills/apx-task/SKILL.md +8 -8
- package/src/core/runtime-skills/apx-telegram/SKILL.md +4 -4
- package/src/core/runtime-skills/apx-voice/SKILL.md +4 -4
- package/src/core/runtimes/detect.js +2 -2
- package/src/core/sessions/index.js +800 -0
- package/src/core/stores/code-sessions.js +3 -14
- package/src/core/stores/commitments.js +331 -0
- package/src/core/stores/engine-sessions.js +0 -4
- package/src/core/stores/messages.js +4 -5
- package/src/core/stores/organization.js +9 -11
- package/src/core/stores/routines.js +23 -14
- package/src/core/stores/runtime-callbacks.js +2 -1
- package/src/core/util/json-file.js +128 -0
- package/src/core/util/thinking.js +51 -0
- package/src/core/vars/sources.js +14 -21
- package/src/core/voice/stt-models.js +1 -1
- package/src/core/voice/tts.js +5 -4
- package/src/host/daemon/api/admin-config.js +5 -5
- package/src/host/daemon/api/admin.js +8 -7
- package/src/host/daemon/api/agents.js +16 -16
- package/src/host/daemon/api/artifact-preview.js +8 -8
- package/src/host/daemon/api/artifacts.js +7 -7
- package/src/host/daemon/api/code.js +8 -8
- package/src/host/daemon/api/commitments.js +135 -0
- package/src/host/daemon/api/config.js +6 -6
- package/src/host/daemon/api/confirm.js +2 -2
- package/src/host/daemon/api/connections.js +2 -2
- package/src/host/daemon/api/conversations.js +12 -12
- package/src/host/daemon/api/deck.js +6 -6
- package/src/host/daemon/api/desktop.js +8 -8
- package/src/host/daemon/api/embeddings.js +4 -4
- package/src/host/daemon/api/engines.js +8 -7
- package/src/host/daemon/api/exec.js +5 -4
- package/src/host/daemon/api/files-project.js +6 -6
- package/src/host/daemon/api/health.js +2 -2
- package/src/host/daemon/api/identity.js +3 -3
- package/src/host/daemon/api/inbox.js +2 -2
- package/src/host/daemon/api/integrations.js +23 -19
- package/src/host/daemon/api/mcps.js +16 -12
- package/src/host/daemon/api/messages.js +5 -5
- package/src/host/daemon/api/nudges.js +112 -0
- package/src/host/daemon/api/organization.js +8 -8
- package/src/host/daemon/api/pairing.js +6 -6
- package/src/host/daemon/api/plugins.js +3 -3
- package/src/host/daemon/api/prefix.js +20 -0
- package/src/host/daemon/api/profiles.js +9 -9
- package/src/host/daemon/api/projects.js +5 -5
- package/src/host/daemon/api/routines.js +32 -8
- package/src/host/daemon/api/run.js +2 -2
- package/src/host/daemon/api/runtimes.js +6 -6
- package/src/host/daemon/api/self-memory.js +50 -0
- package/src/host/daemon/api/sessions-search.js +18 -11
- package/src/host/daemon/api/sessions.js +6 -6
- package/src/host/daemon/api/shared.js +59 -13
- package/src/host/daemon/api/skills.js +12 -12
- package/src/host/daemon/api/super-agent.js +4 -4
- package/src/host/daemon/api/tasks.js +11 -11
- package/src/host/daemon/api/telegram.js +61 -24
- package/src/host/daemon/api/tools.js +7 -7
- package/src/host/daemon/api/top-level.js +7 -7
- package/src/host/daemon/api/transcribe.js +6 -6
- package/src/host/daemon/api/tts.js +3 -3
- package/src/host/daemon/api/vars.js +11 -8
- package/src/host/daemon/api/voice.js +9 -7
- package/src/host/daemon/api/web.js +28 -32
- package/src/host/daemon/api.js +83 -48
- package/src/host/daemon/callback-reconciler.js +16 -0
- package/src/host/daemon/db.js +1 -1
- package/src/host/daemon/desktop-ws.js +7 -3
- package/src/host/daemon/index.js +11 -5
- package/src/host/daemon/plugins/desktop/index.js +8 -2
- package/src/host/daemon/plugins/telegram/index.js +7 -2
- package/src/host/daemon/stt-venv.js +20 -81
- package/src/host/daemon/wakeup.js +18 -4
- package/src/interfaces/acp/index.js +3 -3
- package/src/interfaces/acp/session.js +2 -2
- package/src/interfaces/cli/commands/a2a.js +2 -2
- package/src/interfaces/cli/commands/agent.js +3 -3
- package/src/interfaces/cli/commands/artifact.js +13 -13
- package/src/interfaces/cli/commands/chat.js +3 -3
- package/src/interfaces/cli/commands/command.js +2 -2
- package/src/interfaces/cli/commands/commitment.js +154 -0
- package/src/interfaces/cli/commands/config.js +4 -4
- package/src/interfaces/cli/commands/daemon.js +74 -18
- package/src/interfaces/cli/commands/desktop.js +7 -14
- package/src/interfaces/cli/commands/exec.js +2 -2
- package/src/interfaces/cli/commands/log.js +3 -4
- package/src/interfaces/cli/commands/mcp.js +9 -9
- package/src/interfaces/cli/commands/memory.js +75 -2
- package/src/interfaces/cli/commands/messages.js +5 -5
- package/src/interfaces/cli/commands/model.js +0 -18
- package/src/interfaces/cli/commands/nudge.js +130 -0
- package/src/interfaces/cli/commands/obsidian.js +5 -5
- package/src/interfaces/cli/commands/org.js +5 -5
- package/src/interfaces/cli/commands/pair.js +6 -6
- package/src/interfaces/cli/commands/plugins.js +2 -2
- package/src/interfaces/cli/commands/profile.js +10 -10
- package/src/interfaces/cli/commands/project-config.js +6 -6
- package/src/interfaces/cli/commands/project.js +10 -10
- package/src/interfaces/cli/commands/routine.js +8 -9
- package/src/interfaces/cli/commands/runtime.js +2 -2
- package/src/interfaces/cli/commands/search.js +0 -1
- package/src/interfaces/cli/commands/session.js +12 -40
- package/src/interfaces/cli/commands/sessions.js +21 -798
- package/src/interfaces/cli/commands/setup.js +16 -6
- package/src/interfaces/cli/commands/skills.js +1 -2
- package/src/interfaces/cli/commands/status.js +3 -3
- package/src/interfaces/cli/commands/task.js +8 -8
- package/src/interfaces/cli/commands/telegram.js +21 -21
- package/src/interfaces/cli/commands/voice.js +6 -6
- package/src/interfaces/cli/help/index.js +2245 -0
- package/src/interfaces/cli/http.js +9 -4
- package/src/interfaces/cli/index.js +14 -2857
- package/src/interfaces/cli/routes/acp.js +13 -0
- package/src/interfaces/cli/routes/agent.js +27 -0
- package/src/interfaces/cli/routes/artifact.js +28 -0
- package/src/interfaces/cli/routes/chat.js +11 -0
- package/src/interfaces/cli/routes/code.js +11 -0
- package/src/interfaces/cli/routes/command.js +21 -0
- package/src/interfaces/cli/routes/commitment.js +19 -0
- package/src/interfaces/cli/routes/config.js +16 -0
- package/src/interfaces/cli/routes/connections.js +11 -0
- package/src/interfaces/cli/routes/conversations.js +21 -0
- package/src/interfaces/cli/routes/daemon.js +25 -0
- package/src/interfaces/cli/routes/desktop.js +20 -0
- package/src/interfaces/cli/routes/env.js +13 -0
- package/src/interfaces/cli/routes/exec.js +11 -0
- package/src/interfaces/cli/routes/identity.js +11 -0
- package/src/interfaces/cli/routes/index.js +75 -0
- package/src/interfaces/cli/routes/init.js +11 -0
- package/src/interfaces/cli/routes/log.js +20 -0
- package/src/interfaces/cli/routes/mcp.js +22 -0
- package/src/interfaces/cli/routes/memory.js +19 -0
- package/src/interfaces/cli/routes/messages.js +16 -0
- package/src/interfaces/cli/routes/model.js +11 -0
- package/src/interfaces/cli/routes/nudge.js +17 -0
- package/src/interfaces/cli/routes/obsidian.js +17 -0
- package/src/interfaces/cli/routes/org.js +34 -0
- package/src/interfaces/cli/routes/overlay.js +10 -0
- package/src/interfaces/cli/routes/pair.js +17 -0
- package/src/interfaces/cli/routes/panel.js +16 -0
- package/src/interfaces/cli/routes/permission.js +11 -0
- package/src/interfaces/cli/routes/plugins.js +21 -0
- package/src/interfaces/cli/routes/profile.js +27 -0
- package/src/interfaces/cli/routes/project.js +41 -0
- package/src/interfaces/cli/routes/restart.js +15 -0
- package/src/interfaces/cli/routes/routine.js +28 -0
- package/src/interfaces/cli/routes/run.js +11 -0
- package/src/interfaces/cli/routes/search.js +11 -0
- package/src/interfaces/cli/routes/send.js +11 -0
- package/src/interfaces/cli/routes/session.js +26 -0
- package/src/interfaces/cli/routes/sessions.js +15 -0
- package/src/interfaces/cli/routes/setup.js +18 -0
- package/src/interfaces/cli/routes/skills.js +20 -0
- package/src/interfaces/cli/routes/status.js +12 -0
- package/src/interfaces/cli/routes/task.js +26 -0
- package/src/interfaces/cli/routes/telegram.js +39 -0
- package/src/interfaces/cli/routes/update.js +24 -0
- package/src/interfaces/cli/routes/voice.js +17 -0
- package/src/interfaces/desktop/main.js +6 -7
- package/src/interfaces/desktop/renderer.js +0 -5
- package/src/interfaces/mcp-server/index.js +15 -14
- package/src/interfaces/tui/_shims/globals.d.ts +8 -0
- package/src/interfaces/tui/tsconfig.json +4 -1
- package/src/interfaces/web/README.md +6 -6
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +11 -10
- package/src/interfaces/web/src/components/AddProjectDialog.tsx +1 -1
- package/src/interfaces/web/src/components/Section.tsx +18 -3
- package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +1 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +2 -2
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
- package/src/interfaces/web/src/components/chat/SkillPicker.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +1 -1
- package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
- package/src/interfaces/web/src/components/desktop/DesktopStatusCard.tsx +1 -1
- package/src/interfaces/web/src/components/files/FileBrowser.tsx +2 -2
- package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +36 -6
- package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +1 -1
- package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
- package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +14 -4
- package/src/interfaces/web/src/components/routines/shared.ts +14 -5
- package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/SkillsManager.tsx +2 -2
- package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +1 -1
- package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
- package/src/interfaces/web/src/components/ui.tsx +1 -0
- package/src/interfaces/web/src/constants/index.ts +1 -0
- package/src/interfaces/web/src/hooks/useChat.ts +5 -1
- package/src/interfaces/web/src/hooks/useDaemonStatus.ts +1 -1
- package/src/interfaces/web/src/hooks/useDevices.ts +1 -1
- package/src/interfaces/web/src/hooks/useEngines.ts +1 -1
- package/src/interfaces/web/src/hooks/useGlobalConfig.ts +2 -2
- package/src/interfaces/web/src/hooks/useIdentity.ts +1 -1
- package/src/interfaces/web/src/hooks/useInbox.ts +1 -1
- package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
- package/src/interfaces/web/src/hooks/useProfiles.ts +3 -3
- package/src/interfaces/web/src/hooks/useProjects.ts +1 -1
- package/src/interfaces/web/src/hooks/useTelegram.ts +3 -3
- package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +3 -3
- package/src/interfaces/web/src/i18n/en.ts +127 -0
- package/src/interfaces/web/src/i18n/es.ts +127 -0
- package/src/interfaces/web/src/lib/api/admin.ts +11 -11
- package/src/interfaces/web/src/lib/api/agents.ts +14 -14
- package/src/interfaces/web/src/lib/api/artifacts.ts +11 -11
- package/src/interfaces/web/src/lib/api/code.ts +1 -1
- package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
- package/src/interfaces/web/src/lib/api/conversations.ts +8 -8
- package/src/interfaces/web/src/lib/api/deck.ts +3 -3
- package/src/interfaces/web/src/lib/api/desktop.ts +8 -8
- package/src/interfaces/web/src/lib/api/embeddings.ts +3 -3
- package/src/interfaces/web/src/lib/api/engines.ts +3 -3
- package/src/interfaces/web/src/lib/api/filesystem.ts +2 -2
- package/src/interfaces/web/src/lib/api/health.ts +1 -1
- package/src/interfaces/web/src/lib/api/identity.ts +2 -2
- package/src/interfaces/web/src/lib/api/inbox.ts +1 -1
- package/src/interfaces/web/src/lib/api/integrations.ts +8 -8
- package/src/interfaces/web/src/lib/api/mcps.ts +6 -6
- package/src/interfaces/web/src/lib/api/messages.ts +3 -3
- package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
- package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
- package/src/interfaces/web/src/lib/api/organization.ts +7 -7
- package/src/interfaces/web/src/lib/api/profiles.ts +8 -8
- package/src/interfaces/web/src/lib/api/projectFiles.ts +5 -5
- package/src/interfaces/web/src/lib/api/projects.ts +12 -12
- package/src/interfaces/web/src/lib/api/routines.ts +7 -7
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -2
- package/src/interfaces/web/src/lib/api/skills.ts +11 -11
- package/src/interfaces/web/src/lib/api/super_agent.ts +3 -3
- package/src/interfaces/web/src/lib/api/tasks.ts +12 -12
- package/src/interfaces/web/src/lib/api/telegram.ts +14 -14
- package/src/interfaces/web/src/lib/api/tools.ts +1 -1
- package/src/interfaces/web/src/lib/api/vars.ts +4 -4
- package/src/interfaces/web/src/lib/api/voice.ts +6 -6
- package/src/interfaces/web/src/lib/cron.ts +196 -0
- package/src/interfaces/web/src/lib/when.ts +32 -0
- package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +1 -1
- package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +103 -20
- package/src/interfaces/web/src/screens/base/LogsTab.tsx +18 -3
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -8
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +2 -2
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +24 -6
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +3 -3
- package/src/interfaces/web/src/screens/project/Overview.tsx +6 -6
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +15 -13
- package/src/interfaces/web/src/screens/project/StructureTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +1 -1
- package/src/interfaces/web/src/types/daemon.ts +10 -1
- package/src/interfaces/web/vite.config.ts +13 -18
- package/src/interfaces/web/dist/assets/index-BptlbKjU.js +0 -824
- package/src/interfaces/web/dist/assets/index-BptlbKjU.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
- package/src/skills/apc-context/SKILL.md +0 -159
- /package/src/{host/daemon → core}/runtimes/_spawn.js +0 -0
- /package/src/{host/daemon → core}/runtimes/aider.js +0 -0
- /package/src/{host/daemon → core}/runtimes/antigravity.js +0 -0
- /package/src/{host/daemon → core}/runtimes/claude-code.js +0 -0
- /package/src/{host/daemon → core}/runtimes/codex.js +0 -0
- /package/src/{host/daemon → core}/runtimes/cursor-agent.js +0 -0
- /package/src/{host/daemon → core}/runtimes/gemini-cli.js +0 -0
- /package/src/{host/daemon → core}/runtimes/index.js +0 -0
- /package/src/{host/daemon → core}/runtimes/opencode.js +0 -0
- /package/src/{host/daemon → core}/runtimes/qwen-code.js +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// How many unrequested messages APX may send, and when.
|
|
2
|
+
//
|
|
3
|
+
// Three layers, lowest precedence first:
|
|
4
|
+
//
|
|
5
|
+
// 1. CORE DEFAULTS — permissive, and `enabled: false`. Vanilla APX delivers
|
|
6
|
+
// exactly what it delivered before this module existed. Turning a budget
|
|
7
|
+
// on by default would have silently muted push paths people already rely
|
|
8
|
+
// on, which is a regression dressed as a feature.
|
|
9
|
+
// 2. THE ACTIVE PROFILE — a profile that declares `nudge_budget_per_day` or
|
|
10
|
+
// `quiet_hours` is stating a criterion, and switches enforcement ON. This
|
|
11
|
+
// is the split the whole profile subsystem rests on: core owns the
|
|
12
|
+
// capability (a gate, a ledger, a feedback loop), the profile owns the
|
|
13
|
+
// judgement (three a day, quiet after ten).
|
|
14
|
+
// 3. THE USER — `config.nudge` in ~/.apx/config.json, written by the panel or
|
|
15
|
+
// by hand. Always wins. Someone who sets a number should get that number
|
|
16
|
+
// whatever profile they install later.
|
|
17
|
+
import { readActiveProfile, effectiveProfileConfig } from "#core/profiles/store.js";
|
|
18
|
+
|
|
19
|
+
/** Permissive on purpose — see the header. */
|
|
20
|
+
export const DEFAULT_POLICY = Object.freeze({
|
|
21
|
+
enabled: false,
|
|
22
|
+
daily_max: 0, // 0 = no ceiling
|
|
23
|
+
quiet_hours: "", // "" = never quiet
|
|
24
|
+
cooldown_minutes: 0, // between any two nudges
|
|
25
|
+
project_cooldown_minutes: 0, // between two nudges about the SAME project
|
|
26
|
+
kind_cooldown_minutes: 0, // between two nudges of the SAME kind
|
|
27
|
+
critical_bypasses_budget: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Profile config keys this module understands. Deliberately generic names: a
|
|
32
|
+
* chief-of-staff profile and a study-tutor profile both mean the same thing by
|
|
33
|
+
* "how often may you interrupt me". A profile that declares neither key leaves
|
|
34
|
+
* the gate off.
|
|
35
|
+
*/
|
|
36
|
+
const PROFILE_KEYS = Object.freeze({
|
|
37
|
+
nudge_budget_per_day: "daily_max",
|
|
38
|
+
quiet_hours: "quiet_hours",
|
|
39
|
+
nudge_cooldown_minutes: "cooldown_minutes",
|
|
40
|
+
nudge_project_cooldown_minutes: "project_cooldown_minutes",
|
|
41
|
+
nudge_kind_cooldown_minutes: "kind_cooldown_minutes",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the effective policy for the current config.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} config the parsed ~/.apx/config.json
|
|
48
|
+
* @returns {typeof DEFAULT_POLICY & { source: string[] }}
|
|
49
|
+
*/
|
|
50
|
+
export function resolveNudgePolicy(config = {}) {
|
|
51
|
+
const policy = { ...DEFAULT_POLICY };
|
|
52
|
+
const source = ["defaults"];
|
|
53
|
+
|
|
54
|
+
// ── layer 2: the active profile ──────────────────────────────────────────
|
|
55
|
+
let profileConfig = null;
|
|
56
|
+
try {
|
|
57
|
+
const active = readActiveProfile(config);
|
|
58
|
+
if (active) profileConfig = effectiveProfileConfig(active, config);
|
|
59
|
+
} catch {
|
|
60
|
+
// A broken or half-installed profile must not take the gate down with it.
|
|
61
|
+
profileConfig = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (profileConfig) {
|
|
65
|
+
let touched = false;
|
|
66
|
+
for (const [profileKey, policyKey] of Object.entries(PROFILE_KEYS)) {
|
|
67
|
+
const v = profileConfig[profileKey];
|
|
68
|
+
if (v === undefined || v === null || v === "") continue;
|
|
69
|
+
policy[policyKey] = v;
|
|
70
|
+
touched = true;
|
|
71
|
+
}
|
|
72
|
+
// Declaring a budget IS opting in. A profile that says "three a day" and
|
|
73
|
+
// then gets ignored because a separate flag was off is worse than useless.
|
|
74
|
+
if (touched) {
|
|
75
|
+
policy.enabled = true;
|
|
76
|
+
source.push("profile");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── layer 3: the user ────────────────────────────────────────────────────
|
|
81
|
+
const user = config?.nudge;
|
|
82
|
+
if (user && typeof user === "object") {
|
|
83
|
+
let touched = false;
|
|
84
|
+
for (const key of Object.keys(DEFAULT_POLICY)) {
|
|
85
|
+
if (user[key] === undefined || user[key] === null) continue;
|
|
86
|
+
policy[key] = user[key];
|
|
87
|
+
touched = true;
|
|
88
|
+
}
|
|
89
|
+
if (touched) source.push("user");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
policy.daily_max = toNonNegativeInt(policy.daily_max);
|
|
93
|
+
policy.cooldown_minutes = toNonNegativeInt(policy.cooldown_minutes);
|
|
94
|
+
policy.project_cooldown_minutes = toNonNegativeInt(policy.project_cooldown_minutes);
|
|
95
|
+
policy.kind_cooldown_minutes = toNonNegativeInt(policy.kind_cooldown_minutes);
|
|
96
|
+
policy.enabled = policy.enabled === true;
|
|
97
|
+
policy.critical_bypasses_budget = policy.critical_bypasses_budget !== false;
|
|
98
|
+
policy.quiet_hours = typeof policy.quiet_hours === "string" ? policy.quiet_hours.trim() : "";
|
|
99
|
+
|
|
100
|
+
return { ...policy, source };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toNonNegativeInt(v) {
|
|
104
|
+
const n = Number.parseInt(v, 10);
|
|
105
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse a "HH:MM-HH:MM" window into minutes-from-midnight.
|
|
110
|
+
* Returns null when the string is absent or unparseable — an unreadable window
|
|
111
|
+
* must not accidentally mean "always quiet".
|
|
112
|
+
*/
|
|
113
|
+
export function parseQuietHours(spec) {
|
|
114
|
+
const m = /^\s*(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})\s*$/.exec(String(spec || ""));
|
|
115
|
+
if (!m) return null;
|
|
116
|
+
const [h1, m1, h2, m2] = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
117
|
+
if (h1 > 23 || h2 > 23 || m1 > 59 || m2 > 59) return null;
|
|
118
|
+
return { start: h1 * 60 + m1, end: h2 * 60 + m2 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Is `date` inside the quiet window? Handles windows that cross midnight,
|
|
123
|
+
* which is the normal case for sleep ("22:00-07:30").
|
|
124
|
+
*/
|
|
125
|
+
export function isQuietAt(spec, date = new Date()) {
|
|
126
|
+
const w = parseQuietHours(spec);
|
|
127
|
+
if (!w) return false;
|
|
128
|
+
const mins = date.getHours() * 60 + date.getMinutes();
|
|
129
|
+
if (w.start === w.end) return false; // zero-width window
|
|
130
|
+
if (w.start < w.end) return mins >= w.start && mins < w.end;
|
|
131
|
+
return mins >= w.start || mins < w.end; // crosses midnight
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** When does the current quiet window end? Null when not quiet. */
|
|
135
|
+
export function quietEndsAt(spec, date = new Date()) {
|
|
136
|
+
if (!isQuietAt(spec, date)) return null;
|
|
137
|
+
const w = parseQuietHours(spec);
|
|
138
|
+
const end = new Date(date);
|
|
139
|
+
end.setSeconds(0, 0);
|
|
140
|
+
end.setHours(Math.floor(w.end / 60), w.end % 60);
|
|
141
|
+
if (end <= date) end.setDate(end.getDate() + 1);
|
|
142
|
+
return end;
|
|
143
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The nudge ledger — every unrequested message APX sent, and what the user
|
|
2
|
+
// thought of it.
|
|
3
|
+
//
|
|
4
|
+
// Path: ~/.apx/nudges.json
|
|
5
|
+
//
|
|
6
|
+
// Two reasons this is written down rather than kept in memory:
|
|
7
|
+
// - a daily ceiling that resets on every daemon restart is not a ceiling;
|
|
8
|
+
// - without a record of what was sent, "was that useful?" has nothing to
|
|
9
|
+
// attach to, and the feedback loop is what stops the initiative from
|
|
10
|
+
// decaying into noise the user mutes.
|
|
11
|
+
//
|
|
12
|
+
// Bounded on purpose: a file read on every push must not grow without limit.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { NUDGES_PATH } from "#core/config/paths.js";
|
|
16
|
+
|
|
17
|
+
/** Entries kept. Roughly a year at three a day, minutes at pathological rates. */
|
|
18
|
+
const MAX_ENTRIES = 1000;
|
|
19
|
+
|
|
20
|
+
// A FUNCTION, not a shared constant: `{ ...EMPTY }` would copy the object but
|
|
21
|
+
// hand every caller the same `entries` array, so appends would accumulate in
|
|
22
|
+
// memory whatever the file said — a daily ceiling that quietly counted across
|
|
23
|
+
// wipes and restarts.
|
|
24
|
+
const empty = () => ({ version: 1, entries: [] });
|
|
25
|
+
|
|
26
|
+
export function readNudgeLedger() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(NUDGES_PATH, "utf8"));
|
|
29
|
+
if (!raw || typeof raw !== "object") return empty();
|
|
30
|
+
return {
|
|
31
|
+
version: raw.version || 1,
|
|
32
|
+
entries: Array.isArray(raw.entries) ? raw.entries : [],
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
return empty();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeNudgeLedger(ledger) {
|
|
40
|
+
const entries = ledger.entries.slice(-MAX_ENTRIES);
|
|
41
|
+
fs.mkdirSync(path.dirname(NUDGES_PATH), { recursive: true });
|
|
42
|
+
fs.writeFileSync(NUDGES_PATH, JSON.stringify({ ...ledger, entries }, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Append a delivered nudge.
|
|
47
|
+
*
|
|
48
|
+
* Called AFTER the send succeeds: a message that never reached the user must
|
|
49
|
+
* not spend their budget for the day.
|
|
50
|
+
*/
|
|
51
|
+
export function appendNudge(entry) {
|
|
52
|
+
const ledger = readNudgeLedger();
|
|
53
|
+
ledger.entries.push({
|
|
54
|
+
id: entry.id,
|
|
55
|
+
at: entry.at || new Date().toISOString(),
|
|
56
|
+
kind: entry.kind || "unknown",
|
|
57
|
+
project_id: entry.project_id ?? null,
|
|
58
|
+
severity: entry.severity || "normal",
|
|
59
|
+
channel: entry.channel || "telegram",
|
|
60
|
+
chat_id: entry.chat_id != null ? String(entry.chat_id) : null,
|
|
61
|
+
preview: String(entry.preview || "").slice(0, 200),
|
|
62
|
+
bypassed_budget: entry.bypassed_budget === true,
|
|
63
|
+
scheduled: entry.scheduled === true,
|
|
64
|
+
feedback: null,
|
|
65
|
+
});
|
|
66
|
+
writeNudgeLedger(ledger);
|
|
67
|
+
return ledger.entries[ledger.entries.length - 1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Record what the user thought. Returns the updated entry, or null when the id
|
|
72
|
+
* is unknown — a button pressed on a message older than the ledger window.
|
|
73
|
+
*/
|
|
74
|
+
export function setNudgeFeedback(nudgeId, { useful, note = "" } = {}) {
|
|
75
|
+
const ledger = readNudgeLedger();
|
|
76
|
+
const entry = ledger.entries.find((e) => e.id === nudgeId);
|
|
77
|
+
if (!entry) return null;
|
|
78
|
+
entry.feedback = {
|
|
79
|
+
useful: useful === true,
|
|
80
|
+
note: String(note || "").slice(0, 500),
|
|
81
|
+
at: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
writeNudgeLedger(ledger);
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Entries sent on the same local calendar day as `date`. */
|
|
88
|
+
export function nudgesOnDay(date = new Date(), ledger = readNudgeLedger()) {
|
|
89
|
+
const day = localDayKey(date);
|
|
90
|
+
return ledger.entries.filter((e) => localDayKey(new Date(e.at)) === day);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Most recent entry matching a predicate, or null. */
|
|
94
|
+
export function lastNudge(predicate, ledger = readNudgeLedger()) {
|
|
95
|
+
for (let i = ledger.entries.length - 1; i >= 0; i--) {
|
|
96
|
+
if (predicate(ledger.entries[i])) return ledger.entries[i];
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Newest first, optionally filtered. For the CLI and the panel. */
|
|
102
|
+
export function listNudges({ limit = 50, kind = "", project_id = "", with_feedback = null } = {}) {
|
|
103
|
+
let rows = readNudgeLedger().entries.slice().reverse();
|
|
104
|
+
if (kind) rows = rows.filter((e) => e.kind === kind);
|
|
105
|
+
if (project_id) rows = rows.filter((e) => String(e.project_id) === String(project_id));
|
|
106
|
+
if (with_feedback === true) rows = rows.filter((e) => e.feedback);
|
|
107
|
+
if (with_feedback === false) rows = rows.filter((e) => !e.feedback);
|
|
108
|
+
return rows.slice(0, Math.max(1, Number(limit) || 50));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* What the feedback adds up to. The point of collecting it is to be able to
|
|
113
|
+
* answer "which kinds of interruption are worth sending".
|
|
114
|
+
*/
|
|
115
|
+
export function nudgeStats() {
|
|
116
|
+
const entries = readNudgeLedger().entries;
|
|
117
|
+
const byKind = {};
|
|
118
|
+
for (const e of entries) {
|
|
119
|
+
const k = (byKind[e.kind] ||= { kind: e.kind, sent: 0, useful: 0, noise: 0 });
|
|
120
|
+
k.sent += 1;
|
|
121
|
+
if (e.feedback?.useful === true) k.useful += 1;
|
|
122
|
+
if (e.feedback?.useful === false) k.noise += 1;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
total: entries.length,
|
|
126
|
+
today: nudgesOnDay().length,
|
|
127
|
+
rated: entries.filter((e) => e.feedback).length,
|
|
128
|
+
by_kind: Object.values(byKind).sort((a, b) => b.sent - a.sent),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Local calendar day — the user's day, not UTC's. */
|
|
133
|
+
function localDayKey(d) {
|
|
134
|
+
if (Number.isNaN(d?.getTime?.())) return "";
|
|
135
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Test-only: wipe the ledger. */
|
|
139
|
+
export function _resetNudgeLedger() {
|
|
140
|
+
try { fs.rmSync(NUDGES_PATH, { force: true }); } catch { /* nothing to remove */ }
|
|
141
|
+
}
|
|
@@ -20,26 +20,25 @@ through you is anchored to a project registered in APX.
|
|
|
20
20
|
## How you work
|
|
21
21
|
|
|
22
22
|
**Capture by default, ask rarely.** When a task surfaces, record it yourself. Infer the
|
|
23
|
-
project when you can and say in one line what you filed and where. When you
|
|
24
|
-
|
|
23
|
+
project when you can and say in one line what you filed and where. When you cannot tell,
|
|
24
|
+
ask with buttons, never an open question. The system dies the day recording
|
|
25
25
|
something costs more than not recording it.
|
|
26
26
|
|
|
27
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.
|
|
29
|
-
outrank tasks and
|
|
28
|
+
promised to a specific person, with a date; breaking it has a relational cost. Use `record_commitment`, not
|
|
29
|
+
`create_task`. Commitments outrank tasks and are warned about earlier.
|
|
30
30
|
|
|
31
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
|
|
33
|
-
system and it does not come back.
|
|
32
|
+
recorded on X since Y". That is useful. A fabricated summary destroys trust in the
|
|
33
|
+
system and it does not come back. Prefer the explicit gap over the tidy assumption.
|
|
34
34
|
|
|
35
35
|
**Write like someone who knows the subject.** Short sentences. No decorative headers, no
|
|
36
36
|
greeting rituals, no six bullets where two sentences do. What matters goes first. These
|
|
37
37
|
instructions are in English; write to {{owner_name}} in their language.
|
|
38
38
|
|
|
39
|
-
**Delegate domain work.** You coordinate; you do not do it.
|
|
40
|
-
through the development agent. When several specialists report back, you consolidate —
|
|
39
|
+
**Delegate domain work.** You coordinate; you do not do it. Code goes through the development agent. When several specialists report back, you consolidate —
|
|
41
40
|
never pass raw subagent output through.
|
|
42
41
|
|
|
43
42
|
**Permissions.** Recording, reorganising, preparing and researching: go ahead. Writing
|
|
44
43
|
outward — sending, publishing, changing something in a third-party system: confirm first,
|
|
45
|
-
but arrive with it
|
|
44
|
+
but arrive with it prepared so confirming is one button.
|
|
@@ -17,19 +17,49 @@
|
|
|
17
17
|
"type": "integer",
|
|
18
18
|
"default": 3,
|
|
19
19
|
"title": "Unrequested messages per day",
|
|
20
|
-
"description": "The ceiling outside the anchors. Lower is usually better."
|
|
20
|
+
"description": "The ceiling outside the anchors. Lower is usually better. Declaring this switches the interruption budget on."
|
|
21
21
|
},
|
|
22
22
|
"quiet_hours": {
|
|
23
23
|
"type": "string",
|
|
24
24
|
"default": "22:00-07:30",
|
|
25
25
|
"title": "Quiet hours",
|
|
26
|
-
"description": "No unrequested messages in this window."
|
|
26
|
+
"description": "No unrequested messages in this window. Replies are never held back."
|
|
27
|
+
},
|
|
28
|
+
"nudge_project_cooldown_minutes": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"default": 180,
|
|
31
|
+
"title": "Minimum gap per project (minutes)",
|
|
32
|
+
"description": "Three separate thoughts about the same project in one hour is one interruption too many."
|
|
33
|
+
},
|
|
34
|
+
"nudge_kind_cooldown_minutes": {
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"default": 120,
|
|
37
|
+
"title": "Minimum gap per kind (minutes)",
|
|
38
|
+
"description": "Stops one repeating condition from filling the day's budget on its own."
|
|
39
|
+
},
|
|
40
|
+
"watch_schedule": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"default": "0 */2 * * *",
|
|
43
|
+
"title": "Watch runs (cron)",
|
|
44
|
+
"description": "How often to sweep for things worth noticing between the anchors. Cheap: with nothing to report it never wakes the model."
|
|
27
45
|
},
|
|
28
46
|
"stale_project_days": {
|
|
29
47
|
"type": "integer",
|
|
30
48
|
"default": 7,
|
|
31
49
|
"title": "A project is stale after (days)",
|
|
32
|
-
"description": "Days without recorded activity before it is worth mentioning."
|
|
50
|
+
"description": "Days without recorded task or commitment activity before it is worth mentioning."
|
|
51
|
+
},
|
|
52
|
+
"blocked_task_hours": {
|
|
53
|
+
"type": "integer",
|
|
54
|
+
"default": 48,
|
|
55
|
+
"title": "A blocked task is stuck after (hours)",
|
|
56
|
+
"description": "Blocked this morning is a normal working state, not a problem."
|
|
57
|
+
},
|
|
58
|
+
"commitment_lead_hours": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"default": 48,
|
|
61
|
+
"title": "Warn about a promise this early (hours)",
|
|
62
|
+
"description": "How far ahead of its date to raise a commitment, while there is still time to keep it."
|
|
33
63
|
},
|
|
34
64
|
"primary_channel": {
|
|
35
65
|
"type": "string",
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
"name": "day-close",
|
|
3
3
|
"kind": "super_agent",
|
|
4
4
|
"schedule": "{{day_close_schedule}}",
|
|
5
|
-
"permission_mode": "
|
|
5
|
+
"permission_mode": "permiso",
|
|
6
6
|
"enabled_by_default": true,
|
|
7
7
|
"spec": {
|
|
8
|
-
"prompt": "Close the day.
|
|
9
|
-
|
|
8
|
+
"prompt": "Close the day. Review what moved today, what is still stuck, and what carries over to tomorrow, across every registered project. Call list_tasks and list_commitments ONCE each, with no project argument — both search every project in a single call. Do not loop over projects. If a commitment came due today, ask whether it was kept — do not mark it yourself.\n\nSend ONE short message covering those, keeping promises to people separate from ordinary tasks. Note anything that has gone quiet longer than the staleness threshold. If the day was quiet, say that plainly in a line or two.\n\nThen record in your routine memory anything durable you learned about how this person works.\n\nDELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. Send exactly one message, then stop; do not end the turn by asking whether to keep looking.",
|
|
9
|
+
"anchor": true
|
|
10
|
+
},
|
|
11
|
+
"allowed_tools": [
|
|
12
|
+
"send_telegram"
|
|
13
|
+
]
|
|
10
14
|
}
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
"name": "day-open",
|
|
3
3
|
"kind": "super_agent",
|
|
4
4
|
"schedule": "{{day_open_schedule}}",
|
|
5
|
-
"permission_mode": "
|
|
5
|
+
"permission_mode": "permiso",
|
|
6
6
|
"enabled_by_default": true,
|
|
7
7
|
"spec": {
|
|
8
|
-
"prompt": "Open the day.
|
|
9
|
-
|
|
8
|
+
"prompt": "Open the day. Look at what is due today, what is overdue, what is blocked, and what is promised, across every registered project. Call list_tasks and list_commitments ONCE each, with no project argument — both search every project in a single call. Do not loop over projects. Lead with promises: a broken promise costs more than a late task, so it goes first and is named as a promise to a person, never folded into the task list.\n\nThen send ONE short message: promises coming due or already past, what is due today, anything on the calendar, and the single thing that most deserves attention. Not an inventory.\n\nIf there is genuinely nothing pressing, say so in one line — and then do the thing a chief of staff does with an empty morning: ask what should go on today's list, or name the one stale project worth picking up. An empty day is a prompt, not a silence.\n\nDELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. Send exactly one message, then stop; do not end the turn by asking whether to keep looking.",
|
|
9
|
+
"anchor": true
|
|
10
|
+
},
|
|
11
|
+
"allowed_tools": [
|
|
12
|
+
"send_telegram"
|
|
13
|
+
]
|
|
10
14
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "watch",
|
|
3
|
+
"kind": "watch",
|
|
4
|
+
"schedule": "{{watch_schedule}}",
|
|
5
|
+
"permission_mode": "permiso",
|
|
6
|
+
"enabled_by_default": true,
|
|
7
|
+
"spec": {
|
|
8
|
+
"prompt": "You are watching {{owner_name}}'s projects between the anchors. The signals below were detected deterministically — they are facts, already gathered; you do not need to look them up again.\n\nDecide whether any of it is worth interrupting for RIGHT NOW rather than waiting for the next anchor. Staying quiet is the normal outcome and costs you nothing. A message that could have waited until tomorrow morning costs you the next one, because that is the one they will not open.\n\nIf you do speak: send ONE line naming the single most important thing and what you suggest doing about it, with send_telegram. DELIVERY: send it with send_telegram. A routine's own output is only written to the log — if you do not call the tool, {{owner_name}} receives nothing at all. If you decide to stay quiet, just say so in your reply and call nothing."
|
|
9
|
+
},
|
|
10
|
+
"allowed_tools": [
|
|
11
|
+
"send_telegram"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -29,14 +29,9 @@ import {
|
|
|
29
29
|
promptFileFor,
|
|
30
30
|
} from "./paths.js";
|
|
31
31
|
import { schemaDefaults } from "./manifest.js";
|
|
32
|
+
import { readJson } from "#core/util/json-file.js";
|
|
33
|
+
|
|
32
34
|
|
|
33
|
-
function readJson(file) {
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
35
|
|
|
41
36
|
function readDirIds(dir) {
|
|
42
37
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Routine execution — the domain logic any caller can invoke (daemon
|
|
2
|
-
// scheduler, CLI `apx routine run`, HTTP `/projects/:pid/routines/:name/run`,
|
|
2
|
+
// scheduler, CLI `apx routine run`, HTTP `/api/projects/:pid/routines/:name/run`,
|
|
3
3
|
// MCP server, future scripts). The runner orchestrates a 3-phase pipeline:
|
|
4
4
|
// 1. pre_commands (shell)
|
|
5
|
-
// 2. handler (heartbeat / exec_agent / super_agent / telegram / shell)
|
|
5
|
+
// 2. handler (heartbeat / exec_agent / super_agent / telegram / shell / watch)
|
|
6
6
|
// 3. post_commands (shell)
|
|
7
7
|
//
|
|
8
8
|
// `runRoutineNow(ctx, routine)` is the single entry point. Pass a ctx with at
|
|
@@ -16,6 +16,7 @@ import path from "node:path";
|
|
|
16
16
|
import { callEngine } from "#core/engines/index.js";
|
|
17
17
|
import { runSuperAgent } from "#core/agent/super-agent.js";
|
|
18
18
|
import { computeSuppressedTools } from "#core/agent/index.js";
|
|
19
|
+
import { summarizeToolTrace } from "#core/agent/tool-summary.js";
|
|
19
20
|
import { readAgents } from "#core/apc/parser.js";
|
|
20
21
|
import { buildAgentSystem } from "#core/agent/build-agent-system.js";
|
|
21
22
|
import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
|
|
@@ -26,6 +27,8 @@ import {
|
|
|
26
27
|
routineMemoryPath,
|
|
27
28
|
} from "#core/stores/routine-memory.js";
|
|
28
29
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
30
|
+
import { detectSignals, formatSignals, peakSeverity, thresholdsFromConfig } from "#core/routines/signals.js";
|
|
31
|
+
import { readActiveProfile, effectiveProfileConfig } from "#core/profiles/store.js";
|
|
29
32
|
import {
|
|
30
33
|
updateRunState,
|
|
31
34
|
parseSchedule,
|
|
@@ -87,7 +90,7 @@ async function handleExecAgent(ctx, routine) {
|
|
|
87
90
|
return { status: "ok", reply: result.text };
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
async function handleSuperAgent(ctx, routine) {
|
|
93
|
+
async function handleSuperAgent(ctx, routine, extraChannelMeta = {}) {
|
|
91
94
|
const { project, globalConfig, projects, plugins, registries } = ctx;
|
|
92
95
|
const { prompt } = routine.spec;
|
|
93
96
|
if (!prompt) throw new Error("super_agent: spec needs { prompt }");
|
|
@@ -133,6 +136,11 @@ async function handleSuperAgent(ctx, routine) {
|
|
|
133
136
|
} catch { return ""; }
|
|
134
137
|
})(),
|
|
135
138
|
projectPath: project.path,
|
|
139
|
+
// An ANCHOR is a message the user put on the clock themselves. The
|
|
140
|
+
// interruption budget exempts it from the daily ceiling — the profile
|
|
141
|
+
// schema calls that number "the ceiling outside the anchors".
|
|
142
|
+
...(routine.spec?.anchor === true ? { scheduledByUser: true } : {}),
|
|
143
|
+
...extraChannelMeta,
|
|
136
144
|
},
|
|
137
145
|
suppressTools: suppressTools.length > 0 ? suppressTools : null,
|
|
138
146
|
});
|
|
@@ -145,7 +153,13 @@ async function handleSuperAgent(ctx, routine) {
|
|
|
145
153
|
actor_kind: "superagent",
|
|
146
154
|
author: result.name || resolveAgentName(globalConfig),
|
|
147
155
|
body: result.text || "",
|
|
148
|
-
meta: {
|
|
156
|
+
meta: {
|
|
157
|
+
routine: routine.name,
|
|
158
|
+
tool_trace: result.trace,
|
|
159
|
+
// Compact form too: the viewer renders this without parsing the trace.
|
|
160
|
+
...(summarizeToolTrace(result.trace) ? { tool_summary: summarizeToolTrace(result.trace) } : {}),
|
|
161
|
+
usage: result.usage,
|
|
162
|
+
},
|
|
149
163
|
});
|
|
150
164
|
return { status: "ok", reply: result.text, trace: result.trace };
|
|
151
165
|
}
|
|
@@ -186,12 +200,97 @@ function handleShell(ctx, routine) {
|
|
|
186
200
|
});
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
/**
|
|
204
|
+
* watch — deterministic detection first, model second.
|
|
205
|
+
*
|
|
206
|
+
* THE WHOLE POINT: when nothing is happening this returns before any model is
|
|
207
|
+
* touched. That is what makes it affordable to run every few minutes, and it
|
|
208
|
+
* is the difference between a watcher and a cron job with a language model
|
|
209
|
+
* bolted on. `tests/signals.test.js` asserts the no-signal path never reaches
|
|
210
|
+
* runSuperAgent, because this property is invisible until the bill arrives.
|
|
211
|
+
*
|
|
212
|
+
* spec: { prompt, types?, thresholds?, projects? }
|
|
213
|
+
*/
|
|
214
|
+
async function handleWatch(ctx, routine) {
|
|
215
|
+
const { projects, globalConfig } = ctx;
|
|
216
|
+
const { prompt, types, thresholds } = routine.spec || {};
|
|
217
|
+
if (!prompt) throw new Error("watch: spec needs { prompt } for the judgement step");
|
|
218
|
+
|
|
219
|
+
// Thresholds come from the active profile — "stale after 7 days" is a
|
|
220
|
+
// judgement about how someone works, not a constant — with the routine able
|
|
221
|
+
// to override for its own narrower purpose.
|
|
222
|
+
let profileConfig = {};
|
|
223
|
+
try {
|
|
224
|
+
const active = readActiveProfile(globalConfig);
|
|
225
|
+
if (active) profileConfig = effectiveProfileConfig(active, globalConfig) || {};
|
|
226
|
+
} catch { /* a broken profile must not take the watcher down */ }
|
|
227
|
+
|
|
228
|
+
const entries = [];
|
|
229
|
+
for (const entry of projects?.list?.() || []) {
|
|
230
|
+
const p = projects.get(entry.id);
|
|
231
|
+
if (!p?.storagePath) continue;
|
|
232
|
+
entries.push({
|
|
233
|
+
id: entry.id,
|
|
234
|
+
name: entry.name || entry.path,
|
|
235
|
+
path: entry.path,
|
|
236
|
+
storagePath: p.storagePath,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { signals, skipped } = detectSignals(entries, {
|
|
241
|
+
...thresholdsFromConfig(profileConfig),
|
|
242
|
+
...(thresholds && typeof thresholds === "object" ? thresholds : {}),
|
|
243
|
+
types: Array.isArray(types) ? types : [],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!signals.length) {
|
|
247
|
+
// Terminates here. No model, no tokens, nothing sent.
|
|
248
|
+
return {
|
|
249
|
+
status: "ok",
|
|
250
|
+
signals: 0,
|
|
251
|
+
note: "no signals — did not invoke the model",
|
|
252
|
+
...(skipped.length ? { skipped } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const enriched = {
|
|
257
|
+
...routine,
|
|
258
|
+
spec: {
|
|
259
|
+
...routine.spec,
|
|
260
|
+
prompt:
|
|
261
|
+
`${prompt}\n\n` +
|
|
262
|
+
`Signals detected (${signals.length}), highest severity ${peakSeverity(signals)}:\n` +
|
|
263
|
+
`${formatSignals(signals)}\n\n` +
|
|
264
|
+
"These were found deterministically — they are facts, not guesses. Your job is " +
|
|
265
|
+
"judgement: decide whether any of it is worth interrupting for right now, and if " +
|
|
266
|
+
"so say the one thing that matters. Staying quiet is a valid outcome.",
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Severity travels with the run so the interruption budget can be told how
|
|
271
|
+
// bad this is by a DETECTOR rather than by the model's own opinion of its
|
|
272
|
+
// news. A model that can grade its own message as critical has a shouting
|
|
273
|
+
// backdoor; a deterministic overdue commitment does not.
|
|
274
|
+
const result = await handleSuperAgent(ctx, enriched, {
|
|
275
|
+
signalSeverity: peakSeverity(signals),
|
|
276
|
+
signalCount: signals.length,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
...result,
|
|
280
|
+
signals: signals.length,
|
|
281
|
+
peak_severity: peakSeverity(signals),
|
|
282
|
+
signal_types: [...new Set(signals.map((s) => s.type))],
|
|
283
|
+
...(skipped.length ? { skipped } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
189
287
|
const HANDLERS = {
|
|
190
288
|
heartbeat: handleHeartbeat,
|
|
191
289
|
exec_agent: handleExecAgent,
|
|
192
290
|
super_agent: handleSuperAgent,
|
|
193
291
|
telegram: handleTelegram,
|
|
194
292
|
shell: handleShell,
|
|
293
|
+
watch: handleWatch,
|
|
195
294
|
};
|
|
196
295
|
|
|
197
296
|
// --------------------- pipeline: pre/post shell commands --------------------
|