Files
summercms/.planning/phases/11-jobs-realtime-and-search-infrastructure/11-02-PLAN.md
2026-09-29 14:34:06 +02:00

26 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, coupling_justified, estimate, must_haves
phase plan type wave depends_on files_modified autonomous requirements coupling_justified estimate must_haves
11-jobs-realtime-and-search-infrastructure 02 execute 2
11-01
modules/pact/capabilities.go
modules/pact/README.md
modules/bonfire/call.go
modules/bonfire/call_test.go
modules/bonfire/README.md
modules/conga/schedule.go
modules/conga/scheduler.go
modules/conga/worker.go
modules/conga/commands.go
modules/conga/schedule_test.go
modules/conga/README.md
internal/build/build.go
internal/build/build_test.go
cmd/summer/main.go
cmd/summer/runtime.go
cmd/summer/main_test.go
examples/hello/main.go
../fonoteka.go/main.go
../fonoteka.go/plugins/golem15/fonoteka/schedule.go
true
CLI-04
11-03: both extend the River worker built by 11-01 (this plan adds periodic jobs and the scheduled queue inside conga; 11-03 registers the broadcast job through the conga.Manager API without editing conga files); disjoint files, order-independent
tokens raw_tokens tasks confidence
90000 90000 2 low
truths artifacts key_links prohibitions
Per D-18, plugins declare recurring commands through a new `pact.HasSchedule` (`Schedule() []pact.ScheduledCommand`) with `pact.Daily()`, `pact.DailyAt(hour, minute)` or `pact.Every(d)` cadences, and pact still imports no River code.
Per D-18 and CLI-04, every worker (serve, queue:work) and `summer schedule:run` carries the same River PeriodicJobs built from all plugins' schedules; each scheduled run is a River job on the `scheduled` queue with MaxAttempts 1 and UniqueOpts.ByPeriod equal to the cadence period, whose worker runs the named command in-process through `bonfire.Call`.
Per RESEARCH Pattern 6, `Daily` computes the next wall-clock hh:mm in the `app.timezone` location (default UTC) and `Every(d)` the next multiple of d since local midnight, so a restart never delays a daily run by up to 24h the way PeriodicInterval(24h) would.
Per D-18, `schedule:run` runs a foreground scheduler-only River client (scheduled queue plus periodic jobs) until SIGINT/SIGTERM, and `schedule:run --once` runs, without River, every entry due in the current minute (Laravel schedule:run semantics for system cron), printing `Running scheduled command: <name>` per entry or `No scheduled commands are ready to run.`
Per D-18 and user decision 5, fonoteka declares `fonoteka:prune-notifications` daily; until Phase 14 registers that command, the scheduler logs a warning naming it and skips it, without failing boot or other entries.
Edge (CLI-04 adjacency): `Daily(h, m).Next(t)` for t exactly at h:m:00 returns the next day's h:m, and `Every(d).Next(t)` for t exactly on a multiple of d returns t+d (strictly after).
Edge (CLI-04 empty): a plugin whose Schedule() returns nil or an empty slice adds no periodic job; an entry with an empty Command, a zero Cadence, or an Every interval that does not evenly divide 24h fails worker start with an error naming the plugin id and entry index.
Edge (CLI-04 ordering): periodic jobs are registered in plugin activation order, then declaration order, with ids `<plugin id>#<index>:<command>`; entries sharing a cadence each get their own job and their relative execution order is unspecified.
Edge (CLI-04 idempotency): two enqueues of one entry inside one cadence period collapse to one River job through UniqueOpts.ByPeriod; `schedule:run --once` invoked twice in the same minute runs the due entries twice, as Laravel does (no overlap lock).
statement verification
Edge (CLI-04 concurrency): with several worker processes, River leader election means exactly one process enqueues each period; a scheduled command interrupted mid-run is not retried (MaxAttempts 1) and the next period runs normally. backstop
The generated app main publishes the final command list as a `*bonfire.Catalog` on the app before executing, so scheduled runs can call any registered app command.
path provides contains
modules/pact/capabilities.go HasSchedule, ScheduledCommand, Cadence, Daily, DailyAt, Every type HasSchedule interface
path provides contains
modules/bonfire/call.go Catalog, NewCatalog, Call, ErrUnknownCommand func Call(
path provides
modules/conga/schedule.go river.PeriodicSchedule implementations for Daily and Every
path provides contains
modules/conga/scheduler.go periodic job construction, ScheduledCommandArgs and its worker ByPeriod
path provides contains
../fonoteka.go/plugins/golem15/fonoteka/schedule.go fonoteka:prune-notifications daily entry fonoteka:prune-notifications
from to via pattern
modules/conga/worker.go modules/conga/scheduler.go StartWorker sets river.Config.PeriodicJobs from plugins' HasSchedule PeriodicJobs
from to via pattern
modules/conga/scheduler.go modules/bonfire/call.go scheduled worker resolves *bonfire.Catalog from the app and calls the command Catalog
from to via pattern
internal/build/build.go modules/bonfire/call.go generated main publishes bonfire.NewCatalog(commands) bonfire.NewCatalog
requirement_id category statement status verification
CLI-04 safety The scheduler MUST NOT execute a command name or argument list that does not come from a compiled pact.HasSchedule entry (never from config, database rows or request input) resolved test
requirement_id category statement status verification
CLI-04 safety A scheduled entry for an unregistered command MUST NOT fail boot or stop other entries; it is skipped with a warning naming the command resolved test

Phase Goal

ROADMAP Phase 11 goal (verbatim, not in user-story form): River jobs run on the correct dual-driver split, Centrifugo publishing and channel authorization match the existing server, and Typesense sync stays a re-gated pre-filter — all brought up before the API phases that depend on them.

This plan's slice: a plugin declares a daily or interval command and it runs on schedule inside summer serve, in a dedicated summer schedule:run process, or from system cron with --once (CLI-04, ROADMAP SC-2).

Add the schedule capability to pact, a `bonfire.Call` in-process command runner, and a River-periodic-job scheduler in conga with `schedule:run`, then declare fonoteka's first schedule entry.

Purpose: Laravel's registerSchedule has no Go counterpart yet; Phase 14's prune-notifications command and later maintenance jobs need it. Decisions implemented: D-18; user decision 5 (unknown command warns and skips); RESEARCH Pattern 6 and its anti-pattern on PeriodicInterval(24h). Output: pact.HasSchedule and cadences, bonfire Catalog/Call, conga Daily/Every schedules and scheduler wiring, schedule:run [--once], generated main catalog publish, summer schedule:run delegate, fonoteka schedule.go.

Repos: summercms.go (framework) and fonoteka.go (schedule.go, regenerated main.go). This plan does not edit fonoteka's plugin.go (the schedule lives in its own file so plan 11-03 can edit plugin.go in the same wave). Planning docs and code in separate commits. Never add co-author tags.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/11-jobs-realtime-and-search-infrastructure/11-CONTEXT.md @.planning/phases/11-jobs-realtime-and-search-infrastructure/11-RESEARCH.md @.planning/phases/11-jobs-realtime-and-search-infrastructure/11-01-SUMMARY.md @modules/pact/capabilities.go @modules/bonfire/root.go @modules/bonfire/command.go @modules/conga/worker.go @modules/conga/commands.go @internal/build/build.go - From plan 11-01 (read its SUMMARY for the final names): `conga.From(app) (*Manager, error)`, `(*Manager).Register(jobs ...pact.Job) error`, `conga.Job[T pact.JobArgs](fn, opts...)`, `conga.OnQueue`, `conga.MaxAttempts`, `conga.StartWorker(ctx, app, plugins, WorkerOptions{Queues})`, `(*Worker).Stop(ctx)`, `conga.RuntimeCommands(app, plugins)`, unexported riverConfig/queue-set helpers in client.go. - modules/bonfire/root.go: `NewRoot(name, commands, out)`, `NewRootIO(name, commands, in, out, errW)` builds a cobra root; `validCommandName` requires `namespace:verb` except build/dev/serve/migrate. - modules/pact/capabilities.go:368-379: the "Future capability families" comment lists HasListeners and HasSchedule; the kernel type-assert list follows it. - River v0.47.0: `river.PeriodicSchedule` = `Next(current time.Time) time.Time`; `river.NewPeriodicJob(schedule, func() (river.JobArgs, *river.InsertOpts), &river.PeriodicJobOpts{ID string, RunOnStart bool})`; `river.Config.PeriodicJobs []*river.PeriodicJob`; `river.InsertOpts.UniqueOpts.ByPeriod time.Duration`. River's periodic enqueuer runs only on the elected leader and keeps only in-memory state (restarts reset it). - Laravel reference: `$schedule->command('fonoteka:prune-notifications')->daily()` in /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php:328-331 (daily = `0 0 * * *`); PHP config/app.php timezone is UTC.

Artifacts this phase produces

(This plan's share.)

  • pact: HasSchedule (Schedule() []ScheduledCommand), ScheduledCommand{Command string; Args []string; Cadence Cadence}, Cadence (opaque struct with IsZero(), Interval() time.Duration, At() (hour, minute int, ok bool)), constructors Daily(), DailyAt(hour, minute int), Every(d time.Duration).
  • bonfire: Catalog, NewCatalog(commands []Command) *Catalog, (*Catalog).Has(name string) bool, (*Catalog).Call(ctx, name string, args []string, out io.Writer) error, Call(ctx, commands []Command, name string, args []string, out io.Writer) error, ErrUnknownCommand.
  • conga: Daily{Hour, Minute int; Loc *time.Location} and Every{Interval time.Duration; Loc *time.Location} (both Next(time.Time) time.Time), ScheduledCommandArgs{Entry, Command string; Args []string} with Kind summer.scheduled_command, queue constant QueueScheduled = "scheduled", the schedule:run [--once] command.
  • CLI: app command schedule:run (flag once, bare); summer schedule:run delegate forwarding --once.
  • Config key: app.timezone (read, default UTC).
  • Files: modules/bonfire/call.go, modules/conga/schedule.go, modules/conga/scheduler.go, ../fonoteka.go/plugins/golem15/fonoteka/schedule.go.
Task 1: A plugin's interval command runs on schedule inside a worker through bonfire.Call Plan 11-01 is executed: `go doc ./modules/conga StartWorker` exits 0 and `docker info` exits 0. modules/pact/capabilities.go, modules/pact/README.md, modules/bonfire/call.go, modules/bonfire/call_test.go, modules/bonfire/README.md, modules/conga/schedule.go, modules/conga/scheduler.go, modules/conga/worker.go, modules/conga/schedule_test.go, modules/conga/README.md, internal/build/build.go, internal/build/build_test.go, examples/hello/main.go, ../fonoteka.go/main.go modules/pact/capabilities.go (JobArgs/Job/HasJobs block and the closing "Future capability families" comment), modules/pact/README.md, modules/bonfire/root.go, modules/bonfire/command.go, modules/bonfire/README.md, modules/conga/worker.go, modules/conga/client.go, modules/conga/conga.go (Register), modules/conga/postgres_test.go (harness), internal/build/build.go (generateMain), internal/build/build_test.go, $(go env GOMODCACHE)/github.com/riverqueue/river@v0.47.0/periodic_job.go, .planning/phases/11-jobs-realtime-and-search-infrastructure/11-RESEARCH.md (Pattern 6, Anti-Patterns, Code Examples "Daily periodic schedule") (1) pact (D-18): after HasJobs add `type Cadence struct` with unexported fields (kind, hour, minute, every) and exported constructors `Daily() Cadence` (00:00, Laravel daily), `DailyAt(hour, minute int) Cadence` and `Every(d time.Duration) Cadence`, plus accessors `IsZero() bool`, `Interval() time.Duration` (24h for daily cadences) and `At() (hour, minute int, ok bool)` (ok is false for an Every cadence); `type ScheduledCommand struct { Command string; Args []string; Cadence Cadence }`; `type HasSchedule interface { Schedule() []ScheduledCommand }`. Remove HasSchedule from the "Future capability families" comment and add "HasSchedule (conga workers)" to the kernel type-assert list. No River import in pact. README Features/API reference updated in the same commit.

(2) bonfire, new modules/bonfire/call.go: var ErrUnknownCommand = errors.New("bonfire: unknown command"); func Call(ctx context.Context, commands []Command, name string, args []string, out io.Writer) error finds the command by exact name (else fmt.Errorf("%w: %q", ErrUnknownCommand, name)), builds a root with NewRootIO(name, []Command{cmd}, strings.NewReader(""), out, out) (empty stdin so prompts take their defaults), sets args append([]string{name}, args...) and runs ExecuteContext(ctx); type Catalog struct holding a copy of the commands, NewCatalog(commands []Command) *Catalog, (*Catalog).Has(name) bool, (*Catalog).Call(ctx, name, args, out) error. A smoke TestCall in call_test.go (a command receiving its args and writing to out; unknown name wraps ErrUnknownCommand). README updated.

(3) conga schedules, new modules/conga/schedule.go (RESEARCH Pattern 6): type Daily struct { Hour, Minute int; Loc *time.Location } whose Next returns the first Hour:Minute:00 in Loc strictly after now (adding a day when not after, using time.Date so DST gaps resolve the way Go normalizes them); type Every struct { Interval time.Duration; Loc *time.Location } whose Next returns local midnight plus the smallest multiple of Interval strictly after now (rolling into the next day at midnight). A helper scheduleFor(c pact.Cadence, loc *time.Location) (river.PeriodicSchedule, time.Duration, error) returns the schedule and its period, rejecting a zero cadence, a non-positive interval and an interval that does not evenly divide 24h. Location comes from app.timezone (default "UTC", time.LoadLocation; an invalid name is a start error).

(4) conga scheduler, new modules/conga/scheduler.go: const QueueScheduled = "scheduled"; type ScheduledCommandArgs struct { Entry string; Command string; Args []string } with Kind() "summer.scheduled_command". periodicJobs(app, plugins) ([]*river.PeriodicJob, map[string]pact.ScheduledCommand, error) walks plugins in activation order and each pact.HasSchedule entry in declaration order; entry id <plugin id>#<index>:<command>; an empty Command or invalid cadence is an error naming plugin id and index; each periodic job's constructor returns the args plus &river.InsertOpts{Queue: QueueScheduled, MaxAttempts: 1, UniqueOpts: river.UniqueOpts{ByPeriod: period}} with &river.PeriodicJobOpts{ID: entryID}. The built-in job (registered once through conga.Job on QueueScheduled, MaxAttempts 1) looks the Entry up in the compiled table and runs only when Command and Args match it exactly (T-11-09); it resolves *bonfire.Catalog from the app (missing catalog: warn and skip), and on errors.Is(err, bonfire.ErrUnknownCommand) logs Warn schedule: command not registered; skipping with the command attribute and returns nil (user decision 5); other command errors are logged with duration and returned (no retry because MaxAttempts is 1). Command output goes to an io.Writer that logs each line at Info with the command attribute.

(5) Wiring: in modules/conga/worker.go StartWorker registers the built-in job, adds QueueScheduled to the known queues (MaxWorkers from queue.queues.scheduled, default 1) and sets river.Config.PeriodicJobs from periodicJobs for every worker regardless of the queue filter (only the elected leader enqueues). In internal/build/build.go the generated main, after collecting plugin commands and before bonfire.NewRoot, emits if err := app.Publish(bonfire.NewCatalog(commands)); err != nil { return err }; add TestGenerateMainPublishesCommandCatalog to build_test.go; regenerate examples/hello/main.go and ../fonoteka.go/main.go with the framework CLI.

(6) Smoke test modules/conga/schedule_test.go: TestScheduleNext (Daily and Every adjacency: exactly-on-boundary returns the next occurrence; DST day in Europe/Warsaw keeps wall-clock hh:mm) and TestScheduleRunsCommand (Postgres harness: an acme test plugin declaring acme:tick with pact.Every(time.Second), a catalog holding an acme:tick command that signals a channel, StartWorker with all queues, assert the command runs within 10s; a second entry acme:missing produces the skip warning in a captured slog handler). Document the scheduler in modules/conga/README.md (Usage, API reference, CLI commands). go vet ./... && go test ./modules/pact ./modules/bonfire ./internal/build -count=1 && go test ./modules/bonfire -run '^TestCall$' -count=1 -v && go test ./modules/conga -run '^(TestScheduleNext|TestScheduleRunsCommand)$' -count=1 -v && go test ./internal/build -run '^TestGenerateMainPublishesCommandCatalog$' -count=1 -v <fails_when>Any command exits non-zero; a verbose run lacks "--- PASS" for TestCall, TestScheduleNext, TestScheduleRunsCommand or TestGenerateMainPublishesCommandCatalog, or prints "no tests to run" or "--- SKIP".</fails_when> <acceptance_criteria> - go doc ./modules/pact HasSchedule, go doc ./modules/pact ScheduledCommand, go doc ./modules/pact Every, go doc ./modules/bonfire Call and go doc ./modules/conga Daily exit 0. - grep -c 'riverqueue' modules/pact/capabilities.go prints 0. - grep -c 'ByPeriod' modules/conga/scheduler.go prints at least 1 and grep -c 'PeriodicJobs' modules/conga/worker.go prints at least 1. - grep -c 'bonfire.NewCatalog(commands)' ../fonoteka.go/main.go examples/hello/main.go prints 1 for each file. - grep -c 'HasSchedule' modules/pact/README.md prints at least 1 and grep -c 'Catalog' modules/bonfire/README.md prints at least 1. </acceptance_criteria> A plugin declares Every(1s) and its command runs through a River periodic job and bonfire.Call inside a worker, unknown commands are skipped with a warning, and every generated main publishes the command catalog.

Task 2: schedule:run works as a dedicated process and from system cron, and fonoteka declares its daily prune entry modules/conga/commands.go, modules/conga/scheduler.go, modules/conga/schedule_test.go, modules/conga/README.md, cmd/summer/main.go, cmd/summer/runtime.go, cmd/summer/main_test.go, ../fonoteka.go/plugins/golem15/fonoteka/schedule.go modules/conga/commands.go (queue:work signal loop), modules/conga/scheduler.go (Task 1), cmd/summer/runtime.go (delegateRollbackCommand, delegateQueueWorkCommand from 11-01), cmd/summer/main_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go (Plugin type and compile-time assertion list, read only), /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php (registerSchedule), .planning/phases/11-jobs-realtime-and-search-infrastructure/11-RESEARCH.md (Pattern 6 "--once", Open Question 5) - `schedule:run --once` at 00:00 app time runs a `Daily()` entry and prints `Running scheduled command: acme:tick`; at 00:01 it prints `No scheduled commands are ready to run.` - `schedule:run --once` with an `Every(5*time.Minute)` entry runs it at 10:05 and not at 10:07. - `schedule:run --once` with an unregistered command prints a warning naming it, exits 0 and still runs the other due entries. - A ScheduledCommandArgs whose Command or Args differ from its compiled Entry is logged and skipped without running anything. - Two inserts of the same periodic entry inside one period leave one river_job row (ByPeriod). (1) `schedule:run` in modules/conga/commands.go (D-18), added to RuntimeCommands: description "Run the scheduler in the foreground (or once for system cron)", bare flag `once`. Without `--once`: open the DB the conga way, start `StartWorker` with `WorkerOptions{Queues: []string{QueueScheduled}}` (the scheduled queue plus periodic jobs), print `scheduler started`, block on the SIGINT/SIGTERM loop, stop with a 10s timeout. With `--once`: no River; compute the current minute in `app.timezone` through an injectable clock (unexported, for tests), select entries whose cadence is due that minute (Daily: hour and minute match; Every(d): d at or below one minute is due every run, otherwise minutes since local midnight times 60s is a multiple of d), run each in order through the app's `*bonfire.Catalog` printing `Running scheduled command: `; an unknown command prints and logs the skip warning and continues; none due prints `No scheduled commands are ready to run.`; the exit status is the first command error, after all due entries ran.

(2) summer delegate: in cmd/summer/runtime.go add delegateScheduleRunCommand() declaring the bare once flag and forwarding --once when set; register it in cmd/summer/main.go toolCommands and extend the expected list in main_test.go.

(3) fonoteka.go (D-18, user decision 5): new plugins/golem15/fonoteka/schedule.go with var _ pact.HasSchedule = (*Plugin)(nil) and func (p *Plugin) Schedule() []pact.ScheduledCommand { return []pact.ScheduledCommand{{Command: "fonoteka:prune-notifications", Cadence: pact.Daily()}} } and a comment that the command itself ships in Phase 14 and is skipped with a warning until then. Do not edit plugin.go.

(4) Tests for the behavior list in modules/conga/schedule_test.go (TestScheduleRunOnce, TestScheduledEntryMismatchSkipped, TestScheduleUniqueByPeriod); the ByPeriod test inserts the same constructed args twice through the periodic constructor within one period and counts river_job rows. Update modules/conga/README.md CLI commands with schedule:run and --once. go vet ./... && go test ./modules/conga -run '^(TestScheduleRunOnce|TestScheduledEntryMismatchSkipped|TestScheduleUniqueByPeriod)$' -count=1 -v && go test ./cmd/summer -count=1 && go test ./... && (cd ../fonoteka.go && go vet ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/... && go test ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/...) <fails_when>Any command exits non-zero; the verbose run lacks "--- PASS" for TestScheduleRunOnce, TestScheduledEntryMismatchSkipped or TestScheduleUniqueByPeriod, or prints "no tests to run" or "--- SKIP"; any package reports FAIL.</fails_when> <acceptance_criteria> - grep -c '"schedule:run"' modules/conga/commands.go prints at least 1 and grep -c 'schedule:run' cmd/summer/main_test.go prints at least 1. - grep -c 'No scheduled commands are ready to run.' modules/conga/commands.go prints 1. - grep -c 'fonoteka:prune-notifications' ../fonoteka.go/plugins/golem15/fonoteka/schedule.go prints 1 and grep -c 'pact.Daily()' ../fonoteka.go/plugins/golem15/fonoteka/schedule.go prints 1. - (cd ../fonoteka.go &amp;&amp; SUMMER_GOLEM15__USER__JWT__SECRET=test-only-cli-secret go run . schedule:run --help) output contains "--once" (the app refuses to boot without a user JWT secret, so a test-only value is passed). </acceptance_criteria> schedule:run runs as a scheduler-only process or once per cron minute, fonoteka declares its daily prune entry (skipped with a warning until Phase 14), and both repositories pass their full suites.

<threat_model>

Trust Boundaries

Boundary Description
river_job rows (database) → scheduled-command worker Job args stored in Postgres name a command to execute in-process
System cron → schedule:run --once An external trigger runs due commands
Go module proxy → go.mod/go.sum No new module in this plan

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-11-09 Tampering scheduled-command worker high mitigate Command names come only from compiled pact.HasSchedule entries; the worker runs a job only when its Entry exists in the compiled table and Command/Args match exactly, so a forged river_job row cannot run an arbitrary command (Tasks 1-2, TestScheduledEntryMismatchSkipped).
T-11-17 Denial of Service periodic enqueue on leader failover low mitigate UniqueOpts.ByPeriod dedupes an entry within its period; wall-clock Daily/Every schedules avoid the restart drift of PeriodicInterval (Task 1-2).
T-11-18 Repudiation scheduled runs low mitigate Each run, skip and failure is logged with the command name and duration; skips of unregistered commands are Warn level (Task 1).
T-11-SC Tampering Go module installs high mitigate No dependency added; River stays pinned at v0.47.0 from plan 11-01; cron parsing is not added because Daily/Every cover the cadences.
</threat_model>
After Task 2: `go vet ./... && go test ./...` in summercms.go and the fonoteka.go full vet/test command pass; `go test ./modules/conga -run 'TestSchedule' -count=1 -v` passes; `fonoteka schedule:run --once` prints either a Running line or the no-commands line and exits 0.

<success_criteria>

  • pact.HasSchedule exists with Daily/DailyAt/Every cadences and no River import.
  • Every worker carries the periodic jobs; scheduled runs go through bonfire.Call with ByPeriod dedupe and MaxAttempts 1.
  • schedule:run runs in the foreground or --once; unknown commands warn and skip.
  • fonoteka declares fonoteka:prune-notifications daily in schedule.go.
  • pact, bonfire and conga READMEs updated in the same commits. </success_criteria>
Create `.planning/phases/11-jobs-realtime-and-search-infrastructure/11-02-SUMMARY.md` when done.