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.
- package/README.md +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- 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).
|