dsh-global-todo 0.3.2 → 0.4.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
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] — 2026-10-19
11
+
12
+ The first release after the plugin was listed in a community catalog. It is a reliability
13
+ release: the features that already existed now tell the truth — a sort that honours every level,
14
+ a page that says it was cut, a language setting that applies, and an automatic collector that can
15
+ explain itself and survives a failed question.
16
+
17
+ - **An unknown working directory is a reason not to ask.** The exemption is keyed on where a
18
+ conversation works; a place nobody can name cannot be checked against it, and asking there would
19
+ volunteer a question in a workspace somebody may well have excluded. The decision now reports
20
+ `unknown-dir` and stays quiet, and the panel's switch tooltip says so. The cost is a conversation
21
+ whose cwd the Host never reported, which the event tracker normally supplies on its first message.
22
+
23
+ - **The automatic collect switch shows the last decision.** `/health` now reports the full decision
24
+ list, so the switch's tooltip can say *why* nothing has happened — not quiet long enough, waiting
25
+ for you, cooling down, retrying, or an exempt workspace — together with how many candidates are
26
+ waiting and how many are due.
27
+
28
+ - **The automatic collect switch can explain itself.** The decision layer knew why a conversation
29
+ was skipped — switched off, exempt, not quiet yet, waiting for a person, cooling down — and the
30
+ timer threw that reason away, so `/health` could report `enabled: true` while nothing ever
31
+ happened and give no way to tell why. The whole decision list is reported now, with the last one,
32
+ the number of waiting candidates, and in-memory counters for asked / succeeded / failed / dropped.
33
+ The settings file also reports how it loaded (`file` / `missing` / `corrupt`): "off because
34
+ somebody turned it off" and "off because the file was unreadable" used to look identical.
35
+
36
+ - **A failed question no longer silences a conversation.** The cooldown was booked before the ask
37
+ and survived a failed request, so `awaiting-human` kept that conversation quiet until a person
38
+ happened to speak again — one transient resume failure turned the feature off for that
39
+ conversation, permanently and invisibly. A failure now releases the booking and schedules a
40
+ bounded retry (1 / 5 / 25 minutes, three attempts) before dropping the candidate with the reason
41
+ recorded. The ask is bounded too: an `agents.resume` that never settles now times out after ten
42
+ seconds instead of leaving the global `asking` flag set and silencing every conversation.
43
+
44
+ - **A priority sort that put `urgent` last.** The rank table was written when there were three
45
+ levels, and adding a fourth did not touch it: the comparator read `undefined` for `urgent` and
46
+ produced `NaN`, so `sort=priority` (and the panel's level grouping) moved P0 to the bottom.
47
+ Every level now has a rank, and the test names all four in order.
48
+
49
+ - **The list stopped at 200 without saying so.** `/list` cut the page at a fixed limit while
50
+ `counts` was computed over the whole file, so a list of 350 showed "350 open" above 200 rows.
51
+ The reply now carries `matched` (before the cut) and `truncated`, the panel prints "showing the
52
+ first N of M", and `global_todo_list` tells the model the same thing instead of letting it
53
+ reason about a page as if it were the whole set.
54
+
55
+ - **The panel described the quiet window with the cooldown number.** The hint said "quiet for
56
+ {n} minutes" while `{n}` was `cooldownMinutes`; the gate it describes is `idleMinutes`, and the
57
+ two only looked identical because both default to 30. A hint that lies once the value is
58
+ changed is worse than no hint.
59
+
60
+ - **Switching the interface language did nothing.** `resolveLang` read `locale` / `id` / `language`
61
+ / `current` from the locale snapshot, but the snapshot carries `active`; the value was never
62
+ found and the panel always fell back to `navigator.language`. It reads `active` first now, and
63
+ the unused `workspaces` client dependency is gone.
64
+
65
+ - **The English panel was partly Chinese.** Seventeen keys — the whole `note.*` and `completion.*`
66
+ groups, plus `link.missing`, `search.hint` and `detail.notes` — existed only in the Chinese
67
+ dictionary and silently fell back; `meta.completed` was Chinese in the English table as well.
68
+ Both tables now carry the same 186 keys, guarded by a test that also refuses a Chinese value in
69
+ the English one.
70
+
71
+ - **Two empty states, one wrong.** The panel decided between "no todos yet" and "nothing in this
72
+ filter" by looking at the current filter, so "all" with a search term and an empty list showed
73
+ the wrong sentence. The decision now uses the list's real size and whether a search is active.
74
+
75
+ - **Opening a row near the bottom did nothing visible.** The detail expanded below the viewport;
76
+ the row now scrolls into view when it opens.
77
+
78
+ - **The no-literal-colours test was checking fourteen characters.** It sliced from `const CSS =`
79
+ to the first `.gt-root` inside the stylesheet, so it never inspected a colour. The slice now
80
+ runs to the end of the template, which makes the guard real — and the theme-token rule is the
81
+ one thing that keeps dark mode working.
82
+
83
+ - **Documentation that had drifted from the tree.** Both READMEs gained the five routes they were
84
+ missing (`note`, `dirs`, `settings`, `assign`, `diagnostics`); `docs/DESIGN.md` records the four
85
+ fields and two activity kinds added since the table was written; `cordis.patch.yml` names
86
+ `guidance` and `browseRoot`, which a wholesale `config` override would otherwise drop;
87
+ `docs/ROADMAP.md` cites files and functions instead of line numbers that had moved by dozens of
88
+ lines.
89
+
90
+ - **The contribution terms are written down.** MIT in, MIT out — no CLA, no copyright assignment —
91
+ so a future patch cannot quietly stay its author's property and block a license change years
92
+ later. The READMEs also say plainly that this is a community plugin, not a DeepSeek project.
93
+
10
94
  ## [0.3.2] — 2026-10-19
11
95
 
12
96
  No code change. `0.3.1` was published to npm by hand, so it carries no provenance attestation;
package/CONTRIBUTING.md CHANGED
@@ -56,6 +56,20 @@ Node 22.19 or newer is all you need.
56
56
  - **No secrets, ever.** The store is local and the plugin stores no credentials. The URL
57
57
  rule that refuses `user:password@` exists for this reason — do not relax it.
58
58
 
59
+ ## Licensing of contributions
60
+
61
+ The plugin is [MIT](LICENSE)-licensed, and contributions arrive under the same terms — no CLA, no
62
+ copyright assignment. Opening a pull request is the agreement. If that is not something you can do
63
+ (an employer owns your work, say), say so in the PR before writing much code and we will work out
64
+ the shape of the contribution first.
65
+
66
+ Why this is written down rather than assumed: without a statement, a patch stays the property of
67
+ whoever wrote it, and one unrelated contribution would be enough to block the project from ever
68
+ changing its license without tracking that person down. Inbound = outbound, said once, keeps that
69
+ door closed.
70
+
71
+ `Signed-off-by` is welcome but not required.
72
+
59
73
  ## Releasing
60
74
 
61
75
  Facts checked on 2026-10-14, so nobody has to re-derive them:
package/README.md CHANGED
@@ -40,8 +40,14 @@ profile already installed, or whatever `DSH_PLAYWRIGHT` points at.
40
40
 
41
41
  ## Requirements
42
42
 
43
- - DeepSeek Harness `0.2.x` (developed and verified against `0.2.1-rc.2`)
43
+ - DeepSeek Harness `0.2.x` (developed and verified against `0.2.1-rc.2`; `engines.dsh` is
44
+ `>=0.2.0-rc.1 <0.3.0`, and a Host outside it skips the bundle instead of warning)
44
45
  - Node.js `>= 22.19.0` (the Web profile's own requirement)
46
+ - A **Web** client: the panel is served by the Host's web server. Headless / ACP / SDK keep the
47
+ agent tools but have no panel; an Electron-style desktop client (file:// + IPC) is not
48
+ supported yet
49
+ - After a Host upgrade, refresh the page — the client module graph belongs to the release the
50
+ page booted with
45
51
  - No runtime dependencies. Nothing is imported from `@deepseek-ai/*`.
46
52
 
47
53
  ## Install
@@ -57,6 +63,10 @@ dsh plugin --profile web add /absolute/path/to/dsh-global-todo
57
63
  The plugin is a Loader row, not a hot-loaded script: **restart the host** (`dsh web`)
58
64
  after installing, then refresh the page. A "待办 / Todos" entry appears in the sidebar.
59
65
 
66
+ The npm package is the source of truth. A community catalog can lag behind it or point at an
67
+ older release; pin the version you want (`dsh plugin --profile web add dsh-global-todo@0.4.0`)
68
+ when it matters.
69
+
60
70
  To roll back:
61
71
 
62
72
  ```sh
@@ -224,12 +234,13 @@ fence when that service is present, and by a strict loopback check otherwise.
224
234
 
225
235
  | Method | Path | Body / query |
226
236
  |---|---|---|
227
- | GET | `/api/global-todo/list` | `status`, `query`, `tag`, `sort`, `limit` |
237
+ | GET | `/api/global-todo/list` | `status`, `query`, `tag`, `sort`, `limit` — the reply carries `matched` and `truncated` when the page was cut |
238
+ | GET | `/api/global-todo/dirs` | `path?` — list subdirectories for the panel's built-in folder picker (the shell's own picker is preferred when mounted) |
228
239
  | GET | `/api/global-todo/get` | `id` |
229
240
  | GET | `/api/global-todo/export` | — (the store's own JSON document, for backup or migration) |
230
241
  | POST | `/api/global-todo/import` | `{ doc, mode?: replace\|merge }` — `replace` is an exact round trip, `merge` updates by id and appends (what the panel sends) |
231
242
  | POST | `/api/global-todo/capture` | `{ sessionId? }` — ask the current conversation to collect its follow-ups |
232
- | GET | `/api/global-todo/health` | — (store location, counts, dispatch capabilities, plus **tool registration** and **whether the completion watcher attached** — both fail quietly, so both are reported — and `metrics`: dispatch success rate, agent tool calls, list growth) |
243
+ | GET | `/api/global-todo/health` | — (store location, counts, dispatch capabilities, plus **tool registration** and **whether the completion watcher attached** — both fail quietly, so both are reported — and `metrics`: dispatch success rate, agent tool calls, list growth; `autoCollect`: why each waiting conversation was or was not asked, counters, and how the settings file loaded) |
233
244
  | POST | `/api/global-todo/create` | `{ title, description?, links?, tags?, priority?, dueAt? }` |
234
245
  | POST | `/api/global-todo/update` | `{ id, ...fields, by?: human\|agent }` |
235
246
  | POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
@@ -237,6 +248,9 @@ fence when that service is present, and by a strict loopback check otherwise.
237
248
  | POST | `/api/global-todo/restore` | `{ todo }` — put a removed record back verbatim (undo) |
238
249
  | POST | `/api/global-todo/link` | `{ id, add?: [...], remove?: [value \| index] }` |
239
250
  | POST | `/api/global-todo/dispatch` | `{ id, mode?: auto\|bound\|new\|current, sessionId?, dir? }` |
251
+ | POST | `/api/global-todo/assign` | `{ ids, items?, target?, dir? }` — hand several todos over in one call; an id that matches nothing comes back instead of being sent elsewhere |
252
+ | POST | `/api/global-todo/settings` | `{ autoCollect?, idleMinutes?, cooldownMinutes?, exemptWorkspaces? }` — the Host-side switches behind automatic collect |
253
+ | POST | `/api/global-todo/diagnostics` | `{ scope, status?, reason?, message?, url? }` — the panel's own error report |
240
254
 
241
255
  `by` on `/update` and `/status` says whose write this is: omitted (what the panel sends) it is
242
256
  the person at the keyboard, and a script acting for the agent says `by: 'agent'` rather than
@@ -299,8 +313,13 @@ npm run check # parse every file, assert the shipping shape, and — when a
299
313
  # Host's own validator
300
314
  npm test # 100+ unit, contract and bundle tests, zero dependencies
301
315
  npm run verify # both
316
+ npm run check:matrix # the running dsh against engines.dsh, then the manifest,
317
+ # bundle and tool-schema contracts (no preview writes)
302
318
  ```
303
319
 
320
+ `engines.dsh` is the one version fact that matters: a Host outside it skips the whole bundle
321
+ instead of warning, so `check:matrix` is the smoke to run before widening the range.
322
+
304
323
  There is **no build step**. The host half is plain ESM under `lib/`; the client half is a
305
324
  classic script that registers itself with `window.__ModuleLoader__`, so no bundler,
306
325
  TypeScript toolchain or CI artifact is involved. `lib/client.js` edits take effect on a
@@ -342,3 +361,6 @@ Please report vulnerabilities as described in [SECURITY.md](SECURITY.md).
342
361
  ## License
343
362
 
344
363
  [MIT](LICENSE) © 2026 LUgABeEEE
364
+
365
+ A community plugin, not a DeepSeek project: the `dsh` prefix marks the ecosystem it plugs into, not
366
+ an endorsement. "DeepSeek" and "DeepSeek Harness" remain their owners' marks.
package/README.zh.md CHANGED
@@ -36,8 +36,12 @@ HTML(不依赖宿主、零依赖),再以 1280×720 拍进 `docs/screenshot
36
36
 
37
37
  ## 环境要求
38
38
 
39
- - DeepSeek Harness `0.2.x`(按 `0.2.1-rc.2` 开发与验证)
39
+ - DeepSeek Harness `0.2.x`(按 `0.2.1-rc.2` 开发与验证;`engines.dsh` 是 `>=0.2.0-rc.1 <0.3.0`,
40
+ 区间外的宿主会**整层跳过**这个 bundle,不是警告)
40
41
  - Node.js `>= 22.19.0`(Web profile 本身的要求)
42
+ - **Web 客户端**:面板由宿主的 web server 提供。headless / ACP / SDK 只有 Agent 工具、没有面板;
43
+ Electron 式桌面端(file:// + IPC)暂不支持
44
+ - 宿主升级后要**刷新页面**——客户端模块图属于页面启动时那个 release
41
45
  - **零运行期依赖**,也不 import 任何 `@deepseek-ai/*` 包
42
46
 
43
47
  ## 安装
@@ -53,6 +57,9 @@ dsh plugin --profile web add /绝对路径/dsh-global-todo
53
57
  插件是宿主组合里的**一行 Loader**,不是热加载脚本:**装完要重启宿主**(`dsh web`),
54
58
  再刷新页面。侧栏会出现「待办」入口。
55
59
 
60
+ npm 上的包才是事实来源。社区目录可能落后于 npm、或指向某个旧 release;需要确定版本时
61
+ 直接钉版本(`dsh plugin --profile web add dsh-global-todo@0.4.0`)。
62
+
56
63
  回滚:
57
64
 
58
65
  ```sh
@@ -129,13 +136,14 @@ dsh plugin --profile web remove dsh-global-todo
129
136
 
130
137
  | 方法 | 路径 | 参数 |
131
138
  |---|---|---|
132
- | GET | `/api/global-todo/list` | `status`、`query`、`tag`、`sort`、`limit` |
139
+ | GET | `/api/global-todo/list` | `status`、`query`、`tag`、`sort`、`limit`——被截断时响应带 `matched` 与 `truncated` |
140
+ | GET | `/api/global-todo/dirs` | `path?`——列子目录,供面板内置的目录浏览器使用(宿主自己的选择器在时会优先) |
133
141
  | GET | `/api/global-todo/get` | `id` |
134
142
  | GET | `/api/global-todo/export` | ——(存储用的那份 JSON 文档,用于备份或迁移) |
135
143
  | POST | `/api/global-todo/import` | `{ doc, mode?: replace\|merge }`——`replace` 是无损往返,`merge` 同 id 覆盖、新 id 追加(面板用的是这个) |
136
144
  | POST | `/api/global-todo/note` | `{ id, text }`(追加进展记录) |
137
145
  | POST | `/api/global-todo/capture` | `{ sessionId? }`(请当前对话把要跟进的事抓进清单) |
138
- | GET | `/api/global-todo/health` | —(数据位置、统计、派发能力,以及**工具注册状态**与**自动收尾观察者是否挂上**——这两种失败是静默的,所以必须能查;还有 `metrics`:派发成功率、工具调用次数、清单增长) |
146
+ | GET | `/api/global-todo/health` | —(数据位置、统计、派发能力,以及**工具注册状态**与**自动收尾观察者是否挂上**——这两种失败是静默的,所以必须能查;还有 `metrics`:派发成功率、工具调用次数、清单增长;`autoCollect`:每个候选对话**为什么被问/没被问**、计次,以及设置文件是怎么加载进来的) |
139
147
  | POST | `/api/global-todo/create` | `{ title, description?, links?, tags?, priority?, dueAt? }` |
140
148
  | POST | `/api/global-todo/update` | `{ id, ...字段, by?: human\|agent }` |
141
149
  | POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
@@ -143,6 +151,9 @@ dsh plugin --profile web remove dsh-global-todo
143
151
  | POST | `/api/global-todo/restore` | `{ todo }` —— 原样还原被删除的记录(撤销) |
144
152
  | POST | `/api/global-todo/link` | `{ id, add?: [...], remove?: [值 或 下标] }` |
145
153
  | POST | `/api/global-todo/dispatch` | `{ id, mode?: auto\|bound\|new\|current, sessionId?, dir? }` |
154
+ | POST | `/api/global-todo/assign` | `{ ids, items?, target?, dir? }`——一次把多条交给 Agent;配不上的 id 原样报回,不擅自送到别处 |
155
+ | POST | `/api/global-todo/settings` | `{ autoCollect?, idleMinutes?, cooldownMinutes?, exemptWorkspaces? }`——自动获取的开关与阈值 |
156
+ | POST | `/api/global-todo/diagnostics` | `{ scope, status?, reason?, message?, url? }`——面板自己上报的错误 |
146
157
 
147
158
  `/update` 与 `/status` 的 `by` 说明这次写是谁做的:不传(面板就是这样)就是人在键盘前,
148
159
  脚本替 agent 干活时写 `by: 'agent'`,而不是被记成人的操作。传别的值是 `400`——
@@ -200,8 +211,13 @@ npm run check # 解析全部文件、校验发布形态;本机有 DSH prof
200
211
  # 自己的校验器逐条验证工具 schema
201
212
  npm test # 100+ 单元/契约/bundle 测试,零依赖
202
213
  npm run verify # 两者都跑
214
+ npm run check:matrix # 当前 dsh 与 engines.dsh 的实际匹配,再跑 manifest/bundle/
215
+ # 工具 schema 三类契约(不写 preview)
203
216
  ```
204
217
 
218
+ `engines.dsh` 是唯一真正决定“装不装得上”的版本事实:区间外的宿主会**整层跳过**这个
219
+ bundle,而不是警告;所以放宽区间之前先跑 `check:matrix`。
220
+
205
221
  **没有构建步骤。** 宿主半区是 `lib/` 下的纯 ESM;客户端半区是一个自注册到
206
222
  `window.__ModuleLoader__` 的普通脚本——不需要打包器、不需要 TypeScript 工具链、
207
223
  CI 里也没有构建产物。改 `lib/client.js` 刷新页面即生效;改宿主半区要重启宿主。
@@ -242,3 +258,5 @@ HTTP 接口只服务本机,清单是本地文件,任何凭据都不会被存
242
258
  ## 许可
243
259
 
244
260
  [MIT](LICENSE) © 2026 LUgABeEEE
261
+
262
+ 社区插件,不是 DeepSeek 官方项目:包名里的 `dsh` 只表示它接入的生态,不代表任何背书;「DeepSeek」与「DeepSeek Harness」的相关权利归其所有者。
package/cordis.patch.yml CHANGED
@@ -21,3 +21,9 @@
21
21
  # Reject links whose file path does not exist at write time. `false` lets
22
22
  # you attach a document before creating it.
23
23
  requireExistingFiles: true
24
+ # Inject the short usage guidance into every turn. `false` leaves the tools
25
+ # available but stops asking the model to record things on its own.
26
+ guidance: true
27
+ # Starting point for the panel's built-in folder browser when the shell has
28
+ # no native picker. Empty = home directory, then the Host's cwd.
29
+ browseRoot: ''
package/docs/DESIGN.md CHANGED
@@ -15,7 +15,7 @@ so the file stays readable, hand-editable and diffable.
15
15
  | `title` | string | 1–200 characters, trimmed |
16
16
  | `description` | string | up to 10 000 characters, Markdown-ish plain text |
17
17
  | `status` | `open` \| `doing` \| `done` \| `cancelled` | `doing` means someone is on it |
18
- | `priority` | `low` \| `normal` \| `high` | three levels people actually use |
18
+ | `priority` | `low` \| `normal` \| `high` \| `urgent` | `urgent` was added in front of the first three; `normal` stays the default |
19
19
  | `dueAt` | ISO-8601 string \| `null` | canonicalised to UTC on write |
20
20
  | `tags` | string[] | ≤ 20, each ≤ 32, de-duplicated case-insensitively |
21
21
  | `links` | `{ kind: 'url' \| 'file', value, title }[]` | documents and addresses |
@@ -25,11 +25,15 @@ so the file stays readable, hand-editable and diffable.
25
25
  | `updatedBy` | `human` \| `agent` | which side changed it last |
26
26
  | `doneAt` | epoch ms \| `null` | set on `done`, cleared on leaving `done` |
27
27
  | `sessionId` | string | the conversation this todo owns; `''` until handed off |
28
+ | `originDir` | string | the working directory it was written from; resolves a hand-off's cwd |
29
+ | `originSessionId` | string | the conversation that wrote it; the panel groups by this |
28
30
  | `lastDispatch` | `{ at, mode, ok, sessionId?, error? }` \| `null` | last hand-off, including its failure reason |
31
+ | `completion` | `{ at, by, summary, sessionId, artifacts }` \| `null` | what the finishing turn said and produced; cleared on reopen |
32
+ | `notes` | `{ at, by, text }[]` | append-only progress notes, capped at `LIMITS.notes` |
29
33
  | `activity` | `{ at, by, kind, summary? }[]` | trimmed to `activityLimit` (default 50) |
30
34
 
31
35
  The activity kinds are `created`, `updated`, `status`, `done`, `reopened`, `linked`,
32
- `unlinked`, `dispatched`, `dispatch-failed`.
36
+ `unlinked`, `dispatched`, `dispatch-failed`, `noted`, `completed`.
33
37
 
34
38
  ## Storage
35
39
 
@@ -192,7 +196,9 @@ that fills itself with noise gets ignored, and then the real items get ignored t
192
196
  remembered from the conversation's own events rather than looked up when the timer runs: these
193
197
  conversations are idle most of the time, and an idle one is exactly the one the Host may have
194
198
  unloaded, where a live lookup answers `''` and the exemption would stop applying to the only
195
- conversations it exists for;
199
+ conversations it exists for. A directory nobody can name is therefore treated as **un-askable**
200
+ rather than as exempt-or-allowed: the exemption cannot be checked against a place that cannot be
201
+ named, and the safe direction is silence (`unknown-dir`);
196
202
  - **only clean turns**: an aborted, failed or truncated turn is not a moment to ask;
197
203
  - **one question per message the person wrote**. The question is itself a message in the
198
204
  conversation, so it restarted the quiet clock, and the answer to it ended a clean turn — which
@@ -208,6 +214,27 @@ The switch lives on the Host rather than in the panel because the Host is what a
208
214
  panel may be closed when the question is due, and `/health` reports the state so the mirror can
209
215
  never drift from the thing it mirrors.
210
216
 
217
+ ### What it did is part of the feature
218
+
219
+ A gate whose reason is discarded is a gate nobody can debug. The decision layer always knew why a
220
+ conversation was skipped — off, exempt, not idle yet, awaiting a person, cooling down — but the
221
+ timer kept only the verdict, so `/health` could say `enabled: true` while nothing ever happened and
222
+ offer no way to tell why. The whole decision list is reported now (`autoCollect.decisions`, capped
223
+ at twenty), along with the last decision, how many candidates are waiting, and in-memory counters
224
+ for asked / succeeded / failed / dropped.
225
+
226
+ The settings file reports how it loaded (`file` / `missing` / `corrupt`) and when. "Off" because a
227
+ person turned it off and "off" because the file could not be read looked identical from the panel,
228
+ and the second one is a silent failure rather than a decision.
229
+
230
+ A failed question is not an answer. The booking used to survive a failed request, and
231
+ `awaiting-human` then kept that conversation quiet until somebody happened to speak again: one
232
+ transient resume failure switched the feature off for that conversation, permanently and
233
+ invisibly. A failure now releases the booking, schedules a bounded retry (1 / 5 / 25 minutes,
234
+ three attempts), and only then drops the candidate with the reason recorded. The request itself is
235
+ bounded too: an `agents.resume` that never settles used to leave the global `asking` flag set,
236
+ which silenced the feature for every conversation rather than the broken one.
237
+
211
238
  ## An action belongs where its subject is unambiguous
212
239
 
213
240
  "Collect from this chat" was a toolbar button, and in a global panel "this chat" is a guess: the
package/docs/ROADMAP.md CHANGED
@@ -4,8 +4,9 @@ What could come next, ordered by value against effort. Everything here is a
4
4
  proposal, not a commitment.
5
5
 
6
6
  This file was audited against the tree: everything under **Landed** is in the code
7
- today, with the `file:line` that shows it. Line numbers move, so the durable part
8
- of a citation is the function or the label beside it.
7
+ today, cited by the file and the function that shows it. Line numbers were removed
8
+ after they drifted by dozens of lines: the function or the label beside a citation
9
+ is the durable part, and a stale number is worse than none.
9
10
 
10
11
  ## Landed
11
12
 
@@ -28,80 +29,87 @@ Things that used to sit on the Next list (or in the Later table) and are facts i
28
29
  tree now.
29
30
 
30
31
  **Attachment health** — a file link is probed when the list is read, never when it is
31
- stored: `annotateLinks()` (`lib/service.js:347`) sets `missing: true` for a path
32
- that no longer resolves (`lib/service.js:360`), and both read routes apply it
33
- (`GET /list` `lib/routes.js:257`, `GET /get` `lib/routes.js:311`). The panel
34
- shows the chip on the row and in the detail (`lib/client.js:1408`,
35
- `lib/client.js:1678`) with the label 「文件不在了」 (`lib/client.js:209`); the render
36
- check asserts it (`tools/preview.mjs:572`).
32
+ stored: `annotateLinks()` (`lib/service.js`) sets `missing: true` for a path
33
+ that no longer resolves (`lib/service.js`), and both read routes apply it
34
+ (`GET /list` `lib/routes.js`, `GET /get` `lib/routes.js`). The panel
35
+ shows the chip on the row and in the detail (`lib/client.js`,
36
+ `lib/client.js`) with the label 「文件不在了」 (`lib/client.js`); the render
37
+ check asserts it (`tools/preview.mjs`).
37
38
 
38
39
  Still owed from the original idea: 「重新挂载」 next to a dead link, and a unit test for
39
40
  `annotateLinks` itself — today the render assertion is the only coverage.
40
41
 
41
- **Smarter search** — `parseQuery()` (`lib/service.js:129`) turns `tag:发布`,
42
- `#发布`, `level:p0` (aliases at `lib/service.js:117`), `is:…` and `has:…` into
43
- filters, and `matchesSyntax()` (`lib/service.js:152`) applies `is:overdue`,
42
+ **Smarter search** — `parseQuery()` (`lib/service.js`) turns `tag:发布`,
43
+ `#发布`, `level:p0` (aliases at `lib/service.js`), `is:…` and `has:…` into
44
+ filters, and `matchesSyntax()` (`lib/service.js`) applies `is:overdue`,
44
45
  `is:due`, `is:active`, `has:link`, `has:note` and `is:<status>`; free text
45
- still has to match every word (`lib/service.js:196`). The panel advertises the syntax
46
- on the search box (`lib/client.js:210`, `lib/client.js:2705`) and the render check
47
- asserts the hint (`tools/preview.mjs:573`). Test: `test/service.test.mjs`, "the
46
+ still has to match every word (`lib/service.js`). The panel advertises the syntax
47
+ on the search box (`lib/client.js`) and the render check
48
+ asserts the hint (`tools/preview.mjs`). Test: `test/service.test.mjs`, "the
48
49
  search box understands tag:, is:, has: and level: tokens".
49
50
 
50
51
  Not landed from the same idea: `before:` / `after:` date bounds.
51
52
 
52
- **Progress notes** — an append-only trail per todo: `addNote()` (`lib/model.js:387`),
53
- repaired on load by `normalizeNotes()` (`lib/model.js:361`), reached through
54
- `note()` (`lib/service.js:329`), `POST /note` (`lib/routes.js:423`) and the agent
55
- tool `global_todo_note` (`lib/tools.js:618`), and shown as 「进展记录」 in the detail
56
- (`lib/client.js:200`, `lib/client.js:1648`). Tests: `test/model.test.mjs:210`,
57
- `test/tools.test.mjs:301`, `test/routes.test.mjs:495`.
53
+ **Progress notes** — an append-only trail per todo: `addNote()` (`lib/model.js`),
54
+ repaired on load by `normalizeNotes()` (`lib/model.js`), reached through
55
+ `note()` (`lib/service.js`), `POST /note` (`lib/routes.js`) and the agent
56
+ tool `global_todo_note` (`lib/tools.js`), and shown as 「进展记录」 in the detail
57
+ (`lib/client.js`). Tests: `test/model.test.mjs`,
58
+ `test/tools.test.mjs`, `test/routes.test.mjs`.
58
59
 
59
60
  **JSON export and import** — `GET /export` hands back the store's own document and
60
- `POST /import` writes one back (`lib/routes.js:320`, `lib/routes.js:332`), through
61
- `exportDocument()` / `importDocument()` (`lib/service.js:536`,
62
- `lib/service.js:554`). `replace` is a byte-for-byte round trip; `merge` updates by
61
+ `POST /import` writes one back (`lib/routes.js`), through
62
+ `exportDocument()` / `importDocument()` (`lib/service.js`,
63
+ `lib/service.js`). `replace` is a byte-for-byte round trip; `merge` updates by
63
64
  id and appends what is new. The panel has 导出 / 导入 in the header
64
- (`lib/client.js:2570`, `lib/client.js:2580`) and imports in merge mode on purpose: a
65
+ (`lib/client.js`) and imports in merge mode on purpose: a
65
66
  file picked by mistake must not erase the list. Round trip: `test/transfer.test.mjs`,
66
67
  "an export imported back reproduces the list exactly".
67
68
 
68
69
  Not landed: the Markdown / TaskPaper formats this entry used to ask for, and the 1 MiB
69
- request cap stays (`lib/routes.js:32`), so a list exported beyond that size cannot be
70
+ request cap stays (`lib/routes.js`), so a list exported beyond that size cannot be
70
71
  imported back through the route.
71
72
 
72
73
  **Metrics in /health** — the health reply now carries `metrics`
73
- (`lib/routes.js:349`): dispatch success rate, agent tool call counts, and list growth
74
+ (`lib/routes.js`): dispatch success rate, agent tool call counts, and list growth
74
75
  (created / removed / restored / imports / noted, current size, peak), built in
75
76
  `lib/metrics.js`. The counters are in memory and reset with the Host, and the field is
76
77
  `null` when nothing mounted them — an absent metric is not a zero. Test:
77
78
  `test/metrics.test.mjs`.
78
79
 
79
80
  **Reopen, with an honest trail** — a wrongly-finished todo has a dedicated way back:
80
- `reopen()` (`lib/service.js:318`) does `done` / `cancelled` → `open` and refuses anything else,
81
- reached by the tool `global_todo_reopen` (`lib/tools.js:747`); the reason it records lands in
82
- the activity trail via `setStatus()` (`lib/model.js:536`, `lib/model.js:554`). Writing through
81
+ `reopen()` (`lib/service.js`) does `done` / `cancelled` → `open` and refuses anything else,
82
+ reached by the tool `global_todo_reopen` (`lib/tools.js`); the reason it records lands in
83
+ the activity trail via `setStatus()` (`lib/model.js`). Writing through
83
84
  the panel's routes no longer forces a false provenance: `/update` and `/status`
84
- (`lib/routes.js:462`, `lib/routes.js:471`) take an explicit `by: 'agent'` and default to the
85
- person, and anything else is a 400 (`actorOf` at `lib/routes.js:254`). Tests:
86
- `test/tools.test.mjs:230`, `test/routes.test.mjs:142`.
85
+ (`lib/routes.js`) take an explicit `by: 'agent'` and default to the
86
+ person, and anything else is a 400 (`actorOf` at `lib/routes.js`). Tests:
87
+ `test/tools.test.mjs`, `test/routes.test.mjs`.
87
88
 
88
89
  Not landed: the same `by` on `/create`, `/note`, `/remove` and `/link`, which still record the
89
90
  person — the agent has its own tools for those. `/restore` (the panel's undo) has no agent-facing
90
91
  equivalent at all, so nothing there can be misattributed today.
91
92
 
93
+ **Automatic collect with a memory** — the decision layer keeps its reasons: `/health` reports
94
+ every waiting candidate's verdict (`off`, `exempt-workspace`, `unknown-dir`, `retry-wait`,
95
+ `awaiting-human`, `not-idle-yet`, `cooldown`), the last one, and in-memory counters for
96
+ asked / succeeded / failed / dropped (`explainAutoCollect` in `lib/settings.js`, `autoCollectState`
97
+ in `lib/index.js`). A failed question releases its booking and retries after 1 / 5 / 25 minutes
98
+ (`retryDelayMs`), and an ask that never settles is bounded by a ten-second timeout. An unknown
99
+ working directory is never asked, because the exemption cannot be checked against it.
92
100
  ### Partly landed
93
101
 
94
102
  - **Batch and multi-select** — the selection mode, its per-row markers and the batch
95
- 一键分配 are in (`lib/client.js:2209`, `lib/client.js:2884`, `POST /assign` at
96
- `lib/routes.js:413`, `assignMany()` at `lib/dispatch.js:195`; tests
97
- `test/dispatch.test.mjs:657`). Batch 完成 / 打标签 across a selection do not exist.
103
+ 一键分配 are in (`lib/client.js`, `POST /assign` at
104
+ `lib/routes.js`, `assignMany()` at `lib/dispatch.js`; tests
105
+ `test/dispatch.test.mjs`). Batch 完成 / 打标签 across a selection do not exist.
98
106
  - **Session read-back** — `sessionAlive` is on every listed todo
99
- (`lib/routes.js:259`, `lib/dispatch.js:698`). Reading what that session last said,
107
+ (`lib/routes.js`, `lib/dispatch.js`). Reading what that session last said,
100
108
  and linking to it, is not: `lib/completion.js` reads `assistant/message` only to
101
109
  close a todo this plugin dispatched, and stores one summary in its completion record.
102
110
  - **What the agents produced** — the per-todo record with a summary and the changed
103
- files landed (`completeTodo()` at `lib/model.js:415`, 「完成记录」 at
104
- `lib/client.js:1648`, the 已完成 filter and group at `lib/client.js:153`). The
111
+ files landed (`completeTodo()` at `lib/model.js`, 「完成记录」 at
112
+ `lib/client.js`, the 已完成 filter and group at `lib/client.js`). The
105
113
  cross-todo "what did the sessions finish this week, and what did they touch" view did
106
114
  not.
107
115
 
@@ -163,14 +171,14 @@ something you can review, and would be the honest way to show that a hand-off di
163
171
 
164
172
  | Idea | Note |
165
173
  |---|---|
166
- | Image attachments | `kind: 'image'` next to `url` / `file`. The shape is already prepared for it, but `LINK_KINDS` is still `['url', 'file']` (`lib/links.js:21`). |
167
- | Official `storageDomain` backend | A second store backend behind the same interface; the file stays the default because it is the feature. Today there is one backend and it says so (`kind: 'json'`, `lib/store.js:212`). |
174
+ | Image attachments | `kind: 'image'` next to `url` / `file`. The shape is already prepared for it, but `LINK_KINDS` is still `['url', 'file']` (`lib/links.js`). |
175
+ | Official `storageDomain` backend | A second store backend behind the same interface; the file stays the default because it is the feature. Today there is one backend and it says so (`kind: 'json'`, `lib/store.js`). |
168
176
  | Virtualised list | Only needed past a few hundred rows; measure first. Nothing in `lib/client.js` virtualises today. |
169
177
  | Drag to reorder | Manual order is meaningful to some people; it also needs a stable `position` field and a decision about how it interacts with due-date grouping. |
170
178
  | Multi-list / projects | A `list` field with one default list; deferred because "global single list" is the current identity. |
171
- | Archive instead of delete | Keeps history without cluttering. `POST /remove` deletes for real (`lib/routes.js:445`); only the panel's short undo brings a record back. |
179
+ | Archive instead of delete | Keeps history without cluttering. `POST /remove` deletes for real (`lib/routes.js`); only the panel's short undo brings a record back. |
172
180
  | Cross-machine conflict notice | The file is portable (NAS, sync folder). Detect "changed underneath me" and offer a reload instead of a silent overwrite — `store.read` still trusts the file whenever it parses. |
173
- | Capture from outside the chat | Notifications, mail, announcements — the sources `dsh-todo-list` reads. Needs an ingest path and a policy for deduplication; `POST /capture` only asks a conversation to collect (`lib/routes.js:418`). |
181
+ | Capture from outside the chat | Notifications, mail, announcements — the sources `dsh-todo-list` reads. Needs an ingest path and a policy for deduplication; `POST /capture` only asks a conversation to collect (`lib/routes.js`). |
174
182
  | Accessible-only sweep | Full keyboard path is close; a screen-reader pass over the panel and the group headers is still owed. |
175
183
 
176
184
  ## Explicitly not planned
package/docs/icon.svg ADDED
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512" role="img" aria-label="Global todos">
2
+ <rect width="512" height="512" rx="116" fill="#0d6b5e"/>
3
+ <rect x="92" y="104" width="328" height="304" rx="32" fill="#f4faf8"/>
4
+ <g>
5
+ <circle cx="152" cy="184" r="23" fill="#e7f2ef"/>
6
+ <path d="M141 184l8 8 15-17" fill="none" stroke="#0d6b5e" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
7
+ <rect x="198" y="174" width="168" height="20" rx="10" fill="#c8d6d1"/>
8
+ </g>
9
+ <g>
10
+ <circle cx="152" cy="264" r="23" fill="#e7f2ef"/>
11
+ <path d="M141 264l8 8 15-17" fill="none" stroke="#0d6b5e" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <rect x="198" y="254" width="132" height="20" rx="10" fill="#c8d6d1"/>
13
+ </g>
14
+ <g>
15
+ <circle cx="152" cy="344" r="23" fill="#f7edda"/>
16
+ <circle cx="152" cy="344" r="9" fill="#c98a1e"/>
17
+ <rect x="198" y="334" width="150" height="20" rx="10" fill="#d8e2de"/>
18
+ </g>
19
+ </svg>
Binary file
Binary file
Binary file