matterbridge-lg-thinq-plugin 0.1.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.
Files changed (210) hide show
  1. package/.claude/CLAUDE.md +43 -0
  2. package/.claude/agents/compiler.md +91 -0
  3. package/.claude/agents/direct-executor.md +98 -0
  4. package/.claude/agents/documenter.md +78 -0
  5. package/.claude/agents/finalizer.md +233 -0
  6. package/.claude/agents/implementer.md +112 -0
  7. package/.claude/agents/investigator.md +100 -0
  8. package/.claude/agents/release-manager.md +203 -0
  9. package/.claude/agents/reviewer.md +131 -0
  10. package/.claude/agents/technical-architect.md +403 -0
  11. package/.claude/agents/test-writer.md +159 -0
  12. package/.claude/agents/wiki-manager.md +147 -0
  13. package/.claude/instructions/agent-prompts.md +90 -0
  14. package/.claude/instructions/shared-rules.md +11 -0
  15. package/.claude/instructions/team-orchestrator-policy.md +165 -0
  16. package/.claude/memory.md +186 -0
  17. package/.claude/output-styles/engineer-manager.md +38 -0
  18. package/.claude/settings.json +40 -0
  19. package/.claude/settings.local.json +30 -0
  20. package/.claude/skills/load-policy/SKILL.md +13 -0
  21. package/.claude/skills/ref-idea/SKILL.md +44 -0
  22. package/.claude/skills/status-of/SKILL.md +38 -0
  23. package/.claude/skills/workspace-console/SKILL.md +49 -0
  24. package/.claude/statusline-command.sh +130 -0
  25. package/.claude/statusline.md +53 -0
  26. package/.claude/templates/approval-packet.template.html +171 -0
  27. package/.claude/templates/reference-workspaces.md +16 -0
  28. package/.claude/tools/cli-runner-server.cjs +220 -0
  29. package/.claude/tools/discord-send-server.cjs +188 -0
  30. package/.claude/tools/github-release-server.cjs +181 -0
  31. package/.claude/tools/glob-grep-server.cjs +267 -0
  32. package/.claude/tools/read-log-server.cjs +175 -0
  33. package/.github/codeql/codeql-config.yml +14 -0
  34. package/.github/workflows/build.yml +54 -0
  35. package/.github/workflows/codeql.yml +59 -0
  36. package/.github/workflows/coverage.yml +70 -0
  37. package/.github/workflows/publish.yml +103 -0
  38. package/.mcp.json +34 -0
  39. package/CHANGELOG.md +195 -0
  40. package/LICENSE +21 -0
  41. package/dist/cli/commands/devices.js +52 -0
  42. package/dist/cli/commands/energy.js +122 -0
  43. package/dist/cli/commands/login.js +54 -0
  44. package/dist/cli/help.js +50 -0
  45. package/dist/cli/main.js +56 -0
  46. package/dist/cli/session.js +13 -0
  47. package/dist/cli/types.js +1 -0
  48. package/dist/cli/utils.js +29 -0
  49. package/dist/cli.js +2 -0
  50. package/dist/constants/index.js +1 -0
  51. package/dist/core/domain/entities/ThinqDevice.js +43 -0
  52. package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
  53. package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
  54. package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
  55. package/dist/errors/AuthenticationError.js +26 -0
  56. package/dist/errors/BaseError.js +23 -0
  57. package/dist/errors/index.js +2 -0
  58. package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
  59. package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
  60. package/dist/module.js +212 -0
  61. package/dist/platform/deviceRegistry.js +26 -0
  62. package/dist/platform/platformConfigManager.js +89 -0
  63. package/dist/platform/platformState.js +9 -0
  64. package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
  65. package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
  66. package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
  67. package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
  68. package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
  69. package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
  70. package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
  71. package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
  72. package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
  73. package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
  74. package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
  75. package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
  76. package/dist/services/authentication/AccountAuthStrategy.js +135 -0
  77. package/dist/services/authentication/AuthContext.js +1 -0
  78. package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
  79. package/dist/services/authentication/IAuthStrategy.js +1 -0
  80. package/dist/services/authentication/TokenAuthStrategy.js +27 -0
  81. package/dist/services/authentication/UserDataRepository.js +32 -0
  82. package/dist/services/thinq/gateway.js +61 -0
  83. package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
  84. package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
  85. package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
  86. package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
  87. package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
  88. package/dist/services/thinq/serviceContainer.js +73 -0
  89. package/dist/services/thinq/session.js +48 -0
  90. package/dist/services/thinq/thinqApiClient.js +258 -0
  91. package/dist/services/thinq/thinqDeviceService.js +75 -0
  92. package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
  93. package/dist/settings.js +9 -0
  94. package/dist/tests/helpers/testUtils.js +87 -0
  95. package/dist/utils/mask.js +39 -0
  96. package/docs/README_CLI.md +166 -0
  97. package/eslint.config.js +194 -0
  98. package/matterbridge-lg-thinq-plugin.config.json +27 -0
  99. package/matterbridge-lg-thinq-plugin.schema.json +520 -0
  100. package/package.json +107 -0
  101. package/prettier.config.js +49 -0
  102. package/scripts/parse-junit-failures.mjs +45 -0
  103. package/scripts/parse-tsc-failures.mjs +48 -0
  104. package/scripts/run-build-local-summary.mjs +29 -0
  105. package/scripts/run-format-summary.mjs +36 -0
  106. package/scripts/run-lint-fix-summary.mjs +38 -0
  107. package/scripts/run-precommit-summary.mjs +94 -0
  108. package/src/cli/commands/devices.ts +67 -0
  109. package/src/cli/commands/energy.ts +160 -0
  110. package/src/cli/commands/login.ts +74 -0
  111. package/src/cli/help.ts +53 -0
  112. package/src/cli/main.ts +66 -0
  113. package/src/cli/session.ts +15 -0
  114. package/src/cli/types.ts +10 -0
  115. package/src/cli/utils.ts +30 -0
  116. package/src/cli.ts +3 -0
  117. package/src/constants/index.ts +8 -0
  118. package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
  119. package/src/core/domain/entities/ThinqDevice.ts +74 -0
  120. package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
  121. package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
  122. package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
  123. package/src/errors/AuthenticationError.ts +37 -0
  124. package/src/errors/BaseError.ts +38 -0
  125. package/src/errors/index.ts +24 -0
  126. package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
  127. package/src/module.ts +298 -0
  128. package/src/platform/deviceRegistry.ts +58 -0
  129. package/src/platform/platformConfigManager.ts +133 -0
  130. package/src/platform/platformState.ts +16 -0
  131. package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
  132. package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
  133. package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
  134. package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
  135. package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
  136. package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
  137. package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
  138. package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
  139. package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
  140. package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
  141. package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
  142. package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
  143. package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
  144. package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
  145. package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
  146. package/src/services/authentication/AccountAuthStrategy.ts +209 -0
  147. package/src/services/authentication/AuthContext.ts +19 -0
  148. package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
  149. package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
  150. package/src/services/authentication/IAuthStrategy.ts +10 -0
  151. package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
  152. package/src/services/authentication/TokenAuthStrategy.ts +40 -0
  153. package/src/services/authentication/UserDataRepository.test.ts +149 -0
  154. package/src/services/authentication/UserDataRepository.ts +46 -0
  155. package/src/services/thinq/gateway.ts +86 -0
  156. package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
  157. package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
  158. package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
  159. package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
  160. package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
  161. package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
  162. package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
  163. package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
  164. package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
  165. package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
  166. package/src/services/thinq/serviceContainer.test.ts +203 -0
  167. package/src/services/thinq/serviceContainer.ts +105 -0
  168. package/src/services/thinq/session.test.ts +287 -0
  169. package/src/services/thinq/session.ts +69 -0
  170. package/src/services/thinq/thinqApiClient.test.ts +702 -0
  171. package/src/services/thinq/thinqApiClient.ts +418 -0
  172. package/src/services/thinq/thinqDeviceService.test.ts +415 -0
  173. package/src/services/thinq/thinqDeviceService.ts +101 -0
  174. package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
  175. package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
  176. package/src/settings.ts +18 -0
  177. package/src/tests/cli/commands/devices.test.ts +293 -0
  178. package/src/tests/cli/commands/login.test.ts +240 -0
  179. package/src/tests/cli/help.test.ts +46 -0
  180. package/src/tests/cli/main.test.ts +214 -0
  181. package/src/tests/cli/session.test.ts +198 -0
  182. package/src/tests/cli/utils.test.ts +194 -0
  183. package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
  184. package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
  185. package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
  186. package/src/tests/helpers/testUtils.ts +118 -0
  187. package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
  188. package/src/tests/platform/deviceRegistry.test.ts +177 -0
  189. package/src/tests/platform/platformConfigManager.test.ts +790 -0
  190. package/src/tests/platform/platformState.test.ts +41 -0
  191. package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
  192. package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
  193. package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
  194. package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
  195. package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
  196. package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
  197. package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
  198. package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
  199. package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
  200. package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
  201. package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
  202. package/src/tests/settings.test.ts +33 -0
  203. package/src/utils/mask.ts +50 -0
  204. package/tsconfig.base.json +25 -0
  205. package/tsconfig.json +9 -0
  206. package/tsconfig.production.json +13 -0
  207. package/tsconfig.vitest.json +5 -0
  208. package/vite.config.ts +43 -0
  209. package/wiki/memory-archive.md +12 -0
  210. package/wiki/reference-workspaces.md +23 -0
@@ -0,0 +1,186 @@
1
+ # Project Shared Memory
2
+
3
+ Durable knowledge store. Read at session start **only** by: `technical-architect`, `investigator`, `implementer`, `reviewer`, `test-writer` (and `wiki-manager` as a gather-mode source). Other agents skip it.
4
+ It is version-controlled — commit and push changes so teammates can pull the latest knowledge.
5
+
6
+ **Pruning rules:** max 10 bullets per section, **max 2 lines per bullet**. When adding would exceed the cap, move the oldest/least-durable entry to `wiki/memory-archive.md` (never delete outright). `reviewer` enforces the caps whenever it appends. Compress wording — keep file paths and hard facts, drop rationale prose.
7
+
8
+ ---
9
+
10
+ ## Architecture Insights
11
+
12
+ <!-- Patterns and relationships discovered during analysis -->
13
+
14
+ - V1 room-detection flip-flop root cause (confirmed Aug 3, 2026, real S8 log + floor-plan photo): `MapInfoListener.tryParseV1MapBinary` (`mapInfoListener.ts:211-238`) resolves+publishes a room from EVERY independent V1 map push with zero temporal smoothing (`onV1RoomResolved`→`AreaManagementService.setV1ResolvedSegment`, unconditional overwrite). Rc05's containment tiers (PR #154) are correct per-snapshot; the miss is one layer up. Floor plans where one room (e.g. a corridor) borders most others amplify this: `resolveCurrentRoom`'s tier-3 `findNearestSegment` (bounding-box center, not true centroid) flips between different neighbors on tiny pose noise near that hub. Fix: 2-consecutive-match confirmation gate added to `setV1ResolvedSegment` (`v1PendingResolution` map) — see `workspace/room-detection-flip-flop-investigation/plan.md`.
15
+ - Live map updates: `resolveInitialAreas` must sync-bootstrap via `fetchAndApplyMapInfo`/`fetchAndApplyRoomMap` (ignore V2); public `getMapInfo`/`getRoomMap` stay V2-only when `liveMapUpdates`. `handleActiveMapChanged` must intersect with Matter `supportedAreas` before writing `selectedAreas`.
16
+ - Multi-map areas: partial room-map fetch/push merges by `mapId` via `mergeSupportedAreasByMap` (keeps other maps, re-indexes areaIds); full `mapInfo.allRooms` / V1 map-info push stays full replace.
17
+ - Multi-map + `enableMultipleMap`: ON → bootstrap all physical maps via `switchMap`+sync fetch, merge by mapId; OFF → primary only (`maps[0]`) via `getSupportedAreas(..., false)` — wired from `configManager.isMultipleMapEnabled`.
18
+ - No code path calls V1 `dispatcher.getHomeMap` (`get_map_v1`) on a recurring basis — `PollingService` only calls `getDeviceStatus`; `AreaManagementService.startPeriodicRefresh` only calls `getMapInfo` (`get_multi_maps_list`). A V1 position-based fallback needs its own explicit trigger.
19
+ - `MapInfoListener.tryParseB01MapBinary` skips `Protocol.map_response` when `deviceProtocol === ProtocolVersion.V1` (`mapInfoListener.ts:165`), but a SEPARATE `tryParseV1MapBinary` (`:211-238`) handles V1 map binaries via `decryptAndUnzipV1Map`→`resolveCurrentRoom`→`onV1RoomResolved`; confirmed firing correctly in real S8 logs (Jul 25, corrects a prior stale claim that V1 binaries were dropped entirely).
20
+ - `OperationStatusCode.WashingTheMop`(23) override (`stateResolver.ts:128-139`) writes `operationalState=FillingWaterTank` for ~2-3min at clean-start (real mop-pad wash phase) before `Running` — Apple Home master tile likely shows this as "Preparing" (unconfirmed, HomeKit-side); `RvcRunMode.currentMode`/`RvcCleanMode.currentMode` update correctly/promptly throughout, independent clusters.
21
+ - V1 `mapParser.ts` `resolveCurrentRoom`: exact-pixel-match tier only matches pixels where `parseImageBlock` recorded a segment — misses on wall/obstacle/type-0 pixels, common at room boundaries. Fallback tiers: (1) bounding-box containment check (single-match-only, Jul 28 merged), (2) nearest-centroid distance. **Jul 28:** `LegacySegmentInfo` now carries `boundingBox` (raw pixel-space); `findContainingSegment` private method inserted in `resolveCurrentRoom` between exact-pixel and `findNearestSegment`, returns single segment only (0 or 2+ → falls through to centroid). No shared point-in-rect helper (`legacyIslandDetection.ts` only has rect-rect `boxesOverlap`).
22
+ - V1 `"rr"`-frame block types beyond this repo's known 4 (charger=1, image=2, robot=8, cleaned=11): `FORBIDDEN_ZONES=9` (no-go zone), `VIRTUAL_WALLS=10`, `NO_MOP_ZONE=12` — same buffer, same `get_map_v1` RPC, no new RPC needed (corroborated byte-for-byte by ioBroker.roborock + roborock-gitlab, independent authors). Wall payload = 2 (x,y) endpoints/8 bytes; zone payload (9 and 12 share one decoder) = 4 corners/16 bytes; count field is `u32` at block offset `+0x08`, entries start at `dataPosition+hlength`. Coordinate units inferred as raw mm (same space as robotPosition), medium confidence only — no real captured buffer with these blocks populated exists anywhere checked (this repo or the 2 corroborating OSS repos). See `workspace/virtual-wall-detection-feasibility/answers-virtual-wall.md`.
23
+
24
+ - Greenfield repo confirmed Sep 8, 2026: `matterbridge-lg-thinq-plugin` had no `src/`/`package.json` at task start — `wiki/reference-workspaces.md` created this cycle (4 rows: matterbridge-example-dynamic-platform, homebridge-lg-thinq, homebridge-webos-tv, matterbridge-roborock-vacuum-plugin) for future investigator/wiki-manager lookups.
25
+
26
+ ## Known Patterns
27
+
28
+ <!-- Coding patterns established in this project -->
29
+
30
+ - **Commit messages:** `<type>(<optional scope>): <short summary>` + optional why-body. Types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `style`, `perf`. Imperative mood; no `Co-Authored-By`.
31
+ - Per-device override pattern: array of objects with key (`serialNumber`) + value (`productName`), gated by a boolean in `advancedFeature.settings`. See `DeviceProductNameOverride` (`RoborockPluginPlatformConfig.ts:58`).
32
+ - New config sections in schema use JSON Schema `if/then` blocks under `advancedFeature.allOf`.
33
+ - **Agent frontmatter:** `effort`/`maxTurns` after `color`, before `tools`; `tools` MUST be a comma-separated string — YAML block lists silently fail and the subagent falls back to Read/Write/Edit/Bash only.
34
+ - **Subagent toolsets (macOS, re-verified Jul 10 2026):** native `Glob`/`Grep` absent from Task subagents — `mcp__glob-grep__*` is the only working search. `LSP` newly reachable in wildcard-tools subagents via deferred ToolSearch (one probe; explicit `tools:` lists historically never got it) — don't rely on it in agent definitions.
35
+ - **Subagent task tracking:** `TaskCreate`/`TaskUpdate`/`TodoWrite` never reach Task subagents (verified Jul 10 2026, direct-executor probe) — never list them in agent frontmatter; EM tracks pipeline steps in the main session, subagents report plain text (use a `progress.md` file in the task folder if live status is ever needed).
36
+
37
+ ## Decisions Made
38
+
39
+ <!-- Architectural and design decisions with rationale -->
40
+
41
+ - LG ThinQ+webOS migration (Sep 8, 2026, greenfield `matterbridge-lg-thinq-plugin`): two device families, one platform — `services/thinq/`+`platform/thinq/` (cloud EMP auth+REST+poll) and `services/webos/`+`platform/webos/` (local SSAP websocket) share only `module.ts` entrypoint + one namespaced `node-persist` instance. Phased roadmap (Phase1=ThinQ AC only, Phase2=rest of ThinQ, Phase3=webOS, Phase4=MQTT/deferred, Phase5=ThinQ1-legacy/deferred). See `workspace/migrate-lg-thinq-to-matterbridge/plan.md`.
42
+ - Matter's `OperationalState`/`OvenMode`/`OvenCavityOperationalState`/`TemperatureControl` clusters (via `matterbridge/devices` classes `LaundryWasher`/`Dishwasher`/`Oven`/`MicrowaveOven`) cleanly replace homebridge-lg-thinq's `Valve`/`Television`+`InputSource` HomeKit idiom (no native HomeKit washer/oven service existed) — confirmed live in `matterbridge-example-dynamic-platform/src/module.ts:2599-3000`.
43
+ - webOS TV maps to Matterbridge `BasicVideoPlayer`+`Speaker` device classes (`MediaPlayback`/`KeypadInput` command handlers), not a custom composition — confirmed via `matterbridge-example-dynamic-platform/src/module.ts:2783-2816`.
44
+ - `MatterbridgeServiceAreaServer.selectAreas` (`@matterbridge/core`) unconditionally calls `super.selectAreas(request)` with the ORIGINAL request AFTER our command handler runs, writing `this.state.selectedAreas` directly (bypasses `updateAttribute`). Any empty-input resolution must intercept in `RoborockServiceAreaServer.selectAreas` and forward a RESOLVED request to `super.selectAreas`, not `updateAttribute` from `roborockVacuumCleaner.ts`'s command handler — that write always loses the race.
45
+ - `extra_time` → `estimatedEndTime` removed (Jul 2026); replacement: V1-only opt-in `enableEstimatedEndTime` (default off) + `clean_time`/`clean_percent` linear ETA in `src/share/estimatedEndTime.ts`; B01/Q7/Q10 stay `null`.
46
+ - `robot.homeInFo.activeMapId` inits to `-1` (`deviceConfigurator.ts:105`), only written via B01/Q7 `onActiveMapChanged` — V10/V1 NEVER updates it. `RoomIndexMap.getAreaId(roomId,mapId)` then always misses for V10/V1 multi-map; `getAreaIdV2(roomId)` is the correct fallback.
47
+ - No "currently selected map" Matter attribute exists — Apple Home infers active map from which map's rooms appear first in `supportedAreas`/`selectedAreas` (mirrors `roborockService.ts:350` `buildCleanCommand`).
48
+ - `handleActiveMapChanged` (`serviceAreaHandler.ts:234-275`) has no idle/cleaning guard — unconditionally overwrites `selectedAreas`/`currentArea`/`progress`; only matters if the device reports a genuinely different active map mid-clean.
49
+ - Shared state-update helper pattern (`applyResolvedStateUpdates`, `deviceStateHandler.ts:22-49`): extract identical Promise.all/snapshot/completion blocks into a private async helper; callers keep their own state resolution and return values.
50
+ - `AreaManagementService.supportedRoutines` (routine-as-room, `mapId=999`) is structurally separate from `supportedAreas` — "all rooms of active map" logic from `getSupportedAreas` excludes routines automatically.
51
+ - `homeInFo.activeMapId` alone is unreliable for active-map inference (stays -1 for V10/V1). Fallback order used in `SELECT_AREAS` empty-list handling: Matter `selectedAreas` mapId → `activeMapId` (if not -1) → first `supportedAreas` mapId.
52
+ - `RvcRunMode.ChangeToMode(Idle)` is now handled via `IdleModeHandler` (new, Jul 10 2026) → `roborockService.pauseClean`. `Mapping` deferred: `AbstractMessageDispatcher` (V10/Q7/Q10) has zero mapping/explore-start command.
53
+ - `getSupportedAreas()` (`initialData/getSupportedAreas.ts`) is the single point enforcing "Areas non-null mapId ⇒ supportedMaps non-empty" (Matter `#assertSupportedAreas`). `processValidData` branch always assigns numeric `mapId`; empty `mapInfo.maps` (e.g. `MapInfo.empty()` fallback) previously left `supportedMaps=[]` — fixed via `buildPlaceholderSupportedMaps` (pair, don't null — matches existing `createFallbackArea` convention).
54
+ - V1 currentArea fallback (planned): CLI's `extractNamedRooms`/`roomDisplayName` (`cli/mapListHelpers.ts`) stay CLI-only — runtime resolves `segmentId → areaId` via existing `roomIndexMap.getAreaId`/`getAreaIdV2`, doesn't need room names, so no move/share needed.
55
+ - `progress` reset-on-new-clean (Jul 13, 2026, revised): TWO reset triggers — `selectAreas` AND `handleServiceAreaUpdate`'s Idle branch (replaces old Operating→Completed flip with direct `[]` clear, since both writes happen in one sync call with no observable gap). NOT `RoborockService.startClean` — its `HandlerContext` chain never carries `robot`/`device`.
56
+ - `RoborockVacuumCleaner.roborockService` exposed as `public readonly` (not private) to enable behaviors/servers (e.g., `RoborockServiceAreaServer`) to call `setProgress()`/`getProgress()` directly without routing through platform. Companion to existing public readonly `device` and `homeInFo` fields.
57
+ - Dock→Matter mapping (Jul 2026): default bundle Items 1+2+3+4 in `DockStationStatus.ts` only; Item 3 vs 5 mutually exclusive (full dss vs dustBag-only); codes 32/33/35 as `DockErrorCode` enum; dss priority clearWater→dirty→dustBag→cleanFluid→filter→updown.
58
+ - `progress` reset on start-of-clean (Jul 14, 2026): 3rd trigger added — `handleServiceAreaUpdate` compares stored-vs-current `operationalState` "actively cleaning" classification (Docked/Stopped/Error = not cleaning), NOT raw `CLEANING_STATES` membership (misses `ReturningDock`/`EmptyingDustContainer` detours → false positives). Flag stored in `AreaManagementService` (proxied via `RoborockService`), not a module-level var.
59
+ - Vacuum→Matter mapping (Jul 2026): bundle ideas 2–6 in `VacuumStatus.ts`; export `VACUUM_ERROR_TO_MATTER`; unknown non-zero → `UnableToCompleteOperation`; add `VacuumErrorCode.AutoEmptyDockFanError=33`; 8 semantic refinements per spike answer.md.
60
+ - `dss` field `isUpdownWaterReady` (bits 0-1) does NOT reliably follow the shared Unknown/Error/OK convention on real hardware: dss=2729 shows it=1 (Error) persistently while idle/charging with zero other error signal — false positive. Excluded from `hasError()`/`DSS_FIELD_PRIORITY` (Jul 22, 2026 fix); still parsed, just not error-consulted. Prior Item-3 inclusion (Jul 2026) was only "medium confidence" (ioBroker label, no RVC enum) — now disproven. Re-check (Jul 22): python-roborock `v1_containers.py:277-278` is `hatch_door_status` (bits 12-14), NOT bits 0-1 as previously miscited — python-roborock parses no field at bits 0-1 at all, any version.
61
+ - ThinQ CLI login helper (Sep 8, 2026): `src/cli.ts`+`src/cli/` mirrors Roborock's CLI shape but is a NEW, standalone consumer of `AuthenticationCoordinator`/`AccountAuthStrategy`/`TokenAuthStrategy`/`ThinqApiClient` (same ctor args as `ThinqServiceContainer`) — bypasses `UserDataRepository`/node-persist/`PlatformConfigManager` entirely so it runs with zero platform bootstrap. Own flat `.cli-session.json` (gitignored) instead.
62
+ - ThinQ AC command wiring (Sep 9, 2026): NO custom Behavior subclass needed — `@matterbridge/core`'s `MatterbridgeOnOffServer`/`ThermostatServer`/`FanControlServer` already dispatch bare `addCommandHandler('on'/'off'/'setpointRaiseLower')` before running `super.<cmd>()` (attribute mutation). Only `FanControl.percentSetting` and `Thermostat.occupiedCoolingSetpoint`/`occupiedHeatingSetpoint` bypass Behavior overrides entirely (raw attribute-write) — need `subscribeAttribute` + `context.fabric===undefined` guard to avoid a feedback loop with our own state-sync `updateAttribute` writes. See `workspace/thinq-ac-command-wiring/plan.md`.
63
+ - ThinQ `sendCommand` command field: default `'Set'`, but AC power needs `command: 'Operation'` (homebridge-lg-thinq parity) — `ThinqCommandPayload` widened with optional `command?: string`, spread order already lets it override.
64
+ - ThinQ AC command wiring implemented (Sep 9, 2026): `THINQ_FAN_SPEED_*` constants live in `thinqAirConditionerCommandHandlers.ts` (moved+exported, +`HIGH=6`); `thinqDeviceConfigurator.ts` imports them back for `mapWindStrengthToFanMode` (now exported). `thinqAirConditionerStateSync.ts` must import the constants directly from the command-handlers file, NOT re-export through `thinqDeviceConfigurator.ts` (`tsc` TS2459 — non-exported locals aren't transitively visible via re-export chains).
65
+ - ThinQ AC per-device capability gating (Sep 9, 2026): `AirConditioner`'s constructor (`@matterbridge/core/dist/devices/airConditioner.js`) unconditionally builds full Heating+Cooling+AutoMode Thermostat + Auto+Step FanControl — CANNOT be narrowed after construction. `Behaviors.require()` (`@matter/node/src/endpoint/properties/Behaviors.ts:310-321`) THROWS on a 2nd call for the same cluster id (sibling `.with(features)` types are never mutually `instanceof`-compatible, confirmed via `ClusterBehaviorType.ts`). Reduced-feature devices MUST be hand-composed via `MatterbridgeEndpoint` primitives (`createDefault{Heating,Cooling}ThermostatClusterServer`, `create{OnOff,Base,MultiSpeed,Complete}FanControlClusterServer`) chosen per-device BEFORE any `require()` for that cluster — exact tiered pattern already demonstrated in `matterbridge-example-dynamic-platform/src/module.ts:1920-1980` (thermostat tiers) and `:2100-2230` (4 fan tiers).
66
+ - `roomAirConditioner`/`powerSource`/`MatterbridgeEndpoint` are exported from the `matterbridge` package ROOT only (`@matterbridge/core/dist/export.js:27,29`), NOT from the `matterbridge/devices` subpath (that subpath only re-exports device _classes_ like `AirConditioner`).
67
+ - Matter's `Thermostat.Feature` has no Dry/FanOnly flag — `SystemMode.Dry(8)`/`FanOnly(7)` are ungated plain enum values; `ThermostatServer#assertSystemModeChanging` (`ThermostatServer.ts:615-634`) only forbids Heat/Cool crossing `controlSequenceOfOperation`, never Dry/FanOnly — Dry-gating is code-only (state-sync must never emit it), not a Matter feature-map concern. This same guard DOES throw `ConstraintErrorError` synchronously on `updateAttribute`-driven writes (not just user writes) if state-sync ever pushes `Heat`/`Auto` onto a `CoolingOnly` endpoint — remap defensively.
68
+ - FanControl's `percentSetting`/`percentCurrent` are unconditionally mandatory (`conformance:"M"`, `@matter/model/.../fan-control.element.ts:33-67`) regardless of `Feature.Step` — matter.js does NOT block a percent write on a reduced/no-feature FanControl cluster; the plugin's own command handler must add the capability guard, no framework help exists. `FanModeSequence` has exactly 6 members: `OffLowMedHigh=0, OffLowHigh=1, OffLowMedHighAuto=2, OffLowHighAuto=3, OffHighAuto=4, OffHigh=5`.
69
+
70
+ - `overrideMatterConfiguration` port planned (Sep 19, 2026, simplified vs Roborock): reuses existing
71
+ `ThinqDeviceConfigEntry.productName?` (sibling of `capabilities`/`sceneButtons`), NOT a parallel
72
+ `deviceProductNames[]` array. No `enableAdvancedFeature` flag exists in this repo (verified via grep,
73
+ zero hits) — schema gates on `settings.overrideMatterConfiguration===true` alone via `dependencies`
74
+ (this repo's own idiom, matching `thinq.dependencies.loginType`), not `allOf`. See `workspace/override-matter-configuration/plan.md`.
75
+ - Energy layout (current since Sep 20, 2026): when `supportsEnergyMonitoring`, the AC endpoint itself has device types [roomAirConditioner, powerSource, electricalSensor] + PowerTopology + ElectricalPowerMeasurement (activePower starts 0, not null); no `EnergyMonitor` child.
76
+ Why: live iOS 27 tests showed the Energy tab and per-device detail only work with this layout AND a bridged AC (hardcoded `mode = 'server'` removed; server mode showed nothing); the old child layout gave only the whole-home total, blank detail.
77
+ - Partial LG MQTT push fix (Sep 19, 2026): state sync writes each Matter attr only if its raw key exists (`ThinqSnapshot.has`); systemMode w/o `airState.operation` derives power from current Matter `OnOff` (else skip); `onCurrent` = watts (no /100). See `workspace/partial-push-and-power-scale/plan.md`.
78
+ - ThinQ Washer v1 planned (Sep 22, 2026, read-only): Matter maps to `@matterbridge/core`'s ready-made `LaundryWasher` class (`matterbridge/devices`) used directly (`new LaundryWasher(name, id)`, zero hand-composition — no capability-gated feature tiers needed, unlike AC). `isWasherDevice()` scoped to exactly `type==='WASHER'` (deviceType 201) — `WASHER_NEW`/`WASH_TOWER`/`WASH_TOWER_2` stay unsupported (223 uses a different raw snapshot prefix `washer.*` vs `washerDryer.*`, unverified). All write commands (on/off/pause/stop/start/resume/changeToMode) `throw` inside `addCommandHandler` — `CommandHandler.executeHandler` has no try/catch so a throw aborts before local Matter attribute mutation (verified in `@matterbridge/core/dist/matterbridgeEndpointCommandHandler.js`). No washer capability-flag config/schema in v1 (no variable Matter feature set to gate). See `workspace/washer-integration/plan.md`.
79
+ - **CORRECTION to the above (Sep 22, 2026, verified directly against installed matterbridge 3.10.9 `matterbridgeBehaviors.js`/`NamedHandler.js`, not the file the v1 entry cited):** command dispatch (`on`/`off`/`pause`/`stop`/`start`/`resume`/`changeToMode`, all of them) calls `commandHandler.executeHandler(...)` WITHOUT `await`, then immediately mutates local Matter state and returns success — a throw in our async handler does NOT block mutation or block the success response to the controller. Any handler that must guarantee "no optimistic UI state on failure" needs an explicit revert-on-failure (`updateAttribute` back to the prior value in a `.catch()`), mirroring `revertSetpointOnFailure`/`revertToggleOnFailure` already in `thinqAirConditionerCommandHandlers.ts`/`thinqAirConditionerAuxiliaryToggles.ts` — a throw alone is not a safety mechanism.
80
+ - ThinQ Washer v2 planned (Sep 22, 2026, real control — plan-v2-control.md): scope narrowed to ONE real command, `OperationalState.stop`, double-gated (per-device config opt-in default-off AND a `ControlWifi.WMStop`/`WMOff` payload resolved from the device's own `modelJsonUri` at registration time, fetched via new `ThinqApiClient.getDeviceModel()`). `start`/`changeToMode` confirmed structurally inseparable from a course `dataSetList` payload in `ioBroker.lg-thinq` (`main.js:3289-3306`, `helper.js:1316-1405`) — no safe path without new course-table infra. No `pause` primitive exists at the ThinQ API layer at all (exhaustive grep, zero hits) — `pause`/`resume` stay permanently rejected, not aliased to stop/start. See `workspace/washer-integration/answers-washer-v2-control.md`.
81
+ - ThinQ Washer v2 Stop + v1 revert-on-throw implemented (Sep 22, 2026): `OperationalState.id`/`LaundryWasherMode.id` (and every other `matterbridge/matter/clusters` cluster namespace, e.g. `OnOff.id`) export `id` directly on the namespace in the installed matterbridge 3.10.9 (`@matter/types` re-export) — NOT nested under `.Cluster.id` as some upstream matter.js source suggests; use `<Cluster>.id` directly, matches existing codebase usage. `updateAttribute`'s 3rd param (`value`) must be typed as the attribute's real type (`number`/`boolean`), not `unknown`/`unknown|undefined` — `unknown` fails all 3 overloads (TS2769); read via `getAttribute(...) as T | undefined` then `?? fallback` before passing in.
82
+ - ThinQ AC filter monitoring planned (Sep 23, 2026, verified directly against installed SDK):
83
+ `HepaFilterMonitoring` (id 113/0x71) is an OPTIONAL serverCluster directly on the `RoomAirConditioner`
84
+ device type (`room-air-conditioner-device.element.js`, conformance "O") — no child endpoint/AirPurifier
85
+ device type needed, unlike humidity/air-quality sensors. `MatterbridgeEndpoint.createDefaultHepaFilterMonitoringClusterServer(condition=100, changeIndication=Ok, inPlaceIndicator=true, lastChangedTime=null, replacementProductList=[])`
86
+ already exists (same family as `createDefaultRelativeHumidityMeasurementClusterServer`), hardcodes
87
+ `degradationDirection: Down` (matches "remaining life %", decreasing = worse). The fabricated
88
+ "FilterMaintenance" (0x0050) cluster from an earlier bad AI answer does not exist in the SDK at all.
89
+ Data source: `ThinqApiClient.sendCommandAndGetResponse` (already added for `--probe-filter` CLI flag)
90
+ POSTs `control-sync` with `ctrlKey:'filterMngStateCtrl', command:'Get'` — REQUEST uses singular
91
+ `airState.filterMngState.*` keys, but the RESPONSE comes back keyed with PLURAL
92
+ `airState.filterMngStates.useTime`/`.maxTime`/`.changeDate` (confirmed live, `remainTime`/`type`
93
+ requested but never returned — compute remaining as `maxTime-useTime`). See `workspace/ac-filter-monitoring/plan.md`.
94
+ - ThinQ AC filter monitoring implemented (Sep 23, 2026): `ThinqFilterState.fromRaw` returns `undefined`
95
+ (not zeroed) when `useTime`/`maxTime` missing or `maxTime<=0`; `ThinqFilterMonitoringService` is a
96
+ standalone interval-timer service (separate from `ThinqDeviceService`), `serviceContainer.getFilterMonitoringService()`
97
+ singleton; `applyThinqFilterStateToAirConditioner` (new file, not `thinqAirConditionerStateSync.ts`)
98
+ writes `HepaFilterMonitoring.condition`/`changeIndication` only when state is defined.
99
+ - Device-type-scoped config UI + per-device product identity planned (Sep 24, 2026): `MatterbridgeEndpoint.createDefaultBasicInformationClusterServer(...)` is a PLAIN PROPERTY SETTER (`this.vendorId=...`, `matterbridgeEndpoint.js:549-566`), NOT a `Behaviors.require()` call — safe to re-invoke after `new LaundryWasher(...)` to override vendor/product identity (no throw-on-2nd-call risk, unlike Thermostat/FanControl). `LaundryWasherOptions` itself still has no vendor/product fields — override must be applied via this post-construction re-call, not constructor options. Product identity (name/id) moves from one global `matterOverrideSettings` fallback to per-device-type coded defaults (`DEFAULT_PRODUCT_IDENTITY`, AC=32768/'LG Air Conditioner', WASHER=32769/'LG Washer') + per-device override; vendor identity (name/id) stays global. See `workspace/device-type-scoped-settings/plan.md`.
100
+ - ThinQ AC filter RESET (v2) planned (Sep 23, 2026, verified against installed SDK):
101
+ `@matterbridge/core/dist/behaviors/hepaFilterMonitoringServer.js` `MatterbridgeHepaFilterMonitoringServer.resetCondition()`
102
+ DOES `await device.commandHandler.executeHandler(...)` before writing `state.condition=100`/
103
+ `changeIndication=Ok`/`lastChangedTime=now` directly — those writes are SKIPPED entirely if our
104
+ `addCommandHandler('resetCondition', ...)` handler throws, so a failed real ThinQ `Set` call can
105
+ never flip the attributes. Do NOT manually `updateAttribute` those fields on success — the SDK
106
+ already does it, would race/duplicate. `ThinqApiClient.sendCommand` already supports arbitrary
107
+ `ctrlKey`/`command`/`dataSetList` overrides (proven by washer's `stop`) — no new API-client method
108
+ needed; reset is `sendCommand(id, {ctrlKey:'filterMngStateCtrl', command:'Set', dataSetList})`.
109
+ New registration kept in its OWN file/function (`registerFilterResetCommandHandler`), NOT folded
110
+ into `registerAirConditionerCommandHandlers` — that function has 39 existing call sites in its test
111
+ file that a new required param would break. See `workspace/ac-filter-monitoring/plan-v2-reset.md`.
112
+
113
+ ## Test Patterns
114
+
115
+ - **ThinQ partial push presence-gating tests (Sep 19, 2026):** `ThinqSnapshot.has(key)` returns true iff `data[key]` is neither undefined nor null (falsy values like 0, false, '' still count as present). `applyThinqSnapshotToAirConditioner` gates each Matter attribute write on `snapshot.has(sourceKey)`: operation required for OnOff/0-write, operation+opMode for systemMode (or opMode+OnOff-current-true for power-derived mode), windStrength for fanMode, swing axes for rockSetting. systemMode derives from opMode only when: operation=0 (→ Off), operation=1+opMode present (→ mapped), operation=1 only (→ skip), opMode only+OnOff true (→ mapped), else skip. Energy: operation present gates the zero-when-off fallback. Debug log two-line format: first lists pushed attributes, second lists skipped. `applyAuxiliaryToggleSnapshot` skips toggle when spec.dataKey absent from snapshot.
116
+ - **CLI utility tests (parseArgs, maskSecret, saveSession):** pure functions/I/O mocks — `parseArgs` handles flags/values, boolean flags (`--debug` → `'true'`), consumes next arg if not flag-prefixed; `maskSecret` preserves length, returns empty for falsy, fully masks if `value.length <= visibleChars`, else `'*'.repeat(length-visible) + value.slice(-visible)`. `saveSession` via `vi.mock('node:fs')` + spy `fs.writeFileSync`; verify formatted JSON (2-space indent) matches round-trip. Don't test edge cases (e.g. `visibleChars=0`) unless in plan.
117
+ - **ModeResolver.createDefaultModeResolver/createSmartModeResolver tests:** gated feature branches (e.g., OneTime→VacFollowedByMop mode 11) require two test paths — unsupported configs array (baseCleanModeConfigs, smartPlanModeConfig only) + supported array (inject vacFollowedByMopModeConfig). Test both branches and priority order (OneTime before isCustomMode before exact-match). Serialization key is `suctionPower:waterFlow:mopRoute` (sequenceType omitted).
118
+ - **RoborockServiceAreaServer.selectAreas override tests:** spy on parent prototype's selectAreas via `vi.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(server)), 'selectAreas')` and mock its response to avoid deep matterbridge machinery; verify override calls `device.resolveAllRoomsForActiveMap()` for empty input, `device.trySwitchMap(areas)` for non-empty input, forwards resolved/original request to super with correct arguments.
119
+ - **Status listener error-code tests (V1StatusListener, handleHomeDataMessage):** listener fires `onError` whenever error-related field is _defined_ (including 0/None), not only non-zero; test three cases: payload with None/0 codes, field-absent (no call), sequential error→clear (both called). For home-data, `updateRobotWithPayload(ErrorOccurred)` only if errorCode defined; missing field never dispatches.
120
+ - **handleCleaningWithoutInfo + resolveV1CurrentArea tests:** test realistic multi-room `selectedAreas = [0, 1, 2...]` (not empty arrays) to match real-world scheduled-clean scenarios. Verify V1 resolution attempts before falling back to pin-first-area. Cover: fresh cache (resolves correct room), cache miss (pin-first + refresh), segment outside selectedAreas (pin-first + no refresh), non-V1 protocol (pin-first + no V1 calls), single area (immediate pin, no resolution), zero areas (null + empty).
121
+ - **DockStationStatus tests:** parse raw dss integers, 5-field priority in `hasError()`/`getMatterOperationalError()` (clearWater→dirty→dustBag→cleanFluid→filter, EXCLUDE isUpdownWaterReady). `isUpdownWaterReady` still parsed but false-positive on real hardware (dss=2729 idle = bit Error), verified via real-log fixture. Use `createDockStatus()` helper + `OK`/`ERR` constants for override tests.
122
+ - **resolveAreaFromCleaningInfo falsy-zero tests:** test areaId=0 separately (segment→0 resolves as 0, not null); verify `mappedArea === undefined` check (not `!mappedArea`). RoomIndexMap mock: multi-area roomMapData + roomInfo for segment→areaId mapping.
123
+ - **handleCleaningWithoutInfo last-known-area tests:** mock `getAttribute(ServiceArea.id, 'currentArea')` to return prior value; test first-update (undefined → fallback selectedAreas[0]), mid-run valid (→ reuse), mid-run falsy-zero (0 in selectedAreas[0,1,2] → use 0), stale value (not in selectedAreas → fallback). Use separate mockImplementation for each currentArea return value.
124
+ - **ModeResolver.resolveFallback category-aware tests:** import `vacuumOnlyModeConfigs`, `mopOnlyModeConfigs`, `vacuumAndMopModeConfigs` alongside `baseCleanModeConfigs`; test Max/Balanced/Quiet suction with non-canonical water/route → match same-category mode (not Default); test vacuum-off/mop-off branches; test unrecognized suction → Default fallback. Exact-triple match still takes priority (no regression).
125
+ - **LegacyMapParser tests (virtual walls/zones, containment tier):** V1 block decoders via `buildWallBlock`/`buildZoneBlock` (8-byte-per-wall, 16-byte-per-zone stride); truncated buffers drop partial entries. Bounding-box containment tier (new Jul 28): tests verify 5 scenarios — (1) containment wins over numerically-closer centroid, (2) ambiguous 2+ matches fall back to nearest-center, (3) zero matches fall back to nearest-center, (4) exact-pixel match still takes priority, (5) namedRooms name lookup works through containment path. Fixtures use multi-pixel segments to test bbox containment; robot mm→pixel conversion uses Y-flip: `pyRel = height - 1 - (pyAbs - top)`. Island detection: 4-connectivity BFS only (diagonal non-adjacent), wall tolerance ±3px, size-heuristic 30% threshold per-island (not map-wide).
126
+ - **V1 room-detection debounce tests (AreaManagementService.setV1ResolvedSegment/getV1ResolvedSegment, Aug 3, 2026):** debounce requires 2 consecutive matching calls to confirm; test matrix: (1) single call → undefined (not published), (2) two same → published, (3) two-matching + one-outlier → still first value (outlier doesn't reset published, only resets pending counter), (4) outlier + two-matching-new → new value published, (5) all-different sequence → never published, (6) clearAll() resets both pending+confirmed maps (test single call before/after clear), (7) per-duid isolation. Existing TTL tests must call setV1ResolvedSegment twice before time assertions.
127
+ - **ThinQ AC capability-gating tests (Sep 14, 2026):** `resolveAirConditionerCapabilities` pure resolver function tests cover: undefined/empty array → all-true default, device not found → default, missing capabilities key → default, mixed true/false per-device, multiple devices isolated resolution. `mapOperationModeToSystemMode` regression parity critical: default capabilities must produce identical SystemMode for every opMode/isPowerOn combo vs pre-change behavior; test Heat/Dry/Auto remapping when unsupported. `registerAirConditionerCommandHandlers` capability gating: supportsHeat=false → no heat subscribeAttribute (assert mock never called with attribute name), supportsFanSpeedControl=false → no-op on percent/fanMode writes + debug log. `applyThinqSnapshotToAirConditioner` parity + gating: full-support default parity assertion on all updateAttribute calls, heat setpoint skipped when unsupported, percentCurrent skipped when fan control unsupported. Minimal MatterbridgeEndpoint mock (log, addCommandHandler, subscribeAttribute, updateAttribute as vi.fn()) cast as `unknown as MatterbridgeEndpoint` to avoid 190+ property interface bloat.
128
+ - **ThinqServiceContainer singleton tests (Sep 16, 2026):** DI container getters (apiClient, getUserDataRepository, getAuthenticationCoordinator, getDeviceService, getDeviceDiscovery, getDeviceConfigurator, getMqttListener) all return fresh instances on first call, then same instance on repeated calls (singleton pattern, use `toBe` for identity check, not `toEqual`). Verify inter-instance wiring (e.g., `getDeviceDiscovery()` reuses same `ThinqDeviceService` as `getDeviceService()`) via bracket-notation private-field access; if public accessors exist, prefer those. Constructor parameters (logger, persist, configManager, mqttCertDir) flow through to constructed dependencies via mocks (`asPartial`).
129
+ - **ThinqSession tests (Sep 16, 2026):** Boundary-condition tests for expiry: `isExpired()` returns `false` at exact equality (check source: `expiresAtEpochSeconds < now`, so equal is NOT expired). Fake-timer setup required: `vi.useFakeTimers()` → `vi.setSystemTime(date)` → verify at boundary → `vi.useRealTimers()`. Round-trip serialization via `toData()` → `fromData()` must preserve all field values including falsy `''` accessToken. Static `expiryFromExpiresIn(seconds)` adds to current epoch (mocked via fake timers). `updateAccessToken()` mutates in-place (accessToken and expiresAtEpochSeconds), refreshToken immutable (`readonly`). All 4 hasValidToken boolean branches covered (empty+expired, empty+valid, has+expired, has+valid).
130
+ - **ThinqMqttListener.connect() happy path (Sep 16, 2026):** `aws-iot-device-sdk` mock requires constructor function returning mockDevice (not arrow function), placed at top-level `vi.mock()` block: `device: function() { return mockMqttDevice }` — enables `new awsIotDevice(...)`. Route → Key → CSR → Register → Certificate → ConstructDevice → WireEvents execution order verified via call tracking (not exact-time sequencing, simpler and clearer for this async flow). `wireMqttDeviceEvents` internally calls `device.on()`, verify via spy on that method. Separate happy-path tests from retry-exhaustion path (`start integration` tests) — split reduces mock fragmentation.
131
+ - **ThinqDeviceService.keepAliveOnce() tests (Sep 17, 2026):** Timer-based keep-alive tests using `vi.useFakeTimers()` + `advanceTimersByTimeAsync()`. `discoverDevices()` filters invalid device IDs via UUID regex (`isValidThinqDeviceId`), so test fixtures must use valid UUIDs (not strings like `'online-ac'`) or devices are silently dropped before `isAirConditionerDevice()` check, causing empty `airConditionerOnlineStatus` map. AC device filtering happens post-UUID validation. Error handling per-device (swallow + debug log, never throw out of interval callback), mirrors `pollOnce` pattern. Track `online` status in map during polling tick, reuse in keep-alive tick.
132
+ - **ThinQ AC Phase B swing-mode tests (Sep 18, 2026):** `isVerticalSwingOn`/`isHorizontalSwingOn` snapshot getters: value `100` (numeric) and `'100'` (string JSON cross-serialization) both return `true`; zero/absent/other → false (matches `isPowerOn` boolean-always contract, NOT optional-number). `supportsSwingMode` capability flag gates new `createCompleteFanControlClusterServer` call (vs. `createDefaultFanControlClusterServer`); test 3-way branch: (1) both `supportsFanSpeedControl` + `supportsSwingMode` true → Complete with Rocking features enabled + initial state false, (2) fan-speed true + swing false → Default (old behavior, regression), (3) fan-speed false + swing true → OnOff (swing silently ignored, no error thrown). `rockSetting` subscribeAttribute: handler guards on `supportsSwingMode` (mirror `percentSetting`'s guard pattern). Diff individual axes (vertical/horizontal) via old vs. new values, ignore rockRound-only changes. Compound `dataSetList` write (`ctrlKey:'favoriteCtrl'`) only when BOTH axes flip to true **simultaneously**; single-axis or asymmetric flips use individual writes. Handler ignores undefined new/old values. State-sync push: `rockRound` computed as `vertical && horizontal`, per reference behavior.
133
+ - **ThinQ AC Phase C sensor tests (Sep 18, 2026):** `humidityPercent` snapshot getter: value `<=100` → passthrough, `>100` → divide by 10 (heuristic for backend reporting in wrong scale). `airQualityOverall`/`pm25`/`pm10`: passthrough numbers or `undefined`. Capability flags `supportsHumiditySensor`/`supportsAirQualitySensor` default false (opt-in). Endpoint factory: `addChildDeviceType('HumiditySensor', [humiditySensor])` when humidity enabled, chained with `createDefaultRelativeHumidityMeasurementClusterServer(0)` and separate `createDefaultAirQualityClusterServer` + pm2.5/pm10 cluster servers when air-quality enabled; test via mock's `addChildDeviceType` spy filtering by child name. Regression parity: default capabilities (both false) yield zero child endpoint creation. State-sync push: test defensive child-exists check (getChildEndpointById may return undefined — no crash), value-undefined skip (even if capability true, don't push if snapshot value undefined), full parity with default capabilities on existing attributes (power/mode/temp/fan).
134
+ - **ThinQ AC Phase D energy monitoring tests (Sep 19, 2026):** `powerConsumptionWatts` getter returns raw `airState.energy.onCurrent` as real watts (no /100; raw 185 vs app 0.18 kW); key absent or NaN → undefined. State sync writes `Math.round(watts * 1000)` mW to `ElectricalPowerMeasurement.activePower` on the AC endpoint (no child), gated on `supportsEnergyMonitoring` (default false); since Sep 20 the write is 0 when AC is known off (snapshot `airState.operation` = 0, else endpoint OnOff exactly false) because LG reports a floor of about 50 when off.
135
+ - **ThinQ AC Phase F scene buttons tests (Sep 19, 2026):** Scene buttons config is NOT a capability flag (no `resolveAirConditionerCapabilities`), carried as `ThinqSceneButtonConfig[]` in config only. `addSceneButtonEndpoints`: sanitize each button's name via `replaceAll(' ', '')`, deduplicate by index if two buttons collide (e.g. "Power Off" + "PowerOff" both → "PowerOff", "PowerOff0"). `genericSwitch` device type + `createDefaultMomentarySwitchClusterServer()` per button (no other clusters). `registerSceneButtonCommandHandlers`: same deduplication, `addCommandHandler('toggle', ...)` per button (MomentarySwitch cluster's press event → 'toggle' command), sends `{ dataKey: 'airState.opMode', dataValue: button.opMode }` with standard error handling (no special gates like toggles). Endpoint factory receives buttons via optional `BuildAirConditionerEndpointOptions` param (not capability-gated, called unconditionally if non-empty). PlatformConfigManager getter: `getSceneButtons(deviceId)` → `config.thinq.devices.find(...).sceneButtons ?? []` (mirrors getDeviceCapabilities shape).
136
+ - **ThinQ token-refresh dual-trigger tests (Sep 19, 2026):** `ThinqApiClient.request()` retries on 401 OR HTTP 400 + `resultCode:'0102'` via single `isTokenExpiredError()` predicate. Test 5 parallel cases (vs. existing 401 cases): (1) 0102-retry-succeeds via `getListHomes`, stub refresh, assert resolves to homes; (2) 0102-retry-exhausted throws `TokenExpiredError` both calls return 400+0102; (3) `sendCommand`-to-control-sync 0102 self-heals, verify POST count = 2; (4) different `resultCode` (e.g. 0110) under 400 rejects without retry/refresh attempt; (5) malformed/absent body (400 with no data, or 400 + non-JSON string) doesn't crash, rejects with 1 GET call and 0 refresh calls. Use `mockAxios.history.post.filter((h) => h.url?.includes('oauth2/token'))` length assertion to prove (no-)refresh attempt.
137
+ - **ThinQ Washer command handler extraction pattern (Sep 22, 2026):** Mock handler callbacks from `vi.mocked(mockEndpoint.addCommandHandler).mock.calls` require explicit type annotation `(call: any[])` on `.find()` / `.filter()` callbacks (fixes TS7006 implicit-any errors). Replace non-null assertion `![1]` with optional chaining + explicit cast: `const stopCall = vi.mocked(...).find((call: any[]) => call[0] === 'stop'); const handler = stopCall?.[1] as () => Promise<void>;` — avoids forbidden non-null operator. CLI function argument expansion (3-arg cmdDevices): test both no-flag (third arg = undefined) and with-flag (third arg = flag-value) paths separately.
138
+ - **ThinqDeviceConfigurator.registerAirConditioner regression test (Sep 20, 2026):** No hardcoded `mode: 'server'` assignment — mock endpoint pre-seeded with `mode: 'server'` was removed, test asserting `result.mode === 'server'` was removed. NEW regression test: endpoint returned from `registerAirConditioner` has `mode` undefined (proving the function doesn't set it). Mock endpoint factory removed the seed and only returns chainable mock methods (`createDefaultTemperatureMeasurementClusterServer`, `addRequiredClusterServers`). Existing capability/handler-wiring tests unaffected.
139
+
140
+ ## Common Pitfalls
141
+
142
+ <!-- Things to avoid — bugs found, anti-patterns, footguns -->
143
+
144
+ - `handleCleaningWithoutInfo`'s `selectedAreas[0]` pin (`serviceAreaHandler.ts:311`) DOES reuse last-known `currentArea` first (PR #150/ef1efb1) via `robot.getAttribute(ServiceArea.id,'currentArea',...)` — this is the ONLY call site of that read in `src/`; re-verified Aug 3, 2026 against a fresh real-S8 log: all 34 occurrences match the prior write correctly. A prior note claiming this read was unreliable and had an Aug-25 `AreaManagementService.lastKnownAreaCache` fix was WRONG — that field never existed in `src/` (confirmed via grep) — do not rely on it existing.
145
+ - `resolveAreaFromCleaningInfo`'s falsy-zero check is fixed (`mappedArea === undefined`, `serviceAreaHandler.ts:405`) — confirmed correct Jul 25, no gap.
146
+ - `DockStationStatus.isUpdownWaterReady` (bits 0-1 of dss) is parsed/observable but EXCLUDED from `hasError()`/`getMatterOperationalError()` (Jul 22, 2026 fix, `rvc-unable-to-complete-cleaning-error`). Real hardware shows it=1 (Error) as steady-state idle/charging, not transient fault — unreliable error signal, unlike the 5 other dss fields.
147
+ - `modeResolver.ts`'s `createDefaultModeResolver`/`createSmartModeResolver` `customCheckFn` OneTime branch (pre-fix Jul 22) unconditionally returned `VacFollowedByMop` mode (11) regardless of `configs` — real `seq_type` wire field (`v1StatusListener.ts:96`) reaches it on ANY non-B01 device (`connectionService.ts:161`), not just capability-gated ones. Fix: gate on `configs.some(mode===11)`, computed once per factory call (closures already have `configs` in scope, no signature change).
148
+ - CORRECTED (Sep 17, 2026): prior "Phase 1 auth fields not exposed in schema.json" note was stale — fixed same-day by commit `7366426` (`dependencies.if/then/else` block, mirrors roborock's `authenticationMethod` pattern). Schema/config.json now fully sync w/ `LgThinqPluginPlatformConfig.ts`; verified field-by-field Sep 17. See `workspace/schema-config-gap/plan.md`.
149
+ - `refreshIntervalSeconds` default mismatch (Sep 17, 2026): `platformConfigManager.ts:16` code fallback = 5s, but `schema.json`/`config.json` advertise `default:60`. Only bites if the field is truly absent from saved config (not a normal-UI-path issue) — flagged, not fixed.
150
+ - AC keep-alive gap confirmed real (Sep 17, 2026): reference's `temperatureKeepAliveCommandForDevice` (`AirConditioner.ts:644-661`) POSTs `service/devices/{id}/control` w/ `ctrlKey:'allEventEnable'`, `dataKey:'airState.mon.timeout'=70`, every 60s — DIFFERENT call than the GET-based poll (`platformMonitor.ts:104-123` runs both concurrently, proving non-redundancy). This repo's poll (`thinqDeviceService.ts` `getListDevices`, GET only) does NOT cover it — zero hits for `allEventEnable`/`mon.timeout` in `src/` pre-fix. See `workspace/ac-keepalive-heartbeat/plan.md`.
151
+ - ThinQ MQTT push (Phase 4, Sep 15, 2026 plan): reference (`homebridge-lg-thinq`) uses `aws-iot-device-sdk` (^2.2.16) + `node-forge` (^1.4.0) for cert-based AWS-IoT MQTT, NOT the generic `mqtt` npm package — confirmed via reference `package.json`. Cert flow: `GET https://common.lgthinq.com/route` → `POST service/users/client` → `POST service/users/client/certificate` (csr) — all through the SAME authenticated `ThinqApiClient.request()` (absolute URIs pass through `new URL(uri, base)` unaffected). Root-CA PEM download (amazontrust/sectigo/digicert) is a plain unauthenticated GET — reference attaches ThinQ auth headers to it too (harmless but unnecessary), our port deliberately skips that.
152
+ - ThinQ MQTT message shape confirmed identical to REST snapshot shape: `{ deviceId, data: { state: { reported: {...} } } }`, and `reported` is the same flat `airState.*`-keyed bag `ThinqSnapshot`'s constructor already accepts — zero new mapping logic needed, both polling and MQTT feed the exact same `ThinqDeviceUpdateListener` callback.
153
+ - ThinQ MQTT push implemented (Sep 15, 2026): `services/thinq/mqtt/{mqttCertificate,mqttConnectionEvents,mqttRetry,mqttKeyRepository,thinqMqttListener}.ts` (new) + `thinqApiClient.ts`/`serviceContainer.ts`/`module.ts` wiring, per `workspace/thinq-mqtt-push/plan.md`. `MqttRuntimeDevice.on()` needed `'connect' | 'offline'` combined into ONE overload signature — ESLint `@typescript-eslint/unified-signatures` fails otherwise (2 overloads differing only in event-name literal, same handler shape).
154
+ - `npm install` (plain, no args) SILENTLY REMOVES a prior `npm link matterbridge` symlink from `node_modules/matterbridge` (matterbridge isn't a `package.json` dependency, only linked) — any implementer adding a new npm dependency mid-task must `npm link matterbridge` again afterward, or `type-check:ci`/`tsc` floods with `Cannot find module 'matterbridge'` across the whole repo (false-looking whole-codebase break, actually just the missing link).
155
+
156
+ ## Module Notes
157
+
158
+ <!-- Notes about specific modules, non-obvious behaviors -->
159
+
160
+ - `roomNameNormalizer.ts` (b01): pure module, no imports. `normalizeB01RoomName(roomName, roomTypeId?, roomId?)` returns non-empty; callers may still chain `|| fallback`. `rr_other` (typeId 0) always resolves to `Room ${roomId}`.
161
+
162
+ - Q10 map format: python-roborock's `lz4_block_decompress` (`b01_q10_map_parser.py:190-237`) is a hand-rolled zero-dep LZ4 _block_-format decoder, NOT a call into the Python `lz4` package — no size param, decodes until input exhausted.
163
+ - ioBroker's Q10 parser lives at `ioBroker.roborock/src/lib/map/q10/Q10YxMapParser.ts` (not `b01/`) — a structurally different "YxMap" format (28-byte header, version/pixLen/pixLzLen fields) vs python-roborock's `01 01`+offset-27/29 layout; only LZ4-block-format + big-endian u16 width/height are cross-corroborated.
164
+ - Q10 fix landed: `b01MapParser.ts.parseRoomsFromEncryptedBinary` routes `0x01 0x01`-prefixed payloads to new `b01Q10MapParser.ts`/`lz4BlockDecompressor.ts` (hand-rolled, zero-dep); Q7 AES+zlib pipeline untouched. `0x02 0x01` trace packets now route to `b01Q10TraceParser.ts` (`parseQ10TracePacket` → `B01MapInfo.currentPose` = last point; rooms/mapId/roomMatrix always empty/undefined) instead of falling through to the Q7 path — superseded a same-day minimal "recognize+skip, no parse" fallback that had landed on `dev` first.
165
+ - LG ThinQ washer snapshot shape (Sep 22, 2026, cross-checked homebridge-lg-thinq + ioBroker.lg-thinq): raw key prefix `washerDryer.*` for deviceType 201/221/222, but `washer.*` (remapped) for 223. Fields confirmed read: `state` (raw string enum: POWEROFF/POWERFAIL/RUNNING/PAUSE/ERROR/END/etc, NOT model-resolved), `remainTimeHour`/`remainTimeMinute` (zeroed when not running), `doorLock`/`remoteStart` (DeviceModel-lookup enums), `TCLCount` (maintenance counter). No course/cycle/spin-speed/water-temp fields read by either reference. `homebridge-lg-thinq`'s washer is 100% read-only (zero `deviceControl()` calls); `ioBroker.lg-thinq` DOES send washer commands (course download/start/stop via a heavy model-JSON-driven `dataSetList` builder, `lib/helper.js:1316-1405`) but has no TS precedent and no local sample model JSON to verify against. See `workspace/washer-integration/answers-washer-reference.md`.
166
+ - `0x02 0x01` confirmed = python-roborock `Q10TracePacket` (10-byte header + repeating i16be x/y pairs, robot_position=last point); NOT corroborated by ioBroker (different wire format, no `02 01` marker at all). Real-hardware repro: 598B during active cleaning vs 7076B full map. Live-validated on real Q10 S5+: real changing (x,y) coordinates, zero crashes across 8 invocations. `mapInfoListener.ts`/`roomMatrixResolver.ts` needed zero changes (already consume `currentPose` generically); area/room wiring stays out of scope.
167
+
168
+ - ThinQ token-refresh dual-trigger (Sep 19, 2026, `thinq-resultcode-0102-fix`): `ThinqApiClient.request()`'s reactive refresh-and-retry-once now fires on HTTP 401 OR HTTP 400 body `resultCode:'0102'` (LG's own expiry signal, `control-sync` uses this shape) via one `isTokenExpiredError()` predicate — mirrors `homebridge-lg-thinq/src/lib/request.ts:80-97`. Co-located `thinqApiClient.test.ts` already covered 401 retry; extended with parallel 0102 cases.
169
+ - `FanControl` cluster (Matter model, `@matter/model/src/standard/elements/fan-control.element.ts:33-67`): `PercentSetting`/`PercentCurrent` are conformance `"M"` (mandatory, unconditional) — NOT gated by any Feature bit. Only SpeedMax/Setting/Current (MultiSpeed), Rock*(Rocking), Wind*(Wind), AirflowDirection(AirflowDirection), and the `Step` command itself (Step feature) are feature-conditional.
170
+ - `createOnOffFanControlClusterServer` (zero Feature flags) still fires `subscribeAttribute(FanControl,'percentSetting',...)` normally — matter.js has no protocol-level rejection for feature-less fan writes; caller must self-guard. Proven both by conformance model and live example (`matterbridge-example-dynamic-platform/src/module.ts:2163-2196`, `fanOnHigh`). `FanModeSequence` enum has exactly 6 members (0-5): OffLowMedHigh, OffLowHigh, OffLowMedHighAuto, OffLowHighAuto, OffHighAuto, OffHigh.
171
+ - `matterbridge-example-dynamic-platform/src/module.ts:1920-1980,2100-2230` has a full reduced-feature demo block: heating-only/cooling-only thermostats + 4 fan tiers (default/base/on-off/complete), each a manual `MatterbridgeEndpoint([fan,...])` composition with its own subscribeAttribute clamp/guard — reusable pattern for per-device Matter feature gating.
172
+
173
+ ## Open Questions
174
+
175
+ <!-- Unresolved questions for the team -->
176
+
177
+ - Does our TypeScript plugin call `APP_GET_INIT_STATUS`? If so, are `newFeatureInfo`/`newFeatureInfoStr`/`featureInfo` captured and stored?
178
+ - `roomMatrix` (RobotMap field 13) confirmed undecoded in python-roborock/ioBroker.roborock/roborock-gitlab too — all define it, none decode it (room-pixel data comes from `roomChain`/occupancy grid instead). Still needs real Q10 packet capture.
179
+ - `OperationStatusCode` 104: confirmed absent from canonical status enum in all 3 reference repos (identical `103→202` gap everywhere). The only "104" found is an unrelated DP-id (`BREAKPOINT_CLEAN`), not a status value — coincidence, not the answer.
180
+
181
+ - Coverage-gate test-plan (Sep 16, 2026): `src/module.ts` (`LgThinqMatterbridgePlatform`) stays an accepted gap a 3rd time — `vi.mock('matterbridge')`-stubbing `MatterbridgeDynamicPlatform`'s base-class chain (protected ctor, `@matterbridge/core`) is theoretically viable (smaller surface than `MatterbridgeEndpoint`) but unproven and not required: Tiers 1-4 in `workspace/coverage-threshold-gap/test-plan.md` alone clear the 75% floor (+71 lines/+70 stmts/+16 fn/+32 branches needed).
182
+ - Repo has TWO test-file layouts, both current: colocated (`src/services/**/*.test.ts`, e.g. `serviceContainer.test.ts`) and centralized (`src/tests/**/*.test.ts`, e.g. `cli/*`, some `platform/thinq/*`). Match the sibling directory's existing convention, don't invent a third.
183
+
184
+ ## Archive
185
+
186
+ Older entries and one-off research (B01/Q7/Q10 current-room detection, featureSet decoding reference) live in `wiki/memory-archive.md` — consult it when working on those areas.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: Engineer Manager
3
+ description: Orchestrator persona for this repo — classify, dispatch subagents, gate approvals; never write code directly. Writes in plain English.
4
+ keep-coding-instructions: true
5
+ ---
6
+
7
+ # Engineer Manager
8
+
9
+ You are the **Engineer Manager (EM)** for the Matterbridge Roborock Vacuum Plugin. You run in the **main session** and coordinate specialist subagents through the `Agent` tool. You are never a subagent yourself, and subagents never talk to each other.
10
+
11
+ **`.claude/instructions/team-orchestrator-policy.md` is the full, authoritative procedure** (load with `/load-policy`; the SessionStart hook also reminds you). This style holds only your identity and your non-negotiables. Detailed steps — the exact pipeline, which artifacts to publish at the gate, the task-folder layout, the verification gates — live in the policy, not here, so the two never drift. When in doubt, follow the policy.
12
+
13
+ ## How I write to you
14
+
15
+ I keep every message easy to read for a non-native English reader.
16
+
17
+ - I start with the main point in one short line.
18
+ - I use short sentences — one idea each.
19
+ - I use simple, common words. No idioms or phrasal verbs.
20
+ - I explain any code or technical term in plain words, in brackets.
21
+ - I use bullets and small tables instead of long paragraphs.
22
+ - When I need a choice from you, I end with one clear line: **Next: …**
23
+ - I cut filler. I say only what matters.
24
+
25
+ ## Prime directive
26
+
27
+ **Never write production code, tests, or fixes yourself. Never edit `src/` or `src/tests/`.** All implementation, tests, and code fixes flow through subagents. My job is to classify work, dispatch the fewest capable specialists, review their summaries, and hold approval gates. If a verification step fails, I re-spawn or resume the responsible agent with the compact output — I never patch it myself.
28
+
29
+ ## How I operate
30
+
31
+ - **Echo first.** I restate the request in plain English before spawning. I confirm for medium/high; I auto-confirm obvious low.
32
+ - **Low → lite path.** I spawn `direct-executor` with the request plus scope limits, then review its report.
33
+ - **Medium/High → full pipeline** through `technical-architect`. I **never skip the business-brief approval gate, and never implement before approval.** The policy has the exact steps and the artifacts to publish.
34
+ - **Explain questions → `technical-architect` in explain mode.** I do not read `src/` or search code myself.
35
+ - **I stay lean.** I review each subagent's ≤10-line summary, not its files. I never paste raw exploration into chat. I use the compact `*:ci` scripts only.
36
+ - **Models come from agent frontmatter.** I never pass `model:` except `implementer` on high (`"sonnet"`).
37
+ - **I don't do the specialists' jobs.** No production code or tests from me. I never spawn `wiki-manager` (gather) or `investigator` directly — the architect nests them.
38
+ - **Nothing commits without you.** No `Co-Authored-By`; subagents never run git.
@@ -0,0 +1,40 @@
1
+ {
2
+ "statusLine": {
3
+ "type": "command",
4
+ "command": "bash .claude/statusline-command.sh"
5
+ },
6
+ "enabledMcpjsonServers": ["glob-grep"],
7
+ "includeCoAuthoredBy": false,
8
+ "enabledPlugins": {
9
+ "ponytail@ponytail": false,
10
+ "pyright-lsp@claude-plugins-official": false,
11
+ "github@claude-plugins-official": false,
12
+ "code-simplifier@claude-plugins-official": false
13
+ },
14
+ "permissions": {
15
+ "allow": []
16
+ },
17
+ "hooks": {
18
+ "SessionStart": [
19
+ {
20
+ "hooks": [
21
+ {
22
+ "type": "command",
23
+ "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"You are the MAIN SESSION acting as Engineer Manager. Read .claude/instructions/team-orchestrator-policy.md before orchestrating any task; subagents must NOT read it. ECHO FIRST: restate the requirement in simple English (user may write Vietnamese) before spawning. LOW -> direct-executor lite path. MEDIUM/HIGH -> full pipeline with business-brief approval gate; never implement before approval.\"}}'",
24
+ "timeout": 5
25
+ }
26
+ ]
27
+ }
28
+ ],
29
+ "UserPromptSubmit": [
30
+ {
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "codegraph prompt-hook"
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run test:*)",
5
+ "Bash(npm run build:local:*)",
6
+ "Bash(npm run coverage:*)",
7
+ "Bash(npm run type-check:*)",
8
+ "Bash(npm run precommit:*)",
9
+ "mcp__codecov__get_pull_request_coverage",
10
+ "mcp__plugin_claude-mem_mcp-search__smart_search",
11
+ "mcp__plugin_claude-mem_mcp-search__smart_unfold",
12
+ "Glob",
13
+ "Grep",
14
+ "mcp__codegraph__*",
15
+ "Bash(node scripts/clean-paths.mjs*)",
16
+ "Bash(codegraph *)",
17
+ "Bash(git log*)",
18
+ "Bash(git add*)",
19
+ "Bash(git status*)",
20
+ "mcp__read-log__ReadLog"
21
+ ]
22
+ },
23
+ "enabledMcpjsonServers": ["codegraph", "glob-grep", "cli-runner", "read-log", "discord-send", "github-release"],
24
+ "statusLine": {
25
+ "type": "command",
26
+ "command": "bash .claude/statusline-command.sh"
27
+ },
28
+ "outputStyle": "Engineer Manager",
29
+ "spinnerTipsEnabled": true
30
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: load-policy
3
+ description: "Force-loads the team orchestration policy into the current session. Use when Claude has not read the orchestration policy or it is not active. Main session only — subagents must not load this."
4
+ ---
5
+
6
+ Read and apply the orchestration policy and agent prompts now:
7
+
8
+ @.claude/instructions/team-orchestrator-policy.md
9
+ @.claude/instructions/agent-prompts.md
10
+
11
+ Confirm to the user: "Policy loaded. Acting as Engineer Manager."
12
+
13
+ **Progress tracking:** the EM registers pipeline steps with `TaskCreate`/`TaskUpdate` (main session only — these tools never reach subagents). Subagents report results in plain text; no internal checklists.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: ref-idea
3
+ description: "Research the reference codebases for an idea/solution, then optionally apply it to this repo. Usage: /ref-idea <question>. Main session (EM) only — runs the normal investigation pipeline with the standard reference-research boilerplate pre-filled."
4
+ ---
5
+
6
+ You are the Engineer Manager. The user wants to research the reference codebases for an idea and possibly apply it here. The user's question is in the arguments. Run the **normal pipeline** — this skill only pre-fills the boilerplate.
7
+
8
+ ## Phase 1 — Research
9
+
10
+ 1. **Echo** the question in 1–3 bullets of simple English and confirm (the user may write in Vietnamese). If the arguments are empty or unclear, ask for the question first.
11
+ 2. **Check the allowlist** — `wiki/reference-workspaces.md` must exist and list the reference repo paths. If it is missing or has unfilled `<path>` placeholders, ask the user for the paths and update the file before spawning anything (investigator may only read paths listed there).
12
+ 3. **Create the task folder** — `workspace/<short-task-description>/requirement.md` with `type: explain` and this boilerplate baked in:
13
+
14
+ ```markdown
15
+ ## Question
16
+ <the confirmed question>
17
+
18
+ ## Research rules
19
+ - FIRST check `.claude/memory.md` and any earlier `answer.md`/`answers-*.md` files — investigate only the gaps; if memory already answers it, say so and stop.
20
+ - Sources in priority order from `wiki/reference-workspaces.md` (top first). Stop early once the question is answered.
21
+ - CodeGraph works on this repo only — use Grep/Glob/Read inside reference workspaces (read-only).
22
+ - If a needed source is unreachable or not allowlisted, report it in the summary instead of improvising.
23
+
24
+ ## Deliverable — answer.md
25
+ - Verdict: does a solution exist? (yes / partial / no / already known)
26
+ - Per-source findings with file:line evidence
27
+ - Applicability: how each idea maps onto our codebase (which layer/service it would touch)
28
+ - Recommended candidate(s), numbered, so the user can say "apply idea N"
29
+ ```
30
+
31
+ 4. **Spawn `technical-architect`** (explain mode) per the normal policy — resume `ta_id` only if it still holds context on the same topic; otherwise spawn fresh.
32
+ 5. **Present `answer.md`** to the user. Do not read `src/` or the reference workspaces yourself.
33
+
34
+ ## Phase 2 — Apply (only when the user picks an idea)
35
+
36
+ When the user says "apply idea N" (or similar):
37
+
38
+ 1. Start a **normal implement cycle**: new `requirement.md` (`type: implement`, assess complexity) that cites `workspace/<research-task>/answer.md` as the design input — do not re-explain the idea in chat.
39
+ 2. Full pipeline as usual: architect → **user approval** → implementer → reviewer → test-writer → documenter. No implementation before brief approval.
40
+
41
+ ## Rules
42
+
43
+ - This skill changes nothing about the pipeline — echo gate, approval gate, and model policy all apply as normal.
44
+ - Never let research and implementation share one cycle: Phase 1 produces `answer.md`, Phase 2 consumes it.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: status-of
3
+ description: "Feature status report: what we currently do for <feature>, what is missing, what is planned, and past decisions. Usage: /status-of <feature>. Main session (EM) only — runs explain mode with a status-report contract. Often used before /ref-idea."
4
+ ---
5
+
6
+ You are the Engineer Manager. The user wants a **status report** on a feature area — not just how it works, but what exists, what is missing, and what was already decided. The feature is in the arguments.
7
+
8
+ ## Flow
9
+
10
+ 1. **Echo** the feature in one line of simple English and confirm (the user may write in Vietnamese). If the arguments are empty, ask what feature to report on.
11
+ 2. **Create the task folder** — `workspace/<short-task-description>/requirement.md` with `type: explain` and this contract baked in:
12
+
13
+ ```markdown
14
+ ## Question
15
+ What is the current status of: <feature>?
16
+
17
+ ## Sources to check (beyond normal explain research)
18
+ - `.claude/memory.md` — past decisions, pitfalls, and open questions about this area
19
+ - `workspace/to_do.md` — planned or deferred work touching this area
20
+ - `workspace/claude_history.md` — recent completed work touching this area (skim titles; read only matching entries)
21
+ - Source code — confirm what actually exists today (wiki/memory claims must be verified against `src/`)
22
+
23
+ ## Deliverable — answer.md
24
+ - **Works today** — what the plugin currently does for this feature (plain language)
25
+ - **How it works** — brief mechanism with file paths
26
+ - **Gaps / half-done** — what is missing, stubbed, or deferred (cite evidence: TODO, stub, memory note)
27
+ - **Planned** — matching items from to_do.md, or "none"
28
+ - **Past decisions** — relevant decisions from memory.md with their reasons, or "none"
29
+ - **Suggested next step** — optional: a ready-to-use `/ref-idea <question>` line if a gap needs reference research
30
+ ```
31
+
32
+ 3. **Spawn `technical-architect`** (explain mode) per the normal policy — resume `ta_id` only if it still holds context on the same area; otherwise spawn fresh.
33
+ 4. **Present `answer.md`** to the user. Do not read `src/`, `wiki/`, or the docs sources yourself — only the task-folder artifacts.
34
+
35
+ ## Rules
36
+
37
+ - This is a read-only report — no plan, no brief, no implementation. If the user wants a change afterward, that is a new cycle (or `/ref-idea` first if the gap needs reference research).
38
+ - Normal pipeline rules apply — echo gate, model policy, ≤10-line TA summary.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: workspace-console
3
+ description: Rebuild and update the Workspace Console artifact from local workspace/ tasks. Appends/updates tasks in a persistent gitignored index, then publishes to the SAME artifact URL so nothing is lost. Use when the user wants to refresh their workspace dashboard, add a task to it, or asks for /workspace-console. Main session only — a subagent cannot publish artifacts.
4
+ ---
5
+
6
+ # Workspace Console
7
+
8
+ Keep a single tabbed dashboard Artifact in sync with the local `workspace/` task folders — **including gitignored, ephemeral ones**. The artifact is a _render_ of a persistent index, never appended to one task at a time, so re-publishing never drops earlier tasks.
9
+
10
+ **Main session only.** Only the main session holds the `Artifact` tool; a subagent cannot publish. Do not delegate the publish step.
11
+
12
+ ## Model
13
+
14
+ - **Source of truth:** `workspace/.console-index.json` — a gitignored, local, append-only index. It survives `finalizer` cleanup of ephemeral `workspace/<task>/` folders, so a task stays on the dashboard even after its folder is deleted.
15
+ - **Renderer:** `scripts/workspace-console.mjs` (committed) turns the index into `workspace/.console.html` (gitignored). It owns all layout — never hand-write the HTML.
16
+ - **Publish target:** the `artifactUrl` field in the index (seeded with the canonical Workspace Console URL). Passing it as `url` updates that artifact **in place** — same link.
17
+
18
+ ## Arguments
19
+
20
+ - **(none)** → upsert **every** `workspace/*/` task folder into the index, then render + publish.
21
+ - **`<folder>`** (e.g. `/workspace-console todo-task`) → upsert **only** that one folder; every other task already in the index is preserved.
22
+
23
+ ## Steps
24
+
25
+ 1. **Ensure the index exists:** run `node scripts/workspace-console.mjs init` (no-op if it already exists — never clobbers).
26
+ 2. **Pick scope:** all `workspace/*/` folders (skip dotfiles and non-directories), or just the folder argument. Ignore the permanent helpers `to_do.md` / `claude_history.md` unless the user asks to include them.
27
+ 3. **Distill each in-scope task** by reading whatever exists in the folder (`requirement.md`, `business-brief.md`, `plan.md`, `change-map.md`, `answer.md`, `README.md`, proposals) into one index entry:
28
+ - `id` — the folder name (stable key; this is what makes it an upsert, not a duplicate).
29
+ - `title` — short human name.
30
+ - `status` — one of `done` · `in-progress` · `pending` · `deferred` · `reference`.
31
+ - `source` — the most representative file path.
32
+ - `summary` — 1–3 plain sentences.
33
+ - `keyPoints` — up to ~5 bullets (plain strings).
34
+ - `changeMapMermaid` — _optional_ raw Mermaid source (no fences) if the task has a `change-map.md`; the renderer wraps it in `<pre class="mermaid">`.
35
+ 4. **Upsert into `workspace/.console-index.json`:** replace the entry with the same `id`, or append if new. **Never remove other tasks.** Refresh the top-level `updatedAt` (ISO string). Keep `artifactUrl` as-is.
36
+ 5. **Render:** `node scripts/workspace-console.mjs render` → writes `workspace/.console.html`.
37
+ 6. **Read** `workspace/.console.html` (you did not author it directly), then **publish** with the `Artifact` tool:
38
+ - `file_path`: `workspace/.console.html`
39
+ - `url`: the `artifactUrl` from the index (**required** — omitting it mints a new link and defeats the purpose)
40
+ - `title`: `Workspace Console — Roborock Plugin` · `favicon`: `🤖` (keep both stable across runs)
41
+ - `description`: one line, e.g. "Tabbed dashboard of the Roborock plugin workspace tasks."
42
+ 7. **Report** the artifact URL and how many tasks it now carries.
43
+
44
+ ## Notes
45
+
46
+ - **Deleting a task** from the dashboard is deliberate: remove its entry from the index by `id`, then render + publish. Don't do it unless asked.
47
+ - **Cross-machine:** the index is local by default (matches gitignored tasks). To share one dashboard across machines instead, move the index to a committed path (e.g. `.claude/workspace-console.json`) and update `INDEX_PATH` in `scripts/workspace-console.mjs` — but that puts task summaries in git.
48
+ - **Ownership:** updating in place requires being logged into the account that owns the artifact. If the `url` update is ever rejected as not-owned, publish fresh (no `url`), then save the new URL into the index's `artifactUrl`.
49
+ - **Lost URL?** `Artifact` `action: "list"` finds it by title (`Workspace Console — Roborock Plugin`).