dsh-foreman 0.1.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,95 +1,8 @@
1
1
  # Changelog
2
2
 
3
- 本仓库的历史从本项目自己的初始提交开始,**不携带上游 git 历史**(见 README 的「由来与致谢」)。
4
- 下面 `0.2.0` 一节是从代码基线 `dsh-superagent-local` 沿袭下来的、那一份代码相对上游
5
- `dsh-superagent-prototype` 的改动记录,保留它是为了说明基线里已经有什么;**它不是本项目的发布版本**。
3
+ 本仓库是 dsh-foreman(研究方向原型)的本地改造版。下面是相对上游 0.1.0 的改动。
6
4
 
7
- 本项目自己的版本从 `0.1.0` 开始。
8
-
9
- ## 0.1.1 — 2026-11-07
10
-
11
- 修的是 `0.1.0` 发布时才暴露的两个问题,都是真机验证(上海服务器 srv-02)发现的。
12
-
13
- ### 修复:运维脚本没有进包
14
-
15
- `0.1.0` 的 `files` 漏了 `scripts/`,于是装完 `dsh-foreman@0.1.0` 之后
16
- `node scripts/add-device.mjs` **不存在** —— 而那是加设备的唯一入口,
17
- 文档里却写着让人这么跑。现在随包发三个脚本:
18
-
19
- - `scripts/add-device.mjs`(加设备 / 换令牌 / 吊销)
20
- - `scripts/migrate.mjs`(迁机打包与自检)
21
- - `scripts/dsh-foreman`(服务脚本)
22
-
23
- `scripts/sync-wsl.sh`、`scripts/deploy-preview.sh` 是开发用的,**不发布**。
24
-
25
- ### 修复:一个不带正文的帧会崩掉握手
26
-
27
- `ws.ts` 把收到的任何消息一律 `.toString()`,于是 payload 缺失时会得到字符串 `"undefined"`,
28
- 再被当成帧去解析。现在只接受真的带文本的帧(字符串 / Buffer / Buffer[]),
29
- 其余直接忽略,并在握手阶段给出可读原因。
30
-
31
- ## 0.1.0 — 2026-11-07
32
-
33
- 首个公开发布。**一个常驻主 loop,把工单派给多台在线设备上各自完整运行的 DSH,账本只有一份。**
34
-
35
- ### 新增:设备与派单
36
-
37
- - **设备注册表**(`src/foreman/devices.ts`):同一份 SQLite 里的 `foreman-device:` 家族,
38
- 令牌只存 SHA-256。加设备、换令牌、吊销都不需要重启主 loop。
39
- CLI:`scripts/add-device.mjs add | list | rotate | revoke`。
40
- - **Selector**(`src/foreman/selector.ts`):纯函数。吊销的、离线的、没有对应项目 root 的设备先出局;
41
- 显式点名或标签要求再收窄;剩下的按在跑数量最少,平局取最近心跳。
42
- - **选择落在 accept 上**:`Ticket.target` 在 accept 时确定,`newAssignment` 复制到
43
- `Assignment.target`。选设备在 `prepareTicket` 缝上,**不在** Executor 里——Executor 没有写库通道。
44
- - **ForemanExecutor**(`src/foreman/executor.ts`):本机目标原样交给 `DshExecutor`;
45
- 远端目标变成一个 `assign` 帧。设备不可达时报 `blocked`,**不伪造 failed,也不悄悄改成本机执行**。
46
- - **Foreman Hub**(`src/foreman/hub.ts` + `ws.ts`):设备主动外连,`ws` 只出现在一个文件里,
47
- 只监听 `127.0.0.1`(对外由隧道终结 TLS)。握手校验令牌与协议版本;吊销在下一帧就生效。
48
- - **文本渲染**(`src/foreman/render.ts`):任务文本在主 loop 侧定稿,**不带任何主 loop 路径**;
49
- 附件走 base64 字节,按编码后大小卡 256 KiB 上限。
50
-
51
- ### 新增:断线与恢复
52
-
53
- - **断线绝不自动重派**。`assignmentInterrupted` 会走 `Tickets.restart` 立刻重开一张,
54
- 而设备可能还在跑——那是「同一件事执行两遍」。超时只转 `blocked`,交给人。
55
- - Hub 重启时,在飞的远端 assignment 一律先转 `blocked`,等设备 `hello` 对齐;
56
- `DshExecutor.resume()` 按 target 跳过远端项。
57
- - 设备重连后先 `hello` 报「我在跑哪些」,Hub 对齐而不是假设。
58
-
59
- ### 新增:设备侧插件(另一个包)
60
-
61
- - [`dsh-foreman-node`](https://www.npmjs.com/package/dsh-foreman-node):连出去、接单、回报三件事。
62
- job 会话是 `job-<assignmentId>`,权限是 **`workspace-write`**(不是主 loop 自己的 `danger-full-access`:
63
- 笔记本前没人,弹审批等于卡死)。
64
- - 设备上注册 `foreman_report` 工具(`progress` / `ready` / `blocked` / `failed`)。
65
- **`dropped` 不允许**:那是判断,设备没有判断权。
66
-
67
- ### 新增:面板
68
-
69
- - 「设置 › 设备」:在线点、在跑数、最近活动、项目名(**不显示设备自己的路径**)、吊销与换令牌。
70
-
71
- ### 新增:飞书群即项目
72
-
73
- - Policy 的 `routing.chats`:群 → 工作区,同时约束设备选择(内网群只选带 `intranet` 标签的设备)。
74
- 映射按 settings 修订号缓存,改了对新到的消息生效。
75
-
76
- ### 新增:迁移
77
-
78
- - `scripts/migrate.mjs`:`plan` / `checkpoint` / `pack` / `verify`。
79
- 必须先 checkpoint——库跑在 WAL 模式,只拷 `.sqlite` 会**静默丢掉**最近的工单。
80
- - [docs/migration.md](docs/migration.md) 五步 runbook。
81
-
82
- ### 依赖
83
-
84
- - 唯一新增的运行时依赖是 `ws@8`。
85
- - `@deepseek-ai/*` 全是 `peerDependencies`,由装了本插件的 DSH 实例提供。
86
-
87
- ---
88
-
89
- ## 0.2.0 — 2026-11-01(上游基线,非本项目发布)
90
-
91
- > 这一节记录的是代码基线 `dsh-superagent-local` 相对上游 `dsh-superagent-prototype` 的改动。
92
- > 本项目的 `0.1.0` 已包含其中全部内容。
5
+ ## 0.2.0 — 2026-11-01
93
6
 
94
7
  ### 新增:需求来自本机操作者
95
8
 
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
package/cordis.patch.yml CHANGED
@@ -6,11 +6,11 @@
6
6
  name: dsh-foreman
7
7
  - id: dsh-foreman-github
8
8
  name: dsh-foreman/github-watch
9
- # 椋炰功 IM 鏉ユ簮锛氱兢娑堟伅涓庣鑱婅繘宸ュ崟銆傛病鏈夐厤缃満鍣ㄤ汉 CLI 鏃跺畠鑷繁鎶ャ€屾湭璁剧疆鏈哄櫒浜?CLI銆嶏紝
10
- # 涓嶅奖鍝嶅埆鐨勬潵婧愶紝鎵€浠ラ粯璁ゅ紑鐫€锛岃浜鸿兘鐩存帴鍦ㄨ缃噷濉€?
9
+ # 飞书 IM 来源:群消息与私聊进工单。没有配置机器人 CLI 时它自己报「未设置机器人 CLI」,
10
+ # 不影响别的来源,所以默认开着,让人能直接在设置里填。
11
11
  - id: dsh-foreman-feishu
12
12
  name: dsh-foreman/feishu-watch
13
- # 鏈湴 Source锛氭妸鎿嶄綔鑰呭湪涓讳細璇濋噷璇寸殑璇濆綋浣滈渶姹傛潵婧愶紱涓€涓疄渚嬭鐩栨墍鏈夊伐浣滃尯銆?
13
+ # 本地 Source:把操作者在主会话里说的话当作需求来源;一个实例覆盖所有工作区。
14
14
  - id: dsh-foreman-dsh
15
15
  name: dsh-foreman/local-watch
16
16
  # The agent preset of every Executor Session (WORKER_PRESET in src/app/sessions.ts); the main Session has its own, below.
@@ -24,8 +24,8 @@
24
24
  name: '@deepseek-ai/dsh-agent-preset'
25
25
  config:
26
26
  id: dsh-foreman-worker
27
- name: dsh-foreman 鎵ц鍣?
28
- description: dsh-foreman 鐨勬墽琛屽櫒 Session 涓撶敤锛屼笉渚涙墜鍔ㄩ€夋嫨銆傚墠鍙板懡浠よ秴 5 绉掕浆鍏ュ悗鍙帮紝鍙惈鎵ц宸ヤ綔鎵€闇€鐨勫伐鍏枫€?
27
+ name: dsh-foreman 执行器
28
+ description: dsh-foreman 的执行器 Session 专用,不供手动选择。前台命令超 5 秒转入后台,只含执行工作所需的工具。
29
29
  order: 90
30
30
  plugins:
31
31
  - id: persona
@@ -129,8 +129,8 @@
129
129
  name: '@deepseek-ai/dsh-agent-preset'
130
130
  config:
131
131
  id: dsh-foreman-router
132
- name: dsh-foreman 涓?Session
133
- description: dsh-foreman 鐨勪富 Session 涓撶敤锛屼笉渚涙墜鍔ㄩ€夋嫨銆傚墠鍙板懡浠ゅ拰浠诲姟绛夊緟瓒?5 绉掕浆鍏ュ悗鍙帮紝鍙惈璺敱鎵€闇€鐨勫伐鍏枫€?
132
+ name: dsh-foreman 主 Session
133
+ description: dsh-foreman 的主 Session 专用,不供手动选择。前台命令和任务等待超 5 秒转入后台,只含路由所需的工具。
134
134
  order: 91
135
135
  plugins:
136
136
  - id: agent-instructions
package/icon.svg CHANGED
File without changes
package/lib/client.js CHANGED
@@ -4598,7 +4598,18 @@ window.__ModuleLoader__.load({
4598
4598
  devicesOnline: ["在线", "Online"],
4599
4599
  devicesRunning: ["在跑", "Running"],
4600
4600
  devicesEmpty: ["还没有登记任何设备", "No device is registered yet"],
4601
- devicesEmptyHint: ["在服务器上运行 dsh-foreman 的注册命令生成令牌,再把域名、设备 id 与令牌填进设备自己的配置。", "Mint a token with the hub's registration command, then put the address, device id and token into that device's own configuration."],
4601
+ devicesEmptyHint: ["在下面登记一台设备,它会给出三样东西填进那台设备的配置页。", "Register a machine below; it gives you the three values to put on that machine's own configuration page."],
4602
+ devicesAdd: ["登记一台设备", "Register a machine"],
4603
+ devicesAddHint: ["登记后这台设备才能接单。登记不会连上它——连接由那台设备自己发起。", "A machine takes work only after it is registered here. Registering does not connect it: the machine dials out itself."],
4604
+ devicesAddId: ["设备 id", "Device id"],
4605
+ devicesAddName: ["显示名", "Display name"],
4606
+ devicesAddRoots: ["它能接的项目", "Projects it can take"],
4607
+ devicesAddRoot: ["加一个项目", "Add a project"],
4608
+ devicesAddProject: ["项目名", "Project"],
4609
+ devicesAddPath: ["那台机器上的路径", "Path on that machine"],
4610
+ devicesAddSubmit: ["登记", "Register"],
4611
+ devicesAddMinted: ["把这四样填进那台设备的配置页", "Put these values on that machine's configuration page"],
4612
+ devicesAddMintedHint: ["令牌只显示这一次,库里只存哈希。关掉或刷新这一页就再也拿不回来了;丢了只能换一个。", "The token is shown once; only its hash is stored. Closing or refreshing this page loses it for good, and the only way back is a new one."],
4602
4613
  deviceOnline: ["在线", "Online"],
4603
4614
  deviceRevoked: ["已吊销", "Revoked"],
4604
4615
  deviceRunning: ["在跑 {count} 件", "{count} running"],
@@ -5545,7 +5556,7 @@ window.__ModuleLoader__.load({
5545
5556
  //#endregion
5546
5557
  //#region src/client/devices.tsx
5547
5558
  /** Devices settings group: which machines may take work, whether each is online, and what it is running. */
5548
- /** The device list, read-only apart from revoking and rotating a token. */
5559
+ /** The device list, the registration form, and the two token actions. */
5549
5560
  function DevicesSettings() {
5550
5561
  const { t, call } = usePanel();
5551
5562
  const { data, error, reload } = usePoll(() => call("GET devices"), []);
@@ -5585,6 +5596,9 @@ window.__ModuleLoader__.load({
5585
5596
  })
5586
5597
  ]
5587
5598
  }),
5599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddDevice, { onAdded: () => {
5600
+ reload();
5601
+ } }),
5588
5602
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
5589
5603
  title: t("devicesList"),
5590
5604
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListSection, {
@@ -5697,6 +5711,143 @@ window.__ModuleLoader__.load({
5697
5711
  })]
5698
5712
  });
5699
5713
  }
5714
+ /**
5715
+ * Registering a machine, in place.
5716
+ *
5717
+ * The three values the machine needs are shown together and once: the hub keeps only a token hash, so a token that
5718
+ * leaves this card is gone. That is why the card is not dismissible: closing it is losing the token, and the page
5719
+ * says so instead of letting someone find out at the device.
5720
+ */
5721
+ function AddDevice({ onAdded }) {
5722
+ const { t, call } = usePanel();
5723
+ const { data: state } = usePoll(() => call("GET state"), []);
5724
+ const [id, setId] = (0, react.useState)("");
5725
+ const [name, setName] = (0, react.useState)("");
5726
+ const [roots, setRoots] = (0, react.useState)([]);
5727
+ const [minted, setMinted] = (0, react.useState)();
5728
+ const [refusal, setRefusal] = (0, react.useState)();
5729
+ const [busy, setBusy] = (0, react.useState)(false);
5730
+ const register = async () => {
5731
+ setBusy(true);
5732
+ setRefusal(void 0);
5733
+ try {
5734
+ const result = await call("POST devices", { body: {
5735
+ id: id.trim(),
5736
+ name: name.trim(),
5737
+ labels: [],
5738
+ roots: roots.map((root) => ({
5739
+ project: root.project.trim(),
5740
+ path: root.path.trim()
5741
+ }))
5742
+ } });
5743
+ setMinted({
5744
+ id: result.id,
5745
+ token: result.token
5746
+ });
5747
+ setId("");
5748
+ setName("");
5749
+ setRoots([]);
5750
+ onAdded();
5751
+ } catch (error) {
5752
+ setRefusal(errorText(error));
5753
+ } finally {
5754
+ setBusy(false);
5755
+ }
5756
+ };
5757
+ const edit = (index, patch) => {
5758
+ setRoots((current) => current.map((root, at) => at === index ? {
5759
+ ...root,
5760
+ ...patch
5761
+ } : root));
5762
+ };
5763
+ const hubPort = state?.setup.machine.hubPort ?? 0;
5764
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
5765
+ title: t("devicesAdd"),
5766
+ hint: t("devicesAddHint"),
5767
+ children: [
5768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(FieldGrid, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
5769
+ label: t("devicesAddId"),
5770
+ required: true,
5771
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
5772
+ value: id,
5773
+ onChange: (event) => setId(event.target.value),
5774
+ placeholder: "laptop"
5775
+ })
5776
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
5777
+ label: t("devicesAddName"),
5778
+ required: true,
5779
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
5780
+ value: name,
5781
+ onChange: (event) => setName(event.target.value)
5782
+ })
5783
+ })] }),
5784
+ roots.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldGrid, { children: roots.map((root, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
5785
+ label: index === 0 ? t("devicesAddProject") : `${t("devicesAddProject")} ${index + 1}`,
5786
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
5787
+ value: root.project,
5788
+ onChange: (event) => edit(index, { project: event.target.value }),
5789
+ placeholder: "shop"
5790
+ })
5791
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
5792
+ label: index === 0 ? t("devicesAddPath") : `${t("devicesAddPath")} ${index + 1}`,
5793
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
5794
+ value: root.path,
5795
+ onChange: (event) => edit(index, { path: event.target.value }),
5796
+ placeholder: "F:\\\\dsh\\\\shop"
5797
+ })
5798
+ })] }, index)) }),
5799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5800
+ className: "dt-form-actions",
5801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5802
+ size: "sm",
5803
+ onClick: () => setRoots((current) => [...current, {
5804
+ project: "",
5805
+ path: ""
5806
+ }]),
5807
+ children: t("devicesAddRoot")
5808
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5809
+ size: "sm",
5810
+ variant: "primary",
5811
+ busy,
5812
+ disabled: id.trim() === "" || name.trim() === "",
5813
+ onClick: () => {
5814
+ register();
5815
+ },
5816
+ children: t("devicesAddSubmit")
5817
+ })]
5818
+ }),
5819
+ refusal !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorLine, {
5820
+ error: refusal,
5821
+ title: t("loadFailed")
5822
+ })
5823
+ ]
5824
+ }), minted && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
5825
+ title: t("devicesAddMinted"),
5826
+ tone: "warn",
5827
+ children: [
5828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, { children: t("devicesAddMintedHint") }),
5829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValues, { rows: mintedRows(minted, hubPort) }),
5830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
5831
+ className: "dt-mono",
5832
+ "data-device-token": true,
5833
+ children: minted.token
5834
+ })
5835
+ ]
5836
+ })] });
5837
+ }
5838
+ /**
5839
+ * The rows a device needs, with the hub's real port.
5840
+ *
5841
+ * A hub with `hubPort: 0` has its listener switched off, so no address a card could print would work; saying so is
5842
+ * more use than a plausible-looking port that nothing listens on.
5843
+ */
5844
+ function mintedRows(minted, hubPort) {
5845
+ return [
5846
+ ["url", hubPort > 0 ? `ws://127.0.0.1:${hubPort}` : "the hub is off: set hubPort in the plugin configuration"],
5847
+ ["deviceId", minted.id],
5848
+ ["token", minted.token]
5849
+ ];
5850
+ }
5700
5851
  //#endregion
5701
5852
  //#region src/client/logs.tsx
5702
5853
  /**
package/lib/index.js CHANGED
@@ -8802,6 +8802,10 @@ var RouteError = class extends Error {
8802
8802
  this.status = status;
8803
8803
  }
8804
8804
  };
8805
+ /** Whether a registration failed because the id is taken, the only failure that is the caller's to fix. */
8806
+ function isDuplicateDevice(error) {
8807
+ return error instanceof Error && /is already registered/.test(error.message);
8808
+ }
8805
8809
  async function readBody(req, maxBytes = 131072) {
8806
8810
  let size = 0;
8807
8811
  const chunks = [];
@@ -8924,12 +8928,13 @@ function failure(error, internal) {
8924
8928
  id
8925
8929
  }];
8926
8930
  }
8927
- function machineView({ stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs }) {
8931
+ function machineView({ stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs, hubPort }) {
8928
8932
  return {
8929
8933
  stateDir,
8930
8934
  promptMaxChars: promptMaxChars.get(),
8931
8935
  admissionContextMaxPages: admissionContextMaxPages.get(),
8932
- admissionContextTimeoutMs: admissionContextTimeoutMs.get()
8936
+ admissionContextTimeoutMs: admissionContextTimeoutMs.get(),
8937
+ hubPort
8933
8938
  };
8934
8939
  }
8935
8940
  /** Every setup problem the panel shows: global first, then the ones that pause a single Source, each with that Source's title. */
@@ -9169,6 +9174,24 @@ function routes(runtime) {
9169
9174
  devices: runtime.devices(),
9170
9175
  online: runtime.onlineDevices()
9171
9176
  }),
9177
+ /** 登记一台新设备。令牌只在这一次响应里出现,刷新就没了。 */
9178
+ "POST devices": async (input) => {
9179
+ const body = z.object({
9180
+ id: z.string().trim().min(1),
9181
+ name: z.string().trim().min(1),
9182
+ labels: z.array(z.string().trim().min(1)).optional(),
9183
+ roots: z.array(z.object({
9184
+ project: z.string().trim().min(1),
9185
+ path: z.string().trim().min(1)
9186
+ }))
9187
+ }).parse(await input.body());
9188
+ try {
9189
+ return runtime.addDevice(body);
9190
+ } catch (error) {
9191
+ if (isDuplicateDevice(error)) throw new RouteError(400, error.message);
9192
+ throw error;
9193
+ }
9194
+ },
9172
9195
  /** 吊销一台设备:登记保留(审计要看),下一次心跳就断开。 */
9173
9196
  "POST devices/:id/revoke": ({ params }) => {
9174
9197
  runtime.revokeDevice(decodeURIComponent(params.id));
@@ -12376,6 +12399,14 @@ var Runtime = class extends Service {
12376
12399
  models: () => this.ctx.sessionController.modelCatalog(),
12377
12400
  devices: () => runtime.devices.list(),
12378
12401
  onlineDevices: () => runtime.hub?.snapshot() ?? [],
12402
+ /** 登记一台设备并铸一枚令牌;和 `scripts/add-device.mjs` 走同一个 `Devices.add`。 */
12403
+ addDevice: (input) => {
12404
+ const { device, token } = runtime.devices.add(input);
12405
+ return {
12406
+ id: device.id,
12407
+ token
12408
+ };
12409
+ },
12379
12410
  revokeDevice: (id) => {
12380
12411
  runtime.devices.revoke(id);
12381
12412
  runtime.hub?.detach(id);
package/locale/en.json CHANGED
File without changes
package/locale/zh.json CHANGED
File without changes
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-foreman",
3
3
  "author": "niechao136",
4
4
  "license": "MIT",
5
- "version": "0.1.1",
5
+ "version": "0.3.0",
6
6
  "description": "dsh-foreman:主 loop 在多设备上派单的 DSH bundle:需求来自本机操作者的主会话而不是 GitHub/飞书,每个工作区各有一条主会话,主会话只负责路由,真正的活儿派给 Worker 在该工作区目录里执行。",
7
7
  "type": "module",
8
8
  "main": "lib/index.js",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "packageManager": "pnpm@11.7.0",
64
64
  "dependencies": {
65
- "dsh-foreman-node": "0.1.0",
65
+ "dsh-foreman-node": "0.2.0",
66
66
  "proper-lockfile": "4.1.2",
67
67
  "ws": "8.22.0",
68
68
  "zod": "4.4.3"
@@ -28,7 +28,6 @@ The host posts it as a reply to the triggering message; do not quote that messag
28
28
  Return rule_assessments with exactly one entry for every enabled rule: rule_id, matched and a factual reason.
29
29
  Include negative judgments as matched=false; a reason saying that a rule does not apply cannot be a positive match.
30
30
  Never include disabled, unknown or repeated rule IDs.
31
- Fill device only when the request itself says where the work has to happen — a named machine, an office-only system, a machine the person says they are at. Otherwise leave it out: the host picks the least busy online machine that can reach the project, and a guess here can send work to a machine that cannot do it.
32
31
  The host derives positive-only matches from those booleans.
33
32
  One source message produces at most one ticket.
34
33
  If matching rules conflict or the scope is ambiguous, ignore quietly.
package/prompts/cli.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/prompts/shared.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/prompts/work.md CHANGED
File without changes
package/prompts/worker.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes