dsh-global-todo 0.3.2 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,111 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] — 2026-10-19
11
+
12
+ The half-finished half: the selection could only be assigned, and the only way to get a record out
13
+ of the list was to delete it. Both are now actions of their own.
14
+
15
+ - **A selection can be completed, archived and tagged.** `POST /bulk` applies one change across the
16
+ picked rows, and the selection bar gained 完成 / 归档 / a tag field. Each record still goes through
17
+ the same validation and activity trail as a single edit; an id that no longer exists is reported
18
+ in `unmatched` rather than failing the whole batch, because a selection is a human gesture and one
19
+ stale row must not discard the other nine.
20
+
21
+ - **Archive instead of delete.** `archived` joined the statuses, so a finished-or-abandoned record can
22
+ leave the active view without losing its history. The panel offers 归档 / 取消归档 in the row menu,
23
+ the filter chip appears only once something is archived, and `global_todo_update` now accepts
24
+ `status` at all — before this, marking something in progress required the tool named
25
+ `global_todo_done`, which is exactly the kind of name/capability mismatch that makes a model misuse
26
+ a tool.
27
+
28
+ Still deliberately out of scope for this release: the wide two-column detail layout, and the
29
+ cross-machine conflict check — both deserve their own pass rather than riding along with a data-model
30
+ change.
31
+ ## [0.4.0] — 2026-10-19
32
+
33
+ The first release after the plugin was listed in a community catalog. It is a reliability
34
+ release: the features that already existed now tell the truth — a sort that honours every level,
35
+ a page that says it was cut, a language setting that applies, and an automatic collector that can
36
+ explain itself and survives a failed question.
37
+
38
+ - **An unknown working directory is a reason not to ask.** The exemption is keyed on where a
39
+ conversation works; a place nobody can name cannot be checked against it, and asking there would
40
+ volunteer a question in a workspace somebody may well have excluded. The decision now reports
41
+ `unknown-dir` and stays quiet, and the panel's switch tooltip says so. The cost is a conversation
42
+ whose cwd the Host never reported, which the event tracker normally supplies on its first message.
43
+
44
+ - **The automatic collect switch shows the last decision.** `/health` now reports the full decision
45
+ list, so the switch's tooltip can say *why* nothing has happened — not quiet long enough, waiting
46
+ for you, cooling down, retrying, or an exempt workspace — together with how many candidates are
47
+ waiting and how many are due.
48
+
49
+ - **The automatic collect switch can explain itself.** The decision layer knew why a conversation
50
+ was skipped — switched off, exempt, not quiet yet, waiting for a person, cooling down — and the
51
+ timer threw that reason away, so `/health` could report `enabled: true` while nothing ever
52
+ happened and give no way to tell why. The whole decision list is reported now, with the last one,
53
+ the number of waiting candidates, and in-memory counters for asked / succeeded / failed / dropped.
54
+ The settings file also reports how it loaded (`file` / `missing` / `corrupt`): "off because
55
+ somebody turned it off" and "off because the file was unreadable" used to look identical.
56
+
57
+ - **A failed question no longer silences a conversation.** The cooldown was booked before the ask
58
+ and survived a failed request, so `awaiting-human` kept that conversation quiet until a person
59
+ happened to speak again — one transient resume failure turned the feature off for that
60
+ conversation, permanently and invisibly. A failure now releases the booking and schedules a
61
+ bounded retry (1 / 5 / 25 minutes, three attempts) before dropping the candidate with the reason
62
+ recorded. The ask is bounded too: an `agents.resume` that never settles now times out after ten
63
+ seconds instead of leaving the global `asking` flag set and silencing every conversation.
64
+
65
+ - **A priority sort that put `urgent` last.** The rank table was written when there were three
66
+ levels, and adding a fourth did not touch it: the comparator read `undefined` for `urgent` and
67
+ produced `NaN`, so `sort=priority` (and the panel's level grouping) moved P0 to the bottom.
68
+ Every level now has a rank, and the test names all four in order.
69
+
70
+ - **The list stopped at 200 without saying so.** `/list` cut the page at a fixed limit while
71
+ `counts` was computed over the whole file, so a list of 350 showed "350 open" above 200 rows.
72
+ The reply now carries `matched` (before the cut) and `truncated`, the panel prints "showing the
73
+ first N of M", and `global_todo_list` tells the model the same thing instead of letting it
74
+ reason about a page as if it were the whole set.
75
+
76
+ - **The panel described the quiet window with the cooldown number.** The hint said "quiet for
77
+ {n} minutes" while `{n}` was `cooldownMinutes`; the gate it describes is `idleMinutes`, and the
78
+ two only looked identical because both default to 30. A hint that lies once the value is
79
+ changed is worse than no hint.
80
+
81
+ - **Switching the interface language did nothing.** `resolveLang` read `locale` / `id` / `language`
82
+ / `current` from the locale snapshot, but the snapshot carries `active`; the value was never
83
+ found and the panel always fell back to `navigator.language`. It reads `active` first now, and
84
+ the unused `workspaces` client dependency is gone.
85
+
86
+ - **The English panel was partly Chinese.** Seventeen keys — the whole `note.*` and `completion.*`
87
+ groups, plus `link.missing`, `search.hint` and `detail.notes` — existed only in the Chinese
88
+ dictionary and silently fell back; `meta.completed` was Chinese in the English table as well.
89
+ Both tables now carry the same 186 keys, guarded by a test that also refuses a Chinese value in
90
+ the English one.
91
+
92
+ - **Two empty states, one wrong.** The panel decided between "no todos yet" and "nothing in this
93
+ filter" by looking at the current filter, so "all" with a search term and an empty list showed
94
+ the wrong sentence. The decision now uses the list's real size and whether a search is active.
95
+
96
+ - **Opening a row near the bottom did nothing visible.** The detail expanded below the viewport;
97
+ the row now scrolls into view when it opens.
98
+
99
+ - **The no-literal-colours test was checking fourteen characters.** It sliced from `const CSS =`
100
+ to the first `.gt-root` inside the stylesheet, so it never inspected a colour. The slice now
101
+ runs to the end of the template, which makes the guard real — and the theme-token rule is the
102
+ one thing that keeps dark mode working.
103
+
104
+ - **Documentation that had drifted from the tree.** Both READMEs gained the five routes they were
105
+ missing (`note`, `dirs`, `settings`, `assign`, `diagnostics`); `docs/DESIGN.md` records the four
106
+ fields and two activity kinds added since the table was written; `cordis.patch.yml` names
107
+ `guidance` and `browseRoot`, which a wholesale `config` override would otherwise drop;
108
+ `docs/ROADMAP.md` cites files and functions instead of line numbers that had moved by dozens of
109
+ lines.
110
+
111
+ - **The contribution terms are written down.** MIT in, MIT out — no CLA, no copyright assignment —
112
+ so a future patch cannot quietly stay its author's property and block a license change years
113
+ later. The READMEs also say plainly that this is a community plugin, not a DeepSeek project.
114
+
10
115
  ## [0.3.2] — 2026-10-19
11
116
 
12
117
  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
@@ -184,7 +194,7 @@ Your list is not touched by install or uninstall: it lives outside the plugin, a
184
194
  | `global_todo_capture` | **Record a batch**: for 「抓一下待办」 or a turn that produced several follow-ups; each entry is validated on its own |
185
195
  | `global_todo_list` | Query by state, tag or text; every row carries its creation time |
186
196
  | `global_todo_get` | Read one item in full, including attachments and the last hand-off |
187
- | `global_todo_update` | Change any field of one item; `""` clears, `[]` empties |
197
+ | `global_todo_update` | Change any field of one item, including `status` (`archived` archives without deleting); `""` clears, `[]` empties |
188
198
  | `global_todo_link` | Attach or detach documents and addresses |
189
199
  | `global_todo_done` | Mark done, back to open, or in progress |
190
200
  | `global_todo_reopen` | Put a **wrongly finished** item back (`done` / `cancelled` → `open`); refuses anything else, and the trail records it as the agent |
@@ -224,12 +234,14 @@ 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
+ | POST | `/api/global-todo/bulk` | `{ ids, status?, addTags?, removeTags? }` — one change across a selection; a stale id lands in `unmatched` instead of failing the batch |
244
+ | 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
245
  | POST | `/api/global-todo/create` | `{ title, description?, links?, tags?, priority?, dueAt? }` |
234
246
  | POST | `/api/global-todo/update` | `{ id, ...fields, by?: human\|agent }` |
235
247
  | POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
@@ -237,6 +249,9 @@ fence when that service is present, and by a strict loopback check otherwise.
237
249
  | POST | `/api/global-todo/restore` | `{ todo }` — put a removed record back verbatim (undo) |
238
250
  | POST | `/api/global-todo/link` | `{ id, add?: [...], remove?: [value \| index] }` |
239
251
  | POST | `/api/global-todo/dispatch` | `{ id, mode?: auto\|bound\|new\|current, sessionId?, dir? }` |
252
+ | 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 |
253
+ | POST | `/api/global-todo/settings` | `{ autoCollect?, idleMinutes?, cooldownMinutes?, exemptWorkspaces? }` — the Host-side switches behind automatic collect |
254
+ | POST | `/api/global-todo/diagnostics` | `{ scope, status?, reason?, message?, url? }` — the panel's own error report |
240
255
 
241
256
  `by` on `/update` and `/status` says whose write this is: omitted (what the panel sends) it is
242
257
  the person at the keyboard, and a script acting for the agent says `by: 'agent'` rather than
@@ -299,8 +314,13 @@ npm run check # parse every file, assert the shipping shape, and — when a
299
314
  # Host's own validator
300
315
  npm test # 100+ unit, contract and bundle tests, zero dependencies
301
316
  npm run verify # both
317
+ npm run check:matrix # the running dsh against engines.dsh, then the manifest,
318
+ # bundle and tool-schema contracts (no preview writes)
302
319
  ```
303
320
 
321
+ `engines.dsh` is the one version fact that matters: a Host outside it skips the whole bundle
322
+ instead of warning, so `check:matrix` is the smoke to run before widening the range.
323
+
304
324
  There is **no build step**. The host half is plain ESM under `lib/`; the client half is a
305
325
  classic script that registers itself with `window.__ModuleLoader__`, so no bundler,
306
326
  TypeScript toolchain or CI artifact is involved. `lib/client.js` edits take effect on a
@@ -342,3 +362,6 @@ Please report vulnerabilities as described in [SECURITY.md](SECURITY.md).
342
362
  ## License
343
363
 
344
364
  [MIT](LICENSE) © 2026 LUgABeEEE
365
+
366
+ A community plugin, not a DeepSeek project: the `dsh` prefix marks the ecosystem it plugs into, not
367
+ 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
@@ -113,7 +120,7 @@ dsh plugin --profile web remove dsh-global-todo
113
120
  | `global_todo_capture` | **一次登记多条**(批量抓取):用户说「抓一下待办」,或你一轮里发现好几件要跟进的;逐条校验,坏的那条只跳过它自己 |
114
121
  | `global_todo_list` | 按状态/标签/关键词查询,每条都带上加入时间 |
115
122
  | `global_todo_get` | 读一条的全文,含附件与最近一次派发 |
116
- | `global_todo_update` | 改任意字段;`""` 清空,`[]` 清空列表 |
123
+ | `global_todo_update` | 改任意字段,包括 `status`(`archived` 归档而不删除);`""` 清空,`[]` 清空列表 |
117
124
  | `global_todo_link` | 挂上或摘掉文档与地址 |
118
125
  | `global_todo_done` | 标完成 / 标回未完成 / 标进行中 |
119
126
  | `global_todo_reopen` | 把**被误标完成**的一条标回未完成(只做 `done` / `cancelled` → `open`);本来就未完成的会被拒绝,动作按 agent 记进活动轨迹 |
@@ -129,13 +136,15 @@ 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
+ | POST | `/api/global-todo/bulk` | `{ ids, status?, addTags?, removeTags? }`——一次改一整批;失效的 id 进 `unmatched`,不会让整批失败 |
147
+ | GET | `/api/global-todo/health` | —(数据位置、统计、派发能力,以及**工具注册状态**与**自动收尾观察者是否挂上**——这两种失败是静默的,所以必须能查;还有 `metrics`:派发成功率、工具调用次数、清单增长;`autoCollect`:每个候选对话**为什么被问/没被问**、计次,以及设置文件是怎么加载进来的) |
139
148
  | POST | `/api/global-todo/create` | `{ title, description?, links?, tags?, priority?, dueAt? }` |
140
149
  | POST | `/api/global-todo/update` | `{ id, ...字段, by?: human\|agent }` |
141
150
  | POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
@@ -143,6 +152,9 @@ dsh plugin --profile web remove dsh-global-todo
143
152
  | POST | `/api/global-todo/restore` | `{ todo }` —— 原样还原被删除的记录(撤销) |
144
153
  | POST | `/api/global-todo/link` | `{ id, add?: [...], remove?: [值 或 下标] }` |
145
154
  | POST | `/api/global-todo/dispatch` | `{ id, mode?: auto\|bound\|new\|current, sessionId?, dir? }` |
155
+ | POST | `/api/global-todo/assign` | `{ ids, items?, target?, dir? }`——一次把多条交给 Agent;配不上的 id 原样报回,不擅自送到别处 |
156
+ | POST | `/api/global-todo/settings` | `{ autoCollect?, idleMinutes?, cooldownMinutes?, exemptWorkspaces? }`——自动获取的开关与阈值 |
157
+ | POST | `/api/global-todo/diagnostics` | `{ scope, status?, reason?, message?, url? }`——面板自己上报的错误 |
146
158
 
147
159
  `/update` 与 `/status` 的 `by` 说明这次写是谁做的:不传(面板就是这样)就是人在键盘前,
148
160
  脚本替 agent 干活时写 `by: 'agent'`,而不是被记成人的操作。传别的值是 `400`——
@@ -200,8 +212,13 @@ npm run check # 解析全部文件、校验发布形态;本机有 DSH prof
200
212
  # 自己的校验器逐条验证工具 schema
201
213
  npm test # 100+ 单元/契约/bundle 测试,零依赖
202
214
  npm run verify # 两者都跑
215
+ npm run check:matrix # 当前 dsh 与 engines.dsh 的实际匹配,再跑 manifest/bundle/
216
+ # 工具 schema 三类契约(不写 preview)
203
217
  ```
204
218
 
219
+ `engines.dsh` 是唯一真正决定“装不装得上”的版本事实:区间外的宿主会**整层跳过**这个
220
+ bundle,而不是警告;所以放宽区间之前先跑 `check:matrix`。
221
+
205
222
  **没有构建步骤。** 宿主半区是 `lib/` 下的纯 ESM;客户端半区是一个自注册到
206
223
  `window.__ModuleLoader__` 的普通脚本——不需要打包器、不需要 TypeScript 工具链、
207
224
  CI 里也没有构建产物。改 `lib/client.js` 刷新页面即生效;改宿主半区要重启宿主。
@@ -242,3 +259,5 @@ HTTP 接口只服务本机,清单是本地文件,任何凭据都不会被存
242
259
  ## 许可
243
260
 
244
261
  [MIT](LICENSE) © 2026 LUgABeEEE
262
+
263
+ 社区插件,不是 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
@@ -14,8 +14,8 @@ so the file stays readable, hand-editable and diffable.
14
14
  | `id` | string | `t-` + 8 hex; unique within the file |
15
15
  | `title` | string | 1–200 characters, trimmed |
16
16
  | `description` | string | up to 10 000 characters, Markdown-ish plain text |
17
- | `status` | `open` \| `doing` \| `done` \| `cancelled` | `doing` means someone is on it |
18
- | `priority` | `low` \| `normal` \| `high` | three levels people actually use |
17
+ | `status` | `open` \| `doing` \| `done` \| `cancelled` \| `archived` | `doing` means someone is on it; `archived` keeps the record out of the active view without deleting it |
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,92 @@ 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.
100
+ **Batch completion and tagging, and archive** — `/bulk` applies a status or tag change across a
101
+ selection in one request; a stale id is reported in `unmatched` instead of discarding the batch
102
+ (`bulk()` in `lib/service.js`, `runBulk` in `lib/client.js`). `archived` joined the statuses, so a
103
+ record can leave the active view without being deleted; the filter chip only appears once something
104
+ is archived, and `global_todo_update` finally accepts `status`.
92
105
  ### Partly landed
93
106
 
94
107
  - **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.
108
+ 一键分配 are in (`lib/client.js`, `POST /assign` at
109
+ `lib/routes.js`, `assignMany()` at `lib/dispatch.js`; tests
110
+ `test/dispatch.test.mjs`). Batch 完成 / 打标签 across a selection do not exist.
98
111
  - **Session read-back** — `sessionAlive` is on every listed todo
99
- (`lib/routes.js:259`, `lib/dispatch.js:698`). Reading what that session last said,
112
+ (`lib/routes.js`, `lib/dispatch.js`). Reading what that session last said,
100
113
  and linking to it, is not: `lib/completion.js` reads `assistant/message` only to
101
114
  close a todo this plugin dispatched, and stores one summary in its completion record.
102
115
  - **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
116
+ files landed (`completeTodo()` at `lib/model.js`, 「完成记录」 at
117
+ `lib/client.js`, the 已完成 filter and group at `lib/client.js`). The
105
118
  cross-todo "what did the sessions finish this week, and what did they touch" view did
106
119
  not.
107
120
 
@@ -163,14 +176,14 @@ something you can review, and would be the honest way to show that a hand-off di
163
176
 
164
177
  | Idea | Note |
165
178
  |---|---|
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`). |
179
+ | 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`). |
180
+ | 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
181
  | Virtualised list | Only needed past a few hundred rows; measure first. Nothing in `lib/client.js` virtualises today. |
169
182
  | 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
183
  | 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. |
184
+ | 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
185
  | 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`). |
186
+ | 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
187
  | Accessible-only sweep | Full keyboard path is close; a screen-reader pass over the panel and the group headers is still owed. |
175
188
 
176
189
  ## 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