Parent Epic
#11831 — Sub 20 of Epic. Consistency follow-up after Sub 15 (#11858 / PR #11863) established the scheduling/<task>.mjs pure-function pattern. Without this Sub, the existing 3 services remain class-instances while Subs 15/16/17 land as pure functions — Sub 18 (#11862) wire-up would need to handle two consumer shapes.
Premise
Sub 15 (PR #11863) discovered through 6 iterations that the orchestrator-internal due-trigger projections should be pure functions in scheduling/<task>.mjs, NOT class-based services. Three existing services match the wrong-shape precedent I copied initially:
ai/daemons/orchestrator/services/SummarizationCoordinatorService.mjs — class extends Base + singleton: true + Neo.setupClass, but the class has no reactive configs, no lifecycle, no Neo class-system features beyond namespace registration. Just wraps buildSummaryTrigger pure function + getDueTask adapter
ai/daemons/orchestrator/services/BackupCoordinatorService.mjs — same anti-pattern
ai/daemons/orchestrator/services/PrimaryRepoSyncService.mjs — partially: getDueTask is the projection (should move to scheduling/); runTask may be genuinely service-shaped (state + execution) and stay in services/ — verify per-file
Operator's KISS framing: complexity belongs where it's earned. These services use zero Neo class-system features and should drop the machinery.
Prescription
For each of the 3:
SummarizationCoordinatorService.mjs → scheduling/summary.mjs:
- Move
buildSummaryTrigger (pure function — genuinely earned, has 2 sources + handover-priority branching) + getDueTask({db, state, now, summarySweepIntervalMs, getUnreadSunsetHandoversFn, markNodesAsReadFn, log}) to scheduling/summary.mjs
- Drop class + singleton + Neo.setupClass
- Move spec to
test/.../scheduling/summary.spec.mjs
- Delete
services/SummarizationCoordinatorService.mjs
BackupCoordinatorService.mjs → scheduling/backup.mjs:
- Same shape — move
getDueTask to pure function
- Move spec, delete services/ file
PrimaryRepoSyncService.mjs split (verify shape first):
- If
runTask has genuine state/execution responsibilities, keep services/PrimaryRepoSyncService.mjs for just runTask; move getDueTask to scheduling/primaryDevSync.mjs
- If
runTask is also a thin wrapper, fold both into scheduling/primaryDevSync.mjs and delete services/ file
Acceptance Criteria
ai/daemons/orchestrator/scheduling/summary.mjs + backup.mjs + primaryDevSync.mjs ship as pure-function modules
- Each spec moves to
test/.../scheduling/<task>.spec.mjs with Coordinator/Service naming dropped
- Orchestrator's imports update:
import {getDueTask as summaryGetDueTask} from './scheduling/summary.mjs' etc. (currently the orchestrator uses the singleton-method-call shape SummarizationCoordinatorService.getDueTask({...}))
- Delete
services/SummarizationCoordinatorService.mjs + services/BackupCoordinatorService.mjs (and services/PrimaryRepoSyncService.mjs if fully migrated; trim if only runTask remains)
- All existing unit tests for the migrated services pass at the new locations + new shapes
- No JSDoc decay-prone references (no line numbers, no section labels, no neighboring-file method names — per Sub 15 lesson)
Avoided Traps
- DON'T just rename without dropping the class machinery. The point is removing
extends Base + singleton: true + Neo.setupClass, not just relocating
- DON'T preserve
Neo.gatekeep registration "for tooling discoverability" — pure-function modules don't need namespace registration; consumers import directly
- DON'T fold all 3 into one file. Per-task files preserve parallel-reviewability + match the precedent Sub 15 established
- DON'T add JSDoc cross-references that decay (per Sub 15 final lesson)
Depends on
Epic #11831. Sub 15 #11858 (precedent — merged via PR #11863).
Unblocks
Sub 18 #11862 — Sub 18 can now consume ALL coordinators via the same import shape (import {getDueTask as X} from './scheduling/X.mjs') instead of needing branched logic for class-instances vs pure functions.
Authority
Operator KISS framing in #11863 merge feedback: complexity belongs where it's earned; the 3 existing services use zero Neo class-system features and should drop the machinery for consistency with Sub 15's pure-function pattern.
Authored by: [Claude Opus 4.7] (Claude Code)
Parent Epic
#11831 — Sub 20 of Epic. Consistency follow-up after Sub 15 (#11858 / PR #11863) established the
scheduling/<task>.mjspure-function pattern. Without this Sub, the existing 3 services remain class-instances while Subs 15/16/17 land as pure functions — Sub 18 (#11862) wire-up would need to handle two consumer shapes.Premise
Sub 15 (PR #11863) discovered through 6 iterations that the orchestrator-internal due-trigger projections should be pure functions in
scheduling/<task>.mjs, NOT class-based services. Three existing services match the wrong-shape precedent I copied initially:ai/daemons/orchestrator/services/SummarizationCoordinatorService.mjs— class extends Base + singleton: true + Neo.setupClass, but the class has no reactive configs, no lifecycle, no Neo class-system features beyond namespace registration. Just wrapsbuildSummaryTriggerpure function +getDueTaskadapterai/daemons/orchestrator/services/BackupCoordinatorService.mjs— same anti-patternai/daemons/orchestrator/services/PrimaryRepoSyncService.mjs— partially:getDueTaskis the projection (should move to scheduling/);runTaskmay be genuinely service-shaped (state + execution) and stay in services/ — verify per-fileOperator's KISS framing: complexity belongs where it's earned. These services use zero Neo class-system features and should drop the machinery.
Prescription
For each of the 3:
SummarizationCoordinatorService.mjs → scheduling/summary.mjs:
buildSummaryTrigger(pure function — genuinely earned, has 2 sources + handover-priority branching) +getDueTask({db, state, now, summarySweepIntervalMs, getUnreadSunsetHandoversFn, markNodesAsReadFn, log})toscheduling/summary.mjstest/.../scheduling/summary.spec.mjsservices/SummarizationCoordinatorService.mjsBackupCoordinatorService.mjs → scheduling/backup.mjs:
getDueTaskto pure functionPrimaryRepoSyncService.mjs split (verify shape first):
runTaskhas genuine state/execution responsibilities, keepservices/PrimaryRepoSyncService.mjsfor justrunTask; movegetDueTasktoscheduling/primaryDevSync.mjsrunTaskis also a thin wrapper, fold both intoscheduling/primaryDevSync.mjsand delete services/ fileAcceptance Criteria
ai/daemons/orchestrator/scheduling/summary.mjs+backup.mjs+primaryDevSync.mjsship as pure-function modulestest/.../scheduling/<task>.spec.mjswithCoordinator/Servicenaming droppedimport {getDueTask as summaryGetDueTask} from './scheduling/summary.mjs'etc. (currently the orchestrator uses the singleton-method-call shapeSummarizationCoordinatorService.getDueTask({...}))services/SummarizationCoordinatorService.mjs+services/BackupCoordinatorService.mjs(andservices/PrimaryRepoSyncService.mjsif fully migrated; trim if onlyrunTaskremains)Avoided Traps
extends Base+singleton: true+Neo.setupClass, not just relocatingNeo.gatekeepregistration "for tooling discoverability" — pure-function modules don't need namespace registration; consumers import directlyDepends on
Epic #11831. Sub 15 #11858 (precedent — merged via PR #11863).
Unblocks
Sub 18 #11862 — Sub 18 can now consume ALL coordinators via the same import shape (
import {getDueTask as X} from './scheduling/X.mjs') instead of needing branched logic for class-instances vs pure functions.Authority
Operator KISS framing in #11863 merge feedback: complexity belongs where it's earned; the 3 existing services use zero Neo class-system features and should drop the machinery for consistency with Sub 15's pure-function pattern.
Authored by: [Claude Opus 4.7] (Claude Code)