@mmerterden/multi-agent-pipeline 20.33.0 → 20.35.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 (390) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/README.md +3 -1
  3. package/README.tr.md +2 -1
  4. package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
  5. package/docs/adr/README.md +2 -1
  6. package/docs/facts.json +5 -4
  7. package/docs/features.md +1 -1
  8. package/install/_dev-only-files.mjs +1 -0
  9. package/install/catalog-history.json +1 -1
  10. package/install/claude.mjs +24 -2
  11. package/install/templates/claude-hooks.json +34 -1
  12. package/manifest.json +393 -339
  13. package/package.json +3 -1
  14. package/pipeline/agents/bulk-reader.md +3 -0
  15. package/pipeline/agents/code-reviewer.md +9 -0
  16. package/pipeline/agents/explorer.md +4 -0
  17. package/pipeline/agents/plan-critic.md +4 -2
  18. package/pipeline/commands/multi-agent/SKILL.md +17 -136
  19. package/pipeline/commands/multi-agent/channels/SKILL.md +16 -298
  20. package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
  21. package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
  22. package/pipeline/commands/multi-agent/office-on/SKILL.md +7 -3
  23. package/pipeline/commands/multi-agent/office-status/SKILL.md +3 -2
  24. package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
  25. package/pipeline/commands/multi-agent/review/SKILL.md +7 -131
  26. package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
  27. package/pipeline/contract/CHANGELOG.md +9 -0
  28. package/pipeline/contract/README.md +6 -2
  29. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  30. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  31. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  32. package/pipeline/contract/manifest.json +14 -1
  33. package/pipeline/contract/types/index.d.ts +24 -2
  34. package/pipeline/lib/json-file-lock.mjs +61 -15
  35. package/pipeline/lib/office-remote.mjs +228 -0
  36. package/pipeline/lib/qr-code.mjs +360 -0
  37. package/pipeline/lib/repo-hygiene.sh +1 -0
  38. package/pipeline/lib/run-completion.mjs +17 -0
  39. package/pipeline/multi-agent-refs/channels/generator.md +146 -0
  40. package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -1
  41. package/pipeline/multi-agent-refs/channels/menu.md +55 -0
  42. package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
  43. package/pipeline/multi-agent-refs/channels/preview.md +25 -0
  44. package/pipeline/multi-agent-refs/channels/reference.md +98 -0
  45. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
  46. package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
  47. package/pipeline/multi-agent-refs/features/office-remote-protocol.md +166 -0
  48. package/pipeline/multi-agent-refs/features/office-threat-model.md +2 -2
  49. package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
  50. package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
  51. package/pipeline/multi-agent-refs/features/scope-check.md +25 -1
  52. package/pipeline/multi-agent-refs/features/verify-by-test.md +10 -0
  53. package/pipeline/multi-agent-refs/keychain.md +7 -0
  54. package/pipeline/multi-agent-refs/knowledge.md +9 -0
  55. package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
  56. package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
  57. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +8 -10
  58. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +18 -20
  59. package/pipeline/multi-agent-refs/phases/phase-3-review.md +20 -14
  60. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +8 -7
  61. package/pipeline/multi-agent-refs/phases/phase-5-report.md +6 -3
  62. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  63. package/pipeline/multi-agent-refs/resume/details.md +35 -0
  64. package/pipeline/multi-agent-refs/review/inputs.md +87 -0
  65. package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
  66. package/pipeline/multi-agent-refs/review/valven.md +42 -0
  67. package/pipeline/multi-agent-refs/rules.md +3 -2
  68. package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
  69. package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
  70. package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
  71. package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
  72. package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
  73. package/pipeline/multi-agent-refs/setup/language.md +46 -0
  74. package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
  75. package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
  76. package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
  77. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  78. package/pipeline/office/NOTICE.md +4 -1
  79. package/pipeline/office/assets/char_12.png +0 -0
  80. package/pipeline/office/assets/char_13.png +0 -0
  81. package/pipeline/office/assets/char_14.png +0 -0
  82. package/pipeline/office/assets/char_15.png +0 -0
  83. package/pipeline/office/assets/char_16.png +0 -0
  84. package/pipeline/office/assets/char_17.png +0 -0
  85. package/pipeline/office/assets/char_18.png +0 -0
  86. package/pipeline/office/assets/char_19.png +0 -0
  87. package/pipeline/office/assets/char_20.png +0 -0
  88. package/pipeline/office/assets/char_21.png +0 -0
  89. package/pipeline/office/assets/char_22.png +0 -0
  90. package/pipeline/office/assets/char_23.png +0 -0
  91. package/pipeline/office/assets/char_24.png +0 -0
  92. package/pipeline/office/assets/char_25.png +0 -0
  93. package/pipeline/office/assets/char_26.png +0 -0
  94. package/pipeline/office/assets/char_27.png +0 -0
  95. package/pipeline/office/assets/char_28.png +0 -0
  96. package/pipeline/office/assets/char_29.png +0 -0
  97. package/pipeline/office/assets/char_30.png +0 -0
  98. package/pipeline/office/index.html +304 -55
  99. package/pipeline/office/toolkit.json +1 -1
  100. package/pipeline/rules/response-shape.md +6 -7
  101. package/pipeline/schemas/agent-state.schema.json +36 -0
  102. package/pipeline/schemas/phases.json +4 -4
  103. package/pipeline/schemas/plan-critique.schema.json +9 -2
  104. package/pipeline/schemas/planning-output.schema.json +43 -2
  105. package/pipeline/schemas/pr-reach.schema.json +55 -0
  106. package/pipeline/schemas/prefs.schema.json +37 -8
  107. package/pipeline/schemas/reviewer-output.schema.json +20 -2
  108. package/pipeline/schemas/token-budget.json +4 -4
  109. package/pipeline/schemas/triage-output.schema.json +32 -3
  110. package/pipeline/schemas/unattended-policy.json +2 -1
  111. package/pipeline/scripts/_graph-analysis.mjs +270 -0
  112. package/pipeline/scripts/_guard_rules.py +449 -1
  113. package/pipeline/scripts/_memory-policy.mjs +165 -0
  114. package/pipeline/scripts/_secret-scan.mjs +105 -0
  115. package/pipeline/scripts/_test-gaming.mjs +36 -0
  116. package/pipeline/scripts/_unified-diff.mjs +2 -2
  117. package/pipeline/scripts/agent-guard.py +79 -19
  118. package/pipeline/scripts/anonymize-findings.mjs +99 -0
  119. package/pipeline/scripts/answer-check.mjs +113 -0
  120. package/pipeline/scripts/answer-check.sh +20 -0
  121. package/pipeline/scripts/autopilot-publish.mjs +9 -2
  122. package/pipeline/scripts/capture-flush.sh +12 -2
  123. package/pipeline/scripts/check-read-size.sh +9 -0
  124. package/pipeline/scripts/code-graph-rules/android.json +8 -3
  125. package/pipeline/scripts/contract-server.mjs +71 -16
  126. package/pipeline/scripts/debt-ledger.mjs +323 -0
  127. package/pipeline/scripts/file-context.mjs +261 -0
  128. package/pipeline/scripts/file-context.sh +20 -0
  129. package/pipeline/scripts/gate-ledger.mjs +144 -21
  130. package/pipeline/scripts/graph-links.mjs +230 -0
  131. package/pipeline/scripts/graph-query.mjs +133 -1
  132. package/pipeline/scripts/graph-report.mjs +65 -0
  133. package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
  134. package/pipeline/scripts/learnings-ledger.mjs +143 -24
  135. package/pipeline/scripts/office-build.mjs +276 -5
  136. package/pipeline/scripts/office-control.mjs +70 -10
  137. package/pipeline/scripts/office-pair.mjs +407 -0
  138. package/pipeline/scripts/office-remote-client.mjs +463 -0
  139. package/pipeline/scripts/phase-tracker.sh +8 -0
  140. package/pipeline/scripts/phase0-exit-gate.mjs +40 -37
  141. package/pipeline/scripts/phone-devices.mjs +9 -1
  142. package/pipeline/scripts/plan-critique-gate.mjs +11 -2
  143. package/pipeline/scripts/pre-commit-check.sh +28 -2
  144. package/pipeline/scripts/pre-push-check.sh +6 -12
  145. package/pipeline/scripts/rework-base.mjs +87 -0
  146. package/pipeline/scripts/rework-rulings.mjs +106 -0
  147. package/pipeline/scripts/run-commit-check.mjs +104 -0
  148. package/pipeline/scripts/run-scope-guard.mjs +58 -3
  149. package/pipeline/scripts/run-scope-guard.sh +13 -1
  150. package/pipeline/scripts/scope-check-gate.mjs +213 -2
  151. package/pipeline/scripts/task-mirror.mjs +106 -15
  152. package/pipeline/scripts/task-mirror.sh +3 -2
  153. package/pipeline/scripts/test-stability-gate.mjs +112 -0
  154. package/pipeline/scripts/triage-memory.mjs +64 -37
  155. package/pipeline/scripts/update-check.sh +7 -1
  156. package/pipeline/scripts/update-issue-progress.sh +73 -8
  157. package/pipeline/scripts/validate-planning.mjs +172 -4
  158. package/pipeline/scripts/validate-reviewer.mjs +20 -0
  159. package/pipeline/scripts/validate-triage.mjs +149 -4
  160. package/pipeline/scripts/work-items.mjs +119 -1
  161. package/pipeline/scripts/worktree-prepare-all.mjs +200 -0
  162. package/pipeline/scripts/worktree-prepare.sh +40 -4
  163. package/pipeline/scripts/write-state.mjs +67 -24
  164. package/pipeline/skills/.skill-manifest.json +9 -9
  165. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +5 -5
  166. package/pipeline/skills/shared/core/google-play-compliance/references/rules.yml +1 -1
  167. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +1 -1
  168. package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
  169. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
  170. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +2 -2
  171. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +2 -0
  172. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
  173. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +14 -0
  174. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +6 -0
  175. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +10 -0
  176. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +11 -0
  177. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +8 -0
  178. package/pipeline/skills/shared/external/android-jetpack-compose-expert/references/patterns.md +6 -0
  179. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +10 -0
  180. package/pipeline/skills/shared/external/android-performance/references/patterns.md +13 -0
  181. package/pipeline/skills/shared/external/android-security/references/patterns.md +16 -0
  182. package/pipeline/skills/shared/external/api-patterns/references/contract-details.md +9 -0
  183. package/pipeline/skills/shared/external/api-security-best-practices/references/abuse-controls.md +12 -0
  184. package/pipeline/skills/shared/external/api-security-best-practices/references/identity-and-access.md +13 -0
  185. package/pipeline/skills/shared/external/api-security-best-practices/references/request-handling.md +12 -0
  186. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +9 -0
  187. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +8 -0
  188. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +10 -0
  189. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +21 -0
  190. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +9 -0
  191. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +7 -0
  192. package/pipeline/skills/shared/external/app-store-review/SKILL.md +2 -2
  193. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +10 -0
  194. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +10 -1
  195. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +22 -0
  196. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +17 -0
  197. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +22 -0
  198. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +17 -0
  199. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +9 -0
  200. package/pipeline/skills/shared/external/authentication/references/passkeys.md +11 -0
  201. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +13 -0
  202. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +9 -0
  203. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +10 -0
  204. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +15 -0
  205. package/pipeline/skills/shared/external/compose-components/references/patterns.md +7 -0
  206. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +7 -0
  207. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +15 -0
  208. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +9 -0
  209. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +11 -0
  210. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +10 -0
  211. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +13 -0
  212. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +20 -0
  213. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +16 -0
  214. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +16 -0
  215. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +14 -0
  216. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +13 -0
  217. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +10 -0
  218. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +10 -0
  219. package/pipeline/skills/shared/external/fastapi-pro/references/app-structure.md +10 -0
  220. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +17 -0
  221. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +12 -0
  222. package/pipeline/skills/shared/external/hig-app-icons/references/icon-workflow.md +10 -0
  223. package/pipeline/skills/shared/external/hig-components-content/references/content-views.md +10 -0
  224. package/pipeline/skills/shared/external/hig-components-layout/references/bars-and-tabs.md +11 -0
  225. package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +13 -0
  226. package/pipeline/skills/shared/external/hig-foundations/references/type-color-spacing.md +13 -0
  227. package/pipeline/skills/shared/external/hig-patterns/references/onboarding-and-states.md +11 -0
  228. package/pipeline/skills/shared/external/hig-platforms/references/ipad-layout.md +12 -0
  229. package/pipeline/skills/shared/external/hig-technologies/references/technologies.md +16 -0
  230. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +9 -0
  231. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +18 -0
  232. package/pipeline/skills/shared/external/ios-accessibility/references/large-content-and-appearance.md +9 -0
  233. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +9 -0
  234. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +7 -0
  235. package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +13 -0
  236. package/pipeline/skills/shared/external/ios-design-review/references/appearance-matrix.md +9 -0
  237. package/pipeline/skills/shared/external/ios-design-review/references/checklist.md +15 -0
  238. package/pipeline/skills/shared/external/ios-haptics/references/core-haptics.md +11 -0
  239. package/pipeline/skills/shared/external/ios-iphone-duo/references/api-map.md +11 -0
  240. package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +15 -0
  241. package/pipeline/skills/shared/external/ios-iphone-duo/references/poses-and-panes.md +11 -0
  242. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +18 -0
  243. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +19 -0
  244. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +15 -0
  245. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +7 -0
  246. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +8 -0
  247. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +12 -0
  248. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +17 -0
  249. package/pipeline/skills/shared/external/ios-security/references/app-review-guidelines.md +8 -0
  250. package/pipeline/skills/shared/external/ios-security/references/cryptokit-advanced.md +14 -0
  251. package/pipeline/skills/shared/external/ios-security/references/file-storage-patterns.md +8 -0
  252. package/pipeline/skills/shared/external/ios-security/references/privacy-manifest.md +9 -0
  253. package/pipeline/skills/shared/external/ios-sf-symbols/references/effects-and-custom-symbols.md +9 -0
  254. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +13 -0
  255. package/pipeline/skills/shared/external/kotlin-coroutines-expert/references/patterns.md +8 -0
  256. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +21 -0
  257. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +9 -0
  258. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -0
  259. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +16 -0
  260. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +21 -0
  261. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +10 -0
  262. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +9 -0
  263. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +12 -0
  264. package/pipeline/skills/shared/external/nodejs-backend-patterns/references/runtime-patterns.md +11 -0
  265. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +11 -0
  266. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +18 -0
  267. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +11 -0
  268. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +11 -0
  269. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +15 -0
  270. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +14 -0
  271. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +14 -0
  272. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +14 -0
  273. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +14 -0
  274. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +10 -0
  275. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +12 -0
  276. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +14 -0
  277. package/pipeline/skills/shared/external/room-database/references/patterns.md +16 -0
  278. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +10 -0
  279. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +10 -0
  280. package/pipeline/skills/shared/external/spm-build-analysis/references/build-optimization-sources.md +14 -0
  281. package/pipeline/skills/shared/external/spm-build-analysis/references/spm-analysis-checks.md +19 -0
  282. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +13 -0
  283. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +31 -0
  284. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +8 -0
  285. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +9 -0
  286. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +10 -0
  287. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +8 -0
  288. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +24 -0
  289. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +8 -0
  290. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +14 -0
  291. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +12 -0
  292. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +10 -0
  293. package/pipeline/skills/shared/external/swift-concurrency-expert/references/swift-6-2-concurrency.md +8 -0
  294. package/pipeline/skills/shared/external/swift-concurrency-pro/references/actors.md +10 -0
  295. package/pipeline/skills/shared/external/swift-concurrency-pro/references/cancellation.md +7 -0
  296. package/pipeline/skills/shared/external/swift-concurrency-pro/references/interop.md +10 -0
  297. package/pipeline/skills/shared/external/swift-concurrency-pro/references/new-features.md +11 -0
  298. package/pipeline/skills/shared/external/swift-concurrency-pro/references/structured.md +9 -0
  299. package/pipeline/skills/shared/external/swift-concurrency-pro/references/testing.md +13 -0
  300. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +7 -0
  301. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +14 -0
  302. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +16 -0
  303. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +17 -0
  304. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +19 -0
  305. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +16 -0
  306. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +19 -0
  307. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +18 -0
  308. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +16 -0
  309. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +17 -0
  310. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +17 -0
  311. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +17 -0
  312. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +18 -0
  313. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +21 -0
  314. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +16 -0
  315. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +16 -0
  316. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +9 -0
  317. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +24 -0
  318. package/pipeline/skills/shared/external/swift-testing-pro/references/async-tests.md +9 -0
  319. package/pipeline/skills/shared/external/swift-testing-pro/references/new-features.md +11 -0
  320. package/pipeline/skills/shared/external/swift-testing-pro/references/writing-better-tests.md +13 -0
  321. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +8 -0
  322. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +17 -0
  323. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +12 -0
  324. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +11 -0
  325. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +7 -0
  326. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +13 -0
  327. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +24 -0
  328. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +12 -0
  329. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +15 -0
  330. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +15 -0
  331. package/pipeline/skills/shared/external/swiftui-animation/references/scroll-effects.md +12 -0
  332. package/pipeline/skills/shared/external/swiftui-expert-skill/references/accessibility-patterns.md +11 -0
  333. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +7 -0
  334. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +13 -0
  335. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +9 -0
  336. package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts-accessibility.md +9 -0
  337. package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts.md +17 -0
  338. package/pipeline/skills/shared/external/swiftui-expert-skill/references/image-optimization.md +9 -0
  339. package/pipeline/skills/shared/external/swiftui-expert-skill/references/latest-apis.md +10 -0
  340. package/pipeline/skills/shared/external/swiftui-expert-skill/references/layout-best-practices.md +12 -0
  341. package/pipeline/skills/shared/external/swiftui-expert-skill/references/list-patterns.md +12 -0
  342. package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-scenes.md +13 -0
  343. package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-views.md +12 -0
  344. package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-window-styling.md +12 -0
  345. package/pipeline/skills/shared/external/swiftui-expert-skill/references/performance-patterns.md +7 -0
  346. package/pipeline/skills/shared/external/swiftui-expert-skill/references/scroll-patterns.md +9 -0
  347. package/pipeline/skills/shared/external/swiftui-expert-skill/references/sheet-navigation-patterns.md +10 -0
  348. package/pipeline/skills/shared/external/swiftui-expert-skill/references/state-management.md +18 -0
  349. package/pipeline/skills/shared/external/swiftui-expert-skill/references/view-structure.md +16 -0
  350. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +11 -0
  351. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +8 -0
  352. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +14 -0
  353. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +9 -0
  354. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +22 -0
  355. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/uikit-glass.md +14 -0
  356. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +10 -0
  357. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +10 -0
  358. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +13 -0
  359. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +10 -0
  360. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +14 -0
  361. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +30 -0
  362. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +16 -0
  363. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +11 -0
  364. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +11 -0
  365. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +10 -0
  366. package/pipeline/skills/shared/external/swiftui-performance-audit/references/code-smells.md +8 -0
  367. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/app-wiring.md +12 -0
  368. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/menu-bar.md +10 -0
  369. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/navigationstack.md +11 -0
  370. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/scroll-reveal.md +12 -0
  371. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/sheets.md +14 -0
  372. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/tabview.md +10 -0
  373. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +10 -0
  374. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +13 -0
  375. package/pipeline/skills/shared/external/swiftui-view-refactor/references/mv-patterns.md +12 -0
  376. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +8 -0
  377. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +8 -0
  378. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +14 -0
  379. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +17 -0
  380. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +11 -0
  381. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +11 -0
  382. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +27 -0
  383. package/pipeline/skills/shared/external/xcode-build-fixer/references/build-settings-best-practices.md +9 -0
  384. package/pipeline/skills/shared/external/xcode-build-fixer/references/fix-patterns.md +7 -0
  385. package/pipeline/skills/shared/external/xcode-build-orchestrator/references/build-settings-best-practices.md +9 -0
  386. package/pipeline/skills/shared/external/xcode-compilation-analyzer/references/build-optimization-sources.md +14 -0
  387. package/pipeline/skills/shared/external/xcode-compilation-analyzer/references/code-compilation-checks.md +8 -0
  388. package/pipeline/skills/shared/external/xcode-project-analyzer/references/build-optimization-sources.md +14 -0
  389. package/pipeline/skills/shared/external/xcode-project-analyzer/references/build-settings-best-practices.md +9 -0
  390. package/pipeline/skills/shared/external/xcode-project-analyzer/references/project-audit-checks.md +13 -0
@@ -23,11 +23,11 @@
23
23
  * status running or not, the URL without the token, the idle
24
24
  * limit, remote access, the paired devices the phone
25
25
  * registry lists, and the last panel actions from
26
- * office-actions.jsonl. Remote access is "not available in
27
- * this installation" unless a remote layer is present: a
28
- * prefs.global.office.remote block, or a separately
29
- * installed module at ~/.claude/office-remote/. This
30
- * script only reports it; it carries no remote code.
26
+ * office-actions.jsonl. Remote access is "off" until
27
+ * office-pair pairs a phone, "configured" when
28
+ * prefs.global.office.remote is set but nothing is paired,
29
+ * and "paired" once a device is; while paired, office-on
30
+ * starts office-remote-client.mjs beside the server.
31
31
  *
32
32
  * State: one 0600 file, ~/.claude/logs/multi-agent/office/office.json, with
33
33
  * the pid, port and token file of the server this script started. The server
@@ -58,6 +58,7 @@ import { homedir } from "node:os";
58
58
  import { dirname, join } from "node:path";
59
59
 
60
60
  import { codeDigest } from "../lib/code-digest.mjs";
61
+ import { phoneDir } from "./_phone-auth.mjs";
61
62
  import { fileURLToPath } from "node:url";
62
63
 
63
64
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -72,6 +73,7 @@ const START_SKEW_MS = 120 * 1000;
72
73
  const home = (env) => env.HOME || homedir();
73
74
  const stateDir = (env) => join(home(env), ".claude", "logs", "multi-agent", "office");
74
75
  const stateFile = (env) => join(stateDir(env), "office.json");
76
+ const clientFile = (env) => join(stateDir(env), "remote-client.json");
75
77
  const actionsLog = (env) =>
76
78
  join(home(env), ".claude", "logs", "multi-agent", "office-actions.jsonl");
77
79
 
@@ -86,16 +88,22 @@ const readJson = (p) => {
86
88
  /** The panel page: next to this scripts directory, in a checkout and in the install. */
87
89
  export const pagePath = () => join(HERE, "..", "office", "index.html");
88
90
 
89
- /** Whether a remote layer is present; none ships with the pipeline. */
91
+ /** Whether a device is paired for remote use (office-pair wrote the remote config). */
92
+ export const remotePaired = (env = process.env) => existsSync(join(phoneDir(env), "remote.json"));
93
+
94
+ /** Whether the remote layer is configured: a paired device, or a prefs block. */
90
95
  export function remoteLayer(env = process.env) {
96
+ if (remotePaired(env)) return true;
91
97
  const prefs = readJson(join(home(env), ".claude", "multi-agent-preferences.json"));
92
98
  const block = prefs?.global?.office?.remote;
93
- if (block && typeof block === "object" && !Array.isArray(block)) return true;
94
- return existsSync(join(home(env), ".claude", "office-remote"));
99
+ return Boolean(block && typeof block === "object" && !Array.isArray(block));
95
100
  }
96
101
 
97
- const remoteText = (env) =>
98
- remoteLayer(env) ? "provided by a separate remote layer" : "not available in this installation";
102
+ const remoteText = (env) => {
103
+ if (remotePaired(env)) return "paired: the remote client runs while the office is on";
104
+ if (remoteLayer(env)) return "configured, no phone paired (office-pair start)";
105
+ return "off: no phone paired";
106
+ };
99
107
 
100
108
  /** The contract server: beside this script, else the install. */
101
109
  export function serverScript(env = process.env) {
@@ -127,6 +135,55 @@ function psField(pid, field) {
127
135
  return r.status === 0 ? r.stdout.trim() : "";
128
136
  }
129
137
 
138
+ /** The remote client this script started, if it is still running. */
139
+ function remoteClient(env) {
140
+ const c = readJson(clientFile(env));
141
+ if (!c || !Number.isInteger(c.pid) || !alive(c.pid)) return null;
142
+ return psField(c.pid, "command").includes("office-remote-client.mjs") ? c : null;
143
+ }
144
+
145
+ /**
146
+ * Start office-remote-client.mjs beside the server when a device is paired,
147
+ * or keep the one already running. Without a pairing nothing starts, so
148
+ * remote stays off by default; the client also stops by itself once the
149
+ * server is gone.
150
+ */
151
+ export function startRemoteClient(env = process.env) {
152
+ if (!remotePaired(env)) return false;
153
+ if (remoteClient(env)) return true;
154
+ const script = join(HERE, "office-remote-client.mjs");
155
+ if (!existsSync(script)) return false;
156
+ const child = spawn(process.execPath, [script, "loop"], {
157
+ env,
158
+ detached: true,
159
+ stdio: "ignore",
160
+ });
161
+ child.unref();
162
+ mkdirSync(stateDir(env), { recursive: true, mode: 0o700 });
163
+ writeFileSync(
164
+ clientFile(env),
165
+ JSON.stringify({ pid: child.pid, startedAt: new Date().toISOString() }) + "\n",
166
+ { mode: 0o600 },
167
+ );
168
+ return true;
169
+ }
170
+
171
+ /** Stop the remote client this script started. */
172
+ export function stopRemoteClient(env = process.env) {
173
+ const c = remoteClient(env);
174
+ let stopped = false;
175
+ if (c) {
176
+ try {
177
+ process.kill(c.pid, "SIGTERM");
178
+ stopped = true;
179
+ } catch {
180
+ stopped = false;
181
+ }
182
+ }
183
+ rmSync(clientFile(env), { force: true });
184
+ return stopped;
185
+ }
186
+
130
187
  /**
131
188
  * Whether `pid` is still the server a state record names: its token file
132
189
  * names the same pid, the process runs contract-server.mjs, and it started
@@ -313,6 +370,7 @@ export async function officeOn({ env = process.env, open = true } = {}) {
313
370
  if (!s) throw new Error("the server stopped right after it started");
314
371
  }
315
372
  const page = `${s.url}/office`;
373
+ const remote = startRemoteClient(env);
316
374
  const opened = open ? openBrowser(`${page}#code=${await panelCode(s)}`, env) : false;
317
375
  return {
318
376
  running: true,
@@ -321,12 +379,14 @@ export async function officeOn({ env = process.env, open = true } = {}) {
321
379
  url: page,
322
380
  pid: s.pid,
323
381
  idleMinutes: s.idleMinutes,
382
+ remote,
324
383
  opened,
325
384
  };
326
385
  }
327
386
 
328
387
  /** Stop the server this script started. */
329
388
  export function officeOff({ env = process.env } = {}) {
389
+ stopRemoteClient(env);
330
390
  const s = readJson(stateFile(env));
331
391
  let stopped = false;
332
392
  if (isOurServer(s, s ? readJson(s.tokenFile ?? "") : null)) {
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file office-pair.mjs - pair a phone with this machine for the office
4
+ * remote layer (features/office-remote-protocol.md, Pairing).
5
+ *
6
+ * start [--relay <url>] register a pairing with the relay and show its QR
7
+ * code on this machine's screen
8
+ * wait wait for the phone's pairing message, open it and
9
+ * print the comparison code
10
+ * confirm <code> enrol the device once the code the phone shows is
11
+ * typed back here, and send the phone its device id
12
+ * in the first sealed machine-to-phone frame
13
+ * cancel drop the pending pairing
14
+ *
15
+ * What stays on this machine: the machine private key and the relay token
16
+ * live in <phone dir>/remote.json (0600); the relay only gets the SHA-256 of
17
+ * the token. The one-time secret and the machine public key travel in the QR
18
+ * link's fragment, which a browser never sends to a server, so the relay can
19
+ * neither open the phone's pairing message nor swap the machine key. The QR
20
+ * page is a 0600 file in a 0700 directory opened in the local browser; the
21
+ * secret is never printed. Enrolment goes through `phone-devices.mjs add`
22
+ * with scopes read,answer, so `launch` stays a separate local switch.
23
+ *
24
+ * Every subcommand refuses an unattended run (unattended-policy.json).
25
+ * Exit: 0 done, 1 refused or failed, 2 usage, 3 unattended.
26
+ *
27
+ * @module pipeline/scripts/office-pair
28
+ */
29
+
30
+ import { spawnSync } from "node:child_process";
31
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
32
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { dirname, join } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+
37
+ import {
38
+ comparisonCode,
39
+ createMachineKeys,
40
+ directionKeys,
41
+ openPairing,
42
+ sealFrame,
43
+ } from "../lib/office-remote.mjs";
44
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
45
+ import { encodeQr, qrSvg } from "../lib/qr-code.mjs";
46
+ import { isUnattended } from "../lib/unattended.mjs";
47
+ import { phoneDir } from "./_phone-auth.mjs";
48
+ import { readConfig, writeConfig } from "./office-remote-client.mjs";
49
+
50
+ const HERE = dirname(fileURLToPath(import.meta.url));
51
+ export const PAIRING_TTL_MS = 10 * 60_000;
52
+ const NAME = /^[\p{L}\p{N} ._'-]{1,64}$/u;
53
+ const B64U = /^[A-Za-z0-9_-]+$/;
54
+
55
+ const home = (env) => env.HOME || homedir();
56
+ export const pendingPath = (env = process.env) => join(phoneDir(env), "pair-pending.json");
57
+ const pageDir = (env) => join(home(env), ".claude", "logs", "multi-agent", "office");
58
+ const pagePath = (env) => join(pageDir(env), "pair.html");
59
+ const b64u = (b) => Buffer.from(b).toString("base64url");
60
+ const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
61
+
62
+ /** First 16 hex digits of SHA-256 over a public key, in groups of four (the phone shows the same). */
63
+ export const keyFingerprint = (publicKey) =>
64
+ createHash("sha256")
65
+ .update(Buffer.from(publicKey, "base64url"))
66
+ .digest("hex")
67
+ .slice(0, 16)
68
+ .replace(/(.{4})(?=.)/g, "$1 ");
69
+
70
+ class Refusal extends Error {}
71
+
72
+ function readJson(p) {
73
+ try {
74
+ return JSON.parse(readFileSync(p, "utf8"));
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function writePrivate(file, text) {
81
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
82
+ chmodSync(dirname(file), 0o700);
83
+ writeFileSync(file, text, { mode: 0o600 });
84
+ chmodSync(file, 0o600);
85
+ }
86
+
87
+ /** A relay URL: https, or http on the loopback; no credentials, query or fragment. */
88
+ export function relayOrigin(raw) {
89
+ let u;
90
+ try {
91
+ u = new URL(String(raw));
92
+ } catch {
93
+ throw new Refusal("the relay URL is not a URL");
94
+ }
95
+ const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
96
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback)) {
97
+ throw new Refusal("the relay URL must be https");
98
+ }
99
+ if (u.username || u.password || u.search || u.hash || (u.pathname && u.pathname !== "/")) {
100
+ throw new Refusal("the relay URL is an origin only (scheme, host, port)");
101
+ }
102
+ return u.origin;
103
+ }
104
+
105
+ function relayFromPrefs(env) {
106
+ const prefs = readJson(join(home(env), ".claude", "multi-agent-preferences.json"));
107
+ return prefs?.global?.office?.remote?.relayUrl ?? null;
108
+ }
109
+
110
+ /** The link the QR code carries; everything after `#` stays on the phone. */
111
+ export function pairingLink({ relayUrl, pairingId, secret, machinePublic }) {
112
+ return `${relayUrl}/office/pair#p=${pairingId}&s=${secret}&k=${machinePublic}`;
113
+ }
114
+
115
+ function pageHtml(svg) {
116
+ return (
117
+ "<!doctype html><html><head><meta charset=utf-8>" +
118
+ `<meta http-equiv="Content-Security-Policy" content="default-src 'none'">` +
119
+ "<title>Office pairing</title></head><body>" +
120
+ "<p>Scan with the phone, signed in to your site. The code expires in 10 minutes.</p>" +
121
+ `${svg}</body></html>`
122
+ );
123
+ }
124
+
125
+ function openPage(file, env) {
126
+ const bin = env.MA_OFFICE_OPENER || "open";
127
+ const r = spawnSync(bin, [`file://${file}`], { stdio: "ignore", timeout: 15000 });
128
+ return r.status === 0;
129
+ }
130
+
131
+ async function relayCall(fetchImpl, url, init) {
132
+ const res = await fetchImpl(url, { ...init, signal: AbortSignal.timeout(30_000) });
133
+ const text = await res.text();
134
+ let body;
135
+ try {
136
+ body = text ? JSON.parse(text) : null;
137
+ } catch {
138
+ body = null;
139
+ }
140
+ return { status: res.status, ok: res.ok, body };
141
+ }
142
+
143
+ /**
144
+ * Register a pairing with the relay and show its QR code.
145
+ *
146
+ * @returns {Promise<{pairingId: string, expiresAt: string, page: string, opened: boolean}>}
147
+ */
148
+ export async function start({ relay, env = process.env, fetchImpl = fetch, now = Date.now() }) {
149
+ const relayUrl = relayOrigin(relay ?? relayFromPrefs(env) ?? "");
150
+ const existing = readConfig(env);
151
+ const machine =
152
+ existing?.machinePrivatePem && existing?.machinePublic
153
+ ? { privatePem: existing.machinePrivatePem, publicKey: existing.machinePublic }
154
+ : createMachineKeys();
155
+ const relayToken =
156
+ existing?.relayToken && existing.relayUrl === relayUrl
157
+ ? existing.relayToken
158
+ : b64u(randomBytes(32));
159
+ const pairingId = b64u(randomBytes(16));
160
+ const secret = b64u(randomBytes(16));
161
+ const expiresAt = new Date(now + PAIRING_TTL_MS).toISOString();
162
+ const r = await relayCall(fetchImpl, `${relayUrl}/api/office/relay/pairings`, {
163
+ method: "POST",
164
+ headers: { "content-type": "application/json" },
165
+ body: JSON.stringify({
166
+ pairingId,
167
+ machinePublic: machine.publicKey,
168
+ tokenHash: sha256hex(relayToken),
169
+ expiresAt,
170
+ }),
171
+ });
172
+ if (!r.ok) throw new Refusal(`the relay refused the pairing (${r.status})`);
173
+ writeConfig(
174
+ {
175
+ ...(existing ?? {}),
176
+ relayUrl,
177
+ relayToken,
178
+ pairingId: existing?.pairingId ?? null,
179
+ machinePrivatePem: machine.privatePem,
180
+ machinePublic: machine.publicKey,
181
+ devices: existing?.devices ?? {},
182
+ },
183
+ env,
184
+ );
185
+ writePrivate(
186
+ pendingPath(env),
187
+ JSON.stringify({ relayUrl, pairingId, secret, expiresAt, relayToken }) + "\n",
188
+ );
189
+ const link = pairingLink({ relayUrl, pairingId, secret, machinePublic: machine.publicKey });
190
+ writePrivate(pagePath(env), pageHtml(qrSvg(encodeQr(link, { ecc: "M" }))));
191
+ return { pairingId, expiresAt, page: pagePath(env), opened: openPage(pagePath(env), env) };
192
+ }
193
+
194
+ function pending(env, now) {
195
+ const p = readJson(pendingPath(env));
196
+ if (!p?.pairingId) throw new Refusal("no pairing is pending; run office-pair start");
197
+ if (Date.parse(p.expiresAt) <= now) {
198
+ dropPending(env);
199
+ throw new Refusal("the pairing expired; run office-pair start again");
200
+ }
201
+ return p;
202
+ }
203
+
204
+ function dropPending(env) {
205
+ rmSync(pendingPath(env), { force: true });
206
+ rmSync(pagePath(env), { force: true });
207
+ }
208
+
209
+ /** Check what the phone sent before anything of it is kept. */
210
+ export function checkPairingMessage(value, relayUrl) {
211
+ const v = value ?? {};
212
+ if (typeof v.deviceName !== "string" || !NAME.test(v.deviceName))
213
+ throw new Refusal("the device name is not 1 to 64 letters, digits, spaces or ._'-");
214
+ if (typeof v.devicePublicKey !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(v.devicePublicKey))
215
+ throw new Refusal("the device key is not a base64url Ed25519 public key");
216
+ const pk = v.passkey ?? {};
217
+ if (typeof pk.credential !== "string" || !B64U.test(pk.credential) || pk.credential.length > 400)
218
+ throw new Refusal("the passkey public key is missing");
219
+ if (typeof pk.credentialId !== "string" || !B64U.test(pk.credentialId))
220
+ throw new Refusal("the passkey credential id is missing");
221
+ if (pk.origin !== relayUrl) throw new Refusal("the passkey origin is not the relay's");
222
+ if (pk.rpId !== new URL(relayUrl).hostname)
223
+ throw new Refusal("the passkey rp id is not the relay's host");
224
+ return {
225
+ deviceName: v.deviceName,
226
+ devicePublicKey: v.devicePublicKey,
227
+ passkey: {
228
+ credential: pk.credential,
229
+ credentialId: pk.credentialId,
230
+ rpId: pk.rpId,
231
+ origin: pk.origin,
232
+ },
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Wait for the phone's pairing message, open it and keep it with the pending
238
+ * pairing. Prints the comparison code, never a key.
239
+ */
240
+ export async function wait({
241
+ env = process.env,
242
+ fetchImpl = fetch,
243
+ now = Date.now,
244
+ pollMs = 2000,
245
+ }) {
246
+ const p = pending(env, now());
247
+ const config = readConfig(env);
248
+ while (now() < Date.parse(p.expiresAt)) {
249
+ const r = await relayCall(
250
+ fetchImpl,
251
+ `${p.relayUrl}/api/office/relay/pairings/${encodeURIComponent(p.pairingId)}`,
252
+ { headers: { Authorization: `Bearer ${p.relayToken}` } },
253
+ );
254
+ if (r.ok && r.body?.envelope) {
255
+ const { sessionPublic, sealed } = r.body.envelope;
256
+ if (typeof sessionPublic !== "string" || !B64U.test(sessionPublic))
257
+ throw new Refusal("the phone sent no session key");
258
+ const opened = openPairing({
259
+ privatePem: config.machinePrivatePem,
260
+ peerPublic: sessionPublic,
261
+ secret: p.secret,
262
+ pairingId: p.pairingId,
263
+ sealed,
264
+ });
265
+ if (!opened.ok) throw new Refusal("the pairing message did not open with this pairing");
266
+ const message = checkPairingMessage(opened.value, p.relayUrl);
267
+ writePrivate(pendingPath(env), JSON.stringify({ ...p, sessionPublic, message }) + "\n");
268
+ return {
269
+ deviceName: message.deviceName,
270
+ code: comparisonCode(config.machinePublic, sessionPublic),
271
+ machineKey: keyFingerprint(config.machinePublic),
272
+ };
273
+ }
274
+ if (!r.ok && r.status !== 404) throw new Refusal(`the relay answered ${r.status}`);
275
+ await new Promise((res) => setTimeout(res, pollMs));
276
+ }
277
+ dropPending(env);
278
+ throw new Refusal("the pairing expired before the phone answered");
279
+ }
280
+
281
+ function enrol(message, env) {
282
+ const r = spawnSync(
283
+ process.execPath,
284
+ [
285
+ join(HERE, "phone-devices.mjs"),
286
+ "add",
287
+ "--name",
288
+ message.deviceName,
289
+ "--public-key",
290
+ message.devicePublicKey,
291
+ "--scopes",
292
+ "read,answer",
293
+ ],
294
+ { env, encoding: "utf8", timeout: 30_000 },
295
+ );
296
+ if (r.status !== 0) {
297
+ throw new Refusal(`phone-devices refused the device: ${(r.stderr || "").trim()}`);
298
+ }
299
+ return JSON.parse(r.stdout);
300
+ }
301
+
302
+ /**
303
+ * Enrol the device once the user typed the code the phone shows.
304
+ *
305
+ * @returns {Promise<{id: string, name: string, scopes: string[]}>}
306
+ */
307
+ export async function confirm({ code, env = process.env, fetchImpl = fetch, now = Date.now() }) {
308
+ const p = pending(env, now);
309
+ if (!p.message || !p.sessionPublic) throw new Refusal("run office-pair wait first");
310
+ const config = readConfig(env);
311
+ const want = Buffer.from(comparisonCode(config.machinePublic, p.sessionPublic));
312
+ const got = Buffer.from(String(code ?? "").trim());
313
+ if (got.length !== want.length || !timingSafeEqual(got, want)) {
314
+ dropPending(env);
315
+ throw new Refusal("the code does not match; the pairing was dropped, start again");
316
+ }
317
+ const device = enrol(p.message, env);
318
+ const keys = directionKeys({
319
+ privatePem: config.machinePrivatePem,
320
+ peerPublic: p.sessionPublic,
321
+ pairingId: p.pairingId,
322
+ role: "machine",
323
+ });
324
+ const welcome = sealFrame(keys.send, 1, "machine-to-phone", {
325
+ deviceId: device.id,
326
+ scopes: device.scopes,
327
+ });
328
+ const r = await relayCall(
329
+ fetchImpl,
330
+ `${p.relayUrl}/api/office/relay/pairings/${encodeURIComponent(p.pairingId)}/confirm`,
331
+ {
332
+ method: "POST",
333
+ headers: { Authorization: `Bearer ${p.relayToken}`, "content-type": "application/json" },
334
+ body: JSON.stringify({ welcome }),
335
+ },
336
+ );
337
+ if (!r.ok) {
338
+ spawnSync(process.execPath, [join(HERE, "phone-devices.mjs"), "revoke", device.id], {
339
+ env,
340
+ stdio: "ignore",
341
+ timeout: 30_000,
342
+ });
343
+ dropPending(env);
344
+ throw new Refusal(
345
+ `the relay refused the confirmation (${r.status}); the device was revoked again, start over`,
346
+ );
347
+ }
348
+ writeConfig(
349
+ {
350
+ ...config,
351
+ pairingId: p.pairingId,
352
+ devices: {
353
+ [device.id]: {
354
+ id: device.id,
355
+ sessionPublic: p.sessionPublic,
356
+ passkey: { ...p.message.passkey, signCount: 0 },
357
+ recvCounter: 0,
358
+ sendCounter: 1,
359
+ },
360
+ },
361
+ },
362
+ env,
363
+ );
364
+ dropPending(env);
365
+ return { id: device.id, name: device.name, scopes: device.scopes };
366
+ }
367
+
368
+ /** Drop the pending pairing and its QR page. */
369
+ export function cancel({ env = process.env } = {}) {
370
+ const had = existsSync(pendingPath(env));
371
+ dropPending(env);
372
+ return { cancelled: had };
373
+ }
374
+
375
+ async function main(argv) {
376
+ const [cmd, ...rest] = argv;
377
+ if (!["start", "wait", "confirm", "cancel"].includes(cmd)) {
378
+ process.stderr.write(
379
+ "usage: office-pair.mjs start [--relay <url>] | wait | confirm <code> | cancel\n",
380
+ );
381
+ return 2;
382
+ }
383
+ if (isUnattended()) {
384
+ process.stderr.write("office-pair: an unattended run may not pair a device\n");
385
+ return 3;
386
+ }
387
+ try {
388
+ let out;
389
+ if (cmd === "start") {
390
+ const i = rest.indexOf("--relay");
391
+ out = await start({ relay: i === -1 ? undefined : rest[i + 1] });
392
+ } else if (cmd === "wait") out = await wait({});
393
+ else if (cmd === "confirm") out = await confirm({ code: rest[0] });
394
+ else out = cancel();
395
+ process.stdout.write(JSON.stringify(out) + "\n");
396
+ return 0;
397
+ } catch (err) {
398
+ process.stderr.write(`office-pair: ${err.message}\n`);
399
+ return 1;
400
+ }
401
+ }
402
+
403
+ if (invokedDirectly(import.meta.url)) {
404
+ main(process.argv.slice(2)).then((code) => {
405
+ process.exitCode = code;
406
+ });
407
+ }