@sagmans/dsh-tui 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
  2. package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
  3. package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
  4. package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
  5. package/.agents/skills/dsh-tui-update-models/SKILL.md +121 -0
  6. package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +162 -0
  7. package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +178 -0
  8. package/README.md +131 -17
  9. package/lib/agent/host.d.ts +11 -0
  10. package/lib/agent/host.d.ts.map +1 -1
  11. package/lib/agent/host.js +16 -2
  12. package/lib/agent/host.js.map +1 -1
  13. package/lib/agent/present.d.ts.map +1 -1
  14. package/lib/agent/present.js +2 -1
  15. package/lib/agent/present.js.map +1 -1
  16. package/lib/agent/undo.d.ts +52 -0
  17. package/lib/agent/undo.d.ts.map +1 -0
  18. package/lib/agent/undo.js +106 -0
  19. package/lib/agent/undo.js.map +1 -0
  20. package/lib/cards/composition.d.ts +68 -0
  21. package/lib/cards/composition.d.ts.map +1 -0
  22. package/lib/cards/composition.js +117 -0
  23. package/lib/cards/composition.js.map +1 -0
  24. package/lib/cards/presenter.d.ts +23 -0
  25. package/lib/cards/presenter.d.ts.map +1 -0
  26. package/lib/cards/presenter.js +394 -0
  27. package/lib/cards/presenter.js.map +1 -0
  28. package/lib/cards/preview.d.ts +42 -0
  29. package/lib/cards/preview.d.ts.map +1 -0
  30. package/lib/cards/preview.js +51 -0
  31. package/lib/cards/preview.js.map +1 -0
  32. package/lib/cards.d.ts +42 -95
  33. package/lib/cards.d.ts.map +1 -1
  34. package/lib/cards.js +4 -526
  35. package/lib/cards.js.map +1 -1
  36. package/lib/compat/section.d.ts +29 -0
  37. package/lib/compat/section.d.ts.map +1 -0
  38. package/lib/compat/section.js +82 -0
  39. package/lib/compat/section.js.map +1 -0
  40. package/lib/config.d.ts +27 -2
  41. package/lib/config.d.ts.map +1 -1
  42. package/lib/config.js +61 -1
  43. package/lib/config.js.map +1 -1
  44. package/lib/contracts.d.ts +28 -0
  45. package/lib/contracts.d.ts.map +1 -1
  46. package/lib/fold-cursor.d.ts +8 -0
  47. package/lib/fold-cursor.d.ts.map +1 -1
  48. package/lib/fold-cursor.js +18 -0
  49. package/lib/fold-cursor.js.map +1 -1
  50. package/lib/gates/question-card.d.ts +77 -0
  51. package/lib/gates/question-card.d.ts.map +1 -0
  52. package/lib/gates/question-card.js +113 -0
  53. package/lib/gates/question-card.js.map +1 -0
  54. package/lib/gates/questions.d.ts +127 -0
  55. package/lib/gates/questions.d.ts.map +1 -0
  56. package/lib/gates/questions.js +418 -0
  57. package/lib/gates/questions.js.map +1 -0
  58. package/lib/gates.d.ts +1 -113
  59. package/lib/gates.d.ts.map +1 -1
  60. package/lib/gates.js +1 -484
  61. package/lib/gates.js.map +1 -1
  62. package/lib/herdr/reporter.d.ts +9 -4
  63. package/lib/herdr/reporter.d.ts.map +1 -1
  64. package/lib/herdr/reporter.js +5 -9
  65. package/lib/herdr/reporter.js.map +1 -1
  66. package/lib/herdr/state.d.ts +35 -4
  67. package/lib/herdr/state.d.ts.map +1 -1
  68. package/lib/herdr/state.js +37 -4
  69. package/lib/herdr/state.js.map +1 -1
  70. package/lib/index.d.ts +1 -0
  71. package/lib/index.d.ts.map +1 -1
  72. package/lib/index.js +343 -2267
  73. package/lib/index.js.map +1 -1
  74. package/lib/input/action-catalog.d.ts +74 -0
  75. package/lib/input/action-catalog.d.ts.map +1 -0
  76. package/lib/input/action-catalog.js +137 -0
  77. package/lib/input/action-catalog.js.map +1 -0
  78. package/lib/input/actions.d.ts +5 -81
  79. package/lib/input/actions.d.ts.map +1 -1
  80. package/lib/input/actions.js +4 -621
  81. package/lib/input/actions.js.map +1 -1
  82. package/lib/input/completion.d.ts +11 -1
  83. package/lib/input/completion.d.ts.map +1 -1
  84. package/lib/input/completion.js +17 -1
  85. package/lib/input/completion.js.map +1 -1
  86. package/lib/input/file-index.d.ts +40 -0
  87. package/lib/input/file-index.d.ts.map +1 -0
  88. package/lib/input/file-index.js +141 -0
  89. package/lib/input/file-index.js.map +1 -0
  90. package/lib/input/file-search.d.ts +1 -38
  91. package/lib/input/file-search.d.ts.map +1 -1
  92. package/lib/input/file-search.js +1 -487
  93. package/lib/input/file-search.js.map +1 -1
  94. package/lib/input/key-press.d.ts +57 -0
  95. package/lib/input/key-press.d.ts.map +1 -0
  96. package/lib/input/key-press.js +218 -0
  97. package/lib/input/key-press.js.map +1 -0
  98. package/lib/input/keymap-conflicts.d.ts +59 -0
  99. package/lib/input/keymap-conflicts.d.ts.map +1 -0
  100. package/lib/input/keymap-conflicts.js +292 -0
  101. package/lib/input/keymap-conflicts.js.map +1 -0
  102. package/lib/input/keymap-settings.js +1 -1
  103. package/lib/input/keymap-settings.js.map +1 -1
  104. package/lib/input/keymap.d.ts.map +1 -1
  105. package/lib/input/keymap.js +5 -1
  106. package/lib/input/keymap.js.map +1 -1
  107. package/lib/input/submission.d.ts +11 -1
  108. package/lib/input/submission.d.ts.map +1 -1
  109. package/lib/input/submission.js +10 -1
  110. package/lib/input/submission.js.map +1 -1
  111. package/lib/input/workspace-files.d.ts +10 -0
  112. package/lib/input/workspace-files.d.ts.map +1 -0
  113. package/lib/input/workspace-files.js +359 -0
  114. package/lib/input/workspace-files.js.map +1 -0
  115. package/lib/install-skills.d.ts +20 -0
  116. package/lib/install-skills.d.ts.map +1 -0
  117. package/lib/install-skills.js +198 -0
  118. package/lib/install-skills.js.map +1 -0
  119. package/lib/keys-command.d.ts +2 -1
  120. package/lib/keys-command.d.ts.map +1 -1
  121. package/lib/keys-command.js +2 -1
  122. package/lib/keys-command.js.map +1 -1
  123. package/lib/model-list.d.ts +37 -0
  124. package/lib/model-list.d.ts.map +1 -0
  125. package/lib/model-list.js +60 -0
  126. package/lib/model-list.js.map +1 -0
  127. package/lib/startup.d.ts +8 -0
  128. package/lib/startup.d.ts.map +1 -1
  129. package/lib/startup.js +120 -24
  130. package/lib/startup.js.map +1 -1
  131. package/lib/subagents.d.ts +7 -3
  132. package/lib/subagents.d.ts.map +1 -1
  133. package/lib/subagents.js +28 -4
  134. package/lib/subagents.js.map +1 -1
  135. package/lib/surface/appearance-preferences.d.ts +26 -0
  136. package/lib/surface/appearance-preferences.d.ts.map +1 -0
  137. package/lib/surface/appearance-preferences.js +139 -0
  138. package/lib/surface/appearance-preferences.js.map +1 -0
  139. package/lib/surface/appearance.d.ts +87 -0
  140. package/lib/surface/appearance.d.ts.map +1 -0
  141. package/lib/surface/appearance.js +353 -0
  142. package/lib/surface/appearance.js.map +1 -0
  143. package/lib/surface/background-work.d.ts +49 -0
  144. package/lib/surface/background-work.d.ts.map +1 -0
  145. package/lib/surface/background-work.js +146 -0
  146. package/lib/surface/background-work.js.map +1 -0
  147. package/lib/surface/commands.d.ts +80 -0
  148. package/lib/surface/commands.d.ts.map +1 -0
  149. package/lib/surface/commands.js +364 -0
  150. package/lib/surface/commands.js.map +1 -0
  151. package/lib/surface/modal-input.d.ts +58 -0
  152. package/lib/surface/modal-input.d.ts.map +1 -0
  153. package/lib/surface/modal-input.js +237 -0
  154. package/lib/surface/modal-input.js.map +1 -0
  155. package/lib/surface/model-choice.d.ts +54 -0
  156. package/lib/surface/model-choice.d.ts.map +1 -0
  157. package/lib/surface/model-choice.js +325 -0
  158. package/lib/surface/model-choice.js.map +1 -0
  159. package/lib/surface/preset-choice.d.ts +78 -0
  160. package/lib/surface/preset-choice.d.ts.map +1 -0
  161. package/lib/surface/preset-choice.js +163 -0
  162. package/lib/surface/preset-choice.js.map +1 -0
  163. package/lib/surface/prompt-input.d.ts +97 -0
  164. package/lib/surface/prompt-input.d.ts.map +1 -0
  165. package/lib/surface/prompt-input.js +215 -0
  166. package/lib/surface/prompt-input.js.map +1 -0
  167. package/lib/surface/prompt-memory.d.ts +76 -0
  168. package/lib/surface/prompt-memory.d.ts.map +1 -0
  169. package/lib/surface/prompt-memory.js +167 -0
  170. package/lib/surface/prompt-memory.js.map +1 -0
  171. package/lib/surface/session-lifecycle.d.ts +120 -0
  172. package/lib/surface/session-lifecycle.d.ts.map +1 -0
  173. package/lib/surface/session-lifecycle.js +321 -0
  174. package/lib/surface/session-lifecycle.js.map +1 -0
  175. package/lib/surface/session-picker.d.ts +39 -0
  176. package/lib/surface/session-picker.d.ts.map +1 -0
  177. package/lib/surface/session-picker.js +107 -0
  178. package/lib/surface/session-picker.js.map +1 -0
  179. package/lib/surface/session-view.d.ts +86 -0
  180. package/lib/surface/session-view.d.ts.map +1 -0
  181. package/lib/surface/session-view.js +215 -0
  182. package/lib/surface/session-view.js.map +1 -0
  183. package/lib/surface/staged-turns.d.ts +81 -0
  184. package/lib/surface/staged-turns.d.ts.map +1 -0
  185. package/lib/surface/staged-turns.js +206 -0
  186. package/lib/surface/staged-turns.js.map +1 -0
  187. package/lib/surface/terminal-lifecycle.d.ts +76 -0
  188. package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
  189. package/lib/surface/terminal-lifecycle.js +222 -0
  190. package/lib/surface/terminal-lifecycle.js.map +1 -0
  191. package/lib/terminal/warning-screen.d.ts +8 -1
  192. package/lib/terminal/warning-screen.d.ts.map +1 -1
  193. package/lib/terminal/warning-screen.js +9 -0
  194. package/lib/terminal/warning-screen.js.map +1 -1
  195. package/lib/terminal-text/scan.d.ts +44 -0
  196. package/lib/terminal-text/scan.d.ts.map +1 -0
  197. package/lib/terminal-text/scan.js +195 -0
  198. package/lib/terminal-text/scan.js.map +1 -0
  199. package/lib/terminal-text/sgr.d.ts +32 -0
  200. package/lib/terminal-text/sgr.d.ts.map +1 -0
  201. package/lib/terminal-text/sgr.js +203 -0
  202. package/lib/terminal-text/sgr.js.map +1 -0
  203. package/lib/terminal-text.d.ts +1 -1
  204. package/lib/terminal-text.d.ts.map +1 -1
  205. package/lib/terminal-text.js +2 -379
  206. package/lib/terminal-text.js.map +1 -1
  207. package/lib/theme-command.js +2 -1
  208. package/lib/theme-command.js.map +1 -1
  209. package/lib/theme-defaults.d.ts +64 -0
  210. package/lib/theme-defaults.d.ts.map +1 -0
  211. package/lib/theme-defaults.js +248 -0
  212. package/lib/theme-defaults.js.map +1 -0
  213. package/lib/theme-resolver.d.ts +36 -0
  214. package/lib/theme-resolver.d.ts.map +1 -0
  215. package/lib/theme-resolver.js +112 -0
  216. package/lib/theme-resolver.js.map +1 -0
  217. package/lib/theme-schema.d.ts +6 -6
  218. package/lib/theme-schema.d.ts.map +1 -1
  219. package/lib/theme-schema.js +3 -2
  220. package/lib/theme-schema.js.map +1 -1
  221. package/lib/theme-settings.d.ts +13 -15
  222. package/lib/theme-settings.d.ts.map +1 -1
  223. package/lib/theme-settings.js +27 -19
  224. package/lib/theme-settings.js.map +1 -1
  225. package/lib/theme-tokens.d.ts +1 -90
  226. package/lib/theme-tokens.d.ts.map +1 -1
  227. package/lib/theme-tokens.js +11 -341
  228. package/lib/theme-tokens.js.map +1 -1
  229. package/lib/theme.d.ts +1 -1
  230. package/lib/theme.d.ts.map +1 -1
  231. package/lib/theme.js +2 -1
  232. package/lib/theme.js.map +1 -1
  233. package/lib/tool-display.d.ts.map +1 -1
  234. package/lib/tool-display.js +2 -1
  235. package/lib/tool-display.js.map +1 -1
  236. package/lib/transcript/message-content.d.ts +21 -0
  237. package/lib/transcript/message-content.d.ts.map +1 -0
  238. package/lib/transcript/message-content.js +61 -0
  239. package/lib/transcript/message-content.js.map +1 -0
  240. package/lib/transcript/tool-calls.d.ts +106 -0
  241. package/lib/transcript/tool-calls.d.ts.map +1 -0
  242. package/lib/transcript/tool-calls.js +254 -0
  243. package/lib/transcript/tool-calls.js.map +1 -0
  244. package/lib/transcript.d.ts +38 -43
  245. package/lib/transcript.d.ts.map +1 -1
  246. package/lib/transcript.js +58 -245
  247. package/lib/transcript.js.map +1 -1
  248. package/lib/ui/copy.d.ts +17 -0
  249. package/lib/ui/copy.d.ts.map +1 -0
  250. package/lib/ui/copy.js +104 -0
  251. package/lib/ui/copy.js.map +1 -0
  252. package/lib/ui/dock.d.ts +35 -3
  253. package/lib/ui/dock.d.ts.map +1 -1
  254. package/lib/ui/dock.js +124 -23
  255. package/lib/ui/dock.js.map +1 -1
  256. package/lib/ui/editor.d.ts.map +1 -1
  257. package/lib/ui/editor.js +2 -1
  258. package/lib/ui/editor.js.map +1 -1
  259. package/lib/ui/frame.d.ts +31 -0
  260. package/lib/ui/frame.d.ts.map +1 -1
  261. package/lib/ui/frame.js +27 -4
  262. package/lib/ui/frame.js.map +1 -1
  263. package/lib/ui/keymap-picker.d.ts +2 -1
  264. package/lib/ui/keymap-picker.d.ts.map +1 -1
  265. package/lib/ui/keymap-picker.js +1 -0
  266. package/lib/ui/keymap-picker.js.map +1 -1
  267. package/lib/ui/view/gate-card.d.ts +21 -0
  268. package/lib/ui/view/gate-card.d.ts.map +1 -0
  269. package/lib/ui/view/gate-card.js +104 -0
  270. package/lib/ui/view/gate-card.js.map +1 -0
  271. package/lib/ui/view/tool-card.d.ts +138 -0
  272. package/lib/ui/view/tool-card.d.ts.map +1 -0
  273. package/lib/ui/view/tool-card.js +413 -0
  274. package/lib/ui/view/tool-card.js.map +1 -0
  275. package/lib/ui/view/transcript-message.d.ts +62 -0
  276. package/lib/ui/view/transcript-message.d.ts.map +1 -0
  277. package/lib/ui/view/transcript-message.js +158 -0
  278. package/lib/ui/view/transcript-message.js.map +1 -0
  279. package/lib/ui/view.d.ts +87 -156
  280. package/lib/ui/view.d.ts.map +1 -1
  281. package/lib/ui/view.js +122 -556
  282. package/lib/ui/view.js.map +1 -1
  283. package/package.json +3 -1
  284. package/themes/deepseek-blue.yaml +9 -0
  285. package/themes/violet-orbit.yaml +7 -0
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: dsh-tui-dogfood
3
+ description: "Use when a dsh plugin checkout must be tested against a real profile without writing to the developer's live home: clone the home, relink only that plugin, and run or hand off the profile."
4
+ ---
5
+ # Dogfood a dsh plugin checkout
6
+
7
+ A fresh `DSH_HOME` misses the installed bundles, profile patch, settings, and themes.
8
+ A run against the real home writes real sessions and state. Clone the source
9
+ home and relink only the plugin under test inside the clone.
10
+
11
+ From **any dsh plugin checkout root**, run the packaged helper:
12
+
13
+ ```sh
14
+ ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh --no-launch
15
+ ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh feat/plugin-fix -- --resume
16
+ ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh ../plugin-fix --dry-run
17
+ ```
18
+
19
+ No target means the current checkout. A name matches its repository's worktree
20
+ directory or branch tail. A path selects that checkout directly. The helper reads
21
+ `package.json` for the plugin name; it never assumes `@sagmans/dsh-tui`. It
22
+ runs `pnpm run build` only when the checkout declares a build script. It adds
23
+ the bundle once if absent from the cloned profile, updates only its dependency,
24
+ restores local links from `link:` specifications, and launches
25
+ `dsh --profile tui` by default. The legacy
26
+ `scripts/dogfood/run-tui-from-worktree.sh` remains available in dsh-tui and
27
+ defaults to that script's checkout even when called elsewhere.
28
+
29
+ | Need | Option |
30
+ | --- | --- |
31
+ | Inspect clone and linked bundle | `--status` |
32
+ | Copy saved sessions for `--resume` | `--with-sessions` |
33
+ | Copy credentials and settings only | `--fresh` |
34
+ | Select profile, source home, or scratch home | `--profile NAME`, `--source-home DIR`, `--home DIR` |
35
+ | Print plan without mutation | `--dry-run` |
36
+ | Set up without starting dsh | `--no-launch` |
37
+ | Discard and clone again | `--reseed` |
38
+ | Remove matching scratch clone | `--clean` |
39
+ | List checkout worktrees | `--list` |
40
+
41
+ The helper ignores `$DSH_HOME` as source. Pass `--source-home "$DSH_HOME"`
42
+ when you intend to clone it. The default scratch path includes the checkout
43
+ name and path hash to avoid reuse across repositories.
44
+
45
+ The clone contains copied credentials. Store it only in private scratch storage
46
+ and remove it with `--clean`. Cleanup and reseed refuse nonmatching markers,
47
+ source-home overlap, and home-root paths.
48
+
49
+ External links are allowed only under `profiles/node_modules`,
50
+ `profiles/*/node_modules`, and `profiles/*/.dsh-module-fallback/node_modules`.
51
+ These package links can point to global packages or external checkouts, but
52
+ never into the source home. Every other cloned link must stay inside the clone.
53
+ An instruction file linked into a shared prompt tree (`AGENTS.md`, `CLAUDE.md`)
54
+ is read only, so seeding copies it in as a regular file; a link to state the run
55
+ may write — credentials, settings, storages, sessions, profiles — still stops the
56
+ run. Mutable cloned files with multiple hardlinks are rejected. Never write test
57
+ state through a package link.
58
+
59
+ The old dsh-tui entry point still requires a prelisted bundle. Do not use
60
+ `dsh plugin add` on a cloned profile with other bundles; it can drop them.
61
+
62
+ Use `--status` to verify the existing bundles and the checkout `link:`
63
+ dependency. In dsh-tui, drive a cloned profile with:
64
+
65
+ ```sh
66
+ node tools/pty-drive.mjs --launcher "$(command -v dsh)" --home <clone> --prompt 'Reply with exactly: pong'
67
+ ```
68
+
69
+ Dsh-tui source edits are invisible until `pnpm run build` updates `lib/`.
70
+ Omit `--no-build` for real runs.
71
+
72
+ If the checkout or source home was removed, use
73
+ `--clean --home <clone> --source-home <original-source-path>`. Run from the
74
+ checkout while it exists, or pass its path as the target from another
75
+ directory. If the checkout was removed, run from any directory. Supply the
76
+ original `--profile NAME` when it was not `tui`.
77
+
78
+ Cleanup canonicalizes a missing source path only for `--clean --home` and
79
+ still requires its marker to match. Orphan cleanup requires a private marker
80
+ owned by the current user. It validates home, source, profile, and absent
81
+ target, but cannot recheck a removed checkout.
82
+
83
+ For home contents, storage, and cleanup details, read [references/home-state.md](references/home-state.md).
@@ -0,0 +1,78 @@
1
+ # What a ~/.dsh home holds, and what a dogfood run touches
2
+
3
+ Read this when a dogfood run behaves differently from the developer's own
4
+ profile. It gives the operational detail behind [../SKILL.md](../SKILL.md).
5
+
6
+ ## The entries
7
+
8
+ | Entry | Kind | What reads or writes it |
9
+ | --- | --- | --- |
10
+ | `.credentials.yaml` | secret | the provider layer, at start-up; read-only |
11
+ | `settings.yaml`, `settings.yaml.imported` | config | the `dsh-tui:` section: theme, tokens, dock, stash, prompt history |
12
+ | `themes/` | data | created and watched at start-up; saving a file changes the live surface |
13
+ | `prompt-history.json` | data | the editor's history recall; appended as prompts are sent |
14
+ | `tui-stash/` | data | the parked-draft bank, one file per session, behind a lock |
15
+ | `storages/` | data | durable plugin state |
16
+ | `attachments/` | data | images pasted into prompts |
17
+ | `profiles/<name>/` | config | `package.json` (bundle list and deps), `cordis.yml`, `cordis.patch.yml`, a pnpm lockfile, `node_modules` |
18
+ | `sessions/<bucket>/<id>/` | data | the transcript (`session.v3.jsonl.zstd`) and a lock; the bucket name encodes the launch directory |
19
+ | `AGENTS.md` | config | injected into the agent's instructions |
20
+
21
+ ## Home state versus profile state
22
+
23
+ Credentials, settings, themes, history, the stash, storages, and sessions belong
24
+ to the **home**: every profile in it shares them. The bundle list and the patch
25
+ overlay belong to the **profile**: a new profile name starts from the base plus
26
+ whatever is added to it, and inherits none of another profile's bundles or
27
+ patches. That is the trap behind "test it in a profile named after the feature" —
28
+ the run is compositionally thinner than the developer's own while still writing
29
+ their real state.
30
+
31
+ ## What the dogfood clone changes
32
+
33
+ The checkout package name selects its bundle in `profiles/<name>/package.json`.
34
+ The generic helper adds it once if absent, repoints only its dependency, and
35
+ rebuilds cloned local symlinks. It uses `dsh plugin add` only if the profile is
36
+ missing after `dsh --help`, and does not modify the lockfile of an existing
37
+ profile. Other bundles and the profile patch remain copied from the source home.
38
+ One module, `scripts/clone-links.mjs`, holds the clone's link policy, so the
39
+ seeding step and the validator read the same rules.
40
+
41
+ `sessions/` is the only entry left out by default: it is the bulk of a home
42
+ (190M of 200M in a busy one) and a test drive rarely needs it. `--with-sessions`
43
+ copies it when present, which makes `--resume` reach the developer's history.
44
+
45
+ ## Failure modes worth knowing
46
+
47
+ - **A missing row is silent.** `dsh plugin install` drops a bundle whose path
48
+ does not resolve and still exits 0; the profile then composes the base alone and
49
+ the surface waits with no output. Check
50
+ `node -p "require('<home>/profiles/<name>/package.json').dsh.profile.bundles"`.
51
+ - **Mutable home state must remain inside the clone.** External symlinks in
52
+ settings, storages, or sessions and multiply linked mutable files stop the run.
53
+ Package links in `profiles/node_modules`, `profiles/*/node_modules`, and
54
+ `profiles/*/.dsh-module-fallback/node_modules` can point to global installs or
55
+ external checkouts, but not into the source home. An instruction file linked
56
+ into a shared prompt tree (`AGENTS.md`, `CLAUDE.md`) is read only and is copied
57
+ in while seeding, so the clone stays self-contained; a link to state the run may
58
+ write still stops the run. Writes through permitted package links are not
59
+ isolated. Re-seed with `--reseed` when the source changes.
60
+ - **The clone keeps a copy of the credentials.** Create it in a scratch
61
+ directory, keep it `700`, and `--clean` it when done.
62
+ - **The launcher decides the harness, not the profile.** `pnpm dsh` runs the
63
+ workspace-linked harness checkout (a dev build, which may fail to compose);
64
+ `dsh` on `PATH` is the released one. A profile cannot tell them apart.
65
+ - **A turn can fail for reasons outside the surface** — no provider credit, a
66
+ sandbox policy, a missing credential. Read the error line in the transcript
67
+ before treating it as a rendering bug.
68
+
69
+ ## Cleanup
70
+
71
+ ```sh
72
+ ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh --status # which clone, which target
73
+ ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh --clean # removes it (marker-guarded)
74
+ ```
75
+
76
+ The default clone lives at `${TMPDIR:-/tmp}/dsh-dogfood/<worktree>-<checkout-id>`
77
+ and survives until `--clean` removes it. Cleanup validates the scratch-home
78
+ marker against its canonical home, source, checkout, and profile.
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Link policy for the dogfood clone.
4
+ *
5
+ * A cloned home is a sandbox: a dogfood run may write only inside it. Two kinds
6
+ * of link are legitimate there — a package entry that resolves to an installed
7
+ * package or another checkout, and a link that stays inside the clone. A link
8
+ * that escapes is either copied in or refused, and which one depends on who
9
+ * writes the entry: the instruction files below are only read, so the bytes come
10
+ * along; everything else (credentials, settings, storages, sessions, profiles)
11
+ * is state the run may write, and a link out of the clone would let that write
12
+ * reach the developer's own home. Refusing is the safe answer there.
13
+ *
14
+ * Usage: clone-links.mjs check|materialize <home> <source>
15
+ */
16
+ import fs from 'node:fs'
17
+ import path from 'node:path'
18
+ import { pathToFileURL } from 'node:url'
19
+
20
+ /**
21
+ * Entries the surface only reads. A developer commonly links AGENTS.md into a
22
+ * shared tree, and refusing that would stop an otherwise safe run, so the copy
23
+ * is made instead. Nothing here may be added that any component writes.
24
+ */
25
+ export const INSTRUCTION_ENTRIES = new Set(['AGENTS.md', 'CLAUDE.md'])
26
+
27
+ /**
28
+ * Paths whose whole purpose is to point at an installed package, which may live
29
+ * outside the clone. A link here is expected; a link anywhere else is not.
30
+ */
31
+ export const packageEntry = (parts) => parts[0] === 'profiles' && (
32
+ parts[1] === 'node_modules' && parts.length > 2 ||
33
+ parts[2] === 'node_modules' && parts.length > 3 ||
34
+ parts[2] === '.dsh-module-fallback' && parts[3] === 'node_modules' && parts.length > 4
35
+ )
36
+
37
+ export const inside = (root, file) => file === root || file.startsWith(root + path.sep)
38
+
39
+ const instruction = (home, file) => {
40
+ const parts = path.relative(home, file).split(path.sep)
41
+ return parts.length === 1 && INSTRUCTION_ENTRIES.has(parts[0])
42
+ }
43
+
44
+ /**
45
+ * Absolute path a link target will occupy once it exists. A clone may hold a
46
+ * link to a file an install has not created yet, and the missing leaf can still
47
+ * sit under a link that crosses the clone boundary.
48
+ */
49
+ export function destination(file) {
50
+ let ancestor = file
51
+ while (!fs.existsSync(ancestor)) {
52
+ const parent = path.dirname(ancestor)
53
+ if (parent === ancestor) throw Error('cannot resolve cloned link: ' + file)
54
+ ancestor = parent
55
+ }
56
+ return path.resolve(fs.realpathSync(ancestor), path.relative(ancestor, file))
57
+ }
58
+
59
+ const entries = (root) => fs.readdirSync(root, { withFileTypes: true })
60
+ const partsOf = (home, file) => path.relative(home, file).split(path.sep)
61
+
62
+ /** Every reason the clone cannot be run as a sandbox, in the order they are met. */
63
+ export function problems(home, source) {
64
+ const found = []
65
+ const visit = (dir) => {
66
+ for (const entry of entries(dir)) {
67
+ const file = path.join(dir, entry.name)
68
+ const parts = partsOf(home, file)
69
+ if (entry.isSymbolicLink()) {
70
+ const target = destination(path.resolve(dir, fs.readlinkSync(file)))
71
+ const allowed = packageEntry(parts) ? !inside(source, target) : inside(home, target)
72
+ if (!allowed) found.push('unsafe cloned symlink escaping scratch or entering source home: ' + file)
73
+ } else if (entry.isDirectory()) {
74
+ visit(file)
75
+ } else if (entry.isFile() && !packageEntry(parts) && fs.lstatSync(file).nlink > 1) {
76
+ found.push('unsafe cloned hardlink outside package modules: ' + file)
77
+ }
78
+ }
79
+ }
80
+ visit(home)
81
+ return found
82
+ }
83
+
84
+ /** Copy every escaping instruction link in, so the clone can read it as its own. */
85
+ export function materialize(home) {
86
+ const report = { copied: 0, dropped: 0 }
87
+ const visit = (dir) => {
88
+ for (const entry of entries(dir)) {
89
+ const file = path.join(dir, entry.name)
90
+ if (entry.isSymbolicLink()) {
91
+ const parts = partsOf(home, file)
92
+ if (packageEntry(parts) || !instruction(home, file)) continue
93
+ const target = destination(path.resolve(dir, fs.readlinkSync(file)))
94
+ if (inside(home, target)) continue
95
+ if (!fs.existsSync(target)) {
96
+ fs.rmSync(file)
97
+ console.log('materialize: dropped dangling link ' + path.relative(home, file))
98
+ report.dropped += 1
99
+ continue
100
+ }
101
+ const stat = fs.statSync(target)
102
+ fs.rmSync(file, { recursive: true, force: true })
103
+ if (stat.isDirectory()) {
104
+ fs.cpSync(target, file, { recursive: true, dereference: true })
105
+ } else {
106
+ fs.copyFileSync(target, file)
107
+ fs.chmodSync(file, stat.mode & 0o777)
108
+ }
109
+ console.log('materialize: copied ' + path.relative(home, file))
110
+ report.copied += 1
111
+ continue
112
+ }
113
+ if (entry.isDirectory()) visit(file)
114
+ }
115
+ }
116
+ visit(home)
117
+ return report
118
+ }
119
+
120
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
121
+ const [mode, home, source] = process.argv.slice(2)
122
+ if (mode === 'check') {
123
+ const found = problems(home, source)
124
+ for (const problem of found) console.error(problem)
125
+ process.exitCode = found.length ? 1 : 0
126
+ } else if (mode === 'materialize') {
127
+ const report = materialize(home)
128
+ console.log('materialize: ' + report.copied + ' instruction link(s) copied, ' + report.dropped + ' dangling link(s) dropped')
129
+ } else {
130
+ console.error('usage: clone-links.mjs check|materialize <home> <source>')
131
+ process.exitCode = 2
132
+ }
133
+ }