@crouton-kit/crouter 0.3.28 → 0.3.29

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 (322) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +5 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  49. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  50. package/dist/clients/attach/attach-cmd.js +282 -46
  51. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  52. package/dist/clients/attach/auth-pickers.js +194 -0
  53. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  54. package/dist/clients/attach/canvas-panels.js +45 -17
  55. package/dist/clients/attach/chat-view.d.ts +27 -2
  56. package/dist/clients/attach/chat-view.js +98 -9
  57. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  58. package/dist/clients/attach/clipboard-image.js +150 -26
  59. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  60. package/dist/clients/attach/clipboard-text.js +39 -0
  61. package/dist/clients/attach/config-load.d.ts +54 -7
  62. package/dist/clients/attach/config-load.js +99 -3
  63. package/dist/clients/attach/context-message.d.ts +12 -0
  64. package/dist/clients/attach/context-message.js +72 -0
  65. package/dist/clients/attach/git-info.d.ts +20 -0
  66. package/dist/clients/attach/git-info.js +44 -0
  67. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  68. package/dist/clients/attach/graph-overlay.js +59 -19
  69. package/dist/clients/attach/input-controller.d.ts +75 -13
  70. package/dist/clients/attach/input-controller.js +305 -67
  71. package/dist/clients/attach/pickers.d.ts +58 -0
  72. package/dist/clients/attach/pickers.js +171 -0
  73. package/dist/clients/attach/slash-commands.d.ts +22 -0
  74. package/dist/clients/attach/slash-commands.js +91 -25
  75. package/dist/clients/attach/titled-editor.d.ts +33 -0
  76. package/dist/clients/attach/titled-editor.js +84 -0
  77. package/dist/clients/attach/view-socket.d.ts +18 -1
  78. package/dist/clients/attach/view-socket.js +70 -1
  79. package/dist/clients/web/dev-server.d.ts +7 -0
  80. package/dist/clients/web/dev-server.js +59 -0
  81. package/dist/clients/web/events.d.ts +14 -0
  82. package/dist/clients/web/events.js +151 -0
  83. package/dist/clients/web/server.d.ts +18 -0
  84. package/dist/clients/web/server.js +450 -0
  85. package/dist/clients/web/web-cmd.d.ts +2 -0
  86. package/dist/clients/web/web-cmd.js +120 -0
  87. package/dist/commands/canvas.js +1 -2
  88. package/dist/commands/chord.js +1 -1
  89. package/dist/commands/dashboard.js +5 -1
  90. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  91. package/dist/commands/memory/lint.js +14 -2
  92. package/dist/commands/memory/read.js +2 -2
  93. package/dist/commands/memory/shared.d.ts +7 -1
  94. package/dist/commands/memory/shared.js +18 -3
  95. package/dist/commands/memory/write.js +8 -4
  96. package/dist/commands/memory.js +4 -4
  97. package/dist/commands/node.d.ts +1 -1
  98. package/dist/commands/node.js +64 -65
  99. package/dist/commands/pkg.js +1 -2
  100. package/dist/commands/revive.js +11 -3
  101. package/dist/commands/sys/sync.d.ts +1 -0
  102. package/dist/commands/sys/sync.js +187 -0
  103. package/dist/commands/sys.js +3 -2
  104. package/dist/commands/view-cycle.js +2 -2
  105. package/dist/commands/view-list.js +8 -8
  106. package/dist/commands/view-new.js +21 -17
  107. package/dist/commands/view-pick.js +1 -1
  108. package/dist/commands/view-run.js +35 -14
  109. package/dist/commands/view.js +8 -6
  110. package/dist/commands/workspace.d.ts +2 -0
  111. package/dist/commands/workspace.js +161 -0
  112. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  114. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  116. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  117. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  118. package/dist/core/__tests__/close.test.js +14 -20
  119. package/dist/core/__tests__/context-intro.test.js +19 -19
  120. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  121. package/dist/core/__tests__/draw-style.test.js +1 -1
  122. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  123. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  124. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  125. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  126. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  127. package/dist/core/__tests__/focuses.test.js +25 -13
  128. package/dist/core/__tests__/fork.test.js +22 -5
  129. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  130. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  131. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  133. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  135. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  136. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  137. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  138. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  139. package/dist/core/__tests__/helpers/harness.js +19 -36
  140. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  141. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  142. package/dist/core/__tests__/lifecycle.test.js +1 -19
  143. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  144. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  145. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  146. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  147. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  148. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  149. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  150. package/dist/core/__tests__/reset.test.js +19 -79
  151. package/dist/core/__tests__/steer-note.test.js +9 -7
  152. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  153. package/dist/core/bootstrap.d.ts +0 -3
  154. package/dist/core/bootstrap.js +1 -143
  155. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  156. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  157. package/dist/core/canvas/browse/app.js +131 -8
  158. package/dist/core/canvas/browse/model.d.ts +23 -5
  159. package/dist/core/canvas/browse/model.js +68 -13
  160. package/dist/core/canvas/browse/render.d.ts +7 -1
  161. package/dist/core/canvas/browse/render.js +178 -47
  162. package/dist/core/canvas/canvas.js +1 -1
  163. package/dist/core/canvas/focuses.d.ts +4 -4
  164. package/dist/core/canvas/focuses.js +7 -7
  165. package/dist/core/canvas/nav-model.js +8 -7
  166. package/dist/core/canvas/render.d.ts +68 -9
  167. package/dist/core/canvas/render.js +195 -61
  168. package/dist/core/canvas/types.d.ts +16 -23
  169. package/dist/core/config.js +1 -4
  170. package/dist/core/memory-resolver.js +43 -5
  171. package/dist/core/runtime/bearings.d.ts +1 -1
  172. package/dist/core/runtime/bearings.js +8 -8
  173. package/dist/core/runtime/branded-host.d.ts +16 -0
  174. package/dist/core/runtime/branded-host.js +127 -0
  175. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  176. package/dist/core/runtime/broker.d.ts +4 -1
  177. package/dist/core/runtime/broker.js +494 -58
  178. package/dist/core/runtime/close.d.ts +12 -2
  179. package/dist/core/runtime/close.js +37 -16
  180. package/dist/core/runtime/front-door.js +6 -14
  181. package/dist/core/runtime/host.d.ts +14 -34
  182. package/dist/core/runtime/host.js +14 -50
  183. package/dist/core/runtime/launch.d.ts +12 -9
  184. package/dist/core/runtime/launch.js +27 -15
  185. package/dist/core/runtime/lifecycle.d.ts +1 -1
  186. package/dist/core/runtime/lifecycle.js +15 -19
  187. package/dist/core/runtime/nodes.d.ts +0 -55
  188. package/dist/core/runtime/nodes.js +6 -74
  189. package/dist/core/runtime/placement.d.ts +91 -335
  190. package/dist/core/runtime/placement.js +262 -840
  191. package/dist/core/runtime/promote.d.ts +2 -0
  192. package/dist/core/runtime/promote.js +24 -6
  193. package/dist/core/runtime/recap.d.ts +8 -0
  194. package/dist/core/runtime/recap.js +107 -0
  195. package/dist/core/runtime/recycle.js +25 -61
  196. package/dist/core/runtime/reset.d.ts +43 -37
  197. package/dist/core/runtime/reset.js +131 -218
  198. package/dist/core/runtime/revive.d.ts +9 -29
  199. package/dist/core/runtime/revive.js +40 -139
  200. package/dist/core/runtime/spawn.d.ts +15 -16
  201. package/dist/core/runtime/spawn.js +165 -194
  202. package/dist/core/runtime/surface-bg.d.ts +11 -0
  203. package/dist/core/runtime/surface-bg.js +68 -0
  204. package/dist/core/runtime/tmux.d.ts +13 -50
  205. package/dist/core/runtime/tmux.js +33 -87
  206. package/dist/core/scope.d.ts +1 -2
  207. package/dist/core/scope.js +1 -5
  208. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  209. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  210. package/dist/core/skill-sync/builtins.d.ts +42 -0
  211. package/dist/core/skill-sync/builtins.js +112 -0
  212. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  213. package/dist/core/skill-sync/claude-plugins.js +71 -0
  214. package/dist/core/skill-sync/engine.d.ts +42 -0
  215. package/dist/core/skill-sync/engine.js +633 -0
  216. package/dist/core/skill-sync/export.d.ts +23 -0
  217. package/dist/core/skill-sync/export.js +86 -0
  218. package/dist/core/skill-sync/manifest.d.ts +64 -0
  219. package/dist/core/skill-sync/manifest.js +181 -0
  220. package/dist/core/skill-sync/profile.d.ts +76 -0
  221. package/dist/core/skill-sync/profile.js +173 -0
  222. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  223. package/dist/core/skill-sync/snapshot.js +120 -0
  224. package/dist/core/substrate/index.d.ts +1 -1
  225. package/dist/core/substrate/index.js +1 -1
  226. package/dist/core/substrate/injected-store.js +3 -3
  227. package/dist/core/substrate/on-read.js +68 -6
  228. package/dist/core/substrate/render.d.ts +8 -11
  229. package/dist/core/substrate/render.js +29 -43
  230. package/dist/core/substrate/schema.d.ts +10 -3
  231. package/dist/core/substrate/schema.js +6 -3
  232. package/dist/core/tui/host.d.ts +12 -4
  233. package/dist/core/tui/host.js +280 -149
  234. package/dist/core/view/bridge.d.ts +10 -0
  235. package/dist/core/view/bridge.js +31 -0
  236. package/dist/core/view/chrome.d.ts +9 -0
  237. package/dist/core/view/chrome.js +22 -0
  238. package/dist/core/view/contract.d.ts +171 -0
  239. package/dist/core/view/contract.js +23 -0
  240. package/dist/core/view/loader.d.ts +31 -0
  241. package/dist/core/view/loader.js +188 -0
  242. package/dist/core/view/transport-local.d.ts +7 -0
  243. package/dist/core/view/transport-local.js +70 -0
  244. package/dist/core/view/transport.d.ts +4 -0
  245. package/dist/core/view/transport.js +15 -0
  246. package/dist/daemon/crtrd.d.ts +38 -27
  247. package/dist/daemon/crtrd.js +303 -376
  248. package/dist/daemon/manage.js +6 -1
  249. package/dist/index.js +1 -1
  250. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  251. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  252. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  253. package/dist/pi-extensions/canvas-nav.js +5 -0
  254. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  255. package/dist/pi-extensions/canvas-recap.js +236 -0
  256. package/dist/pi-extensions/canvas-resume.js +2 -1
  257. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  258. package/dist/pi-extensions/canvas-stophook.js +87 -101
  259. package/dist/pi-extensions/canvas-view.js +2 -1
  260. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  261. package/dist/pi-extensions/widget-order-bus.js +34 -0
  262. package/dist/prompts/view.d.ts +2 -2
  263. package/dist/prompts/view.js +145 -73
  264. package/dist/types.d.ts +1 -6
  265. package/dist/types.js +1 -3
  266. package/dist/web/ViewChrome.d.ts +7 -0
  267. package/dist/web/ViewChrome.js +28 -0
  268. package/dist/web/ViewPane.d.ts +39 -0
  269. package/dist/web/ViewPane.js +48 -0
  270. package/dist/web/index.d.ts +6 -0
  271. package/dist/web/index.js +16 -0
  272. package/dist/web/runtime.d.ts +39 -0
  273. package/dist/web/runtime.js +133 -0
  274. package/dist/web/states.d.ts +24 -0
  275. package/dist/web/states.js +24 -0
  276. package/dist/web/transport-http.d.ts +5 -0
  277. package/dist/web/transport-http.js +28 -0
  278. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  279. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  280. package/dist/web-client/index.html +13 -0
  281. package/package.json +20 -6
  282. package/dist/builtin-views/canvas/client.mjs +0 -303
  283. package/dist/builtin-views/canvas/view.mjs +0 -576
  284. package/dist/builtin-views/git-pr/client.mjs +0 -440
  285. package/dist/builtin-views/git-pr/view.mjs +0 -675
  286. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  287. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  288. package/dist/builtin-views/inbox/view.mjs +0 -889
  289. package/dist/builtin-views/linkedin/client.mjs +0 -610
  290. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  291. package/dist/commands/pkg/bridge.d.ts +0 -1
  292. package/dist/commands/pkg/bridge.js +0 -137
  293. package/dist/commands/skill/author.d.ts +0 -3
  294. package/dist/commands/skill/author.js +0 -140
  295. package/dist/commands/skill/shared.d.ts +0 -3
  296. package/dist/commands/skill/shared.js +0 -19
  297. package/dist/commands/skill.d.ts +0 -2
  298. package/dist/commands/skill.js +0 -21
  299. package/dist/commands/tmux-spread.d.ts +0 -2
  300. package/dist/commands/tmux-spread.js +0 -144
  301. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  302. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  303. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  304. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  305. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  306. package/dist/core/__tests__/home-session.test.js +0 -186
  307. package/dist/core/__tests__/relaunch.test.js +0 -335
  308. package/dist/core/bridge-map.d.ts +0 -19
  309. package/dist/core/bridge-map.js +0 -73
  310. package/dist/core/tui/contract.d.ts +0 -83
  311. package/dist/core/tui/contract.js +0 -8
  312. package/dist/core/tui/loader.d.ts +0 -16
  313. package/dist/core/tui/loader.js +0 -94
  314. package/dist/prompts/skill.d.ts +0 -2
  315. package/dist/prompts/skill.js +0 -650
  316. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  317. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  318. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * One-way export of crtr-generated host artifacts — the sole writer of
3
+ * generated content to `~/.claude/skills`, `~/.claude/commands`, and
4
+ * `~/.pi/agent/prompts`. Replaces the two host-dir writers that lived in
5
+ * bootstrap.ts (`ensureBootSkill` + `ensureSlashCommands`), collapsing their
6
+ * two clobber-guards into one `writeIfOurs` and their two kill-switches
7
+ * (`CRTR_NO_BOOT_SKILL` / `CRTR_NO_MODE_CMDS`) into one `CRTR_NO_EXPORTS`.
8
+ *
9
+ * Triggered per-run by cli.ts (argv-gated, exactly as the old writers were) so
10
+ * first-run provisioning is preserved. Best-effort: a provisioning fault must
11
+ * never break an unrelated `crtr` command (the deliberate, pre-existing
12
+ * behavior of the deleted writers).
13
+ */
14
+ import { writeFileSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { dirname, join } from 'node:path';
17
+ import { ensureDir, pathExists, readText } from '../fs-utils.js';
18
+ import { builtinExportPairs } from './builtins.js';
19
+ const SKIP_SUBCOMMANDS = new Set([
20
+ 'help',
21
+ '--help',
22
+ '-h',
23
+ '--version',
24
+ '-v',
25
+ ]);
26
+ /** Skip provisioning for help/version invocations — never write on `-h`. */
27
+ function shouldSkipForArgv(argv) {
28
+ const sub = argv[2];
29
+ if (sub === undefined)
30
+ return true;
31
+ return SKIP_SUBCOMMANDS.has(sub);
32
+ }
33
+ /** Whether a host runtime is in use (its root dir exists). crtr writes into a
34
+ * host's dirs only when present and never creates `~/.claude` or `~/.pi`. */
35
+ function hostRootExists(host) {
36
+ if (host === 'claude')
37
+ return pathExists(join(homedir(), '.claude'));
38
+ return pathExists(join(homedir(), '.pi', 'agent'));
39
+ }
40
+ /** Resolve the on-disk path for one target from its layout. */
41
+ function targetPath(target, name) {
42
+ const dir = target.dir();
43
+ return target.layout === 'bundle'
44
+ ? join(dir, name, 'SKILL.md')
45
+ : join(dir, `${name}.md`);
46
+ }
47
+ /** Write `content` to `path` unless a user-customized file is already there.
48
+ * Rolls forward our own (marker-bearing) versions; skips if identical. Creates
49
+ * only the leaf dir, never the host root. The single generalization of
50
+ * bootstrap.ts's two clobber-guards. */
51
+ export function writeIfOurs(path, content, markerPrefix) {
52
+ if (pathExists(path)) {
53
+ const existing = readText(path);
54
+ if (!existing.includes(markerPrefix))
55
+ return; // user-owned → never clobber
56
+ if (existing === content)
57
+ return; // already current
58
+ }
59
+ ensureDir(dirname(path));
60
+ writeFileSync(path, content, 'utf8');
61
+ }
62
+ /** Render and write every built-in export artifact to every present host.
63
+ * Best-effort: swallows errors (debug-logged) so a provisioning fault never
64
+ * breaks an unrelated `crtr` command. Kill switch: `CRTR_NO_EXPORTS=1`. */
65
+ export function provisionExports(root, argv = process.argv) {
66
+ try {
67
+ if (process.env.CRTR_NO_EXPORTS === '1')
68
+ return;
69
+ if (shouldSkipForArgv(argv))
70
+ return;
71
+ for (const pair of builtinExportPairs(root)) {
72
+ const content = pair.render();
73
+ for (const target of pair.targets) {
74
+ if (!hostRootExists(target.host))
75
+ continue;
76
+ writeIfOurs(targetPath(target, pair.name), content, pair.markerPrefix);
77
+ }
78
+ }
79
+ }
80
+ catch (e) {
81
+ if (process.env.CRTR_DEBUG === '1') {
82
+ const msg = e instanceof Error ? e.message : String(e);
83
+ process.stderr.write(`crtr: exports error: ${msg}\n`);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Layered manifest for bidirectional skill sync (Phase P2).
3
+ *
4
+ * Reads the user-scope (`~/.crouter/skill-sync.json`) and project-scope
5
+ * (`<proj>/.crouter/skill-sync.json`) manifests, validates each against the
6
+ * R-I1 schema with a hand-rolled strict validator (no new dependency), and
7
+ * merges them into a single additive, project-wins pair list.
8
+ *
9
+ * Contract (R-U7/R-U8/R-U9/R-I1/R-X4):
10
+ * - Missing file → empty layer (absence ≠ malformation).
11
+ * - Invalid JSON → HARD error naming the file (no lenient fallback).
12
+ * - Schema-invalid pair → HARD error naming the file (no skip-and-proceed).
13
+ * - Merge → additive union keyed on `id`; a project pair with an
14
+ * `id` present in the user layer REPLACES it entirely.
15
+ *
16
+ * The shared data shapes (`Pair`, `Endpoint`, `TranslationOverride`) are
17
+ * exported from here; P5/P6/P7 import them.
18
+ */
19
+ /** The scope a sync endpoint resolves within. `plugin` is crtr-side only (a
20
+ * skill bundle inside a crtr-registry plugin); `claude-plugin` is Claude-side
21
+ * only (a skill IN PLACE inside its Claude plugin's install path). */
22
+ export type EndpointScope = 'user' | 'project' | 'plugin' | 'claude-plugin';
23
+ /** One side of a sync pair — a single SKILL.md bundle on either the crtr or the
24
+ * Claude side. Resolves to `<dir>/<name>/SKILL.md` (R-I2, resolved by P6).
25
+ * `plugin` is required iff `scope` is `plugin` (crtr) or `claude-plugin`
26
+ * (Claude) — it names the owning plugin (a `claude-plugin` value is the
27
+ * marketplace-qualified key `<plugin>@<marketplace>`). */
28
+ export interface Endpoint {
29
+ scope: EndpointScope;
30
+ name: string;
31
+ plugin?: string;
32
+ }
33
+ /** Per-pair override of the default translation profile — shallow-merged over
34
+ * it by P5's `resolveProfile` (R-I5). The JSON-overridable surface is the
35
+ * owned-field lists; P5 performs the profile-shape validation and rejects
36
+ * unknown keys, so this manifest validator only checks it is an object. */
37
+ export interface TranslationOverride {
38
+ crtrOwned?: string[];
39
+ claudeOwned?: string[];
40
+ }
41
+ /** A single bidirectional sync pair (R-I1). `kind` is optional and, if present,
42
+ * MUST be `"skill"` — the other endpoint kinds (`command`/`agent`/`plugin`) are
43
+ * built-in export artifacts (skill-sync/builtins.ts), not manifest-enrollable. */
44
+ export interface Pair {
45
+ id: string;
46
+ kind?: 'skill';
47
+ crtr: Endpoint;
48
+ claude: Endpoint;
49
+ frontmatter?: TranslationOverride;
50
+ }
51
+ /** The merged, validated manifest. */
52
+ export interface LayeredManifest {
53
+ pairs: Pair[];
54
+ }
55
+ /**
56
+ * Read, validate, and merge the user- and project-layer manifests.
57
+ *
58
+ * @param cwd starting directory for project-scope resolution (defaults to the
59
+ * process cwd via `findProjectScopeRoot`).
60
+ * @returns `{ pairs }` — the additive, project-wins union (R-U8).
61
+ * @throws `usage()` error naming the offending file on malformed JSON or a
62
+ * schema-invalid layer (R-U7/R-X4). NEVER swallows a bad layer.
63
+ */
64
+ export declare function readLayeredManifest(cwd?: string): LayeredManifest;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Layered manifest for bidirectional skill sync (Phase P2).
3
+ *
4
+ * Reads the user-scope (`~/.crouter/skill-sync.json`) and project-scope
5
+ * (`<proj>/.crouter/skill-sync.json`) manifests, validates each against the
6
+ * R-I1 schema with a hand-rolled strict validator (no new dependency), and
7
+ * merges them into a single additive, project-wins pair list.
8
+ *
9
+ * Contract (R-U7/R-U8/R-U9/R-I1/R-X4):
10
+ * - Missing file → empty layer (absence ≠ malformation).
11
+ * - Invalid JSON → HARD error naming the file (no lenient fallback).
12
+ * - Schema-invalid pair → HARD error naming the file (no skip-and-proceed).
13
+ * - Merge → additive union keyed on `id`; a project pair with an
14
+ * `id` present in the user layer REPLACES it entirely.
15
+ *
16
+ * The shared data shapes (`Pair`, `Endpoint`, `TranslationOverride`) are
17
+ * exported from here; P5/P6/P7 import them.
18
+ */
19
+ import { readFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { userScopeRoot, findProjectScopeRoot } from '../scope.js';
22
+ import { usage } from '../errors.js';
23
+ // ── Public API ───────────────────────────────────────────────────────────────
24
+ const MANIFEST_FILE = 'skill-sync.json';
25
+ const VALID_SCOPES = ['user', 'project', 'plugin', 'claude-plugin'];
26
+ /**
27
+ * Read, validate, and merge the user- and project-layer manifests.
28
+ *
29
+ * @param cwd starting directory for project-scope resolution (defaults to the
30
+ * process cwd via `findProjectScopeRoot`).
31
+ * @returns `{ pairs }` — the additive, project-wins union (R-U8).
32
+ * @throws `usage()` error naming the offending file on malformed JSON or a
33
+ * schema-invalid layer (R-U7/R-X4). NEVER swallows a bad layer.
34
+ */
35
+ export function readLayeredManifest(cwd) {
36
+ const userPath = join(userScopeRoot(), MANIFEST_FILE);
37
+ const userPairs = readLayer(userPath);
38
+ const projectRoot = findProjectScopeRoot(cwd);
39
+ const projectPairs = projectRoot
40
+ ? readLayer(join(projectRoot, MANIFEST_FILE))
41
+ : [];
42
+ // Additive union keyed on `id`; project replaces user on collision (R-U8).
43
+ // A Map preserves first-insertion order, so a project pair overwriting a user
44
+ // pair keeps the user pair's position while replacing its value.
45
+ const merged = new Map();
46
+ for (const p of userPairs)
47
+ merged.set(p.id, p);
48
+ for (const p of projectPairs)
49
+ merged.set(p.id, p);
50
+ return { pairs: [...merged.values()] };
51
+ }
52
+ // ── Layer read + strict validation ──────────────────────────────────────────
53
+ /** Read one layer file. Missing → empty (R-U7). Present-but-malformed → throw
54
+ * naming the file (R-U7/R-X4). Returns the validated pairs of that one layer. */
55
+ function readLayer(path) {
56
+ let raw;
57
+ try {
58
+ raw = readFileSync(path, 'utf8');
59
+ }
60
+ catch (err) {
61
+ if (err.code === 'ENOENT')
62
+ return []; // empty layer
63
+ throw usage(`failed to read ${path}: ${err.message}`);
64
+ }
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(raw);
68
+ }
69
+ catch (err) {
70
+ throw usage(`invalid JSON in ${path}: ${err.message}`);
71
+ }
72
+ return validateLayer(parsed, path);
73
+ }
74
+ /** Validate one parsed layer against the R-I1 schema. Throws naming `path` on
75
+ * any violation. Duplicate-`id` detection is scoped to THIS layer only —
76
+ * cross-layer `id` collisions are the legal project-wins merge, not an error. */
77
+ function validateLayer(doc, path) {
78
+ if (!isObject(doc)) {
79
+ throw usage(`malformed manifest ${path}: top level must be a JSON object`);
80
+ }
81
+ // Strict-require `version: 2` — any existing manifest MUST declare it (no
82
+ // advisory hedge). v1 has zero on-disk instances, so this is a clean forward
83
+ // marker, not a migration.
84
+ const version = doc.version;
85
+ if (version !== 2) {
86
+ throw usage(`malformed manifest ${path}: "version" must be 2 (got ${JSON.stringify(version)})`);
87
+ }
88
+ // `pairs` absent → empty manifest; present → must be an array.
89
+ const rawPairs = doc.pairs;
90
+ if (rawPairs === undefined)
91
+ return [];
92
+ if (!Array.isArray(rawPairs)) {
93
+ throw usage(`malformed manifest ${path}: "pairs" must be an array`);
94
+ }
95
+ const seen = new Set();
96
+ const pairs = [];
97
+ rawPairs.forEach((entry, i) => {
98
+ const pair = validatePair(entry, i, path);
99
+ if (seen.has(pair.id)) {
100
+ throw usage(`malformed manifest ${path}: duplicate pair id "${pair.id}" within one layer`);
101
+ }
102
+ seen.add(pair.id);
103
+ pairs.push(pair);
104
+ });
105
+ return pairs;
106
+ }
107
+ function validatePair(entry, i, path) {
108
+ const where = `pairs[${i}]`;
109
+ if (!isObject(entry)) {
110
+ throw usage(`malformed manifest ${path}: ${where} must be an object`);
111
+ }
112
+ const e = entry;
113
+ if (typeof e.id !== 'string' || e.id.length === 0) {
114
+ throw usage(`malformed manifest ${path}: ${where} is missing a non-empty string "id"`);
115
+ }
116
+ const id = e.id;
117
+ // `kind` is optional; absent → "skill". Any value other than "skill" is a hard
118
+ // error — export kinds (command/agent/plugin) are built-in, not enrollable.
119
+ let kind;
120
+ if (e.kind !== undefined) {
121
+ if (e.kind !== 'skill') {
122
+ throw usage(`malformed manifest ${path}: ${where}.kind must be "skill" or absent — ` +
123
+ `kinds like "command"/"agent"/"plugin" are built-in export artifacts, ` +
124
+ `not manifest-enrollable`);
125
+ }
126
+ kind = e.kind;
127
+ }
128
+ const crtr = validateEndpoint(e.crtr, `${where}.crtr`, path, 'crtr');
129
+ const claude = validateEndpoint(e.claude, `${where}.claude`, path, 'claude');
130
+ let frontmatter;
131
+ if (e.frontmatter !== undefined) {
132
+ if (!isObject(e.frontmatter)) {
133
+ throw usage(`malformed manifest ${path}: ${where}.frontmatter must be an object`);
134
+ }
135
+ frontmatter = e.frontmatter;
136
+ }
137
+ return { id, kind, crtr, claude, frontmatter };
138
+ }
139
+ function validateEndpoint(value, where, path, side) {
140
+ if (!isObject(value)) {
141
+ throw usage(`malformed manifest ${path}: ${where} is missing or not an object`);
142
+ }
143
+ const ep = value;
144
+ if (!isScope(ep.scope)) {
145
+ throw usage(`malformed manifest ${path}: ${where}.scope must be one of ${VALID_SCOPES.join('|')}`);
146
+ }
147
+ const scope = ep.scope;
148
+ // Scopes are side-specific: `plugin` resolves a crtr-registry plugin, so it is
149
+ // only legal on the crtr side; `claude-plugin` resolves a Claude install path,
150
+ // so it is only legal on the Claude side (R-I1).
151
+ if (scope === 'plugin' && side !== 'crtr') {
152
+ throw usage(`malformed manifest ${path}: ${where}.scope "plugin" is only valid for a crtr ` +
153
+ `endpoint — for a Claude plugin skill use "claude-plugin"`);
154
+ }
155
+ if (scope === 'claude-plugin' && side !== 'claude') {
156
+ throw usage(`malformed manifest ${path}: ${where}.scope "claude-plugin" is only valid for a ` +
157
+ `Claude endpoint`);
158
+ }
159
+ if (typeof ep.name !== 'string' || ep.name.length === 0) {
160
+ throw usage(`malformed manifest ${path}: ${where}.name must be a non-empty string`);
161
+ }
162
+ const name = ep.name;
163
+ // `plugin` required iff scope names a plugin (`plugin` or `claude-plugin`).
164
+ if (scope === 'plugin' || scope === 'claude-plugin') {
165
+ if (typeof ep.plugin !== 'string' || ep.plugin.length === 0) {
166
+ throw usage(`malformed manifest ${path}: ${where} has scope "${scope}" but no non-empty "plugin" name`);
167
+ }
168
+ return { scope, name, plugin: ep.plugin };
169
+ }
170
+ if (ep.plugin !== undefined) {
171
+ throw usage(`malformed manifest ${path}: ${where}.plugin is only valid when scope is "plugin" or "claude-plugin"`);
172
+ }
173
+ return { scope, name };
174
+ }
175
+ // ── Type guards ──────────────────────────────────────────────────────────────
176
+ function isObject(v) {
177
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
178
+ }
179
+ function isScope(v) {
180
+ return typeof v === 'string' && VALID_SCOPES.includes(v);
181
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Translation profile (Phase P5) — the single home of the crtr ↔ Claude
3
+ * frontmatter asymmetry.
4
+ *
5
+ * Body and asset reconciliation is symmetric (a plain 3-way merge); frontmatter
6
+ * is the ONLY asymmetric axis, and all of that asymmetry lives here (R-U3). The
7
+ * forward translator (`claudeToCrtr.description`) reshapes a Claude `description`
8
+ * into a crtr read-routing `when-and-why-to-read` sentence + `short-form`; the
9
+ * reverse translator (`crtrToClaude.description`) inverts that back into the
10
+ * canonical Claude form `"<gist>. Use when <situation>."` so a round-trip is
11
+ * byte-stable (R-S4 / OD-1).
12
+ *
13
+ * The forward string functions (`reshapeWhenAndWhy`, `cleanClause`,
14
+ * `toShortForm`, `yamlQuote`, `USE_WHEN_RE`) preserve the legacy reshape
15
+ * behavior exactly — `reshapeWhenAndWhy` output is byte-identical to that
16
+ * legacy for the same input (R-I4). Frontmatter writing and generator stamping
17
+ * are intentionally NOT part of this module — the reconcile engine owns all
18
+ * writes.
19
+ */
20
+ import type { TranslationOverride } from './manifest.js';
21
+ /** Reshape a Claude `description` into a read-routing sentence per
22
+ * taste/why-field-means-why-to-read: "When <situation>, this skill should be
23
+ * read [because <gist>]." Claude descriptions are typically
24
+ * "<gist>. Use when <situation>." — we lift the use-when clause into the
25
+ * situation and the preceding text into the payoff. With no explicit clause,
26
+ * the whole description becomes the situation. */
27
+ export declare function reshapeWhenAndWhy(description: string): string;
28
+ /** One-line short-form for the human inventory (never loaded into agent
29
+ * context): the source description collapsed to a single line. */
30
+ export declare function toShortForm(description: string): string;
31
+ /** Always-double-quote a YAML scalar, escaping backslashes and quotes. The two
32
+ * generated fields are single-line, so this is safe regardless of colons,
33
+ * commas, or other YAML-significant characters. */
34
+ export declare function yamlQuote(s: string): string;
35
+ /**
36
+ * Reverse translator: a crtr `when-and-why-to-read` (plus optional `short-form`
37
+ * for the gist when the sentence carries none) → the canonical Claude
38
+ * `description` form `"<gist>. Use when <situation>."`.
39
+ *
40
+ * This is the exact inverse of `reshapeWhenAndWhy`, normalized so that
41
+ * `crtrToClaudeDescription(reshapeWhenAndWhy(d), toShortForm(d))` is a fixed
42
+ * point under re-application — a round-trip is byte-stable (R-S4 / OD-1). The
43
+ * `gist` is recovered from the sentence's `because` clause; only when that is
44
+ * absent do we fall back to the (use-when-stripped) `short-form`, so a
45
+ * `short-form` that is itself a "Use when …" line contributes nothing and
46
+ * cannot ping-pong.
47
+ */
48
+ export declare function crtrToClaudeDescription(whenAndWhy: string, shortForm?: string): string;
49
+ /** The crtr ↔ Claude frontmatter contract: the two translatable `description`
50
+ * axes plus the owned-field lists that are NEVER crossed to the other side
51
+ * (R-X2). Shape per R-I4. */
52
+ export interface TranslationProfile {
53
+ claudeToCrtr: {
54
+ description: (d: string) => {
55
+ whenAndWhy: string;
56
+ shortForm: string;
57
+ };
58
+ };
59
+ crtrToClaude: {
60
+ description: (whenAndWhy: string, shortForm?: string) => string;
61
+ };
62
+ crtrOwned: readonly string[];
63
+ claudeOwned: readonly string[];
64
+ }
65
+ /** The default profile (R-I4). The forward translator reuses
66
+ * `reshapeWhenAndWhy`/`toShortForm` verbatim; the reverse uses
67
+ * `crtrToClaudeDescription`. Owned-field lists match the field taxonomy. */
68
+ export declare const DEFAULT_PROFILE: TranslationProfile;
69
+ /**
70
+ * Resolve a pair's effective profile: a SHALLOW merge of its `frontmatter`
71
+ * override over `DEFAULT_PROFILE`, validated against the profile shape (R-I5).
72
+ * An unknown key or a non-string-array value is a malformed-manifest error
73
+ * (R-U7). NEVER mutates `DEFAULT_PROFILE` — the override path returns a fresh
74
+ * object; the no-override path returns the (unmodified) constant.
75
+ */
76
+ export declare function resolveProfile(override?: TranslationOverride): TranslationProfile;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Translation profile (Phase P5) — the single home of the crtr ↔ Claude
3
+ * frontmatter asymmetry.
4
+ *
5
+ * Body and asset reconciliation is symmetric (a plain 3-way merge); frontmatter
6
+ * is the ONLY asymmetric axis, and all of that asymmetry lives here (R-U3). The
7
+ * forward translator (`claudeToCrtr.description`) reshapes a Claude `description`
8
+ * into a crtr read-routing `when-and-why-to-read` sentence + `short-form`; the
9
+ * reverse translator (`crtrToClaude.description`) inverts that back into the
10
+ * canonical Claude form `"<gist>. Use when <situation>."` so a round-trip is
11
+ * byte-stable (R-S4 / OD-1).
12
+ *
13
+ * The forward string functions (`reshapeWhenAndWhy`, `cleanClause`,
14
+ * `toShortForm`, `yamlQuote`, `USE_WHEN_RE`) preserve the legacy reshape
15
+ * behavior exactly — `reshapeWhenAndWhy` output is byte-identical to that
16
+ * legacy for the same input (R-I4). Frontmatter writing and generator stamping
17
+ * are intentionally NOT part of this module — the reconcile engine owns all
18
+ * writes.
19
+ */
20
+ import { usage } from '../errors.js';
21
+ // ── Forward translation (Claude `description` → crtr `when-and-why-to-read`) ──
22
+ // "Use when …" / "Use this when …" / "This skill should be used when …" / "Used when …"
23
+ const USE_WHEN_RE = /\b(?:use\s+this\s+skill\s+when|use\s+this\s+when|this\s+skill\s+should\s+be\s+used\s+when|used\s+when|use\s+when)\b\s*/i;
24
+ /** Collapse whitespace, strip surrounding connective punctuation, and lowercase
25
+ * a leading Capitalized word (but never an ACRONYM) so the clause reads inline
26
+ * after "When " / "because ". */
27
+ function cleanClause(s) {
28
+ let c = s.replace(/\s+/g, ' ').trim();
29
+ c = c.replace(/^[,;:.\s]+/, '').replace(/[,;:.\s]+$/, '');
30
+ if (/^[A-Z][a-z]/.test(c))
31
+ c = c.charAt(0).toLowerCase() + c.slice(1);
32
+ return c;
33
+ }
34
+ /** Reshape a Claude `description` into a read-routing sentence per
35
+ * taste/why-field-means-why-to-read: "When <situation>, this skill should be
36
+ * read [because <gist>]." Claude descriptions are typically
37
+ * "<gist>. Use when <situation>." — we lift the use-when clause into the
38
+ * situation and the preceding text into the payoff. With no explicit clause,
39
+ * the whole description becomes the situation. */
40
+ export function reshapeWhenAndWhy(description) {
41
+ const desc = description.replace(/\s+/g, ' ').trim();
42
+ if (desc === '')
43
+ return 'When this skill applies, this skill should be read.';
44
+ const m = desc.match(USE_WHEN_RE);
45
+ if (m && m.index !== undefined) {
46
+ const gist = cleanClause(desc.slice(0, m.index));
47
+ const situation = cleanClause(desc.slice(m.index + m[0].length));
48
+ if (situation !== '') {
49
+ let out = `When ${situation}, this skill should be read`;
50
+ if (gist !== '')
51
+ out += ` because ${gist}`;
52
+ return out + '.';
53
+ }
54
+ }
55
+ return `When ${cleanClause(desc)}, this skill should be read.`;
56
+ }
57
+ /** One-line short-form for the human inventory (never loaded into agent
58
+ * context): the source description collapsed to a single line. */
59
+ export function toShortForm(description) {
60
+ return description.replace(/\s+/g, ' ').trim();
61
+ }
62
+ /** Always-double-quote a YAML scalar, escaping backslashes and quotes. The two
63
+ * generated fields are single-line, so this is safe regardless of colons,
64
+ * commas, or other YAML-significant characters. */
65
+ export function yamlQuote(s) {
66
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
67
+ }
68
+ // ── Reverse translation (crtr `when-and-why-to-read` → Claude `description`) ──
69
+ /** The forward `reshapeWhenAndWhy` form, parsed back into its parts:
70
+ * "When <situation>, this skill should be read[ because <gist>]." */
71
+ const RESHAPE_RE = /^When (.+?), this skill should be read(?: because (.+?))?\.?$/s;
72
+ /** Recover `{ situation, gist }` from a `when-and-why-to-read` sentence. A
73
+ * value emitted by `reshapeWhenAndWhy` matches `RESHAPE_RE`; anything else is
74
+ * treated as a bare situation with no gist. The clauses are normalized through
75
+ * `cleanClause` so the reverse is the exact inverse of the forward split. */
76
+ function parseWhenAndWhy(whenAndWhy) {
77
+ const w = whenAndWhy.replace(/\s+/g, ' ').trim();
78
+ const m = w.match(RESHAPE_RE);
79
+ if (m) {
80
+ return { situation: cleanClause(m[1]), gist: cleanClause(m[2] ?? '') };
81
+ }
82
+ return { situation: cleanClause(w), gist: '' };
83
+ }
84
+ /** Pull a gist out of a `short-form` line: the text before any "Use when …"
85
+ * clause (mirroring the forward split), or the whole line if there is none. */
86
+ function gistFromShortForm(shortForm) {
87
+ const s = shortForm.replace(/\s+/g, ' ').trim();
88
+ const m = s.match(USE_WHEN_RE);
89
+ if (m && m.index !== undefined)
90
+ return cleanClause(s.slice(0, m.index));
91
+ return cleanClause(s);
92
+ }
93
+ /** Capitalize the first character of a clause (no-op for an empty string or a
94
+ * leading acronym, which is already upper-case). */
95
+ function capitalizeFirst(s) {
96
+ return s === '' ? s : s.charAt(0).toUpperCase() + s.slice(1);
97
+ }
98
+ /**
99
+ * Reverse translator: a crtr `when-and-why-to-read` (plus optional `short-form`
100
+ * for the gist when the sentence carries none) → the canonical Claude
101
+ * `description` form `"<gist>. Use when <situation>."`.
102
+ *
103
+ * This is the exact inverse of `reshapeWhenAndWhy`, normalized so that
104
+ * `crtrToClaudeDescription(reshapeWhenAndWhy(d), toShortForm(d))` is a fixed
105
+ * point under re-application — a round-trip is byte-stable (R-S4 / OD-1). The
106
+ * `gist` is recovered from the sentence's `because` clause; only when that is
107
+ * absent do we fall back to the (use-when-stripped) `short-form`, so a
108
+ * `short-form` that is itself a "Use when …" line contributes nothing and
109
+ * cannot ping-pong.
110
+ */
111
+ export function crtrToClaudeDescription(whenAndWhy, shortForm) {
112
+ const { situation, gist: parsedGist } = parseWhenAndWhy(whenAndWhy);
113
+ let gist = parsedGist;
114
+ if (gist === '' && shortForm !== undefined)
115
+ gist = gistFromShortForm(shortForm);
116
+ const useWhen = situation === '' ? '' : `Use when ${situation}.`;
117
+ if (gist === '')
118
+ return useWhen;
119
+ const cap = capitalizeFirst(gist);
120
+ return useWhen === '' ? `${cap}.` : `${cap}. ${useWhen}`;
121
+ }
122
+ /** The default profile (R-I4). The forward translator reuses
123
+ * `reshapeWhenAndWhy`/`toShortForm` verbatim; the reverse uses
124
+ * `crtrToClaudeDescription`. Owned-field lists match the field taxonomy. */
125
+ export const DEFAULT_PROFILE = {
126
+ claudeToCrtr: {
127
+ description: (d) => ({ whenAndWhy: reshapeWhenAndWhy(d), shortForm: toShortForm(d) }),
128
+ },
129
+ crtrToClaude: {
130
+ description: (whenAndWhy, shortForm) => crtrToClaudeDescription(whenAndWhy, shortForm),
131
+ },
132
+ crtrOwned: ['kind', 'system-prompt-visibility', 'file-read-visibility', 'gate', 'applies-to'],
133
+ claudeOwned: ['argument-hint', 'user-invocable', 'paths', 'context', 'model', 'agent', 'keywords', 'type'],
134
+ };
135
+ /** The only fields a per-pair `frontmatter` override may set (R-I5). The
136
+ * translators are functions and cannot be expressed in JSON, so the
137
+ * overridable surface is exactly the owned-field lists. */
138
+ const OVERRIDABLE_KEYS = ['crtrOwned', 'claudeOwned'];
139
+ /**
140
+ * Resolve a pair's effective profile: a SHALLOW merge of its `frontmatter`
141
+ * override over `DEFAULT_PROFILE`, validated against the profile shape (R-I5).
142
+ * An unknown key or a non-string-array value is a malformed-manifest error
143
+ * (R-U7). NEVER mutates `DEFAULT_PROFILE` — the override path returns a fresh
144
+ * object; the no-override path returns the (unmodified) constant.
145
+ */
146
+ export function resolveProfile(override) {
147
+ if (override === undefined)
148
+ return DEFAULT_PROFILE;
149
+ if (typeof override !== 'object' || override === null || Array.isArray(override)) {
150
+ throw usage('malformed manifest: pair frontmatter override must be an object');
151
+ }
152
+ for (const key of Object.keys(override)) {
153
+ if (!OVERRIDABLE_KEYS.includes(key)) {
154
+ throw usage(`malformed manifest: unknown frontmatter override key "${key}" (allowed: ${OVERRIDABLE_KEYS.join(', ')})`);
155
+ }
156
+ }
157
+ return {
158
+ claudeToCrtr: DEFAULT_PROFILE.claudeToCrtr,
159
+ crtrToClaude: DEFAULT_PROFILE.crtrToClaude,
160
+ crtrOwned: validateOwned(override.crtrOwned, 'crtrOwned') ?? DEFAULT_PROFILE.crtrOwned,
161
+ claudeOwned: validateOwned(override.claudeOwned, 'claudeOwned') ?? DEFAULT_PROFILE.claudeOwned,
162
+ };
163
+ }
164
+ /** Validate one owned-field override list: undefined → inherit the default;
165
+ * anything other than an array of strings → malformed-manifest error. */
166
+ function validateOwned(value, key) {
167
+ if (value === undefined)
168
+ return undefined;
169
+ if (!Array.isArray(value) || !value.every((x) => typeof x === 'string')) {
170
+ throw usage(`malformed manifest: frontmatter override "${key}" must be an array of strings`);
171
+ }
172
+ return value;
173
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The translatable values recorded for each side at last sync. These are the
3
+ * ONLY values stored (§Resolved-here-4): owned fields are never recorded —
4
+ * each side reads its own owned fields through, so they need no merge base.
5
+ */
6
+ export interface SnapshotMeta {
7
+ /** ISO 8601 timestamp of the sync that produced this base. */
8
+ syncedAt: string;
9
+ /** crtr-side translatable frontmatter at last sync. */
10
+ crtrFrontmatter: {
11
+ whenAndWhy?: string;
12
+ shortForm?: string;
13
+ };
14
+ /** Claude-side translatable frontmatter at last sync. */
15
+ claudeFrontmatter: {
16
+ description?: string;
17
+ };
18
+ }
19
+ /** A fully-loaded merge base for one pair. */
20
+ export interface Snapshot {
21
+ /** Shared body, frontmatter stripped. */
22
+ body: string;
23
+ /** Non-SKILL.md base assets, keyed by POSIX-style relpath under `assets/`. */
24
+ assets: Map<string, Buffer>;
25
+ /** Recorded translatable values + sync timestamp. */
26
+ meta: SnapshotMeta;
27
+ }
28
+ /** `~/.crouter/skill-sync/` — the machine-local, uncommitted root (R-U9). */
29
+ export declare function skillSyncRoot(): string;
30
+ /** `~/.crouter/skill-sync/snapshots/<id>/`. */
31
+ export declare function snapshotDir(id: string): string;
32
+ /** `~/.crouter/skill-sync/conflicts/<id>.md`. */
33
+ export declare function conflictReportPath(id: string): string;
34
+ /**
35
+ * Load the merge base for `id`, or `null` when the snapshot dir is absent
36
+ * (no prior sync — the engine seeds on first reconcile, R-S1/R-S3).
37
+ */
38
+ export declare function readSnapshot(id: string): Snapshot | null;
39
+ /**
40
+ * Write the merge base for `id` byte-for-byte. The snapshot dir is rebuilt
41
+ * from scratch so stale assets from a prior base never linger — the on-disk
42
+ * `assets/` always mirrors exactly the passed `assets` map (R-U5/R-I3).
43
+ */
44
+ export declare function writeSnapshot(id: string, snapshot: Snapshot): void;
45
+ /**
46
+ * Write the current conflict report for `id` to `conflicts/<id>.md`. The engine
47
+ * calls this on any true conflict (overlapping body/asset merge or both-sides
48
+ * non-equivalent frontmatter) after deciding to write nothing for the pair.
49
+ */
50
+ export declare function writeConflictReport(id: string, text: string): void;
51
+ /**
52
+ * Delete a stale `conflicts/<id>.md` once the pair reconciles cleanly or
53
+ * becomes a no-op (OD-4). No-op if absent — never throws on a missing report.
54
+ */
55
+ export declare function clearConflictReport(id: string): void;
56
+ /** True iff a conflict report currently exists for `id`. */
57
+ export declare function hasConflictReport(id: string): boolean;