@aitne/daemon 0.1.3 → 0.1.4
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/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
- package/dist/adapters/whatsapp-adapter.js +0 -1
- package/dist/adapters/whatsapp-adapter.js.map +1 -1
- package/dist/api/integration-route-gate.d.ts +15 -11
- package/dist/api/integration-route-gate.d.ts.map +1 -1
- package/dist/api/integration-route-gate.js +60 -23
- package/dist/api/integration-route-gate.js.map +1 -1
- package/dist/api/json-body.d.ts +22 -7
- package/dist/api/json-body.d.ts.map +1 -1
- package/dist/api/json-body.js +27 -8
- package/dist/api/json-body.js.map +1 -1
- package/dist/api/routes/agent.d.ts.map +1 -1
- package/dist/api/routes/agent.js +18 -0
- package/dist/api/routes/agent.js.map +1 -1
- package/dist/api/routes/backends.d.ts.map +1 -1
- package/dist/api/routes/backends.js +96 -1
- package/dist/api/routes/backends.js.map +1 -1
- package/dist/api/routes/books.js +1 -1
- package/dist/api/routes/books.js.map +1 -1
- package/dist/api/routes/context.d.ts.map +1 -1
- package/dist/api/routes/context.js +13 -1
- package/dist/api/routes/context.js.map +1 -1
- package/dist/api/routes/dashboard.d.ts.map +1 -1
- package/dist/api/routes/dashboard.js +75 -5
- package/dist/api/routes/dashboard.js.map +1 -1
- package/dist/api/routes/github.d.ts.map +1 -1
- package/dist/api/routes/github.js +38 -5
- package/dist/api/routes/github.js.map +1 -1
- package/dist/api/routes/integrations.d.ts +35 -6
- package/dist/api/routes/integrations.d.ts.map +1 -1
- package/dist/api/routes/integrations.js +191 -16
- package/dist/api/routes/integrations.js.map +1 -1
- package/dist/api/routes/mail.d.ts.map +1 -1
- package/dist/api/routes/mail.js +112 -46
- package/dist/api/routes/mail.js.map +1 -1
- package/dist/api/routes/observations.d.ts.map +1 -1
- package/dist/api/routes/observations.js +161 -8
- package/dist/api/routes/observations.js.map +1 -1
- package/dist/api/routes/setup-migrate.d.ts +9 -1
- package/dist/api/routes/setup-migrate.d.ts.map +1 -1
- package/dist/api/routes/setup-migrate.js +4 -2
- package/dist/api/routes/setup-migrate.js.map +1 -1
- package/dist/api/routes/skills.d.ts.map +1 -1
- package/dist/api/routes/skills.js +39 -1
- package/dist/api/routes/skills.js.map +1 -1
- package/dist/api/routes/voice.d.ts.map +1 -1
- package/dist/api/routes/voice.js +62 -4
- package/dist/api/routes/voice.js.map +1 -1
- package/dist/bootstrap/adapters.d.ts +109 -0
- package/dist/bootstrap/adapters.d.ts.map +1 -0
- package/dist/bootstrap/adapters.js +237 -0
- package/dist/bootstrap/adapters.js.map +1 -0
- package/dist/bootstrap/catchup.d.ts +23 -0
- package/dist/bootstrap/catchup.d.ts.map +1 -0
- package/dist/bootstrap/catchup.js +124 -0
- package/dist/bootstrap/catchup.js.map +1 -0
- package/dist/bootstrap/schedule-helpers.d.ts +18 -0
- package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
- package/dist/bootstrap/schedule-helpers.js +96 -0
- package/dist/bootstrap/schedule-helpers.js.map +1 -0
- package/dist/bootstrap/services.d.ts +60 -0
- package/dist/bootstrap/services.d.ts.map +1 -0
- package/dist/bootstrap/services.js +209 -0
- package/dist/bootstrap/services.js.map +1 -0
- package/dist/core/backends/backend-router.d.ts +23 -0
- package/dist/core/backends/backend-router.d.ts.map +1 -1
- package/dist/core/backends/backend-router.js +48 -3
- package/dist/core/backends/backend-router.js.map +1 -1
- package/dist/core/backends/claude-auth.d.ts +70 -0
- package/dist/core/backends/claude-auth.d.ts.map +1 -0
- package/dist/core/backends/claude-auth.js +198 -0
- package/dist/core/backends/claude-auth.js.map +1 -0
- package/dist/core/backends/claude-code-core.d.ts +47 -119
- package/dist/core/backends/claude-code-core.d.ts.map +1 -1
- package/dist/core/backends/claude-code-core.js +112 -1565
- package/dist/core/backends/claude-code-core.js.map +1 -1
- package/dist/core/backends/claude-delegated.d.ts +86 -0
- package/dist/core/backends/claude-delegated.d.ts.map +1 -0
- package/dist/core/backends/claude-delegated.js +801 -0
- package/dist/core/backends/claude-delegated.js.map +1 -0
- package/dist/core/backends/claude-errors.d.ts +39 -0
- package/dist/core/backends/claude-errors.d.ts.map +1 -0
- package/dist/core/backends/claude-errors.js +71 -0
- package/dist/core/backends/claude-errors.js.map +1 -0
- package/dist/core/backends/claude-probe.d.ts +103 -0
- package/dist/core/backends/claude-probe.d.ts.map +1 -0
- package/dist/core/backends/claude-probe.js +336 -0
- package/dist/core/backends/claude-probe.js.map +1 -0
- package/dist/core/backends/claude-tool-collection.d.ts +135 -0
- package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
- package/dist/core/backends/claude-tool-collection.js +831 -0
- package/dist/core/backends/claude-tool-collection.js.map +1 -0
- package/dist/core/backends/gemini-cli-core.d.ts +21 -0
- package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
- package/dist/core/backends/gemini-cli-core.js +84 -6
- package/dist/core/backends/gemini-cli-core.js.map +1 -1
- package/dist/core/backends/prompt-utils.d.ts +1 -0
- package/dist/core/backends/prompt-utils.d.ts.map +1 -1
- package/dist/core/backends/prompt-utils.js +60 -3
- package/dist/core/backends/prompt-utils.js.map +1 -1
- package/dist/core/context-builder.d.ts +36 -12
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +179 -89
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/dispatcher-date-utils.d.ts +49 -0
- package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
- package/dist/core/dispatcher-date-utils.js +132 -0
- package/dist/core/dispatcher-date-utils.js.map +1 -0
- package/dist/core/dispatcher-error-handling.d.ts +159 -0
- package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
- package/dist/core/dispatcher-error-handling.js +393 -0
- package/dist/core/dispatcher-error-handling.js.map +1 -0
- package/dist/core/dispatcher-hourly-check.d.ts +150 -0
- package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
- package/dist/core/dispatcher-hourly-check.js +665 -0
- package/dist/core/dispatcher-hourly-check.js.map +1 -0
- package/dist/core/dispatcher-message-handler.d.ts +170 -0
- package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
- package/dist/core/dispatcher-message-handler.js +1054 -0
- package/dist/core/dispatcher-message-handler.js.map +1 -0
- package/dist/core/dispatcher-morning-routine.d.ts +169 -0
- package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
- package/dist/core/dispatcher-morning-routine.js +434 -0
- package/dist/core/dispatcher-morning-routine.js.map +1 -0
- package/dist/core/dispatcher-prompt.d.ts +107 -0
- package/dist/core/dispatcher-prompt.d.ts.map +1 -0
- package/dist/core/dispatcher-prompt.js +227 -0
- package/dist/core/dispatcher-prompt.js.map +1 -0
- package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
- package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
- package/dist/core/dispatcher-repository-helpers.js +86 -0
- package/dist/core/dispatcher-repository-helpers.js.map +1 -0
- package/dist/core/dispatcher-result-processor.d.ts +145 -0
- package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
- package/dist/core/dispatcher-result-processor.js +414 -0
- package/dist/core/dispatcher-result-processor.js.map +1 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
- package/dist/core/dispatcher-scheduled-tasks.js +998 -0
- package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
- package/dist/core/dispatcher-types.d.ts +296 -0
- package/dist/core/dispatcher-types.d.ts.map +1 -0
- package/dist/core/dispatcher-types.js +106 -0
- package/dist/core/dispatcher-types.js.map +1 -0
- package/dist/core/dispatcher.d.ts +86 -610
- package/dist/core/dispatcher.d.ts.map +1 -1
- package/dist/core/dispatcher.js +293 -3542
- package/dist/core/dispatcher.js.map +1 -1
- package/dist/core/integration-health.d.ts +18 -10
- package/dist/core/integration-health.d.ts.map +1 -1
- package/dist/core/integration-health.js +31 -1
- package/dist/core/integration-health.js.map +1 -1
- package/dist/core/integration-lifecycle.d.ts +65 -0
- package/dist/core/integration-lifecycle.d.ts.map +1 -1
- package/dist/core/integration-lifecycle.js +167 -16
- package/dist/core/integration-lifecycle.js.map +1 -1
- package/dist/core/integration-main-backend.d.ts +40 -0
- package/dist/core/integration-main-backend.d.ts.map +1 -1
- package/dist/core/integration-main-backend.js +89 -2
- package/dist/core/integration-main-backend.js.map +1 -1
- package/dist/core/management-md.d.ts +51 -17
- package/dist/core/management-md.d.ts.map +1 -1
- package/dist/core/management-md.js +233 -56
- package/dist/core/management-md.js.map +1 -1
- package/dist/core/output-language-policy.d.ts +74 -0
- package/dist/core/output-language-policy.d.ts.map +1 -0
- package/dist/core/output-language-policy.js +194 -0
- package/dist/core/output-language-policy.js.map +1 -0
- package/dist/core/prompts.d.ts +1 -0
- package/dist/core/prompts.d.ts.map +1 -1
- package/dist/core/prompts.js +121 -3
- package/dist/core/prompts.js.map +1 -1
- package/dist/core/repository-management-docs.d.ts +24 -0
- package/dist/core/repository-management-docs.d.ts.map +1 -1
- package/dist/core/repository-management-docs.js +210 -26
- package/dist/core/repository-management-docs.js.map +1 -1
- package/dist/core/routine-acquisition-plan.d.ts +131 -0
- package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
- package/dist/core/routine-acquisition-plan.js +268 -0
- package/dist/core/routine-acquisition-plan.js.map +1 -0
- package/dist/core/routine-fetch-window-runner.d.ts +201 -0
- package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-runner.js +661 -0
- package/dist/core/routine-fetch-window-runner.js.map +1 -0
- package/dist/core/routine-windows.d.ts +156 -0
- package/dist/core/routine-windows.d.ts.map +1 -0
- package/dist/core/routine-windows.js +330 -0
- package/dist/core/routine-windows.js.map +1 -0
- package/dist/core/skills-compiler.d.ts +11 -0
- package/dist/core/skills-compiler.d.ts.map +1 -1
- package/dist/core/skills-compiler.js +102 -13
- package/dist/core/skills-compiler.js.map +1 -1
- package/dist/core/skills-manifest.d.ts.map +1 -1
- package/dist/core/skills-manifest.js +26 -0
- package/dist/core/skills-manifest.js.map +1 -1
- package/dist/core/system-reset.d.ts.map +1 -1
- package/dist/core/system-reset.js +25 -2
- package/dist/core/system-reset.js.map +1 -1
- package/dist/db/observations.d.ts +45 -2
- package/dist/db/observations.d.ts.map +1 -1
- package/dist/db/observations.js +112 -14
- package/dist/db/observations.js.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +13 -25
- package/dist/db/schema.js.map +1 -1
- package/dist/index.js +83 -610
- package/dist/index.js.map +1 -1
- package/dist/observers/delegated-sync-worker.d.ts +45 -2
- package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
- package/dist/observers/delegated-sync-worker.js +71 -21
- package/dist/observers/delegated-sync-worker.js.map +1 -1
- package/dist/observers/mail-poller.d.ts +12 -5
- package/dist/observers/mail-poller.d.ts.map +1 -1
- package/dist/observers/mail-poller.js +36 -14
- package/dist/observers/mail-poller.js.map +1 -1
- package/dist/observers/manager.d.ts +37 -5
- package/dist/observers/manager.d.ts.map +1 -1
- package/dist/observers/manager.js +28 -10
- package/dist/observers/manager.js.map +1 -1
- package/dist/services/delegated-backend-invoker.d.ts +1 -51
- package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
- package/dist/services/delegated-backend-invoker.js +41 -480
- package/dist/services/delegated-backend-invoker.js.map +1 -1
- package/dist/services/delegated-invoker-audit.d.ts +94 -0
- package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
- package/dist/services/delegated-invoker-audit.js +238 -0
- package/dist/services/delegated-invoker-audit.js.map +1 -0
- package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
- package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
- package/dist/services/delegated-invoker-cache-hits.js +104 -0
- package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
- package/dist/services/delegated-invoker-janitors.d.ts +28 -0
- package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
- package/dist/services/delegated-invoker-janitors.js +104 -0
- package/dist/services/delegated-invoker-janitors.js.map +1 -0
- package/dist/services/delegated-invoker-utils.d.ts +42 -0
- package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
- package/dist/services/delegated-invoker-utils.js +100 -0
- package/dist/services/delegated-invoker-utils.js.map +1 -0
- package/dist/services/delegated-task-runtime.d.ts +1 -1
- package/dist/services/delegated-task-runtime.js +1 -1
- package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
- package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
- package/dist/services/integrations/snapshot-partitions.js +12 -0
- package/dist/services/integrations/snapshot-partitions.js.map +1 -1
- package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
- package/dist/services/voice/transcriber-impl.js +7 -8
- package/dist/services/voice/transcriber-impl.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ScheduledTaskRunner` — owns every non-message dispatch path that
|
|
3
|
+
* routes through the dispatcher's main `dispatch` switch:
|
|
4
|
+
* - `scheduled.task` (generic + repository run + git project doc +
|
|
5
|
+
* today_refresh + morning-routine retry);
|
|
6
|
+
* - `routine.morning_routine` retries (the wake-task fast path);
|
|
7
|
+
* - `routine.roadmap_refresh` (with the cross-request roadmap write
|
|
8
|
+
* lock + skip-on-conflict semantics);
|
|
9
|
+
* - `routine.skill_curation` (P22 §3.4 — optimizer workdir
|
|
10
|
+
* materialization + hard-clamped tool envelope);
|
|
11
|
+
* - the catch-all `executeDefault` for every routine that doesn't
|
|
12
|
+
* have its own dedicated runner method (today_refresh,
|
|
13
|
+
* evening_review, weekly_review, …).
|
|
14
|
+
*
|
|
15
|
+
* Plus the today.md utilities the morning-routine path consults
|
|
16
|
+
* through callbacks: `rotateDayFiles`, `diagnoseTodayMdState`,
|
|
17
|
+
* `hasCurrentAgentDayTodayMd`.
|
|
18
|
+
*
|
|
19
|
+
* Extracted from `core/dispatcher.ts` as part of phase D-2 of
|
|
20
|
+
* `docs/design/appendices/file-split-plan.md`. Pattern B (stateful
|
|
21
|
+
* coordinator): the runner has no mutable state of its own; it
|
|
22
|
+
* borrows lazy accessors for the dispatcher's optimizer hooks
|
|
23
|
+
* (set by `setSkillCurationHooks` after construction) and bridges
|
|
24
|
+
* back into `MorningRoutineRunner.executeMorningRoutine` when a
|
|
25
|
+
* morning-routine retry wake-task fires.
|
|
26
|
+
*
|
|
27
|
+
* Dispatcher entry points served:
|
|
28
|
+
* - `EventDispatcher.dispatch` switches on event type; each non-
|
|
29
|
+
* message branch now calls into a `runner.X()` method here
|
|
30
|
+
* (`executeScheduledTask`, `executeRoadmapRefresh`,
|
|
31
|
+
* `executeSkillCurationRoutine`, `executeDefault`);
|
|
32
|
+
* - `MorningRoutineRunner` uses `rotateDayFiles` /
|
|
33
|
+
* `diagnoseTodayMdState` via the dep callbacks the dispatcher
|
|
34
|
+
* wires at construction time.
|
|
35
|
+
*
|
|
36
|
+
* Shared-state references held:
|
|
37
|
+
* - `getMaterializeOptimizerWorkdir` / `getTeardownOptimizerWorkdir`
|
|
38
|
+
* — lazy accessors; the optimizer hooks are wired by
|
|
39
|
+
* `setSkillCurationHooks` after the dispatcher is constructed.
|
|
40
|
+
* Reading through the closures means the runner sees the current
|
|
41
|
+
* value at call time.
|
|
42
|
+
* - `roadmapWriteLock` — read-only reference to the dispatcher's
|
|
43
|
+
* write-lock manager. The runner calls `acquire` / `release` but
|
|
44
|
+
* does not own the manager's lifecycle.
|
|
45
|
+
*/
|
|
46
|
+
import { EventPriority, createEvent, formatSqliteDatetime, getAgentDayDateStr, isBackendId, isKnowledgeImportEvent, isRoutineEvent, resolveProcessKey, } from "@aitne/shared";
|
|
47
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
import { randomUUID } from "node:crypto";
|
|
50
|
+
import { CONTEXT_RELATIVE_PATHS } from "./context-paths.js";
|
|
51
|
+
import { getContextDir } from "../config.js";
|
|
52
|
+
import { cleanupSessionWorkdir, ensureBackendMaterialized, } from "./workdir.js";
|
|
53
|
+
import { readIntegrations } from "../db/integrations-store.js";
|
|
54
|
+
import { getRepository, getRepositoryByLocalPath, recordManagementInitDone, recordManagementScan, } from "../db/repositories-store.js";
|
|
55
|
+
import { runRepositoryManagementInit, runRepositoryManagementScan, } from "./repository-management-docs.js";
|
|
56
|
+
import { routineWindowKeyFromEvent } from "./routine-fetch-window-runner.js";
|
|
57
|
+
import { routineHasWindows } from "./routine-windows.js";
|
|
58
|
+
import { parseGithubRepoSlug, normalizeRepositoryClassification, normalizeRepositoryCategory, parseRepositoryRunTaskContext, repositoryRunInstructionFilename, safeRepositoryRunDirName, } from "./dispatcher-repository-helpers.js";
|
|
59
|
+
import { createLogger } from "../logging.js";
|
|
60
|
+
const logger = createLogger("dispatcher-scheduled-tasks");
|
|
61
|
+
/**
|
|
62
|
+
* P22 §3.4 step 4 — the optimizer-only allowedTools envelope. Every
|
|
63
|
+
* `routine.skill_curation` event runs the agent with exactly these tools
|
|
64
|
+
* and nothing else. The curl glob is anchored on the daemon's loopback URL
|
|
65
|
+
* so a hook-bypassed request still hits the curation API's chokepoint
|
|
66
|
+
* (Zod, run-token, smoke test); `Read` is required for the agent to
|
|
67
|
+
* consume the inlined data dump under the workdir's `data/` subtree.
|
|
68
|
+
*
|
|
69
|
+
* Kept narrow on purpose: adding any other tool here widens the optimizer's
|
|
70
|
+
* blast radius. If a future signal source needs the agent to write to a
|
|
71
|
+
* different surface, add a new curation API endpoint and let the curl glob
|
|
72
|
+
* cover it — do NOT add `Bash(*)` or `Write` here.
|
|
73
|
+
*/
|
|
74
|
+
export const SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS = [
|
|
75
|
+
"Read",
|
|
76
|
+
"Bash(curl http://localhost:8321/api/skill-curation/*)",
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* Read-only tool envelope for `git.project.refresh_architecture`. This agent
|
|
80
|
+
* walks the user's local git worktree at `<task_context.localPath>` to compose
|
|
81
|
+
* the `## Architecture` section of `git/<slug>/overview.md`, and lands the
|
|
82
|
+
* result through `PUT /api/repositories/:id/architecture-section` — the one
|
|
83
|
+
* daemon-side chokepoint. Without this clamp the session would inherit
|
|
84
|
+
* `CLAUDE_DEFAULT_ALLOWED_TOOLS` (Write/Edit/`Bash(git *)`/`Bash(curl *)`),
|
|
85
|
+
* which would let a prompt-injected README or a misbehaving turn mutate the
|
|
86
|
+
* user's repository (e.g. `git reset --hard`, `git push --force`, arbitrary
|
|
87
|
+
* `Write` to source files) OR exfiltrate via other Autonomous daemon APIs
|
|
88
|
+
* (`POST /api/notify` to DM the owner with attacker content, `POST
|
|
89
|
+
* /api/observations` to inject fake observations, `PUT /api/obsidian/notes`
|
|
90
|
+
* to overwrite vault notes, etc.). The Architecture analysis itself only
|
|
91
|
+
* needs to *read* the worktree.
|
|
92
|
+
*
|
|
93
|
+
* What is INCLUDED and why:
|
|
94
|
+
* - `Read` / `Glob` / `Grep` — the task-flow's only durable need (README,
|
|
95
|
+
* manifests, source files, design docs). `Glob` covers the literal
|
|
96
|
+
* `ls <localPath>` step without giving the agent shell access.
|
|
97
|
+
* - `Bash(curl http://localhost:8321/api/repositories/*\/architecture-section*)`
|
|
98
|
+
* — endpoint-pinned write path. The SDK's prefix-glob layer forbids the
|
|
99
|
+
* command from reaching ANY other host, port, or daemon-API namespace.
|
|
100
|
+
* The curl PreToolUse hook adds defense-in-depth (rechecks host/port,
|
|
101
|
+
* denies connection-override flags); the API risk classifier supplies
|
|
102
|
+
* the floor (only `PUT .../architecture-section` is Autonomous under
|
|
103
|
+
* `/api/repositories/`; everything else inherits Approve and 401s a
|
|
104
|
+
* tokenless agent curl). Port is hardcoded to the daemon's default
|
|
105
|
+
* `8321` matching the optimizer-clamp convention; operators who change
|
|
106
|
+
* `PA_API_PORT` accept the gap consciously (the same constraint applies
|
|
107
|
+
* to `SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS`).
|
|
108
|
+
* - `Bash(jq *)` — body construction. The PUT body is
|
|
109
|
+
* `{"markdown":"..."}` and the markdown contains arbitrary characters
|
|
110
|
+
* that must be JSON-escaped; `jq -n --arg md "$body" '{markdown:$md}'`
|
|
111
|
+
* is the only robust escape path under a no-`Write` envelope. The jq
|
|
112
|
+
* hook denies `--slurpfile` / `--rawfile` / `-L` / `env`-filter
|
|
113
|
+
* exfiltration.
|
|
114
|
+
*
|
|
115
|
+
* What is INTENTIONALLY EXCLUDED:
|
|
116
|
+
* - `Write` / `Edit` — would let the agent write anywhere, including the
|
|
117
|
+
* user's checked-out worktree. The chokepoint is the daemon API.
|
|
118
|
+
* - `Bash(git *)` — even read-only verbs let the agent chain into
|
|
119
|
+
* `git push --force`, `git reset --hard`, `git checkout --`, etc. via
|
|
120
|
+
* shell separators; the `always-disallowed.ts` classifier hook catches
|
|
121
|
+
* `rm -rf` / `sudo` / pipe-to-shell but does NOT classify mutating git
|
|
122
|
+
* subcommands. The Architecture analysis doesn't need git CLI:
|
|
123
|
+
* filesystem reads via `Read` / `Glob` suffice for module / data-flow /
|
|
124
|
+
* build / test-surface description.
|
|
125
|
+
* - `Bash(ls *)` — `Glob` covers directory enumeration without shell
|
|
126
|
+
* access.
|
|
127
|
+
* - `Skill` / `WebSearch` — not referenced by the task-flow; smaller
|
|
128
|
+
* surface is better.
|
|
129
|
+
*
|
|
130
|
+
* Defense-in-depth layering:
|
|
131
|
+
* - SDK `allowedTools` (this list) — first gate. Prefix glob forces the
|
|
132
|
+
* curl command to literally begin with the architecture-section URL.
|
|
133
|
+
* - SDK `disallowedTools` — `ALWAYS_DISALLOWED_TOOLS` +
|
|
134
|
+
* `config.disallowedTools` still merge on top.
|
|
135
|
+
* - PreToolUse hooks — curl localhost-only + jq exfil bans + Write/Edit
|
|
136
|
+
* context-dir chokepoint stay armed. `claude-code-core.ts` forces
|
|
137
|
+
* strict hook mode (curl + jq hooks re-enabled) whenever any
|
|
138
|
+
* `allowedToolsOverride` is active, so Allow-mode operators do not
|
|
139
|
+
* inadvertently widen this surface — see the `optimizerClampActive`
|
|
140
|
+
* branch.
|
|
141
|
+
* - Allow mode bypass — `claude-code-core.ts` detects the non-empty
|
|
142
|
+
* `allowedToolsOverride` and forces `permissionMode: "dontAsk"` for
|
|
143
|
+
* this run, stripping `bypassPermissions` even if the operator has
|
|
144
|
+
* Allow mode globally enabled.
|
|
145
|
+
* - API risk classifier — `PUT /api/repositories/:id/architecture-section`
|
|
146
|
+
* is RiskTier.Autonomous (agent-callable, no Bearer required) but
|
|
147
|
+
* enforces marker-bracketed body validation and 64KB size cap
|
|
148
|
+
* server-side. All sibling routes under `/api/repositories/` inherit
|
|
149
|
+
* the blanket Approve tier and 401 a tokenless agent curl.
|
|
150
|
+
*
|
|
151
|
+
* Multi-request defenses (closed in `claude-tool-collection.ts:bashCurlHook`,
|
|
152
|
+
* benefit every clamped session inheriting the curl hook):
|
|
153
|
+
* 1. **Shell-chained second curl** — `curl ARCH_URL ; curl
|
|
154
|
+
* http://localhost:8321/api/notify -d @evil` and the `&&` / `||` /
|
|
155
|
+
* `|` / newline / backtick / `$(…)` variants. The hook counts
|
|
156
|
+
* `curl` tokens anchored at command-start positions (mirroring the
|
|
157
|
+
* `cmdStart` regex in `safety/always-disallowed.ts`) and blocks any
|
|
158
|
+
* command with more than one anchored `curl`. A single
|
|
159
|
+
* `jq -n '{markdown:$md}' | curl URL -d @-` pipeline still counts
|
|
160
|
+
* as ONE curl token and is allowed.
|
|
161
|
+
* 2. **`--next` / `-:` URL multiplexing** — curl's same-process URL
|
|
162
|
+
* separator that resets option state per transaction. Hook-blocked
|
|
163
|
+
* via flag regex (covers `--next`, `--next=URL`, and the `-:`
|
|
164
|
+
* short form).
|
|
165
|
+
* 3. **Multi-positional URLs** — `curl URL1 URL2 -X PUT -d @body`
|
|
166
|
+
* sends identical options to both URLs sequentially. The hook
|
|
167
|
+
* tokenizes the command at the top level (outside paired single /
|
|
168
|
+
* double quotes) and blocks when more than one URL appears as a
|
|
169
|
+
* top-level token. URLs that legitimately appear inside `-d '…'`
|
|
170
|
+
* / `-H "…"` strings — e.g. external links inside the architecture
|
|
171
|
+
* markdown body — are not counted and not host-checked, so the
|
|
172
|
+
* agent can reference external code in its analysis.
|
|
173
|
+
*
|
|
174
|
+
* Per-backend support:
|
|
175
|
+
* - Claude (`ClaudeCodeCore`) — consumes this list verbatim.
|
|
176
|
+
* - Codex / Gemini — no per-execute allowedTools surface today (mirrors
|
|
177
|
+
* `AgentExecuteParams.allowedToolsOverride` JSDoc). The default
|
|
178
|
+
* `process_backend_config` seed binds `git.project.refresh_architecture`
|
|
179
|
+
* to the medium tier (Sonnet), so the realistic risk surface today is
|
|
180
|
+
* Claude-only. An operator who reroutes this process key to a
|
|
181
|
+
* non-Claude backend via `/settings/models` accepts the gap
|
|
182
|
+
* consciously.
|
|
183
|
+
*/
|
|
184
|
+
export const REFRESH_ARCHITECTURE_ALLOWED_TOOLS = [
|
|
185
|
+
"Read",
|
|
186
|
+
"Glob",
|
|
187
|
+
"Grep",
|
|
188
|
+
"Bash(curl http://localhost:8321/api/repositories/*/architecture-section*)",
|
|
189
|
+
"Bash(jq *)",
|
|
190
|
+
];
|
|
191
|
+
/**
|
|
192
|
+
* Backends that honor the per-execute `allowedToolsOverride` clamp end-to-
|
|
193
|
+
* end. Claude consumes the list verbatim through the SDK's `dontAsk` +
|
|
194
|
+
* `allowedTools` posture and the dispatcher swaps Allow mode back to
|
|
195
|
+
* strict for the run. Codex / Gemini have no per-execute allowedTools
|
|
196
|
+
* surface today (see `AgentExecuteParams.allowedToolsOverride` JSDoc),
|
|
197
|
+
* so the clamp would silently drop and the read-only contract would
|
|
198
|
+
* become a no-op. We refuse-at-execute rather than silently widen the
|
|
199
|
+
* envelope; the operator sees an `agent_actions` row of action_type
|
|
200
|
+
* `scheduled_task_clamp_unsupported` and a clear log line.
|
|
201
|
+
*
|
|
202
|
+
* Add a backend here only after verifying its core threads
|
|
203
|
+
* `allowedToolsOverride` through to its concrete deny enforcement layer
|
|
204
|
+
* — NOT just into the CLI flag set.
|
|
205
|
+
*/
|
|
206
|
+
export const TOOL_CLAMP_SUPPORTING_BACKENDS = new Set([
|
|
207
|
+
"claude",
|
|
208
|
+
]);
|
|
209
|
+
export class ScheduledTaskRunner {
|
|
210
|
+
db;
|
|
211
|
+
config;
|
|
212
|
+
contextBuilder;
|
|
213
|
+
agentRouter;
|
|
214
|
+
prompt;
|
|
215
|
+
errorRouter;
|
|
216
|
+
resultProcessor;
|
|
217
|
+
morningRoutine;
|
|
218
|
+
fetchWindowRunner;
|
|
219
|
+
roadmapWriteLock;
|
|
220
|
+
writeTracker;
|
|
221
|
+
getConfiguredServices;
|
|
222
|
+
getActiveMailAccounts;
|
|
223
|
+
getMaterializeOptimizerWorkdir;
|
|
224
|
+
getTeardownOptimizerWorkdir;
|
|
225
|
+
constructor(deps) {
|
|
226
|
+
this.db = deps.db;
|
|
227
|
+
this.config = deps.config;
|
|
228
|
+
this.contextBuilder = deps.contextBuilder;
|
|
229
|
+
this.agentRouter = deps.agentRouter;
|
|
230
|
+
this.prompt = deps.prompt;
|
|
231
|
+
this.errorRouter = deps.errorRouter;
|
|
232
|
+
this.resultProcessor = deps.resultProcessor;
|
|
233
|
+
this.morningRoutine = deps.morningRoutine;
|
|
234
|
+
this.fetchWindowRunner = deps.fetchWindowRunner;
|
|
235
|
+
this.roadmapWriteLock = deps.roadmapWriteLock;
|
|
236
|
+
this.writeTracker = deps.writeTracker;
|
|
237
|
+
this.getConfiguredServices = deps.getConfiguredServices;
|
|
238
|
+
this.getActiveMailAccounts = deps.getActiveMailAccounts;
|
|
239
|
+
this.getMaterializeOptimizerWorkdir = deps.getMaterializeOptimizerWorkdir;
|
|
240
|
+
this.getTeardownOptimizerWorkdir = deps.getTeardownOptimizerWorkdir;
|
|
241
|
+
}
|
|
242
|
+
// ────── Repository run + scheduled task entry points ──────
|
|
243
|
+
buildRepositoryRunPrompt(ctx) {
|
|
244
|
+
const lines = [
|
|
245
|
+
"{context}",
|
|
246
|
+
"",
|
|
247
|
+
"## Repository Run",
|
|
248
|
+
`Repository id: ${ctx.repositoryId}`,
|
|
249
|
+
`Repository slug: ${ctx.slug}`,
|
|
250
|
+
`GitHub repo: ${ctx.githubRepo ?? "(none)"}`,
|
|
251
|
+
`Local path: ${ctx.localPath ?? "(none)"}`,
|
|
252
|
+
`Workdir mode: ${ctx.workdirMode}`,
|
|
253
|
+
`Trigger source: ${ctx.triggerSource}`,
|
|
254
|
+
];
|
|
255
|
+
if (ctx.triggerId || ctx.triggerName || ctx.triggerEventType) {
|
|
256
|
+
lines.push("", "## Trigger", `Trigger id: ${ctx.triggerId ?? "(manual)"}`, `Trigger name: ${ctx.triggerName ?? "(manual)"}`, `Event type: ${ctx.triggerEventType ?? "(manual)"}`);
|
|
257
|
+
if (ctx.triggerEventPayload !== undefined) {
|
|
258
|
+
lines.push("", "<trigger_event_payload>", JSON.stringify(ctx.triggerEventPayload, null, 2), "</trigger_event_payload>");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
lines.push("", "## User Prompt", ctx.prompt);
|
|
262
|
+
return lines.join("\n");
|
|
263
|
+
}
|
|
264
|
+
prepareRepositoryRunSessionDir(ctx, backendId) {
|
|
265
|
+
if (ctx.workdirMode === "local-clone") {
|
|
266
|
+
if (!ctx.localPath) {
|
|
267
|
+
throw new Error("Repository local-clone run missing localPath");
|
|
268
|
+
}
|
|
269
|
+
ensureBackendMaterialized(this.config.workspaceDir, ctx.localPath, backendId, "scheduled.task", "agent.task", this.getConfiguredServices(), this.getActiveMailAccounts(), readIntegrations(this.db), this.config.character);
|
|
270
|
+
return { sessionDir: ctx.localPath, cleanup: false };
|
|
271
|
+
}
|
|
272
|
+
if (!ctx.instructionMd) {
|
|
273
|
+
throw new Error("Repository temp run missing instructionMd");
|
|
274
|
+
}
|
|
275
|
+
const sessionDir = join(this.config.dataDir, "run", `${safeRepositoryRunDirName(ctx.slug)}-${Date.now()}-${randomUUID().slice(0, 8)}`);
|
|
276
|
+
mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
|
277
|
+
try {
|
|
278
|
+
ensureBackendMaterialized(this.config.workspaceDir, sessionDir, backendId, "scheduled.task", "agent.task", this.getConfiguredServices(), this.getActiveMailAccounts(), readIntegrations(this.db), this.config.character);
|
|
279
|
+
writeFileSync(join(sessionDir, repositoryRunInstructionFilename(backendId)), ctx.instructionMd, "utf-8");
|
|
280
|
+
return { sessionDir, cleanup: true };
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
cleanupSessionWorkdir(sessionDir);
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async executeRepositoryRunTask(event, ctx) {
|
|
288
|
+
const context = await this.contextBuilder.build(event);
|
|
289
|
+
const processKey = "agent.task";
|
|
290
|
+
const requestedTier = event.requestedModel
|
|
291
|
+
? (event.requestedModel === "sonnet" ? "medium" : "high")
|
|
292
|
+
: undefined;
|
|
293
|
+
const internalBackendOverride = event.requestedBackendId
|
|
294
|
+
&& isBackendId(event.requestedBackendId)
|
|
295
|
+
&& typeof event.requestedModelId === "string"
|
|
296
|
+
? {
|
|
297
|
+
requestedBackendId: event.requestedBackendId,
|
|
298
|
+
requestedModelId: event.requestedModelId,
|
|
299
|
+
}
|
|
300
|
+
: {};
|
|
301
|
+
const binding = this.agentRouter.resolveBinding(event, {
|
|
302
|
+
processKey,
|
|
303
|
+
requestedTier,
|
|
304
|
+
...internalBackendOverride,
|
|
305
|
+
});
|
|
306
|
+
const prompt = this.buildRepositoryRunPrompt(ctx);
|
|
307
|
+
const { sessionDir, cleanup } = this.prepareRepositoryRunSessionDir(ctx, binding.main.backendId);
|
|
308
|
+
try {
|
|
309
|
+
const result = await this.errorRouter.executeWithRetry(() => this.agentRouter.execute({
|
|
310
|
+
prompt,
|
|
311
|
+
context,
|
|
312
|
+
event,
|
|
313
|
+
processKey,
|
|
314
|
+
requestedTier,
|
|
315
|
+
preResolvedBinding: binding,
|
|
316
|
+
reassemblePrompt: () => prompt,
|
|
317
|
+
sessionDir,
|
|
318
|
+
workdirEventType: "scheduled.task",
|
|
319
|
+
workdirProcessKey: processKey,
|
|
320
|
+
...internalBackendOverride,
|
|
321
|
+
}), event);
|
|
322
|
+
await this.resultProcessor.processResult(result, event);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
if (cleanup) {
|
|
326
|
+
cleanupSessionWorkdir(sessionDir);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Execute a scheduled task with the model specified when the task was
|
|
332
|
+
* registered via POST /api/schedule.
|
|
333
|
+
*
|
|
334
|
+
* Morning-routine retry tasks take a dedicated fast path: they skip
|
|
335
|
+
* the generic scheduled.task prompt and run the *real* morning routine
|
|
336
|
+
* flow via executeMorningRoutine, so the retry carries the same rotateDayFiles
|
|
337
|
+
* / prompt selection / roadmap-refresh chain as the cron-fired path.
|
|
338
|
+
*/
|
|
339
|
+
async executeScheduledTask(event) {
|
|
340
|
+
// Morning-routine retry detection: if taskContext says this wake
|
|
341
|
+
// task is a morning-routine retry, dispatch through executeMorningRoutine
|
|
342
|
+
// with a synthesized RoutineEvent instead of the generic flow.
|
|
343
|
+
const taskCtx = event.taskContext;
|
|
344
|
+
if (taskCtx &&
|
|
345
|
+
typeof taskCtx === "object" &&
|
|
346
|
+
taskCtx.routine === "morning_routine") {
|
|
347
|
+
await this.handleMorningRoutineRetry(event, taskCtx);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (taskCtx &&
|
|
351
|
+
typeof taskCtx === "object" &&
|
|
352
|
+
taskCtx.routine === "today_refresh") {
|
|
353
|
+
await this.executeScheduledRoutine(event, "today_refresh");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const repositoryRunCtx = parseRepositoryRunTaskContext(taskCtx);
|
|
357
|
+
if (repositoryRunCtx) {
|
|
358
|
+
await this.executeRepositoryRunTask(event, repositoryRunCtx);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (await this.executeGitProjectDocTaskIfApplicable(event, taskCtx)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const context = await this.contextBuilder.build(event);
|
|
365
|
+
const processKeyOverride = taskCtx
|
|
366
|
+
&& typeof taskCtx === "object"
|
|
367
|
+
&& typeof taskCtx.processKey === "string"
|
|
368
|
+
? taskCtx.processKey
|
|
369
|
+
: null;
|
|
370
|
+
const processKey = (processKeyOverride ?? resolveProcessKey(event));
|
|
371
|
+
const promptKey = processKeyOverride ?? event.type;
|
|
372
|
+
const requestedTier = event.requestedModel
|
|
373
|
+
? (event.requestedModel === "sonnet" ? "medium" : "high")
|
|
374
|
+
: undefined;
|
|
375
|
+
const internalBackendOverride = event.requestedBackendId
|
|
376
|
+
&& isBackendId(event.requestedBackendId)
|
|
377
|
+
&& typeof event.requestedModelId === "string"
|
|
378
|
+
? {
|
|
379
|
+
requestedBackendId: event.requestedBackendId,
|
|
380
|
+
requestedModelId: event.requestedModelId,
|
|
381
|
+
}
|
|
382
|
+
: {};
|
|
383
|
+
const binding = this.agentRouter.resolveBinding(event, {
|
|
384
|
+
processKey,
|
|
385
|
+
requestedTier,
|
|
386
|
+
...internalBackendOverride,
|
|
387
|
+
});
|
|
388
|
+
const reassemblePrompt = (bid) => this.prompt.assemble(promptKey, processKey, bid);
|
|
389
|
+
const prompt = reassemblePrompt(binding.main.backendId);
|
|
390
|
+
// Daily-git-management safety clamp — see
|
|
391
|
+
// `REFRESH_ARCHITECTURE_ALLOWED_TOOLS` JSDoc. The check is on
|
|
392
|
+
// `processKey` (carried by the agent_schedule row's task_context)
|
|
393
|
+
// rather than `event.source` so a downstream rename of the schedule
|
|
394
|
+
// source string cannot silently widen the envelope; the process key
|
|
395
|
+
// is the contract surface.
|
|
396
|
+
const refreshArchitectureOverride = processKey === "git.project.refresh_architecture"
|
|
397
|
+
? REFRESH_ARCHITECTURE_ALLOWED_TOOLS
|
|
398
|
+
: undefined;
|
|
399
|
+
if (refreshArchitectureOverride
|
|
400
|
+
&& !this.clampSupportedByBackend(processKey, binding.main.backendId, event.correlationId, "REFRESH_ARCHITECTURE_ALLOWED_TOOLS")) {
|
|
401
|
+
// Refuse-at-execute. The audit row + log line are written inside
|
|
402
|
+
// the guard; mark the schedule row done so the operator's only
|
|
403
|
+
// path to "fix it" is via /settings/models, not by waiting for a
|
|
404
|
+
// retry storm.
|
|
405
|
+
this.markScheduledTaskCompleted(event);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const result = await this.errorRouter.executeWithRetry(() => this.agentRouter.execute({
|
|
409
|
+
prompt,
|
|
410
|
+
context,
|
|
411
|
+
event,
|
|
412
|
+
processKey,
|
|
413
|
+
requestedTier,
|
|
414
|
+
preResolvedBinding: binding,
|
|
415
|
+
reassemblePrompt,
|
|
416
|
+
...(refreshArchitectureOverride
|
|
417
|
+
? { allowedToolsOverride: refreshArchitectureOverride }
|
|
418
|
+
: {}),
|
|
419
|
+
}), event);
|
|
420
|
+
await this.resultProcessor.processResult(result, event);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Legacy git project documentation tasks used to run as autonomous Claude
|
|
424
|
+
* task-flows. That made file creation probabilistic: the backend could
|
|
425
|
+
* finish "successfully" without calling the daemon context API, or fail
|
|
426
|
+
* before receiving the `<task_context>` block. The daemon now owns these
|
|
427
|
+
* writes directly, matching the manual Daily git management buttons and
|
|
428
|
+
* the repository-management cron.
|
|
429
|
+
*/
|
|
430
|
+
async executeGitProjectDocTaskIfApplicable(event, taskCtx) {
|
|
431
|
+
const processKey = this.resolveGitProjectDocProcessKey(event, taskCtx);
|
|
432
|
+
if (!processKey)
|
|
433
|
+
return false;
|
|
434
|
+
const ctx = taskCtx && typeof taskCtx === "object"
|
|
435
|
+
? taskCtx
|
|
436
|
+
: {};
|
|
437
|
+
const repo = this.resolveRepositoryForGitProjectDocTask(ctx);
|
|
438
|
+
const triggerSource = typeof ctx.triggerSource === "string"
|
|
439
|
+
? ctx.triggerSource
|
|
440
|
+
: null;
|
|
441
|
+
const isManagementSource = triggerSource === "repository_management_cron" ||
|
|
442
|
+
triggerSource === "repository_management_manual";
|
|
443
|
+
try {
|
|
444
|
+
if (processKey === "git.project.init") {
|
|
445
|
+
const result = runRepositoryManagementInit({
|
|
446
|
+
db: this.db,
|
|
447
|
+
repo,
|
|
448
|
+
contextDir: getContextDir(this.config, this.db),
|
|
449
|
+
timezone: this.config.timezone || undefined,
|
|
450
|
+
writeTracker: this.writeTracker,
|
|
451
|
+
});
|
|
452
|
+
if (isManagementSource) {
|
|
453
|
+
recordManagementInitDone(this.db, repo.id);
|
|
454
|
+
}
|
|
455
|
+
this.markScheduledTaskCompleted(event);
|
|
456
|
+
logger.info({
|
|
457
|
+
scheduleId: event.scheduleId ?? null,
|
|
458
|
+
repositoryId: repo.id,
|
|
459
|
+
slug: repo.slug,
|
|
460
|
+
result: result.status,
|
|
461
|
+
architectureScheduleId: result.architectureScheduleId,
|
|
462
|
+
}, "Handled git.project.init with direct markdown writer");
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const lookbackHours = typeof ctx.lookbackHours === "number"
|
|
466
|
+
&& Number.isFinite(ctx.lookbackHours)
|
|
467
|
+
&& ctx.lookbackHours > 0
|
|
468
|
+
? ctx.lookbackHours
|
|
469
|
+
: undefined;
|
|
470
|
+
const result = await runRepositoryManagementScan({
|
|
471
|
+
db: this.db,
|
|
472
|
+
repo,
|
|
473
|
+
contextDir: getContextDir(this.config, this.db),
|
|
474
|
+
timezone: this.config.timezone || undefined,
|
|
475
|
+
lookbackHours,
|
|
476
|
+
writeTracker: this.writeTracker,
|
|
477
|
+
});
|
|
478
|
+
if (isManagementSource) {
|
|
479
|
+
recordManagementScan(this.db, repo.id, result.status === "skipped_no_activity" ? "skipped_no_activity" : "ok");
|
|
480
|
+
}
|
|
481
|
+
this.markScheduledTaskCompleted(event);
|
|
482
|
+
logger.info({
|
|
483
|
+
scheduleId: event.scheduleId ?? null,
|
|
484
|
+
repositoryId: repo.id,
|
|
485
|
+
slug: repo.slug,
|
|
486
|
+
result: result.status,
|
|
487
|
+
journalPath: result.journalPath,
|
|
488
|
+
}, "Handled git.project.update with direct markdown writer");
|
|
489
|
+
}
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
if (isManagementSource) {
|
|
494
|
+
try {
|
|
495
|
+
recordManagementScan(this.db, repo.id, "failed");
|
|
496
|
+
}
|
|
497
|
+
catch (recordErr) {
|
|
498
|
+
logger.error({ err: recordErr, repositoryId: repo.id }, "Failed to record repository management direct-writer failure");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (event.scheduleId) {
|
|
502
|
+
this.db
|
|
503
|
+
.prepare("UPDATE agent_schedule SET status = 'failed' WHERE id = ? AND status = 'running'")
|
|
504
|
+
.run(event.scheduleId);
|
|
505
|
+
}
|
|
506
|
+
logger.error({ err, scheduleId: event.scheduleId ?? null, repositoryId: repo.id }, "Git project documentation direct writer failed");
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
resolveGitProjectDocProcessKey(event, taskCtx) {
|
|
511
|
+
const ctxProcessKey = taskCtx &&
|
|
512
|
+
typeof taskCtx === "object" &&
|
|
513
|
+
typeof taskCtx.processKey === "string"
|
|
514
|
+
? taskCtx.processKey
|
|
515
|
+
: null;
|
|
516
|
+
const value = ctxProcessKey ?? event.source;
|
|
517
|
+
return value === "git.project.init" || value === "git.project.update"
|
|
518
|
+
? value
|
|
519
|
+
: null;
|
|
520
|
+
}
|
|
521
|
+
resolveRepositoryForGitProjectDocTask(ctx) {
|
|
522
|
+
const repositoryId = typeof ctx.repositoryId === "string"
|
|
523
|
+
? ctx.repositoryId
|
|
524
|
+
: null;
|
|
525
|
+
if (repositoryId) {
|
|
526
|
+
const byId = getRepository(this.db, repositoryId);
|
|
527
|
+
if (byId)
|
|
528
|
+
return byId;
|
|
529
|
+
}
|
|
530
|
+
const localPath = typeof ctx.localPath === "string"
|
|
531
|
+
? ctx.localPath
|
|
532
|
+
: typeof ctx.repository?.localPath === "string"
|
|
533
|
+
? ctx.repository.localPath
|
|
534
|
+
: null;
|
|
535
|
+
if (localPath) {
|
|
536
|
+
const byPath = getRepositoryByLocalPath(this.db, localPath);
|
|
537
|
+
if (byPath)
|
|
538
|
+
return byPath;
|
|
539
|
+
}
|
|
540
|
+
const slug = typeof ctx.slug === "string"
|
|
541
|
+
? ctx.slug
|
|
542
|
+
: typeof ctx.repository?.slug === "string"
|
|
543
|
+
? ctx.repository.slug
|
|
544
|
+
: null;
|
|
545
|
+
if (!slug || !localPath) {
|
|
546
|
+
throw new Error("git project documentation task requires repositoryId or slug/localPath task context");
|
|
547
|
+
}
|
|
548
|
+
const githubRepo = typeof ctx.githubRepo === "string"
|
|
549
|
+
? ctx.githubRepo
|
|
550
|
+
: typeof ctx.repository?.githubRepo === "string"
|
|
551
|
+
? ctx.repository.githubRepo
|
|
552
|
+
: null;
|
|
553
|
+
const [githubOwner, githubRepoName] = parseGithubRepoSlug(githubRepo);
|
|
554
|
+
const now = Date.now();
|
|
555
|
+
return {
|
|
556
|
+
id: repositoryId ?? (githubRepo ? `github:${githubRepo}` : `local:${slug}`),
|
|
557
|
+
githubOwner,
|
|
558
|
+
githubRepo: githubRepoName,
|
|
559
|
+
githubAccount: null,
|
|
560
|
+
localPath,
|
|
561
|
+
localOnly: githubRepo === null,
|
|
562
|
+
displayName: typeof ctx.displayName === "string" ? ctx.displayName : slug,
|
|
563
|
+
classification: normalizeRepositoryClassification(ctx.classification),
|
|
564
|
+
category: normalizeRepositoryCategory(ctx.category),
|
|
565
|
+
pollPriority: "normal",
|
|
566
|
+
pollIntervalSec: null,
|
|
567
|
+
slug,
|
|
568
|
+
createdAt: now,
|
|
569
|
+
updatedAt: now,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
markScheduledTaskCompleted(event) {
|
|
573
|
+
if (!event.scheduleId)
|
|
574
|
+
return;
|
|
575
|
+
this.db
|
|
576
|
+
.prepare("UPDATE agent_schedule SET status = 'completed' WHERE id = ? AND status = 'running'")
|
|
577
|
+
.run(event.scheduleId);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Defense-in-depth gate for per-execute tool clamps. When the
|
|
581
|
+
* dispatcher pins an `allowedToolsOverride` for a known-safe envelope
|
|
582
|
+
* (refresh_architecture, skill_curation) the clamp MUST hold; if the
|
|
583
|
+
* router resolves to a backend that ignores per-execute clamps the
|
|
584
|
+
* call would silently widen back to the default tool surface and the
|
|
585
|
+
* read-only contract documented in the clamp's JSDoc would dissolve.
|
|
586
|
+
*
|
|
587
|
+
* Returns `true` when the resolved main backend honors clamps (the
|
|
588
|
+
* caller should pass the override through to `execute`). Returns
|
|
589
|
+
* `false` when the operator has rebound the process key to a backend
|
|
590
|
+
* we cannot trust — the caller bails out of the execute, an
|
|
591
|
+
* `agent_actions` row records the refusal for the audit log, and an
|
|
592
|
+
* error-level log line surfaces the misconfiguration immediately.
|
|
593
|
+
*
|
|
594
|
+
* Implementation note: the audit row uses `result = 'failed'` to
|
|
595
|
+
* match the `blocked_absolute` precedent — the `agent_actions.result`
|
|
596
|
+
* CHECK constraint only permits the canonical settle states
|
|
597
|
+
* (success / failed / partial / skipped / in_progress); a literal
|
|
598
|
+
* `"blocked"` here would silently violate the constraint and the
|
|
599
|
+
* try/catch would swallow the audit. The `action_type` is the
|
|
600
|
+
* discriminator that lets dashboards / queries distinguish a "blocked
|
|
601
|
+
* by clamp" row from a real agent failure.
|
|
602
|
+
*/
|
|
603
|
+
clampSupportedByBackend(processKey, backendId, correlationId, clampName) {
|
|
604
|
+
if (TOOL_CLAMP_SUPPORTING_BACKENDS.has(backendId))
|
|
605
|
+
return true;
|
|
606
|
+
logger.error({ processKey, backendId, clampName, correlationId }, "Refusing scheduled task: process key carries a per-execute tool clamp that the resolved backend cannot enforce. Reconfigure /settings/models to bind this process key to a backend in TOOL_CLAMP_SUPPORTING_BACKENDS (currently: claude) or remove the clamp.");
|
|
607
|
+
try {
|
|
608
|
+
const detail = {
|
|
609
|
+
process_key: processKey,
|
|
610
|
+
backend: backendId,
|
|
611
|
+
clamp: clampName,
|
|
612
|
+
supported_backends: Array.from(TOOL_CLAMP_SUPPORTING_BACKENDS),
|
|
613
|
+
correlation_id: correlationId ?? null,
|
|
614
|
+
reason: "allowedToolsOverride is not enforceable on this backend " +
|
|
615
|
+
"(no per-execute allowedTools surface); refused at dispatch.",
|
|
616
|
+
};
|
|
617
|
+
this.db
|
|
618
|
+
.prepare(`INSERT INTO agent_actions
|
|
619
|
+
(action_type, trigger, result, detail, started_at, completed_at)
|
|
620
|
+
VALUES ('scheduled_task_clamp_unsupported', 'autonomous', 'failed', json(?), datetime('now'), datetime('now'))`)
|
|
621
|
+
.run(JSON.stringify(detail));
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
logger.warn({ err }, "Failed to record clamp_unsupported audit row");
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
async executeScheduledRoutine(event, routine) {
|
|
629
|
+
const routineEvent = {
|
|
630
|
+
...createEvent({
|
|
631
|
+
type: `routine.${routine}`,
|
|
632
|
+
source: typeof event.taskContext.source === "string"
|
|
633
|
+
? event.taskContext.source
|
|
634
|
+
: event.source,
|
|
635
|
+
priority: EventPriority.NORMAL,
|
|
636
|
+
correlationId: event.correlationId,
|
|
637
|
+
data: {
|
|
638
|
+
...event.taskContext,
|
|
639
|
+
scheduleId: event.scheduleId ?? null,
|
|
640
|
+
},
|
|
641
|
+
}),
|
|
642
|
+
routine,
|
|
643
|
+
...(event.requestedModel ? { requestedModel: event.requestedModel } : {}),
|
|
644
|
+
};
|
|
645
|
+
try {
|
|
646
|
+
await this.executeDefault(routineEvent);
|
|
647
|
+
if (event.scheduleId) {
|
|
648
|
+
this.db
|
|
649
|
+
.prepare("UPDATE agent_schedule SET status = 'completed' WHERE id = ? AND status = 'running'")
|
|
650
|
+
.run(event.scheduleId);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
if (event.scheduleId) {
|
|
655
|
+
this.db
|
|
656
|
+
.prepare("UPDATE agent_schedule SET status = 'failed' WHERE id = ? AND status = 'running'")
|
|
657
|
+
.run(event.scheduleId);
|
|
658
|
+
}
|
|
659
|
+
throw err;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Handle a morning-routine retry wake task.
|
|
664
|
+
*
|
|
665
|
+
* Steps:
|
|
666
|
+
* 1. Early skip: if today.md already exists (e.g., the cron-fired
|
|
667
|
+
* morning routine raced us to it), mark this wake task completed
|
|
668
|
+
* without running the agent — saves one Opus session.
|
|
669
|
+
* 2. Synthesize a RoutineEvent with `event.data.retryCount` carrying
|
|
670
|
+
* the current attempt number, so that the recursive
|
|
671
|
+
* scheduleMorningRetry call from executeMorningRoutine can increment the
|
|
672
|
+
* retry chain naturally via the event.data code path.
|
|
673
|
+
* 3. Invoke executeMorningRoutine — this reuses the full morning-routine flow
|
|
674
|
+
* (rotateDayFiles, prompt selection, agent execute, post-result
|
|
675
|
+
* today.md check, roadmap_refresh emission).
|
|
676
|
+
* 4. Mark the wake task row completed. processResult inside the
|
|
677
|
+
* executeMorningRoutine call operates on the synthetic RoutineEvent, which
|
|
678
|
+
* is not an AgentTaskEvent, so it does not touch scheduleId — we
|
|
679
|
+
* must do it ourselves.
|
|
680
|
+
*/
|
|
681
|
+
async handleMorningRoutineRetry(event, taskCtx) {
|
|
682
|
+
const retryCount = Number(taskCtx.retryCount ?? 0);
|
|
683
|
+
// O1: early skip only when the current agent day's today.md already exists
|
|
684
|
+
if (this.hasCurrentAgentDayTodayMd()) {
|
|
685
|
+
logger.info({
|
|
686
|
+
retryCount,
|
|
687
|
+
originalCorrelationId: taskCtx.originalCorrelationId,
|
|
688
|
+
}, "Morning routine retry skipped — today.md already exists (cron likely raced us)");
|
|
689
|
+
if (event.scheduleId) {
|
|
690
|
+
this.db
|
|
691
|
+
.prepare("UPDATE agent_schedule SET status = 'completed' WHERE id = ? AND status = 'running'")
|
|
692
|
+
.run(event.scheduleId);
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// Synthesize a RoutineEvent for executeMorningRoutine. event.data.retryCount
|
|
697
|
+
// carries the previous attempt so executeMorningRoutine → scheduleMorningRetry
|
|
698
|
+
// can increment properly. correlationId tracks back to the original
|
|
699
|
+
// cron morning_routine for log correlation.
|
|
700
|
+
const synthEvent = {
|
|
701
|
+
...createEvent({
|
|
702
|
+
type: "routine.morning_routine",
|
|
703
|
+
source: typeof taskCtx.source === "string"
|
|
704
|
+
? taskCtx.source
|
|
705
|
+
: retryCount > 0
|
|
706
|
+
? `morning_routine_retry_${retryCount}`
|
|
707
|
+
: "scheduled_morning_routine",
|
|
708
|
+
priority: retryCount > 0 ? EventPriority.NORMAL : EventPriority.HIGH,
|
|
709
|
+
correlationId: taskCtx.originalCorrelationId ?? event.correlationId,
|
|
710
|
+
data: {
|
|
711
|
+
...(retryCount > 0 ? { retryCount, isRetry: true } : {}),
|
|
712
|
+
...(Array.isArray(taskCtx.postCatchupRoutines)
|
|
713
|
+
? { postCatchupRoutines: taskCtx.postCatchupRoutines }
|
|
714
|
+
: {}),
|
|
715
|
+
...(taskCtx.postCatchupHourlyCheck === true
|
|
716
|
+
? { postCatchupHourlyCheck: true }
|
|
717
|
+
: {}),
|
|
718
|
+
...(typeof taskCtx.source === "string"
|
|
719
|
+
? { queuedSource: taskCtx.source }
|
|
720
|
+
: {}),
|
|
721
|
+
},
|
|
722
|
+
}),
|
|
723
|
+
routine: "morning_routine",
|
|
724
|
+
};
|
|
725
|
+
logger.info({ retryCount, correlationId: synthEvent.correlationId }, "Morning routine retry — routing to executeMorningRoutine with synthesized RoutineEvent");
|
|
726
|
+
await this.morningRoutine.executeMorningRoutine(synthEvent);
|
|
727
|
+
// Mark the wake task row completed — executeMorningRoutine doesn't know about
|
|
728
|
+
// scheduleId since it received a RoutineEvent, not an AgentTaskEvent.
|
|
729
|
+
if (event.scheduleId) {
|
|
730
|
+
this.db
|
|
731
|
+
.prepare("UPDATE agent_schedule SET status = 'completed' WHERE id = ? AND status = 'running'")
|
|
732
|
+
.run(event.scheduleId);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
hasCurrentAgentDayTodayMd() {
|
|
736
|
+
return this.diagnoseTodayMdState().kind === "fresh";
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Inspect today.md and report its state relative to the current agent-day.
|
|
740
|
+
* Used by the post-routine retry gate so the log can distinguish between
|
|
741
|
+
* "file is missing" and "file has stale H1 date", which are different
|
|
742
|
+
* failure modes (process crash vs. format-confusion bug).
|
|
743
|
+
*/
|
|
744
|
+
diagnoseTodayMdState() {
|
|
745
|
+
const todayPath = join(getContextDir(this.config, this.db), "today.md");
|
|
746
|
+
if (!existsSync(todayPath)) {
|
|
747
|
+
return { kind: "missing" };
|
|
748
|
+
}
|
|
749
|
+
const content = readFileSync(todayPath, "utf-8");
|
|
750
|
+
const writtenDate = content.match(/^#.*(\d{4}-\d{2}-\d{2})/)?.[1];
|
|
751
|
+
if (!writtenDate) {
|
|
752
|
+
return { kind: "no_h1_date" };
|
|
753
|
+
}
|
|
754
|
+
const expectedAgentDay = getAgentDayDateStr(this.config.timezone || undefined, this.config.dayBoundaryHour);
|
|
755
|
+
if (writtenDate !== expectedAgentDay) {
|
|
756
|
+
return { kind: "wrong_date", writtenDate, expectedAgentDay };
|
|
757
|
+
}
|
|
758
|
+
return { kind: "fresh" };
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Rotate day files before Morning Routine:
|
|
762
|
+
* 1. today.md → schedule/YYYY-MM-DD.md (archive)
|
|
763
|
+
* 2. today.md → yesterday.md (rename for context injection)
|
|
764
|
+
*
|
|
765
|
+
* After this, ContextBuilder will read yesterday.md as <yesterday>
|
|
766
|
+
* and today.md will not exist (agent generates it fresh).
|
|
767
|
+
*/
|
|
768
|
+
rotateDayFiles() {
|
|
769
|
+
const contextDir = getContextDir(this.config, this.db);
|
|
770
|
+
const todayPath = join(contextDir, "today.md");
|
|
771
|
+
if (!existsSync(todayPath))
|
|
772
|
+
return;
|
|
773
|
+
const content = readFileSync(todayPath, "utf-8");
|
|
774
|
+
const dateStr = content.match(/^#.*(\d{4}-\d{2}-\d{2})/)?.[1];
|
|
775
|
+
// Skip if today.md is already today's date (no rotation needed)
|
|
776
|
+
const todayDateStr = getAgentDayDateStr(this.config.timezone || undefined, this.config.dayBoundaryHour);
|
|
777
|
+
if (dateStr === todayDateStr)
|
|
778
|
+
return;
|
|
779
|
+
if (!dateStr)
|
|
780
|
+
return;
|
|
781
|
+
// B-007 §5.9 — mechanical copy to schedule/ is retired. The only
|
|
782
|
+
// rotation artifact we preserve is a DB snapshot of the closing
|
|
783
|
+
// today.md; the synthesized `daily/YYYY-MM-DD.md` is written later by
|
|
784
|
+
// the morning routine from yesterday.md + SQLite event records.
|
|
785
|
+
// 1. Snapshot to DB for rebuild safety
|
|
786
|
+
try {
|
|
787
|
+
this.db
|
|
788
|
+
.prepare("INSERT INTO md_file_snapshots (file_path, content, trigger) VALUES (?, ?, ?)")
|
|
789
|
+
.run("today", content, "day_rotation");
|
|
790
|
+
}
|
|
791
|
+
catch (err) {
|
|
792
|
+
logger.warn({ err }, "Failed to save rotation snapshot");
|
|
793
|
+
}
|
|
794
|
+
// 2. Rename today.md → yesterday.md
|
|
795
|
+
const yesterdayPath = join(contextDir, CONTEXT_RELATIVE_PATHS.yesterday);
|
|
796
|
+
renameSync(todayPath, yesterdayPath);
|
|
797
|
+
logger.info({ archived: `schedule/${dateStr}.md` }, "Day files rotated");
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Roadmap-refresh execution with an exclusive cross-request write
|
|
801
|
+
* lock. The lockId is surfaced to the session context as
|
|
802
|
+
* `<roadmap_write_lock_id>` so the task-flow PUT / PATCH calls can
|
|
803
|
+
* pass `X-Lock-Id` and other concurrent flows (DM handler, evening
|
|
804
|
+
* sweeper) that attempt to write `/api/context/roadmap` during the
|
|
805
|
+
* refresh receive a 409.
|
|
806
|
+
*
|
|
807
|
+
* If the lock cannot be acquired (another session is mid-write), the
|
|
808
|
+
* refresh is skipped — `emitRoadmapRefresh` will retry on the next
|
|
809
|
+
* qualifying signal (dedup window permitting). This is the correct
|
|
810
|
+
* behaviour: the holder is already producing a fresher roadmap than
|
|
811
|
+
* anything we would emit right now.
|
|
812
|
+
*/
|
|
813
|
+
async executeRoadmapRefresh(event) {
|
|
814
|
+
let lockId = null;
|
|
815
|
+
let effectiveEvent = event;
|
|
816
|
+
if (this.roadmapWriteLock) {
|
|
817
|
+
const lock = this.roadmapWriteLock.acquire();
|
|
818
|
+
if (!lock.ok) {
|
|
819
|
+
logger.info({
|
|
820
|
+
eventType: event.type,
|
|
821
|
+
source: event.source,
|
|
822
|
+
holder: lock.holder,
|
|
823
|
+
}, "roadmap.md write lock held — skipping this refresh");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
lockId = lock.lockId;
|
|
827
|
+
effectiveEvent = {
|
|
828
|
+
...event,
|
|
829
|
+
data: {
|
|
830
|
+
...event.data,
|
|
831
|
+
roadmapWriteLockId: lockId,
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
await this.executeDefault(effectiveEvent);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
if (lockId && this.roadmapWriteLock) {
|
|
840
|
+
this.roadmapWriteLock.release(lockId);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* P22 §3.4 — skill curation routine. Provisions an isolated optimizer
|
|
846
|
+
* workdir, hands the runId + runToken into the agent's task context via
|
|
847
|
+
* `event.data`, and tears the workdir down regardless of success/failure.
|
|
848
|
+
*
|
|
849
|
+
* The standard `executeDefault` path produces the agent session itself —
|
|
850
|
+
* the only differences from a normal routine are: (a) the workdir is the
|
|
851
|
+
* pre-built optimizer dir (built by `materializeOptimizerWorkdir`), and
|
|
852
|
+
* (b) `executeDefault` recognises `routine.skill_curation` events and
|
|
853
|
+
* pins `allowedToolsOverride` to `SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS`,
|
|
854
|
+
* which the Claude SDK consumes verbatim and which suspends Allow-mode
|
|
855
|
+
* `bypassPermissions`. The curation API's run-token + Zod chokepoint
|
|
856
|
+
* remains the safety floor for the rare case the override is bypassed
|
|
857
|
+
* (e.g. a future backend that doesn't read `allowedTools`).
|
|
858
|
+
*/
|
|
859
|
+
async executeSkillCurationRoutine(event) {
|
|
860
|
+
const materialize = this.getMaterializeOptimizerWorkdir();
|
|
861
|
+
if (!materialize)
|
|
862
|
+
return;
|
|
863
|
+
// P22 §6.4 — manual run flag rides on the routine event's `data.manual`
|
|
864
|
+
// (set by `POST /api/skill-curation/runs/manual` from the dashboard).
|
|
865
|
+
// Cadence-driven cron events have no `manual` key, so the default is
|
|
866
|
+
// false — exactly the desired contract.
|
|
867
|
+
const eventData = event.data ?? {};
|
|
868
|
+
const manual = eventData.manual === true;
|
|
869
|
+
const targetSkillsOverride = Array.isArray(eventData.target_skills)
|
|
870
|
+
? eventData.target_skills
|
|
871
|
+
: undefined;
|
|
872
|
+
let workdir = null;
|
|
873
|
+
try {
|
|
874
|
+
workdir = await materialize({ manual, ...(targetSkillsOverride ? { targetSkillsOverride } : {}) });
|
|
875
|
+
logger.info({ runId: workdir.runId, targetSkills: workdir.targetSkills, workdirPath: workdir.workdirPath, manual }, "Skill-curation optimizer run starting");
|
|
876
|
+
// Inject the runId + token into the event so the agent core can pick
|
|
877
|
+
// them up. The standard executor path runs from here.
|
|
878
|
+
const enriched = {
|
|
879
|
+
...event,
|
|
880
|
+
data: {
|
|
881
|
+
...event.data,
|
|
882
|
+
skill_curation_run_id: workdir.runId,
|
|
883
|
+
skill_curation_run_token: workdir.runToken,
|
|
884
|
+
skill_curation_workdir: workdir.workdirPath,
|
|
885
|
+
skill_curation_target_skills: workdir.targetSkills,
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
await this.executeDefault(enriched);
|
|
889
|
+
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
logger.error({ err, runId: workdir?.runId }, "Skill-curation routine failed");
|
|
892
|
+
throw err;
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
const teardown = this.getTeardownOptimizerWorkdir();
|
|
896
|
+
if (workdir && teardown) {
|
|
897
|
+
try {
|
|
898
|
+
teardown(workdir.workdirPath);
|
|
899
|
+
}
|
|
900
|
+
catch (err) {
|
|
901
|
+
logger.warn({ err, workdirPath: workdir.workdirPath }, "Skill-curation workdir teardown failed");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async executeDefault(event) {
|
|
907
|
+
// ROUTINE_DATA_ACQUISITION_DESIGN.md Phase 4 / D4 — pre-pass for
|
|
908
|
+
// routine events whose ProcessKey appears in `ROUTINE_WINDOWS`
|
|
909
|
+
// (today_refresh, evening_review, weekly_review). The hourly_check
|
|
910
|
+
// and morning_routine dispatch paths attach their own
|
|
911
|
+
// `fetchReportBlock` upstream (D2 / D3); we honour an existing
|
|
912
|
+
// attachment to avoid double-spawning the fetcher. `monthly_review`
|
|
913
|
+
// has zero rows and short-circuits inside the runner.
|
|
914
|
+
//
|
|
915
|
+
// skill_curation / roadmap_refresh / user_profile_sweep are not in
|
|
916
|
+
// `ROUTINE_WINDOWS`, so `routineWindowKeyFromEvent` returns null
|
|
917
|
+
// and the pre-pass is skipped without touching the runner.
|
|
918
|
+
let effectiveEvent = event;
|
|
919
|
+
if (isRoutineEvent(event)) {
|
|
920
|
+
const routineKey = routineWindowKeyFromEvent(event);
|
|
921
|
+
const alreadyPrepassed = typeof event.data?.fetchReportBlock === "string";
|
|
922
|
+
if (routineKey && !alreadyPrepassed && routineHasWindows(routineKey)) {
|
|
923
|
+
const prepass = await this.fetchWindowRunner.run(event, routineKey);
|
|
924
|
+
effectiveEvent = {
|
|
925
|
+
...event,
|
|
926
|
+
data: {
|
|
927
|
+
...event.data,
|
|
928
|
+
fetchReportBlock: prepass.block,
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const context = await this.contextBuilder.build(effectiveEvent);
|
|
934
|
+
const processKey = resolveProcessKey(effectiveEvent);
|
|
935
|
+
// Honour run-now's `requestedModel` hint for routine events. Other event
|
|
936
|
+
// types (messages, scheduled.task) have their own dedicated paths that
|
|
937
|
+
// already handle tier selection, so this branch is routine-only.
|
|
938
|
+
const routineHint = isRoutineEvent(effectiveEvent) && effectiveEvent.requestedModel
|
|
939
|
+
? effectiveEvent.requestedModel === "opus"
|
|
940
|
+
? "high"
|
|
941
|
+
: "medium"
|
|
942
|
+
: undefined;
|
|
943
|
+
// Knowledge-import events carry the dashboard form's backend/model
|
|
944
|
+
// pick. Honor the (backendId, modelId) pair only when the event was
|
|
945
|
+
// emitted by the dashboard route — same defense-in-depth gate as the
|
|
946
|
+
// chat picker — so a malformed event from another path cannot pin a
|
|
947
|
+
// specific model.
|
|
948
|
+
const importOverride = isKnowledgeImportEvent(effectiveEvent)
|
|
949
|
+
&& effectiveEvent.platform === "dashboard"
|
|
950
|
+
&& effectiveEvent.requestedBackendId
|
|
951
|
+
&& effectiveEvent.requestedModelId
|
|
952
|
+
? {
|
|
953
|
+
requestedBackendId: effectiveEvent.requestedBackendId,
|
|
954
|
+
requestedModelId: effectiveEvent.requestedModelId,
|
|
955
|
+
}
|
|
956
|
+
: undefined;
|
|
957
|
+
const binding = this.agentRouter.resolveBinding(effectiveEvent, {
|
|
958
|
+
processKey,
|
|
959
|
+
...(routineHint ? { requestedTier: routineHint } : {}),
|
|
960
|
+
...(importOverride ?? {}),
|
|
961
|
+
});
|
|
962
|
+
const reassemblePrompt = (bid) => this.prompt.assemble(effectiveEvent.type, processKey, bid);
|
|
963
|
+
const prompt = reassemblePrompt(binding.main.backendId);
|
|
964
|
+
// P22 §3.4 step 4 — optimizer agent runs with a hard-clamped tool
|
|
965
|
+
// envelope. The check is on event type rather than processKey so the
|
|
966
|
+
// override is impossible to widen by accident from a downstream
|
|
967
|
+
// dispatch refactor; the only path to skill_curation execution is
|
|
968
|
+
// through `routine.skill_curation` events, which have no other code
|
|
969
|
+
// path that strips the override.
|
|
970
|
+
const skillCurationOverride = isRoutineEvent(effectiveEvent) && effectiveEvent.routine === "skill_curation"
|
|
971
|
+
? SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS
|
|
972
|
+
: undefined;
|
|
973
|
+
if (skillCurationOverride
|
|
974
|
+
&& !this.clampSupportedByBackend(processKey, binding.main.backendId, effectiveEvent.correlationId, "SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS")) {
|
|
975
|
+
// Refuse-at-execute. The skill curation routine has no schedule
|
|
976
|
+
// row to mark (it runs from the optimizer cron), so the audit row
|
|
977
|
+
// + log line written by the guard are the entire signal.
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const result = await this.errorRouter.executeWithRetry(() => this.agentRouter.execute({
|
|
981
|
+
prompt,
|
|
982
|
+
context,
|
|
983
|
+
event: effectiveEvent,
|
|
984
|
+
processKey,
|
|
985
|
+
preResolvedBinding: binding,
|
|
986
|
+
reassemblePrompt,
|
|
987
|
+
...(skillCurationOverride
|
|
988
|
+
? { allowedToolsOverride: skillCurationOverride }
|
|
989
|
+
: {}),
|
|
990
|
+
}), effectiveEvent);
|
|
991
|
+
await this.resultProcessor.processResult(result, effectiveEvent);
|
|
992
|
+
}
|
|
993
|
+
/** Bridge for `MorningRoutineRunner`'s `formatSqliteDatetime` use. */
|
|
994
|
+
static formatScheduledFor(date) {
|
|
995
|
+
return formatSqliteDatetime(date);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
//# sourceMappingURL=dispatcher-scheduled-tasks.js.map
|