@micha.bigler/ui-core-micha 2.12.0 → 2.14.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.
@@ -0,0 +1,214 @@
1
+ # WORK ORDER NOTIF-12 (ui-core-micha + django-core-micha) — popup channel via the wizard renderer
2
+
3
+ **EXECUTION DIRECTIVE.** Implement through `codex exec` in the background — invoked **directly via
4
+ Bash** (never the `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check`
5
+ and `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from this
6
+ file. Fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
7
+
8
+ Feature-level WO (Expertenchat envelope). Seams verified against ucm 2.12.0 / dcm 2.32.0.
9
+
10
+ ## HARD GATE — SATISFIED 2026-07-30
11
+ NOTIF-13 landed and published: ucm **2.13.0** (`7a83ee9`), dcm **2.33.0** (`de77335`), both verified
12
+ live on npm/PyPI. This WO was re-verified against the landed code on 2026-07-30 and the context
13
+ package below describes the **actual shipped API**, not the pre-NOTIF-13 state. Cleared to start.
14
+
15
+ ## TIER
16
+ Tier 2 — this extracts a renderer out of `OnboardingWizard`, which **cockpit and jg run in
17
+ production**. The roadmap calls the popup channel "uncritical"; that refers to business urgency, not
18
+ blast radius. Independent `reviewer` mandatory. `sec_reviewer` only on explicit operator request.
19
+
20
+ ## SCAFFOLDING NOTICE — READ FIRST
21
+ **This channel ships with zero producers, deliberately.** Verified 2026-07-29 across dcm, jg-ferien,
22
+ cockpit, hram, spesix and survey_app: **no notification type declares `popup` in `eligible_channels`**,
23
+ so `resolve_channels()` will never route anything to it until some app opts a type in. The operator
24
+ decided to build the scaffolding anyway (2026-07-29), having been shown this.
25
+
26
+ Two obligations follow, and they are part of the acceptance criteria:
27
+ 1. **Do not overclaim.** The CHANGELOG entry and any docstring must state plainly that the popup
28
+ channel is wired but inactive until a notification type declares `popup`. Precedent: NOTIF-4's
29
+ reviewer (R1) had to soften a CHANGELOG that promised retry behaviour no shipped dispatcher could
30
+ reach. Do not repeat that.
31
+ 2. **Scaffolding must be tested scaffolding.** Register a test-local notification type that declares
32
+ `popup` and prove the whole path end-to-end. Untested dead code is not an acceptable outcome.
33
+
34
+ ## GOAL
35
+ Make `popup` a real delivery channel: a notification whose type opts into `popup` is rendered to the
36
+ recipient in a modal that reuses the onboarding **wizard renderer**, with its seen/dismissed status
37
+ recorded on `NotificationRecipient` — **not** in the onboarding progress store.
38
+
39
+ ## EXPECTED OUTCOME
40
+ - dcm's `PopupDispatcher` stops being a logging stub and delivers for real.
41
+ - ucm exports a popup surface that renders popup-eligible notifications and marks them via the
42
+ existing `feed/mark/` path.
43
+ - Onboarding behaviour is bit-for-bit unchanged for existing consumers.
44
+ - With no producer registered, nothing visibly changes in any app.
45
+
46
+ ## CONTEXT PACKAGE — verified current state
47
+
48
+ **dcm `notifications/dispatch.py`:**
49
+ - `PopupDispatcher` (`:117-121`) is a stub: logs "queued for unimplemented popup channel" and returns
50
+ `DeliveryResult(ok=None, error="pending")`. It is registered in `_DISPATCHERS` (`:126-133`).
51
+ - The model to copy is `ChipDispatcher` (`dispatch.py:64-75`, post-NOTIF-13): it calls
52
+ `push_to_users([recipient.user], notification_envelope({"type": notification.notification_type, "content": ..., "notification_id": ...}))`
53
+ and returns `DeliveryResult(ok=True)`.
54
+
55
+ **How NOTIF-13 actually shipped — read this before writing scope D.** The envelope is a
56
+ **domain-level** discriminator, not a per-channel one:
57
+ - `delivery.py:25` `NOTIFICATION_ENVELOPE = "notification"`; `delivery.py:28-35`
58
+ `notification_envelope(payload)` merges `{"envelope": "notification"}` into any payload. **Additive**
59
+ — the pre-existing fields, including the overloaded `type`, are unchanged.
60
+ - ucm `src/notifications/realtime.jsx:68` routes purely on `data.envelope ?? DEFAULT_ENVELOPE`, then
61
+ fans to every handler subscribed to that envelope.
62
+ - **Inside** the notification envelope, `NotificationsProvider` keeps the original two-way split:
63
+ `data.type === 'notification.status'` patches status (`:98`), and **everything else is appended to
64
+ the feed with `unreadCount + 1`** (`:120`).
65
+
66
+ **Consequence — the trap this WO must avoid:** if `PopupDispatcher` simply calls
67
+ `notification_envelope({...})` like Chip does, its message is **indistinguishable from a chip
68
+ message**. The popup surface subscribing to the `"notification"` envelope would receive every chip
69
+ notification and pop up for all of them, and a notification routed to *both* chip and popup would
70
+ produce **two feed entries and `unreadCount + 2` for one notification**. Scope D and D2 below exist
71
+ to close exactly this.
72
+ - `_render_content(content, user)` (`:44-60`) renders `title_key`/`body_key`/`params` in the
73
+ recipient's language — reuse it rather than re-deriving text client-side if the payload needs text.
74
+ - `prefs.py:22` already treats `popup` as a channel defaulting to True; `router.py:24` narrows to
75
+ `ntype.eligible_channels`. **No prefs or router change is needed** — only a type opting in.
76
+
77
+ **ucm `src/onboarding/OnboardingWizard.jsx` (75 LOC)** is the renderer to reuse:
78
+ - MUI `Dialog open fullWidth maxWidth="sm"` (`:59`), `DialogTitle` with a step counter +
79
+ `LinearProgress` (`:60-67`), `DialogContent` rendering
80
+ `<StepComponent onComplete={...} onDismiss={...} ctx={...} />` (`:69`).
81
+ - Step contract: `{ id, Component, blocking, persistDismissed }`. `blocking` disables escape/backdrop
82
+ close (`:59`).
83
+ - **The parts that must NOT come along** (design decision D-F7,
84
+ `django-core-micha/docs/design/notifications-platform.md:70-71`): `onboarding.markStepSeen` (`:35`),
85
+ `dismissStep` (`:54`), and the sequential progress store behind them. Popup status lives on
86
+ `NotificationRecipient`.
87
+ - Exported at `src/index.js:61`; the four existing steps at `:62-65`.
88
+
89
+ **ucm notification status API:** `useNotifications()` already returns `markSeen` / `markDismissed` /
90
+ `markDone`, backed by `feed/mark/`. Use `markSeen` when a popup is shown and `markDismissed` when the
91
+ user closes it. **Never `markDone`** — done is a todo-channel semantic.
92
+
93
+ ## SCOPE
94
+
95
+ **A. ucm — extract the dialog shell.** Lift the presentational shell out of `OnboardingWizard` into a
96
+ reusable renderer (dialog frame, counter/progress header, the `onComplete`/`onDismiss`/`ctx` child
97
+ contract). `OnboardingWizard` keeps its current behaviour by composing that shell with the onboarding
98
+ store; the popup surface composes the same shell with notification state.
99
+
100
+ **B. ucm — the popup surface.** A component that renders popup-eligible notifications through the
101
+ shell, marks `seen` on display and `dismissed` on close. For live popups it subscribes via
102
+ `useRealtime()` to the **`"notification"` envelope** (`DEFAULT_ENVELOPE` — there is no popup-specific
103
+ envelope, see D) and filters on `channel === "popup"`, ignoring `notification.status` messages and
104
+ channel-less legacy payloads. Export additively from `src/index.js`.
105
+
106
+ **C. Coexistence rule with onboarding — specify and test it.** Both surfaces render a MUI `Dialog`, so
107
+ two modals can be open at once. Required behaviour: **a blocking onboarding step wins**; the popup
108
+ waits until no blocking onboarding step is active. Non-blocking coexistence must not stack two dialogs.
109
+
110
+ **D. dcm — implement `PopupDispatcher.deliver`, with a channel discriminator *inside* the envelope.**
111
+ Emit through `notification_envelope()` (so Layer-1 routing stays domain-keyed and messaging can still
112
+ ride alongside) and add an explicit **`"channel": "popup"`** field to the payload. Return
113
+ `DeliveryResult(ok=True)`. Keep delivery-record semantics identical to `ChipDispatcher` — the
114
+ surrounding `dispatch()` machinery already records pending → sent/failed; do not re-implement it.
115
+
116
+ **Do NOT invent a second envelope value** (e.g. `"notification.popup"`). The envelope discriminates
117
+ *domains* so a future messaging stream can share the socket; overloading it with channels would
118
+ undo NOTIF-13's design. Channel selection belongs inside the payload.
119
+
120
+ For symmetry and so the client can tell the cases apart, `ChipDispatcher` should carry
121
+ `"channel": "chip"` as well. **Backward compatibility:** a payload with **no** `channel` field must
122
+ keep behaving exactly as today (feed entry + unread increment), because an app pinned to an older dcm
123
+ will still send channel-less payloads.
124
+
125
+ **D2. ucm — de-duplicate by `notification_id` in `NotificationsProvider`.** A notification routed to
126
+ both chip and popup now produces two WS messages. The provider must fold them into **one** feed entry
127
+ and **one** unread increment (de-dupe on `notification_id`, last-write-wins on content). Without this,
128
+ enabling popup alongside chip double-counts the badge. This is an edit to `NotificationsProvider`,
129
+ which is explicitly in scope here — it is not part of NOTIF-13's realtime core.
130
+
131
+ ## DO NOT TOUCH
132
+ - **The onboarding progress store** — `markStepSeen`, `dismissStep`, `persistDismissed` persistence,
133
+ `OnboardingProvider`'s step-selection semantics, `stepSelection.js`.
134
+ - The four existing step components (`CookieConsentStep`, `CompleteNameStep`, `BrowserPushStep`,
135
+ `PwaInstallStep`) and their props.
136
+ - **NOTIF-13's realtime core** (`src/notifications/realtime.jsx`) — consume `useRealtime()` /
137
+ `subscribe(envelope, handler)`, do not restructure it. In particular leave the `seeded` gate, the
138
+ backoff/reconnect semantics and the envelope-routing alone. (`NotificationsProvider` itself IS
139
+ editable — scope D2 requires it.)
140
+ - `feed/*` REST contract, `NotificationBell`, `NotificationSettings`.
141
+ - `prefs.py`, `router.py`, `resolve_channels` — popup already works there; a type opting in is an
142
+ app-side change, not this WO's.
143
+ - Schema, migrations, dependencies, CI.
144
+ - **Do not register a `popup` producer in any app.** This WO ships the channel only.
145
+
146
+ ## RISKS
147
+ - **Onboarding regression** is the main risk: the shell extraction touches a wizard that cockpit and jg
148
+ run in production. The step counter (`totalRef`/`completed`), the session-dismissed Set, and the
149
+ `blocking` escape-key behaviour are easy to break silently.
150
+ - **Stacked modals** if C is skipped — two dialogs, neither reachable.
151
+ - **Badge double-count** if D2 is skipped: a notification routed to both chip and popup arrives as two
152
+ WS messages and increments `unreadCount` twice for one notification. This is the single most likely
153
+ silent defect in this WO.
154
+ - **Popup storm** if B subscribes to the envelope without filtering on `channel` — every chip
155
+ notification would pop up a modal.
156
+ - **Regressing the NOTIF-13 R1 fix:** the `seeded` gate in `NotificationsProvider` exists because
157
+ decoupling the REST seed from the socket connect opened a silent-message-loss race. Do not touch it
158
+ while implementing D2.
159
+ - **Dead code rot:** with no producer, only the tests exercise the path. Hence the mandatory test-local
160
+ producer type.
161
+ - **Status semantics drift:** marking a popup `done` instead of `dismissed` would pollute todo-channel
162
+ semantics.
163
+
164
+ ## REQUIRED TESTS / ACCEPTANCE
165
+ ucm (`pnpm test`, vitest):
166
+ - a popup-eligible notification renders in the shell; `markSeen` fires once on display;
167
+ - closing it fires `markDismissed`, never `markDone`;
168
+ - **coexistence:** with a blocking onboarding step active, no popup dialog renders; once it clears, the
169
+ popup appears — and never two dialogs at once;
170
+ - **onboarding regression guard:** existing `OnboardingWizard` behaviour unchanged — step counter,
171
+ progress bar, session-dismissed set, `blocking` disabling escape/backdrop close, `persistDismissed`
172
+ still calling `dismissStep`;
173
+ - with no popup-eligible notification present, nothing renders;
174
+ - **no popup storm:** a `channel: "chip"` message and a channel-less legacy message both leave the
175
+ popup surface silent;
176
+ - **no double-count (D2):** two WS messages for the same `notification_id` (one `chip`, one `popup`)
177
+ yield exactly one feed entry and `unreadCount + 1`;
178
+ - **NOTIF-13 R1 guard:** the `seeded` gate still holds — no socket messages are processed before the
179
+ REST seed completes.
180
+
181
+ dcm (`pytest` on the notifications package):
182
+ - register a **test-local** notification type declaring `eligible_channels=["popup"]`, call `notify()`,
183
+ and assert the dispatcher delivered (`ok=True`) with `envelope: "notification"` **and**
184
+ `channel: "popup"` on the wire — the end-to-end proof required by the scaffolding notice;
185
+ - a chip delivery still carries `envelope: "notification"` and now `channel: "chip"`, with every other
186
+ field unchanged (no regression for existing consumers);
187
+ - `resolve_channels` still excludes `popup` for types that do not declare it (no accidental widening);
188
+ - the existing `test_dispatch.py` registry assertion (`:25`) still passes.
189
+
190
+ Plus `pnpm build` (tsc) green.
191
+
192
+ ## RELEASE
193
+ ucm minor bump + publish. dcm minor bump + publish (publish-from-main), with a **registry live-check
194
+ before any app pin bump**. No app pin bumps in this WO. CHANGELOG in both repos must carry the
195
+ inactive-until-a-producer-opts-in wording from the scaffolding notice.
196
+
197
+ ## TARGET REPO / WORKING DIRECTORY
198
+ - Primary: `C:\Users\biglmi\Documents\webapps\ui-core-micha`
199
+ - Secondary (scope D): `C:\Users\biglmi\Documents\webapps\django-core-micha`
200
+ - Never the workspace root `…\webapps`.
201
+ Both are platform repos: commit directly to the trunk, no feature branches.
202
+
203
+ ## PROGRESS CONTRACT
204
+ Emit a `PLAN: <step1> | <step2> | …` line up front, then a single-line
205
+ `PROGRESS: [<n>/<total>] <present-tense action>` **before every relevant action** (file opened, file
206
+ edited, command/test run) and `PROGRESS: [<n>/<total>] done` on step completion, spaced so no gap
207
+ exceeds ~2 min. stdout unbuffered. Exactly one final `RESULT: DONE|BLOCKED <reason>`.
208
+
209
+ ## MINI-HANDOVER (paste into a fresh Orchestrator session)
210
+ ```
211
+ Orchestrator: implement work-orders/NOTIF-12.md in ui-core-micha (main; scope D also touches
212
+ django-core-micha). The NOTIF-13 gate is satisfied (ucm 2.13.0 / dcm 2.33.0). git pull, read the WO,
213
+ then follow orchestrate-codex (Codex-first, own independent review, commit on green).
214
+ ```
@@ -0,0 +1,142 @@
1
+ # WORK ORDER NOTIF-13 (ui-core-micha + django-core-micha) — Layer-1 transport extraction
2
+
3
+ **EXECUTION DIRECTIVE.** Implement through `codex exec` in the background — invoked **directly via
4
+ Bash** (never the `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check`
5
+ and `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from this
6
+ file. Fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
7
+
8
+ Feature-level WO (Expertenchat envelope). The seams below are **verified against the landed code**
9
+ (ucm 2.12.0, dcm 2.32.0) and are the required scope. The Orchestrator fills the exact line map.
10
+
11
+ ## TIER
12
+ Tier 2 — restructures a provider that **cockpit consumes in production**, and changes a WebSocket
13
+ message contract shared across apps. Independent `reviewer` mandatory. Per AGENTS.md Tier 2,
14
+ `sec_reviewer` is NOT auto-included; request it only if the operator asks.
15
+
16
+ ## GOAL
17
+ Turn ucm's notification-only socket into a generic **Layer-1 realtime primitive**: one socket,
18
+ subscribe-by-message-type, so a second domain (messaging) can ride it without being misread as a
19
+ notification. **Additive**: `NotificationsProvider` remains the sole mount point and its public
20
+ context value is unchanged (operator decision 2026-07-29).
21
+
22
+ ## EXPECTED OUTCOME
23
+ - ucm exports a `useRealtime()`-style subscribe primitive alongside the unchanged `useNotifications()`.
24
+ - An unknown/unsubscribed message type is **ignored** instead of being appended to the notification feed.
25
+ - No app has to change its provider tree. cockpit and jg keep working untouched.
26
+ - NOTIF-15 (jg bell adoption, retiring jg's local context) becomes implementable; Phase B messaging
27
+ gets its substrate.
28
+
29
+ ## CONTEXT PACKAGE — verified current state
30
+
31
+ **ucm `src/notifications/NotificationsProvider.jsx` (232 LOC)** owns *both* the socket and the
32
+ notifications domain state:
33
+ - socket ownership: `socketRef`, `connect()`, exponential backoff `INITIAL_BACKOFF_MS` 1s →
34
+ `MAX_BACKOFF_MS` 30s, auth-gated effect keyed on `[authenticated, refresh, replaceNotifications, wsUrlBase]` (`:87-179`).
35
+ - `handleMessage` (`:103-133`) dispatches on exactly **one** envelope type, `notification.status` (`:111`).
36
+ - **The blocker:** everything else falls through to `normalizeNotificationPush(data)` and is appended
37
+ to the feed with `unreadCount + 1` (`:130-132`). A messaging payload on this socket becomes a fake
38
+ notification today.
39
+ - **Field overload — the reason a third stream cannot simply be added:** in the status branch
40
+ `data.type` is the *envelope* type; in the new-notification branch `normalizeNotificationPush`
41
+ reads `data.type` as the *domain* notification-type key (`:25`). One field, two meanings. The
42
+ envelope contract must resolve this without breaking either reading.
43
+ - WS URL hardcoded to `/ws/notifications/` (`:36`, `:40`).
44
+ - exports in `src/index.js:48` (`NotificationsProvider`, `useNotifications`).
45
+
46
+ **dcm transport is ALREADY generic — correct the roadmap wording here.** The roadmap §4 calls for a
47
+ "dcm multi-stream consumer"; that overstates the dcm work:
48
+ - `notifications/delivery.py::push_to_users(users, payload)` (`:20-43`) fans an **arbitrary** payload
49
+ into the per-user group `notifications_user_{id}`.
50
+ - `notifications/consumers.py::NotificationConsumer.message()` (`:21-22`) is `send_json(event["payload"])` —
51
+ fully stream-agnostic. jg's messaging **already** rides this same group via `push_to_users`.
52
+ - The only dcm-side envelope producer is `notifications/views.py:235` (`"type": "notification.status"`);
53
+ the contract is documented at `views.py:3`.
54
+ - **So the dcm work is the envelope contract + a thin authoring helper + doc — NOT a consumer rewrite.**
55
+
56
+ **Reference implementation to promote:** jg's `frontend/src/context/NotificationsContext.jsx` (91 LOC,
57
+ docstring "WebSocket owner only") already does exactly this multiplexing with
58
+ `registerMessageCallback` / `registerChatCallback`. Read it for the shape. **Do not edit jg here.**
59
+
60
+ ## SCOPE
61
+
62
+ **A. ucm — extract the socket into an internal realtime core.** Single owner; preserve the existing
63
+ backoff, reconnect, auth-gating and cleanup semantics **verbatim**. Expose
64
+ `subscribe(type, handler) -> unsubscribe`.
65
+
66
+ **B. ucm — `NotificationsProvider` becomes the first subscriber.** It registers handlers for the
67
+ notification envelope types. Its returned context value stays **exactly**
68
+ `{ notifications, unreadCount, markSeen, markDismissed, markDone, refresh }`.
69
+
70
+ **C. Envelope contract.** Introduce an explicit envelope discriminator that does not collide with the
71
+ domain `type` field (see the field-overload note above). Unknown / unsubscribed envelope types are
72
+ **ignored** — the catch-all at `:130-132` dies. This is a deliberate behaviour change and must be
73
+ called out in the CHANGELOG.
74
+
75
+ **D. Backward compatibility in BOTH directions** — apps pin dcm and ucm independently, so mismatched
76
+ versions are normal:
77
+ - *new ucm + old dcm* (payload has no envelope field): must still be recognised as a new notification,
78
+ exactly as today. Without this the feed silently goes dead against an un-bumped backend.
79
+ - *old ucm + new dcm*: dcm must keep emitting the legacy fields so a ucm 2.12.0 client is unaffected
80
+ (**cockpit runs 2.12.0 in staging/prod**).
81
+
82
+ **E. dcm — formalize the contract.** Document the envelope next to `views.py:3` and add a thin
83
+ authoring helper so producers stop hand-rolling the dict. `push_to_users`'s signature stays unchanged.
84
+
85
+ ## DO NOT TOUCH
86
+ - **jg-ferien** (`NotificationsContext.jsx`, `MessagingContext.jsx`) — that is NOTIF-15.
87
+ - The messaging domain itself — Phase B (`MSG-*`).
88
+ - `NotificationBell.jsx`, `NotificationSettings.jsx`, `feedApi.js`, the `feed/*` REST contract.
89
+ - `BaseSecureConsumer` / S112 permission model, `IsAuthenticatedWs`, the group name `notifications_user_{id}`.
90
+ - **No standalone `RealtimeProvider` export that apps must mount.** Operator decision 2026-07-29:
91
+ additive only. A standalone provider is a later, separate WO if a second consumer ever needs it.
92
+ - Reconnect/backoff semantics and the single-socket-owner invariant — preserve, do not "improve".
93
+ - Schema, migrations, dependencies, CI.
94
+ - **No app pin bumps** in this WO (cockpit/jg stay where they are).
95
+
96
+ ## RISKS
97
+ - **Silent feed death** if C lands without D: an envelope-less payload from an older dcm would be
98
+ ignored instead of shown. D is not optional.
99
+ - **cockpit is a live consumer** at ucm 2.12.0 — any change to the returned context shape breaks it.
100
+ - **Subscription leaks / double delivery:** `subscribe` must return a stable unsubscribe, and handlers
101
+ must **survive reconnects** without the caller re-registering them.
102
+ - **Reconnect loop:** the socket effect currently depends on `refresh` / `replaceNotifications`;
103
+ moving the socket out must not make the socket re-create on every feed update.
104
+
105
+ ## REQUIRED TESTS / ACCEPTANCE
106
+ ucm runs `vitest` (`pnpm test`) and has **no tests under `src/notifications/` today** — this WO adds
107
+ the first ones:
108
+ - an unknown envelope type is ignored: feed and `unreadCount` both untouched (the regression that
109
+ protects messaging from being misread as a notification);
110
+ - two subscribers on distinct types each receive only their own messages, from **one** socket —
111
+ assert exactly one `WebSocket` was constructed;
112
+ - `unsubscribe` stops delivery for that subscriber and does not affect the others;
113
+ - handlers keep receiving after a close → backoff → reopen cycle, with no re-registration by the caller;
114
+ - a legacy envelope-less payload still lands in the feed (backward-compat D);
115
+ - `notification.status` handling and the `unreadCount` delta are unchanged — this guards the NOTIF-6
116
+ R1 fix (badge went stale after a cross-device mark);
117
+ - `NotificationsProvider`'s public context value is unchanged.
118
+
119
+ Plus `pnpm build` (tsc) green. If E lands in dcm, run the dcm notifications `pytest` package.
120
+
121
+ ## RELEASE
122
+ ucm minor bump + publish (publish-from-main). dcm only if E lands there — then publish-from-main with
123
+ a **registry live-check before any app pin bump**. No app pin bumps in this WO.
124
+
125
+ ## TARGET REPO / WORKING DIRECTORY
126
+ - Primary: `C:\Users\biglmi\Documents\webapps\ui-core-micha`
127
+ - Secondary (scope E only): `C:\Users\biglmi\Documents\webapps\django-core-micha`
128
+ - Never the workspace root `…\webapps`.
129
+ Both are platform repos: commit directly to the trunk, no feature branches.
130
+
131
+ ## PROGRESS CONTRACT
132
+ Emit a `PLAN: <step1> | <step2> | …` line up front, then a single-line
133
+ `PROGRESS: [<n>/<total>] <present-tense action>` **before every relevant action** (file opened, file
134
+ edited, command/test run) and `PROGRESS: [<n>/<total>] done` on step completion, spaced so no gap
135
+ exceeds ~2 min. stdout unbuffered. Exactly one final `RESULT: DONE|BLOCKED <reason>`.
136
+
137
+ ## MINI-HANDOVER (paste into a fresh Orchestrator session)
138
+ ```
139
+ Orchestrator: implement work-orders/NOTIF-13.md in ui-core-micha (main; scope E also touches
140
+ django-core-micha). git pull first, read the WO, then follow orchestrate-codex (Codex-first, own
141
+ independent review, commit on green).
142
+ ```