@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.
Files changed (249) hide show
  1. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  2. package/dist/adapters/whatsapp-adapter.js +0 -1
  3. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  4. package/dist/api/integration-route-gate.d.ts +15 -11
  5. package/dist/api/integration-route-gate.d.ts.map +1 -1
  6. package/dist/api/integration-route-gate.js +60 -23
  7. package/dist/api/integration-route-gate.js.map +1 -1
  8. package/dist/api/json-body.d.ts +22 -7
  9. package/dist/api/json-body.d.ts.map +1 -1
  10. package/dist/api/json-body.js +27 -8
  11. package/dist/api/json-body.js.map +1 -1
  12. package/dist/api/routes/agent.d.ts.map +1 -1
  13. package/dist/api/routes/agent.js +18 -0
  14. package/dist/api/routes/agent.js.map +1 -1
  15. package/dist/api/routes/backends.d.ts.map +1 -1
  16. package/dist/api/routes/backends.js +96 -1
  17. package/dist/api/routes/backends.js.map +1 -1
  18. package/dist/api/routes/books.js +1 -1
  19. package/dist/api/routes/books.js.map +1 -1
  20. package/dist/api/routes/context.d.ts.map +1 -1
  21. package/dist/api/routes/context.js +13 -1
  22. package/dist/api/routes/context.js.map +1 -1
  23. package/dist/api/routes/dashboard.d.ts.map +1 -1
  24. package/dist/api/routes/dashboard.js +75 -5
  25. package/dist/api/routes/dashboard.js.map +1 -1
  26. package/dist/api/routes/github.d.ts.map +1 -1
  27. package/dist/api/routes/github.js +38 -5
  28. package/dist/api/routes/github.js.map +1 -1
  29. package/dist/api/routes/integrations.d.ts +35 -6
  30. package/dist/api/routes/integrations.d.ts.map +1 -1
  31. package/dist/api/routes/integrations.js +191 -16
  32. package/dist/api/routes/integrations.js.map +1 -1
  33. package/dist/api/routes/mail.d.ts.map +1 -1
  34. package/dist/api/routes/mail.js +112 -46
  35. package/dist/api/routes/mail.js.map +1 -1
  36. package/dist/api/routes/observations.d.ts.map +1 -1
  37. package/dist/api/routes/observations.js +161 -8
  38. package/dist/api/routes/observations.js.map +1 -1
  39. package/dist/api/routes/setup-migrate.d.ts +9 -1
  40. package/dist/api/routes/setup-migrate.d.ts.map +1 -1
  41. package/dist/api/routes/setup-migrate.js +4 -2
  42. package/dist/api/routes/setup-migrate.js.map +1 -1
  43. package/dist/api/routes/skills.d.ts.map +1 -1
  44. package/dist/api/routes/skills.js +39 -1
  45. package/dist/api/routes/skills.js.map +1 -1
  46. package/dist/api/routes/voice.d.ts.map +1 -1
  47. package/dist/api/routes/voice.js +62 -4
  48. package/dist/api/routes/voice.js.map +1 -1
  49. package/dist/bootstrap/adapters.d.ts +109 -0
  50. package/dist/bootstrap/adapters.d.ts.map +1 -0
  51. package/dist/bootstrap/adapters.js +237 -0
  52. package/dist/bootstrap/adapters.js.map +1 -0
  53. package/dist/bootstrap/catchup.d.ts +23 -0
  54. package/dist/bootstrap/catchup.d.ts.map +1 -0
  55. package/dist/bootstrap/catchup.js +124 -0
  56. package/dist/bootstrap/catchup.js.map +1 -0
  57. package/dist/bootstrap/schedule-helpers.d.ts +18 -0
  58. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
  59. package/dist/bootstrap/schedule-helpers.js +96 -0
  60. package/dist/bootstrap/schedule-helpers.js.map +1 -0
  61. package/dist/bootstrap/services.d.ts +60 -0
  62. package/dist/bootstrap/services.d.ts.map +1 -0
  63. package/dist/bootstrap/services.js +209 -0
  64. package/dist/bootstrap/services.js.map +1 -0
  65. package/dist/core/backends/backend-router.d.ts +23 -0
  66. package/dist/core/backends/backend-router.d.ts.map +1 -1
  67. package/dist/core/backends/backend-router.js +48 -3
  68. package/dist/core/backends/backend-router.js.map +1 -1
  69. package/dist/core/backends/claude-auth.d.ts +70 -0
  70. package/dist/core/backends/claude-auth.d.ts.map +1 -0
  71. package/dist/core/backends/claude-auth.js +198 -0
  72. package/dist/core/backends/claude-auth.js.map +1 -0
  73. package/dist/core/backends/claude-code-core.d.ts +47 -119
  74. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  75. package/dist/core/backends/claude-code-core.js +112 -1565
  76. package/dist/core/backends/claude-code-core.js.map +1 -1
  77. package/dist/core/backends/claude-delegated.d.ts +86 -0
  78. package/dist/core/backends/claude-delegated.d.ts.map +1 -0
  79. package/dist/core/backends/claude-delegated.js +801 -0
  80. package/dist/core/backends/claude-delegated.js.map +1 -0
  81. package/dist/core/backends/claude-errors.d.ts +39 -0
  82. package/dist/core/backends/claude-errors.d.ts.map +1 -0
  83. package/dist/core/backends/claude-errors.js +71 -0
  84. package/dist/core/backends/claude-errors.js.map +1 -0
  85. package/dist/core/backends/claude-probe.d.ts +103 -0
  86. package/dist/core/backends/claude-probe.d.ts.map +1 -0
  87. package/dist/core/backends/claude-probe.js +336 -0
  88. package/dist/core/backends/claude-probe.js.map +1 -0
  89. package/dist/core/backends/claude-tool-collection.d.ts +135 -0
  90. package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
  91. package/dist/core/backends/claude-tool-collection.js +831 -0
  92. package/dist/core/backends/claude-tool-collection.js.map +1 -0
  93. package/dist/core/backends/gemini-cli-core.d.ts +21 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  95. package/dist/core/backends/gemini-cli-core.js +84 -6
  96. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  97. package/dist/core/backends/prompt-utils.d.ts +1 -0
  98. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  99. package/dist/core/backends/prompt-utils.js +60 -3
  100. package/dist/core/backends/prompt-utils.js.map +1 -1
  101. package/dist/core/context-builder.d.ts +36 -12
  102. package/dist/core/context-builder.d.ts.map +1 -1
  103. package/dist/core/context-builder.js +179 -89
  104. package/dist/core/context-builder.js.map +1 -1
  105. package/dist/core/dispatcher-date-utils.d.ts +49 -0
  106. package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
  107. package/dist/core/dispatcher-date-utils.js +132 -0
  108. package/dist/core/dispatcher-date-utils.js.map +1 -0
  109. package/dist/core/dispatcher-error-handling.d.ts +159 -0
  110. package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
  111. package/dist/core/dispatcher-error-handling.js +393 -0
  112. package/dist/core/dispatcher-error-handling.js.map +1 -0
  113. package/dist/core/dispatcher-hourly-check.d.ts +150 -0
  114. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
  115. package/dist/core/dispatcher-hourly-check.js +665 -0
  116. package/dist/core/dispatcher-hourly-check.js.map +1 -0
  117. package/dist/core/dispatcher-message-handler.d.ts +170 -0
  118. package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
  119. package/dist/core/dispatcher-message-handler.js +1054 -0
  120. package/dist/core/dispatcher-message-handler.js.map +1 -0
  121. package/dist/core/dispatcher-morning-routine.d.ts +169 -0
  122. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
  123. package/dist/core/dispatcher-morning-routine.js +434 -0
  124. package/dist/core/dispatcher-morning-routine.js.map +1 -0
  125. package/dist/core/dispatcher-prompt.d.ts +107 -0
  126. package/dist/core/dispatcher-prompt.d.ts.map +1 -0
  127. package/dist/core/dispatcher-prompt.js +227 -0
  128. package/dist/core/dispatcher-prompt.js.map +1 -0
  129. package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
  130. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
  131. package/dist/core/dispatcher-repository-helpers.js +86 -0
  132. package/dist/core/dispatcher-repository-helpers.js.map +1 -0
  133. package/dist/core/dispatcher-result-processor.d.ts +145 -0
  134. package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
  135. package/dist/core/dispatcher-result-processor.js +414 -0
  136. package/dist/core/dispatcher-result-processor.js.map +1 -0
  137. package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
  138. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
  139. package/dist/core/dispatcher-scheduled-tasks.js +998 -0
  140. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
  141. package/dist/core/dispatcher-types.d.ts +296 -0
  142. package/dist/core/dispatcher-types.d.ts.map +1 -0
  143. package/dist/core/dispatcher-types.js +106 -0
  144. package/dist/core/dispatcher-types.js.map +1 -0
  145. package/dist/core/dispatcher.d.ts +86 -610
  146. package/dist/core/dispatcher.d.ts.map +1 -1
  147. package/dist/core/dispatcher.js +293 -3542
  148. package/dist/core/dispatcher.js.map +1 -1
  149. package/dist/core/integration-health.d.ts +18 -10
  150. package/dist/core/integration-health.d.ts.map +1 -1
  151. package/dist/core/integration-health.js +31 -1
  152. package/dist/core/integration-health.js.map +1 -1
  153. package/dist/core/integration-lifecycle.d.ts +65 -0
  154. package/dist/core/integration-lifecycle.d.ts.map +1 -1
  155. package/dist/core/integration-lifecycle.js +167 -16
  156. package/dist/core/integration-lifecycle.js.map +1 -1
  157. package/dist/core/integration-main-backend.d.ts +40 -0
  158. package/dist/core/integration-main-backend.d.ts.map +1 -1
  159. package/dist/core/integration-main-backend.js +89 -2
  160. package/dist/core/integration-main-backend.js.map +1 -1
  161. package/dist/core/management-md.d.ts +51 -17
  162. package/dist/core/management-md.d.ts.map +1 -1
  163. package/dist/core/management-md.js +233 -56
  164. package/dist/core/management-md.js.map +1 -1
  165. package/dist/core/output-language-policy.d.ts +74 -0
  166. package/dist/core/output-language-policy.d.ts.map +1 -0
  167. package/dist/core/output-language-policy.js +194 -0
  168. package/dist/core/output-language-policy.js.map +1 -0
  169. package/dist/core/prompts.d.ts +1 -0
  170. package/dist/core/prompts.d.ts.map +1 -1
  171. package/dist/core/prompts.js +121 -3
  172. package/dist/core/prompts.js.map +1 -1
  173. package/dist/core/repository-management-docs.d.ts +24 -0
  174. package/dist/core/repository-management-docs.d.ts.map +1 -1
  175. package/dist/core/repository-management-docs.js +210 -26
  176. package/dist/core/repository-management-docs.js.map +1 -1
  177. package/dist/core/routine-acquisition-plan.d.ts +131 -0
  178. package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
  179. package/dist/core/routine-acquisition-plan.js +268 -0
  180. package/dist/core/routine-acquisition-plan.js.map +1 -0
  181. package/dist/core/routine-fetch-window-runner.d.ts +201 -0
  182. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
  183. package/dist/core/routine-fetch-window-runner.js +661 -0
  184. package/dist/core/routine-fetch-window-runner.js.map +1 -0
  185. package/dist/core/routine-windows.d.ts +156 -0
  186. package/dist/core/routine-windows.d.ts.map +1 -0
  187. package/dist/core/routine-windows.js +330 -0
  188. package/dist/core/routine-windows.js.map +1 -0
  189. package/dist/core/skills-compiler.d.ts +11 -0
  190. package/dist/core/skills-compiler.d.ts.map +1 -1
  191. package/dist/core/skills-compiler.js +102 -13
  192. package/dist/core/skills-compiler.js.map +1 -1
  193. package/dist/core/skills-manifest.d.ts.map +1 -1
  194. package/dist/core/skills-manifest.js +26 -0
  195. package/dist/core/skills-manifest.js.map +1 -1
  196. package/dist/core/system-reset.d.ts.map +1 -1
  197. package/dist/core/system-reset.js +25 -2
  198. package/dist/core/system-reset.js.map +1 -1
  199. package/dist/db/observations.d.ts +45 -2
  200. package/dist/db/observations.d.ts.map +1 -1
  201. package/dist/db/observations.js +112 -14
  202. package/dist/db/observations.js.map +1 -1
  203. package/dist/db/schema.d.ts.map +1 -1
  204. package/dist/db/schema.js +13 -25
  205. package/dist/db/schema.js.map +1 -1
  206. package/dist/index.js +83 -610
  207. package/dist/index.js.map +1 -1
  208. package/dist/observers/delegated-sync-worker.d.ts +45 -2
  209. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  210. package/dist/observers/delegated-sync-worker.js +71 -21
  211. package/dist/observers/delegated-sync-worker.js.map +1 -1
  212. package/dist/observers/mail-poller.d.ts +12 -5
  213. package/dist/observers/mail-poller.d.ts.map +1 -1
  214. package/dist/observers/mail-poller.js +36 -14
  215. package/dist/observers/mail-poller.js.map +1 -1
  216. package/dist/observers/manager.d.ts +37 -5
  217. package/dist/observers/manager.d.ts.map +1 -1
  218. package/dist/observers/manager.js +28 -10
  219. package/dist/observers/manager.js.map +1 -1
  220. package/dist/services/delegated-backend-invoker.d.ts +1 -51
  221. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  222. package/dist/services/delegated-backend-invoker.js +41 -480
  223. package/dist/services/delegated-backend-invoker.js.map +1 -1
  224. package/dist/services/delegated-invoker-audit.d.ts +94 -0
  225. package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
  226. package/dist/services/delegated-invoker-audit.js +238 -0
  227. package/dist/services/delegated-invoker-audit.js.map +1 -0
  228. package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
  229. package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
  230. package/dist/services/delegated-invoker-cache-hits.js +104 -0
  231. package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
  232. package/dist/services/delegated-invoker-janitors.d.ts +28 -0
  233. package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
  234. package/dist/services/delegated-invoker-janitors.js +104 -0
  235. package/dist/services/delegated-invoker-janitors.js.map +1 -0
  236. package/dist/services/delegated-invoker-utils.d.ts +42 -0
  237. package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
  238. package/dist/services/delegated-invoker-utils.js +100 -0
  239. package/dist/services/delegated-invoker-utils.js.map +1 -0
  240. package/dist/services/delegated-task-runtime.d.ts +1 -1
  241. package/dist/services/delegated-task-runtime.js +1 -1
  242. package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
  243. package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
  244. package/dist/services/integrations/snapshot-partitions.js +12 -0
  245. package/dist/services/integrations/snapshot-partitions.js.map +1 -1
  246. package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
  247. package/dist/services/voice/transcriber-impl.js +7 -8
  248. package/dist/services/voice/transcriber-impl.js.map +1 -1
  249. 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