@productbrain/cli 0.1.0-beta.30 → 0.1.0-beta.3015

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 (1089) hide show
  1. package/README.md +72 -174
  2. package/dist/__tests__/adapters.test.js +54 -1
  3. package/dist/__tests__/adapters.test.js.map +1 -1
  4. package/dist/__tests__/audit.test.js +65 -44
  5. package/dist/__tests__/audit.test.js.map +1 -1
  6. package/dist/__tests__/authority-domains-queue.test.d.ts +2 -0
  7. package/dist/__tests__/authority-domains-queue.test.d.ts.map +1 -0
  8. package/dist/__tests__/authority-domains-queue.test.js +169 -0
  9. package/dist/__tests__/authority-domains-queue.test.js.map +1 -0
  10. package/dist/__tests__/authority-domains.test.d.ts +2 -0
  11. package/dist/__tests__/authority-domains.test.d.ts.map +1 -0
  12. package/dist/__tests__/authority-domains.test.js +48 -0
  13. package/dist/__tests__/authority-domains.test.js.map +1 -0
  14. package/dist/__tests__/canonicalRefs.vocab.test.d.ts +2 -0
  15. package/dist/__tests__/canonicalRefs.vocab.test.d.ts.map +1 -0
  16. package/dist/__tests__/canonicalRefs.vocab.test.js +251 -0
  17. package/dist/__tests__/canonicalRefs.vocab.test.js.map +1 -0
  18. package/dist/__tests__/capture-receipt-coaching.test.d.ts +2 -0
  19. package/dist/__tests__/capture-receipt-coaching.test.d.ts.map +1 -0
  20. package/dist/__tests__/capture-receipt-coaching.test.js +89 -0
  21. package/dist/__tests__/capture-receipt-coaching.test.js.map +1 -0
  22. package/dist/__tests__/capture-receipt-domain.test.d.ts +2 -0
  23. package/dist/__tests__/capture-receipt-domain.test.d.ts.map +1 -0
  24. package/dist/__tests__/capture-receipt-domain.test.js +29 -0
  25. package/dist/__tests__/capture-receipt-domain.test.js.map +1 -0
  26. package/dist/__tests__/capture.test.js +645 -40
  27. package/dist/__tests__/capture.test.js.map +1 -1
  28. package/dist/__tests__/coaching-mirror-drift.test.d.ts +2 -0
  29. package/dist/__tests__/coaching-mirror-drift.test.d.ts.map +1 -0
  30. package/dist/__tests__/coaching-mirror-drift.test.js +47 -0
  31. package/dist/__tests__/coaching-mirror-drift.test.js.map +1 -0
  32. package/dist/__tests__/codex-prep-parity.test.d.ts +2 -0
  33. package/dist/__tests__/codex-prep-parity.test.d.ts.map +1 -0
  34. package/dist/__tests__/codex-prep-parity.test.js +107 -0
  35. package/dist/__tests__/codex-prep-parity.test.js.map +1 -0
  36. package/dist/__tests__/collection-registry-seed-drift.test.d.ts +2 -0
  37. package/dist/__tests__/collection-registry-seed-drift.test.d.ts.map +1 -0
  38. package/dist/__tests__/collection-registry-seed-drift.test.js +58 -0
  39. package/dist/__tests__/collection-registry-seed-drift.test.js.map +1 -0
  40. package/dist/__tests__/config-prod-fallthrough.test.d.ts +2 -0
  41. package/dist/__tests__/config-prod-fallthrough.test.d.ts.map +1 -0
  42. package/dist/__tests__/config-prod-fallthrough.test.js +82 -0
  43. package/dist/__tests__/config-prod-fallthrough.test.js.map +1 -0
  44. package/dist/__tests__/config.test.d.ts +8 -0
  45. package/dist/__tests__/config.test.d.ts.map +1 -0
  46. package/dist/__tests__/config.test.js +566 -0
  47. package/dist/__tests__/config.test.js.map +1 -0
  48. package/dist/__tests__/constants.test.js +54 -2
  49. package/dist/__tests__/constants.test.js.map +1 -1
  50. package/dist/__tests__/constellation.test.js +16 -22
  51. package/dist/__tests__/constellation.test.js.map +1 -1
  52. package/dist/__tests__/context-strategy.test.js +8 -8
  53. package/dist/__tests__/context-strategy.test.js.map +1 -1
  54. package/dist/__tests__/conversation.test.d.ts +2 -0
  55. package/dist/__tests__/conversation.test.d.ts.map +1 -0
  56. package/dist/__tests__/conversation.test.js +113 -0
  57. package/dist/__tests__/conversation.test.js.map +1 -0
  58. package/dist/__tests__/entry-formatter.test.d.ts +2 -0
  59. package/dist/__tests__/entry-formatter.test.d.ts.map +1 -0
  60. package/dist/__tests__/entry-formatter.test.js +82 -0
  61. package/dist/__tests__/entry-formatter.test.js.map +1 -0
  62. package/dist/__tests__/envelope-contract.test.d.ts +15 -0
  63. package/dist/__tests__/envelope-contract.test.d.ts.map +1 -0
  64. package/dist/__tests__/envelope-contract.test.js +152 -0
  65. package/dist/__tests__/envelope-contract.test.js.map +1 -0
  66. package/dist/__tests__/errors.test.d.ts +2 -0
  67. package/dist/__tests__/errors.test.d.ts.map +1 -0
  68. package/dist/__tests__/errors.test.js +130 -0
  69. package/dist/__tests__/errors.test.js.map +1 -0
  70. package/dist/__tests__/experiment.test.d.ts +6 -0
  71. package/dist/__tests__/experiment.test.d.ts.map +1 -0
  72. package/dist/__tests__/experiment.test.js +69 -0
  73. package/dist/__tests__/experiment.test.js.map +1 -0
  74. package/dist/__tests__/fields.test.js +36 -36
  75. package/dist/__tests__/fields.test.js.map +1 -1
  76. package/dist/__tests__/glossary.test.d.ts +2 -0
  77. package/dist/__tests__/glossary.test.d.ts.map +1 -0
  78. package/dist/__tests__/glossary.test.js +32 -0
  79. package/dist/__tests__/glossary.test.js.map +1 -0
  80. package/dist/__tests__/handshake-augment.test.d.ts +2 -0
  81. package/dist/__tests__/handshake-augment.test.d.ts.map +1 -0
  82. package/dist/__tests__/handshake-augment.test.js +424 -0
  83. package/dist/__tests__/handshake-augment.test.js.map +1 -0
  84. package/dist/__tests__/handshake-dormancy.test.d.ts +2 -0
  85. package/dist/__tests__/handshake-dormancy.test.d.ts.map +1 -0
  86. package/dist/__tests__/handshake-dormancy.test.js +207 -0
  87. package/dist/__tests__/handshake-dormancy.test.js.map +1 -0
  88. package/dist/__tests__/handshake-formatter.test.d.ts +2 -0
  89. package/dist/__tests__/handshake-formatter.test.d.ts.map +1 -0
  90. package/dist/__tests__/handshake-formatter.test.js +67 -0
  91. package/dist/__tests__/handshake-formatter.test.js.map +1 -0
  92. package/dist/__tests__/handshake-preview.test.d.ts +2 -0
  93. package/dist/__tests__/handshake-preview.test.d.ts.map +1 -0
  94. package/dist/__tests__/handshake-preview.test.js +950 -0
  95. package/dist/__tests__/handshake-preview.test.js.map +1 -0
  96. package/dist/__tests__/handshake.e2e.test.d.ts +2 -0
  97. package/dist/__tests__/handshake.e2e.test.d.ts.map +1 -0
  98. package/dist/__tests__/handshake.e2e.test.js +1255 -0
  99. package/dist/__tests__/handshake.e2e.test.js.map +1 -0
  100. package/dist/__tests__/handshake.test.js +796 -19
  101. package/dist/__tests__/handshake.test.js.map +1 -1
  102. package/dist/__tests__/health-gap-kinds-drift.test.d.ts +2 -0
  103. package/dist/__tests__/health-gap-kinds-drift.test.d.ts.map +1 -0
  104. package/dist/__tests__/health-gap-kinds-drift.test.js +41 -0
  105. package/dist/__tests__/health-gap-kinds-drift.test.js.map +1 -0
  106. package/dist/__tests__/hook-intents.test.d.ts +2 -0
  107. package/dist/__tests__/hook-intents.test.d.ts.map +1 -0
  108. package/dist/__tests__/hook-intents.test.js +184 -0
  109. package/dist/__tests__/hook-intents.test.js.map +1 -0
  110. package/dist/__tests__/ingest.test.js +37 -28
  111. package/dist/__tests__/ingest.test.js.map +1 -1
  112. package/dist/__tests__/init.test.d.ts +7 -0
  113. package/dist/__tests__/init.test.d.ts.map +1 -0
  114. package/dist/__tests__/init.test.js +146 -0
  115. package/dist/__tests__/init.test.js.map +1 -0
  116. package/dist/__tests__/login.test.js +29 -30
  117. package/dist/__tests__/login.test.js.map +1 -1
  118. package/dist/__tests__/manifest.test.d.ts +6 -0
  119. package/dist/__tests__/manifest.test.d.ts.map +1 -0
  120. package/dist/__tests__/manifest.test.js +255 -0
  121. package/dist/__tests__/manifest.test.js.map +1 -0
  122. package/dist/__tests__/method-publish-coherency.test.d.ts +9 -0
  123. package/dist/__tests__/method-publish-coherency.test.d.ts.map +1 -0
  124. package/dist/__tests__/method-publish-coherency.test.js +71 -0
  125. package/dist/__tests__/method-publish-coherency.test.js.map +1 -0
  126. package/dist/__tests__/method-registry.integration.test.d.ts +6 -0
  127. package/dist/__tests__/method-registry.integration.test.d.ts.map +1 -0
  128. package/dist/__tests__/method-registry.integration.test.js +17 -0
  129. package/dist/__tests__/method-registry.integration.test.js.map +1 -0
  130. package/dist/__tests__/method-registry.test.d.ts +12 -0
  131. package/dist/__tests__/method-registry.test.d.ts.map +1 -0
  132. package/dist/__tests__/method-registry.test.js +86 -0
  133. package/dist/__tests__/method-registry.test.js.map +1 -0
  134. package/dist/__tests__/notice-marker.test.d.ts +2 -0
  135. package/dist/__tests__/notice-marker.test.d.ts.map +1 -0
  136. package/dist/__tests__/notice-marker.test.js +41 -0
  137. package/dist/__tests__/notice-marker.test.js.map +1 -0
  138. package/dist/__tests__/onboarding-path-b.test.d.ts +2 -0
  139. package/dist/__tests__/onboarding-path-b.test.d.ts.map +1 -0
  140. package/dist/__tests__/onboarding-path-b.test.js +46 -0
  141. package/dist/__tests__/onboarding-path-b.test.js.map +1 -0
  142. package/dist/__tests__/onboarding.test.d.ts +6 -0
  143. package/dist/__tests__/onboarding.test.d.ts.map +1 -0
  144. package/dist/__tests__/onboarding.test.js +347 -0
  145. package/dist/__tests__/onboarding.test.js.map +1 -0
  146. package/dist/__tests__/orient.test.js +485 -11
  147. package/dist/__tests__/orient.test.js.map +1 -1
  148. package/dist/__tests__/perimeter.test.d.ts +2 -0
  149. package/dist/__tests__/perimeter.test.d.ts.map +1 -0
  150. package/dist/__tests__/perimeter.test.js +165 -0
  151. package/dist/__tests__/perimeter.test.js.map +1 -0
  152. package/dist/__tests__/personal-layer.test.d.ts +11 -0
  153. package/dist/__tests__/personal-layer.test.d.ts.map +1 -0
  154. package/dist/__tests__/personal-layer.test.js +268 -0
  155. package/dist/__tests__/personal-layer.test.js.map +1 -0
  156. package/dist/__tests__/preview-key-refresh.test.d.ts +2 -0
  157. package/dist/__tests__/preview-key-refresh.test.d.ts.map +1 -0
  158. package/dist/__tests__/preview-key-refresh.test.js +126 -0
  159. package/dist/__tests__/preview-key-refresh.test.js.map +1 -0
  160. package/dist/__tests__/profiles.test.d.ts +2 -0
  161. package/dist/__tests__/profiles.test.d.ts.map +1 -0
  162. package/dist/__tests__/profiles.test.js +320 -0
  163. package/dist/__tests__/profiles.test.js.map +1 -0
  164. package/dist/__tests__/promote-batch.test.d.ts +2 -0
  165. package/dist/__tests__/promote-batch.test.d.ts.map +1 -0
  166. package/dist/__tests__/promote-batch.test.js +185 -0
  167. package/dist/__tests__/promote-batch.test.js.map +1 -0
  168. package/dist/__tests__/promote.test.js +297 -40
  169. package/dist/__tests__/promote.test.js.map +1 -1
  170. package/dist/__tests__/prompts.test.d.ts +6 -0
  171. package/dist/__tests__/prompts.test.d.ts.map +1 -0
  172. package/dist/__tests__/prompts.test.js +146 -0
  173. package/dist/__tests__/prompts.test.js.map +1 -0
  174. package/dist/__tests__/proof-run.test.d.ts +2 -0
  175. package/dist/__tests__/proof-run.test.d.ts.map +1 -0
  176. package/dist/__tests__/proof-run.test.js +255 -0
  177. package/dist/__tests__/proof-run.test.js.map +1 -0
  178. package/dist/__tests__/proposals.test.js +24 -47
  179. package/dist/__tests__/proposals.test.js.map +1 -1
  180. package/dist/__tests__/relate.test.js +20 -37
  181. package/dist/__tests__/relate.test.js.map +1 -1
  182. package/dist/__tests__/restore.test.d.ts +2 -0
  183. package/dist/__tests__/restore.test.d.ts.map +1 -0
  184. package/dist/__tests__/restore.test.js +85 -0
  185. package/dist/__tests__/restore.test.js.map +1 -0
  186. package/dist/__tests__/runner.test.js +19 -15
  187. package/dist/__tests__/runner.test.js.map +1 -1
  188. package/dist/__tests__/session-close-loop.test.d.ts +2 -0
  189. package/dist/__tests__/session-close-loop.test.d.ts.map +1 -0
  190. package/dist/__tests__/session-close-loop.test.js +231 -0
  191. package/dist/__tests__/session-close-loop.test.js.map +1 -0
  192. package/dist/__tests__/session-label.test.d.ts +2 -0
  193. package/dist/__tests__/session-label.test.d.ts.map +1 -0
  194. package/dist/__tests__/session-label.test.js +46 -0
  195. package/dist/__tests__/session-label.test.js.map +1 -0
  196. package/dist/__tests__/session-list-show.test.d.ts +2 -0
  197. package/dist/__tests__/session-list-show.test.d.ts.map +1 -0
  198. package/dist/__tests__/session-list-show.test.js +174 -0
  199. package/dist/__tests__/session-list-show.test.js.map +1 -0
  200. package/dist/__tests__/session-reset.test.d.ts +2 -0
  201. package/dist/__tests__/session-reset.test.d.ts.map +1 -0
  202. package/dist/__tests__/session-reset.test.js +138 -0
  203. package/dist/__tests__/session-reset.test.js.map +1 -0
  204. package/dist/__tests__/session-resume-backstop.test.d.ts +2 -0
  205. package/dist/__tests__/session-resume-backstop.test.d.ts.map +1 -0
  206. package/dist/__tests__/session-resume-backstop.test.js +99 -0
  207. package/dist/__tests__/session-resume-backstop.test.js.map +1 -0
  208. package/dist/__tests__/session-start-key-refresh.test.d.ts +2 -0
  209. package/dist/__tests__/session-start-key-refresh.test.d.ts.map +1 -0
  210. package/dist/__tests__/session-start-key-refresh.test.js +183 -0
  211. package/dist/__tests__/session-start-key-refresh.test.js.map +1 -0
  212. package/dist/__tests__/session-state-machine.test.d.ts +2 -0
  213. package/dist/__tests__/session-state-machine.test.d.ts.map +1 -0
  214. package/dist/__tests__/session-state-machine.test.js +379 -0
  215. package/dist/__tests__/session-state-machine.test.js.map +1 -0
  216. package/dist/__tests__/session-switch.test.d.ts +2 -0
  217. package/dist/__tests__/session-switch.test.d.ts.map +1 -0
  218. package/dist/__tests__/session-switch.test.js +132 -0
  219. package/dist/__tests__/session-switch.test.js.map +1 -0
  220. package/dist/__tests__/session-touch.test.js +11 -11
  221. package/dist/__tests__/session-touch.test.js.map +1 -1
  222. package/dist/__tests__/session.test.js +4 -10
  223. package/dist/__tests__/session.test.js.map +1 -1
  224. package/dist/__tests__/setup-ingest.test.d.ts +2 -0
  225. package/dist/__tests__/setup-ingest.test.d.ts.map +1 -0
  226. package/dist/__tests__/setup-ingest.test.js +71 -0
  227. package/dist/__tests__/setup-ingest.test.js.map +1 -0
  228. package/dist/__tests__/setup-resolver.test.d.ts +14 -0
  229. package/dist/__tests__/setup-resolver.test.d.ts.map +1 -0
  230. package/dist/__tests__/setup-resolver.test.js +228 -0
  231. package/dist/__tests__/setup-resolver.test.js.map +1 -0
  232. package/dist/__tests__/skill-vocabulary.test.d.ts +21 -0
  233. package/dist/__tests__/skill-vocabulary.test.d.ts.map +1 -0
  234. package/dist/__tests__/skill-vocabulary.test.js +192 -0
  235. package/dist/__tests__/skill-vocabulary.test.js.map +1 -0
  236. package/dist/__tests__/spinner-labels.test.d.ts +2 -0
  237. package/dist/__tests__/spinner-labels.test.d.ts.map +1 -0
  238. package/dist/__tests__/spinner-labels.test.js +23 -0
  239. package/dist/__tests__/spinner-labels.test.js.map +1 -0
  240. package/dist/__tests__/state.test.d.ts +6 -0
  241. package/dist/__tests__/state.test.d.ts.map +1 -0
  242. package/dist/__tests__/state.test.js +97 -0
  243. package/dist/__tests__/state.test.js.map +1 -0
  244. package/dist/__tests__/surfaces.test.d.ts +2 -0
  245. package/dist/__tests__/surfaces.test.d.ts.map +1 -0
  246. package/dist/__tests__/surfaces.test.js +46 -0
  247. package/dist/__tests__/surfaces.test.js.map +1 -0
  248. package/dist/__tests__/update-check.test.d.ts +2 -0
  249. package/dist/__tests__/update-check.test.d.ts.map +1 -0
  250. package/dist/__tests__/update-check.test.js +215 -0
  251. package/dist/__tests__/update-check.test.js.map +1 -0
  252. package/dist/__tests__/update.test.js +122 -81
  253. package/dist/__tests__/update.test.js.map +1 -1
  254. package/dist/__tests__/upgrade-runner.test.d.ts +2 -0
  255. package/dist/__tests__/upgrade-runner.test.d.ts.map +1 -0
  256. package/dist/__tests__/upgrade-runner.test.js +54 -0
  257. package/dist/__tests__/upgrade-runner.test.js.map +1 -0
  258. package/dist/__tests__/upgrade.test.d.ts +2 -0
  259. package/dist/__tests__/upgrade.test.d.ts.map +1 -0
  260. package/dist/__tests__/upgrade.test.js +56 -0
  261. package/dist/__tests__/upgrade.test.js.map +1 -0
  262. package/dist/__tests__/verify-formatter.test.d.ts +2 -0
  263. package/dist/__tests__/verify-formatter.test.d.ts.map +1 -0
  264. package/dist/__tests__/verify-formatter.test.js +85 -0
  265. package/dist/__tests__/verify-formatter.test.js.map +1 -0
  266. package/dist/__tests__/vocabulary-leak.test.d.ts +39 -0
  267. package/dist/__tests__/vocabulary-leak.test.d.ts.map +1 -0
  268. package/dist/__tests__/vocabulary-leak.test.js +539 -0
  269. package/dist/__tests__/vocabulary-leak.test.js.map +1 -0
  270. package/dist/__tests__/workspace.test.d.ts +2 -0
  271. package/dist/__tests__/workspace.test.d.ts.map +1 -0
  272. package/dist/__tests__/workspace.test.js +328 -0
  273. package/dist/__tests__/workspace.test.js.map +1 -0
  274. package/dist/__tests__/workspaceNotice.test.d.ts +2 -0
  275. package/dist/__tests__/workspaceNotice.test.d.ts.map +1 -0
  276. package/dist/__tests__/workspaceNotice.test.js +338 -0
  277. package/dist/__tests__/workspaceNotice.test.js.map +1 -0
  278. package/dist/commands/__tests__/connect-handoff.test.d.ts +11 -0
  279. package/dist/commands/__tests__/connect-handoff.test.d.ts.map +1 -0
  280. package/dist/commands/__tests__/connect-handoff.test.js +111 -0
  281. package/dist/commands/__tests__/connect-handoff.test.js.map +1 -0
  282. package/dist/commands/__tests__/rework.test.d.ts +2 -0
  283. package/dist/commands/__tests__/rework.test.d.ts.map +1 -0
  284. package/dist/commands/__tests__/rework.test.js +149 -0
  285. package/dist/commands/__tests__/rework.test.js.map +1 -0
  286. package/dist/commands/__tests__/setup-detect-surfaces.test.d.ts +15 -0
  287. package/dist/commands/__tests__/setup-detect-surfaces.test.d.ts.map +1 -0
  288. package/dist/commands/__tests__/setup-detect-surfaces.test.js +149 -0
  289. package/dist/commands/__tests__/setup-detect-surfaces.test.js.map +1 -0
  290. package/dist/commands/__tests__/setup-state.test.d.ts +2 -0
  291. package/dist/commands/__tests__/setup-state.test.d.ts.map +1 -0
  292. package/dist/commands/__tests__/setup-state.test.js +194 -0
  293. package/dist/commands/__tests__/setup-state.test.js.map +1 -0
  294. package/dist/commands/accept.d.ts.map +1 -1
  295. package/dist/commands/accept.js +10 -6
  296. package/dist/commands/accept.js.map +1 -1
  297. package/dist/commands/admin/cockpit.d.ts +90 -0
  298. package/dist/commands/admin/cockpit.d.ts.map +1 -0
  299. package/dist/commands/admin/cockpit.js +618 -0
  300. package/dist/commands/admin/cockpit.js.map +1 -0
  301. package/dist/commands/admin/index.d.ts +21 -0
  302. package/dist/commands/admin/index.d.ts.map +1 -0
  303. package/dist/commands/admin/index.js +256 -0
  304. package/dist/commands/admin/index.js.map +1 -0
  305. package/dist/commands/admin/inspect.d.ts +30 -0
  306. package/dist/commands/admin/inspect.d.ts.map +1 -0
  307. package/dist/commands/admin/inspect.js +555 -0
  308. package/dist/commands/admin/inspect.js.map +1 -0
  309. package/dist/commands/admin/inspect.test.d.ts +7 -0
  310. package/dist/commands/admin/inspect.test.d.ts.map +1 -0
  311. package/dist/commands/admin/inspect.test.js +90 -0
  312. package/dist/commands/admin/inspect.test.js.map +1 -0
  313. package/dist/commands/admin/manage.d.ts +8 -0
  314. package/dist/commands/admin/manage.d.ts.map +1 -0
  315. package/dist/commands/admin/manage.js +260 -0
  316. package/dist/commands/admin/manage.js.map +1 -0
  317. package/dist/commands/admin/manage.test.d.ts +16 -0
  318. package/dist/commands/admin/manage.test.d.ts.map +1 -0
  319. package/dist/commands/admin/manage.test.js +159 -0
  320. package/dist/commands/admin/manage.test.js.map +1 -0
  321. package/dist/commands/admin/seed.d.ts +75 -0
  322. package/dist/commands/admin/seed.d.ts.map +1 -0
  323. package/dist/commands/admin/seed.js +944 -0
  324. package/dist/commands/admin/seed.js.map +1 -0
  325. package/dist/commands/admin/seed.test.d.ts +11 -0
  326. package/dist/commands/admin/seed.test.d.ts.map +1 -0
  327. package/dist/commands/admin/seed.test.js +127 -0
  328. package/dist/commands/admin/seed.test.js.map +1 -0
  329. package/dist/commands/admin/seedRegistryEntries.generated.d.ts +14 -0
  330. package/dist/commands/admin/seedRegistryEntries.generated.d.ts.map +1 -0
  331. package/dist/commands/admin/seedRegistryEntries.generated.js +153 -0
  332. package/dist/commands/admin/seedRegistryEntries.generated.js.map +1 -0
  333. package/dist/commands/admin/seedRegistryEntries.test.d.ts +13 -0
  334. package/dist/commands/admin/seedRegistryEntries.test.d.ts.map +1 -0
  335. package/dist/commands/admin/seedRegistryEntries.test.js +91 -0
  336. package/dist/commands/admin/seedRegistryEntries.test.js.map +1 -0
  337. package/dist/commands/audit.d.ts.map +1 -1
  338. package/dist/commands/audit.js +42 -7
  339. package/dist/commands/audit.js.map +1 -1
  340. package/dist/commands/authority-domains.d.ts +157 -0
  341. package/dist/commands/authority-domains.d.ts.map +1 -0
  342. package/dist/commands/authority-domains.js +387 -0
  343. package/dist/commands/authority-domains.js.map +1 -0
  344. package/dist/commands/brief.d.ts.map +1 -1
  345. package/dist/commands/brief.js +14 -5
  346. package/dist/commands/brief.js.map +1 -1
  347. package/dist/commands/capture.d.ts +12 -0
  348. package/dist/commands/capture.d.ts.map +1 -1
  349. package/dist/commands/capture.js +277 -80
  350. package/dist/commands/capture.js.map +1 -1
  351. package/dist/commands/chain-walk.d.ts.map +1 -1
  352. package/dist/commands/chain-walk.js +8 -3
  353. package/dist/commands/chain-walk.js.map +1 -1
  354. package/dist/commands/changes.d.ts.map +1 -1
  355. package/dist/commands/changes.js +8 -3
  356. package/dist/commands/changes.js.map +1 -1
  357. package/dist/commands/codex-prep.d.ts +12 -1
  358. package/dist/commands/codex-prep.d.ts.map +1 -1
  359. package/dist/commands/codex-prep.js +25 -87
  360. package/dist/commands/codex-prep.js.map +1 -1
  361. package/dist/commands/collections.d.ts.map +1 -1
  362. package/dist/commands/collections.js +11 -7
  363. package/dist/commands/collections.js.map +1 -1
  364. package/dist/commands/conflicts.d.ts +10 -0
  365. package/dist/commands/conflicts.d.ts.map +1 -0
  366. package/dist/commands/conflicts.js +103 -0
  367. package/dist/commands/conflicts.js.map +1 -0
  368. package/dist/commands/conflicts.test.d.ts +2 -0
  369. package/dist/commands/conflicts.test.d.ts.map +1 -0
  370. package/dist/commands/conflicts.test.js +97 -0
  371. package/dist/commands/conflicts.test.js.map +1 -0
  372. package/dist/commands/connect-config.test.d.ts +2 -0
  373. package/dist/commands/connect-config.test.d.ts.map +1 -0
  374. package/dist/commands/connect-config.test.js +44 -0
  375. package/dist/commands/connect-config.test.js.map +1 -0
  376. package/dist/commands/connect-context.d.ts +45 -0
  377. package/dist/commands/connect-context.d.ts.map +1 -0
  378. package/dist/commands/connect-context.js +83 -0
  379. package/dist/commands/connect-context.js.map +1 -0
  380. package/dist/commands/connect-context.test.d.ts +2 -0
  381. package/dist/commands/connect-context.test.d.ts.map +1 -0
  382. package/dist/commands/connect-context.test.js +158 -0
  383. package/dist/commands/connect-context.test.js.map +1 -0
  384. package/dist/commands/connect-handoff.d.ts +51 -0
  385. package/dist/commands/connect-handoff.d.ts.map +1 -0
  386. package/dist/commands/connect-handoff.js +70 -0
  387. package/dist/commands/connect-handoff.js.map +1 -0
  388. package/dist/commands/connect-integration.test.d.ts +7 -0
  389. package/dist/commands/connect-integration.test.d.ts.map +1 -0
  390. package/dist/commands/connect-integration.test.js +219 -0
  391. package/dist/commands/connect-integration.test.js.map +1 -0
  392. package/dist/commands/connect-screens.d.ts +33 -0
  393. package/dist/commands/connect-screens.d.ts.map +1 -0
  394. package/dist/commands/connect-screens.js +124 -0
  395. package/dist/commands/connect-screens.js.map +1 -0
  396. package/dist/commands/connect.d.ts +42 -0
  397. package/dist/commands/connect.d.ts.map +1 -0
  398. package/dist/commands/connect.js +361 -0
  399. package/dist/commands/connect.js.map +1 -0
  400. package/dist/commands/connect.test.d.ts +6 -0
  401. package/dist/commands/connect.test.d.ts.map +1 -0
  402. package/dist/commands/connect.test.js +428 -0
  403. package/dist/commands/connect.test.js.map +1 -0
  404. package/dist/commands/constellation.d.ts.map +1 -1
  405. package/dist/commands/constellation.js +8 -3
  406. package/dist/commands/constellation.js.map +1 -1
  407. package/dist/commands/context.d.ts.map +1 -1
  408. package/dist/commands/context.js +8 -3
  409. package/dist/commands/context.js.map +1 -1
  410. package/dist/commands/cross-cut.d.ts.map +1 -1
  411. package/dist/commands/cross-cut.js +2 -2
  412. package/dist/commands/cross-cut.js.map +1 -1
  413. package/dist/commands/doctor.d.ts +9 -2
  414. package/dist/commands/doctor.d.ts.map +1 -1
  415. package/dist/commands/doctor.js +215 -42
  416. package/dist/commands/doctor.js.map +1 -1
  417. package/dist/commands/doctor.test.d.ts +3 -1
  418. package/dist/commands/doctor.test.d.ts.map +1 -1
  419. package/dist/commands/doctor.test.js +370 -11
  420. package/dist/commands/doctor.test.js.map +1 -1
  421. package/dist/commands/feedback.d.ts +36 -0
  422. package/dist/commands/feedback.d.ts.map +1 -0
  423. package/dist/commands/feedback.js +187 -0
  424. package/dist/commands/feedback.js.map +1 -0
  425. package/dist/commands/feedback.test.d.ts +2 -0
  426. package/dist/commands/feedback.test.d.ts.map +1 -0
  427. package/dist/commands/feedback.test.js +119 -0
  428. package/dist/commands/feedback.test.js.map +1 -0
  429. package/dist/commands/fields.d.ts.map +1 -1
  430. package/dist/commands/fields.js +8 -4
  431. package/dist/commands/fields.js.map +1 -1
  432. package/dist/commands/files.d.ts +26 -0
  433. package/dist/commands/files.d.ts.map +1 -0
  434. package/dist/commands/files.js +160 -0
  435. package/dist/commands/files.js.map +1 -0
  436. package/dist/commands/files.test.d.ts +2 -0
  437. package/dist/commands/files.test.d.ts.map +1 -0
  438. package/dist/commands/files.test.js +243 -0
  439. package/dist/commands/files.test.js.map +1 -0
  440. package/dist/commands/get.d.ts.map +1 -1
  441. package/dist/commands/get.js +14 -6
  442. package/dist/commands/get.js.map +1 -1
  443. package/dist/commands/handshake.d.ts +225 -2
  444. package/dist/commands/handshake.d.ts.map +1 -1
  445. package/dist/commands/handshake.js +2139 -81
  446. package/dist/commands/handshake.js.map +1 -1
  447. package/dist/commands/ingest.d.ts.map +1 -1
  448. package/dist/commands/ingest.js +26 -9
  449. package/dist/commands/ingest.js.map +1 -1
  450. package/dist/commands/init.d.ts +14 -0
  451. package/dist/commands/init.d.ts.map +1 -0
  452. package/dist/commands/init.js +109 -0
  453. package/dist/commands/init.js.map +1 -0
  454. package/dist/commands/login.d.ts.map +1 -1
  455. package/dist/commands/login.js +72 -35
  456. package/dist/commands/login.js.map +1 -1
  457. package/dist/commands/method.d.ts +99 -0
  458. package/dist/commands/method.d.ts.map +1 -0
  459. package/dist/commands/method.js +804 -0
  460. package/dist/commands/method.js.map +1 -0
  461. package/dist/commands/migrate-setup.d.ts +18 -0
  462. package/dist/commands/migrate-setup.d.ts.map +1 -0
  463. package/dist/commands/migrate-setup.js +148 -0
  464. package/dist/commands/migrate-setup.js.map +1 -0
  465. package/dist/commands/organisation.d.ts +41 -0
  466. package/dist/commands/organisation.d.ts.map +1 -0
  467. package/dist/commands/organisation.js +353 -0
  468. package/dist/commands/organisation.js.map +1 -0
  469. package/dist/commands/organisation.test.d.ts +2 -0
  470. package/dist/commands/organisation.test.d.ts.map +1 -0
  471. package/dist/commands/organisation.test.js +233 -0
  472. package/dist/commands/organisation.test.js.map +1 -0
  473. package/dist/commands/organisationHierarchy.d.ts +8 -0
  474. package/dist/commands/organisationHierarchy.d.ts.map +1 -0
  475. package/dist/commands/organisationHierarchy.js +67 -0
  476. package/dist/commands/organisationHierarchy.js.map +1 -0
  477. package/dist/commands/organisationHierarchy.test.d.ts +2 -0
  478. package/dist/commands/organisationHierarchy.test.d.ts.map +1 -0
  479. package/dist/commands/organisationHierarchy.test.js +55 -0
  480. package/dist/commands/organisationHierarchy.test.js.map +1 -0
  481. package/dist/commands/organisationWrite.d.ts +18 -0
  482. package/dist/commands/organisationWrite.d.ts.map +1 -0
  483. package/dist/commands/organisationWrite.js +50 -0
  484. package/dist/commands/organisationWrite.js.map +1 -0
  485. package/dist/commands/orient.d.ts +121 -2
  486. package/dist/commands/orient.d.ts.map +1 -1
  487. package/dist/commands/orient.js +222 -7
  488. package/dist/commands/orient.js.map +1 -1
  489. package/dist/commands/profile.d.ts +34 -0
  490. package/dist/commands/profile.d.ts.map +1 -0
  491. package/dist/commands/profile.js +170 -0
  492. package/dist/commands/profile.js.map +1 -0
  493. package/dist/commands/promote.d.ts +12 -1
  494. package/dist/commands/promote.d.ts.map +1 -1
  495. package/dist/commands/promote.js +246 -55
  496. package/dist/commands/promote.js.map +1 -1
  497. package/dist/commands/proof-run.d.ts +51 -0
  498. package/dist/commands/proof-run.d.ts.map +1 -0
  499. package/dist/commands/proof-run.js +209 -0
  500. package/dist/commands/proof-run.js.map +1 -0
  501. package/dist/commands/proposals.d.ts.map +1 -1
  502. package/dist/commands/proposals.js +2 -2
  503. package/dist/commands/proposals.js.map +1 -1
  504. package/dist/commands/reject.d.ts.map +1 -1
  505. package/dist/commands/reject.js +14 -7
  506. package/dist/commands/reject.js.map +1 -1
  507. package/dist/commands/relate.d.ts.map +1 -1
  508. package/dist/commands/relate.js +45 -14
  509. package/dist/commands/relate.js.map +1 -1
  510. package/dist/commands/restore.d.ts +2 -0
  511. package/dist/commands/restore.d.ts.map +1 -0
  512. package/dist/commands/restore.js +83 -0
  513. package/dist/commands/restore.js.map +1 -0
  514. package/dist/commands/rework.d.ts +24 -0
  515. package/dist/commands/rework.d.ts.map +1 -0
  516. package/dist/commands/rework.js +97 -0
  517. package/dist/commands/rework.js.map +1 -0
  518. package/dist/commands/scoreboard.d.ts +28 -0
  519. package/dist/commands/scoreboard.d.ts.map +1 -0
  520. package/dist/commands/scoreboard.js +40 -0
  521. package/dist/commands/scoreboard.js.map +1 -0
  522. package/dist/commands/search.d.ts.map +1 -1
  523. package/dist/commands/search.js +2 -2
  524. package/dist/commands/search.js.map +1 -1
  525. package/dist/commands/session.d.ts +55 -2
  526. package/dist/commands/session.d.ts.map +1 -1
  527. package/dist/commands/session.js +485 -45
  528. package/dist/commands/session.js.map +1 -1
  529. package/dist/commands/setup-audit.d.ts +59 -0
  530. package/dist/commands/setup-audit.d.ts.map +1 -0
  531. package/dist/commands/setup-audit.js +250 -0
  532. package/dist/commands/setup-audit.js.map +1 -0
  533. package/dist/commands/setup-detect-surfaces.d.ts +38 -0
  534. package/dist/commands/setup-detect-surfaces.d.ts.map +1 -0
  535. package/dist/commands/setup-detect-surfaces.js +76 -0
  536. package/dist/commands/setup-detect-surfaces.js.map +1 -0
  537. package/dist/commands/setup-ingest.d.ts +17 -0
  538. package/dist/commands/setup-ingest.d.ts.map +1 -0
  539. package/dist/commands/setup-ingest.js +174 -0
  540. package/dist/commands/setup-ingest.js.map +1 -0
  541. package/dist/commands/setup-resolver.d.ts +58 -0
  542. package/dist/commands/setup-resolver.d.ts.map +1 -0
  543. package/dist/commands/setup-resolver.js +150 -0
  544. package/dist/commands/setup-resolver.js.map +1 -0
  545. package/dist/commands/setup-state.d.ts +42 -0
  546. package/dist/commands/setup-state.d.ts.map +1 -0
  547. package/dist/commands/setup-state.js +93 -0
  548. package/dist/commands/setup-state.js.map +1 -0
  549. package/dist/commands/setup.d.ts +17 -10
  550. package/dist/commands/setup.d.ts.map +1 -1
  551. package/dist/commands/setup.js +50 -194
  552. package/dist/commands/setup.js.map +1 -1
  553. package/dist/commands/update.d.ts +8 -0
  554. package/dist/commands/update.d.ts.map +1 -1
  555. package/dist/commands/update.js +90 -49
  556. package/dist/commands/update.js.map +1 -1
  557. package/dist/commands/upgrade.d.ts +5 -0
  558. package/dist/commands/upgrade.d.ts.map +1 -0
  559. package/dist/commands/upgrade.js +110 -0
  560. package/dist/commands/upgrade.js.map +1 -0
  561. package/dist/commands/usage.d.ts +40 -0
  562. package/dist/commands/usage.d.ts.map +1 -0
  563. package/dist/commands/usage.js +214 -0
  564. package/dist/commands/usage.js.map +1 -0
  565. package/dist/commands/verify.d.ts.map +1 -1
  566. package/dist/commands/verify.js +19 -7
  567. package/dist/commands/verify.js.map +1 -1
  568. package/dist/commands/welcome.d.ts +21 -0
  569. package/dist/commands/welcome.d.ts.map +1 -0
  570. package/dist/commands/welcome.js +50 -0
  571. package/dist/commands/welcome.js.map +1 -0
  572. package/dist/commands/whoami.d.ts +12 -0
  573. package/dist/commands/whoami.d.ts.map +1 -0
  574. package/dist/commands/whoami.js +70 -0
  575. package/dist/commands/whoami.js.map +1 -0
  576. package/dist/commands/whoami.test.d.ts +2 -0
  577. package/dist/commands/whoami.test.d.ts.map +1 -0
  578. package/dist/commands/whoami.test.js +50 -0
  579. package/dist/commands/whoami.test.js.map +1 -0
  580. package/dist/commands/workspace.d.ts +115 -0
  581. package/dist/commands/workspace.d.ts.map +1 -0
  582. package/dist/commands/workspace.js +263 -0
  583. package/dist/commands/workspace.js.map +1 -0
  584. package/dist/formatters/__tests__/orient-possibleConflict.test.d.ts +2 -0
  585. package/dist/formatters/__tests__/orient-possibleConflict.test.d.ts.map +1 -0
  586. package/dist/formatters/__tests__/orient-possibleConflict.test.js +68 -0
  587. package/dist/formatters/__tests__/orient-possibleConflict.test.js.map +1 -0
  588. package/dist/formatters/__tests__/orient-provenance.test.d.ts +7 -0
  589. package/dist/formatters/__tests__/orient-provenance.test.d.ts.map +1 -0
  590. package/dist/formatters/__tests__/orient-provenance.test.js +548 -0
  591. package/dist/formatters/__tests__/orient-provenance.test.js.map +1 -0
  592. package/dist/formatters/__tests__/session-list-show.test.d.ts +2 -0
  593. package/dist/formatters/__tests__/session-list-show.test.d.ts.map +1 -0
  594. package/dist/formatters/__tests__/session-list-show.test.js +159 -0
  595. package/dist/formatters/__tests__/session-list-show.test.js.map +1 -0
  596. package/dist/formatters/__tests__/session.test.d.ts +2 -0
  597. package/dist/formatters/__tests__/session.test.d.ts.map +1 -0
  598. package/dist/formatters/__tests__/session.test.js +113 -0
  599. package/dist/formatters/__tests__/session.test.js.map +1 -0
  600. package/dist/formatters/audit.d.ts +6 -0
  601. package/dist/formatters/audit.d.ts.map +1 -1
  602. package/dist/formatters/audit.js.map +1 -1
  603. package/dist/formatters/capture.d.ts +8 -0
  604. package/dist/formatters/capture.d.ts.map +1 -1
  605. package/dist/formatters/capture.js +9 -0
  606. package/dist/formatters/capture.js.map +1 -1
  607. package/dist/formatters/coaching.d.ts +51 -0
  608. package/dist/formatters/coaching.d.ts.map +1 -0
  609. package/dist/formatters/coaching.js +46 -0
  610. package/dist/formatters/coaching.js.map +1 -0
  611. package/dist/formatters/conflicts.d.ts +44 -0
  612. package/dist/formatters/conflicts.d.ts.map +1 -0
  613. package/dist/formatters/conflicts.js +44 -0
  614. package/dist/formatters/conflicts.js.map +1 -0
  615. package/dist/formatters/conflicts.test.d.ts +2 -0
  616. package/dist/formatters/conflicts.test.d.ts.map +1 -0
  617. package/dist/formatters/conflicts.test.js +83 -0
  618. package/dist/formatters/conflicts.test.js.map +1 -0
  619. package/dist/formatters/context.d.ts +1 -1
  620. package/dist/formatters/context.d.ts.map +1 -1
  621. package/dist/formatters/context.js.map +1 -1
  622. package/dist/formatters/entry.d.ts +44 -0
  623. package/dist/formatters/entry.d.ts.map +1 -1
  624. package/dist/formatters/entry.js +70 -7
  625. package/dist/formatters/entry.js.map +1 -1
  626. package/dist/formatters/feedback.d.ts +56 -0
  627. package/dist/formatters/feedback.d.ts.map +1 -0
  628. package/dist/formatters/feedback.js +89 -0
  629. package/dist/formatters/feedback.js.map +1 -0
  630. package/dist/formatters/feedback.test.d.ts +2 -0
  631. package/dist/formatters/feedback.test.d.ts.map +1 -0
  632. package/dist/formatters/feedback.test.js +112 -0
  633. package/dist/formatters/feedback.test.js.map +1 -0
  634. package/dist/formatters/handshake.d.ts +39 -0
  635. package/dist/formatters/handshake.d.ts.map +1 -1
  636. package/dist/formatters/handshake.js +133 -3
  637. package/dist/formatters/handshake.js.map +1 -1
  638. package/dist/formatters/orient.coherencyCheckTag.test.d.ts +2 -0
  639. package/dist/formatters/orient.coherencyCheckTag.test.d.ts.map +1 -0
  640. package/dist/formatters/orient.coherencyCheckTag.test.js +22 -0
  641. package/dist/formatters/orient.coherencyCheckTag.test.js.map +1 -0
  642. package/dist/formatters/orient.d.ts +245 -4
  643. package/dist/formatters/orient.d.ts.map +1 -1
  644. package/dist/formatters/orient.js +302 -25
  645. package/dist/formatters/orient.js.map +1 -1
  646. package/dist/formatters/orient.rcrtMiss.test.d.ts +2 -0
  647. package/dist/formatters/orient.rcrtMiss.test.d.ts.map +1 -0
  648. package/dist/formatters/orient.rcrtMiss.test.js +146 -0
  649. package/dist/formatters/orient.rcrtMiss.test.js.map +1 -0
  650. package/dist/formatters/promote.d.ts +43 -0
  651. package/dist/formatters/promote.d.ts.map +1 -1
  652. package/dist/formatters/promote.js +32 -0
  653. package/dist/formatters/promote.js.map +1 -1
  654. package/dist/formatters/scoreboard.d.ts +11 -0
  655. package/dist/formatters/scoreboard.d.ts.map +1 -0
  656. package/dist/formatters/scoreboard.js +107 -0
  657. package/dist/formatters/scoreboard.js.map +1 -0
  658. package/dist/formatters/scoreboard.test.d.ts +2 -0
  659. package/dist/formatters/scoreboard.test.d.ts.map +1 -0
  660. package/dist/formatters/scoreboard.test.js +144 -0
  661. package/dist/formatters/scoreboard.test.js.map +1 -0
  662. package/dist/formatters/session-feedback.test.d.ts +2 -0
  663. package/dist/formatters/session-feedback.test.d.ts.map +1 -0
  664. package/dist/formatters/session-feedback.test.js +49 -0
  665. package/dist/formatters/session-feedback.test.js.map +1 -0
  666. package/dist/formatters/session.d.ts +101 -1
  667. package/dist/formatters/session.d.ts.map +1 -1
  668. package/dist/formatters/session.js +272 -9
  669. package/dist/formatters/session.js.map +1 -1
  670. package/dist/formatters/update.d.ts +3 -0
  671. package/dist/formatters/update.d.ts.map +1 -1
  672. package/dist/formatters/update.js +4 -0
  673. package/dist/formatters/update.js.map +1 -1
  674. package/dist/formatters/verify.d.ts +12 -4
  675. package/dist/formatters/verify.d.ts.map +1 -1
  676. package/dist/formatters/verify.js +20 -2
  677. package/dist/formatters/verify.js.map +1 -1
  678. package/dist/generated/healthGapKinds.generated.d.ts +12 -0
  679. package/dist/generated/healthGapKinds.generated.d.ts.map +1 -0
  680. package/dist/generated/healthGapKinds.generated.js +24 -0
  681. package/dist/generated/healthGapKinds.generated.js.map +1 -0
  682. package/dist/generators/adapters.d.ts +33 -0
  683. package/dist/generators/adapters.d.ts.map +1 -1
  684. package/dist/generators/adapters.js +64 -4
  685. package/dist/generators/adapters.js.map +1 -1
  686. package/dist/generators/archetypes.d.ts.map +1 -1
  687. package/dist/generators/archetypes.js +46 -9
  688. package/dist/generators/archetypes.js.map +1 -1
  689. package/dist/generators/archetypes.test.js +35 -0
  690. package/dist/generators/archetypes.test.js.map +1 -1
  691. package/dist/generators/boundary-manifest.d.ts +29 -0
  692. package/dist/generators/boundary-manifest.d.ts.map +1 -0
  693. package/dist/generators/boundary-manifest.js +183 -0
  694. package/dist/generators/boundary-manifest.js.map +1 -0
  695. package/dist/generators/boundary-manifest.real-file.test.d.ts +2 -0
  696. package/dist/generators/boundary-manifest.real-file.test.d.ts.map +1 -0
  697. package/dist/generators/boundary-manifest.real-file.test.js +70 -0
  698. package/dist/generators/boundary-manifest.real-file.test.js.map +1 -0
  699. package/dist/generators/boundary-manifest.test.d.ts +2 -0
  700. package/dist/generators/boundary-manifest.test.d.ts.map +1 -0
  701. package/dist/generators/boundary-manifest.test.js +91 -0
  702. package/dist/generators/boundary-manifest.test.js.map +1 -0
  703. package/dist/generators/chain-classifier-kernel-client-drift.test.d.ts +2 -0
  704. package/dist/generators/chain-classifier-kernel-client-drift.test.d.ts.map +1 -0
  705. package/dist/generators/chain-classifier-kernel-client-drift.test.js +41 -0
  706. package/dist/generators/chain-classifier-kernel-client-drift.test.js.map +1 -0
  707. package/dist/generators/chain-classifier.d.ts +67 -14
  708. package/dist/generators/chain-classifier.d.ts.map +1 -1
  709. package/dist/generators/chain-classifier.js +46 -174
  710. package/dist/generators/chain-classifier.js.map +1 -1
  711. package/dist/generators/chain-rules.d.ts +3 -3
  712. package/dist/generators/chain-rules.d.ts.map +1 -1
  713. package/dist/generators/chain-rules.js +118 -6
  714. package/dist/generators/chain-rules.js.map +1 -1
  715. package/dist/generators/chain-rules.test.js +273 -4
  716. package/dist/generators/chain-rules.test.js.map +1 -1
  717. package/dist/generators/context-md.d.ts +1 -1
  718. package/dist/generators/context-md.d.ts.map +1 -1
  719. package/dist/generators/context-md.js +18 -7
  720. package/dist/generators/context-md.js.map +1 -1
  721. package/dist/generators/manifest.d.ts +115 -0
  722. package/dist/generators/manifest.d.ts.map +1 -0
  723. package/dist/generators/manifest.js +277 -0
  724. package/dist/generators/manifest.js.map +1 -0
  725. package/dist/generators/portable-knowledge.d.ts +24 -2
  726. package/dist/generators/portable-knowledge.d.ts.map +1 -1
  727. package/dist/generators/portable-knowledge.js +97 -2
  728. package/dist/generators/portable-knowledge.js.map +1 -1
  729. package/dist/generators/portable-knowledge.test.js +43 -0
  730. package/dist/generators/portable-knowledge.test.js.map +1 -1
  731. package/dist/generators/region-projections.d.ts +18 -0
  732. package/dist/generators/region-projections.d.ts.map +1 -0
  733. package/dist/generators/region-projections.js +49 -0
  734. package/dist/generators/region-projections.js.map +1 -0
  735. package/dist/generators/region-projections.test.d.ts +2 -0
  736. package/dist/generators/region-projections.test.d.ts.map +1 -0
  737. package/dist/generators/region-projections.test.js +63 -0
  738. package/dist/generators/region-projections.test.js.map +1 -0
  739. package/dist/generators/region.d.ts +24 -0
  740. package/dist/generators/region.d.ts.map +1 -0
  741. package/dist/generators/region.js +87 -0
  742. package/dist/generators/region.js.map +1 -0
  743. package/dist/generators/region.test.d.ts +2 -0
  744. package/dist/generators/region.test.d.ts.map +1 -0
  745. package/dist/generators/region.test.js +126 -0
  746. package/dist/generators/region.test.js.map +1 -0
  747. package/dist/generators/surface-profiles.d.ts +1 -2
  748. package/dist/generators/surface-profiles.d.ts.map +1 -1
  749. package/dist/generators/surface-profiles.js.map +1 -1
  750. package/dist/index.js +746 -304
  751. package/dist/index.js.map +1 -1
  752. package/dist/lib/__tests__/parseFields.test.d.ts +2 -0
  753. package/dist/lib/__tests__/parseFields.test.d.ts.map +1 -0
  754. package/dist/lib/__tests__/parseFields.test.js +40 -0
  755. package/dist/lib/__tests__/parseFields.test.js.map +1 -0
  756. package/dist/lib/activation.d.ts.map +1 -1
  757. package/dist/lib/activation.js +8 -4
  758. package/dist/lib/activation.js.map +1 -1
  759. package/dist/lib/activation.test.js +6 -6
  760. package/dist/lib/activation.test.js.map +1 -1
  761. package/dist/lib/canonicalRefs.d.ts +141 -0
  762. package/dist/lib/canonicalRefs.d.ts.map +1 -0
  763. package/dist/lib/canonicalRefs.js +150 -0
  764. package/dist/lib/canonicalRefs.js.map +1 -0
  765. package/dist/lib/client.d.ts +19 -6
  766. package/dist/lib/client.d.ts.map +1 -1
  767. package/dist/lib/client.js +227 -68
  768. package/dist/lib/client.js.map +1 -1
  769. package/dist/lib/collectionRegistry.d.ts +52 -0
  770. package/dist/lib/collectionRegistry.d.ts.map +1 -0
  771. package/dist/lib/collectionRegistry.js +118 -0
  772. package/dist/lib/collectionRegistry.js.map +1 -0
  773. package/dist/lib/compose-wrapup-view.d.ts +48 -0
  774. package/dist/lib/compose-wrapup-view.d.ts.map +1 -0
  775. package/dist/lib/compose-wrapup-view.js +37 -0
  776. package/dist/lib/compose-wrapup-view.js.map +1 -0
  777. package/dist/lib/config.d.ts +204 -4
  778. package/dist/lib/config.d.ts.map +1 -1
  779. package/dist/lib/config.js +590 -40
  780. package/dist/lib/config.js.map +1 -1
  781. package/dist/lib/connectKeyLabel.d.ts +9 -0
  782. package/dist/lib/connectKeyLabel.d.ts.map +1 -0
  783. package/dist/lib/connectKeyLabel.js +12 -0
  784. package/dist/lib/connectKeyLabel.js.map +1 -0
  785. package/dist/lib/constants.d.ts +28 -3
  786. package/dist/lib/constants.d.ts.map +1 -1
  787. package/dist/lib/constants.js +48 -4
  788. package/dist/lib/constants.js.map +1 -1
  789. package/dist/lib/conversation-engine.d.ts +45 -0
  790. package/dist/lib/conversation-engine.d.ts.map +1 -0
  791. package/dist/lib/conversation-engine.js +112 -0
  792. package/dist/lib/conversation-engine.js.map +1 -0
  793. package/dist/lib/conversation-phases.d.ts +59 -0
  794. package/dist/lib/conversation-phases.d.ts.map +1 -0
  795. package/dist/lib/conversation-phases.js +11 -0
  796. package/dist/lib/conversation-phases.js.map +1 -0
  797. package/dist/lib/conversation-signals.d.ts +30 -0
  798. package/dist/lib/conversation-signals.d.ts.map +1 -0
  799. package/dist/lib/conversation-signals.js +64 -0
  800. package/dist/lib/conversation-signals.js.map +1 -0
  801. package/dist/lib/conversation.d.ts +36 -0
  802. package/dist/lib/conversation.d.ts.map +1 -0
  803. package/dist/lib/conversation.js +66 -0
  804. package/dist/lib/conversation.js.map +1 -0
  805. package/dist/lib/deployment.d.ts +23 -0
  806. package/dist/lib/deployment.d.ts.map +1 -0
  807. package/dist/lib/deployment.js +78 -0
  808. package/dist/lib/deployment.js.map +1 -0
  809. package/dist/lib/deployment.test.d.ts +5 -0
  810. package/dist/lib/deployment.test.d.ts.map +1 -0
  811. package/dist/lib/deployment.test.js +54 -0
  812. package/dist/lib/deployment.test.js.map +1 -0
  813. package/dist/lib/errors.d.ts +112 -0
  814. package/dist/lib/errors.d.ts.map +1 -0
  815. package/dist/lib/errors.js +121 -0
  816. package/dist/lib/errors.js.map +1 -0
  817. package/dist/lib/experiment.d.ts +18 -0
  818. package/dist/lib/experiment.d.ts.map +1 -0
  819. package/dist/lib/experiment.js +28 -0
  820. package/dist/lib/experiment.js.map +1 -0
  821. package/dist/lib/format.d.ts +10 -0
  822. package/dist/lib/format.d.ts.map +1 -0
  823. package/dist/lib/format.js +27 -0
  824. package/dist/lib/format.js.map +1 -0
  825. package/dist/lib/frontmatter.d.ts +55 -0
  826. package/dist/lib/frontmatter.d.ts.map +1 -0
  827. package/dist/lib/frontmatter.js +92 -0
  828. package/dist/lib/frontmatter.js.map +1 -0
  829. package/dist/lib/frontmatter.test.d.ts +15 -0
  830. package/dist/lib/frontmatter.test.d.ts.map +1 -0
  831. package/dist/lib/frontmatter.test.js +98 -0
  832. package/dist/lib/frontmatter.test.js.map +1 -0
  833. package/dist/lib/glossary.d.ts +19 -0
  834. package/dist/lib/glossary.d.ts.map +1 -0
  835. package/dist/lib/glossary.js +53 -0
  836. package/dist/lib/glossary.js.map +1 -0
  837. package/dist/lib/hook-intents.d.ts +51 -0
  838. package/dist/lib/hook-intents.d.ts.map +1 -0
  839. package/dist/lib/hook-intents.js +85 -0
  840. package/dist/lib/hook-intents.js.map +1 -0
  841. package/dist/lib/inferSourceDate.d.ts +12 -0
  842. package/dist/lib/inferSourceDate.d.ts.map +1 -0
  843. package/dist/lib/inferSourceDate.js +44 -0
  844. package/dist/lib/inferSourceDate.js.map +1 -0
  845. package/dist/lib/method-registry.d.ts +37 -0
  846. package/dist/lib/method-registry.d.ts.map +1 -0
  847. package/dist/lib/method-registry.js +41 -0
  848. package/dist/lib/method-registry.js.map +1 -0
  849. package/dist/lib/normalizeMaterializedFilename.d.ts +28 -0
  850. package/dist/lib/normalizeMaterializedFilename.d.ts.map +1 -0
  851. package/dist/lib/normalizeMaterializedFilename.js +56 -0
  852. package/dist/lib/normalizeMaterializedFilename.js.map +1 -0
  853. package/dist/lib/normalizeMaterializedFilename.test.d.ts +16 -0
  854. package/dist/lib/normalizeMaterializedFilename.test.d.ts.map +1 -0
  855. package/dist/lib/normalizeMaterializedFilename.test.js +90 -0
  856. package/dist/lib/normalizeMaterializedFilename.test.js.map +1 -0
  857. package/dist/lib/notice-marker.d.ts +3 -0
  858. package/dist/lib/notice-marker.d.ts.map +1 -0
  859. package/dist/lib/notice-marker.js +53 -0
  860. package/dist/lib/notice-marker.js.map +1 -0
  861. package/dist/lib/onboarding-path-b.d.ts +10 -0
  862. package/dist/lib/onboarding-path-b.d.ts.map +1 -0
  863. package/dist/lib/onboarding-path-b.js +243 -0
  864. package/dist/lib/onboarding-path-b.js.map +1 -0
  865. package/dist/lib/onboarding-shared.d.ts +81 -0
  866. package/dist/lib/onboarding-shared.d.ts.map +1 -0
  867. package/dist/lib/onboarding-shared.js +175 -0
  868. package/dist/lib/onboarding-shared.js.map +1 -0
  869. package/dist/lib/onboarding-topics.d.ts +27 -0
  870. package/dist/lib/onboarding-topics.d.ts.map +1 -0
  871. package/dist/lib/onboarding-topics.js +57 -0
  872. package/dist/lib/onboarding-topics.js.map +1 -0
  873. package/dist/lib/onboarding.d.ts +17 -0
  874. package/dist/lib/onboarding.d.ts.map +1 -0
  875. package/dist/lib/onboarding.js +350 -0
  876. package/dist/lib/onboarding.js.map +1 -0
  877. package/dist/lib/parseFields.d.ts +19 -0
  878. package/dist/lib/parseFields.d.ts.map +1 -0
  879. package/dist/lib/parseFields.js +49 -0
  880. package/dist/lib/parseFields.js.map +1 -0
  881. package/dist/lib/previewBanner.d.ts +9 -0
  882. package/dist/lib/previewBanner.d.ts.map +1 -0
  883. package/dist/lib/previewBanner.js +37 -0
  884. package/dist/lib/previewBanner.js.map +1 -0
  885. package/dist/lib/productbrain-files.d.ts +94 -0
  886. package/dist/lib/productbrain-files.d.ts.map +1 -0
  887. package/dist/lib/productbrain-files.js +214 -0
  888. package/dist/lib/productbrain-files.js.map +1 -0
  889. package/dist/lib/productbrain-files.test.d.ts +2 -0
  890. package/dist/lib/productbrain-files.test.d.ts.map +1 -0
  891. package/dist/lib/productbrain-files.test.js +258 -0
  892. package/dist/lib/productbrain-files.test.js.map +1 -0
  893. package/dist/lib/profiles.d.ts +41 -0
  894. package/dist/lib/profiles.d.ts.map +1 -0
  895. package/dist/lib/profiles.js +188 -0
  896. package/dist/lib/profiles.js.map +1 -0
  897. package/dist/lib/prompts.d.ts +65 -0
  898. package/dist/lib/prompts.d.ts.map +1 -0
  899. package/dist/lib/prompts.js +132 -0
  900. package/dist/lib/prompts.js.map +1 -0
  901. package/dist/lib/runner.d.ts +10 -0
  902. package/dist/lib/runner.d.ts.map +1 -1
  903. package/dist/lib/runner.js +44 -7
  904. package/dist/lib/runner.js.map +1 -1
  905. package/dist/lib/session.d.ts +85 -4
  906. package/dist/lib/session.d.ts.map +1 -1
  907. package/dist/lib/session.js +276 -13
  908. package/dist/lib/session.js.map +1 -1
  909. package/dist/lib/spinner.d.ts +27 -0
  910. package/dist/lib/spinner.d.ts.map +1 -0
  911. package/dist/lib/spinner.js +76 -0
  912. package/dist/lib/spinner.js.map +1 -0
  913. package/dist/lib/spinner.test.d.ts +2 -0
  914. package/dist/lib/spinner.test.d.ts.map +1 -0
  915. package/dist/lib/spinner.test.js +39 -0
  916. package/dist/lib/spinner.test.js.map +1 -0
  917. package/dist/lib/startup-resolution-signals.d.ts +34 -0
  918. package/dist/lib/startup-resolution-signals.d.ts.map +1 -0
  919. package/dist/lib/startup-resolution-signals.js +118 -0
  920. package/dist/lib/startup-resolution-signals.js.map +1 -0
  921. package/dist/lib/startup-resolution-signals.test.d.ts +2 -0
  922. package/dist/lib/startup-resolution-signals.test.d.ts.map +1 -0
  923. package/dist/lib/startup-resolution-signals.test.js +113 -0
  924. package/dist/lib/startup-resolution-signals.test.js.map +1 -0
  925. package/dist/lib/startup-signals.d.ts +12 -0
  926. package/dist/lib/startup-signals.d.ts.map +1 -0
  927. package/dist/lib/startup-signals.js +172 -0
  928. package/dist/lib/startup-signals.js.map +1 -0
  929. package/dist/lib/startup-signals.test.d.ts +2 -0
  930. package/dist/lib/startup-signals.test.d.ts.map +1 -0
  931. package/dist/lib/startup-signals.test.js +165 -0
  932. package/dist/lib/startup-signals.test.js.map +1 -0
  933. package/dist/lib/state.d.ts +51 -0
  934. package/dist/lib/state.d.ts.map +1 -0
  935. package/dist/lib/state.js +90 -0
  936. package/dist/lib/state.js.map +1 -0
  937. package/dist/lib/strip.d.ts.map +1 -1
  938. package/dist/lib/strip.js +7 -1
  939. package/dist/lib/strip.js.map +1 -1
  940. package/dist/lib/style.d.ts +96 -0
  941. package/dist/lib/style.d.ts.map +1 -0
  942. package/dist/lib/style.js +169 -0
  943. package/dist/lib/style.js.map +1 -0
  944. package/dist/lib/style.test.d.ts +7 -0
  945. package/dist/lib/style.test.d.ts.map +1 -0
  946. package/dist/lib/style.test.js +263 -0
  947. package/dist/lib/style.test.js.map +1 -0
  948. package/dist/lib/telemetry.d.ts +5 -5
  949. package/dist/lib/telemetry.d.ts.map +1 -1
  950. package/dist/lib/telemetry.js +34 -16
  951. package/dist/lib/telemetry.js.map +1 -1
  952. package/dist/lib/tokenConstants.d.ts +19 -0
  953. package/dist/lib/tokenConstants.d.ts.map +1 -0
  954. package/dist/lib/tokenConstants.js +19 -0
  955. package/dist/lib/tokenConstants.js.map +1 -0
  956. package/dist/lib/update-check.d.ts +56 -0
  957. package/dist/lib/update-check.d.ts.map +1 -0
  958. package/dist/lib/update-check.js +296 -0
  959. package/dist/lib/update-check.js.map +1 -0
  960. package/dist/lib/upgrade-runner.d.ts +22 -0
  961. package/dist/lib/upgrade-runner.d.ts.map +1 -0
  962. package/dist/lib/upgrade-runner.js +110 -0
  963. package/dist/lib/upgrade-runner.js.map +1 -0
  964. package/dist/lib/wizard-surfaces.d.ts +47 -0
  965. package/dist/lib/wizard-surfaces.d.ts.map +1 -0
  966. package/dist/lib/wizard-surfaces.js +176 -0
  967. package/dist/lib/wizard-surfaces.js.map +1 -0
  968. package/dist/lib/wizard-surfaces.test.d.ts +2 -0
  969. package/dist/lib/wizard-surfaces.test.d.ts.map +1 -0
  970. package/dist/lib/wizard-surfaces.test.js +127 -0
  971. package/dist/lib/wizard-surfaces.test.js.map +1 -0
  972. package/dist/lib/wizard-trust.d.ts +31 -0
  973. package/dist/lib/wizard-trust.d.ts.map +1 -0
  974. package/dist/lib/wizard-trust.js +66 -0
  975. package/dist/lib/wizard-trust.js.map +1 -0
  976. package/dist/lib/wizard-trust.test.d.ts +2 -0
  977. package/dist/lib/wizard-trust.test.d.ts.map +1 -0
  978. package/dist/lib/wizard-trust.test.js +32 -0
  979. package/dist/lib/wizard-trust.test.js.map +1 -0
  980. package/dist/lib/workspace-probe.d.ts +19 -0
  981. package/dist/lib/workspace-probe.d.ts.map +1 -0
  982. package/dist/lib/workspace-probe.js +27 -0
  983. package/dist/lib/workspace-probe.js.map +1 -0
  984. package/dist/lib/workspaceNotice.d.ts +59 -0
  985. package/dist/lib/workspaceNotice.d.ts.map +1 -0
  986. package/dist/lib/workspaceNotice.js +77 -0
  987. package/dist/lib/workspaceNotice.js.map +1 -0
  988. package/dist/lib/workspaceVocabCache.d.ts +60 -0
  989. package/dist/lib/workspaceVocabCache.d.ts.map +1 -0
  990. package/dist/lib/workspaceVocabCache.js +98 -0
  991. package/dist/lib/workspaceVocabCache.js.map +1 -0
  992. package/dist/scoreboard/diagnose.d.ts +266 -0
  993. package/dist/scoreboard/diagnose.d.ts.map +1 -0
  994. package/dist/scoreboard/diagnose.js +293 -0
  995. package/dist/scoreboard/diagnose.js.map +1 -0
  996. package/dist/scoreboard/diagnose.test.d.ts +14 -0
  997. package/dist/scoreboard/diagnose.test.d.ts.map +1 -0
  998. package/dist/scoreboard/diagnose.test.js +311 -0
  999. package/dist/scoreboard/diagnose.test.js.map +1 -0
  1000. package/dist/scoreboard/localDrift.d.ts +23 -0
  1001. package/dist/scoreboard/localDrift.d.ts.map +1 -0
  1002. package/dist/scoreboard/localDrift.js +111 -0
  1003. package/dist/scoreboard/localDrift.js.map +1 -0
  1004. package/dist/scoreboard/localDrift.test.d.ts +9 -0
  1005. package/dist/scoreboard/localDrift.test.d.ts.map +1 -0
  1006. package/dist/scoreboard/localDrift.test.js +82 -0
  1007. package/dist/scoreboard/localDrift.test.js.map +1 -0
  1008. package/dist/setup/__tests__/coach-traces.test.d.ts +2 -0
  1009. package/dist/setup/__tests__/coach-traces.test.d.ts.map +1 -0
  1010. package/dist/setup/__tests__/coach-traces.test.js +189 -0
  1011. package/dist/setup/__tests__/coach-traces.test.js.map +1 -0
  1012. package/dist/setup/__tests__/setup-commands.test.d.ts +2 -0
  1013. package/dist/setup/__tests__/setup-commands.test.d.ts.map +1 -0
  1014. package/dist/setup/__tests__/setup-commands.test.js +177 -0
  1015. package/dist/setup/__tests__/setup-commands.test.js.map +1 -0
  1016. package/dist/setup/__tests__/state-machine.test.d.ts +2 -0
  1017. package/dist/setup/__tests__/state-machine.test.d.ts.map +1 -0
  1018. package/dist/setup/__tests__/state-machine.test.js +341 -0
  1019. package/dist/setup/__tests__/state-machine.test.js.map +1 -0
  1020. package/dist/setup/detect-surfaces.d.ts +21 -0
  1021. package/dist/setup/detect-surfaces.d.ts.map +1 -0
  1022. package/dist/setup/detect-surfaces.js +39 -0
  1023. package/dist/setup/detect-surfaces.js.map +1 -0
  1024. package/dist/setup/manifest-writer.d.ts +17 -0
  1025. package/dist/setup/manifest-writer.d.ts.map +1 -0
  1026. package/dist/setup/manifest-writer.js +153 -0
  1027. package/dist/setup/manifest-writer.js.map +1 -0
  1028. package/dist/setup/perimeter.d.ts +72 -0
  1029. package/dist/setup/perimeter.d.ts.map +1 -0
  1030. package/dist/setup/perimeter.js +128 -0
  1031. package/dist/setup/perimeter.js.map +1 -0
  1032. package/dist/setup/state-machine.d.ts +67 -0
  1033. package/dist/setup/state-machine.d.ts.map +1 -0
  1034. package/dist/setup/state-machine.js +124 -0
  1035. package/dist/setup/state-machine.js.map +1 -0
  1036. package/dist/surfaces/__tests__/adapter.test.d.ts +2 -0
  1037. package/dist/surfaces/__tests__/adapter.test.d.ts.map +1 -0
  1038. package/dist/surfaces/__tests__/adapter.test.js +90 -0
  1039. package/dist/surfaces/__tests__/adapter.test.js.map +1 -0
  1040. package/dist/surfaces/__tests__/pb-setup-passthrough.test.d.ts +2 -0
  1041. package/dist/surfaces/__tests__/pb-setup-passthrough.test.d.ts.map +1 -0
  1042. package/dist/surfaces/__tests__/pb-setup-passthrough.test.js +132 -0
  1043. package/dist/surfaces/__tests__/pb-setup-passthrough.test.js.map +1 -0
  1044. package/dist/surfaces/__tests__/telemetry.test.d.ts +2 -0
  1045. package/dist/surfaces/__tests__/telemetry.test.d.ts.map +1 -0
  1046. package/dist/surfaces/__tests__/telemetry.test.js +55 -0
  1047. package/dist/surfaces/__tests__/telemetry.test.js.map +1 -0
  1048. package/dist/surfaces/adapter.d.ts +70 -0
  1049. package/dist/surfaces/adapter.d.ts.map +1 -0
  1050. package/dist/surfaces/adapter.js +2 -0
  1051. package/dist/surfaces/adapter.js.map +1 -0
  1052. package/dist/surfaces/adapters/claude.d.ts +3 -0
  1053. package/dist/surfaces/adapters/claude.d.ts.map +1 -0
  1054. package/dist/surfaces/adapters/claude.js +67 -0
  1055. package/dist/surfaces/adapters/claude.js.map +1 -0
  1056. package/dist/surfaces/adapters/codex.d.ts +3 -0
  1057. package/dist/surfaces/adapters/codex.d.ts.map +1 -0
  1058. package/dist/surfaces/adapters/codex.js +61 -0
  1059. package/dist/surfaces/adapters/codex.js.map +1 -0
  1060. package/dist/surfaces/adapters/copilot.d.ts +3 -0
  1061. package/dist/surfaces/adapters/copilot.d.ts.map +1 -0
  1062. package/dist/surfaces/adapters/copilot.js +59 -0
  1063. package/dist/surfaces/adapters/copilot.js.map +1 -0
  1064. package/dist/surfaces/adapters/cursor.d.ts +3 -0
  1065. package/dist/surfaces/adapters/cursor.d.ts.map +1 -0
  1066. package/dist/surfaces/adapters/cursor.js +78 -0
  1067. package/dist/surfaces/adapters/cursor.js.map +1 -0
  1068. package/dist/surfaces/registry.d.ts +76 -0
  1069. package/dist/surfaces/registry.d.ts.map +1 -0
  1070. package/dist/surfaces/registry.js +117 -0
  1071. package/dist/surfaces/registry.js.map +1 -0
  1072. package/dist/surfaces/telemetry.d.ts +17 -0
  1073. package/dist/surfaces/telemetry.d.ts.map +1 -0
  1074. package/dist/surfaces/telemetry.js +31 -0
  1075. package/dist/surfaces/telemetry.js.map +1 -0
  1076. package/package.json +11 -3
  1077. package/templates/method-registry.json +16 -0
  1078. package/dist/__tests__/setup.test.d.ts +0 -2
  1079. package/dist/__tests__/setup.test.d.ts.map +0 -1
  1080. package/dist/__tests__/setup.test.js +0 -170
  1081. package/dist/__tests__/setup.test.js.map +0 -1
  1082. package/dist/generators/__tests__/surface-profiles.test.d.ts +0 -2
  1083. package/dist/generators/__tests__/surface-profiles.test.d.ts.map +0 -1
  1084. package/dist/generators/__tests__/surface-profiles.test.js +0 -89
  1085. package/dist/generators/__tests__/surface-profiles.test.js.map +0 -1
  1086. package/dist/generators/chain-classifier.test.d.ts +0 -2
  1087. package/dist/generators/chain-classifier.test.d.ts.map +0 -1
  1088. package/dist/generators/chain-classifier.test.js +0 -257
  1089. package/dist/generators/chain-classifier.test.js.map +0 -1
@@ -1,23 +1,254 @@
1
1
  /**
2
2
  * pb handshake — generate context files for AI developer tools.
3
- * The fourth delivery surface: context export (GLO-63, DEC-161).
3
+ * Context export wiring (read-only filesystem bridge; GLO-63, DEC-161) — not a product surface.
4
4
  */
5
- import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync } from 'fs';
6
- import { join, dirname, resolve } from 'path';
5
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync, appendFileSync, unlinkSync, statSync, renameSync, rmSync, realpathSync } from 'fs';
6
+ import { join, dirname, resolve, basename, relative, sep, isAbsolute } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { fileURLToPath } from 'url';
9
- import { createInterface } from 'readline';
9
+ import { createHash } from 'crypto';
10
+ import { execSync } from 'child_process';
10
11
  import { getConfigOrGuide } from '../lib/config.js';
11
- import { mcpCall } from '../lib/client.js';
12
+ import { select as promptSelect, confirm as promptConfirm } from '../lib/prompts.js';
13
+ import { composeHooksFromIntents, getHookStatusForSurface } from '../lib/hook-intents.js';
14
+ import { kernelCall, kernelCallWithSession } from '../lib/client.js';
15
+ import { readSession } from '../lib/session.js';
12
16
  import { detectRepo, extractWorkspaceProfile } from '../lib/repo-detect.js';
13
17
  import { generateContextMd } from '../generators/context-md.js';
14
18
  import { generateBriefingMd } from '../generators/briefing-md.js';
15
- import { MARKER, generateAgentsMd, generateClaudeMd, generateCursorMdc, generateCopilotMd } from '../generators/adapters.js';
16
- import { readCanonicalSkills, readCanonicalRules, generateCursorSkill, generateCursorRule, generateCodexSkill, generateCodexSkillIndex, generateClaudeRule, generateClaudeSkillRouter, shouldEmitToTarget, filterByLevel, validateCodexSkills, evaluateConditions, STAGE_TO_MAX_LEVEL, LEVEL_ORDER, } from '../generators/portable-knowledge.js';
19
+ import { MARKER, generateAgentsMd, generateClaudeMd, generateCursorMdc, generateCopilotMd, generateCopilotInstructionFiles } from '../generators/adapters.js';
20
+ import { readCanonicalSkills, readCanonicalRules, readPersonalLayer, readPersonalSkillsLayer, generateCursorSkill, generateCursorRule, generateCodexSkill, generateCodexSkillIndex, generateClaudeRule, generateClaudeSkillRouter, shouldEmitToTarget, filterByLevel, validateCodexSkills, evaluateConditions, STAGE_TO_MAX_LEVEL, LEVEL_ORDER, } from '../generators/portable-knowledge.js';
17
21
  import { generateChainRules } from '../generators/chain-rules.js';
18
22
  import { saveHandshakeState, loadPreviousState, diffHandshakeState, formatDiff, buildCurrentState, } from '../generators/handshake-diff.js';
19
23
  import { resolveSurfaceProfile } from '../generators/surface-profiles.js';
20
24
  import { formatHandshakeReport } from '../formatters/handshake.js';
25
+ import { classifyAdapterFile, detectEol, spliceAppend, spliceReplace } from '../generators/region.js';
26
+ import { REGION_PROJECTIONS } from '../generators/region-projections.js';
27
+ import { readManifest, readManifestStatus, filterByAdoptionState } from '../generators/manifest.js';
28
+ import { generateBoundaryManifest, getBoundaryEnforcementMode } from '../generators/boundary-manifest.js';
29
+ import { loadMethodRegistry } from '../lib/method-registry.js';
30
+ import { CLIError, ErrorCode } from '../lib/errors.js';
31
+ import { trackEvent } from '../lib/telemetry.js';
32
+ import { replaceVocabTokens } from '../lib/canonicalRefs.js';
33
+ // WP-436 S3: vocab projector — resolves {{vocab:...}} tokens before writing to disk.
34
+ import { getOrFetchVocabCtx } from '../lib/workspaceVocabCache.js';
35
+ import { normalizeMaterializedFilename } from '../lib/normalizeMaterializedFilename.js';
36
+ import { parseSetupFrontmatter } from '../lib/frontmatter.js';
37
+ import { sanitizeStartupResolutionSignals } from '../lib/startup-resolution-signals.js';
38
+ import { assertSetupWritePath } from '../setup/perimeter.js';
39
+ // WP-421 S3: SurfaceAdapter reverse-map for tampered prompts (DEC-952, doneWhen #34).
40
+ import { canonicalPathForAnySurface, SURFACE_GOVERN_NO_SURFACES, SURFACE_REGISTRY, validateSurfacesForMode, } from '../surfaces/registry.js';
41
+ import { getReverseMapFallbackMessage, reportReverseMapMissing } from '../surfaces/telemetry.js';
42
+ const MAX_HANDSHAKE_WAIT_MS = 10_000; // 10 seconds
43
+ const POLL_INTERVAL_MS = 500; // 500 ms per poll
44
+ const MAX_POLLS = MAX_HANDSHAKE_WAIT_MS / POLL_INTERVAL_MS; // 20
45
+ // ── WP-379 S4: Dormant marker ─────────────────────────────────────────────────
46
+ /**
47
+ * DORMANT_MARKER — appended to previously-projected asset files when the asset's
48
+ * gate deactivates (e.g. workspace readiness exceeds the max threshold).
49
+ *
50
+ * Contract:
51
+ * - The file is NOT deleted. It persists on disk so that history is preserved
52
+ * and the agent surface remains inspectable.
53
+ * - The marker is appended at the end of the file, idempotent — if it already
54
+ * exists, no second append occurs.
55
+ * - The marker does NOT trigger a drift TEN. Dormant files are intentionally
56
+ * deactivated, not accidentally forked.
57
+ * - The marker is never included in active file writes — only dormant writes.
58
+ *
59
+ * Used by: writeDormantMarker() (write) and hasDormantMarker() (idempotency check).
60
+ * Exported for use in tests.
61
+ *
62
+ * Chain: WP-379 S4.
63
+ */
64
+ export const DORMANT_MARKER = '<!-- pb-status: dormant -->';
65
+ /**
66
+ * hasDormantMarker — check whether a file on disk already has the dormant marker.
67
+ * Used for idempotency: if the marker is already present, skip the append.
68
+ */
69
+ function hasDormantMarker(content) {
70
+ return content.includes(DORMANT_MARKER);
71
+ }
72
+ /**
73
+ * writeDormantMarker — append the dormant marker to a previously-projected file.
74
+ *
75
+ * Idempotent: if DORMANT_MARKER is already present, no-op.
76
+ * Only operates on files that have the auto-gen MARKER — we never touch
77
+ * manually-authored files.
78
+ *
79
+ * @param filePath Absolute path to the file.
80
+ * @returns 'written' | 'already-dormant' | 'skipped' (no auto-gen marker)
81
+ */
82
+ export function writeDormantMarkerToFile(filePath) {
83
+ if (!existsSync(filePath))
84
+ return 'skipped';
85
+ const content = readFileSync(filePath, 'utf8');
86
+ // Only mark files that were originally projected by pb handshake.
87
+ // Files without the auto-gen MARKER are manually authored — leave them alone.
88
+ if (!content.includes(MARKER))
89
+ return 'skipped';
90
+ if (hasDormantMarker(content))
91
+ return 'already-dormant';
92
+ // Append the marker on its own line. No trailing newline assumption —
93
+ // appendFileSync adds to whatever is already there.
94
+ appendFileSync(filePath, `\n${DORMANT_MARKER}\n`);
95
+ return 'written';
96
+ }
97
+ /**
98
+ * renameSurfaceForDormancy — rename a projected surface file to `<path>.dormant`
99
+ * so external scanners (which glob *.md / *.mdc) stop loading it. WP-426 E4.
100
+ * The HTML-comment marker is invisible to scanners; the extension change removes
101
+ * the file from their glob set. Only touches files carrying the auto-gen MARKER.
102
+ * Runs on an INDEPENDENT existence check (not gated on marker-write result) so
103
+ * legacy WP-379 marker-only files still rename. Idempotent.
104
+ * Codex P1: when a .dormant already exists, only replace it if its body matches the
105
+ * live file. A divergent .dormant (user edited it) is preserved and reported as 'drift'
106
+ * rather than force-deleted — no silent data loss.
107
+ * @returns 'renamed' | 'replaced' | 'already-dormant' | 'skipped' | 'drift'
108
+ */
109
+ export function renameSurfaceForDormancy(filePath) {
110
+ const dormantPath = `${filePath}.dormant`;
111
+ if (!existsSync(filePath))
112
+ return existsSync(dormantPath) ? 'already-dormant' : 'skipped';
113
+ if (!readFileSync(filePath, 'utf8').includes(MARKER))
114
+ return 'skipped';
115
+ const replacing = existsSync(dormantPath);
116
+ if (replacing) {
117
+ // Codex P1: the existing .dormant may carry user edits (e.g. the user reactivated to
118
+ // the live path but kept an edited dormant sibling). Compare normalized bodies
119
+ // (DORMANT_MARKER + volatile auto-gen timestamp stripped from both); if they diverge,
120
+ // preserve the .dormant and signal drift instead of force-replacing it.
121
+ const strip = (s) => normalizeHandshakeContentForComparison(s.split(DORMANT_MARKER).join('')).trimEnd();
122
+ if (strip(readFileSync(filePath, 'utf8')) !== strip(readFileSync(dormantPath, 'utf8'))) {
123
+ return 'drift';
124
+ }
125
+ rmSync(dormantPath, { force: true });
126
+ }
127
+ renameSync(filePath, dormantPath);
128
+ return replacing ? 'replaced' : 'renamed';
129
+ }
130
+ /**
131
+ * restoreSurfaceFromDormant — clean the orphan `<path>.dormant` sibling after a
132
+ * raised (observe→project) asset has been re-projected fresh from the DB body by
133
+ * the normal write loop. WP-426 E4.
134
+ * Normalizes the volatile auto-gen timestamp (via normalizeHandshakeContentForComparison)
135
+ * before comparing, else the lowering-time timestamp would always force a false
136
+ * 'orphan-drift'. Identical body → remove. Differs (user edited) → preserve + drift.
137
+ * @returns 'restored' | 'orphan-drift' | 'skipped'
138
+ */
139
+ export function restoreSurfaceFromDormant(filePath) {
140
+ const dormantPath = `${filePath}.dormant`;
141
+ if (!existsSync(dormantPath))
142
+ return 'skipped';
143
+ // WP-426 E4: no fresh live projection this run (surface filtered / user-owned / not written) →
144
+ // nothing to reconcile; absence of fresh is NOT evidence of a user edit. Leave the .dormant.
145
+ if (!existsSync(filePath))
146
+ return 'skipped';
147
+ const fresh = readFileSync(filePath, 'utf8');
148
+ // Codex P2: only reconcile against a PB-managed projection. A live file WITHOUT the auto-gen
149
+ // MARKER is a user-owned file (shouldWriteAdapter would have skipped reprojecting it),
150
+ // so it was NOT raised this run — comparing the .dormant against it would wrongly delete it or
151
+ // re-fire "edited while dormant" drift. Leave the .dormant untouched.
152
+ if (!fresh.includes(MARKER))
153
+ return 'skipped';
154
+ const dormant = readFileSync(dormantPath, 'utf8').split(DORMANT_MARKER).join('');
155
+ const norm = (s) => normalizeHandshakeContentForComparison(s).trimEnd();
156
+ if (norm(dormant) === norm(fresh)) {
157
+ rmSync(dormantPath, { force: true });
158
+ return 'restored';
159
+ }
160
+ return 'orphan-drift';
161
+ }
162
+ /**
163
+ * deriveDormantFilePaths — compute the set of on-disk file paths that would have
164
+ * been projected for a given dormant asset (by name and assetKind).
165
+ *
166
+ * Assets are projected to one or more surfaces (cursor/claude/codex) depending
167
+ * on shouldEmitToTarget. Since we don't re-run shouldEmitToTarget here, we
168
+ * speculatively probe all known surface paths and let writeDormantMarkerToFile
169
+ * decide whether each exists and has the auto-gen MARKER.
170
+ *
171
+ * @param asset The dormant asset from the server.
172
+ * @param cwd Current working directory (project root).
173
+ * @returns Each candidate path paired with its owning surface, so callers can
174
+ * filter by the run's allowedTargets (--surfaces) and the perimeter.
175
+ */
176
+ function deriveDormantFilePaths(asset, cwd) {
177
+ // Defense-in-depth: even though `name` originates from platform-seeded DB
178
+ // entries (not user input), validate it against a strict charset before
179
+ // interpolating into a filesystem path. Reject anything that could traverse
180
+ // out of the expected directories. WP-379 S4 review finding.
181
+ if (!/^[A-Za-z0-9 ._-]+$/.test(asset.name)) {
182
+ return [];
183
+ }
184
+ const out = [];
185
+ const { name, assetKind } = asset;
186
+ if (assetKind === 'skill') {
187
+ out.push({ path: join(cwd, '.cursor', 'skills', name, 'SKILL.md'), surface: 'cursor' });
188
+ out.push({ path: join(cwd, '.codex', 'skills', `${name}.md`), surface: 'codex' });
189
+ }
190
+ else if (assetKind === 'rule' || assetKind === 'hook') {
191
+ out.push({ path: join(cwd, '.cursor', 'rules', `${name}.mdc`), surface: 'cursor' });
192
+ out.push({ path: join(cwd, '.claude', 'rules', `${name}.md`), surface: 'claude' });
193
+ }
194
+ return out;
195
+ }
196
+ /**
197
+ * Single-shot health probe — calls `workspace.health` and inspects
198
+ * `starterSetupSeeded`. Does NOT poll internally; polling is the caller's
199
+ * responsibility (connect-context.ts).
200
+ *
201
+ * Returns:
202
+ * - `seeds-ready` — health query succeeded AND starterSetupSeeded is true
203
+ * - `seeds-pending` — health query succeeded but starterSetupSeeded is false
204
+ * - `probe-failed` — health query threw (network, auth, etc.)
205
+ */
206
+ export async function probeStarterSetupSeeded() {
207
+ try {
208
+ const health = await kernelCall('workspace.health', {});
209
+ if (health.starterSetupSeeded) {
210
+ return { status: 'seeds-ready' };
211
+ }
212
+ const starterGaps = (health.gaps ?? []).filter((g) => g.kind === 'starter-setup-missing' || g.kind === 'platform-domains-missing');
213
+ return {
214
+ status: 'seeds-pending',
215
+ gaps: starterGaps.length > 0 ? starterGaps : [
216
+ {
217
+ kind: 'starter-setup-missing',
218
+ severity: 'warn',
219
+ message: 'Starter setup seeds are still running.',
220
+ },
221
+ ],
222
+ };
223
+ }
224
+ catch (err) {
225
+ return {
226
+ status: 'probe-failed',
227
+ error: err instanceof Error ? err.message : String(err),
228
+ };
229
+ }
230
+ }
231
+ /**
232
+ * Poll `probeStarterSetupSeeded` up to MAX_POLLS times (10s at 500ms intervals).
233
+ * Returns the final probe result — caller decides how to render the outcome.
234
+ *
235
+ * Exported so connect-context.ts can use it without re-implementing the loop.
236
+ */
237
+ export async function pollUntilSeedsReady() {
238
+ for (let poll = 0; poll < MAX_POLLS; poll++) {
239
+ const result = await probeStarterSetupSeeded();
240
+ if (result.status === 'seeds-ready')
241
+ return result;
242
+ if (result.status === 'probe-failed')
243
+ return result; // don't retry on auth/network errors
244
+ // seeds-pending — wait before next poll
245
+ if (poll < MAX_POLLS - 1) {
246
+ await new Promise((res) => setTimeout(res, POLL_INTERVAL_MS));
247
+ }
248
+ }
249
+ // Final probe after exhausting waits — return whatever state we have
250
+ return probeStarterSetupSeeded();
251
+ }
21
252
  const LEVELS = {
22
253
  guide: {
23
254
  label: 'Guide me',
@@ -49,9 +280,6 @@ const LEVEL_KEYS = ['guide', 'work', 'silent', 'full-trust'];
49
280
  // Hook failure contract (TEN-712): all hook commands MUST end with '2>/dev/null || true'
50
281
  // so Claude Code always starts even if pb is unavailable. Never remove this suffix.
51
282
  const INIT_PERMISSION = 'Bash(pb:*)';
52
- const INIT_SESSION_START_CMD = 'pb session start 2>/dev/null || true';
53
- const INIT_SESSION_CLOSE_CMD = 'pb session close 2>/dev/null || true';
54
- const INIT_PRECOMPACT_CMD = 'pb session id > /dev/null 2>&1 && echo \'{"systemMessage": "pb session active — capture decisions/tensions before compacting: pb capture \\"DEC: ...\\""}\' || true';
55
283
  function readSettings(filePath) {
56
284
  if (!existsSync(filePath))
57
285
  return {};
@@ -63,9 +291,6 @@ function readSettings(filePath) {
63
291
  return {};
64
292
  }
65
293
  }
66
- function hasCommand(groups, cmd) {
67
- return (groups ?? []).some((g) => g.hooks?.some((h) => h.command === cmd));
68
- }
69
294
  // Team write: hooks + Bash(pb:*) → .claude/settings.json (safe to commit)
70
295
  function writeTeamSettings(cwd, dryRun) {
71
296
  const claudeDir = join(cwd, '.claude');
@@ -81,26 +306,10 @@ function writeTeamSettings(cwd, dryRun) {
81
306
  }
82
307
  settings.permissions = permissions;
83
308
  const hooks = (settings.hooks ?? {});
84
- if (!hasCommand(hooks.SessionStart, INIT_SESSION_START_CMD)) {
85
- hooks.SessionStart = [
86
- ...(hooks.SessionStart ?? []),
87
- { hooks: [{ type: 'command', command: INIT_SESSION_START_CMD, statusMessage: 'Opening pb session...' }] },
88
- ];
89
- added.push('SessionStart → pb session start');
90
- }
91
- if (!hasCommand(hooks.Stop, INIT_SESSION_CLOSE_CMD)) {
92
- hooks.Stop = [
93
- ...(hooks.Stop ?? []),
94
- { hooks: [{ type: 'command', command: INIT_SESSION_CLOSE_CMD, statusMessage: 'Closing pb session...' }] },
95
- ];
96
- added.push('Stop → pb session close');
97
- }
98
- if (!hasCommand(hooks.PreCompact, INIT_PRECOMPACT_CMD)) {
99
- hooks.PreCompact = [
100
- ...(hooks.PreCompact ?? []),
101
- { hooks: [{ type: 'command', command: INIT_PRECOMPACT_CMD }] },
102
- ];
103
- added.push('PreCompact → decision capture reminder');
309
+ const hookAdditions = composeHooksFromIntents(['session-start', 'session-close', 'pre-compact'], hooks);
310
+ for (const addition of hookAdditions) {
311
+ hooks[addition.event] = [...(hooks[addition.event] ?? []), addition.entry];
312
+ added.push(addition.label);
104
313
  }
105
314
  settings.hooks = hooks;
106
315
  if (!dryRun) {
@@ -137,31 +346,26 @@ function writePersonalSettings(levelKey, dryRun) {
137
346
  return added;
138
347
  }
139
348
  async function promptLevel() {
140
- const rl = createInterface({ input: process.stdin, output: process.stdout });
141
- console.log('\nHow much should Claude explain before acting?\n');
142
- LEVEL_KEYS.forEach((key, i) => {
143
- const l = LEVELS[key];
144
- console.log(` ${i + 1}. ${l.label.padEnd(12)} — ${l.description}`);
145
- });
146
- console.log('');
147
- return new Promise((resolve) => {
148
- rl.question('> ', (answer) => {
149
- rl.close();
150
- const n = parseInt(answer.trim(), 10);
151
- if (!(n >= 1 && n <= LEVEL_KEYS.length)) {
152
- console.log(' (Invalid choice — defaulting to "Just work")');
153
- }
154
- resolve(n >= 1 && n <= LEVEL_KEYS.length ? LEVEL_KEYS[n - 1] : 'work');
155
- });
349
+ const result = await promptSelect({
350
+ message: 'How much should Claude explain before acting?',
351
+ options: LEVEL_KEYS.map((key) => ({
352
+ value: key,
353
+ label: LEVELS[key].label,
354
+ hint: LEVELS[key].description,
355
+ })),
156
356
  });
357
+ return result;
157
358
  }
158
359
  export async function runHandshakeInit(options = {}) {
159
360
  const cwd = process.cwd();
160
361
  const dryRun = options.dryRun ?? false;
161
362
  const suffix = dryRun ? ' (dry run)' : '';
162
363
  if (options.level && !LEVEL_KEYS.includes(options.level)) {
163
- console.error(`Unknown level "${options.level}". Valid: ${LEVEL_KEYS.join(', ')}`);
164
- process.exit(1);
364
+ throw new CLIError(`Unknown level "${options.level}".`, {
365
+ code: ErrorCode.VALIDATION_FAILED,
366
+ category: 'validation',
367
+ guidance: `Valid levels: ${LEVEL_KEYS.join(', ')}`,
368
+ });
165
369
  }
166
370
  console.log('Setting up Claude Code integration...\n');
167
371
  // Step 1: Team config — always, no prompt
@@ -176,7 +380,7 @@ export async function runHandshakeInit(options = {}) {
176
380
  console.log(` + ${item}`);
177
381
  }
178
382
  // Step 2: Personal config — wizard or --level flag
179
- // Cast is safe: LEVEL_KEYS.includes() validated above; invalid level already called process.exit(1).
383
+ // Cast is safe: LEVEL_KEYS.includes() validated above; invalid level already threw CLIError.
180
384
  const levelKey = options.level ? options.level : await promptLevel();
181
385
  const level = LEVELS[levelKey];
182
386
  const personalAdded = writePersonalSettings(levelKey, dryRun);
@@ -198,6 +402,31 @@ export async function runHandshakeInit(options = {}) {
198
402
  if (!dryRun)
199
403
  console.log('Reload /hooks in Claude Code (or restart) to activate.');
200
404
  console.log('Run `pb handshake --init --level <guide|work|silent|full-trust>` to change level.');
405
+ // Step 2b: Report multi-surface hook opt-in status (WP-310 E3b)
406
+ // Reads manifest.hooks.{cursor,copilot} and prints an informational note for
407
+ // each opted-in surface. Silence = no manifest or no hooks flags set.
408
+ // DEC-536: Claude-native default is already wired above; this block only fires
409
+ // when the user has explicitly opted in via manifest.
410
+ const pbDirForManifest = join(cwd, '.productbrain');
411
+ const initManifest = readManifest(pbDirForManifest);
412
+ const multiSurfaceOptIns = [];
413
+ if (initManifest?.hooks?.cursor)
414
+ multiSurfaceOptIns.push('cursor');
415
+ if (initManifest?.hooks?.copilot)
416
+ multiSurfaceOptIns.push('copilot');
417
+ if (multiSurfaceOptIns.length > 0) {
418
+ console.log('');
419
+ for (const surface of multiSurfaceOptIns) {
420
+ const status = getHookStatusForSurface(surface);
421
+ if (status.writable) {
422
+ // Future-proofing path: surface has hook events, would write files.
423
+ console.log(`ℹ ${surface} hooks: opted in — hooks will be written`);
424
+ }
425
+ else {
426
+ console.log(`ℹ ${surface} hooks: opted in — no auto-hooks (${surface} has no session events; run \`pb session start\` manually)`);
427
+ }
428
+ }
429
+ }
201
430
  // Step 3: Scaffold starter templates if .productbrain/rules/ is empty
202
431
  const rulesDir = join(cwd, '.productbrain', 'rules');
203
432
  const hasExistingRules = existsSync(rulesDir) && readdirSync(rulesDir).filter((f) => f.endsWith('.md')).length > 0;
@@ -246,6 +475,237 @@ export async function runHandshakeInit(options = {}) {
246
475
  }
247
476
  }
248
477
  }
478
+ const LOCAL_REF_REGEX = /\b(?:WP|TEN|DEC|BET|FEAT|INS|STD|PRI|ARCH)[-\s#]*\d+\b|\bPR\s*#?\s*\d+\b/gi;
479
+ const STARTUP_RESOLUTION_RESOLVER_VERSION = 'startup-resolution-v1';
480
+ // Global cap on collected refs (spec "Contract: StartupResolutionSignals" — arrays are capped).
481
+ const LOCAL_REF_GLOBAL_CAP = 25;
482
+ // Per-source cap so a broad source (context.md) cannot starve the task-specific
483
+ // briefing.md refs (Codex@677). With the global cap at 25, a single source capped
484
+ // at 18 always leaves slots for at least one other source's distinct refs.
485
+ const LOCAL_REF_PER_SOURCE_CAP = 18;
486
+ // PR-ref diff dereference is time-boxed so an unauthed/slow `gh` cannot stall
487
+ // session-start (F4 — guard: time-box + skip gracefully on failure).
488
+ const PR_DIFF_TIMEOUT_MS = 4_000;
489
+ // Cap on how many PR refs we dereference per handshake — bounds the worst-case
490
+ // number of `gh` subprocess invocations.
491
+ const PR_DIFF_MAX_REFS = 5;
492
+ function runGitText(cwd, command) {
493
+ try {
494
+ const value = execSync(command, {
495
+ cwd,
496
+ encoding: 'utf8',
497
+ stdio: ['ignore', 'pipe', 'ignore'],
498
+ }).trim();
499
+ return value.length > 0 ? value : undefined;
500
+ }
501
+ catch {
502
+ return undefined;
503
+ }
504
+ }
505
+ /**
506
+ * Dereference a "PR #N" ref to its changed paths via `gh pr diff <N> --name-only`.
507
+ * F4 (spec "PR refs must dereference to diff paths when available"): the server
508
+ * resolver has no git/gh access, so PR-domain evidence can only be produced
509
+ * here in the CLI. Guarded: `gh` may be absent/unauthed or the PR may not exist,
510
+ * so failures are swallowed (returns []) and the call is time-boxed.
511
+ *
512
+ * Injectable via `run` so tests can drive the dereference without a real `gh`.
513
+ */
514
+ export function dereferencePrRefToPaths(cwd, prNumber, run = runGitTextTimeboxed) {
515
+ if (!Number.isInteger(prNumber) || prNumber <= 0)
516
+ return [];
517
+ const raw = run(cwd, `gh pr diff ${prNumber} --name-only`, PR_DIFF_TIMEOUT_MS);
518
+ if (!raw)
519
+ return [];
520
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
521
+ }
522
+ /** Time-boxed variant of runGitText for the PR-diff dereference (F4). */
523
+ function runGitTextTimeboxed(cwd, command, timeoutMs) {
524
+ try {
525
+ const value = execSync(command, {
526
+ cwd,
527
+ encoding: 'utf8',
528
+ stdio: ['ignore', 'pipe', 'ignore'],
529
+ timeout: timeoutMs,
530
+ }).trim();
531
+ return value.length > 0 ? value : undefined;
532
+ }
533
+ catch {
534
+ return undefined;
535
+ }
536
+ }
537
+ function readOptionalLocalText(cwd, path) {
538
+ const fullPath = join(cwd, path);
539
+ if (!existsSync(fullPath))
540
+ return undefined;
541
+ try {
542
+ return readFileSync(fullPath, 'utf8');
543
+ }
544
+ catch {
545
+ return undefined;
546
+ }
547
+ }
548
+ function normalizeLocalRef(ref) {
549
+ const trimmed = ref.trim().replace(/\s+/g, ' ');
550
+ const pr = trimmed.match(/^PR\s*#?\s*(\d+)$/i);
551
+ if (pr)
552
+ return `PR #${pr[1]}`;
553
+ const entry = trimmed.match(/^([A-Z]+)[-\s#]*(\d+)$/i);
554
+ if (entry)
555
+ return `${entry[1].toUpperCase()}-${entry[2]}`;
556
+ return trimmed;
557
+ }
558
+ /**
559
+ * Collect normalized local refs across several text sources, deduped.
560
+ *
561
+ * Codex@677: a per-source cap (LOCAL_REF_PER_SOURCE_CAP) is applied in addition
562
+ * to the global cap so a broad source (e.g. a context.md carrying 25+ IDs) cannot
563
+ * consume the whole budget before a later, task-specific source (briefing.md) is
564
+ * even read. Each source contributes at most its quota; refs already collected by
565
+ * an earlier source still dedupe across sources (and do not count against the
566
+ * later source's quota, since they are skipped before the per-source counter).
567
+ */
568
+ function collectLocalRefs(texts) {
569
+ const refs = [];
570
+ const seen = new Set();
571
+ for (const text of texts) {
572
+ if (!text)
573
+ continue;
574
+ let fromThisSource = 0;
575
+ for (const match of text.matchAll(LOCAL_REF_REGEX)) {
576
+ const ref = normalizeLocalRef(match[0]);
577
+ const key = ref.toLowerCase();
578
+ if (seen.has(key))
579
+ continue;
580
+ if (fromThisSource >= LOCAL_REF_PER_SOURCE_CAP)
581
+ break;
582
+ seen.add(key);
583
+ refs.push(ref);
584
+ fromThisSource++;
585
+ if (refs.length >= LOCAL_REF_GLOBAL_CAP)
586
+ return refs;
587
+ }
588
+ }
589
+ return refs;
590
+ }
591
+ function deriveHandshakeStartupTask(refs, changedPaths, callerTask) {
592
+ if (callerTask && callerTask.trim())
593
+ return callerTask.trim();
594
+ const parts = [];
595
+ if (refs.length > 0)
596
+ parts.push(refs.join(' '));
597
+ if (changedPaths.length > 0)
598
+ parts.push(`changed paths ${changedPaths.slice(0, 5).join(' ')}`);
599
+ return parts.length > 0 ? parts.join(' ') : undefined;
600
+ }
601
+ const DEFAULT_HANDSHAKE_SIGNAL_DEPS = {
602
+ runGit: runGitText,
603
+ prDiff: (cwd, prNumber) => dereferencePrRefToPaths(cwd, prNumber),
604
+ };
605
+ /** Extract the PR number from a normalized "PR #N" ref, else null. */
606
+ function prNumberFromRef(ref) {
607
+ const m = ref.match(/^PR\s*#?\s*(\d+)$/i);
608
+ if (!m)
609
+ return null;
610
+ const n = Number(m[1]);
611
+ return Number.isInteger(n) && n > 0 ? n : null;
612
+ }
613
+ export function deriveHandshakeStartupSignals(cwd, callerSignals = {}, deps = DEFAULT_HANDSHAKE_SIGNAL_DEPS) {
614
+ const { runGit, prDiff } = deps;
615
+ const branchName = runGit(cwd, 'git branch --show-current');
616
+ const commitSha = runGit(cwd, 'git rev-parse --short HEAD');
617
+ // Changed paths come from THREE sources, unioned + deduped:
618
+ // 1. Dirty worktree (`git diff --name-only HEAD`) — existing behavior.
619
+ // 2. Committed branch delta vs the merge-base with origin/main (Codex@665):
620
+ // after the branch is committed and the worktree is clean, source #1 is
621
+ // empty and the branch delta would be invisible without this.
622
+ // 3. PR-ref dereference (F4) — appended later, once refs are collected.
623
+ const dirtyChangedPaths = runGit(cwd, 'git diff --name-only HEAD')?.split(/\r?\n/).filter(Boolean) ?? [];
624
+ const mergeBase = runGit(cwd, 'git merge-base HEAD origin/main');
625
+ const branchChangedPaths = mergeBase
626
+ ? (runGit(cwd, `git diff --name-only ${mergeBase} HEAD`)?.split(/\r?\n/).filter(Boolean) ?? [])
627
+ : [];
628
+ const worktreeName = basename(cwd);
629
+ const contextText = readOptionalLocalText(cwd, '.productbrain/context.md');
630
+ const briefingText = readOptionalLocalText(cwd, '.productbrain/briefing.md');
631
+ const callerRefs = Array.isArray(callerSignals.reviewedArtifactRefs) ? callerSignals.reviewedArtifactRefs.join('\n') : undefined;
632
+ // Codex@677: briefing.md (task-specific) is read BEFORE context.md (broad) so
633
+ // its refs are collected first; combined with the per-source cap in
634
+ // collectLocalRefs, a context.md with 25+ IDs can no longer starve the briefing.
635
+ const refs = collectLocalRefs([
636
+ callerSignals.taskText ?? undefined,
637
+ callerSignals.explicitScope ?? undefined,
638
+ callerRefs,
639
+ branchName,
640
+ worktreeName,
641
+ briefingText,
642
+ contextText,
643
+ ]);
644
+ // F4: dereference any "PR #N" ref to its diff paths and fold them into the
645
+ // changedPaths evidence so PR refs contribute real path-domain signal. The
646
+ // server resolver records PR refs as dereferenced:false (no git access), so
647
+ // this is the only place the dereference can happen. Time-boxed + try/catch
648
+ // inside prDiff → absent/unauthed gh is a graceful no-op.
649
+ const prDiffPaths = [];
650
+ if (callerSignals.changedPaths === undefined) {
651
+ let derefed = 0;
652
+ for (const ref of refs) {
653
+ const prNumber = prNumberFromRef(ref);
654
+ if (prNumber === null)
655
+ continue;
656
+ if (derefed >= PR_DIFF_MAX_REFS)
657
+ break;
658
+ derefed++;
659
+ try {
660
+ for (const p of prDiff(cwd, prNumber))
661
+ prDiffPaths.push(p);
662
+ }
663
+ catch {
664
+ // gh absent / unauthed / PR missing — skip gracefully, leave existing behavior.
665
+ }
666
+ }
667
+ }
668
+ // Caller-supplied changedPaths win (backward compat); otherwise union the
669
+ // three local sources, deduped, order-stable.
670
+ const unionedChangedPaths = callerSignals.changedPaths ??
671
+ Array.from(new Set([...dirtyChangedPaths, ...branchChangedPaths, ...prDiffPaths]));
672
+ const baseSignals = sanitizeStartupResolutionSignals({
673
+ ...callerSignals,
674
+ sourceSurface: callerSignals.sourceSurface ?? 'cli',
675
+ resolverVersion: callerSignals.resolverVersion ?? STARTUP_RESOLUTION_RESOLVER_VERSION,
676
+ branchName: callerSignals.branchName ?? branchName,
677
+ worktreeName: callerSignals.worktreeName ?? worktreeName,
678
+ changedPaths: unionedChangedPaths,
679
+ reviewedArtifactRefs: callerSignals.reviewedArtifactRefs ?? refs,
680
+ commitSha: callerSignals.commitSha ?? commitSha,
681
+ });
682
+ const taskText = deriveHandshakeStartupTask(baseSignals.reviewedArtifactRefs ?? [], baseSignals.changedPaths ?? [], baseSignals.taskText);
683
+ const sanitized = sanitizeStartupResolutionSignals({
684
+ ...baseSignals,
685
+ taskText,
686
+ });
687
+ const hasMeaningfulEvidence = Boolean(sanitized.taskText) ||
688
+ (sanitized.changedPaths?.length ?? 0) > 0 ||
689
+ (sanitized.reviewedArtifactRefs?.length ?? 0) > 0 ||
690
+ Boolean(sanitized.explicitScope);
691
+ if (hasMeaningfulEvidence)
692
+ return sanitized;
693
+ // Codex@704: even with no task/path/ref/scope evidence, still emit a minimal
694
+ // envelope carrying the attribution fields (resolverVersion, commitSha,
695
+ // branchName). Returning {} here would drop the whole startup block at the
696
+ // caller, losing attribution AND the loud domain-unresolved signal exactly in
697
+ // the low-evidence case the resolver is meant to make queryable
698
+ // (spec "Loud Unresolved State"; AC #4). The envelope has no evidence fields,
699
+ // so the resolver sees no domain and emits domain-unresolved.
700
+ return sanitizeStartupResolutionSignals({
701
+ sourceSurface: sanitized.sourceSurface ?? 'cli',
702
+ resolverVersion: sanitized.resolverVersion ?? STARTUP_RESOLUTION_RESOLVER_VERSION,
703
+ branchName: sanitized.branchName,
704
+ worktreeName: sanitized.worktreeName,
705
+ commitSha: sanitized.commitSha,
706
+ changeId: sanitized.changeId,
707
+ });
708
+ }
249
709
  /**
250
710
  * Normalize volatile handshake-only timestamps before comparing generated files.
251
711
  * This keeps the visible timestamps in generated artifacts while avoiding
@@ -257,13 +717,241 @@ export function normalizeHandshakeContentForComparison(content) {
257
717
  .replace(/^_Generated: [0-9]{4}-[0-9]{2}-[0-9]{2}T.*_$/gm, '_Generated: <TIMESTAMP>_');
258
718
  }
259
719
  function shouldWriteAdapter(filePath, force) {
260
- if (force)
261
- return true;
720
+ // An absent file at an adapter path is a projection target → materialize it.
262
721
  if (!existsSync(filePath))
263
722
  return true;
264
723
  const content = readFileSync(filePath, 'utf8');
724
+ // A file WITHOUT the auto-gen MARKER is user-owned / committed-static — the hand-maintained
725
+ // constitution (CLAUDE.md / AGENTS.md) and the committed Copilot instructions
726
+ // (.github/copilot-instructions.md + .github/instructions/*.instructions.md) all live here.
727
+ // PB NEVER overwrites these — not even under --force. (--force only re-forces PB-managed files
728
+ // and bypasses the tampered-defer downstream; it must never clobber committed static content.
729
+ // Returning false here is what stops `pb handshake --force` from clobbering them; force is
730
+ // therefore not consulted for the user-owned class by design.)
731
+ void force;
265
732
  return content.includes(MARKER);
266
733
  }
734
+ function normalizeSurfaceName(surface) {
735
+ const stripped = surface.startsWith('.') ? surface.slice(1) : surface;
736
+ const normalized = stripped === 'github' ? 'copilot' : stripped;
737
+ return normalized in SURFACE_REGISTRY ? normalized : null;
738
+ }
739
+ function surfacePerimeterRoots(surface) {
740
+ if (surface === 'codex')
741
+ return ['.codex', SURFACE_REGISTRY.codex.hookFilePath];
742
+ if (surface === 'copilot')
743
+ return ['.github', SURFACE_REGISTRY.copilot.hookFilePath];
744
+ if (surface === 'claude')
745
+ return ['.claude', 'CLAUDE.md'];
746
+ return [`.${surface}`];
747
+ }
748
+ function modeRank(mode) {
749
+ return { off: 0, observe: 1, project: 2, govern: 3 }[mode];
750
+ }
751
+ function normalizeSetupAuthoringBody(body) {
752
+ return body.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trimEnd();
753
+ }
754
+ function setupAuthoringAssetHash(asset) {
755
+ const canonical = {
756
+ entryId: asset.entryId,
757
+ name: asset.name,
758
+ description: asset.description ?? '',
759
+ assetKind: asset.assetKind,
760
+ triggers: asset.triggers ?? [],
761
+ semanticRefs: asset.semanticRefs ?? [],
762
+ body: normalizeSetupAuthoringBody(asset.body),
763
+ };
764
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')}`;
765
+ }
766
+ function parseSetupAuthoringFrontmatter(raw) {
767
+ // Shared parser (lib/frontmatter.ts) — array-item quotes are stripped exactly
768
+ // as this function always did; the shared copy makes that behavior canonical
769
+ // for migrate-setup and setup-ingest too. `assetKind` from the union shape is
770
+ // ignored here (handshake derives kind from the directory).
771
+ const parsed = parseSetupFrontmatter(raw);
772
+ return {
773
+ frontmatterId: parsed.frontmatterId,
774
+ name: parsed.name,
775
+ description: parsed.description,
776
+ body: parsed.body,
777
+ triggers: parsed.triggers,
778
+ semanticRefs: parsed.semanticRefs,
779
+ };
780
+ }
781
+ function deriveSetupAuthoringEntryId(filename, kind) {
782
+ const base = basename(filename, '.md');
783
+ const snakeCase = base.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
784
+ return `SETUP-${kind.toUpperCase()}-${snakeCase}`;
785
+ }
786
+ function scanSetupAuthoringFiles(productbrainDir) {
787
+ const dirs = [
788
+ { dir: 'skills', kind: 'skill' },
789
+ { dir: 'rules', kind: 'rule' },
790
+ { dir: 'hooks', kind: 'hook' },
791
+ ];
792
+ const items = [];
793
+ for (const { dir, kind } of dirs) {
794
+ const absDir = join(productbrainDir, dir);
795
+ if (!existsSync(absDir))
796
+ continue;
797
+ for (const file of readdirSync(absDir).filter((f) => f.endsWith('.md'))) {
798
+ const filePath = join(absDir, file);
799
+ const parsed = parseSetupAuthoringFrontmatter(readFileSync(filePath, 'utf8'));
800
+ const fallbackName = basename(file, '.md');
801
+ items.push({
802
+ filePath,
803
+ derivedEntryId: deriveSetupAuthoringEntryId(file, kind),
804
+ frontmatterId: parsed.frontmatterId,
805
+ name: parsed.name || fallbackName,
806
+ description: parsed.description,
807
+ body: parsed.body,
808
+ assetKind: kind,
809
+ triggers: parsed.triggers,
810
+ semanticRefs: parsed.semanticRefs,
811
+ });
812
+ }
813
+ }
814
+ return items.sort((a, b) => a.filePath.localeCompare(b.filePath));
815
+ }
816
+ function setupAuthoringDirForKind(kind) {
817
+ if (kind === 'skill')
818
+ return 'skills';
819
+ if (kind === 'rule')
820
+ return 'rules';
821
+ if (kind === 'hook')
822
+ return 'hooks';
823
+ return null;
824
+ }
825
+ function setupAuthoringFilename(name, entryId) {
826
+ const base = (name || entryId)
827
+ .replace(/[\/\\:*?"<>|]/g, '-')
828
+ .replace(/\s+/g, ' ')
829
+ .trim();
830
+ return `${base || entryId}.md`;
831
+ }
832
+ function setupAuthoringPath(cwd, asset) {
833
+ const dir = setupAuthoringDirForKind(asset.assetKind);
834
+ if (!dir)
835
+ return null;
836
+ // WP-426 E3: recorded authoring source path wins so reprojection lands where the
837
+ // user authored — no duplicate (TEN-1920). Stored relative to .productbrain/.
838
+ if (asset.authoringPath && asset.authoringPath.trim()) {
839
+ // Codex P1/P2: authoringPath comes from the DB and is untrusted at projection
840
+ // time. It is probed via existsSync/readFileSync (and later writeFileSync) in the
841
+ // writeback loop BEFORE any assertSetupWritePath guard runs. Containment alone is
842
+ // not enough — a bad DB row could still point at a directory (e.g. "skills") or an
843
+ // internal PB file (".authoring-sync.json", "manifest.yaml"), which the loop would
844
+ // mis-handle as markdown (throw on a directory read, or overwrite PB state).
845
+ // Constrain to a kind-appropriate markdown file (<dir>/**/*.md); otherwise fall
846
+ // back to the safe name-derived path.
847
+ const pbDir = join(cwd, '.productbrain');
848
+ const candidate = resolve(pbDir, asset.authoringPath);
849
+ const within = relative(pbDir, candidate);
850
+ const withinPosix = within.split(sep).join('/');
851
+ if (!within.startsWith('..') &&
852
+ !isAbsolute(within) &&
853
+ withinPosix.startsWith(`${dir}/`) &&
854
+ withinPosix.toLowerCase().endsWith('.md')) {
855
+ return candidate;
856
+ }
857
+ }
858
+ return join(cwd, '.productbrain', dir, setupAuthoringFilename(asset.name, asset.entryId));
859
+ }
860
+ function renderSetupAuthoringFile(asset) {
861
+ const lines = [
862
+ '---',
863
+ `id: ${asset.entryId}`,
864
+ `name: ${JSON.stringify(asset.name)}`,
865
+ `description: ${JSON.stringify(asset.description ?? '')}`,
866
+ `assetKind: ${asset.assetKind}`,
867
+ ];
868
+ const pushArray = (key, values) => {
869
+ if (!values || values.length === 0)
870
+ return;
871
+ lines.push(`${key}:`);
872
+ for (const value of values)
873
+ lines.push(` - ${JSON.stringify(value)}`);
874
+ };
875
+ pushArray('triggers', asset.triggers);
876
+ pushArray('semanticRefs', asset.semanticRefs);
877
+ lines.push('---', normalizeSetupAuthoringBody(asset.body), '');
878
+ return lines.join('\n');
879
+ }
880
+ function loadAuthoringSyncState(productbrainDir) {
881
+ const statePath = join(productbrainDir, '.authoring-sync.json');
882
+ if (!existsSync(statePath))
883
+ return { version: 1, assets: {} };
884
+ try {
885
+ const parsed = JSON.parse(readFileSync(statePath, 'utf8'));
886
+ if (parsed.version !== 1 || !parsed.assets || typeof parsed.assets !== 'object') {
887
+ return { version: 1, assets: {} };
888
+ }
889
+ // Codex P2: the dormant registries are consumed via .includes() in the dormant loop
890
+ // BEFORE its per-file try/catch, so a malformed .authoring-sync.json (a field set to
891
+ // an object/number instead of an array) would throw a TypeError and abort the whole
892
+ // handshake. Coerce to a string[] (mirrors the assets validation above) to fail open.
893
+ const toStringArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
894
+ return {
895
+ version: 1,
896
+ assets: parsed.assets,
897
+ dormantRenamed: toStringArray(parsed.dormantRenamed),
898
+ dormantReactivated: toStringArray(parsed.dormantReactivated),
899
+ };
900
+ }
901
+ catch {
902
+ return { version: 1, assets: {} };
903
+ }
904
+ }
905
+ function saveAuthoringSyncState(productbrainDir, state) {
906
+ const statePath = join(productbrainDir, '.authoring-sync.json');
907
+ assertSetupWritePath(statePath, { surfaces: [] });
908
+ mkdirSync(productbrainDir, { recursive: true });
909
+ writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
910
+ }
911
+ const DRIFT_HASH_TRAILER_REGEX = /^<!--\s*pb-hash:\s*sha256:([0-9a-f]+)\s*-->\s*$/m;
912
+ const DRIFT_HASH_TRAILER_STRIP = /^<!--\s*pb-hash:.*-->\s*$/gm;
913
+ const DRIFT_TIMESTAMP_STRIP = /^<!--\s*pb-generated-at:.*-->\s*$/gm;
914
+ /**
915
+ * Classify a single projection-target file into one of the three drift buckets.
916
+ *
917
+ * Returns `null` when `filePath` does not exist (first-run / unprojected) — the
918
+ * write loop treats that as "would-write" and the file is not part of any bucket.
919
+ *
920
+ * @param filePath Absolute path to the projection file on disk.
921
+ * @returns { bucket, expectedHash, actualHash } when the file exists.
922
+ * `expectedHash`/`actualHash` are populated only for the
923
+ * tampered bucket so the headless refusal payload can include
924
+ * them verbatim. For clean / user-owned, both are `''`.
925
+ */
926
+ export function classifyDriftBucket(filePath) {
927
+ if (!existsSync(filePath))
928
+ return null;
929
+ const content = readFileSync(filePath, 'utf8');
930
+ // No auto-gen MARKER → user-owned. Never touch.
931
+ if (!content.includes(MARKER)) {
932
+ return { bucket: 'user-owned', expectedHash: '', actualHash: '' };
933
+ }
934
+ // Marker present but no hash trailer → legacy / pre-S0c projection: treat as
935
+ // clean (the hash trailer was added in WP-345 S0c). The user-owned-vs-clean
936
+ // semantic falls back to existing shouldWriteAdapter behavior.
937
+ const trailerMatch = content.match(DRIFT_HASH_TRAILER_REGEX);
938
+ if (!trailerMatch) {
939
+ return { bucket: 'pb-managed-clean', expectedHash: '', actualHash: '' };
940
+ }
941
+ const expectedHash = `sha256:${trailerMatch[1]}`;
942
+ // Recompute the actual hash from the body (strip trailer + timestamp, LF, trim).
943
+ const normalized = content
944
+ .replace(DRIFT_HASH_TRAILER_STRIP, '')
945
+ .replace(DRIFT_TIMESTAMP_STRIP, '')
946
+ .replace(/\r\n/g, '\n')
947
+ .replace(/\r/g, '\n')
948
+ .trimEnd();
949
+ const actualHash = `sha256:${createHash('sha256').update(normalized, 'utf8').digest('hex')}`;
950
+ if (actualHash === expectedHash) {
951
+ return { bucket: 'pb-managed-clean', expectedHash, actualHash };
952
+ }
953
+ return { bucket: 'pb-managed-tampered', expectedHash, actualHash };
954
+ }
267
955
  function deduplicateEntries(entries) {
268
956
  const seen = new Set();
269
957
  const result = [];
@@ -276,13 +964,175 @@ function deduplicateEntries(entries) {
276
964
  }
277
965
  return result;
278
966
  }
967
+ /**
968
+ * resolveProjectionCollision — WP-379 S5b
969
+ *
970
+ * Marker-scoped orphan unlink: enumerates target dirs (.cursor/rules/,
971
+ * .claude/rules/, .claude/skills/, .codex/skills/); for each file that has
972
+ * the auto-gen MARKER whose lowercase-normalized filename does NOT match any
973
+ * current active-asset materializedFilename, the file is unlinked.
974
+ *
975
+ * User-owned files (no MARKER) are never touched, regardless of name.
976
+ *
977
+ * Linux case-collision disambiguation:
978
+ * 1. Exact match (lowercase name == any canonical name): survives.
979
+ * 2. Case-variant with MARKER (marker file, no exact canonical match): unlinked.
980
+ * 3. Ambiguous (zero exact, multiple case-variants with MARKER):
981
+ * newest mtime wins; all others are unlinked; a collision TEN is
982
+ * appended to the session capture queue (not fired inline).
983
+ *
984
+ * Returns a list of unlink results so the caller can log/report them.
985
+ *
986
+ * @param cwd Project root (absolute path).
987
+ * @param assetNames The current set of canonical asset names from the server
988
+ * (e.g. ["Setup-ProductBrain", "chain-rules"]).
989
+ * @param log Progress log function.
990
+ * @param logErr Error log function.
991
+ */
992
+ export function resolveProjectionCollision(cwd, assetNames, log, logErr) {
993
+ // Target directories by extension suffix.
994
+ const TARGET_DIRS_BY_EXT = [
995
+ { dir: join(cwd, '.cursor', 'rules'), ext: '.mdc' },
996
+ { dir: join(cwd, '.claude', 'rules'), ext: '.md' },
997
+ { dir: join(cwd, '.claude', 'skills'), ext: '.md' },
998
+ { dir: join(cwd, '.codex', 'skills'), ext: '.md' },
999
+ ];
1000
+ // Build a set of normalized canonical names (without extension) for fast lookup.
1001
+ // We normalize all asset names to detect case-variant collisions.
1002
+ // For each asset name we derive the normalized basename (the part before the ext).
1003
+ // canonicalNormalizedNames: Set<normalized-stem> (lowercase + slug).
1004
+ const canonicalNormalizedStems = new Set(assetNames.map((n) => normalizeMaterializedFilename(n)));
1005
+ const results = [];
1006
+ const collisionTens = [];
1007
+ for (const { dir, ext } of TARGET_DIRS_BY_EXT) {
1008
+ if (!existsSync(dir))
1009
+ continue;
1010
+ let files;
1011
+ try {
1012
+ files = readdirSync(dir);
1013
+ }
1014
+ catch {
1015
+ continue; // unreadable dir — skip
1016
+ }
1017
+ // Group files by their normalized stem.
1018
+ // normalizedStem → [ { filename, fullPath } ]
1019
+ const groups = new Map();
1020
+ for (const filename of files) {
1021
+ if (!filename.endsWith(ext))
1022
+ continue;
1023
+ const fullPath = join(dir, filename);
1024
+ // Only operate on files that have the auto-gen MARKER.
1025
+ let content;
1026
+ try {
1027
+ content = readFileSync(fullPath, 'utf8');
1028
+ }
1029
+ catch {
1030
+ continue; // unreadable file — skip
1031
+ }
1032
+ if (!content.includes(MARKER))
1033
+ continue; // user-owned — never touch
1034
+ const stem = basename(filename, ext);
1035
+ const normalizedStem = normalizeMaterializedFilename(stem);
1036
+ const group = groups.get(normalizedStem) ?? [];
1037
+ group.push({ filename, fullPath });
1038
+ groups.set(normalizedStem, group);
1039
+ }
1040
+ // Evaluate each normalized stem group.
1041
+ for (const [normalizedStem, members] of groups) {
1042
+ const isKnownCanonical = canonicalNormalizedStems.has(normalizedStem);
1043
+ if (!isKnownCanonical) {
1044
+ // All members of this group are orphans (no canonical asset with this stem).
1045
+ // Unlink them all — they're stale projections of an asset no longer in the server.
1046
+ for (const { filename, fullPath } of members) {
1047
+ try {
1048
+ unlinkSync(fullPath);
1049
+ log(`Orphan unlinked: ${fullPath}`);
1050
+ results.push({ action: 'unlinked', filePath: fullPath, reason: 'orphan-no-canonical-match' });
1051
+ }
1052
+ catch (err) {
1053
+ logErr(`Warning: could not unlink orphan ${fullPath} — ${err instanceof Error ? err.message : String(err)}`);
1054
+ results.push({ action: 'kept', filePath: fullPath, reason: 'unlink-failed' });
1055
+ }
1056
+ }
1057
+ continue;
1058
+ }
1059
+ // The stem IS known canonical. Check for case-collision.
1060
+ if (members.length === 1) {
1061
+ // Single file — no collision.
1062
+ results.push({ action: 'kept', filePath: members[0].fullPath, reason: 'canonical-exact' });
1063
+ continue;
1064
+ }
1065
+ // Multiple files with the same normalized stem → case-collision.
1066
+ // Rule: exact match (filename stem === normalized stem, i.e. already lowercase) wins.
1067
+ const exactMatches = members.filter(({ filename }) => {
1068
+ const stem = basename(filename, ext);
1069
+ return stem === normalizedStem; // lowercase-equal means already normalized
1070
+ });
1071
+ if (exactMatches.length === 1) {
1072
+ // Rule 1: exactly one exact match → keep it, unlink all case-variants.
1073
+ const keeper = exactMatches[0];
1074
+ results.push({ action: 'kept', filePath: keeper.fullPath, reason: 'case-exact-match-wins' });
1075
+ for (const member of members) {
1076
+ if (member.fullPath === keeper.fullPath)
1077
+ continue;
1078
+ try {
1079
+ unlinkSync(member.fullPath);
1080
+ log(`Case-variant unlinked: ${member.fullPath} (kept: ${keeper.filename})`);
1081
+ results.push({ action: 'unlinked', filePath: member.fullPath, reason: 'case-variant-unlinked' });
1082
+ }
1083
+ catch (err) {
1084
+ logErr(`Warning: could not unlink case-variant ${member.fullPath} — ${err instanceof Error ? err.message : String(err)}`);
1085
+ results.push({ action: 'kept', filePath: member.fullPath, reason: 'unlink-failed' });
1086
+ }
1087
+ }
1088
+ continue;
1089
+ }
1090
+ // Rule 3: ambiguous — zero exact matches (or multiple exact matches, which
1091
+ // can't happen on a case-sensitive FS). Newest mtime wins.
1092
+ // Sort by mtime descending: highest mtime = newest = winner.
1093
+ const withStats = members.map(({ filename, fullPath }) => {
1094
+ try {
1095
+ const { mtimeMs } = statSync(fullPath);
1096
+ return { filename, fullPath, mtimeMs };
1097
+ }
1098
+ catch {
1099
+ return { filename, fullPath, mtimeMs: 0 };
1100
+ }
1101
+ });
1102
+ withStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
1103
+ const winner = withStats[0];
1104
+ log(`Case-collision ambiguous for ${normalizedStem}${ext}: newest mtime wins (${winner.filename})`);
1105
+ results.push({ action: 'collision-ten', filePath: winner.fullPath, reason: 'ambiguous-newest-mtime-wins' });
1106
+ const tenMsg = `Handshake case-collision: ambiguous filename for stem "${normalizedStem}${ext}" ` +
1107
+ `(${members.map((m) => m.filename).join(', ')}). ` +
1108
+ `Kept newest: ${winner.filename}. Consider renaming to ${normalizedStem}${ext}.`;
1109
+ collisionTens.push(tenMsg);
1110
+ for (const member of withStats.slice(1)) {
1111
+ try {
1112
+ unlinkSync(member.fullPath);
1113
+ log(`Case-variant (ambiguous) unlinked: ${member.fullPath}`);
1114
+ results.push({ action: 'unlinked', filePath: member.fullPath, reason: 'ambiguous-case-variant-unlinked' });
1115
+ }
1116
+ catch (err) {
1117
+ logErr(`Warning: could not unlink ambiguous case-variant ${member.fullPath} — ${err instanceof Error ? err.message : String(err)}`);
1118
+ results.push({ action: 'kept', filePath: member.fullPath, reason: 'unlink-failed' });
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ return { results, collisionTens };
1124
+ }
279
1125
  export async function runHandshake(options = {}) {
280
- const config = await getConfigOrGuide(() => runHandshake(options));
1126
+ const config = await getConfigOrGuide(async () => { await runHandshake(options); });
281
1127
  if (!config)
282
- return;
1128
+ return undefined;
283
1129
  const cwd = process.cwd();
284
1130
  const force = options.force ?? false;
285
1131
  const dryRun = options.dryRun ?? false;
1132
+ // Preview mode: default when neither --apply nor --dry-run is passed.
1133
+ // --dry-run is kept as a backward-compat alias for preview (same behavior).
1134
+ const preview = !options.apply && !dryRun;
1135
+ const applyMode = options.apply === true && !dryRun;
286
1136
  const level = options.level;
287
1137
  const quiet = options.quiet ?? false;
288
1138
  const generate = options.generate ?? false;
@@ -295,8 +1145,11 @@ export async function runHandshake(options = {}) {
295
1145
  // Validate --level if provided (for handshake content filtering, not --init trust level)
296
1146
  const VALID_HANDSHAKE_LEVELS = ['beginner', 'intermediate', 'expert'];
297
1147
  if (level && !VALID_HANDSHAKE_LEVELS.includes(level)) {
298
- process.stderr.write(`Unknown level "${level}". Valid levels: ${VALID_HANDSHAKE_LEVELS.join(', ')}\n`);
299
- process.exit(1);
1148
+ throw new CLIError(`Unknown level "${level}".`, {
1149
+ code: ErrorCode.VALIDATION_FAILED,
1150
+ category: 'validation',
1151
+ guidance: `Valid levels: ${VALID_HANDSHAKE_LEVELS.join(', ')}`,
1152
+ });
300
1153
  }
301
1154
  // 1. Detect repo
302
1155
  const repo = detectRepo(cwd);
@@ -304,14 +1157,23 @@ export async function runHandshake(options = {}) {
304
1157
  if (repo.detectedStack.length > 0) {
305
1158
  log(`Stack: ${repo.detectedStack.join(', ')}`);
306
1159
  }
1160
+ const startupSignals = deriveHandshakeStartupSignals(cwd, options.startupSignals ?? {});
1161
+ const orientArgs = { tier: 'standard' };
1162
+ if (options.invocationPath)
1163
+ orientArgs.invocationPath = options.invocationPath;
1164
+ if (Object.keys(startupSignals).length > 0) {
1165
+ orientArgs.startupSignals = startupSignals;
1166
+ if (startupSignals.taskText)
1167
+ orientArgs.task = startupSignals.taskText;
1168
+ }
307
1169
  // 2. Fetch orient view + workspace readiness in parallel (budget max +200ms added latency)
308
1170
  log('Fetching workspace context...');
309
1171
  let orientView = null;
310
1172
  let workspaceProfile = null;
311
1173
  try {
312
- const workspaceReadinessPromise = mcpCall('chain.workspaceReadiness', {}).catch(() => null);
1174
+ const workspaceReadinessPromise = kernelCall('chain.workspaceReadiness', {}).catch(() => null);
313
1175
  const [orientResult, readinessRaw] = await Promise.all([
314
- mcpCall('chain.getOrientView', {}).catch((err) => {
1176
+ kernelCall('chain.getOrientView', orientArgs).catch((err) => {
315
1177
  logErr(`Warning: could not fetch workspace context — ${err instanceof Error ? err.message : err}`);
316
1178
  logErr('Continuing with limited context (Chain search + portable knowledge only).');
317
1179
  return null;
@@ -345,20 +1207,347 @@ export async function runHandshake(options = {}) {
345
1207
  let matchedEntries = [];
346
1208
  if (uniqueQueries.length > 0) {
347
1209
  log(`Searching Chain for: ${uniqueQueries.join(', ')}...`);
348
- const searchResults = await Promise.all(uniqueQueries.map((q) => mcpCall('chain.searchEntries', { query: q }).catch(() => [])));
1210
+ const searchResults = await Promise.all(uniqueQueries.map((q) => kernelCall('chain.searchEntries', { query: q }).catch(() => [])));
349
1211
  matchedEntries = deduplicateEntries(searchResults.flat());
350
1212
  }
351
- // 5. Read canonical skills & rules from .productbrain/
1213
+ // 5. Read canonical skills & rules DB-first (WP-345 S0c, TEN-1459), FS fallback.
1214
+ // Primary: query setup.listAssetsForUser from DB (workspace SSOT).
1215
+ // Fallback: read from .productbrain/ filesystem (legacy — used when DB is empty or unavailable).
352
1216
  const pbDir = join(cwd, '.productbrain');
353
- const allSkills = readCanonicalSkills(pbDir);
354
- const manualRules = readCanonicalRules(pbDir);
1217
+ const manifestStatus = readManifestStatus(pbDir);
1218
+ const manifest = manifestStatus.manifest;
1219
+ const surfaceValidation = validateSurfacesForMode(manifestStatus.mode, manifestStatus.surfaces);
1220
+ if (applyMode && surfaceValidation.error === SURFACE_GOVERN_NO_SURFACES) {
1221
+ throw new CLIError('materialize: govern requires at least one registered manifest surface.', {
1222
+ code: ErrorCode.VALIDATION_FAILED,
1223
+ category: 'validation',
1224
+ guidance: 'Add surfaces such as `.cursor` or `.claude` to .productbrain/manifest.yaml.',
1225
+ });
1226
+ }
1227
+ for (const surface of surfaceValidation.unregisteredSurfaces) {
1228
+ logErr(`Warning: manifest surface "${surface}" is not registered; skipping it.`);
1229
+ }
1230
+ const manifestTargets = new Set(surfaceValidation.registeredSurfaces);
1231
+ const cliTargets = new Set();
1232
+ const ignoredCliSurfaces = [];
1233
+ for (const surface of options.surfaces ?? []) {
1234
+ const normalized = normalizeSurfaceName(surface);
1235
+ if (normalized && manifestTargets.has(normalized)) {
1236
+ cliTargets.add(normalized);
1237
+ }
1238
+ else {
1239
+ ignoredCliSurfaces.push(surface);
1240
+ }
1241
+ }
1242
+ if (ignoredCliSurfaces.length > 0) {
1243
+ logErr(`Warning: --surfaces ignored outside manifest.surfaces: ${ignoredCliSurfaces.join(', ')}`);
1244
+ }
1245
+ const allowedTargets = options.surfaces && options.surfaces.length > 0
1246
+ ? cliTargets
1247
+ : manifestTargets;
1248
+ const perimeterManifest = {
1249
+ surfaces: [...manifestTargets].flatMap(surfacePerimeterRoots),
1250
+ };
1251
+ const authorityCanWrite = manifestStatus.mode === 'project' || manifestStatus.mode === 'govern';
1252
+ const authorityPreviewOnly = applyMode && !authorityCanWrite;
1253
+ const writeMode = applyMode && authorityCanWrite;
1254
+ let dbSkills = [];
1255
+ let dbRules = [];
1256
+ let usedDbSource = false;
1257
+ let dbAssetRows = [];
1258
+ // WP-379 S4: dormant assets (gate-failed) — their on-disk files get the dormant marker.
1259
+ let dormantDbAssetRows = [];
1260
+ // WP-428 S2 (Finding #5): tracks entryIds whose body fetch from storage failed.
1261
+ // Hoisted here (same scope as dbAssetRows) so both the skills/rules projection loop
1262
+ // AND the authoring-file projection loop can skip failed assets.
1263
+ const bodyFetchFailedEntryIds = new Set();
1264
+ const dbProjectionHashes = new Map();
1265
+ const syncDriftTensToFire = [];
1266
+ const deferredAuthoringBaselineEntryIds = new Set();
1267
+ if (writeMode) {
1268
+ const authoringItems = scanSetupAuthoringFiles(pbDir);
1269
+ if (authoringItems.length > 0) {
1270
+ // WP-428 S2 (Critical #1): ingestSetupAssetsBatch is an internalMutation — it cannot
1271
+ // call ctx.storage.store(). We now call ingestSetupAssetWithBody (action) per-asset so
1272
+ // each asset gets bodyStorageId written. This loses intra-batch collision detection;
1273
+ // canonical-id collision check is now client-side before dispatch.
1274
+ //
1275
+ // Client-side collision detection (replaces server-side DEC-954 check in the batch mutation):
1276
+ const idToPaths = new Map();
1277
+ for (const item of authoringItems) {
1278
+ const canonicalId = item.frontmatterId?.trim() || item.derivedEntryId;
1279
+ const paths = idToPaths.get(canonicalId) ?? [];
1280
+ paths.push(item.filePath);
1281
+ idToPaths.set(canonicalId, paths);
1282
+ }
1283
+ const conflictingPaths = new Set();
1284
+ for (const [, paths] of idToPaths) {
1285
+ if (paths.length > 1) {
1286
+ for (const p of paths)
1287
+ conflictingPaths.add(p);
1288
+ }
1289
+ }
1290
+ if (conflictingPaths.size > 0) {
1291
+ logErr(`Setup authoring import partial: ${conflictingPaths.size} file(s) refused for duplicate setup id. ` +
1292
+ [...conflictingPaths].join(', '));
1293
+ }
1294
+ const itemsToIngest = authoringItems.filter((item) => !conflictingPaths.has(item.filePath));
1295
+ if (itemsToIngest.length > 0) {
1296
+ try {
1297
+ // Parallel uploads — concurrency limit 10 to avoid overwhelming the gateway.
1298
+ const CONCURRENCY = 10;
1299
+ const ingestResults = [];
1300
+ for (let i = 0; i < itemsToIngest.length; i += CONCURRENCY) {
1301
+ const batch = itemsToIngest.slice(i, i + CONCURRENCY);
1302
+ const settled = await Promise.allSettled(batch.map(async (item) => {
1303
+ const result = await kernelCall('setup.ingestSetupAssetWithBody', {
1304
+ entryId: item.derivedEntryId,
1305
+ frontmatterId: item.frontmatterId,
1306
+ name: item.name,
1307
+ description: item.description,
1308
+ body: item.body,
1309
+ assetKind: item.assetKind,
1310
+ triggers: item.triggers,
1311
+ semanticRefs: item.semanticRefs,
1312
+ authoringPath: relative(pbDir, item.filePath).split(sep).join('/'),
1313
+ });
1314
+ return { filePath: item.filePath, ...result };
1315
+ }));
1316
+ for (const r of settled) {
1317
+ if (r.status === 'fulfilled') {
1318
+ ingestResults.push(r.value);
1319
+ if (r.value.conflict === 'repo-wins') {
1320
+ syncDriftTensToFire.push(`Repo authoring file won setup sync conflict for ${r.value.entryId} at ${r.value.filePath}.`);
1321
+ logErr(`Warning: repo authoring file won sync conflict for ${r.value.entryId}; DB edit was overwritten.`);
1322
+ }
1323
+ }
1324
+ else {
1325
+ trackEvent('setup.authoring_import.item_failed', { error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
1326
+ logErr(`Warning: setup authoring import failed for one asset — ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
1327
+ }
1328
+ }
1329
+ }
1330
+ if (ingestResults.length > 0) {
1331
+ log(`Setup authoring import: ${ingestResults.length} file(s) checked (with bodyStorageId).`);
1332
+ }
1333
+ }
1334
+ catch (err) {
1335
+ trackEvent('setup.authoring_import.failed', { error: err instanceof Error ? err.message : String(err) });
1336
+ logErr(`Warning: setup authoring import failed — ${err instanceof Error ? err.message : String(err)}`);
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+ try {
1342
+ // WP-379 S4: listAssetsForUser now returns { activeAssets, dormantAssets }.
1343
+ // Wire format changed from DbAsset[] to { activeAssets: DbAsset[], dormantAssets: DbAsset[] }.
1344
+ // Fall back to empty arrays if the server returns the old flat-array shape (graceful degradation).
1345
+ const rawResponse = await kernelCall('setup.listAssetsForUser', {}).catch(() => null);
1346
+ let dbAssets = [];
1347
+ if (rawResponse !== null) {
1348
+ if (Array.isArray(rawResponse)) {
1349
+ // Pre-S4 server — treat entire response as active assets with no dormant list.
1350
+ dbAssets = rawResponse;
1351
+ dormantDbAssetRows = [];
1352
+ }
1353
+ else {
1354
+ dbAssets = rawResponse.activeAssets ?? [];
1355
+ dormantDbAssetRows = rawResponse.dormantAssets ?? [];
1356
+ }
1357
+ }
1358
+ if (dbAssets.length > 0) {
1359
+ dbAssetRows = dbAssets;
1360
+ // WP-428 S2: body is no longer inline — fetch from storage per-asset when bodyStorageId is set.
1361
+ // Projectable assets: non-disabled skill/rule/hook entries. Fetch bodies in parallel (bounded).
1362
+ const projectableAssets = dbAssets.filter((a) => !a.disabledByOwner && (a.assetKind === 'skill' || a.assetKind === 'rule' || a.assetKind === 'hook'));
1363
+ const assetsNeedingBodyFetch = projectableAssets.filter((a) => a.bodyStorageId);
1364
+ const bodyFetchMap = new Map(); // entryId → body
1365
+ // WP-428 S2 (Finding #5/#12): track fetch-failed assets so we can skip their projection.
1366
+ // Failed entryIds are excluded from dbSkills/dbRules — no empty body written, no lastProjectedHash update.
1367
+ // bodyFetchFailedEntryIds is declared at outer scope (also used by authoring-file projection loop).
1368
+ if (assetsNeedingBodyFetch.length > 0) {
1369
+ log(`Fetching ${assetsNeedingBodyFetch.length} asset body(s) from storage...`);
1370
+ const bodyFetchResults = await Promise.allSettled(assetsNeedingBodyFetch.map(async (asset) => {
1371
+ const result = await kernelCall('setup.fetchAssetBody', { bodyStorageId: asset.bodyStorageId });
1372
+ return { entryId: asset.entryId, name: asset.name, body: result.body };
1373
+ }));
1374
+ for (let i = 0; i < bodyFetchResults.length; i++) {
1375
+ const result = bodyFetchResults[i];
1376
+ if (result.status === 'fulfilled') {
1377
+ bodyFetchMap.set(result.value.entryId, result.value.body);
1378
+ }
1379
+ else {
1380
+ // WP-428 S2 (Finding #12): include entryId and name in the warning (Finding #5: skip projection).
1381
+ const asset = assetsNeedingBodyFetch[i];
1382
+ logErr(`Warning: failed to fetch body for ${asset.entryId} (${asset.name}) — ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
1383
+ bodyFetchFailedEntryIds.add(asset.entryId);
1384
+ }
1385
+ }
1386
+ if (bodyFetchFailedEntryIds.size > 0) {
1387
+ // WP-439 S4: strict-by-default. Any body-fetch failure is a hard
1388
+ // failure unless --lenient is passed. The strict path surfaces an
1389
+ // actionable pointer at the audit/repair command operators can run
1390
+ // to diagnose and fix orphaned setup-asset rows.
1391
+ const failedList = [...bodyFetchFailedEntryIds].join(', ');
1392
+ if (options.lenient) {
1393
+ logErr(`Warning: ${bodyFetchFailedEntryIds.size} asset(s) skipped due to body fetch failure (--lenient): ${failedList}`);
1394
+ }
1395
+ else {
1396
+ throw new CLIError(`${bodyFetchFailedEntryIds.size} asset(s) failed body fetch: ${failedList}. ` +
1397
+ `Run \`pb setup-audit\` to diagnose, then \`pb setup-audit --repair\` to reseed. ` +
1398
+ `Pass --lenient to suppress this and continue with affected entries skipped (legacy behaviour).`, { code: ErrorCode.INTERNAL, category: 'internal' });
1399
+ }
1400
+ }
1401
+ }
1402
+ // Map DB assets to CanonicalSkill/CanonicalRule shapes
1403
+ for (const asset of dbAssets) {
1404
+ if (asset.disabledByOwner)
1405
+ continue;
1406
+ // WP-428 S2 (Finding #5): skip projection for assets whose body fetch failed.
1407
+ // Do NOT write empty body to disk and do NOT update lastProjectedHash.
1408
+ if (bodyFetchFailedEntryIds.has(asset.entryId))
1409
+ continue;
1410
+ // WP-428 S2: resolve body — prefer storage-fetched body, fall back to inline body (pre-S2 servers).
1411
+ // Fallback: pre-S2 servers (no bodyStorageId) return inline body. Once all servers are S2+, this can be deleted.
1412
+ const resolvedBody = bodyFetchMap.get(asset.entryId) ?? asset.body ?? '';
1413
+ if (asset.assetKind === 'skill') {
1414
+ dbSkills.push({
1415
+ name: asset.name,
1416
+ description: asset.description,
1417
+ triggers: asset.triggers ?? [],
1418
+ body: resolvedBody,
1419
+ sourcePath: `db:${asset.entryId}`,
1420
+ });
1421
+ }
1422
+ else if (asset.assetKind === 'rule' || asset.assetKind === 'hook') {
1423
+ dbRules.push({
1424
+ name: asset.name,
1425
+ description: asset.description,
1426
+ autoApply: false,
1427
+ body: resolvedBody,
1428
+ sourcePath: `db:${asset.entryId}`,
1429
+ });
1430
+ }
1431
+ }
1432
+ usedDbSource = true;
1433
+ log(`Setup assets: ${dbSkills.length} skills, ${dbRules.length} rules/hooks from DB (WP-345 DB-first path)`);
1434
+ if (dormantDbAssetRows.length > 0) {
1435
+ log(`Setup assets: ${dormantDbAssetRows.length} dormant (gate-filtered) asset(s) will be marked on disk`);
1436
+ }
1437
+ }
1438
+ }
1439
+ catch {
1440
+ // DB source unavailable — silently fall through to FS path
1441
+ }
1442
+ const allSkills = usedDbSource ? dbSkills : readCanonicalSkills(pbDir);
1443
+ const manualRules = usedDbSource ? dbRules : readCanonicalRules(pbDir);
1444
+ if (!usedDbSource) {
1445
+ log('Setup assets: reading from filesystem (DB source unavailable or empty)');
1446
+ }
1447
+ // 5a-pre. E4: Resolve semantic refs in DB assets (WP-345, DEC-A, DEC-C, DEC-K)
1448
+ // For each asset with semanticRefs[], resolve them via the Convex resolver and
1449
+ // replace {{ref:key}} placeholders in the body. Runs in apply mode only (not preview).
1450
+ // NG11: PostHog events fire from CLI side only (never inside Convex mutations).
1451
+ if (usedDbSource && writeMode) {
1452
+ const projectableDbAssets = dbAssetRows.filter((a) => !a.disabledByOwner && (a.assetKind === 'skill' || a.assetKind === 'rule' || a.assetKind === 'hook'));
1453
+ const assetsWithRefs = projectableDbAssets.filter((a) => a.semanticRefs && a.semanticRefs.length > 0);
1454
+ if (assetsWithRefs.length > 0) {
1455
+ log(`Resolving semantic refs for ${assetsWithRefs.length} asset(s)...`);
1456
+ // Collect unique ref keys across all assets
1457
+ const allRefKeys = [...new Set(assetsWithRefs.flatMap((a) => a.semanticRefs))];
1458
+ // Resolve all refs in a single batch call. Shape: SetupRefResolution[]
1459
+ // (DEC-767 / WP-354 Build-Order #6 — kind + status discriminator).
1460
+ let resolvedRefs = [];
1461
+ try {
1462
+ resolvedRefs = await kernelCall('setup.resolveSemanticRefs', { semanticRefs: allRefKeys });
1463
+ }
1464
+ catch (err) {
1465
+ trackEvent('setup.refs.resolve_failed', { error: err instanceof Error ? err.message : String(err) });
1466
+ logErr(`Warning: could not resolve semantic refs — ${err instanceof Error ? err.message : String(err)}`);
1467
+ }
1468
+ // Build resolved map: canonicalKey → display name. Only required refs
1469
+ // count as unresolved warnings; seed/unknown refs are not gates.
1470
+ const resolvedMap = new Map();
1471
+ let unresolvedCount = 0;
1472
+ for (const result of resolvedRefs) {
1473
+ if (result.status === 'resolved' && result.localEntryId) {
1474
+ resolvedMap.set(result.ref, result.localEntryId);
1475
+ trackEvent('skill.ref.resolved', { ref: result.ref, kind: result.kind });
1476
+ }
1477
+ else if (result.status === 'unsupported-future') {
1478
+ // seed: refs are explicitly future scope per WP-354 Build-Order #6 — not a warning.
1479
+ trackEvent('skill.ref.future', { ref: result.ref, kind: result.kind });
1480
+ }
1481
+ else if (result.required) {
1482
+ unresolvedCount++;
1483
+ trackEvent('skill.ref.unresolved', { ref: result.ref, kind: result.kind, status: result.status });
1484
+ }
1485
+ }
1486
+ if (unresolvedCount > 0) {
1487
+ logErr(`Warning: ${unresolvedCount} required semantic ref(s) could not be resolved.`);
1488
+ }
1489
+ // Projection must preserve ref tokens as portable machine-readable refs.
1490
+ // resolvedMap is only used for validation/telemetry here; generated setup
1491
+ // artifacts keep {{ref:domain:...}} / {{ref:entry:...}} intact.
1492
+ }
1493
+ // Compute projection hash for each projected DB asset (DEC-K).
1494
+ // The stored hash is sha256 of the normalized resolved asset body; DB persistence
1495
+ // is deferred until the write loop confirms a matching file was emitted.
1496
+ // Strip existing hash trailer and timestamp lines, normalize to LF, then hash.
1497
+ const HASH_TRAILER_REGEX = /^<!--\s*pb-hash:.*-->\s*$/gm;
1498
+ const TIMESTAMP_REGEX = /^<!--\s*pb-generated-at:.*-->\s*$/gm;
1499
+ for (const rawAsset of projectableDbAssets) {
1500
+ const resolvedAsset = [...dbSkills, ...dbRules].find((a) => a.sourcePath === `db:${rawAsset.entryId}`);
1501
+ if (!resolvedAsset)
1502
+ continue;
1503
+ try {
1504
+ // Build the projected body (what will be written to disk)
1505
+ const projectedBody = resolvedAsset.body;
1506
+ // Normalize: strip existing hash/timestamp trailers, convert to LF
1507
+ const normalized = projectedBody
1508
+ .replace(HASH_TRAILER_REGEX, '')
1509
+ .replace(TIMESTAMP_REGEX, '')
1510
+ .replace(/\r\n/g, '\n')
1511
+ .replace(/\r/g, '\n')
1512
+ .trimEnd();
1513
+ // Compute sha256 hash
1514
+ const hash = createHash('sha256').update(normalized, 'utf8').digest('hex');
1515
+ const hashTrailer = `<!-- pb-hash: sha256:${hash} -->`;
1516
+ // Append hash trailer to the projected body
1517
+ resolvedAsset.body = `${normalized}\n${hashTrailer}`;
1518
+ dbProjectionHashes.set(rawAsset.entryId, {
1519
+ hash: `sha256:${hash}`,
1520
+ assetKind: rawAsset.assetKind,
1521
+ });
1522
+ // Drift detection: compare against last known hash
1523
+ if (rawAsset.lastProjectedHash && rawAsset.lastProjectedHash !== `sha256:${hash}`) {
1524
+ trackEvent('skill.drift.detected', {
1525
+ entryId: rawAsset.entryId,
1526
+ assetKind: rawAsset.assetKind,
1527
+ });
1528
+ log(`Drift detected for asset ${rawAsset.entryId} — projecting updated version.`);
1529
+ }
1530
+ trackEvent('skill.projection.succeeded', {
1531
+ entryId: rawAsset.entryId,
1532
+ assetKind: rawAsset.assetKind,
1533
+ });
1534
+ }
1535
+ catch (err) {
1536
+ trackEvent('skill.projection.failed', {
1537
+ entryId: rawAsset.entryId,
1538
+ assetKind: rawAsset.assetKind,
1539
+ });
1540
+ logErr(`Warning: projection failed for ${rawAsset.entryId} — ${err instanceof Error ? err.message : String(err)}`);
1541
+ }
1542
+ }
1543
+ }
355
1544
  // 5a. Optionally fetch and merge Chain-derived rules (--generate flag)
356
1545
  let chainRulesStats = null;
357
1546
  let chainGaps = [];
358
1547
  let allRules = manualRules;
359
1548
  if (generate) {
360
1549
  log('Generating Chain-derived rules...');
361
- const chainResult = await generateChainRules(mcpCall, manualRules);
1550
+ const chainResult = await generateChainRules(kernelCall, manualRules);
362
1551
  if (chainResult.sentinel) {
363
1552
  // MCP unavailable — inject sentinel rule and warn
364
1553
  allRules = [...manualRules, chainResult.sentinel];
@@ -383,12 +1572,51 @@ export async function runHandshake(options = {}) {
383
1572
  log(diffText);
384
1573
  }
385
1574
  }
386
- // Apply level filtering with stage-gating (after read, before target filtering in write loop)
1575
+ // 5b. Read personal layer (WP-310 E2) machine-local rules/skills from .productbrain/.local/
1576
+ // Returns [] when the directory is absent. All returned entries have persist: 'local'.
1577
+ const personalRules = readPersonalLayer(pbDir);
1578
+ if (personalRules.length > 0) {
1579
+ // Name collision detection — warn when a personal rule overrides a team rule
1580
+ const teamRuleNames = new Set(allRules.map((r) => r.name));
1581
+ for (const pr of personalRules) {
1582
+ if (teamRuleNames.has(pr.name)) {
1583
+ logErr(`Personal rule "${pr.name}" overrides team rule (local takes precedence).`);
1584
+ }
1585
+ }
1586
+ // Merge: team rules first, personal rules appended (personal takes precedence via name collision above)
1587
+ allRules = [...allRules, ...personalRules];
1588
+ }
1589
+ const personalSkills = readPersonalSkillsLayer(pbDir);
1590
+ if (personalSkills.length > 0) {
1591
+ const teamSkillNames = new Set(allSkills.map((s) => s.name));
1592
+ for (const ps of personalSkills) {
1593
+ if (teamSkillNames.has(ps.name)) {
1594
+ logErr(`Personal skill "${ps.name}" overrides team skill (local takes precedence).`);
1595
+ }
1596
+ }
1597
+ allSkills.push(...personalSkills);
1598
+ }
1599
+ // 5d. Load method registry (WP-310 E4) — only when manifest is present.
1600
+ let registrySource;
1601
+ let registryStale;
1602
+ if (manifest) {
1603
+ const registryResult = await loadMethodRegistry().catch(() => null);
1604
+ if (registryResult) {
1605
+ registrySource = registryResult.source;
1606
+ registryStale = registryResult.stale;
1607
+ }
1608
+ }
1609
+ const adoptionFilteredSkills = filterByAdoptionState(allSkills, manifest);
1610
+ const adoptionFilteredRules = filterByAdoptionState(allRules, manifest);
1611
+ // Compute adoption counts for report (only meaningful when manifest is present)
1612
+ const adoptedRulesCount = manifest ? adoptionFilteredRules.length : undefined;
1613
+ const rejectedRulesCount = manifest ? allRules.length - adoptionFilteredRules.length : undefined;
1614
+ // Apply level filtering with stage-gating (after adoption filter, before target filtering in write loop)
387
1615
  // Stage caps the effective level: blank→beginner, seed→intermediate, grounded+→expert.
388
1616
  // If stage caps below the requested level, log it so the user knows why items were dropped.
389
1617
  const profileStage = workspaceProfile?.stage;
390
- const levelFilteredSkills = filterByLevel(allSkills, level, profileStage);
391
- const levelFilteredRules = filterByLevel(allRules, level, profileStage);
1618
+ const levelFilteredSkills = filterByLevel(adoptionFilteredSkills, level, profileStage);
1619
+ const levelFilteredRules = filterByLevel(adoptionFilteredRules, level, profileStage);
392
1620
  // Log when stage gating changes the effective level
393
1621
  if (profileStage) {
394
1622
  const stageCap = STAGE_TO_MAX_LEVEL[profileStage];
@@ -434,12 +1662,13 @@ export async function runHandshake(options = {}) {
434
1662
  const agentsWorkspaceContext = workspaceProfile
435
1663
  ? {
436
1664
  stage: workspaceProfile.stage,
437
- focus: orientView?.strategicContext?.currentBet ?? undefined,
1665
+ focus: orientView?.strategicContext?.currentWorkPackage ?? undefined,
438
1666
  governanceMode: workspaceProfile.governanceMode,
439
1667
  totalEntries: workspaceProfile.totalEntries,
440
1668
  }
441
1669
  : undefined;
442
- // Collect codex-targeted skills for AGENTS.md skill directory
1670
+ // Collect codex-targeted skills for AGENTS.md skill directory.
1671
+ // persist: 'local' entries are included; all projections are now local-only.
443
1672
  const agentsCodexSkills = canonicalSkills
444
1673
  .filter((s) => shouldEmitToTarget(s, 'codex'))
445
1674
  .map((s) => ({
@@ -469,7 +1698,7 @@ export async function runHandshake(options = {}) {
469
1698
  skills: copilotSkills.length > 0 ? copilotSkills : undefined,
470
1699
  rules: copilotRules.length > 0 ? copilotRules : undefined,
471
1700
  };
472
- const contextContent = orientView ? generateContextMd(orientView, repo, timestamp) : null;
1701
+ const contextContent = orientView ? generateContextMd(orientView, repo, timestamp, workspaceProfile?.stage) : null;
473
1702
  const briefingContent = generateBriefingMd(matchedEntries, repo, uniqueQueries, timestamp);
474
1703
  const agentsContent = generateAgentsMd(timestamp, {
475
1704
  workspaceContext: agentsWorkspaceContext,
@@ -478,22 +1707,168 @@ export async function runHandshake(options = {}) {
478
1707
  const claudeContent = generateClaudeMd(timestamp);
479
1708
  const cursorContent = generateCursorMdc(timestamp);
480
1709
  const copilotContent = generateCopilotMd(timestamp, copilotOptions);
1710
+ // DEC-1343: fan out one committed, path-scoped .github/instructions/<slug>.instructions.md per
1711
+ // copilot-targeted rule that declares an applyTo glob. Carries the full rule body (review-grade)
1712
+ // so the pb-blind Copilot reviewer is self-contained. Repo-wide rules (no applyTo) stay in
1713
+ // copilot-instructions.md above.
1714
+ const copilotInstructionFiles = generateCopilotInstructionFiles(canonicalRules
1715
+ .filter((r) => shouldEmitToTarget(r, 'copilot'))
1716
+ // `.github/instructions/*.instructions.md` is a COMMITTED surface — never project a
1717
+ // machine-local (`persist: 'local'`, from .productbrain/.local/rules/) rule into it, or a
1718
+ // developer's personal guidance could surface as an unignored repo file and be committed by
1719
+ // accident (personal == never committed; see the trust-level note above). Team rules only.
1720
+ .filter((r) => r.persist !== 'local')
1721
+ .map((r) => ({ name: r.name, body: r.body, applyTo: r.applyTo, sourcePath: r.sourcePath })), timestamp);
1722
+ const boundaryEnforcementMode = getBoundaryEnforcementMode(manifest);
1723
+ const boundaryManifestContent = boundaryEnforcementMode === 'advisory'
1724
+ ? null
1725
+ : generateBoundaryManifest(pbDir);
481
1726
  // 7. Write files
482
1727
  const filesWritten = [];
483
1728
  const filesSkipped = [];
1729
+ const previewPlan = [];
1730
+ if (writeMode && usedDbSource) {
1731
+ const authoringSyncState = loadAuthoringSyncState(pbDir);
1732
+ let authoringSyncStateChanged = false;
1733
+ const personalAssets = dbAssetRows.filter((asset) => asset.scope === 'personal' &&
1734
+ !asset.disabledByOwner &&
1735
+ (asset.assetKind === 'skill' || asset.assetKind === 'rule' || asset.assetKind === 'hook'));
1736
+ for (const asset of personalAssets) {
1737
+ // WP-428 S2 (Finding #5): skip projection for assets whose body fetch failed.
1738
+ // Do NOT write the authoring file with empty body, do NOT update lastProjectedHash.
1739
+ if (bodyFetchFailedEntryIds.has(asset.entryId))
1740
+ continue;
1741
+ const authoringPath = setupAuthoringPath(cwd, asset);
1742
+ if (!authoringPath)
1743
+ continue;
1744
+ // Review: use relative() for correct cross-platform path computation (string-replace
1745
+ // mis-fires when cwd uses a non-'/' separator), matching the dormant passes below.
1746
+ const relativeAuthoringPath = relative(cwd, authoringPath);
1747
+ const bodyHash = setupAuthoringAssetHash(asset);
1748
+ const authoringExists = existsSync(authoringPath);
1749
+ const tracked = authoringSyncState.assets[asset.entryId];
1750
+ const trackedHere = tracked?.path === relativeAuthoringPath;
1751
+ const trackedMatchesServer = Boolean(trackedHere && asset.lastProjectedHash && tracked?.hash === asset.lastProjectedHash);
1752
+ if (!authoringExists && asset.lastProjectedHash && trackedHere && trackedMatchesServer) {
1753
+ try {
1754
+ const dormantResult = await kernelCall('setup.markPersonalSetupAssetDormantFromSync', { entryId: asset.entryId, expectedLastProjectedHash: tracked.hash });
1755
+ if (dormantResult.action === 'conflict') {
1756
+ syncDriftTensToFire.push(`Repo authoring deletion skipped for ${asset.entryId}; server baseline changed during sync, so writeback was deferred until the next DB read.`);
1757
+ logErr(`Warning: authoring deletion for ${asset.entryId} was not applied because the DB baseline changed during sync.`);
1758
+ deferredAuthoringBaselineEntryIds.add(asset.entryId);
1759
+ continue;
1760
+ }
1761
+ else {
1762
+ syncDriftTensToFire.push(`Repo authoring file was deleted for ${asset.entryId}; caller-owned personal setup asset marked dormant.`);
1763
+ log(`Setup authoring deletion: ${asset.entryId} marked dormant.`);
1764
+ delete authoringSyncState.assets[asset.entryId];
1765
+ authoringSyncStateChanged = true;
1766
+ continue;
1767
+ }
1768
+ }
1769
+ catch (err) {
1770
+ logErr(`Warning: could not mark ${asset.entryId} dormant after authoring deletion — ${err instanceof Error ? err.message : String(err)}`);
1771
+ continue;
1772
+ }
1773
+ }
1774
+ else if (!authoringExists && asset.lastProjectedHash && trackedHere) {
1775
+ syncDriftTensToFire.push(`Repo authoring deletion skipped for ${asset.entryId}; local sync baseline is stale, so DB writeback wins.`);
1776
+ logErr(`Warning: authoring deletion for ${asset.entryId} was not applied because the DB baseline changed.`);
1777
+ }
1778
+ const nextAuthoringContent = renderSetupAuthoringFile(asset);
1779
+ if (authoringExists) {
1780
+ const parsed = parseSetupAuthoringFrontmatter(readFileSync(authoringPath, 'utf8'));
1781
+ const fileHash = setupAuthoringAssetHash({
1782
+ entryId: parsed.frontmatterId?.trim() || asset.entryId,
1783
+ name: parsed.name || asset.name,
1784
+ description: parsed.description,
1785
+ body: parsed.body,
1786
+ assetKind: asset.assetKind,
1787
+ triggers: parsed.triggers,
1788
+ semanticRefs: parsed.semanticRefs,
1789
+ });
1790
+ if (fileHash === bodyHash) {
1791
+ if (asset.lastProjectedHash !== bodyHash) {
1792
+ await kernelCall('setup.updateLastProjectedHash', {
1793
+ entryId: asset.entryId,
1794
+ hash: bodyHash,
1795
+ }).catch(() => null);
1796
+ }
1797
+ authoringSyncState.assets[asset.entryId] = { path: relativeAuthoringPath, hash: bodyHash };
1798
+ authoringSyncStateChanged = true;
1799
+ continue;
1800
+ }
1801
+ if (!asset.lastProjectedHash) {
1802
+ syncDriftTensToFire.push(`Repo authoring file differs from DB for ${asset.entryId} at ${authoringPath} with no shared baseline; DB writeback skipped.`);
1803
+ logErr(`Warning: setup authoring drift for ${asset.entryId}; DB writeback skipped because no shared baseline exists.`);
1804
+ continue;
1805
+ }
1806
+ if (fileHash !== asset.lastProjectedHash &&
1807
+ bodyHash !== asset.lastProjectedHash) {
1808
+ syncDriftTensToFire.push(`Repo authoring file won setup sync conflict for ${asset.entryId} at ${authoringPath}.`);
1809
+ logErr(`Warning: repo authoring file won sync conflict for ${asset.entryId}; DB writeback skipped.`);
1810
+ continue;
1811
+ }
1812
+ if (fileHash !== asset.lastProjectedHash) {
1813
+ syncDriftTensToFire.push(`Repo authoring file differs from DB baseline for ${asset.entryId} at ${authoringPath}; DB writeback skipped.`);
1814
+ logErr(`Warning: setup authoring drift for ${asset.entryId}; DB writeback skipped.`);
1815
+ continue;
1816
+ }
1817
+ }
1818
+ try {
1819
+ assertSetupWritePath(authoringPath, perimeterManifest);
1820
+ mkdirSync(dirname(authoringPath), { recursive: true });
1821
+ writeFileSync(authoringPath, nextAuthoringContent);
1822
+ filesWritten.push(relativeAuthoringPath);
1823
+ await kernelCall('setup.updateLastProjectedHash', {
1824
+ entryId: asset.entryId,
1825
+ hash: bodyHash,
1826
+ }).catch(() => null);
1827
+ authoringSyncState.assets[asset.entryId] = { path: relativeAuthoringPath, hash: bodyHash };
1828
+ authoringSyncStateChanged = true;
1829
+ }
1830
+ catch (err) {
1831
+ logErr(`Warning: could not write setup authoring file for ${asset.entryId} — ${err instanceof Error ? err.message : String(err)}`);
1832
+ }
1833
+ }
1834
+ if (authoringSyncStateChanged) {
1835
+ try {
1836
+ saveAuthoringSyncState(pbDir, authoringSyncState);
1837
+ }
1838
+ catch (err) {
1839
+ logErr(`Warning: could not persist setup authoring sync state — ${err instanceof Error ? err.message : String(err)}`);
1840
+ }
1841
+ }
1842
+ }
484
1843
  const writes = [
485
1844
  ...(contextContent ? [{ path: join(cwd, '.productbrain', 'context.md'), relative: '.productbrain/context.md', content: contextContent, dirs: join(cwd, '.productbrain'), isAdapter: false }] : []),
486
1845
  { path: join(cwd, '.productbrain', 'briefing.md'), relative: '.productbrain/briefing.md', content: briefingContent, isAdapter: false },
487
- { path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true },
488
- { path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true },
489
- { path: join(cwd, '.cursor', 'rules', 'chain.mdc'), relative: '.cursor/rules/chain.mdc', content: cursorContent, dirs: join(cwd, '.cursor', 'rules'), isAdapter: true },
490
- { path: join(cwd, '.github', 'copilot-instructions.md'), relative: '.github/copilot-instructions.md', content: copilotContent, dirs: join(cwd, '.github'), isAdapter: true },
1846
+ { path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true, target: 'codex', augmentTarget: true },
1847
+ { path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true, target: 'claude', augmentTarget: true },
1848
+ { path: join(cwd, '.cursor', 'rules', 'chain.mdc'), relative: '.cursor/rules/chain.mdc', content: cursorContent, dirs: join(cwd, '.cursor', 'rules'), isAdapter: true, target: 'cursor' },
1849
+ { path: join(cwd, '.github', 'copilot-instructions.md'), relative: '.github/copilot-instructions.md', content: copilotContent, dirs: join(cwd, '.github'), isAdapter: true, target: 'copilot' },
1850
+ ...copilotInstructionFiles.map((f) => ({
1851
+ path: join(cwd, f.relativePath),
1852
+ relative: f.relativePath,
1853
+ content: f.content,
1854
+ dirs: join(cwd, '.github', 'instructions'),
1855
+ isAdapter: true,
1856
+ target: 'copilot',
1857
+ })),
1858
+ ...(boundaryManifestContent ? [{
1859
+ path: join(cwd, '.productbrain', 'generated', 'boundaries.json'),
1860
+ relative: '.productbrain/generated/boundaries.json',
1861
+ content: boundaryManifestContent,
1862
+ dirs: join(cwd, '.productbrain', 'generated'),
1863
+ isAdapter: false,
1864
+ }] : []),
491
1865
  ];
492
1866
  // Add Cursor skill copies (filtered by target)
493
1867
  const cursorProfile = resolveSurfaceProfile('cursor');
494
1868
  for (const skill of canonicalSkills) {
495
1869
  if (!shouldEmitToTarget(skill, 'cursor'))
496
1870
  continue;
1871
+ const dbAssetEntryId = skill.sourcePath.startsWith('db:') ? skill.sourcePath.slice(3) : undefined;
497
1872
  const skillDir = join(cwd, '.cursor', 'skills', skill.name);
498
1873
  writes.push({
499
1874
  path: join(skillDir, 'SKILL.md'),
@@ -501,18 +1876,23 @@ export async function runHandshake(options = {}) {
501
1876
  content: generateCursorSkill(skill, cursorProfile),
502
1877
  dirs: skillDir,
503
1878
  isAdapter: true,
1879
+ target: 'cursor',
1880
+ dbAssetEntryId,
504
1881
  });
505
1882
  }
506
1883
  // Add Codex skill copies (projected markdown + index)
507
1884
  const codexProfile = resolveSurfaceProfile('codex');
508
1885
  const codexSkills = canonicalSkills.filter((s) => shouldEmitToTarget(s, 'codex'));
509
1886
  for (const skill of codexSkills) {
1887
+ const dbAssetEntryId = skill.sourcePath.startsWith('db:') ? skill.sourcePath.slice(3) : undefined;
510
1888
  writes.push({
511
1889
  path: join(cwd, '.codex', 'skills', `${skill.name}.md`),
512
1890
  relative: `.codex/skills/${skill.name}.md`,
513
1891
  content: generateCodexSkill(skill, codexProfile),
514
1892
  dirs: join(cwd, '.codex', 'skills'),
515
1893
  isAdapter: true,
1894
+ target: 'codex',
1895
+ dbAssetEntryId,
516
1896
  });
517
1897
  }
518
1898
  writes.push({
@@ -521,6 +1901,7 @@ export async function runHandshake(options = {}) {
521
1901
  content: generateCodexSkillIndex(codexSkills),
522
1902
  dirs: join(cwd, '.codex', 'skills'),
523
1903
  isAdapter: true,
1904
+ target: 'codex',
524
1905
  });
525
1906
  // Validate Codex-projected skills for dead references
526
1907
  const codexWarnings = validateCodexSkills(codexSkills);
@@ -528,12 +1909,15 @@ export async function runHandshake(options = {}) {
528
1909
  for (const rule of canonicalRules) {
529
1910
  if (!shouldEmitToTarget(rule, 'cursor'))
530
1911
  continue;
1912
+ const dbAssetEntryId = rule.sourcePath.startsWith('db:') ? rule.sourcePath.slice(3) : undefined;
531
1913
  writes.push({
532
1914
  path: join(cwd, '.cursor', 'rules', `${rule.name}.mdc`),
533
1915
  relative: `.cursor/rules/${rule.name}.mdc`,
534
1916
  content: generateCursorRule(rule, cursorProfile),
535
1917
  dirs: join(cwd, '.cursor', 'rules'),
536
1918
  isAdapter: true,
1919
+ target: 'cursor',
1920
+ dbAssetEntryId,
537
1921
  });
538
1922
  }
539
1923
  // Add Claude Code rule copies (filtered by target)
@@ -541,12 +1925,15 @@ export async function runHandshake(options = {}) {
541
1925
  for (const rule of canonicalRules) {
542
1926
  if (!shouldEmitToTarget(rule, 'claude'))
543
1927
  continue;
1928
+ const dbAssetEntryId = rule.sourcePath.startsWith('db:') ? rule.sourcePath.slice(3) : undefined;
544
1929
  writes.push({
545
1930
  path: join(cwd, '.claude', 'rules', `${rule.name}.md`),
546
1931
  relative: `.claude/rules/${rule.name}.md`,
547
1932
  content: generateClaudeRule(rule, claudeProfile),
548
1933
  dirs: join(cwd, '.claude', 'rules'),
549
1934
  isAdapter: true,
1935
+ target: 'claude',
1936
+ dbAssetEntryId,
550
1937
  });
551
1938
  }
552
1939
  // Add Claude Code skill router (filtered by target)
@@ -559,32 +1946,683 @@ export async function runHandshake(options = {}) {
559
1946
  content: skillRouterContent,
560
1947
  dirs: join(cwd, '.claude', 'rules'),
561
1948
  isAdapter: true,
1949
+ target: 'claude',
562
1950
  });
563
1951
  }
1952
+ // 7a. WP-379 S5b: Resolve projection collisions before writing.
1953
+ // In apply mode, enumerate target dirs and unlink any auto-generated files
1954
+ // whose normalized name no longer matches any active asset from the server.
1955
+ // This prevents case-variant orphans from accumulating across handshakes.
1956
+ // Runs only when we have a DB asset list (usedDbSource) — without a DB source,
1957
+ // we can't determine which files are canonical vs. orphan.
1958
+ const collisionTensToFire = [];
1959
+ if (writeMode && usedDbSource) {
1960
+ const activeAssetNames = dbAssetRows
1961
+ .filter((a) => !a.disabledByOwner)
1962
+ .map((a) => a.name);
1963
+ const { collisionTens } = resolveProjectionCollision(cwd, activeAssetNames, log, logErr);
1964
+ collisionTensToFire.push(...collisionTens);
1965
+ }
1966
+ // ── WP-436 S3: Vocab projector — fetch workspace vocab context once per handshake ──
1967
+ // Source-side (.productbrain/skills/*.md + rules/*.md) stays tokenized.
1968
+ // The projector resolves {{vocab:...}} tokens before writing adapter output to disk
1969
+ // (.cursor/rules/, .claude/rules/, CLAUDE.md, AGENTS.md, .github/copilot-instructions.md).
1970
+ // Fail-open: if vocab fetch fails, skip resolution and write raw token (no breakage).
1971
+ const handshakeVocabCtx = await getOrFetchVocabCtx(config.apiKey, async () => {
1972
+ try {
1973
+ const vocab = await kernelCall('chain.getVocabulary', {});
1974
+ if (vocab?.collectionLabels || vocab?.collectionDefaults) {
1975
+ return {
1976
+ ...(vocab.collectionLabels ? { collectionLabels: vocab.collectionLabels } : {}),
1977
+ ...(vocab.collectionDefaults ? { collectionDefaults: vocab.collectionDefaults } : {}),
1978
+ };
1979
+ }
1980
+ return null;
1981
+ }
1982
+ catch {
1983
+ return null; // fail-open
1984
+ }
1985
+ });
1986
+ const userOwnedSkipped = [];
1987
+ const projectedHashUpdates = new Map();
1988
+ const cleanBucketPaths = [];
1989
+ const tamperedBucket = [];
1990
+ const recordProjectedHash = (entryId) => {
1991
+ if (!applyMode || !entryId)
1992
+ return;
1993
+ if (deferredAuthoringBaselineEntryIds.has(entryId))
1994
+ return;
1995
+ const projection = dbProjectionHashes.get(entryId);
1996
+ if (projection)
1997
+ projectedHashUpdates.set(entryId, projection.hash);
1998
+ };
1999
+ // TEN-2155: region augmentation context + symlink dedupe.
2000
+ // codexActive gates the AGENTS.md skills-index pointer (region-projections.ts).
2001
+ const regionCtx = { codexActive: allowedTargets.has('codex') };
2002
+ const malformedRegionPaths = [];
2003
+ // If two augment targets resolve to the same inode (e.g. CLAUDE.md symlinked to AGENTS.md),
2004
+ // augment only the first-seen (AGENTS.md precedes CLAUDE.md in `writes`) to avoid double-injection.
2005
+ const seenAugmentRealpaths = new Set();
2006
+ const augmentTargetSkip = new Set();
564
2007
  for (const w of writes) {
2008
+ if (!w.augmentTarget || !w.target || !allowedTargets.has(w.target) || !existsSync(w.path))
2009
+ continue;
2010
+ let real;
2011
+ try {
2012
+ real = realpathSync(w.path);
2013
+ }
2014
+ catch {
2015
+ real = w.path;
2016
+ }
2017
+ if (seenAugmentRealpaths.has(real))
2018
+ augmentTargetSkip.add(w.path);
2019
+ else
2020
+ seenAugmentRealpaths.add(real);
2021
+ }
2022
+ for (const w of writes) {
2023
+ // Surface filtering: skip adapter writes for targets not in the allowed set
2024
+ if (w.target && !allowedTargets.has(w.target)) {
2025
+ filesSkipped.push({ path: w.relative, reason: `filtered (surface: ${w.target})` });
2026
+ if (preview)
2027
+ previewPlan.push({ path: w.relative, status: 'filtered' });
2028
+ continue;
2029
+ }
2030
+ // ── TEN-2155: augment user-owned CLAUDE.md / AGENTS.md with a marked PB region ──
2031
+ // Runs ahead of the legacy MARKER-keyed gates. Only existing augmentable/region-present
2032
+ // files are spliced here; absent + legacy-pb-managed fall through to the legacy path.
2033
+ if (w.augmentTarget) {
2034
+ const projection = REGION_PROJECTIONS[w.target];
2035
+ if (projection && existsSync(w.path)) {
2036
+ if (augmentTargetSkip.has(w.path)) {
2037
+ filesSkipped.push({ path: w.relative, reason: 'symlinked to another augment target — augmented once' });
2038
+ continue;
2039
+ }
2040
+ const disk = readFileSync(w.path, 'utf8');
2041
+ const cls = classifyAdapterFile(disk);
2042
+ if (cls === 'augmentable' || cls === 'region-present') {
2043
+ const eol = detectEol(disk);
2044
+ const region = replaceVocabTokens(projection.build(regionCtx, eol), handshakeVocabCtx);
2045
+ // Defense-in-depth: the composed region (post vocab-token resolution) must itself be a
2046
+ // single well-formed region. v1 content has no vocab tokens so this is a no-op, but it
2047
+ // guards the documented future override/vocab seam from injecting a stray sentinel/MARKER.
2048
+ if (classifyAdapterFile(region) !== 'region-present') {
2049
+ filesSkipped.push({ path: w.relative, reason: 'internal: composed PB region malformed — skipped to protect your file' });
2050
+ if (preview)
2051
+ previewPlan.push({ path: w.relative, status: 'needs-attention' });
2052
+ continue;
2053
+ }
2054
+ const candidate = cls === 'augmentable' ? spliceAppend(disk, region, eol) : spliceReplace(disk, region);
2055
+ if (candidate === disk) {
2056
+ filesSkipped.push({ path: w.relative, reason: 'unchanged' });
2057
+ if (preview)
2058
+ previewPlan.push({ path: w.relative, status: 'unchanged' });
2059
+ continue;
2060
+ }
2061
+ if (preview || dryRun) {
2062
+ filesWritten.push(w.relative + (dryRun ? ' (dry run)' : ''));
2063
+ if (preview)
2064
+ previewPlan.push({ path: w.relative, status: 'would-augment' });
2065
+ continue;
2066
+ }
2067
+ if (authorityPreviewOnly) {
2068
+ // apply + materialize observe/off: honor authority — do NOT write; mirror the legacy
2069
+ // preview-only skip so the report does not falsely list it as written.
2070
+ filesSkipped.push({ path: w.relative, reason: `preview-only (materialize: ${manifestStatus.mode})` });
2071
+ continue;
2072
+ }
2073
+ assertSetupWritePath(w.path, perimeterManifest);
2074
+ writeFileSync(w.path, candidate);
2075
+ filesWritten.push(w.relative);
2076
+ continue;
2077
+ }
2078
+ if (cls === 'malformed') {
2079
+ filesSkipped.push({ path: w.relative, reason: 'malformed PB region — left untouched; fix the pb:region sentinels' });
2080
+ if (preview)
2081
+ previewPlan.push({ path: w.relative, status: 'needs-attention' });
2082
+ malformedRegionPaths.push(w.relative);
2083
+ continue;
2084
+ }
2085
+ // cls 'pb-managed' → fall through to the legacy whole-file re-projection path below.
2086
+ // cls 'opt-out' (file carries the pb:no-augment sentinel — e.g. this repo's committed
2087
+ // constitution) → fall through too, landing on the legacy user-owned skip: left untouched,
2088
+ // never spliced. This is how a file declines augmentation without losing its user-owned status.
2089
+ }
2090
+ // absent file → fall through to legacy whole-file CREATE.
2091
+ }
565
2092
  if (w.isAdapter && !shouldWriteAdapter(w.path, force)) {
566
- filesSkipped.push({ path: w.relative, reason: 'exists without auto-generated marker (use --force to overwrite)' });
2093
+ // User-owned: a file at an adapter path without our auto-gen MARKER is the
2094
+ // user's own file. Leave it untouched — never overwrite, never treat as drift
2095
+ // or log a TEN (TEN-2150). Relayed to the connect screen via userOwnedSkipped.
2096
+ filesSkipped.push({ path: w.relative, reason: 'user-owned — left untouched (pb won\'t overwrite your file)' });
2097
+ if (preview) {
2098
+ previewPlan.push({ path: w.relative, status: 'user-owned' });
2099
+ }
2100
+ else {
2101
+ userOwnedSkipped.push(w.relative);
2102
+ }
2103
+ continue;
2104
+ }
2105
+ if (authorityPreviewOnly) {
2106
+ filesSkipped.push({
2107
+ path: w.relative,
2108
+ reason: `preview-only (materialize: ${manifestStatus.mode})`,
2109
+ });
567
2110
  continue;
568
2111
  }
569
- if (dryRun) {
570
- filesWritten.push(w.relative + ' (dry run)');
2112
+ if (preview || dryRun) {
2113
+ // In preview/dry-run mode: check content to distinguish new/update/unchanged
2114
+ if (existsSync(w.path)) {
2115
+ const current = readFileSync(w.path, 'utf8');
2116
+ const nextNormalized = normalizeHandshakeContentForComparison(w.content);
2117
+ const currentNormalized = normalizeHandshakeContentForComparison(current);
2118
+ if (nextNormalized === currentNormalized) {
2119
+ filesSkipped.push({ path: w.relative, reason: 'unchanged' });
2120
+ if (preview)
2121
+ previewPlan.push({ path: w.relative, status: 'unchanged' });
2122
+ }
2123
+ else {
2124
+ filesWritten.push(w.relative + (dryRun ? ' (dry run)' : ''));
2125
+ if (preview)
2126
+ previewPlan.push({ path: w.relative, status: 'would-update' });
2127
+ }
2128
+ }
2129
+ else {
2130
+ filesWritten.push(w.relative + (dryRun ? ' (dry run)' : ''));
2131
+ if (preview)
2132
+ previewPlan.push({ path: w.relative, status: 'would-write' });
2133
+ }
571
2134
  continue;
572
2135
  }
2136
+ // ── WP-421 S3: defer tampered adapter writes (doneWhen #17) ──────────────
2137
+ // For adapter projections that already exist on disk, classify into
2138
+ // pb-managed-clean / pb-managed-tampered. Tampered = MARKER present +
2139
+ // hash trailer mismatches body. Defer (do NOT overwrite); post-loop
2140
+ // resolution handles them. --force bypasses the tampered defer (legacy
2141
+ // semantic: explicit user opt-in to overwrite).
2142
+ if (w.isAdapter && !force) {
2143
+ const drift = classifyDriftBucket(w.path);
2144
+ if (drift && drift.bucket === 'pb-managed-tampered') {
2145
+ tamperedBucket.push({
2146
+ path: w.path,
2147
+ relative: w.relative,
2148
+ content: w.content,
2149
+ expectedHash: drift.expectedHash,
2150
+ actualHash: drift.actualHash,
2151
+ dirs: w.dirs,
2152
+ dbAssetEntryId: w.dbAssetEntryId,
2153
+ });
2154
+ // Do NOT write — defer until prompt/refusal resolves the file.
2155
+ continue;
2156
+ }
2157
+ if (drift && drift.bucket === 'pb-managed-clean') {
2158
+ cleanBucketPaths.push(w.relative);
2159
+ }
2160
+ }
2161
+ assertSetupWritePath(w.path, perimeterManifest);
573
2162
  if (w.dirs)
574
2163
  mkdirSync(w.dirs, { recursive: true });
2164
+ // WP-436 S3: resolve {{vocab:...}} tokens before writing projected adapter files.
2165
+ // Source-side (.productbrain/skills/*.md, rules/*.md) stays tokenized.
2166
+ // Only adapter projections (cursor/rules, claude/rules, CLAUDE.md, AGENTS.md, etc.) get resolved.
2167
+ // Fail-open: if vocabCtx is undefined, replaceVocabTokens falls back to canonicalKey literals.
2168
+ const resolvedContent = w.isAdapter
2169
+ ? replaceVocabTokens(w.content, handshakeVocabCtx)
2170
+ : w.content;
575
2171
  if (existsSync(w.path)) {
576
2172
  const current = readFileSync(w.path, 'utf8');
577
- const nextNormalized = normalizeHandshakeContentForComparison(w.content);
2173
+ const nextNormalized = normalizeHandshakeContentForComparison(resolvedContent);
578
2174
  const currentNormalized = normalizeHandshakeContentForComparison(current);
579
2175
  if (nextNormalized === currentNormalized) {
580
2176
  filesSkipped.push({ path: w.relative, reason: 'unchanged' });
2177
+ recordProjectedHash(w.dbAssetEntryId);
581
2178
  continue;
582
2179
  }
583
2180
  }
584
- writeFileSync(w.path, w.content);
2181
+ writeFileSync(w.path, resolvedContent);
585
2182
  filesWritten.push(w.relative);
2183
+ recordProjectedHash(w.dbAssetEntryId);
2184
+ }
2185
+ if (projectedHashUpdates.size > 0) {
2186
+ const updates = [...projectedHashUpdates];
2187
+ const results = await Promise.allSettled(updates.map(([entryId, hash]) => kernelCall('setup.updateLastProjectedHash', { entryId, hash })));
2188
+ results.forEach((result, index) => {
2189
+ if (result.status === 'rejected') {
2190
+ const [entryId] = updates[index];
2191
+ trackEvent('skill.projection.failed', {
2192
+ entryId,
2193
+ reason: result.reason instanceof Error ? result.reason.message : String(result.reason),
2194
+ });
2195
+ }
2196
+ });
2197
+ }
2198
+ // Ordering note: this refusal runs AFTER the projected-hash flush so that files legitimately
2199
+ // written THIS run still record their hashes; malformed files were never written (no hash to record).
2200
+ // TEN-2155: in non-interactive apply, a malformed PB region is a refusal, not a silent skip.
2201
+ // Gated on applyMode (NOT writeMode): a malformed region is a user-file INTEGRITY fault, so the
2202
+ // refusal fires on any headless `--apply` regardless of materialize authority (STD-263 invariant vi —
2203
+ // "headless → non-zero refusal", unqualified). writeMode would suppress it under observe/off, where
2204
+ // the malformed file still gets enumerated but the corruption signal would be silently dropped.
2205
+ if (applyMode && malformedRegionPaths.length > 0 && (options.noPrompt || !process.stdout.isTTY)) {
2206
+ throw new CLIError(`Malformed PB region in: ${malformedRegionPaths.join(', ')}`, {
2207
+ code: ErrorCode.VALIDATION_FAILED,
2208
+ category: 'validation',
2209
+ guidance: 'Fix the `<!-- pb:region:start -->` / `<!-- pb:region:end -->` sentinels (one balanced pair), then re-run `pb handshake`.',
2210
+ });
2211
+ }
2212
+ // ── WP-421 S3: tampered-bucket resolution (doneWhen #17) ────────────────────
2213
+ // Apply mode only. Tampered files were DEFERRED in the write loop above;
2214
+ // here we either prompt the user (interactive TTY) or refuse (headless).
2215
+ //
2216
+ // Headless = `--no-prompt` flag OR `process.stdout.isTTY === false`. When
2217
+ // headless: enumerate each tampered file to stderr, write a setup_receipt
2218
+ // row with kind='transition' (DEC-962) capturing `refusedTamperedFiles[]`,
2219
+ // then exit non-zero. NEVER auto-resolve. (#17 + exclusions: edits to
2220
+ // projection dirs are detected, NOT silently overwritten.)
2221
+ //
2222
+ // Interactive: for each tampered file, present adopt-or-revert. Adopt =
2223
+ // create a personal-scoped setup_asset draft from the tampered content
2224
+ // (see helper below). Revert = re-project canonical content over the
2225
+ // tampered file (write w.content to w.path).
2226
+ const adoptedTamperedPaths = [];
2227
+ const revertedTamperedPaths = [];
2228
+ if (writeMode && tamperedBucket.length > 0) {
2229
+ const headless = options.noPrompt === true || !process.stdout.isTTY;
2230
+ if (headless) {
2231
+ // ── Headless refusal path (doneWhen #17) ────────────────────────────────
2232
+ logErr('');
2233
+ logErr(`pb handshake: ${tamperedBucket.length} PB-managed projection file(s) were edited downstream of the auto-gen marker.`);
2234
+ logErr('Headless mode (--no-prompt or no TTY) cannot resolve adopt-or-revert — refusing.');
2235
+ logErr('');
2236
+ const refusedTamperedFiles = tamperedBucket.map((t) => ({
2237
+ path: t.relative,
2238
+ expectedHash: t.expectedHash,
2239
+ actualHash: t.actualHash,
2240
+ }));
2241
+ for (const refused of refusedTamperedFiles) {
2242
+ // Per #17: stderr enumerates each tampered file as
2243
+ // {path, expectedHash, actualHash, bucket}.
2244
+ logErr(` ${JSON.stringify({ ...refused, bucket: 'pb-managed-tampered' })}`);
2245
+ }
2246
+ logErr('');
2247
+ logErr('Re-run interactively to resolve, or use --force to overwrite (data loss).');
2248
+ // Write the kind='transition' setup_receipt row. Fail-open on the
2249
+ // network/auth side: if the row cannot be written, we still exit non-zero
2250
+ // (the audit trail is best-effort; refusal is mandatory).
2251
+ try {
2252
+ const manifestStatus = readManifestStatus(pbDir);
2253
+ await kernelCall('setup.recordTamperRefusal', {
2254
+ mode: manifestStatus.mode,
2255
+ refusedTamperedFiles,
2256
+ });
2257
+ trackEvent('setup.transition.refused', {
2258
+ fileCount: refusedTamperedFiles.length,
2259
+ mode: manifestStatus.mode,
2260
+ });
2261
+ }
2262
+ catch (err) {
2263
+ trackEvent('setup.transition.refused.write_failed', {
2264
+ error: err instanceof Error ? err.message : String(err),
2265
+ });
2266
+ logErr(`Warning: could not record transition receipt — ${err instanceof Error ? err.message : String(err)}`);
2267
+ }
2268
+ // Surface the report counts before exit (visibility for CI logs).
2269
+ if (!quiet) {
2270
+ process.stdout.write('\n');
2271
+ process.stdout.write(formatHandshakeReport({
2272
+ filesWritten,
2273
+ filesSkipped,
2274
+ matchedEntries,
2275
+ searchQueries: uniqueQueries,
2276
+ repo,
2277
+ codexWarnings: codexWarnings.length > 0 ? codexWarnings : undefined,
2278
+ chainRulesStats: chainRulesStats ?? undefined,
2279
+ chainGaps: chainGaps.length > 0 ? chainGaps : undefined,
2280
+ adoptedCount: adoptedRulesCount,
2281
+ rejectedCount: rejectedRulesCount,
2282
+ personalRuleCount: personalRules.length > 0 ? personalRules.length : undefined,
2283
+ personalSkillCount: personalSkills.length > 0 ? personalSkills.length : undefined,
2284
+ registrySource,
2285
+ registryStale,
2286
+ userOwnedSkipped: userOwnedSkipped.length > 0 ? userOwnedSkipped : undefined,
2287
+ managedCleanCount: cleanBucketPaths.length || undefined,
2288
+ tamperedFiles: refusedTamperedFiles,
2289
+ }) + '\n');
2290
+ }
2291
+ // Exit non-zero per #17.
2292
+ process.exit(1);
2293
+ }
2294
+ // ── Interactive path: prompt adopt-or-revert per file ──────────────────────
2295
+ log('');
2296
+ log(`pb handshake: ${tamperedBucket.length} PB-managed projection file(s) were edited downstream of the auto-gen marker.`);
2297
+ log('You can ADOPT (capture your edits as a personal-scoped draft) or REVERT (overwrite with canonical content).');
2298
+ // Batch yes-to-all / no-to-all when the user has many tampered files.
2299
+ // Threshold: 5 (arbitrary; mirrors the typical handshake projection set).
2300
+ let batchChoice = null;
2301
+ if (tamperedBucket.length >= 5) {
2302
+ const useBatch = await promptConfirm({
2303
+ message: `Apply the same choice to all ${tamperedBucket.length} tampered files?`,
2304
+ initialValue: false,
2305
+ });
2306
+ if (useBatch) {
2307
+ const choice = await promptSelect({
2308
+ message: 'Apply to all:',
2309
+ options: [
2310
+ { value: 'adopt', label: 'Adopt all — capture each as a personal-scoped draft' },
2311
+ { value: 'revert', label: 'Revert all — overwrite with canonical content' },
2312
+ ],
2313
+ });
2314
+ batchChoice = choice === 'adopt' ? 'adopt-all' : 'revert-all';
2315
+ }
2316
+ }
2317
+ for (const tamper of tamperedBucket) {
2318
+ // Reverse-map the projection path back to the canonical authoring path.
2319
+ const reverse = canonicalPathForAnySurface(tamper.relative);
2320
+ const canonicalHint = reverse
2321
+ ? `Canonical authoring path: ${reverse.canonicalPath}`
2322
+ : (() => {
2323
+ // Telemetry + fallback message per surfaces/telemetry.ts.
2324
+ reportReverseMapMissing({ surface: 'unknown', projectionPath: tamper.relative }, logErr);
2325
+ return `Canonical authoring path: ${getReverseMapFallbackMessage()}`;
2326
+ })();
2327
+ log('');
2328
+ log(`Tampered: ${tamper.relative}`);
2329
+ log(` expected: ${tamper.expectedHash}`);
2330
+ log(` actual: ${tamper.actualHash}`);
2331
+ log(` ${canonicalHint}`);
2332
+ let action;
2333
+ if (batchChoice === 'adopt-all')
2334
+ action = 'adopt';
2335
+ else if (batchChoice === 'revert-all')
2336
+ action = 'revert';
2337
+ else {
2338
+ action = await promptSelect({
2339
+ message: 'Adopt or revert?',
2340
+ options: [
2341
+ { value: 'adopt', label: 'Adopt — capture this as a personal-scoped setup_asset draft' },
2342
+ { value: 'revert', label: 'Revert — overwrite with canonical content' },
2343
+ ],
2344
+ });
2345
+ }
2346
+ if (action === 'revert') {
2347
+ // Re-project canonical content over the tampered file.
2348
+ // WP-436 S3: resolve vocab tokens before writing (all tampered files are adapters).
2349
+ if (tamper.dirs)
2350
+ mkdirSync(tamper.dirs, { recursive: true });
2351
+ assertSetupWritePath(tamper.path, perimeterManifest);
2352
+ writeFileSync(tamper.path, replaceVocabTokens(tamper.content, handshakeVocabCtx));
2353
+ revertedTamperedPaths.push(tamper.relative);
2354
+ recordProjectedHash(tamper.dbAssetEntryId);
2355
+ trackEvent('setup.tampered.reverted', { path: tamper.relative });
2356
+ }
2357
+ else {
2358
+ // Adopt: capture the tampered content as a personal-scoped draft.
2359
+ // Per DEC-953 sync rules: personal scope = push (no fork required).
2360
+ // The mutation is best-effort — if the adopt write fails, the file is
2361
+ // kept on disk untouched and a warning is logged. Adopt does NOT
2362
+ // revert; it preserves the user's edits AND records them as a draft.
2363
+ const draftName = basename(tamper.relative).replace(/\.(md|mdc)$/, '') + ' (adopted)';
2364
+ try {
2365
+ const tamperedContent = readFileSync(tamper.path, 'utf8');
2366
+ const session = readSession();
2367
+ const caller = session ? kernelCallWithSession : kernelCall;
2368
+ await caller('setup.ingestSetupAsset', {
2369
+ entryId: `SETUP-ADOPTED-${Date.now()}-${draftName.replace(/\s+/g, '-')}`,
2370
+ name: draftName,
2371
+ // WP-421 S3
2372
+ description: `Adopted from tampered projection at ${tamper.relative}.`,
2373
+ body: tamperedContent,
2374
+ assetKind: tamper.relative.includes('/skills/') ? 'skill' : 'rule',
2375
+ triggers: [],
2376
+ semanticRefs: [],
2377
+ });
2378
+ adoptedTamperedPaths.push(tamper.relative);
2379
+ trackEvent('setup.tampered.adopted', { path: tamper.relative });
2380
+ }
2381
+ catch (err) {
2382
+ logErr(`Warning: could not adopt ${tamper.relative} as draft — ${err instanceof Error ? err.message : String(err)}`);
2383
+ trackEvent('setup.tampered.adopt_failed', {
2384
+ path: tamper.relative,
2385
+ error: err instanceof Error ? err.message : String(err),
2386
+ });
2387
+ }
2388
+ }
2389
+ }
2390
+ }
2391
+ // 8a. Dormant marker + .dormant rename (WP-379 S4 + WP-426 E4) — apply mode only.
2392
+ const dormantMarkedPaths = [];
2393
+ if (writeMode && dormantDbAssetRows.length > 0) {
2394
+ const dormantState = loadAuthoringSyncState(pbDir);
2395
+ let dormantStateChanged = false;
2396
+ for (const dormantAsset of dormantDbAssetRows) {
2397
+ for (const { path: filePath, surface } of deriveDormantFilePaths(dormantAsset, cwd)) {
2398
+ // Codex P2: deriveDormantFilePaths emits every known surface path. Honor the run's
2399
+ // target set (allowedTargets = the --surfaces selection, else all manifest surfaces)
2400
+ // so a `--surfaces cursor` run never renames .claude/.codex to .dormant, and surfaces
2401
+ // outside the manifest are skipped silently (no false "could not dormant-mark" warning).
2402
+ if (!allowedTargets.has(surface))
2403
+ continue;
2404
+ // FIX 4: use relative() instead of string-replace for correct cross-platform behaviour.
2405
+ const rel = relative(cwd, filePath);
2406
+ const alreadyReactivated = dormantState.dormantReactivated?.includes(rel) ?? false;
2407
+ const previouslyRenamed = dormantState.dormantRenamed?.includes(rel) ?? false;
2408
+ try {
2409
+ assertSetupWritePath(filePath, perimeterManifest);
2410
+ // WP-426 E4: BUG 1 fix — hands-off set.
2411
+ //
2412
+ // Step 1: permanently hands-off — user already reactivated this path on a
2413
+ // prior run. Skip silently every time until they re-lower or raise in PB.
2414
+ // (Task 7 raise-cleanup must later also prune dormantReactivated for raised
2415
+ // assets — see the `dormantReactivated` comment on AuthoringSyncState.)
2416
+ if (alreadyReactivated) {
2417
+ continue; // leave file untouched; no TEN (already fired on first detection)
2418
+ }
2419
+ // Step 2: FIRST detection of manual reactivation — we previously renamed
2420
+ // this to .dormant but the user renamed it back to a live file. Push the
2421
+ // drift TEN exactly once, add to the permanent hands-off set, remove from
2422
+ // dormantRenamed, leave file untouched.
2423
+ if (previouslyRenamed && existsSync(filePath) && !existsSync(`${filePath}.dormant`)) {
2424
+ syncDriftTensToFire.push(`Dormant asset ${dormantAsset.entryId} was manually reactivated at ${rel}; left untouched. Re-lower or raise it in PB to resync.`);
2425
+ logErr(`Warning: ${rel} was manually un-dormanted; leaving it in place (drift).`);
2426
+ dormantState.dormantReactivated = [...(dormantState.dormantReactivated ?? []), rel];
2427
+ dormantState.dormantRenamed = (dormantState.dormantRenamed ?? []).filter((p) => p !== rel);
2428
+ dormantStateChanged = true;
2429
+ continue;
2430
+ }
2431
+ // Step 3: normal lowering — write dormant marker + rename to .dormant.
2432
+ // WP-426 E4 spec: every rename TARGET must also pass the perimeter guard.
2433
+ // Check the post-rename .dormant path BEFORE any FS mutation, so a guard
2434
+ // failure can't leave a half-dormant file (marker appended but not renamed).
2435
+ assertSetupWritePath(`${filePath}.dormant`, perimeterManifest);
2436
+ const markerResult = writeDormantMarkerToFile(filePath);
2437
+ if (markerResult === 'written')
2438
+ log(`Dormant marker written: ${filePath}`);
2439
+ const renameResult = renameSurfaceForDormancy(filePath);
2440
+ if (renameResult === 'renamed' || renameResult === 'replaced') {
2441
+ // dormantMarkedPaths holds post-rename .dormant paths (not the original surface paths).
2442
+ dormantMarkedPaths.push(`${filePath}.dormant`);
2443
+ if (!dormantState.dormantRenamed?.includes(rel)) {
2444
+ dormantState.dormantRenamed = [...(dormantState.dormantRenamed ?? []), rel];
2445
+ dormantStateChanged = true;
2446
+ }
2447
+ log(`Dormant rename: ${filePath} → ${filePath}.dormant`);
2448
+ }
2449
+ else if (renameResult === 'drift') {
2450
+ // Codex P1: an edited .dormant already exists alongside the live file. Preserve
2451
+ // both and flag, rather than overwriting the user's edited dormant copy.
2452
+ syncDriftTensToFire.push(`Edited dormant copy ${rel}.dormant diverges from the live surface for ${dormantAsset.entryId}; left both in place. Resolve manually.`);
2453
+ logErr(`Warning: ${rel}.dormant diverges from the live file; not replacing (possible manual edit).`);
2454
+ }
2455
+ }
2456
+ catch (err) {
2457
+ logErr(`Warning: could not dormant-mark ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
2458
+ }
2459
+ }
2460
+ }
2461
+ if (dormantMarkedPaths.length > 0) {
2462
+ log('Run `pb setup observe --purge` to remove these instead of dormant-renaming.');
2463
+ }
2464
+ if (dormantStateChanged) {
2465
+ try {
2466
+ saveAuthoringSyncState(pbDir, dormantState);
2467
+ }
2468
+ catch (err) {
2469
+ logErr(`Warning: could not persist dormancy state — ${err instanceof Error ? err.message : String(err)}`);
2470
+ }
2471
+ }
2472
+ }
2473
+ // 8b. Raise-cleanup (WP-426 E4): for assets active this run, drop any orphan
2474
+ // <surface>.dormant left by a prior lowering (active surface was re-projected
2475
+ // fresh above). User-edited dormant copies are preserved + flagged. Fail-open.
2476
+ if (writeMode) {
2477
+ const raiseState = loadAuthoringSyncState(pbDir);
2478
+ let raiseStateChanged = false;
2479
+ const dormantIds = new Set(dormantDbAssetRows.map((a) => a.entryId));
2480
+ const activeRows = dbAssetRows.filter((a) => !dormantIds.has(a.entryId) &&
2481
+ (a.assetKind === 'skill' || a.assetKind === 'rule' || a.assetKind === 'hook'));
2482
+ for (const asset of activeRows) {
2483
+ // Codex P2: if this asset's body fetch failed, the write loop did NOT reproject its
2484
+ // surfaces this run (same skip the authoring loop applies), so there's no fresh surface
2485
+ // to reconcile against — skip raise-cleanup to avoid acting on stale content.
2486
+ if (bodyFetchFailedEntryIds.has(asset.entryId))
2487
+ continue;
2488
+ for (const { path: filePath, surface } of deriveDormantFilePaths(asset, cwd)) {
2489
+ // Codex P2: honor the run's target set (parity with the dormant pass) — a
2490
+ // `--surfaces cursor` run must not touch .claude/.codex .dormant files, and surfaces
2491
+ // outside the manifest are skipped silently (no false "raise-cleanup failed" warning).
2492
+ if (!allowedTargets.has(surface))
2493
+ continue;
2494
+ const rel = relative(cwd, filePath);
2495
+ try {
2496
+ assertSetupWritePath(filePath, perimeterManifest); // WP-426 E4: perimeter before any FS mutation (parity with the dormant pass)
2497
+ // E4 spec parity (review): restoreSurfaceFromDormant deletes the .dormant
2498
+ // sibling, so guard that delete TARGET too — exactly as the lowering pass
2499
+ // guards both filePath and ${filePath}.dormant.
2500
+ assertSetupWritePath(`${filePath}.dormant`, perimeterManifest);
2501
+ const r = restoreSurfaceFromDormant(filePath);
2502
+ if (r === 'restored') {
2503
+ log(`Raised: removed superseded ${filePath}.dormant`);
2504
+ }
2505
+ else if (r === 'orphan-drift') {
2506
+ syncDriftTensToFire.push(`Dormant copy ${rel}.dormant was edited while dormant for ${asset.entryId}; preserved on raise. Resolve manually.`);
2507
+ logErr(`Warning: ${rel}.dormant differs from the freshly raised projection; left in place.`);
2508
+ }
2509
+ // Prune the registry only when no .dormant sibling remains for this surface:
2510
+ // • 'restored' removed it, or
2511
+ // • it was already gone (manual .dormant→live reactivation; 'skipped', no .dormant).
2512
+ // Carry-over obligation (Phase 3) + Codex P1: the manual-reactivation case must
2513
+ // leave the hands-off set so a future lowering can re-evaluate the surface.
2514
+ // Codex P2: but if a .dormant STILL exists (surface-filtered 'skipped' with no
2515
+ // fresh live file, or a preserved 'orphan-drift'), KEEP the registry evidence —
2516
+ // otherwise a later manual .dormant→live reactivation would not be recognized as
2517
+ // previouslyRenamed and would be silently re-dormanted on the next lowering.
2518
+ if (!existsSync(`${filePath}.dormant`)) {
2519
+ if (raiseState.dormantRenamed?.includes(rel)) {
2520
+ raiseState.dormantRenamed = raiseState.dormantRenamed.filter((p) => p !== rel);
2521
+ raiseStateChanged = true;
2522
+ }
2523
+ if (raiseState.dormantReactivated?.includes(rel)) {
2524
+ raiseState.dormantReactivated = raiseState.dormantReactivated.filter((p) => p !== rel);
2525
+ raiseStateChanged = true;
2526
+ }
2527
+ }
2528
+ }
2529
+ catch (err) {
2530
+ logErr(`Warning: raise-cleanup failed for ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
2531
+ }
2532
+ }
2533
+ }
2534
+ if (raiseStateChanged) {
2535
+ try {
2536
+ saveAuthoringSyncState(pbDir, raiseState);
2537
+ }
2538
+ catch { /* fail-open */ }
2539
+ }
2540
+ }
2541
+ // 8. User-owned files left untouched are NOT drift (TEN-2150).
2542
+ // A marker-less file at an adapter path is the user's own file: handshake never
2543
+ // wrote it, so there is nothing to "sync" and no draft TEN is logged. These files
2544
+ // are surfaced benignly under "Skipped:" and relayed to the connect screen via
2545
+ // report.userOwnedSkipped. (The legitimate "tampered" and authoring-sync drift
2546
+ // signals below are unaffected.)
2547
+ if (syncDriftTensToFire.length > 0) {
2548
+ const session = readSession();
2549
+ if (session) {
2550
+ for (const driftDescription of syncDriftTensToFire) {
2551
+ kernelCallWithSession('chain.createEntry', {
2552
+ collectionSlug: 'tensions',
2553
+ name: 'TEN: setup authoring sync drift — repo wins',
2554
+ status: 'draft',
2555
+ data: { kind: 'drift', description: driftDescription },
2556
+ sessionId: session.sessionId,
2557
+ createdBy: `agent:${session.sessionId}`,
2558
+ }).catch((err) => {
2559
+ // WP-513 round 3 (P2): still fail-open, but surfaced — an unowned rung-2 refusal must not silently drop the drift evidence.
2560
+ logErr(`Warning: could not persist drift TEN (${driftDescription.slice(0, 60)}…) — ${err instanceof Error ? err.message : String(err)}`);
2561
+ });
2562
+ }
2563
+ }
2564
+ }
2565
+ // 8. Case-collision TENs (WP-379 S5b).
2566
+ // These are distinct from drift TENs: they record ambiguous filename collisions
2567
+ // where the "newest mtime wins" heuristic was applied. They always fire on a
2568
+ // detected collision (collision is a data quality issue, not a drift issue).
2569
+ if (collisionTensToFire.length > 0) {
2570
+ const session = readSession();
2571
+ if (session) {
2572
+ for (const tenDescription of collisionTensToFire) {
2573
+ kernelCallWithSession('chain.createEntry', {
2574
+ collectionSlug: 'tensions',
2575
+ name: `TEN: handshake case-collision — ambiguous filename resolved by mtime`,
2576
+ // Collision audit TENs intentionally stay draft — explicit human review, not auto-commit (mirrors smart-capture.ts recordCommitFailure).
2577
+ status: 'draft',
2578
+ data: { description: tenDescription },
2579
+ sessionId: session.sessionId,
2580
+ createdBy: `agent:${session.sessionId}`,
2581
+ }).catch((err) => {
2582
+ logErr(`Warning: could not persist collision TEN (${tenDescription.slice(0, 60)}…) — ${err instanceof Error ? err.message : String(err)}`);
2583
+ });
2584
+ }
2585
+ }
2586
+ }
2587
+ // 8b. Setup receipt — record which assets were materialized (apply mode only)
2588
+ // Fail-open: receipt write is advisory, never blocks the handshake.
2589
+ if (applyMode) {
2590
+ const session = readSession();
2591
+ const caller = session ? kernelCallWithSession : kernelCall;
2592
+ try {
2593
+ const currentState = await caller('setup.getCurrentSetupState', {});
2594
+ const fromMode = currentState?.effectiveMode ?? 'observe';
2595
+ if (fromMode !== manifestStatus.mode) {
2596
+ await caller('setup.recordTransition', {
2597
+ fromMode, toMode: manifestStatus.mode, parseStatus: manifestStatus.parseStatus,
2598
+ surfaces: manifestStatus.surfaces,
2599
+ lock: manifestStatus.lock,
2600
+ });
2601
+ if (modeRank(manifestStatus.mode) < modeRank(fromMode)) {
2602
+ trackEvent('setup.transition.lowered', { fromMode, toMode: manifestStatus.mode });
2603
+ }
2604
+ }
2605
+ }
2606
+ catch (err) {
2607
+ trackEvent('setup.transition.write_failed', { error: err instanceof Error ? err.message : String(err) });
2608
+ logErr(`Warning: could not record setup transition — ${err instanceof Error ? err.message : String(err)}`);
2609
+ }
2610
+ }
2611
+ if (writeMode) {
2612
+ const session = readSession();
2613
+ const caller = session ? kernelCallWithSession : kernelCall;
2614
+ try {
2615
+ const receiptResult = await caller('setup.materializeSetup', {});
2616
+ if (receiptResult?.assetCount > 0) {
2617
+ log(`Setup receipt: ${receiptResult.assetCount} asset(s) recorded.`);
2618
+ }
2619
+ }
2620
+ catch (err) {
2621
+ trackEvent('setup.receipt.write_failed', { error: err instanceof Error ? err.message : String(err) });
2622
+ logErr(`Warning: could not write setup receipt — ${err instanceof Error ? err.message : String(err)}`);
2623
+ }
586
2624
  }
587
- // 8. Report
2625
+ // 9. Report
588
2626
  const report = {
589
2627
  filesWritten,
590
2628
  filesSkipped,
@@ -594,10 +2632,30 @@ export async function runHandshake(options = {}) {
594
2632
  codexWarnings: codexWarnings.length > 0 ? codexWarnings : undefined,
595
2633
  chainRulesStats: chainRulesStats ?? undefined,
596
2634
  chainGaps: chainGaps.length > 0 ? chainGaps : undefined,
2635
+ adoptedCount: adoptedRulesCount,
2636
+ rejectedCount: rejectedRulesCount,
2637
+ personalRuleCount: personalRules.length > 0 ? personalRules.length : undefined,
2638
+ personalSkillCount: personalSkills.length > 0 ? personalSkills.length : undefined,
2639
+ registrySource,
2640
+ registryStale,
2641
+ preview: preview ? true : undefined,
2642
+ previewPlan: preview && previewPlan.length > 0 ? previewPlan : undefined,
2643
+ userOwnedSkipped: userOwnedSkipped.length > 0 ? userOwnedSkipped : undefined,
2644
+ // WP-421 S3: three-bucket drift report (doneWhen #17). PB-managed-clean is
2645
+ // the count of files whose marker + hash matched. Tampered files were
2646
+ // resolved (adopted/reverted) above and are reported separately.
2647
+ managedCleanCount: cleanBucketPaths.length > 0 ? cleanBucketPaths.length : undefined,
2648
+ adoptedTamperedPaths: adoptedTamperedPaths.length > 0 ? adoptedTamperedPaths : undefined,
2649
+ revertedTamperedPaths: revertedTamperedPaths.length > 0 ? revertedTamperedPaths : undefined,
597
2650
  };
598
2651
  if (!quiet) {
599
2652
  process.stdout.write('\n');
600
2653
  process.stdout.write(formatHandshakeReport(report) + '\n');
601
2654
  }
2655
+ // Return the report so non-UI callers (e.g. prepareConnectContext) can surface
2656
+ // skipped/user-owned files without re-deriving the skip logic (TEN-2107).
2657
+ return report;
602
2658
  }
2659
+ // WP-426 E3/E4: exported test-only surface (not part of the public CLI API).
2660
+ export const __test = { setupAuthoringPath, renameSurfaceForDormancy, restoreSurfaceFromDormant, loadAuthoringSyncState, MARKER };
603
2661
  //# sourceMappingURL=handshake.js.map