stagent 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,551 @@
1
+ # Schedule maxTurns API Control — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Expose the existing `schedules.maxTurns` column on the `create_schedule` and `update_schedule` MCP tool input schemas so operators can tune per-schedule turn budgets via chat instead of editing the DB by hand.
6
+
7
+ **Architecture:** Two Zod input-schema additions (one append to `create_schedule`, one `.optional().nullable()` field on `update_schedule`) plus a single `maxTurns: args.maxTurns ?? null` line in the insert payload. `get_schedule` already echoes the column because it does `db.select().from(schedules)` which returns every column on the row — verified, no change there. The scheduler-side plumbing (`scheduler.ts:284`, `:535`) and DB column (`schema.ts:237-239`) already exist.
8
+
9
+ **Tech Stack:** TypeScript, Zod, Drizzle ORM (SQLite), Vitest.
10
+
11
+ ---
12
+
13
+ ## What already exists
14
+
15
+ Confirmed by reading the current codebase (not trusting the spec's line numbers blindly):
16
+
17
+ | What | Where | Evidence |
18
+ |---|---|---|
19
+ | `maxTurns` column on `schedules` table | `src/lib/db/schema.ts:237-239` | `maxTurns: integer("max_turns")` with doc comment "NULL inherits the global MAX_TURNS setting" |
20
+ | Scheduler handoff schedule→task at firing time | `src/lib/schedules/scheduler.ts:535` | (unverified by this plan — spec asserts it, and nothing in this plan touches it) |
21
+ | Firing metrics capture `maxTurnsAtFiring` | `src/lib/schedules/scheduler.ts:284` | (same — don't touch) |
22
+ | `get_schedule` returns full row | `src/lib/chat/tools/schedule-tools.ts:186-191` | `db.select().from(schedules).where(eq(schedules.id, ...))` — returns every column, including any we add downstream. No schema change needed on read. |
23
+ | `create_schedule` input Zod schema | `src/lib/chat/tools/schedule-tools.ts:49-72` | Current fields: `name`, `prompt`, `interval`, `projectId`, `assignedAgent`, `agentProfile`, `maxFirings`, `expiresInHours`. Insert payload at `:139-155` uses `args.maxFirings ?? null` pattern — we mirror that for `maxTurns`. |
24
+ | `update_schedule` input Zod schema | `src/lib/chat/tools/schedule-tools.ts:205-219` | Uses conditional-set pattern `if (args.X !== undefined) updates.X = args.X` at `:230-235`. No existing field in this file currently supports explicit-null-to-clear; we introduce the pattern via `.optional().nullable()` on `maxTurns`. |
25
+ | `defineTool` factory used by all chat tools | `src/lib/chat/tool-registry.ts:42-52` | Returns `{name, description, zodShape, inputSchema, handler}`. Tests can look up a tool by name and invoke `tool.handler(args)` directly after validating with `z.object(tool.zodShape).safeParse(args)`. |
26
+ | Test pattern for mocking `@/lib/db` + `drizzle-orm` | `src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts:10-54` | Uses `vi.hoisted` + thenable query-builder stub. We can reuse the same shape, extending it with `.insert()` and `.update()` spy methods. |
27
+
28
+ ## NOT in scope
29
+
30
+ - **`maxRunDurationSec` parallel control** (column at `schema.ts:241`). Same shape of problem, but explicitly out per spec's Scope Boundaries. File separately if wanted.
31
+ - **UI surface changes** in `src/components/schedules/`. Spec says "chat-tool access only for now."
32
+ - **Global-default admin setting overrides.** Spec Scope Boundaries.
33
+ - **Migrating historical schedules with `maxTurns: null`.** Spec Scope Boundaries — nulls fall back to system default by design.
34
+ - **Changing the system default `MAX_TURNS`.** Out of scope.
35
+ - **Smoke test against a running dev server.** Not required per TDR-032 / writing-plans override — this file is pure Zod schema additions, no static imports change, no runtime-registry adjacency. Unit tests are sufficient.
36
+
37
+ ## Error & Rescue Registry
38
+
39
+ | Failure mode | Recovery |
40
+ |---|---|
41
+ | Zod accepts out-of-range value (10 > N or N > 500) | `.min(10).max(500)` in the Zod field. Unit test asserts rejection at 9 and 501. |
42
+ | Operator passes `maxTurns: null` on update and the DB write silently drops it | The `.optional().nullable()` Zod schema permits `null`. The conditional-set pattern `if (args.maxTurns !== undefined) updates.maxTurns = args.maxTurns` then writes an explicit `null`. Unit test asserts `updates.maxTurns === null` after a clear-to-null call. |
43
+ | Operator omits `maxTurns` on update and the existing value gets clobbered to null | Same conditional-set pattern — `args.maxTurns` is `undefined` (not `null`), the `!== undefined` guard skips the write, existing value untouched. Unit test asserts `updates.maxTurns` is not set when the field is omitted. |
44
+ | Test file creation collides with existing test file | `ls src/lib/chat/tools/__tests__/` confirms `schedule-tools.test.ts` does not exist. Create fresh. |
45
+ | Zod `.nullable()` interacts unexpectedly with `.optional()` | Both must be present. `.optional()` alone: undefined OK, null rejected. `.nullable()` alone: null OK, undefined required. `.optional().nullable()`: both undefined and null are valid. The persistence test for clear-to-null catches this. |
46
+
47
+ ---
48
+
49
+ ## File Structure
50
+
51
+ Files modified:
52
+
53
+ - `src/lib/chat/tools/schedule-tools.ts` — add `maxTurns` to two Zod schemas, add one line to the create insert payload.
54
+
55
+ Files created:
56
+
57
+ - `src/lib/chat/tools/__tests__/schedule-tools.test.ts` — fresh test file with Zod-validation tests (no DB mock needed) + persistence tests (mocked DB, mocked drizzle operators, mocked dynamic imports).
58
+
59
+ Files NOT touched (explicitly):
60
+
61
+ - `src/lib/db/schema.ts` — column already exists
62
+ - `src/lib/db/bootstrap.ts` — column already exists in the schedules CREATE TABLE
63
+ - `src/lib/schedules/scheduler.ts` — handoff already plumbed
64
+ - Any component under `src/components/schedules/` — UI is out of scope
65
+ - `features/roadmap.md`, `features/changelog.md`, `features/schedule-maxturns-api-control.md` frontmatter — handled in the separate flip-to-completed commit per handoff rule #8
66
+
67
+ ---
68
+
69
+ ## Task 1: Zod schema additions + insert payload wiring
70
+
71
+ **Files:**
72
+ - Modify: `src/lib/chat/tools/schedule-tools.ts` (three small edits)
73
+
74
+ - [ ] **Step 1.1: Add `maxTurns` to `create_schedule` Zod input**
75
+
76
+ In `src/lib/chat/tools/schedule-tools.ts` at the existing `create_schedule` input schema (currently ends at line 72 with `expiresInHours`), append:
77
+
78
+ ```ts
79
+ maxTurns: z
80
+ .number()
81
+ .int()
82
+ .min(10)
83
+ .max(500)
84
+ .optional()
85
+ .describe("Hard cap on turns per firing (10-500). Omit to inherit the system default."),
86
+ ```
87
+
88
+ Place it immediately after the `expiresInHours` field so the create and update schemas stay structurally parallel.
89
+
90
+ - [ ] **Step 1.2: Add `maxTurns` to `update_schedule` Zod input**
91
+
92
+ Same file, at the existing `update_schedule` input schema (currently ends at line 218 with `agentProfile`), append:
93
+
94
+ ```ts
95
+ maxTurns: z
96
+ .number()
97
+ .int()
98
+ .min(10)
99
+ .max(500)
100
+ .optional()
101
+ .nullable()
102
+ .describe("Hard cap on turns per firing (10-500). Pass null to clear an override back to the system default."),
103
+ ```
104
+
105
+ The `.nullable()` is load-bearing — without it, Zod rejects explicit `null` from the client. With it, `undefined` still means "field not provided, don't touch" and `null` means "clear to inherit default."
106
+
107
+ - [ ] **Step 1.3: Thread `maxTurns` into the insert values in `create_schedule`**
108
+
109
+ In the `db.insert(schedules).values({...})` call (currently lines 139-155), add a single line alongside the existing `maxFirings: args.maxFirings ?? null`:
110
+
111
+ ```ts
112
+ maxTurns: args.maxTurns ?? null,
113
+ ```
114
+
115
+ Do not change anything else in that call. Do not reorder fields.
116
+
117
+ - [ ] **Step 1.4: Thread `maxTurns` into the conditional-set block in `update_schedule`**
118
+
119
+ In the `updates` construction (currently lines 230-235, right after `if (args.agentProfile !== undefined) updates.agentProfile = args.agentProfile;`), add:
120
+
121
+ ```ts
122
+ if (args.maxTurns !== undefined) updates.maxTurns = args.maxTurns;
123
+ ```
124
+
125
+ The `!== undefined` check is deliberate — it distinguishes "field omitted" (undefined, skip) from "explicit clear" (null, write). Do not collapse this to a truthy check.
126
+
127
+ - [ ] **Step 1.5: Type check**
128
+
129
+ Run: `npx tsc --noEmit 2>&1 | tail -5; echo exit=$?`
130
+
131
+ Expected: `exit=0`, or if there are pre-existing errors they should be at the handoff-documented lines (`task-file lines 83/407-410/431/668/669`) and completely unrelated to `schedule-tools.ts`.
132
+
133
+ ---
134
+
135
+ ## Task 2: Unit tests
136
+
137
+ **Files:**
138
+ - Create: `src/lib/chat/tools/__tests__/schedule-tools.test.ts`
139
+
140
+ - [ ] **Step 2.1: Write the test scaffold with mocks**
141
+
142
+ Create `src/lib/chat/tools/__tests__/schedule-tools.test.ts` with:
143
+
144
+ ```ts
145
+ import { describe, it, expect, vi, beforeEach } from "vitest";
146
+ import { z } from "zod";
147
+
148
+ interface ScheduleRow {
149
+ id: string;
150
+ maxTurns: number | null;
151
+ [key: string]: unknown;
152
+ }
153
+
154
+ const { mockState } = vi.hoisted(() => ({
155
+ mockState: {
156
+ rows: [] as ScheduleRow[],
157
+ lastInsertValues: null as Record<string, unknown> | null,
158
+ lastUpdateValues: null as Record<string, unknown> | null,
159
+ },
160
+ }));
161
+
162
+ // Minimal drizzle query builder — supports select/insert/update chains
163
+ // used by schedule-tools.ts. Insert + update calls record their payloads
164
+ // into mockState for assertions.
165
+ vi.mock("@/lib/db", () => {
166
+ const selectBuilder = {
167
+ from() { return this; },
168
+ where() { return this; },
169
+ orderBy() { return this; },
170
+ limit() { return this; },
171
+ get() { return Promise.resolve(mockState.rows[0]); },
172
+ then<TResolve>(resolve: (rows: ScheduleRow[]) => TResolve) {
173
+ return Promise.resolve(mockState.rows).then(resolve);
174
+ },
175
+ };
176
+ return {
177
+ db: {
178
+ select: () => selectBuilder,
179
+ insert: () => ({
180
+ values: (v: Record<string, unknown>) => {
181
+ mockState.lastInsertValues = v;
182
+ mockState.rows = [{ id: "sched-1", maxTurns: null, ...v } as ScheduleRow];
183
+ return Promise.resolve();
184
+ },
185
+ }),
186
+ update: () => ({
187
+ set: (v: Record<string, unknown>) => {
188
+ mockState.lastUpdateValues = v;
189
+ mockState.rows[0] = { ...mockState.rows[0], ...v } as ScheduleRow;
190
+ return { where: () => Promise.resolve() };
191
+ },
192
+ }),
193
+ delete: () => ({ where: () => Promise.resolve() }),
194
+ },
195
+ };
196
+ });
197
+
198
+ vi.mock("@/lib/db/schema", () => ({
199
+ schedules: {
200
+ id: "id",
201
+ status: "status",
202
+ projectId: "projectId",
203
+ updatedAt: "updatedAt",
204
+ cronExpression: "cronExpression",
205
+ },
206
+ }));
207
+
208
+ vi.mock("drizzle-orm", () => ({
209
+ eq: () => ({}),
210
+ and: () => ({}),
211
+ desc: () => ({}),
212
+ }));
213
+
214
+ // Dynamic imports inside the tool handlers — mock each.
215
+ vi.mock("@/lib/schedules/interval-parser", () => ({
216
+ parseInterval: () => "*/30 * * * *",
217
+ computeNextFireTime: () => new Date("2026-04-11T10:00:00Z"),
218
+ computeStaggeredCron: (cron: string) => ({
219
+ cronExpression: cron,
220
+ offsetApplied: 0,
221
+ collided: false,
222
+ }),
223
+ }));
224
+
225
+ vi.mock("@/lib/schedules/nlp-parser", () => ({
226
+ parseNaturalLanguage: () => null,
227
+ }));
228
+
229
+ vi.mock("@/lib/schedules/prompt-analyzer", () => ({
230
+ analyzePromptEfficiency: () => [],
231
+ }));
232
+
233
+ import { scheduleTools } from "../schedule-tools";
234
+
235
+ function getTool(name: string) {
236
+ const tools = scheduleTools({ projectId: "proj-1" } as never);
237
+ const tool = tools.find((t) => t.name === name);
238
+ if (!tool) throw new Error(`Tool not found: ${name}`);
239
+ return tool;
240
+ }
241
+
242
+ function parseArgs(toolName: string, args: unknown) {
243
+ const tool = getTool(toolName);
244
+ return z.object(tool.zodShape).safeParse(args);
245
+ }
246
+
247
+ beforeEach(() => {
248
+ mockState.rows = [];
249
+ mockState.lastInsertValues = null;
250
+ mockState.lastUpdateValues = null;
251
+ });
252
+ ```
253
+
254
+ - [ ] **Step 2.2: Run the scaffold to confirm mocks resolve**
255
+
256
+ Run: `npx vitest run src/lib/chat/tools/__tests__/schedule-tools.test.ts 2>&1 | tail -20`
257
+
258
+ Expected: "No test found in file" or similar — this confirms the file compiles and imports resolve. If it fails on an import error, fix the mocks before adding tests.
259
+
260
+ - [ ] **Step 2.3: Add Zod range-validation tests**
261
+
262
+ Append these describe block:
263
+
264
+ ```ts
265
+ describe("create_schedule maxTurns Zod validation", () => {
266
+ const base = {
267
+ name: "test",
268
+ prompt: "hello",
269
+ interval: "every 30 minutes",
270
+ };
271
+
272
+ it("accepts a valid maxTurns value", () => {
273
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 50 });
274
+ expect(result.success).toBe(true);
275
+ });
276
+
277
+ it("accepts omitted maxTurns (inherit default)", () => {
278
+ const result = parseArgs("create_schedule", base);
279
+ expect(result.success).toBe(true);
280
+ });
281
+
282
+ it("rejects maxTurns below 10", () => {
283
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 9 });
284
+ expect(result.success).toBe(false);
285
+ });
286
+
287
+ it("rejects maxTurns above 500", () => {
288
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 501 });
289
+ expect(result.success).toBe(false);
290
+ });
291
+
292
+ it("rejects non-integer maxTurns", () => {
293
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 50.5 });
294
+ expect(result.success).toBe(false);
295
+ });
296
+
297
+ it("rejects explicit null on create (only update supports clear-to-null)", () => {
298
+ const result = parseArgs("create_schedule", { ...base, maxTurns: null });
299
+ expect(result.success).toBe(false);
300
+ });
301
+ });
302
+
303
+ describe("update_schedule maxTurns Zod validation", () => {
304
+ const base = { scheduleId: "sched-1" };
305
+
306
+ it("accepts a valid maxTurns value", () => {
307
+ const result = parseArgs("update_schedule", { ...base, maxTurns: 100 });
308
+ expect(result.success).toBe(true);
309
+ });
310
+
311
+ it("accepts explicit null to clear an override", () => {
312
+ const result = parseArgs("update_schedule", { ...base, maxTurns: null });
313
+ expect(result.success).toBe(true);
314
+ });
315
+
316
+ it("accepts omitted maxTurns (unchanged)", () => {
317
+ const result = parseArgs("update_schedule", base);
318
+ expect(result.success).toBe(true);
319
+ });
320
+
321
+ it("rejects out-of-range maxTurns on update", () => {
322
+ const result = parseArgs("update_schedule", { ...base, maxTurns: 9 });
323
+ expect(result.success).toBe(false);
324
+ });
325
+ });
326
+ ```
327
+
328
+ - [ ] **Step 2.4: Run validation tests and verify they pass**
329
+
330
+ Run: `npx vitest run src/lib/chat/tools/__tests__/schedule-tools.test.ts 2>&1 | tail -20`
331
+
332
+ Expected: All tests in both describe blocks pass. If "rejects explicit null on create" fails, the `create_schedule` schema accidentally has `.nullable()` — remove it.
333
+
334
+ - [ ] **Step 2.5: Add persistence tests for create-with-value**
335
+
336
+ Append:
337
+
338
+ ```ts
339
+ describe("create_schedule maxTurns persistence", () => {
340
+ it("writes maxTurns to the insert payload when provided", async () => {
341
+ const tool = getTool("create_schedule");
342
+ await tool.handler({
343
+ name: "test",
344
+ prompt: "hello",
345
+ interval: "every 30 minutes",
346
+ maxTurns: 75,
347
+ });
348
+ expect(mockState.lastInsertValues).not.toBeNull();
349
+ expect(mockState.lastInsertValues?.maxTurns).toBe(75);
350
+ });
351
+
352
+ it("writes null to maxTurns when omitted (inherit default)", async () => {
353
+ const tool = getTool("create_schedule");
354
+ await tool.handler({
355
+ name: "test",
356
+ prompt: "hello",
357
+ interval: "every 30 minutes",
358
+ });
359
+ expect(mockState.lastInsertValues?.maxTurns).toBe(null);
360
+ });
361
+ });
362
+ ```
363
+
364
+ - [ ] **Step 2.6: Add persistence tests for update-to-new-value and clear-to-null**
365
+
366
+ Append:
367
+
368
+ ```ts
369
+ describe("update_schedule maxTurns persistence", () => {
370
+ beforeEach(() => {
371
+ // Seed an existing schedule row for the "get existing" path.
372
+ mockState.rows = [{
373
+ id: "sched-1",
374
+ name: "existing",
375
+ status: "active",
376
+ maxTurns: 50,
377
+ } as ScheduleRow];
378
+ });
379
+
380
+ it("writes the new maxTurns value when provided", async () => {
381
+ const tool = getTool("update_schedule");
382
+ await tool.handler({ scheduleId: "sched-1", maxTurns: 120 });
383
+ expect(mockState.lastUpdateValues?.maxTurns).toBe(120);
384
+ });
385
+
386
+ it("writes null when explicitly clearing the override", async () => {
387
+ const tool = getTool("update_schedule");
388
+ await tool.handler({ scheduleId: "sched-1", maxTurns: null });
389
+ expect(mockState.lastUpdateValues).not.toBeNull();
390
+ expect("maxTurns" in (mockState.lastUpdateValues ?? {})).toBe(true);
391
+ expect(mockState.lastUpdateValues?.maxTurns).toBe(null);
392
+ });
393
+
394
+ it("does not touch maxTurns when the field is omitted", async () => {
395
+ const tool = getTool("update_schedule");
396
+ await tool.handler({ scheduleId: "sched-1", name: "renamed" });
397
+ expect("maxTurns" in (mockState.lastUpdateValues ?? {})).toBe(false);
398
+ });
399
+ });
400
+ ```
401
+
402
+ - [ ] **Step 2.7: Run all tests and verify they pass**
403
+
404
+ Run: `npx vitest run src/lib/chat/tools/__tests__/schedule-tools.test.ts 2>&1 | tail -30`
405
+
406
+ Expected: Every test passes. Typical count: 6 (create validation) + 4 (update validation) + 2 (create persistence) + 3 (update persistence) = 15 tests.
407
+
408
+ If any persistence test fails because a dynamic import (interval-parser, nlp-parser, prompt-analyzer) returns an unexpected shape, extend the corresponding mock with whatever additional exports the handler touches. Do not change the handler.
409
+
410
+ - [ ] **Step 2.8: Type check again**
411
+
412
+ Run: `npx tsc --noEmit 2>&1 | tail -5; echo exit=$?`
413
+
414
+ Expected: `exit=0` or pre-existing errors only (see Task 1 Step 1.5).
415
+
416
+ - [ ] **Step 2.9: Run a wider sanity test to catch accidental regressions in neighboring files**
417
+
418
+ Run: `npx vitest run src/lib/chat/tools/__tests__/ 2>&1 | tail -15`
419
+
420
+ Expected: `schedule-tools.test.ts`, `enrich-table-tool.test.ts`, and `workflow-tools-dedup.test.ts` all pass. If enrich-table or workflow-tools-dedup breaks, something structural happened — stop and investigate before committing.
421
+
422
+ ---
423
+
424
+ ## Task 3: Commit
425
+
426
+ - [ ] **Step 3.1: Stage the two files and verify the diff**
427
+
428
+ Run: `git status && git diff --stat src/lib/chat/tools/schedule-tools.ts src/lib/chat/tools/__tests__/schedule-tools.test.ts`
429
+
430
+ Expected: Exactly two files changed — `schedule-tools.ts` (modified) and `schedule-tools.test.ts` (new). No other files touched.
431
+
432
+ - [ ] **Step 3.2: Commit**
433
+
434
+ ```bash
435
+ git add src/lib/chat/tools/schedule-tools.ts src/lib/chat/tools/__tests__/schedule-tools.test.ts
436
+ git commit -m "$(cat <<'EOF'
437
+ feat(chat): expose schedules.maxTurns on create/update MCP schemas
438
+
439
+ The schedules.maxTurns column, scheduler handoff, and firing metrics
440
+ already exist — only the chat-tool input schemas were missing, so
441
+ operators had no way to tune per-schedule turn budgets without direct
442
+ DB access.
443
+
444
+ Adds maxTurns (10-500, optional) to create_schedule and the same
445
+ field with .nullable() to update_schedule, so an explicit null clears
446
+ an override back to the system default. get_schedule already echoes
447
+ the column because it returns the full row.
448
+
449
+ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
450
+ EOF
451
+ )"
452
+ ```
453
+
454
+ - [ ] **Step 3.3: Verify commit landed**
455
+
456
+ Run: `git log --oneline -3`
457
+
458
+ Expected: The new commit is HEAD. `git status` is clean.
459
+
460
+ ---
461
+
462
+ ## Task 4: Flip to completed + roadmap + changelog
463
+
464
+ This is a **separate commit** per handoff rule #8, not folded into Task 3.
465
+
466
+ - [ ] **Step 4.1: Update spec frontmatter**
467
+
468
+ In `features/schedule-maxturns-api-control.md`, change the frontmatter:
469
+
470
+ ```yaml
471
+ status: planned
472
+ ```
473
+
474
+ to:
475
+
476
+ ```yaml
477
+ status: completed
478
+ ```
479
+
480
+ - [ ] **Step 4.2: Update roadmap row**
481
+
482
+ In `features/roadmap.md`, find the row for `schedule-maxturns-api-control` (likely under a Platform Hardening or post-MVP section) and flip its `Status` column from `planned` to `completed`. If the row is absent, add it in the right section.
483
+
484
+ - [ ] **Step 4.3: Prepend a changelog entry**
485
+
486
+ In `features/changelog.md`, under today's date section (`## 2026-04-11`), add under a `### Completed` subsection (create if absent):
487
+
488
+ ```markdown
489
+ - `schedule-maxturns-api-control` — exposed per-schedule maxTurns (10-500, clear-to-null) on create_schedule / update_schedule MCP tools. 15 unit tests covering Zod validation + persistence paths.
490
+ ```
491
+
492
+ If a `## 2026-04-11` section does not yet exist, create it at the top above the previous date.
493
+
494
+ - [ ] **Step 4.4: Ship verification**
495
+
496
+ Walk through the spec's Acceptance Criteria checklist and confirm each one has a concrete implementation or test:
497
+
498
+ - [ ] `create_schedule` accepts optional `maxTurns` (10-500) — Task 1 Step 1.1 + test Step 2.3
499
+ - [ ] `update_schedule` accepts same field, supports explicit null — Task 1 Step 1.2 + tests Step 2.3 + 2.6
500
+ - [ ] `get_schedule` reflects the user-set value — confirmed in "What already exists" above (no code change needed; it selects the full row)
501
+ - [ ] Scheduler threads maxTurns from schedule to task — pre-existing, not touched
502
+ - [ ] Null/unset falls back to system default — pre-existing behavior unchanged
503
+ - [ ] Out-of-range values rejected with Zod error — test Step 2.3
504
+ - [ ] Unit test covers create-with-value, update-to-new-value, clear-to-null — tests Step 2.5 + 2.6
505
+
506
+ - [ ] **Step 4.5: Commit the flip**
507
+
508
+ ```bash
509
+ git add features/schedule-maxturns-api-control.md features/roadmap.md features/changelog.md
510
+ git commit -m "$(cat <<'EOF'
511
+ docs(features): flip schedule-maxturns-api-control to completed
512
+
513
+ Chat-tool schemas for create_schedule / update_schedule now expose
514
+ the existing maxTurns column. Ship-verified against all 7 acceptance
515
+ criteria — the surface was two Zod field additions plus one insert
516
+ payload line.
517
+
518
+ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
519
+ EOF
520
+ )"
521
+ ```
522
+
523
+ - [ ] **Step 4.6: Push both commits**
524
+
525
+ Run: `git push origin main`
526
+
527
+ Expected: Two commits pushed, no hook failures. If a hook fails, **do not retry with `--no-verify`** — diagnose, fix, and push again.
528
+
529
+ - [ ] **Step 4.7: Verify remote is in sync**
530
+
531
+ Run: `git log --oneline origin/main..HEAD`
532
+
533
+ Expected: Empty output (local and remote match).
534
+
535
+ ---
536
+
537
+ ## Verification before declaring done
538
+
539
+ - [ ] `npx vitest run src/lib/chat/tools/__tests__/schedule-tools.test.ts` — all ~15 tests green
540
+ - [ ] `npx tsc --noEmit` — exit 0 or pre-existing-errors-only
541
+ - [ ] `git log --oneline -3` shows both new commits
542
+ - [ ] `git status` is clean
543
+ - [ ] Spec frontmatter matches roadmap row matches changelog entry (all three say "completed" for this feature under 2026-04-11)
544
+ - [ ] No changes landed in `src/lib/db/schema.ts`, `src/lib/db/bootstrap.ts`, `src/lib/schedules/scheduler.ts`, or any UI file
545
+
546
+ ## Self-review notes
547
+
548
+ - The `update_schedule` `.optional().nullable()` pattern is new for this file. Worth flagging in the code review that it's a deliberate introduction, not a typo.
549
+ - The test file uses a simpler query-builder stub than `workflow-tools-dedup.test.ts` because the create/update handlers don't need `.limit()` / `.orderBy()`. If the test fails because the handler chains an unexpected method, add a passthrough on the builder.
550
+ - If the handler's dynamic import of `@/lib/schedules/interval-parser` calls an export we haven't mocked (e.g. some helper other than `parseInterval` / `computeNextFireTime` / `computeStaggeredCron`), add it to the mock with a minimal stub.
551
+ - **Do not** add `maxTurns` persistence tests that require the scheduler to actually fire — that's integration scope and the spec explicitly leaves the scheduler-side code untouched (regression by existing scheduler tests).