---
id: 11900
title: 'Sub 18 Phase 2: CadenceEngine.runIfDue drop + dispatch switch + Sub-13 cleanup + test collapse'
state: CLOSED
labels:
  - enhancement
  - ai
  - refactoring
  - architecture
assignees:
  - neo-gpt
createdAt: '2026-05-24T13:09:48Z'
updatedAt: '2026-06-07T03:13:34Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11900'
author: neo-opus-ada
commentsCount: 3
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-06-07T03:13:34Z'
---
# Sub 18 Phase 2: CadenceEngine.runIfDue drop + dispatch switch + Sub-13 cleanup + test collapse

## Context

Follow-up to PR #11899 (Sub 18 Phase 1: scaffolding + MBS wiring). Phase 1 split off the substrate-evolution foundation (registry + collector + picker primitives + MaintenanceBackpressureService activation) and shipped under a focused PR. Phase 2 ships the **dispatch switchover + cleanup** (mechanical work consuming the foundation) to complete ticket #11862 ACs.

This sub-ticket is a delivery-shape adjustment: Phase 1/2 split was operator-velocity-pushback-driven course correction from #11862's monolithic 5-commit Plan agent prescription. Substantive direction unchanged; mechanical execution staged for reviewer-side clarity + ship-velocity recovery.

## The Problem

#11862 ACs not satisfied by PR #11899:
- **AC1 (partial)**: CadenceEngine.runIfDue still has the executeFn parameter; pure-trigger-builder refactor per v13-path.md:117 / #11025 spec is incomplete.
- **AC2**: Orchestrator.mjs currently 611 LOC (was 892); target ≤400 LOC requires Phase 2 dispatch switch + Sub-13 cleanup.
- **AC3**: 13 mock-heavy `Orchestrator.spec.mjs` tests still present; per-module collapse pending.
- **AC6**: Sub-13 cleanup (strip class @summary historical narrative + section divider comments "=== Service-DI Class A/B/C/D ===") pending.

Phase 1's per-poll MBS binding refresh is a defensive substrate idiom that should be revisited during Phase 2 — once `cadenceEngine.runIfDue` is dropped + dispatch becomes a single `collect → pick → execute` pipeline, the per-poll refresh location can be evaluated for consolidation OR replacement with per-prop afterSet hooks.

## Architectural Reality

PR #11899 Phase 1 lands:
- `ai/daemons/orchestrator/scheduling/registry.mjs` — 7 frozen coordinator descriptors
- `ai/daemons/orchestrator/scheduling/collector.mjs` — `collectDueCandidates` pure function
- `ai/daemons/orchestrator/scheduling/picker.mjs` — `pickNextCandidate` pure function with 3-stage pipeline
- 3 spec files (23 tests)
- `Orchestrator.mjs` (892 → 611 LOC) — MBS wiring + 11 method deletes + Class B propagation

Phase 2 consumes Phase 1's primitives:
- `poll()` body currently has 7 `this.cadenceEngine.runIfDue(taskName, dueCheckFn, executeMaintenanceTask(executeFn), context)` blocks
- Phase 2 replaces these with the 3-line pipeline:
  ```js
  const {candidates, errors} = collectDueCandidates({registry: TASK_REGISTRY, context: this.buildContext()});
  // ... orchestrator-side error reporting from errors ...
  const winner = pickNextCandidate({candidates, runningTasks, policyContext: this.buildPolicyContext()});
  if (winner) await this.execute(winner);
  ```
- Add `execute(winner)` method with 4-key dispatch map (`supervised-child-process` → `runViaProcessSupervisor`; `service-runner` → primary-dev-sync runner; `in-process-async` → dream/golden-path/swarm-heartbeat envelope)
- Delete `cadenceEngine.runIfDue` from CadenceEngine.mjs + its dedicated tests
- Class @summary strip: lines 95-141 of current Orchestrator.mjs ("Service-DI 4-way classification" + "No configure(). No DEFAULT_X_*_MS" trailer) → compact 4-line @summary
- Section divider comments: 4-6 "=== Service-DI Class A/B/C/D ===" comments removed

## The Fix

5-commit sequence (each independently green per Plan agent design from #11862):

1. **C3**: Switch dispatch from `cadenceEngine.runIfDue` to `collect → pick → execute` pipeline. Add `execute(winner)` method with 4-key dispatch map. Delete `cadenceEngine.runIfDue` + 3 dedicated tests in CadenceEngine.spec. Orchestrator drops to ~430 LOC.
2. **C4**: Sub-13 cleanup. Strip class @summary historical narrative (lines 95-141, ~46 LOC). Remove 4-6 section divider comments. Replace with compact 4-line @summary citing scheduling/registry.mjs for descriptor model. Orchestrator drops to ~400 LOC (AC2 hit).
3. **C5**: Collapse 13 mock-heavy `Orchestrator.spec.mjs` tests (lines 142-413 per pre-#11862 grep). Keep 4-5 integration smoke tests through real registry+picker+MBS pipeline (one per executionKind). Test count drops from 22 → ~9 in Orchestrator.spec.mjs.

Per-poll MBS binding refresh (from PR #11899 Phase 1) re-evaluation: after C3 collapses the 7 runIfDue blocks into single 3-line pipeline, the per-poll refresh can either stay (defensive idiom; covers future test-bypass-start cases) OR migrate to a single `afterSetWriteLog` hook (cleaner but more code). Recommend stay-as-is unless review surfaces alternative.

## Acceptance Criteria

- [ ] AC1: `cadenceEngine.runIfDue` parameter `executeFn` dropped per v13-path.md:117 / #11025; signature becomes `runIfDue(taskName, dueCheckFn, context)` returning trigger or null. OR `runIfDue` deleted entirely if Phase 2 dispatch shape doesn't need a trigger-builder layer (collector calls `descriptor.getDueTask` directly).
- [ ] AC2: `Orchestrator.mjs` ≤ 400 LOC post-Phase-2 merge.
- [ ] AC3: 13 mock-heavy `Orchestrator.spec.mjs` tests collapsed; 4-5 integration smoke tests retained (one per executionKind + 1 lease-cross-daemon E2E).
- [ ] AC4 (PR #11899 retained): `collectDueCandidates` causes no observable state mutation — NEGATIVE test in collector.spec.mjs already enforces.
- [ ] AC5 (PR #11899 retained): `collectDueCandidates` contains no `switch(profile)` policy body — source-text guardrail test in collector.spec.mjs already enforces.
- [ ] AC6: Sub-13 cleanup folded in — class @summary stripped of "Service-DI 4-way classification" historical narrative + "No `configure()`. No `DEFAULT_X_*_MS`" trailer + 4-6 Class A/B/C/D section divider comments.
- [ ] AC7 (PR #11899 retained): `poll()` calls `MaintenanceBackpressureService.acquireLeaseAndExecute({...})` and `.executeWithGoldenPathDependencyGate({...})` — already wired in Phase 1.
- [ ] AC8: Phase 2 PR body includes before/after Orchestrator.mjs LOC count + Orchestrator.spec.mjs test count for mechanical AC2 / AC3 verification.

## Out of Scope

- No further Orchestrator splitting beyond ≤400 LOC target (Round-3 evaluation deferred to post-measurement per Plan agent design from #11862).
- No bootstrap split (deferred to Round-3).
- No new abstractions beyond the dispatch map (the existing registry primitives are the substrate).

## Avoided Traps

- ❌ Re-implementing per-poll MBS binding refresh logic (PR #11899 idiom should stay until proven unnecessary).
- ❌ Adding more reactive parent-prop afterSet hooks (the per-poll refresh covers more cases for less code — substrate-lesson banked in PR #11899 commit 85e6b4928).
- ❌ Breaking the `runIfDue` API mid-poll (atomic single-commit replacement preserves test green throughout).
- ❌ Aggressive test removal beyond the 13 mock-heavy identified ones (keeps integration smoke coverage).

## Depends on

PR #11899 (Sub 18 Phase 1) — must merge first to ship Phase 1 primitives that Phase 2 consumes.

## Related

- #11862 (parent — partially-resolved by PR #11899 Phase 1)
- Epic #11831 (parent epic — Orchestrator Round-2 closure)
- Discussion #11857 (origination)
- PR #11899 (Phase 1 implementation)
- Sub 19 #11861 (MaintenanceBackpressureService — merged, activated in Phase 1)

Origin Session ID: ba62643a-ae78-41b2-9ce5-e7890155760b

Handoff Retrieval Hints:
- Query Memory Core for `Sub 18 Phase 1 PR #11899 scaffolding MBS wiring per-poll refresh`
- Query Memory Core for `Sub 18 Plan agent 5-commit design`
- Branch: `tobiu/11862-orchestrator-sub-18` continues here after Phase 1 merge

**Authored by:** Claude Opus 4.7 (Claude Code)

## Timeline

- 2026-05-24T13:09:49Z @neo-opus-ada added the `enhancement` label
- 2026-05-24T13:09:50Z @neo-opus-ada added the `ai` label
- 2026-05-24T13:09:50Z @neo-opus-ada added the `refactoring` label
- 2026-05-24T13:09:50Z @neo-opus-ada added the `architecture` label
- 2026-05-24T13:16:27Z @neo-gpt cross-referenced by PR #11899
- 2026-05-24T13:25:54Z @neo-opus-ada cross-referenced by PR #11901
### @neo-opus-ada - 2026-05-24T13:26:42Z

**Scope expansion** per operator critique (2026-05-24 ~13:11Z + ~13:18Z) on Neo-conventions adherence + GPT REQUEST_CHANGES (PRR_kwDODSospM8AAAABA3K5cQ) on PR #11899 + Drop+Supersede of PR #11899.

PR #11899 was Drop+Superseded; PR #11901 ships clean scaffolding-only Phase 1a. This ticket (#11900) now carries:

**Phase 1b (NEW — was Phase 1 of PR #11899)**: MBS wiring into Orchestrator.poll() — must adhere to Neo class system conventions per `core.Base.mjs` reactive config lifecycle:
- `afterSetX(value, oldValue)` signature MANDATORY (both params; `oldValue === undefined` sentinel for initial instantiation per core.Base.mjs:71-77)
- NO per-poll binding refresh hack — proper propagation via reactive setter + afterSet hooks
- Investigate MBS `singleton: true` vs per-Orchestrator-instance lifecycle — test-isolation failures reproduce on independent machines (GPT confirmed 1-3 failures on Orchestrator.spec depending on test ordering); root cause likely singleton-state-leak across `Neo.create(Orchestrator)` calls
- Read `src/Neo.mjs` + `src/core/Base.mjs` BEFORE substantive changes (operator mandate)

**Phase 2 (existing scope from original #11900)**:
- Drop `CadenceEngine.runIfDue` executeFn parameter (or delete `runIfDue` entirely per Plan agent design)
- Switch Orchestrator.poll() dispatch from `runIfDue` blocks to `collectDueCandidates → pickNextCandidate → execute()` pipeline
- Sub-13 cleanup (strip class @summary historical narrative + section divider comments)
- Collapse 13 mock-heavy Orchestrator.spec.mjs tests

**Total deferred work** from PR #11899 archaeology:
- Per-poll binding refresh (commits 85e6b4928 in archived branch) must be REMOVED + replaced with proper Neo afterSet propagation
- `afterSetX(value)` hooks (multiple in commit 6eef402d3) must be rewritten with `(value, oldValue)` signature
- Test-isolation: research whether MBS singleton can safely back multiple Orchestrator instances OR convert to per-instance

**Depends on**: PR #11901 (Phase 1a scaffolding) merge.

— @neo-opus-4-7

- 2026-05-24T16:31:13Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-24T16:43:44Z @neo-opus-ada unassigned from @neo-opus-ada
- 2026-05-24T16:52:19Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-24T17:02:03Z @neo-opus-ada cross-referenced by PR #11918
- 2026-05-24T17:08:22Z @neo-opus-ada referenced in commit `8fac66d` - "refactor(orchestrator): convert MBS to per-instance class per singleton heuristic (#11900)

Operator clarified the singleton heuristic:
> "can live without external configuration => candidate. does not need it => class."

MaintenanceBackpressureService heavily requires external configuration
(heavyMaintenanceLeasePath, taskStateService, healthService, taskDefinitions,
writeLog) — therefore it should be per-instance class, not singleton.

Changes from cycle-1:
- Drop `singleton: true` from MBS static config.
- Lift `maintenanceBackpressureService_` to Orchestrator reactive config slot
  (was a plain Class C field — wrong because Class B beforeSet only fires for
  reactive configs in `static config`; cycle-1 declared beforeSet but the hook
  never fired since the field was plain).
- Drop `MaintenanceBackpressureService.resetBindings()` method — no longer
  needed; per-instance MBS isolates state naturally per `Neo.create(Orchestrator)`.
- Drop test `afterEach` resetBindings call.
- Simplify test helper: only the `heavyMaintenanceLeasePath` sync via
  `applyBindings` remains (plain Orchestrator field, not reactive — Class B
  doesn't reach it through `Neo.create` config arg). Other bindings flow via
  Class B propagation cleanly.
- Update beforeSet docblock to note per-instance rationale.

Net cycle-2 delta: -62 / +26 LOC across same 3 files.

Test evidence: `npm run test-unit -- --grep "Orchestrator|MaintenanceBackpressure|HeavyMaintenance"` -> 252 passed.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T17:11:09Z @neo-opus-ada cross-referenced by PR #11917
- 2026-05-24T17:31:21Z @neo-opus-ada referenced in commit `5a9f8d2` - "fix(orchestrator): lift heavyMaintenanceLeasePath to reactive config (#11900)

Addresses GPT PR #11918 cycle-1 review:

RA1 — leasePath propagation gap (substantive):
`heavyMaintenanceLeasePath` was a plain Orchestrator instance field. Production
`start({heavyMaintenanceLeasePath})` set it AFTER the MBS instance was already
created at Neo.create time with null leasePath, leaving the public start()
option silently disconnected from the service. The test helper manually patched
MBS via applyBindings, so unit tests passed while the production wiring was
broken.

Fix: lift to reactive config `heavyMaintenanceLeasePath_: null` with
`afterSetHeavyMaintenanceLeasePath` propagating to MBS via applyBindings. The
public start() option now flows cleanly through the reactive cycle. Test helper
simplified — explicit applyBindings call removed (no longer needed since
reactive propagation handles it).

RA2 — MBS JSDoc stale singleton language:
Dropped `@singleton` annotation. Updated applyBindings JSDoc to drop
"singleton MBS" phrasing in favor of "MBS instance" (consistent with the
cycle-2 singleton→class conversion).

Net cycle-3: +24 / -10 LOC across the same 3 files. 252 tests pass.

RA3 (close-target Resolves→Refs) handled separately via PR body edit.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T19:26:58Z @neo-opus-ada referenced in commit `4d8dc7c` - "fix(orchestrator): afterSet hooks honor (value, oldValue) + init-skip sentinel (#11900)

Per src/core/Base.mjs:71-80 + src/Neo.mjs:444-446 + the canonical
`afterSetId(value, oldValue)` precedent in Base.mjs:319-339, reactive-config
afterSet hooks MUST take `(value, oldValue)` and skip side-effects during
initial instantiation via `if (oldValue === undefined) return`.

Fixed all 6 afterSet hooks in Orchestrator.mjs:
- afterSetDataDir / afterSetTaskDefinitions / afterSetTaskStateService /
  afterSetHealthService / afterSetSpawnFn (pre-existing, extended in cycles
  1-3 to also propagate to MBS — were already wrong-shape; now corrected)
- afterSetHeavyMaintenanceLeasePath (added in cycle-3; corrected to the
  proper shape)

Without the init-skip guard, every reactive config setter fires its afterSet
during initial Neo.create with `oldValue === undefined`, propagating default
values to child services that are also still initializing — wasted cycles +
unexpected order-of-operations. The Class B beforeSet hooks already pass the
initial values into ProcessSupervisorService / MaintenanceBackpressureService
at creation time, so the afterSet hooks only need to fire on subsequent
RUNTIME changes.

252 tests still pass.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T19:31:43Z @neo-opus-ada referenced in commit `d2fc5bc` - "fix(orchestrator): drop applyBindings wrapper — use direct reactive-config assignment (#11900)

Operator caught this anti-pattern:

    this.maintenanceBackpressureService?.applyBindings?.({taskStateService: value})

That's playing AGAINST the class config system. MBS already has the full set
of reactive configs (`taskStateService_`, `healthService_`, `taskDefinitions_`,
`dataDir_`, `writeLog_`, `heavyMaintenanceLeasePath_`, etc.) with the standard
Neo lifecycle: beforeSet hooks, value validation, oldValue tracking, afterSet
hooks, subscription notifications. The reactive setter IS the API.

Removed:
- `MaintenanceBackpressureService.applyBindings()` method entirely + its 38-line
  JSDoc. Wrapper added zero value over direct reactive-config assignment.

Rewrote the 6 afterSet hooks to use direct property assignment:

    this.maintenanceBackpressureService.taskStateService = value;

Direct assignment triggers MBS's full reactive lifecycle. No optional chaining
defensive paranoia (`?.applyBindings?.()`) — the init-skip sentinel
(`if (oldValue === undefined) return`) already covers the only case where the
child service might not exist.

Updated `beforeSetMaintenanceBackpressureService` docblock to drop applyBindings
references and describe direct-assignment as the wiring contract for both the
local single-repo path and the cloud multi-repo per-poll-rebind path.

Net cycle-5: -66 / +15 LOC. 252 tests still pass.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T19:44:34Z @tobiu referenced in commit `26ef267` - "feat(orchestrator): wire MBS via Class B propagation + applyBindings dual-pattern (#11900) (#11918)

* feat(orchestrator): wire MBS via Class B propagation + applyBindings dual-pattern (#11900)

Implements Phase 1b of Sub 18 (#11862) per operator guidance that per-poll
binding refresh is an ALTERNATIVE pattern for the planned klarso cloud
multi-repo Orchestrator deployment, not a replacement for proper Neo conventions.

Architecture: dual coexisting wiring patterns on MaintenanceBackpressureService:

1. Class B afterSet propagation (primary; local single-repo Orchestrator):
   Orchestrator declares parent-prop afterSet hooks forwarding to
   `mbs.applyBindings({field: newValue})`. Per-prop updates flow individually
   as parent state changes.

2. Per-poll refresh (alternative; cloud multi-repo: 1 Orchestrator polling N
   tenant repos): Orchestrator calls `mbs.applyBindings({...allBindings})`
   per poll cycle to switch the singleton's context across repos. No code in
   this PR uses this path; the API is documented + available.

MBS additions:
- `applyBindings(bindings)`: atomic bulk update; only present keys mutate
  state. Both patterns call this entrypoint.
- `resetBindings()`: test-isolation reset; production callers MUST NOT use it
  (defends against singleton-state-leak between Neo.create() calls in tests).

Orchestrator changes:
- Add `maintenanceBackpressureService` as Class B Service-DI field with
  `beforeSetMaintenanceBackpressureService` wiring via
  `ClassSystemUtil.beforeSetInstance` + per-prop `applyBindings` propagation
  in existing afterSet hooks (`afterSetDataDir`, `afterSetTaskDefinitions`,
  `afterSetTaskStateService`, `afterSetHealthService`).
- Add stable `maintenanceBackpressureWriteLog` logger seam (mirrors
  `processSupervisorWriteLog`).
- Delegate `createMaintenanceExecutor` body to
  `mbs.acquireLeaseAndExecute(...)`.
- Delegate `createGoldenPathExecutor` body to
  `mbs.executeWithGoldenPathDependencyGate(...)`.
- Delete 9 now-unused inline helpers (`isHeavyMaintenanceTask`,
  `isGoldenPathDependencyTask`, `findActiveHeavyMaintenanceTask`,
  `findActiveGoldenPathDependencyTask`, `clearMaintenanceDeferralLogState`,
  `recordMaintenanceDeferral`, `recordCrossDaemonLeaseDeferral`,
  `recordGoldenPathDependencyDeferral`, `resolveHeavyMaintenanceLeasePath`)
  — MBS owns this substrate post-extraction.
- Remove unused `acquireHeavyMaintenanceLeaseSync`/`releaseHeavyMaintenanceLeaseSync`
  imports; MBS owns the lease primitive bindings via its `acquireLeaseFn`/
  `releaseLeaseFn` reactive configs (default to the same imports).

Test isolation:
- `createTestOrchestrator` explicitly calls `mbs.applyBindings({...})` post-
  `Neo.create` to sync the per-test lease path + collaborators to the
  singleton MBS (`heavyMaintenanceLeasePath` is a plain Orchestrator
  instance field, not a reactive config; Class B afterSet propagation
  doesn't reach it through Neo.create config arg).
- `afterEach` calls `MaintenanceBackpressureService.resetBindings()` to
  release singleton state between tests.

Test evidence: `npm run test-unit -- --grep "Orchestrator|MaintenanceBackpressure|HeavyMaintenance"` -> 252 passed (was 252 pre-change with old inline-helpers shape; equivalent green coverage post-refactor).

Net delta: -300 / +155 LOC across 3 files. Orchestrator drops ~250 LOC of
inline lease/deferral logic now owned by MBS.

Out of scope for this PR (Phase 2):
- `cadenceEngine.runIfDue` -> direct `collect -> pick -> execute` pipeline
  switchover (ticket #11900 AC1).
- Sub-13 cleanup of class @summary historical narrative (AC6).
- 13 mock-heavy Orchestrator.spec test collapse to integration smokes (AC3).

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>

* refactor(orchestrator): convert MBS to per-instance class per singleton heuristic (#11900)

Operator clarified the singleton heuristic:
> "can live without external configuration => candidate. does not need it => class."

MaintenanceBackpressureService heavily requires external configuration
(heavyMaintenanceLeasePath, taskStateService, healthService, taskDefinitions,
writeLog) — therefore it should be per-instance class, not singleton.

Changes from cycle-1:
- Drop `singleton: true` from MBS static config.
- Lift `maintenanceBackpressureService_` to Orchestrator reactive config slot
  (was a plain Class C field — wrong because Class B beforeSet only fires for
  reactive configs in `static config`; cycle-1 declared beforeSet but the hook
  never fired since the field was plain).
- Drop `MaintenanceBackpressureService.resetBindings()` method — no longer
  needed; per-instance MBS isolates state naturally per `Neo.create(Orchestrator)`.
- Drop test `afterEach` resetBindings call.
- Simplify test helper: only the `heavyMaintenanceLeasePath` sync via
  `applyBindings` remains (plain Orchestrator field, not reactive — Class B
  doesn't reach it through `Neo.create` config arg). Other bindings flow via
  Class B propagation cleanly.
- Update beforeSet docblock to note per-instance rationale.

Net cycle-2 delta: -62 / +26 LOC across same 3 files.

Test evidence: `npm run test-unit -- --grep "Orchestrator|MaintenanceBackpressure|HeavyMaintenance"` -> 252 passed.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>

* fix(orchestrator): lift heavyMaintenanceLeasePath to reactive config (#11900)

Addresses GPT PR #11918 cycle-1 review:

RA1 — leasePath propagation gap (substantive):
`heavyMaintenanceLeasePath` was a plain Orchestrator instance field. Production
`start({heavyMaintenanceLeasePath})` set it AFTER the MBS instance was already
created at Neo.create time with null leasePath, leaving the public start()
option silently disconnected from the service. The test helper manually patched
MBS via applyBindings, so unit tests passed while the production wiring was
broken.

Fix: lift to reactive config `heavyMaintenanceLeasePath_: null` with
`afterSetHeavyMaintenanceLeasePath` propagating to MBS via applyBindings. The
public start() option now flows cleanly through the reactive cycle. Test helper
simplified — explicit applyBindings call removed (no longer needed since
reactive propagation handles it).

RA2 — MBS JSDoc stale singleton language:
Dropped `@singleton` annotation. Updated applyBindings JSDoc to drop
"singleton MBS" phrasing in favor of "MBS instance" (consistent with the
cycle-2 singleton→class conversion).

Net cycle-3: +24 / -10 LOC across the same 3 files. 252 tests pass.

RA3 (close-target Resolves→Refs) handled separately via PR body edit.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>

* fix(orchestrator): afterSet hooks honor (value, oldValue) + init-skip sentinel (#11900)

Per src/core/Base.mjs:71-80 + src/Neo.mjs:444-446 + the canonical
`afterSetId(value, oldValue)` precedent in Base.mjs:319-339, reactive-config
afterSet hooks MUST take `(value, oldValue)` and skip side-effects during
initial instantiation via `if (oldValue === undefined) return`.

Fixed all 6 afterSet hooks in Orchestrator.mjs:
- afterSetDataDir / afterSetTaskDefinitions / afterSetTaskStateService /
  afterSetHealthService / afterSetSpawnFn (pre-existing, extended in cycles
  1-3 to also propagate to MBS — were already wrong-shape; now corrected)
- afterSetHeavyMaintenanceLeasePath (added in cycle-3; corrected to the
  proper shape)

Without the init-skip guard, every reactive config setter fires its afterSet
during initial Neo.create with `oldValue === undefined`, propagating default
values to child services that are also still initializing — wasted cycles +
unexpected order-of-operations. The Class B beforeSet hooks already pass the
initial values into ProcessSupervisorService / MaintenanceBackpressureService
at creation time, so the afterSet hooks only need to fire on subsequent
RUNTIME changes.

252 tests still pass.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>

* fix(orchestrator): drop applyBindings wrapper — use direct reactive-config assignment (#11900)

Operator caught this anti-pattern:

    this.maintenanceBackpressureService?.applyBindings?.({taskStateService: value})

That's playing AGAINST the class config system. MBS already has the full set
of reactive configs (`taskStateService_`, `healthService_`, `taskDefinitions_`,
`dataDir_`, `writeLog_`, `heavyMaintenanceLeasePath_`, etc.) with the standard
Neo lifecycle: beforeSet hooks, value validation, oldValue tracking, afterSet
hooks, subscription notifications. The reactive setter IS the API.

Removed:
- `MaintenanceBackpressureService.applyBindings()` method entirely + its 38-line
  JSDoc. Wrapper added zero value over direct reactive-config assignment.

Rewrote the 6 afterSet hooks to use direct property assignment:

    this.maintenanceBackpressureService.taskStateService = value;

Direct assignment triggers MBS's full reactive lifecycle. No optional chaining
defensive paranoia (`?.applyBindings?.()`) — the init-skip sentinel
(`if (oldValue === undefined) return`) already covers the only case where the
child service might not exist.

Updated `beforeSetMaintenanceBackpressureService` docblock to drop applyBindings
references and describe direct-assignment as the wiring contract for both the
local single-repo path and the cloud multi-repo per-poll-rebind path.

Net cycle-5: -66 / +15 LOC. 252 tests still pass.

Co-Authored-By: Claude Opus 4.7 (Claude Code) <neo-opus-4-7@neomjs.com>

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-24T20:06:55Z @neo-opus-ada cross-referenced by PR #11919
### @neo-opus-ada - 2026-05-24T20:17:17Z

**Input from Claude Opus 4.7 (Claude Code, 1M context) — @neo-opus-4-7:**

> ❋ ## [sunset-handover] Phase 2 dispatch refactor — picker semantic decision needed
> 
> **Session sunset 2026-05-24 ~20:18Z. Phase 1b shipped, Phase 2 blocked on architectural decision.**
> 
> ### What's done
> 
> - **Phase 1b**: PR #11918 MERGED 19:44Z (`26ef26743`). Orchestrator wires MBS via Class B Service-DI (`maintenanceBackpressureService_: MaintenanceBackpressureService`). All 6 afterSet hooks now `(value, oldValue)` signature with `if (oldValue === undefined) return` skip-during-init sentinel. `heavyMaintenanceLeasePath` lifted from plain field → reactive config `heavyMaintenanceLeasePath_: null`. Direct reactive-config assignment replaces the (rejected) `applyBindings()` wrapper anti-pattern.
> - 5 cycles of operator-driven Neo-conventions corrections landed cleanly. Singleton heuristic applied ("can live without external configuration => candidate; needs external configuration => class") — MBS is per-instance class.
> 
> ### What's blocked
> 
> **Phase 2 = dispatch refactor + Sub-13 cleanup + test collapse.** First attempt branch `tobiu/11900-sub18-phase2-dispatch-refactor` was created, work-in-progress pushed locally, then reverted via `git checkout ai/daemons/orchestrator/Orchestrator.mjs` — **no commits pushed to origin** because 13 `Orchestrator.spec.mjs` tests failed.
> 
> **Root cause**: `ai/daemons/orchestrator/scheduling/picker.mjs#pickNextCandidate` fires **at most one winner per poll cycle** (verified by reading the picker source). The cycle-1 attempt replaced 6 `cadenceEngine.runIfDue(...)` blocks with a single `collect → pick → execute` pipeline. Tests fixtures expect the **old multi-fire model** where the orchestrator runs every due task in the same poll cycle.
> 
> ### Architectural decision needed from operator
> 
> The picker semantic shift was already shipped (probably under one of the Epic #11871/#11829 sub-tickets earlier this session). The question for Phase 2 is which path is architecturally intended:
> 
> - **Path A**: modify picker to fire ALL due winners per poll (revert to multi-fire). Phase 2 dispatch refactor becomes a simple wrapping of the existing 6-block dispatch logic.
> - **Path B**: keep one-winner-per-poll, bump test fixtures + faster default poll cadence to compensate for the throughput cut. Phase 2 dispatch refactor lands as `collect → pick → execute` with one-winner-per-call semantic.
> - **Path C**: hybrid — picker fires all maintenance-class winners but one heavy-lease winner. (Probably wrong shape but listed for completeness.)
> 
> ### Pickup protocol for next-session-me (or whichever peer takes it)
> 
> 1. Read this comment + the picker source `ai/daemons/orchestrator/scheduling/picker.mjs`.
> 2. Read the prior session's git-log near `26ef26743` for the dispatch refactor attempt context.
> 3. Ask operator for the Path A/B/C call BEFORE writing any code. The picker semantic affects the entire dispatch pipeline shape; getting it wrong wastes a full cycle.
> 4. Confirm whether Phase 2 should land under #11900 or get a fresh sub-ticket given the architectural decision exposes scope larger than originally framed.
> 
> ### Signals + anchors
> 
> - Cycle-1 attempt artifacts: branch existed but reverted; no remote commits. Local working tree clean.
> - Test surface: `test/playwright/unit/ai/daemons/orchestrator/Orchestrator.spec.mjs` (252 tests, 13 broken on cycle-1 attempt — re-running after revert restores 252 green).
> - The 4-key dispatch map (`executionKind`): `supervised-child-process` / `service-runner` / `in-process-async` / (4th key TBD from registry.mjs#TASK_REGISTRY).
> 
> Not blocking any other lane. Picking this up requires the architectural decision first; no point spinning Phase 2 without it.
> 
> — @neo-opus-4-7 (Claude Opus 4.7, 1M context)

- 2026-05-24T20:17:50Z @neo-gpt cross-referenced by #11912
- 2026-06-06T22:26:42Z @neo-gpt cross-referenced by #11831
- 2026-06-06T22:39:29Z @neo-opus-ada cross-referenced by #11862
- 2026-06-06T22:54:45Z @neo-opus-vega cross-referenced by PR #12659
- 2026-06-07T00:40:40Z @neo-opus-ada unassigned from @neo-opus-ada
- 2026-06-07T00:40:42Z @neo-opus-ada assigned to @neo-gpt
### @neo-opus-ada - 2026-06-07T00:40:43Z

**`[lane-override]` reassignment audit-trail** (#11537 §AC8)

**Previous assignees:** `@neo-opus-ada`
**New assignees:** `neo-gpt`
**Reason:** Transferring to @neo-gpt per his transfer-request. Picker semantic now settled: #11862 design LOCKED (drop the static deploymentScope descriptor; the picker gates by reading the override-aware getter SSOT). The per-descriptor enable-locus trace that decides (A) drop-filter-entirely vs (B) getter-reading pre-filter is the natural foundation for #11900's poll() dispatch-wiring, so it travels with this ticket. Reduces Ada's self-assigned load per operator directive.

*Audit-trail per AGENTS.md §6.5 — `acknowledgedReassign` reason persistence. Graph-ingested via Retrospective daemon comment-scan path.*

- 2026-06-07T01:09:08Z @neo-gpt cross-referenced by PR #12670
- 2026-06-07T03:13:35Z @tobiu referenced in commit `15eba86` - "feat(orchestrator): consume registry dispatch pipeline (#11900) (#12670)

* feat(orchestrator): consume registry dispatch pipeline (#11900)

* refactor(orchestrator): move dispatch pipeline out of orchestrator (#11862)"
- 2026-06-07T03:13:35Z @tobiu closed this issue

