@dsh-workbench/core 0.2.16 → 0.3.1

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 (193) hide show
  1. package/README.md +6 -2
  2. package/bin/dsh-workbench +17 -15
  3. package/client.js +359 -64
  4. package/dist/client/client.body.js +359 -64
  5. package/dist/client/index.js +658 -2978
  6. package/dist/host/index.js +443 -385
  7. package/dist/types/app/client.d.ts +3 -0
  8. package/dist/types/app/layouts/rightbar/BoardGrid.d.ts +23 -0
  9. package/dist/types/app/layouts/rightbar/board-columns.d.ts +17 -4
  10. package/dist/types/app/layouts/rightbar/rightbar-width.d.ts +16 -0
  11. package/dist/types/app/layouts/rightbar/use-board-split.d.ts +8 -0
  12. package/dist/types/modules/panels/model/diy-report.d.ts +13 -2
  13. package/dist/types/shared/api/host/diy-workshop.d.ts +13 -0
  14. package/dist/types/shared/api/host/runtime-seed.d.ts +0 -2
  15. package/docs/HANDOFF.md +43 -10
  16. package/docs/OPERATIONS.md +52 -0
  17. package/package.json +4 -3
  18. package/preset/diy.patch.yml +3 -0
  19. package/scripts/seed-workshop.sh +461 -13
  20. package/skills/workbench-module-diy/BUILD.md +11 -7
  21. package/skills/workbench-module-diy/DATASOURCE.md +3 -4
  22. package/skills/workbench-module-diy/DESIGN.md +3 -4
  23. package/skills/workbench-module-diy/KIT.md +11 -12
  24. package/skills/workbench-module-diy/LINKAGE.md +6 -7
  25. package/skills/workbench-module-diy/PANEL.md +9 -10
  26. package/skills/workbench-module-diy/REVIEW.md +2 -3
  27. package/skills/workbench-module-diy/SKILL.md +2 -3
  28. package/skills/workbench-module-diy/TOOLING.md +3 -8
  29. package/skills/workbench-module-diy/TOOLS.md +4 -9
  30. package/skills/workbench-module-diy/WIRING.md +3 -3
  31. package/dist/types/app/layouts/__tests__/async-feedback.test.d.ts +0 -1
  32. package/dist/types/app/layouts/__tests__/kit-preview-nav.test.d.ts +0 -1
  33. package/dist/types/app/layouts/__tests__/panel-hint.test.d.ts +0 -1
  34. package/dist/types/app/layouts/__tests__/project-selection.test.d.ts +0 -1
  35. package/dist/types/app/layouts/env-gate/__tests__/force-plan.test.d.ts +0 -1
  36. package/dist/types/app/layouts/env-gate/__tests__/gate-deadline.test.d.ts +0 -1
  37. package/dist/types/app/layouts/env-gate/__tests__/gate-hidden.test.d.ts +0 -1
  38. package/dist/types/app/layouts/env-gate/__tests__/gate-mocks.test.d.ts +0 -1
  39. package/dist/types/app/layouts/env-gate/gate-mock.d.ts +0 -18
  40. package/dist/types/app/layouts/market/__tests__/add-module-mock.test.d.ts +0 -1
  41. package/dist/types/app/layouts/market/__tests__/add-module-stages.test.d.ts +0 -1
  42. package/dist/types/app/layouts/market/__tests__/add-module-text.test.d.ts +0 -1
  43. package/dist/types/app/layouts/market/add-module-mock.d.ts +0 -14
  44. package/dist/types/app/layouts/rightbar/BoardMockStrip.d.ts +0 -5
  45. package/dist/types/app/layouts/rightbar/__tests__/board-fact.test.d.ts +0 -1
  46. package/dist/types/app/layouts/rightbar/__tests__/board-mock.test.d.ts +0 -1
  47. package/dist/types/app/layouts/rightbar/__tests__/board-plan.test.d.ts +0 -1
  48. package/dist/types/app/layouts/rightbar/__tests__/board-selection.test.d.ts +0 -1
  49. package/dist/types/app/layouts/rightbar/__tests__/breadcrumbs.test.d.ts +0 -1
  50. package/dist/types/app/layouts/rightbar/__tests__/market-catalog.test.d.ts +0 -1
  51. package/dist/types/app/layouts/rightbar/__tests__/market-change.test.d.ts +0 -1
  52. package/dist/types/app/layouts/rightbar/__tests__/market-dots.test.d.ts +0 -1
  53. package/dist/types/app/layouts/rightbar/__tests__/market-freshness.test.d.ts +0 -1
  54. package/dist/types/app/layouts/rightbar/__tests__/market-hint-visible.test.d.ts +0 -1
  55. package/dist/types/app/layouts/rightbar/__tests__/market-installed.test.d.ts +0 -1
  56. package/dist/types/app/layouts/rightbar/__tests__/market-live.test.d.ts +0 -1
  57. package/dist/types/app/layouts/rightbar/__tests__/market-mock.test.d.ts +0 -1
  58. package/dist/types/app/layouts/rightbar/__tests__/market-order.test.d.ts +0 -1
  59. package/dist/types/app/layouts/rightbar/__tests__/market-row-icon.test.d.ts +0 -1
  60. package/dist/types/app/layouts/rightbar/__tests__/market-rows-libs.test.d.ts +0 -1
  61. package/dist/types/app/layouts/rightbar/__tests__/market-rows-plugins.test.d.ts +0 -1
  62. package/dist/types/app/layouts/rightbar/__tests__/market-rows.test.d.ts +0 -1
  63. package/dist/types/app/layouts/rightbar/__tests__/market-running-display.test.d.ts +0 -1
  64. package/dist/types/app/layouts/rightbar/__tests__/market-section-icon.test.d.ts +0 -1
  65. package/dist/types/app/layouts/rightbar/__tests__/market-settle.test.d.ts +0 -1
  66. package/dist/types/app/layouts/rightbar/__tests__/market-shared.test.d.ts +0 -1
  67. package/dist/types/app/layouts/rightbar/__tests__/market-state-rules.test.d.ts +0 -1
  68. package/dist/types/app/layouts/rightbar/__tests__/market-workbench-act.test.d.ts +0 -1
  69. package/dist/types/app/layouts/rightbar/__tests__/market-workbench.test.d.ts +0 -1
  70. package/dist/types/app/layouts/rightbar/__tests__/panel-dismiss.test.d.ts +0 -1
  71. package/dist/types/app/layouts/rightbar/__tests__/panel-drag-target.test.d.ts +0 -1
  72. package/dist/types/app/layouts/rightbar/__tests__/panel-mock.test.d.ts +0 -1
  73. package/dist/types/app/layouts/rightbar/__tests__/panel-plugin-rows.test.d.ts +0 -1
  74. package/dist/types/app/layouts/rightbar/__tests__/panel-scope-ui.test.d.ts +0 -1
  75. package/dist/types/app/layouts/rightbar/__tests__/panel-write.test.d.ts +0 -1
  76. package/dist/types/app/layouts/rightbar/__tests__/suite-world.test.d.ts +0 -1
  77. package/dist/types/app/layouts/rightbar/__tests__/use-panels.test.d.ts +0 -1
  78. package/dist/types/app/layouts/rightbar/board-mock.d.ts +0 -22
  79. package/dist/types/app/layouts/rightbar/kit-preview-controls.d.ts +0 -11
  80. package/dist/types/app/layouts/rightbar/kit-preview-frame.d.ts +0 -18
  81. package/dist/types/app/layouts/rightbar/kit-preview-hash.d.ts +0 -1
  82. package/dist/types/app/layouts/rightbar/kit-preview-rows.d.ts +0 -6
  83. package/dist/types/app/layouts/rightbar/kit-preview.d.ts +0 -3
  84. package/dist/types/app/layouts/rightbar/market-installed-mock.d.ts +0 -9
  85. package/dist/types/app/layouts/rightbar/market-mock-types.d.ts +0 -58
  86. package/dist/types/app/layouts/rightbar/market-mock.d.ts +0 -4
  87. package/dist/types/app/layouts/rightbar/panel-mock.d.ts +0 -27
  88. package/dist/types/app/layouts/sidebar/__tests__/current-workspace.test.d.ts +0 -1
  89. package/dist/types/app/layouts/sidebar/__tests__/debug-ui.test.d.ts +0 -1
  90. package/dist/types/app/layouts/sidebar/__tests__/session-pick.test.d.ts +0 -1
  91. package/dist/types/app/layouts/sidebar/__tests__/session-row-ui.test.d.ts +0 -1
  92. package/dist/types/app/layouts/sidebar/__tests__/workspace-groups.test.d.ts +0 -1
  93. package/dist/types/app/layouts/sidebar/__tests__/workspace-name.test.d.ts +0 -1
  94. package/dist/types/app/units/__tests__/fixture-unit-mock.test.d.ts +0 -1
  95. package/dist/types/app/units/__tests__/manifests.test.d.ts +0 -1
  96. package/dist/types/app/units/__tests__/registry.test.d.ts +0 -1
  97. package/dist/types/app/units/fixture-unit-mock.d.ts +0 -7
  98. package/dist/types/dev/MockStrip.d.ts +0 -15
  99. package/dist/types/dev/surface-types.d.ts +0 -53
  100. package/dist/types/dev/surface.d.ts +0 -2
  101. package/dist/types/dev/surface.stub.d.ts +0 -2
  102. package/dist/types/modules/panels/__tests__/approval.test.d.ts +0 -1
  103. package/dist/types/modules/panels/__tests__/config.test.d.ts +0 -1
  104. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +0 -105
  105. package/dist/types/modules/panels/__tests__/diy-report.test.d.ts +0 -1
  106. package/dist/types/modules/panels/__tests__/diy-tools-health.test.d.ts +0 -1
  107. package/dist/types/modules/panels/__tests__/diy-tools-panel.test.d.ts +0 -1
  108. package/dist/types/modules/panels/__tests__/diy-tools-status.test.d.ts +0 -1
  109. package/dist/types/modules/panels/__tests__/diy-tools.test.d.ts +0 -1
  110. package/dist/types/modules/panels/__tests__/face.test.d.ts +0 -1
  111. package/dist/types/modules/panels/__tests__/market-bundle.test.d.ts +0 -1
  112. package/dist/types/modules/panels/__tests__/market-dependency.test.d.ts +0 -1
  113. package/dist/types/modules/panels/__tests__/market-dispatch.test.d.ts +0 -1
  114. package/dist/types/modules/panels/__tests__/market-env.test.d.ts +0 -1
  115. package/dist/types/modules/panels/__tests__/market-plugin.test.d.ts +0 -1
  116. package/dist/types/modules/panels/__tests__/market-registries.test.d.ts +0 -1
  117. package/dist/types/modules/panels/__tests__/market-route.test.d.ts +0 -1
  118. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +0 -45
  119. package/dist/types/modules/panels/__tests__/market-tools.test.d.ts +0 -1
  120. package/dist/types/modules/panels/__tests__/market-update.test.d.ts +0 -1
  121. package/dist/types/modules/panels/__tests__/market-workbench-update.test.d.ts +0 -1
  122. package/dist/types/modules/panels/__tests__/market.test.d.ts +0 -1
  123. package/dist/types/modules/panels/__tests__/plugin-actions.test.d.ts +0 -1
  124. package/dist/types/modules/panels/__tests__/plugin-ledger.test.d.ts +0 -1
  125. package/dist/types/modules/panels/__tests__/plugin-panel-market-parity.test.d.ts +0 -1
  126. package/dist/types/modules/panels/__tests__/plugin-panel.test.d.ts +0 -1
  127. package/dist/types/modules/panels/__tests__/plugin-section.test.d.ts +0 -1
  128. package/dist/types/modules/panels/__tests__/plugin-tool-units-harness.d.ts +0 -69
  129. package/dist/types/modules/panels/__tests__/plugin-tool-units.test.d.ts +0 -1
  130. package/dist/types/modules/panels/__tests__/prefs.test.d.ts +0 -1
  131. package/dist/types/modules/panels/__tests__/routes.test.d.ts +0 -1
  132. package/dist/types/modules/panels/__tests__/scope-machine.test.d.ts +0 -1
  133. package/dist/types/modules/panels/__tests__/scope-support.d.ts +0 -51
  134. package/dist/types/modules/panels/__tests__/scope.test.d.ts +0 -1
  135. package/dist/types/modules/panels/__tests__/service-release.test.d.ts +0 -1
  136. package/dist/types/modules/panels/__tests__/service.test.d.ts +0 -1
  137. package/dist/types/modules/panels/__tests__/tools.test.d.ts +0 -1
  138. package/dist/types/shared/__tests__/host-import-barrel.test.d.ts +0 -1
  139. package/dist/types/shared/__tests__/i18n-keys.test.d.ts +0 -1
  140. package/dist/types/shared/api/__tests__/context-ref.test.d.ts +0 -1
  141. package/dist/types/shared/api/host/__tests__/approval-note-injection.test.d.ts +0 -1
  142. package/dist/types/shared/api/host/__tests__/capability-force.test.d.ts +0 -1
  143. package/dist/types/shared/api/host/__tests__/cli-error.test.d.ts +0 -1
  144. package/dist/types/shared/api/host/__tests__/config-sync.test.d.ts +0 -1
  145. package/dist/types/shared/api/host/__tests__/context-inject.test.d.ts +0 -1
  146. package/dist/types/shared/api/host/__tests__/data-root.test.d.ts +0 -1
  147. package/dist/types/shared/api/host/__tests__/diy-debug-act.test.d.ts +0 -1
  148. package/dist/types/shared/api/host/__tests__/events.test.d.ts +0 -1
  149. package/dist/types/shared/api/host/__tests__/fact-writes.test.d.ts +0 -1
  150. package/dist/types/shared/api/host/__tests__/facts.test.d.ts +0 -1
  151. package/dist/types/shared/api/host/__tests__/host-args.test.d.ts +0 -1
  152. package/dist/types/shared/api/host/__tests__/long-task.test.d.ts +0 -1
  153. package/dist/types/shared/api/host/__tests__/panel-state-route.test.d.ts +0 -1
  154. package/dist/types/shared/api/host/__tests__/plugin-catalog.test.d.ts +0 -1
  155. package/dist/types/shared/api/host/__tests__/plugin-login-hint.test.d.ts +0 -1
  156. package/dist/types/shared/api/host/__tests__/plugin-source.test.d.ts +0 -1
  157. package/dist/types/shared/api/host/__tests__/plugin-tools.test.d.ts +0 -1
  158. package/dist/types/shared/api/host/__tests__/png-fixtures.d.ts +0 -17
  159. package/dist/types/shared/api/host/__tests__/post-channels.test.d.ts +0 -1
  160. package/dist/types/shared/api/host/__tests__/preflight-fixtures.d.ts +0 -19
  161. package/dist/types/shared/api/host/__tests__/preflight-warm.test.d.ts +0 -1
  162. package/dist/types/shared/api/host/__tests__/preflight.test.d.ts +0 -1
  163. package/dist/types/shared/api/host/__tests__/probe-cache.test.d.ts +0 -1
  164. package/dist/types/shared/api/host/__tests__/probe-libs.test.d.ts +0 -1
  165. package/dist/types/shared/api/host/__tests__/probe-units-cache.test.d.ts +0 -1
  166. package/dist/types/shared/api/host/__tests__/probe-units-npm-source.test.d.ts +0 -1
  167. package/dist/types/shared/api/host/__tests__/probe-units-release.test.d.ts +0 -1
  168. package/dist/types/shared/api/host/__tests__/probe-units.test.d.ts +0 -1
  169. package/dist/types/shared/api/host/__tests__/probe-verdict.test.d.ts +0 -1
  170. package/dist/types/shared/api/host/__tests__/profile-registries.test.d.ts +0 -1
  171. package/dist/types/shared/api/host/__tests__/provision-dependency.test.d.ts +0 -1
  172. package/dist/types/shared/api/host/__tests__/registry-declaration.test.d.ts +0 -1
  173. package/dist/types/shared/api/host/__tests__/release.test.d.ts +0 -1
  174. package/dist/types/shared/api/host/__tests__/self-update.test.d.ts +0 -1
  175. package/dist/types/shared/api/host/__tests__/tool-approval.test.d.ts +0 -1
  176. package/dist/types/shared/api/host/__tests__/tool-kit.test.d.ts +0 -1
  177. package/dist/types/shared/api/host/__tests__/tool-scope.test.d.ts +0 -1
  178. package/dist/types/shared/api/host/__tests__/tool-surface.test.d.ts +0 -1
  179. package/dist/types/shared/api/host/__tests__/unit-face.test.d.ts +0 -1
  180. package/dist/types/shared/api/host/__tests__/unit-readiness.test.d.ts +0 -1
  181. package/dist/types/shared/api/host/__tests__/unit-registry.test.d.ts +0 -1
  182. package/dist/types/shared/api/host/__tests__/unit-roster-facts.test.d.ts +0 -1
  183. package/dist/types/shared/api/host/__tests__/value-handoff.test.d.ts +0 -1
  184. package/dist/types/shared/api/host/__tests__/version-range.test.d.ts +0 -1
  185. package/dist/types/shared/api/host/__tests__/workspace-store.test.d.ts +0 -1
  186. package/dist/types/shared/api/host/__tests__/write-result.test.d.ts +0 -1
  187. package/dist/types/shared/hooks/__tests__/mock-mode.test.d.ts +0 -1
  188. package/dist/types/shared/hooks/__tests__/use-api-resource.test.d.ts +0 -1
  189. package/dist/types/shared/hooks/__tests__/use-capabilities.test.d.ts +0 -1
  190. package/dist/types/shared/hooks/__tests__/use-visible-busy.test.d.ts +0 -1
  191. package/dist/types/shared/hooks/mock-mode.d.ts +0 -2
  192. package/dist/types/shared/hooks/use-fixture-pick.d.ts +0 -20
  193. /package/dist/types/app/{__tests__/workbench-styles.test.d.ts → layouts/rightbar/rightbar-width.test.d.ts} +0 -0
@@ -12,7 +12,8 @@
12
12
  #
13
13
  # 用法:
14
14
  # bash scripts/seed-workshop.sh # 完整:骨架(缺才建)+ overrides + 依赖(缺才装)+ 入口
15
- # bash scripts/seed-workshop.sh entry # 只写入口(每次启动用;别的都不碰)
15
+ # bash scripts/seed-workshop.sh entry # 只写入口(每次启动用);顺手比一次代际,不一致就后台 sync
16
+ # bash scripts/seed-workshop.sh sync # 自愈:把工坊那三个包对齐到**当前外壳**那一代(后台跑)
16
17
  #
17
18
  # 幂等:存在的文件不覆盖、依赖在就跳过安装、重复跑不产生副作用。
18
19
 
@@ -43,6 +44,448 @@ else
43
44
  IN_REPO=0
44
45
  fi
45
46
 
47
+ # ── 工坊与「现在跑着的外壳」是不是同一代 ─────────────────────────────────────
48
+ #
49
+ # 不变量:工坊里那三个 @dsh-workbench/* 必须与**当前运行的外壳**同代。不同代时工具链会
50
+ # 静默地给出旧答案 —— 判据、模板、报错话术都属于它那一代,而 agent 读到的像是"你写的
51
+ # 声明错了"(实测:0.1.13 与 0.1.14 两台工具并存,全程一声不响)。
52
+ #
53
+ # **基准是显式输入**:`DSH_WB_SHELL_ROOT` = 正在跑的那份外壳的包根。谁在跑只有调用者知道 ——
54
+ # 宿主自举给它自己的包根、启动器给 profile 里那份 `node_modules/@dsh-workbench/core`。
55
+ # **跑脚本这份副本不算数**,这条是实测踩出来的(同一天两次):启动器那份 0.3.0、profile 里
56
+ # 真正加载的是 0.3.1、工坊已经跟到 0.3.1 —— 启动器先跑 entry(它在 `exec dsh` 之前),拿自己
57
+ # 那份当基准,于是把工坊**降级**到 0.3.0;宿主随后再跑 entry,看到工坊"与它同代" → no-op,
58
+ # 整场会话都停在错的一代。
59
+ #
60
+ # 三条纪律:
61
+ # · 判据只认**实装版本**(工坊 node_modules 里的 package.json);`.sync.json` 只用于退避
62
+ # 与说人话,绝不拿它当"装好了"的证据;
63
+ # · 启动路径(entry)只做一次**便宜比对**,不一致就后台自愈,不许把启动拖住 ——
64
+ # 宿主每次启动都会跑 entry(runtime-seed 的 entry plan),所以这里挂一下是零新增调用点;
65
+ # · 复核过才写 ok —— 不信 pnpm 的退出码(同 self-update 的 on_disk 复核、ensure-selected 的复核)。
66
+ #
67
+ # **没有基准就什么都不做**:不读工坊实装版本、不起 sync、不写 `.sync.json`、不动工坊一个字节。
68
+ # 这正是 `setup`(seed-profile.sh)那趟的情形 —— 那时 profile 里那份还没装/还是旧的,
69
+ # 代际对齐归"启动时"。**基准从不取"跑脚本那份副本"**,否则谁后跑谁说了算。
70
+ #
71
+ # 冻结接口:状态文件 $DSH_HOME/workbench/diy/.sync.json =
72
+ # { state: "ok"|"syncing"|"failed", version: <基准版本>, at: <ISO>, reason?: <一句话> }
73
+ SHELL_ROOT="${DSH_WB_SHELL_ROOT:-}"
74
+ DIY_DIR="$DSH_HOME/workbench/diy"
75
+ SEED_LOG="$DSH_HOME/workbench/seed.log"
76
+ SYNC_STATE="$DIY_DIR/.sync.json"
77
+ SYNC_LOCK="$DIY_DIR/.sync.lock"
78
+ SYNC_BACKOFF_SECONDS=3600
79
+ SYNC_LOCK_STALE_MINUTES=15
80
+ SYNC_LOCK_WAIT_SECONDS=5
81
+
82
+ # 基准版本:只有**显式给了基准**、且它有可读的 package.json 时才有值。空 = 这一轮不做比对。
83
+ shell_version() {
84
+ [ -n "$SHELL_ROOT" ] || return 0
85
+ [ -f "$SHELL_ROOT/package.json" ] || return 0
86
+ node -p "require('$SHELL_ROOT/package.json').version" 2>/dev/null || true
87
+ }
88
+
89
+ # 跑脚本这份副本自己的版本:**只给骨架用**(setup 那趟没有基准,而它要写下的正是"这份副本
90
+ # 这一代" —— profile 就是照它装的)。比对**永远不用**它:它可能比 profile 里那份旧(见上面那段)。
91
+ copy_version() {
92
+ node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || true
93
+ }
94
+
95
+ # 要装的**目标版本**:有基准用基准;没基准(setup 建骨架)用这份副本。
96
+ target_version() {
97
+ local version
98
+ version="$(shell_version)"
99
+ [ -n "$version" ] || version="$(copy_version)"
100
+ printf '%s\n' "$version"
101
+ }
102
+
103
+ # 三个包的装法**只有一处真相**:仓库副本 link 进本仓,安装副本按目标版本装。
104
+ # skeleton 与 sync 都调它 —— 两份规则漂开的症状是"根清单是 link、overrides 是 ^",
105
+ # 于是同一个工坊里两代并存(正是要防的那件事),而且不报错。
106
+ #
107
+ # 这一套值**同时**写进两个地方(devDependencies 与 pnpm.overrides),所以它必须是两边都常规的形状:
108
+ # · 安装态 = 纯范围 `^<版本>`。**别写成 npm 的具名 spec `@包名@^版本`**:那形状放进 overrides 要再
109
+ # 过一遍 pnpm 的 spec 解析,同一形状还可能被当 alias 处理,而 `"@scope/pkg": "^1.2.3"` 才是
110
+ # overrides 的常规写法 —— 这是每次安装都走的 happy path(Lead 的独立探针抓到的就是这一处)。
111
+ # · 仓库态 = `link:<路径>`:link 本仓那份,改工具链源码工坊当场生效。
112
+ compute_specs() {
113
+ if [ "$IN_REPO" = "1" ]; then
114
+ CORE_SPEC="link:$REPO_DIR"
115
+ TOOLS_SPEC="link:$(cd "$REPO_DIR/../module-tools" && pwd)"
116
+ SHARED_SPEC="link:$(cd "$REPO_DIR/../shared" && pwd)"
117
+ else
118
+ local version="${1:-}"
119
+ [ -n "$version" ] || version="$(target_version)"
120
+ [ -n "$version" ] || version=0
121
+ CORE_SPEC="^$version"
122
+ TOOLS_SPEC="^$version"
123
+ SHARED_SPEC="^$version"
124
+ fi
125
+ }
126
+
127
+ # 日志**只有一处**去处:$DSH_HOME/workbench/seed.log(冻结接口)。
128
+ # 后台那条路的 stdout/stderr 已被入口重定向进同一个文件,所以这里只在**终端**上补一份回显
129
+ # (`[ -t 1 ]` 为假 = 有人正把输出收进文件,那就别再写第二遍)。
130
+ #
131
+ # ⚠️ 变量后面紧挨着中文时一律写 `${VAR}`:这台机器的 bash 是 3.2(macOS 自带那份),
132
+ # 它会把多字节字符的**头一个字节**吃进变量名 —— 实测 `$shared_v;` 报 `shared_v?: unbound
133
+ # variable`(`set -u` 下当场死),而同一份脚本在 bash 5 上没事。下面每条话术都照这个写。
134
+ log_line() {
135
+ mkdir -p "$(dirname "$SEED_LOG")" 2>/dev/null || true
136
+ printf '%s\n' "$*" >>"$SEED_LOG" 2>/dev/null || true
137
+ if [ -t 1 ]; then printf '%s\n' "$*"; fi
138
+ }
139
+
140
+ # ── 工坊**自己带着** DIY 的指引:这条软链只有一份写口 ──────────────────────────
141
+ #
142
+ # 指引住进工坊的理由:DIY 会话的 cwd 就是工坊,而 dsh 的 skill-filesystem 是从 **cwd 下的
143
+ # `.agents/skills/`** 发现技能的(本仓自己那条 `module-design` 就是这么被看见的)—— 于是
144
+ # 进工坊的会话天然看得见它,**不用去改用户任何默认设置**(改默认 preset 会连带把用户后面
145
+ # 新建的普通会话也变成 DIY 模式)。
146
+ #
147
+ # ⚠️ 写口只有这一处,而且故意放在**本脚本**里:启动器(`bin/dsh-workbench`)与宿主自举
148
+ # (`src/shared/api/host/runtime-seed.ts`)都会跑它,于是"挂指引"只有一份实现。曾经是两处
149
+ # 各写一遍(启动器一段 bash + TS 的 `linkSkills`),写的是同一个路径、目标也一致 —— 那是
150
+ # **同一件事两个实现**:哪天只有一边改了源或判据,就会每次启动互相改指,症状是"会话里那份
151
+ # 指引时新时旧",还是**不报错**(软链指到哪 skill 发现都不吭声)。
152
+ #
153
+ # 源优先**基准**(正在跑的那份外壳,由调用者显式给)那份,没有才退回跑脚本这份副本;
154
+ # 两者都没有(包还没做完)就**不挂** —— 挂出来是条死链,而 skill 发现对死链不报错。
155
+ # 幂等:`readlink` 已经等于源就**一个字节不写**(entry 每次启动都会跑到这里)。
156
+ # 这条与代际无关:**不要**把它挂在"有基准"的判断上 —— 挂指引不是代际决策。
157
+ link_workshop_skills() {
158
+ local src="$REPO_DIR/skills/workbench-module-diy"
159
+ if [ -n "$SHELL_ROOT" ] && [ -d "$SHELL_ROOT/skills/workbench-module-diy" ]; then
160
+ src="$SHELL_ROOT/skills/workbench-module-diy"
161
+ fi
162
+ [ -d "$src" ] || return 0
163
+
164
+ local link="$DIY_DIR/.agents/skills/workbench-module-diy"
165
+ if [ "$(readlink "$link" 2>/dev/null || true)" = "$src" ]; then return 0; fi
166
+ # 那儿要是别人的**真目录**(不是我们建的软链)就不碰它:`ln -s` 会往它里面再套一层。
167
+ if [ -e "$link" ] && [ ! -L "$link" ]; then
168
+ echo "→ ⚠️ $link 已存在且不是软链 —— 不动它(手工处理,或先把它挪走);这条不影响启动。" >&2
169
+ return 0
170
+ fi
171
+
172
+ # 先 rm -f:旧软链还在时 `ln -s` 会顺着它把新链**套进去**(symlink 指向 symlink)。
173
+ mkdir -p "$(dirname "$link")" 2>/dev/null || true
174
+ rm -f "$link" 2>/dev/null || true
175
+ if ln -s "$src" "$link" 2>/dev/null; then
176
+ echo "→ 工坊指引已链上:$link → $src"
177
+ else
178
+ # 挂不上只留一句话,不当场炸:这条是"锦上添花",不该把启动带崩(自举同理)。
179
+ echo "→ ⚠️ DIY 指引没挂上(${link})—— 手工从 ${src} 建一条软链即可;这条不影响启动。" >&2
180
+ fi
181
+ }
182
+
183
+ # 一次 node 调用把四份 package.json 的版本读出来(基准 / core / module-tools / shared,一行一份)。
184
+ # 为什么合成一次:entry 在启动路径上,不该为比一次版本起四次进程。读不到的给空行 —— 空 =
185
+ # "工坊还没装好 / 这个包不在",是**一个**信号,调用方按"什么都不做"处理。
186
+ # 第一行读的是**基准**($SHELL_ROOT),不是跑脚本这份副本 —— 见上面那段漂移实测。
187
+ read_generations() {
188
+ node -e '
189
+ const fs = require("node:fs");
190
+ const path = require("node:path");
191
+ const versionOf = (file) => {
192
+ try { return String(JSON.parse(fs.readFileSync(file, "utf8")).version || ""); } catch { return ""; }
193
+ };
194
+ const [shellRoot, diy] = process.argv.slice(1);
195
+ const installed = (name) => versionOf(path.join(diy, "node_modules", "@dsh-workbench", name, "package.json"));
196
+ // 空基准要显式挡住:`path.join("", "package.json")` 会去读**当前目录**那份,那是巧合不是判据。
197
+ const shell = shellRoot === "" ? "" : versionOf(path.join(shellRoot, "package.json"));
198
+ process.stdout.write([shell, installed("core"), installed("module-tools"), installed("shared")].join("\n") + "\n");
199
+ ' "$SHELL_ROOT" "$DIY_DIR" 2>/dev/null || true
200
+ }
201
+
202
+ # 退避:上一轮在**同一代**上失败过、且还在窗口内 → 不重试。
203
+ #
204
+ # 为什么必须退避:entry 每次启动都跑,不自愈的话一个装不动的工坊会在每次启动时后台起一轮
205
+ # pnpm(几十秒 + 网络),用户只觉得"启动变慢了",日志里也看不出是谁。窗口按**版本**算:
206
+ # 外壳换了一代就是新问题,哪怕上一代失败过也要重试。
207
+ sync_backoff_active() {
208
+ local version="$1"
209
+ [ -f "$SYNC_STATE" ] || return 1
210
+ node -e '
211
+ const fs = require("node:fs");
212
+ try {
213
+ const state = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
214
+ if (state.state !== "failed" || state.version !== process.argv[2]) process.exit(1);
215
+ const at = Date.parse(state.at);
216
+ process.exit(Number.isFinite(at) && Date.now() - at < Number(process.argv[3]) * 1000 ? 0 : 1);
217
+ } catch { process.exit(1); }
218
+ ' "$SYNC_STATE" "$version" "$SYNC_BACKOFF_SECONDS"
219
+ }
220
+
221
+ # 状态文件的写口只有这一个:手拼 JSON 会在 reason 带引号/换行时写出读不回来的文件。
222
+ write_sync_state() {
223
+ node -e '
224
+ const fs = require("node:fs");
225
+ const [file, state, version, reason] = process.argv.slice(1);
226
+ const body = { state, version, at: new Date().toISOString() };
227
+ if (reason) body.reason = reason;
228
+ fs.writeFileSync(file, JSON.stringify(body, null, 2) + "\n");
229
+ ' "$SYNC_STATE" "$1" "$2" "${3:-}"
230
+ }
231
+
232
+ # 原子锁:`mkdir` 是唯一"失败=已被占"的原子原语(`test -e` + 写文件那条路有 TOCTOU)。
233
+ # 两个实例共享同一个 DSH_HOME 时会撞(用户开两份工作台、或启动器与宿主自举同时跑)。
234
+ # 过期判据必须有:持锁者被 kill -9 时不会有人来解锁,没有它工坊会被**永久**锁死。
235
+ #
236
+ # 锁目录里落三样东西:
237
+ # · `target` —— 这一轮要装到**哪一代**。锁的真相是"目标",不是"有人在跑":同一个 DSH_HOME
238
+ # 下两个 profile 版本不同时(dsh web 与独立实例并存)会撞,那时"后到的那一代"不能永远
239
+ # 没发言权 —— 目标不同就等一轮再抢(见 acquire_sync_lock)。
240
+ # · `owner` —— 谁持的锁。过期判据一旦误判、别人清了锁重抢,原持锁者的 EXIT trap 醒来时
241
+ # **不能**去 `rm -rf` 别人的锁(那会把新持锁者的 install 砍在半路)。
242
+ # · `heartbeat` —— "我还在跑"的进度信号(见 sync_lock_stale)。
243
+ lock_field() {
244
+ [ -f "$SYNC_LOCK/$1" ] || return 0
245
+ head -n 1 "$SYNC_LOCK/$1" 2>/dev/null || true
246
+ }
247
+
248
+ # 过期判据按**进度**判活,**不看锁目录的 mtime**:一次 `pnpm install` 跑十几二十分钟是常事,
249
+ # 只看目录 mtime 会把它判成"持锁者已崩" → 别人清锁重抢 → 两个 install 并发(且原持锁者的
250
+ # trap 还会删掉新锁)。持锁者每 30 秒碰一下 `heartbeat`,判据读的是它;没有 heartbeat
251
+ # (老锁 / 刚建好还没来得及碰)才退回锁目录 mtime。
252
+ sync_lock_stale() {
253
+ local stamp="$SYNC_LOCK"
254
+ [ -f "$SYNC_LOCK/heartbeat" ] && stamp="$SYNC_LOCK/heartbeat"
255
+ [ -n "$(find "$stamp" -maxdepth 0 -mmin +"$SYNC_LOCK_STALE_MINUTES" 2>/dev/null)" ]
256
+ }
257
+
258
+ # 强行清锁:只给"判定持锁者已崩"那条路用(那时锁已经不是原主的了)。
259
+ drop_sync_lock() {
260
+ case "${SYNC_LOCK:-}" in
261
+ */workbench/diy/.sync.lock) rm -rf "$SYNC_LOCK" ;;
262
+ esac
263
+ }
264
+
265
+ # 抢锁的返回值:0 = 抢到;1 = 同一代的人正装着(跳过);2 = 这地方建不出锁;3 = 另一代正装着、等过一轮没抢到。
266
+ acquire_sync_lock() {
267
+ local target="$1" attempt=0 waited=0 held=""
268
+ while [ "$attempt" -lt 2 ]; do
269
+ if mkdir "$SYNC_LOCK" 2>/dev/null; then
270
+ SYNC_LOCK_TOKEN="$$-$(date +%s)-${RANDOM:-0}"
271
+ printf '%s\n' "$target" >"$SYNC_LOCK/target" 2>/dev/null || true
272
+ printf '%s\n' "$SYNC_LOCK_TOKEN" >"$SYNC_LOCK/owner" 2>/dev/null || true
273
+ touch "$SYNC_LOCK/heartbeat" 2>/dev/null || true
274
+ return 0
275
+ fi
276
+ # mkdir 失败**且锁不在** = 这个目录建不出东西(权限 / 只读),不是"别人正持锁" ——
277
+ # 这两种要分开:说成"另一轮正在跑"会让日志把权限问题指成并发问题。
278
+ if [ ! -d "$SYNC_LOCK" ]; then return 2; fi
279
+ if sync_lock_stale; then
280
+ log_line "→ 自愈锁超过 ${SYNC_LOCK_STALE_MINUTES} 分钟没有进度(心跳也没动过),当持锁者已崩,清掉重抢"
281
+ drop_sync_lock
282
+ attempt=$((attempt + 1))
283
+ continue
284
+ fi
285
+ held="$(lock_field target)"
286
+ # 目标是同一代:谁装都一样,让他装完就行,不必排队。
287
+ if [ "$held" = "$target" ]; then return 1; fi
288
+ # 目标是**另一代**:等一小会再抢一次(只等一轮),别让后到的那一代永远排不上。
289
+ if [ "$waited" -eq 0 ]; then
290
+ log_line "→ 自愈锁正在装 ${held:-未知} 那一代,我方目标是 ${target} —— 等 ${SYNC_LOCK_WAIT_SECONDS} 秒再抢一次"
291
+ sleep "$SYNC_LOCK_WAIT_SECONDS"
292
+ waited=1
293
+ continue
294
+ fi
295
+ return 3
296
+ done
297
+ return 1
298
+ }
299
+
300
+ # 持锁期间的"我还在跑":每 30 秒碰一下 heartbeat。锁没了、或者已经不是自己的锁了
301
+ # (被判过期、别人清了重抢)就自己退出 —— 别给**别人的**锁碰心跳,也别留孤儿进程。
302
+ start_sync_heartbeat() {
303
+ (
304
+ while [ -d "$SYNC_LOCK" ] && [ "$(lock_field owner)" = "$SYNC_LOCK_TOKEN" ]; do
305
+ touch "$SYNC_LOCK/heartbeat" 2>/dev/null || exit 0
306
+ sleep 30
307
+ done
308
+ ) &
309
+ SYNC_HEARTBEAT_PID=$!
310
+ }
311
+
312
+ stop_sync_heartbeat() {
313
+ [ -n "${SYNC_HEARTBEAT_PID:-}" ] || return 0
314
+ kill "$SYNC_HEARTBEAT_PID" 2>/dev/null || true
315
+ SYNC_HEARTBEAT_PID=""
316
+ }
317
+
318
+ # 只删**自己的**锁:锁被清过 / 换了主人时,这里必须放手(否则新持锁者的 install 被砍在半路)。
319
+ release_sync_lock() {
320
+ case "${SYNC_LOCK:-}" in
321
+ */workbench/diy/.sync.lock)
322
+ if [ -n "${SYNC_LOCK_TOKEN:-}" ] && [ "$(lock_field owner)" = "$SYNC_LOCK_TOKEN" ]; then
323
+ rm -rf "$SYNC_LOCK"
324
+ fi
325
+ ;;
326
+ esac
327
+ }
328
+
329
+ # sync:幂等、可重跑、可续。**复核过才写 ok**。
330
+ run_sync() {
331
+ # 没有基准 = 不知道谁是"正在跑的外壳":这一轮**一个字节都不动**,也不写 `.sync.json`。
332
+ # (`setup` 那趟就是这种情形:那时 profile 里那份还没装/还是旧的。)
333
+ [ -n "$SHELL_ROOT" ] || return 0
334
+ [ -f "$SHELL_ROOT/package.json" ] || return 0
335
+ # 工坊还没建:那是 skeleton 模式的活,sync 不抢(首次要下几百 MB,不能挂在这条路上)。
336
+ [ -f "$DIY_DIR/package.json" ] || return 0
337
+
338
+ local shell_v reason=""
339
+ shell_v="$(shell_version)"
340
+ if [ -z "$shell_v" ]; then
341
+ log_line "→ ⚠️ 基准 ${SHELL_ROOT} 里读不到版本,这一轮自愈没跑"
342
+ return 1
343
+ fi
344
+
345
+ # 仓库副本:三个包都是 link 进本仓的,改源码工坊当场就变 —— 天然同代。
346
+ if [ "$IN_REPO" = "1" ]; then
347
+ write_sync_state ok "$shell_v" ""
348
+ log_line "→ 工坊是仓库副本(link:),天然同代,写 ok(${shell_v})"
349
+ return 0
350
+ fi
351
+
352
+ if sync_backoff_active "$shell_v"; then
353
+ log_line "→ 同代的上一次自愈失败过,还在一小时退避窗口内 —— 这一轮跳过(原因在 ${SYNC_STATE})"
354
+ return 0
355
+ fi
356
+
357
+ local lock_rc=0
358
+ acquire_sync_lock "$shell_v" || lock_rc=$?
359
+ if [ "$lock_rc" = "2" ]; then
360
+ reason="建不出自愈锁(${SYNC_LOCK})—— ${DIY_DIR} 不可写?"
361
+ log_line "→ ⚠️ 自愈失败:${reason}"
362
+ write_sync_state failed "$shell_v" "$reason"
363
+ return 1
364
+ fi
365
+ if [ "$lock_rc" = "1" ]; then
366
+ log_line "→ 同一代(${shell_v})的自愈正持锁(${SYNC_LOCK})—— 这一轮跳过"
367
+ return 0
368
+ fi
369
+ if [ "$lock_rc" = "3" ]; then
370
+ log_line "→ 另一代的自愈正持锁,等过一轮仍没抢到 —— 这一轮跳过(下一轮启动再试)"
371
+ return 0
372
+ fi
373
+ trap 'stop_sync_heartbeat; release_sync_lock' EXIT
374
+ start_sync_heartbeat
375
+
376
+ write_sync_state syncing "$shell_v" ""
377
+ log_line "→ 自愈:把工坊对齐到基准那一代 $shell_v"
378
+
379
+ compute_specs "$shell_v"
380
+
381
+ # 重写工坊根清单:devDependency 与 overrides 都对齐到这一代。
382
+ # overrides 是**关键的那一半** —— 模块自己声明的 `^0.1.x` 会让 pnpm 给每个模块另装一份
383
+ # **源上**的包,于是同一个工坊里两代工具链并存(那场事故见下面骨架段)。
384
+ # 两处写的是**同一套值**:`compute_specs` 给的就是"同时能当 devDependency 与 overrides 用"的形状。
385
+ # ⚠️ 一份值只从一个地方取:曾经 devDependencies 取 `$shell_v`、overrides 取 `$CORE_SPEC`,
386
+ # 两个变量在正常路径上恰好相等,于是"它们漂开"这种病**判据抓不到**(变异测试当场露馅)。
387
+ # 先说一句为什么先读一遍:清单坏了(手改坏 / 写了一半)时 JSON.parse 会当场抛,
388
+ # 那会带着 `set -e` 直接退出去、状态停在 syncing —— 用户只看到"每次启动都重试"。
389
+ if ! node -e 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"))' \
390
+ "$DIY_DIR/package.json" >>"$SEED_LOG" 2>&1; then
391
+ reason="工坊根清单读不了($DIY_DIR/package.json)—— 它坏了,先把它修回一份合法 JSON"
392
+ write_sync_state failed "$shell_v" "$reason"
393
+ log_line "→ ⚠️ 自愈失败:${reason}"
394
+ return 1
395
+ fi
396
+ node - "$DIY_DIR/package.json" "$CORE_SPEC" "$TOOLS_SPEC" "$SHARED_SPEC" <<'NODE'
397
+ const [file, core, tools, shared] = process.argv.slice(2);
398
+ const fs = require('node:fs');
399
+ const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
400
+ const want = {
401
+ '@dsh-workbench/core': core,
402
+ '@dsh-workbench/module-tools': tools,
403
+ '@dsh-workbench/shared': shared,
404
+ };
405
+ manifest.devDependencies = { ...(manifest.devDependencies ?? {}), ...want };
406
+ manifest.pnpm = {
407
+ ...(manifest.pnpm ?? {}),
408
+ overrides: { ...(manifest.pnpm?.overrides ?? {}), ...want },
409
+ };
410
+ fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
411
+ NODE
412
+
413
+ if ! command -v pnpm >/dev/null 2>&1; then
414
+ reason="PATH 上没有 pnpm;装好 pnpm 后重跑:cd $DIY_DIR && pnpm install"
415
+ write_sync_state failed "$shell_v" "$reason"
416
+ log_line "→ ⚠️ 自愈失败:$reason"
417
+ return 1
418
+ fi
419
+
420
+ log_line "→ 在工坊里 pnpm install(增量,用 .npmrc 里那份 store/cache)"
421
+ local out="" ok=1
422
+ out="$( (cd "$DIY_DIR" && pnpm install) 2>&1 )" || ok=0
423
+ # 原话照录:失败时"为什么"全在这几行里,不能让用户只看到一句"装不上"。
424
+ [ -z "$out" ] || printf '%s\n' "$out" >>"$SEED_LOG" 2>/dev/null || true
425
+
426
+ # 复核实装版本 —— 这一条才是判据,pnpm 的退出码不是。
427
+ local v_shell v_core v_tools v_shared
428
+ {
429
+ read -r v_shell || true
430
+ read -r v_core || true
431
+ read -r v_tools || true
432
+ read -r v_shared || true
433
+ } < <(read_generations)
434
+
435
+ if [ "$v_core" = "$shell_v" ] && [ "$v_tools" = "$shell_v" ] && [ "$v_shared" = "$shell_v" ]; then
436
+ write_sync_state ok "$shell_v" ""
437
+ log_line "→ 自愈完成:工坊与基准同代(${shell_v})"
438
+ return 0
439
+ fi
440
+
441
+ reason="install 之后复核仍不同代:core=${v_core:-读不到} tools=${v_tools:-读不到} shared=${v_shared:-读不到}(目标 ${shell_v})"
442
+ write_sync_state failed "$shell_v" "$reason"
443
+ [ "$ok" = "1" ] || log_line "→ pnpm install 退出码非 0(上面是它的原话)"
444
+ log_line "→ ⚠️ 自愈失败:$reason"
445
+ return 1
446
+ }
447
+
448
+ # entry 的挂钩:便宜比对一次;不一致就**后台**自愈并立刻返回(启动路径不许被拖住)。
449
+ #
450
+ # 没基准(`DSH_WB_SHELL_ROOT` 没设)→ **什么都不做**:不读工坊实装版本、不起 sync、不写状态。
451
+ # 读不到版本(工坊还没装好 / 首次)→ **也不做**:那是 skeleton 模式的活,
452
+ # 把首次安装搬进 entry 就是让第一次启动卡在几百 MB 的下载上。
453
+ maybe_start_workshop_sync() {
454
+ [ -n "$SHELL_ROOT" ] || return 0
455
+ [ -f "$SHELL_ROOT/package.json" ] || return 0
456
+ [ -f "$DIY_DIR/package.json" ] || return 0
457
+
458
+ local shell_v core_v tools_v shared_v
459
+ {
460
+ read -r shell_v || true
461
+ read -r core_v || true
462
+ read -r tools_v || true
463
+ read -r shared_v || true
464
+ } < <(read_generations)
465
+
466
+ [ -n "$shell_v" ] && [ -n "$core_v" ] && [ -n "$tools_v" ] && [ -n "$shared_v" ] || return 0
467
+ if [ "$core_v" = "$shell_v" ] && [ "$tools_v" = "$shell_v" ] && [ "$shared_v" = "$shell_v" ]; then
468
+ return 0
469
+ fi
470
+ if sync_backoff_active "$shell_v"; then
471
+ return 0
472
+ fi
473
+
474
+ mkdir -p "$(dirname "$SEED_LOG")" 2>/dev/null || true
475
+ log_line "→ 工坊与基准不同代(实装 core=$core_v tools=$tools_v shared=${shared_v};基准 ${SHELL_ROOT} = ${shell_v})—— 后台自愈"
476
+ # 两个 fd 都重定向进 seed.log:一来日志只有一处,二来后台子进程**不拖住调用者的管道**
477
+ # (不然 execFileSync 那类调用会一直等它,entry 看起来就像卡住了)。
478
+ # 基准要**显式传下去**:子进程是另一个 shell,不能指望环境里那份一定还在。
479
+ DSH_HOME="$DSH_HOME" DSH_WB_SHELL_ROOT="$SHELL_ROOT" nohup bash "$SELF" sync >>"$SEED_LOG" 2>&1 &
480
+ return 0
481
+ }
482
+
483
+ # sync 可以手敲,也是 entry 挂钩拉起来的那条路。
484
+ if [ "$MODE" = "sync" ]; then
485
+ run_sync || exit $?
486
+ exit 0
487
+ fi
488
+
46
489
  # ── 稳定入口:$DSH_HOME/workbench/bin/dsh-wb-module ──────────────────────────
47
490
  #
48
491
  # 为什么要它:指引让 agent 写 `pnpm exec dsh-wb-module …`(到处都对,但它依赖 cwd 在工坊里、
@@ -69,6 +512,9 @@ chmod +x "$ENTRY_DIR/dsh-wb-module"
69
512
 
70
513
  if [ "$MODE" = "entry" ]; then
71
514
  echo "→ DIY 工坊入口:$ENTRY_DIR/dsh-wb-module"
515
+ # 挂指引**不**看基准(每次启动都跑一遍,幂等):它是"锦上添花",不是代际决策。
516
+ link_workshop_skills
517
+ maybe_start_workshop_sync
72
518
  exit 0
73
519
  fi
74
520
 
@@ -85,19 +531,9 @@ fi
85
531
  # 才不会静默跳过 —— 跳过等于没验)。
86
532
  #
87
533
  # 两个身份同 bin/dsh-workbench:**仓库副本**把三个 @dsh-workbench/* 用 link 指进本仓
88
- # (改工具链立刻生效),**安装包副本**从源上按版本装。
89
- DIY_DIR="$DSH_HOME/workbench/diy"
534
+ # (改工具链立刻生效),**安装包副本**从源上按版本装。判据与值都归上面的 `compute_specs`。
90
535
  mkdir -p "$DIY_DIR/packages"
91
-
92
- if [ "$IN_REPO" = "1" ]; then
93
- CORE_SPEC="link:$REPO_DIR"
94
- TOOLS_SPEC="link:$(cd "$REPO_DIR/../module-tools" && pwd)"
95
- SHARED_SPEC="link:$(cd "$REPO_DIR/../shared" && pwd)"
96
- else
97
- CORE_SPEC="@dsh-workbench/core@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
98
- TOOLS_SPEC="@dsh-workbench/module-tools@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
99
- SHARED_SPEC="@dsh-workbench/shared@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
100
- fi
536
+ compute_specs
101
537
 
102
538
  # 工坊必须**钉住自己那一代 pnpm**(与仓库根钉的同一个)。
103
539
  #
@@ -214,3 +650,15 @@ else
214
650
  echo "→ 装工坊依赖(第一次会下载 react / typescript / vitest,几百 MB,之后不再下)"
215
651
  (cd "$DIY_DIR" && pnpm install 2>&1 | tail -6)
216
652
  fi
653
+
654
+ # 完整的这一趟跑完:先挂指引(不管有没有基准 —— setup 那趟也要建起来),再看要不要自愈。
655
+ link_workshop_skills
656
+
657
+ # 完整的这一趟跑完,也顺手比一次代际 —— **有基准才算**。
658
+ #
659
+ # 这一趟有两个调用者:启动器(它 export 了 `DSH_WB_SHELL_ROOT` = profile 里那份)与
660
+ # `setup`(`seed-profile.sh`,**不设**基准)。前者比一下有意义:重跑 setup/自愈之后工坊当场
661
+ # 就跟上正在跑的那一代,不必等下一次启动被 entry 兜。后者在这一行**空转**(没基准 = 不动)——
662
+ # 那正是要的:setup 时 profile 里那份还没装/还是旧的,拿它当基准就是又一次"谁后跑谁说了算"。
663
+ # 不拖住收尾:`maybe_start_workshop_sync` 只做比对 + `nohup` 后台 spawn,立刻返回。
664
+ maybe_start_workshop_sync
@@ -21,7 +21,7 @@
21
21
  ## 1 落点:工坊
22
22
 
23
23
  ```
24
- $DSH_HOME/workbench/diy/ ← 工坊(一个 pnpm workspace,依赖已经装好)
24
+ $DSH_HOME/workbench/diy/ ← 工坊(一个 pnpm workspace,依赖已经装好、与外壳同代)
25
25
  package.json pnpm-workspace.yaml .npmrc node_modules/
26
26
  packages/
27
27
  module-<unit>/ ← 你要造的模块住这儿
@@ -31,6 +31,11 @@ $DSH_HOME/workbench/diy/ ← 工坊(一个 pnpm workspace,依赖
31
31
 
32
32
  ⚠️ **骨架不在就先补上**:启动器会自愈(少了才建),没建起来时跑一次 `dsh-workbench setup`(幂等)
33
33
  再回来跑模块命令 —— 没有骨架时门禁会把"模块不在任何 workspace 里"讲成"声明写错了",那是症状。
34
+ 工具链跟外壳**不同代**时不用你管:**每一次工作台启动**都会拿"正在跑的那份外壳"(基准经
35
+ `DSH_WB_SHELL_ROOT` 显式传入)比一次,不同代就在后台把它同步到同代 —— 所以升级之后
36
+ **下一次启动**就对上了(`workbench_diy_status` 里能看到「⏳ 工坊工具链正在后台同步到 <版本>」,
37
+ 或同步没成时的「⚠️ 工坊工具链还是上一代(…):<原因>」加一条修法;同步那几行在
38
+ `$DSH_HOME/workbench/seed.log`)。
34
39
 
35
40
  工坊里的 `@dsh-workbench/*` **只有一份**(工坊根的 `link:` + 一份 `pnpm.overrides` 钉着):模块自己的
36
41
  `pnpm build` / `pnpm exec dsh-wb-module` 和工坊根的 `.bin/dsh-wb-module` 是同一个二进制,
@@ -61,7 +66,7 @@ pnpm exec dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --temp
61
66
 
62
67
  | 文件 | 职责 |
63
68
  |---|---|
64
- | `package.json` | 自述清单:包名、`dsh.bundle.patch`、`dsh.client`、`dsh.workbench.unit`、`dsh.workbenchFaces{host,client}`(工具链字段,**不在协议里**,但门禁必填)、四个脚本、peer 依赖 |
69
+ | `package.json` | 自述清单:包名、`dsh.bundle.patch`、`dsh.client`、`dsh.workbench.unit`、`dsh.workbenchFaces{host,client}`(工具链字段,门禁必填)、四个脚本、peer 依赖 |
65
70
  | `cordis.patch.yml` | 把自己插进 profile 的那一行 |
66
71
  | `src/host.ts` | 宿主面:`manifest` + `mount()`(路由 / 工具) |
67
72
  | `src/client.tsx` | 浏览器面:`manifest` + `card` |
@@ -107,10 +112,9 @@ pnpm selfcheck # 构建 + 把 client.js 当加载器跑一遍(`diy` 骨架
107
112
  **自己的门禁自己过,红了别抛给用户。** 十一条判据在 [REVIEW.md](REVIEW.md) 里逐条翻了人话。
108
113
  ⚠️ **`check` 不替你跑 `pnpm test`**(它只看形状与产物):状态级的渲染、换算、边界都在你自己的
109
114
  `test` 里 —— 骨架开箱就带一份,改了数据源与渲染就顺手把它跑一遍,别让红线留在包里。
110
- ⚠️ **工坊里恒有两条是「不适用」,那不是「通过」**:`独立构建` 扫的是 `packages/*` 里的外壳源码
111
- (工坊里没有),`公开包无内部标识` 只扫要发出去的包(DIY 模块一律 `private`)。汇总行下面会把
112
- 这次没查的那几条逐条列出来;**要验它们,回工作台仓库根跑一次 `dsh-wb-module check`**。
113
- 所以「8 通过 · 2 不适用」不等于十一条全绿 —— 别把这句话念给用户听。
115
+ ⚠️ **工坊里恒有两条是「不适用」,那不是「通过」**:`独立构建` 要外壳源码在场才跑得起来,
116
+ `公开包无内部标识` 只扫要发出去的包(DIY 模块一律 `private`)。汇总行下面会把
117
+ 这次没查的那几条逐条列出来;所以「8 通过 · 2 不适用」不等于十一条全绿 —— 别把这句话念给用户听。
114
118
 
115
119
  装上板那一步不是命令,是一套 agent 工具(一个动词一把):`workbench_diy_list` 看工坊里有什么、
116
120
  `workbench_diy_status`(`unit=<单元 id>`)看它现在走到哪一步、`workbench_diy_install` 装上并上板、
@@ -145,6 +149,6 @@ export function apply(ctx) {
145
149
  "空"和"还没开始"是两件事,说错了他会去查他根本没配的东西。
146
150
 
147
151
  **卡片必须在拿不到数据时也画得出来**(第一次读、卡在审批、平台没登录)。
148
- 任何一种输入(待机 / 在读 / 有值 / 有值但空 / 读不到)下抛异常、屏上冒出 `undefined` / `NaN` 的卡片 = 用户眼里的"卡片一直不出来"。这是 `check:cards` 守的那条。
152
+ 任何一种输入(待机 / 在读 / 有值 / 有值但空 / 读不到)下抛异常、屏上冒出 `undefined` / `NaN` 的卡片 = 用户眼里的"卡片一直不出来"。这是 `check --only cards` 守的那条。
149
153
 
150
154
  → 拿到依赖、装上、改一行、发出去:[WIRING.md](WIRING.md)。
@@ -63,8 +63,7 @@
63
63
  那个站点"必须写在脚本第一行:`if (location.origin !== "https://…") return { ok: false, code: "not-on-site" }`。
64
64
  固化读的完整形状是**两拍**:不在站点上 → `open` 那个页 → 等一会儿(约 1 秒)再 eval 一遍。
65
65
  每一拍都由 shared 的 `backgroundLine` 拼(前缀 `env OPENCLI_WINDOW=background`)—— 别绕开它直接拼
66
- `opencli browser …`:漏一拍就是把用户的 Chrome 顶到面前一次(见
67
- [ADR-0036](../../../../docs/adr/0036-opencli-always-background.md))。
66
+ `opencli browser …`:漏一拍就是把用户的 Chrome 顶到面前一次。
68
67
  - **页面里没有接口时(DOM 读)不要用 `eval` 去 `document.querySelectorAll`**:产物边界判据会拦
69
68
  (宿主产物里不许出现 `document.` / `window.` 字样),而且那样解析只能窝在字符串里、测不了。
70
69
  探的时候 `workbench_diy_probe`(`url=…, css=<选择器>`)就是这个口子;
@@ -97,7 +96,7 @@
97
96
  | 超时 / 网络不通 | "这次没读到,稍后重试" + 重试 |
98
97
  | 站点改版、字段没了 | "读到的形状和预期不一样"(别显示空白,也别显示 0) |
99
98
 
100
- 任何一种输入(待机 / 在读 / 有值 / 有值但空 / 读不到)下抛异常、屏上冒出 `undefined` / `NaN` 的卡片 = 用户眼里的"卡片一直不出来"。这条 `check:cards` 会拦(见 [REVIEW.md](REVIEW.md))。
99
+ 任何一种输入(待机 / 在读 / 有值 / 有值但空 / 读不到)下抛异常、屏上冒出 `undefined` / `NaN` 的卡片 = 用户眼里的"卡片一直不出来"。这条 `check --only cards` 会拦(见 [REVIEW.md](REVIEW.md))。
101
100
 
102
101
  ## 错误码不是随便写的(阻断屏按它换文案)
103
102
 
@@ -119,4 +118,4 @@
119
118
 
120
119
  - 数据源写在**宿主半侧**,卡片只消费它的返回;
121
120
  - 只读路由出错时回 `{ ok: false, error: { code, message, hint? } }`(HTTP 仍是 200,外壳原样转发);
122
- - 验收时把「正常 / 空 / 出错」三条路都走一遍 —— 真机上造不出来的那几条,写清**怎么才能真看到**(换个空目录、换个不存在的路径那种),别为它写夹具(**2026-10-16 起模板与指引都不带评审夹具**)。
121
+ - 验收时把「正常 / 空 / 出错」三条路都走一遍 —— 真机上造不出来的那几条,写清**怎么才能真看到**(换个空目录、换个不存在的路径那种),并在模块自己的 `plugin.test.ts` 里把这几态各喂一遍。
@@ -14,7 +14,7 @@
14
14
  | 要 agent 替他做一两件**只读**的事 | **DIY 域** | 能力清单([TOOLS.md](TOOLS.md)) |
15
15
  | 要 agent 动真东西(提交、发布、改配置) | **DIY 域**,但必须写明点头那一步 | 预览 + 确认两拍([TOOLS.md](TOOLS.md) §5–§7) |
16
16
  | 他已经在浏览器里登录过那套系统,只是**没人告诉他数据在哪** | **DIY 域** | 用 opencli 复用他的登录去探([DATASOURCE.md](DATASOURCE.md)) |
17
- | 要**新凭据**(新账号 / 密钥 / token) | **工程域** | 停,说清要谁去配什么,回工作台仓库走工程流程 |
17
+ | 要**新凭据**(新账号 / 密钥 / token) | **工程域** | 停,说清要谁去配什么 —— 别在 DIY 里做 |
18
18
  | 一条流:多步、要等很久、中途要人、失败要能接着来 | **工程域** | 同上 |
19
19
  | 要新建服务 / 新进程 / 新依赖 | **工程域** | 同上 |
20
20
  | 要改工作台外壳,或改一份**已经发布**的模块 | **工程域** | 同上 |
@@ -101,9 +101,8 @@ key 固定 `idle|loading|ready|empty|unreadable`)+ `config[]` + `capabilities[
101
101
  `link` 这类**可选格**留空渲染成 `—`,不算空格。
102
102
 
103
103
  `design.json` 是**设计底稿**:只装人话。"这条能力在哪些工作区出现"**哪里都别写** ——
104
- 它既不在底稿、也不在清单:那是**用户**的事(他在 `workbench_panels_show` / `workbench_panels_hide` 里决定,见 [TOOLS.md](TOOLS.md) §12)。
105
- 协议里那个 `scope`(`tools[].scope` / `card.scope`)**已经删掉**:用户口径是「模块开发者不能定义 scope,
106
- scope 永远是用户来决定的」(见 [ADR-0038 §2.4 / §6.16](../../../../docs/adr/0038-module-declaration-protocol.md))。
104
+ 那是**用户**的事:用户口径是「模块开发者不能定义 scope,scope 永远是用户来决定的」,
105
+ 他在 `workbench_panels_show` / `workbench_panels_hide` 里决定(见 [TOOLS.md](TOOLS.md) §12)。
107
106
  `contracts` 只拿名字把底稿与清单对起来(键集合对账)。
108
107
 
109
108
  五态那五行是矩阵的行,**不能省**;但某一态对这份模块确实不适用时,写一句为什么就行 ——