dsh-global-todo 0.17.0 → 0.17.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
@@ -1,3 +1,17 @@
1
+ ## [0.17.1] — 2026-10-24
2
+
3
+ Two READMEs instead of one: Chinese in `README.md`, English in `README.en.md`, and the switch to
4
+ the other language on the first line of each.
5
+
6
+ - **Two READMEs, one language each.** `README.md` is the Chinese document — the name GitHub's
7
+ repository page shows and the only one npm renders — and `README.en.md` is the English one; each
8
+ opens with a one-line switch to the other. The 0.17.0 npm page had an English document with the
9
+ Chinese only reachable through a relative link, and relative links are exactly what npm rewrites
10
+ or drops, so the switch is an **absolute** GitHub URL (a CJK heading slug is not stable across
11
+ renderers either). `README.zh.md` becomes a two-line landing page so old links still resolve;
12
+ `README.en.md` joined `files`, and `test/manifest.test.mjs` now reads the Node and DSH floors out
13
+ of the two real documents. AGENTS.md records the convention.
14
+
1
15
  ## [0.17.0] — 2026-10-24
2
16
 
3
17
  Documentation and prompt text: the confirmation gate is the headline now, not the third bullet.
package/README.en.md ADDED
@@ -0,0 +1,608 @@
1
+ **English** · [中文](https://github.com/LUgABeEEE/dsh-global-todo/blob/main/README.md)
2
+
3
+ # dsh-global-todo
4
+
5
+ <img src="docs/icon.svg" width="52" height="52" alt="全局待办 icon: a list card with a check and an arrow leaving it">
6
+
7
+ **A cross-session todo list for DeepSeek Harness: one file you own, written by you and your
8
+ agent, and handed to a conversation in one click.**
9
+
10
+ Three things it does that a session-local plan cannot — the first is the one it is known for:
11
+
12
+ 1. **An agent saying "done" is a claim, whichever door it comes through.** Its own report — the
13
+ tool call, a status change — and the host closing the todo after a clean turn are both just
14
+ claims: the record waits in **待确认 / Needs review** until a person confirms it, or sends it
15
+ back with its completion cleared. A session-local plan never asks that question: the agent
16
+ rewrites the table and the work is "finished".
17
+ 2. **One JSON file is the whole list.** `cat`, diff, commit it, put it on a NAS, carry it to
18
+ another machine. Nothing lives inside a session log.
19
+ 3. **Hand-off is a button, not a paragraph.** Four modes, the working directory checked, and
20
+ the todo's own **steps** arrive as a numbered plan.
21
+
22
+ ![The panel in use: list, detail, steps, review, inbox, reminders, selection](docs/screenshots/tour.gif)
23
+
24
+ ```sh
25
+ dsh plugin add dsh-global-todo
26
+ ```
27
+
28
+ [Live page](https://lugabeeee.github.io/dsh-global-todo/) (22-second typographic demo) · [中文](https://github.com/LUgABeEEE/dsh-global-todo/blob/main/README.md#zh) · [5-minute quickstart](docs/QUICKSTART.md) · [FAQ](docs/FAQ.md) · [Every state has a baseline image](#screenshots) · [Design notes](docs/DESIGN.md)
29
+
30
+ ---
31
+
32
+ ## Why another todo plugin
33
+
34
+ DSH's built-in `todo_write` belongs to one agent session: whole-list replacement, three
35
+ states, and a plan that disappears when the session does. The community plugins that fix
36
+ that split into two camps — the ones that dispatch work to agents (auto-resume, bound
37
+ sessions) and the ones that accept real document attachments. This plugin does both, on
38
+ one record:
39
+
40
+ | | built-in `todo_write` | dsh-global-todo |
41
+ |---|---|---|
42
+ | Owner | one agent session | **you** — across sessions and workspaces |
43
+ | Writing | whole-list replacement | per-field, human **and** agent |
44
+ | When it was added | not tracked | **`createdAt`, immutable, shown everywhere** |
45
+ | Context | title + state | description + **documents and addresses** |
46
+ | Hand-off | — | **one click: reuse the bound conversation or open a session** |
47
+ | Finishing | whatever the agent writes | **waits in Needs review for a person** |
48
+ | Storage | session event log | one JSON file you can `cat`, edit, diff and back up |
49
+
50
+ ## Confirmation: no record finishes itself
51
+
52
+ This is the rule the plugin is known for, and the one that separates it from every list that lets
53
+ the agent report its own progress: **an agent's "done" is a sentence it said.** The record is
54
+ written as `done` and carries `review: { pending: true }`; the row shows a **Needs review** badge
55
+ and the header a chip with the count, until a person answers.
56
+
57
+ Both doors are gated, because both are only a claim:
58
+
59
+ | Where "done" came from | The record | Who answers |
60
+ |---|---|---|
61
+ | the agent's `global_todo_done`, or a status change to `done` | `done` **+ pending review** | a person |
62
+ | the dispatched conversation ended cleanly, so the host closed it (`turn/end` = `completed`) | `done` **+ pending review** | a person |
63
+ | you clicked done in the panel | `done` | already a person |
64
+
65
+ - **Two answers.** *Confirm done* drops the mark and records who confirmed it; *Send back* returns
66
+ the todo to `open`, clears `doneAt` and the completion record, and adds a `reopened` entry.
67
+ Nothing is deleted: title, description, steps, attachments and notes all stay.
68
+ - **The automatic close goes through the same gate.** What the host writes on the agent's behalf is
69
+ thinner than what the agent could have written — only the closing message and the files that turn
70
+ changed — so it has even less business counting by itself.
71
+ - **Only clean turns are candidates.** `error`, `aborted`, `interrupted` and `max-tokens` leave the
72
+ todo open. A failed job wearing a "done" label is the one failure mode that would make the whole
73
+ list untrustworthy.
74
+
75
+ There is exactly one implementation of the rule: `completeTodo`, `applyPatch` and a status change in
76
+ `lib/model.js` share the same `by === 'agent'` test, and the panel and the ten tools all go through
77
+ it.
78
+
79
+ ## What 0.9 and 0.10 added (and the two just before)
80
+
81
+ Five surfaces these READMEs had never explained. Session read-back and evidence came in 0.9;
82
+ Markdown and the weekly report in 0.10; steps in 0.8. Each changes what a record can carry, or what
83
+ you can do with it after the fact. (The review mark came in 0.7 and now opens this document, instead
84
+ of hiding among the new features.)
85
+
86
+ ### Steps: a plan inside the todo
87
+
88
+ A todo can carry up to 20 steps of 200 characters (`LIMITS` in `lib/model.js`). A step is
89
+ `{ text, done }` — a line inside one unit of work, not a subtask with its own id and status.
90
+
91
+ - **Add them** in "More fields" → Steps, one per line.
92
+ - **Tick them** in the detail panel; the row shows `steps 2/5`.
93
+ - **Hand off** and the dispatch prompt numbers them, prefixing finished ones with `[已完成]`.
94
+ - **Export** and Markdown writes them as nested `- [ ]` lines; import reads them back.
95
+
96
+ ### Evidence: the sentence it came from
97
+
98
+ `evidence` is the sentence a captured todo came from, quoted verbatim, up to 300 characters.
99
+ `global_todo_capture` asks the model for it, `global_todo_add` accepts it, and the detail panel
100
+ shows it as a blockquote under **In the author's words**. A todo you typed yourself has none.
101
+
102
+ The field exists for one reason: a capture without its source reads as an opinion later. It is
103
+ also in the Markdown export (`原话:`).
104
+
105
+ ### Session read-back
106
+
107
+ A hand-off starts work in another conversation, so the detail panel answers "what did it say"
108
+ without switching away. `GET /api/global-todo/session?id=` returns `alive`, `dir` and a `tail`
109
+ of the last assistant message; the panel shows **Session read-back** with running / dormant, the
110
+ last sentence, and a button that opens that conversation.
111
+
112
+ The tail is bounded and in memory: 200 conversations, 400 characters each, filled from every
113
+ `assistant/message`. Nothing is written to disk, so a Host restart forgets it. A missing tail is
114
+ normal — this process has not seen that conversation speak.
115
+
116
+ ### Markdown in and out
117
+
118
+ The JSON export is the round-trip format. Markdown is the one you read, diff and edit by hand
119
+ (note that `doing` and `cancelled` come back as `open`: a hand-edited list has no work-in-progress
120
+ state, and the JSON remains the format that round-trips exactly):
121
+
122
+ ```text
123
+ - [ ] 标题
124
+ 优先级: 高 · 截止: 2026-10-21T18:00 · 标签: 发布, 文档
125
+ 原话: …
126
+ 描述: 第一行
127
+ - [x] 一步
128
+ ```
129
+
130
+ | Direction | Where | Behaviour |
131
+ |---|---|---|
132
+ | Out | **More actions → Export Markdown**, or `GET /export?format=md` | Grouped 未完成 / 已完成 / 已归档; metadata indented under each task |
133
+ | In | **More actions → Import** (a `.md` file), or `POST /import { markdown }` | Creates new todos through `create`; Markdown has no ids, so it never overwrites an existing record |
134
+
135
+ Tolerant by design: unknown lines are ignored, an unparseable date is dropped, and a file with no
136
+ `- [ ]` line is refused with a reason. Field labels are Chinese (`优先级 / 截止 / 标签 / 原话 /
137
+ 描述 / 附件 / #`), and import reads the same set.
138
+
139
+ ### Weekly report
140
+
141
+ **More actions → This week** (`GET /report?days=7`) answers "what moved, and did any of it land
142
+ in code". Everything is derived from completion records; nothing is stored.
143
+
144
+ | Field | Meaning |
145
+ |---|---|
146
+ | `finished` | completions in the window, newest first, with the actor and the summary |
147
+ | `sessions` | distinct conversations those completions came from |
148
+ | `created` | todos added in the window |
149
+ | `pendingReview` | done items still waiting for an answer |
150
+ | `files` | file artifacts touched, most frequent first — 12 listed, `fileCount` total |
151
+
152
+ `days` above 90 is clamped to 90; a missing or non-positive value falls back to 7.
153
+
154
+ ## Screenshots
155
+
156
+ ![The panel: count chips, grouped list, urgent rows marked](docs/screenshots/panel.png)
157
+
158
+ ![One row opened: inline editing, attachments with 打开 / 复制路径, activity](docs/screenshots/panel-detail.png)
159
+
160
+ ![Settings: grouped fields, hints under each label, one save row](docs/screenshots/panel-settings.png)
161
+
162
+ Every state that only appears under a condition has a fixed baseline image, rendered by the same
163
+ preview and photographed by the same tool:
164
+
165
+ | State | Image |
166
+ |---|---|
167
+ | Empty list | [`panel-empty.png`](docs/screenshots/panel-empty.png) |
168
+ | Filter with no match | [`panel-filtered.png`](docs/screenshots/panel-filtered.png) |
169
+ | Inbox (awaiting confirmation) | [`panel-inbox.png`](docs/screenshots/panel-inbox.png) |
170
+ | Awaiting review (agent finished) | [`panel-review.png`](docs/screenshots/panel-review.png) |
171
+ | Due reminder | [`panel-reminders.png`](docs/screenshots/panel-reminders.png) |
172
+ | Selection mode | [`panel-select.png`](docs/screenshots/panel-select.png) |
173
+
174
+ These are produced by `npm run screenshots` (which chains `npm run preview`): the real client
175
+ half is rendered to static HTML with no Host and no dependencies, then photographed at 1280×720
176
+ into `docs/screenshots/`. Playwright is not a dependency — the tool borrows the copy a DSH
177
+ profile already installed, or whatever `DSH_PLAYWRIGHT` points at.
178
+
179
+ What each file shows, and when it is regenerated, is indexed in
180
+ [docs/screenshots/README.md](docs/screenshots/README.md).
181
+
182
+ ## Requirements
183
+
184
+ - DeepSeek Harness `0.2.x` (developed and verified against `0.2.1-rc.2`; `engines.dsh` is
185
+ `>=0.2.0-rc.1 <0.3.0`, and a Host outside it skips the bundle instead of warning)
186
+ - Node.js `>= 22.19.0` (the Web profile's own requirement)
187
+ - A **Web** client: the panel is served by the Host's web server. Headless / ACP / SDK keep the
188
+ agent tools but have no panel; an Electron-style desktop client (file:// + IPC) is not
189
+ supported yet
190
+ - After a Host upgrade, refresh the page — the client module graph belongs to the release the
191
+ page booted with
192
+ - No runtime dependencies. Nothing is imported from `@deepseek-ai/*`.
193
+
194
+ ## Install
195
+
196
+ **Check your profile name first.** The plugin is installed into one profile, not into the
197
+ machine; a wrong profile name still exits 0, and then nothing shows up in the sidebar after a
198
+ restart — the most common first-run trap.
199
+
200
+ ```sh
201
+ ls ~/.dsh/profiles # Windows: dir %USERPROFILE%\.dsh\profiles
202
+ ```
203
+
204
+ Replace `<profile>` below with one of the names that command prints (`web` on most hosts;
205
+ use your desktop or `headless` profile name if that is what you run):
206
+
207
+ ```sh
208
+ # from npm
209
+ dsh plugin --profile <profile> add dsh-global-todo
210
+
211
+ # or from a checkout, which is also the development path
212
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-global-todo
213
+ ```
214
+
215
+ The plugin is a Loader row, not a hot-loaded script: **restart the host** (`dsh web`, or
216
+ whatever start command you use) after installing, then refresh the page. A "待办 / Todos"
217
+ entry appears in the sidebar.
218
+
219
+ > Sidebar still empty? Check three things in order: ① which profiles `ls` actually printed and
220
+ > whether the one you installed into is the one running; ② whether the host really restarted
221
+ > (the Loader is read once at boot); ③ whether the page was refreshed (the client module graph
222
+ > belongs to the release the page booted with).
223
+
224
+ The npm package is the source of truth. A community catalog can lag behind it or point at an
225
+ older release; pin the version you want (`dsh plugin --profile <profile> add dsh-global-todo@0.13.1`)
226
+ when it matters.
227
+
228
+ ### Installing from DSH's Discover tab
229
+
230
+ DSH's Discover tab reads a plugin market index from a URL you add yourself — dsh ships none, and
231
+ a user-added index is the one path that installs without copying a command. This repository
232
+ publishes one:
233
+
234
+ ```text
235
+ https://lugabeeee.github.io/dsh-global-todo/market/index.json
236
+ ```
237
+
238
+ It is generated from npm by `npm run market` (`docs/market/index.json`) and validated against
239
+ dsh's own `@deepseek-ai/dsh-plugin-market-index` before it is written. `npm test` fails when its
240
+ newest version is not the version in `package.json`, so a release that forgets to regenerate it
241
+ is caught before it ships.
242
+
243
+ **Hand-copied a directory instead?** Check it before wondering why the plugins page looks wrong:
244
+
245
+ ```sh
246
+ npx dsh-global-todo-doctor "$HOME/.dsh/plugins-local/全局待办插件"
247
+ ```
248
+
249
+ It verifies the manifest, the patch file, both entries, `locale/`, the icon and `lib/`, and exits
250
+ non-zero when something required is missing — the `locale/` case is the field failure that makes
251
+ the desktop app print `Plugin metadata … ENOENT … \locale` while the plugin still loads.
252
+
253
+ To roll back:
254
+
255
+ ```sh
256
+ dsh plugin --profile <profile> remove dsh-global-todo
257
+ ```
258
+
259
+ Your list is not touched by install or uninstall: it lives outside the plugin, at
260
+ `$DSH_HOME/global-todo/todos.json`.
261
+
262
+ ## Using it
263
+
264
+ ### The panel
265
+
266
+ - **Add** — type a title and press Enter. "More fields" reveals the description, the
267
+ attachment box (one document path or URL per line), tags, priority and a due time. An
268
+ attachment may also be `{ kind: "image", value: "/abs/shot.png" }` — the row then shows a
269
+ thumbnail and the detail a preview, with the bytes read on demand (png/jpg/jpeg/webp/gif/avif,
270
+ ≤8 MB) and never copied into the list.
271
+ - **Filters** — all / open / doing / done / overdue, plus a text search across titles,
272
+ descriptions, tags and attachments. The count chips in the header are filters too.
273
+ - **Grouped by urgency** — overdue / today / next up / later / no due date / done, each
274
+ collapsible, because "what is late" is the first question a list has to answer.
275
+ - **Rows** — show `added <time> (<age>)` with the exact timestamp on hover, the priority,
276
+ the due time, the tags, the attachment count, and which side created the entry.
277
+ - **Details** — edit title, description, priority and due time inline; attach or detach
278
+ documents and addresses (with 打开 / 复制路径 on each); read the activity trail.
279
+ - **Undo a delete** — deleting keeps the record for eight seconds and offers 撤销, which
280
+ restores it verbatim: same id, same creation time, same attachments.
281
+ - **Keyboard** — `/` focuses search, `n` focuses the new-todo field while the add form is
282
+ open, `Esc` collapses the open detail. Row actions appear on hover or keyboard focus.
283
+ - **Reachable from the LAN and from a tunnel** — the routes defer to the Host's connection fence
284
+ (which honours `--trusted-host`); the plugin's own loopback check is only the fallback for a
285
+ Host without that service. `/health` reports `fence` so you can see which one is in force.
286
+ - **Auto-collect (off by default)** — a panel switch. When on, a conversation you are in that
287
+ finishes a clean turn is asked once whether anything needs following up, but only once it has
288
+ been quiet for 30 minutes, and only if you are actually in that conversation (never a dispatch
289
+ target, a subagent or a background run). Aborted and failed turns are skipped. The question is
290
+ itself a message in the conversation, so it restarts the quiet clock — which is why **only a
291
+ message you wrote arms it again**: one question per turn from you, and it can never chase its own
292
+ answer. The switch is Host state, so it survives the panel being closed and is reported by
293
+ `/health`. Each conversation's group also carries an **`Exempt`** button: it takes that
294
+ conversation's whole **working directory** out of automatic collect (click again to undo). That
295
+ is for conversations a machine drives — a chat companion's room — whose session is recreated with
296
+ a new id when it restarts, so the exemption is keyed on the directory rather than on one
297
+ conversation and cannot quietly stop applying.
298
+ - **Collect per conversation** — the `Collect` action sits on each conversation's group header in
299
+ the list, asks *that* conversation to record what needs following up, and the items land under
300
+ it. The toolbar keeps a "collect from this chat" fallback, shown only when the current
301
+ conversation has no group yet. A conversation that is not running is resumed first, like a
302
+ hand-off.
303
+ - **Batch assignment** — switch on `选择`, tick a few todos, and one click sends each one to the
304
+ conversation that wrote it, else to a same-workspace conversation, else to a target you pick.
305
+ That is an attempt order: an unloadable target means the next one is tried. Anything with
306
+ nowhere to go comes back with a picker rather than being sent somewhere unchosen, and one
307
+ failure does not abort the batch.
308
+ - **Search syntax** — `tag:release` (or `#ops`), `is:overdue`, `is:active`, `has:link`, `before:2026-11-01` / `after:2026-10-01` (deadline bounds; a half-typed date simply stops filtering),
309
+ `has:note`, `level:p0`, plus free words combined with AND (which also match note text). A
310
+ search without a colon behaves exactly as it always did.
311
+ - **Progress notes** — an append-only trail per todo (count on the row, full text and an add
312
+ box in the detail). An agent reporting a blocker writes here instead of rewriting your
313
+ description, and writing one makes automatic completion stand down.
314
+ - **Dead attachments are flagged** — a file link whose target is gone is marked on the row and
315
+ labelled in the detail. Computed on read, never stored.
316
+ - **Where a new conversation goes** — a hand-off that opens one joins the workspace owning
317
+ the conversation that wrote the todo (and starts in that workspace's directory), so
318
+ "continue this work" does not land beside unrelated sessions. The picker also lists the
319
+ workspaces (`+ new conversation · <workspace>`), and a named workspace wins over the
320
+ inherited one. The working directory only decides when neither is known.
321
+ - **The picker lists what you actually use, newest first, without archived conversations.** The
322
+ order is the sidebar's recency key (`updatedAt`). Conversations sharing a title get a short id
323
+ suffix, and a target that is no longer listed (archived since it was chosen) is shown as
324
+ `(archived)` instead of appearing as a different conversation.
325
+ - **You pick the target** — the toolbar shows `交给:[ conversation ▾ ]`, listing your
326
+ conversations: whichever you choose is sent with `authoritative: true` and the Host honours
327
+ it verbatim, with no inference on top. The list marks the conversation the Host would have
328
+ chosen on its own, `自动` restores that behaviour, and `+ a new conversation` starts a fresh
329
+ context. A choice is remembered, and changing it is one click in the same control — what you
330
+ see is what happens.
331
+ - **A receipt for every hand-off** — the panel says which conversation received the work and
332
+ offers to open it. A dispatch starts work *elsewhere*, so staying silent looked exactly
333
+ like "nothing happened".
334
+ - **Hand to agent** — the panel says **where it will go** before you press it, and the
335
+ default is `current`: the conversation **you** last wrote in (read from the session log,
336
+ keeping only `source.kind === 'user'` messages — the plugin's own injected hand-off
337
+ messages are excluded, or each dispatch would drag the next one to its own target; the id
338
+ is also kept next to the data so a restart does not forget it). `auto` prefers the
339
+ conversation this todo already owns, then the current one, and only then opens a fresh
340
+ session; `new` always opens one; `bound` insists on the bound conversation and says why
341
+ if it is gone. When nothing usable can be resolved the hand-off **fails with a reason**
342
+ instead of quietly opening another conversation.
343
+ - **Grouping** — the toolbar offers `conversation / time / level` and defaults to
344
+ **conversation**: everything one agent conversation submitted collapses into a single
345
+ entry that says how many it holds, so a few 「抓一下待办」 runs do not turn the list into a
346
+ wall of rows. A conversation with one item shows that row directly; items with no source
347
+ conversation (added in the panel) sit under 手动添加. `time` is the original
348
+ overdue / today / soon / later / no-due buckets, `level` groups by priority.
349
+ - **Levels P0–P3** — `P0 urgent / P1 high / P2 normal (default) / P3 low`; the API accepts
350
+ either the canonical names or the labels (`priority: "P0"` works), with a badge on the
351
+ row. Records written before levels existed keep their `high/normal/low`, so nothing needed a
352
+ migration. A conversation group shows the highest level it holds, and a green dot marks a
353
+ group with a conversation still running.
354
+ - **Collect from this chat** — one button injects "record what this conversation needs to
355
+ follow up, with `global_todo_capture`" into the conversation you are in, so a chat turns
356
+ into todos without typing them out. The prompt states what *not* to record as well
357
+ (session plans, small talk, finished work) — a capture instruction without that ceiling
358
+ only produces noise.
359
+ - **Automatic completion** — when a handed-off session closes its turn **cleanly**, the Host
360
+ finishes the todo itself: the agent's closing message becomes the summary and the files
361
+ that turn touched become the artifacts. No waiting for the model to remember a tool call.
362
+ A finished row shows 完成于 … plus artifact chips, and its detail carries a completion
363
+ section you can open or copy paths from. Two guards matter: only a session this plugin
364
+ **just dispatched to** can complete itself (idle chatter in a bound conversation must not
365
+ finish work), and a turn that **failed or was interrupted is never marked done** — an open
366
+ todo is far better than a failed job wearing a "done" label. `global_todo_done` with
367
+ `summary` and `artifacts` remains the primary path; the watcher is the safety net.
368
+ - **Working directory** — the toolbar says where a new session will start. It follows the
369
+ conversation you are in by default; 选择 picks one explicitly and remembers it, and
370
+ 跟随当前会话 clears that choice. A todo also remembers the directory it was written
371
+ from (taken from the tool call's own context), and that is inherited too. With none of
372
+ the three available, the hand-off is **refused with a reason** — a session without a
373
+ working directory is a session where the agent can do nothing.
374
+
375
+ ### Agent tools
376
+
377
+ | Tool | Purpose |
378
+ |---|---|
379
+ | `global_todo_add` | Record an item — description, attachments, steps, tags, priority, due time |
380
+ | `global_todo_note` | Append a progress note — where you are, what blocks you, who must decide |
381
+ | `global_todo_capture` | **Record a batch**: for 「抓一下待办」 or a turn that produced several follow-ups; each entry is validated on its own, and entries scoring below `captureMinWeight` are reported as skipped with their weight instead of being registered |
382
+ | `global_todo_list` | Query by state, tag or text; every row carries its creation time |
383
+ | `global_todo_get` | Read one item in full, including attachments and the last hand-off |
384
+ | `global_todo_update` | Change any field of one item, including `status` (`archived` archives without deleting); `""` clears, `[]` empties |
385
+ | `global_todo_link` | Attach or detach documents and addresses |
386
+ | `global_todo_done` | Mark done, back to open, or in progress |
387
+ | `global_todo_reopen` | Put a **wrongly finished** item back (`done` / `cancelled` → `open`); refuses anything else, and the trail records it as the agent |
388
+ | `global_todo_remove` | Delete permanently (done items should be marked, not deleted) |
389
+
390
+ ### Live check
391
+
392
+ The test suite uses doubles; `npm run check:live` checks the **wired** Host instead —
393
+ the routes the panel actually calls, the list it actually reads, and a record that survives
394
+ delete-then-undo. It creates a temporary record, compares every field after the round trip
395
+ (id, creation time, attachments, tags, level, due time, description), asserts that bound
396
+ records carry `sessionAlive`/`sessionDir`, and removes its temporary record even when it
397
+ fails. It needs a running Host, so it is not part of `verify`.
398
+
399
+ ### Proactive capture
400
+
401
+ A guidance section is injected into the system prompt (turn it off with `guidance: false`).
402
+ It states both sides of the policy, because a prompt that only says "remember things"
403
+ produces noise, and a noisy list is worse than an empty one:
404
+
405
+ - **record on your own initiative** when the user mentions something that needs following
406
+ up across sessions (waiting on a reply, due next week, a decision pending, a promise),
407
+ when a turn ends with a step that should not continue in that session, or when the user
408
+ says 「记一下 / 别忘了 / 回头办」;
409
+ - **do not record** what you can finish in this step, the session's own plan, or small talk;
410
+ - say in **one line** what you recorded — do not restate the description;
411
+ - say **「抓一下待办」** and the agent scans the recent conversation, records the batch with
412
+ `global_todo_capture` and answers with a summary.
413
+
414
+ The boundary: it **records, it never starts work**. The list belongs to the human, and
415
+ starting a task stays a human decision (see the non-goals in [ROADMAP](docs/ROADMAP.md)).
416
+
417
+ Capture is not allowed to become noise. Before an item is written, the same normalized title
418
+ (full-width, case, spacing and punctuation folded away), a close paraphrase within the last 72
419
+ hours (Dice similarity over character bigrams, 0.8), and the same errand finished in the last 7
420
+ days are all refused with a reason on the record; archived and cancelled rows do not block a
421
+ new one. The rules live in `lib/capture.js` and are tested on their own.
422
+
423
+ ### HTTP API
424
+
425
+ The panel talks to the host over these routes. They are fenced by the host connection
426
+ fence when that service is present, and by a strict loopback check otherwise.
427
+
428
+ | Method | Path | Body / query |
429
+ |---|---|---|
430
+ | GET | `/api/global-todo/list` | `status`, `query`, `tag`, `sort`, `limit` — the reply carries `matched` and `truncated` when the page was cut |
431
+ | 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) |
432
+ | GET | `/api/global-todo/get` | `id` |
433
+ | GET | `/api/global-todo/export` | — (the store's own JSON document; `?format=md` gives a Markdown task list) |
434
+ | POST | `/api/global-todo/import` | `{ doc, mode?: replace\|merge, dryRun? }` — `replace` is an exact round trip, `merge` updates by id and appends (what the panel sends); `dryRun` returns `wouldCreate` / `wouldUpdate` and writes nothing |
435
+ | POST | `/api/global-todo/reminders/ack` | `{ ids }` — stop listing these passed deadlines as due |
436
+ | POST | `/api/global-todo/note` | `{ id, text }` — append a progress note |
437
+ | POST | `/api/global-todo/capture` | `{ sessionId? }` — ask the current conversation to collect its follow-ups |
438
+ | 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 |
439
+ | POST | `/api/global-todo/ingest` | `{ items: [{ title, source: { kind }, confidence }], dryRun? }` with `Authorization: Bearer <settings.ingestToken>` — external items; below `ingestConfirmBelow` they wait in the inbox |
440
+ | 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; `version` is the running module and `packageVersion`/`stale` say whether the installed package is newer than the running one; `review.items` names the finished todos that still wait for a person, so a check does not have to drive the panel) |
441
+ | POST | `/api/global-todo/create` | `{ title, description?, links?, steps?, tags?, priority?, dueAt? }` |
442
+ | POST | `/api/global-todo/update` | `{ id, ...fields, by?: human\|agent }` |
443
+ | POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
444
+ | POST | `/api/global-todo/remove` | `{ id }` |
445
+ | POST | `/api/global-todo/restore` | `{ todo }` — put a removed record back verbatim (undo) |
446
+ | POST | `/api/global-todo/review` | `{ id, decision: confirm\|reject }` — answer an agent's completion |
447
+ | GET | `/api/global-todo/report?days=7` | what the agents finished in the window, and the files it touched |
448
+ | GET | `/api/global-todo/session?id=` | what a bound conversation last said (`tail`, `alive`, `dir`) |
449
+ | POST | `/api/global-todo/relink` | `{ id, index, value }` — re-point a dead attachment (the value must still be a valid absolute path or URL of that kind) |
450
+ | GET | `/api/global-todo/asset?id=&index=` | the bytes of an **image** attachment (path read from the store, never from the request; ≤8 MB, png/jpg/jpeg/webp/gif/avif) |
451
+ | POST | `/api/global-todo/link` | `{ id, add?: [...], remove?: [value \| index] }` |
452
+ | POST | `/api/global-todo/dispatch` | `{ id, mode?: auto\|bound\|new\|current, sessionId?, dir? }` |
453
+ | 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 |
454
+ | POST | `/api/global-todo/settings` | `{ autoCollect?, idleMinutes?, cooldownMinutes?, exemptWorkspaces?, captureMinWeight? }` — the Host-side switches behind automatic collect; `captureMinWeight` (0–1, default 0.35) is the floor below which a batch-capture candidate is reported as skipped instead of registered |
455
+ | POST | `/api/global-todo/diagnostics` | `{ scope, status?, reason?, message?, url? }` — the panel's own error report |
456
+
457
+ `by` on `/update` and `/status` says whose write this is: omitted (what the panel sends) it is
458
+ the person at the keyboard, and a script acting for the agent says `by: 'agent'` rather than
459
+ being filed as a human. Anything else is a `400` — a provenance field that silently absorbs a
460
+ typo is worse than no field at all.
461
+
462
+ Errors are typed: `400` for a rejected value, `404` for an unknown id, `413` for an
463
+ oversized body, `403` for a request that fails the fence, `502` when a hand-off cannot
464
+ reach the agents service. The body is always `{ ok, ... }` with a machine `reason`.
465
+
466
+ ## Where it is listed
467
+
468
+ npm is the only install source; every registry below is a discovery surface. Verified 2026-10-21 —
469
+ re-check with `node tools/listings.mjs` rather than trusting this list.
470
+
471
+ | Registry | Form | State |
472
+ |---|---|---|
473
+ | [npm](https://www.npmjs.com/package/dsh-global-todo) | the package | `0.11.2` |
474
+ | [1024 Store](https://deepseek1024.com) | catalog entry ([PR #654](https://github.com/imsai-sh/awesome-deepseek-harness-plugins/pull/654), merged) | listed; its install line still resolves to the GitHub source |
475
+ | [dsh.pub](https://dsh.pub/en/plugins/dsh-global-todo/) | published page + `dshpub` CLI | live, pinned to a commit |
476
+ | [DSH-Plugins-Marketplace](https://github.com/bradeGithub/DSH-Plugins-Marketplace) | `registry.json`, collected from the `dsh-plugin` topic | `v0.11.2`, risk tier `safe` |
477
+ | [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) (19.7k★) | one YAML per plugin | merged ([PR #7498](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/7498)) |
478
+ | [walkinglabs](https://github.com/walkinglabs/awesome-deepseek-harness-plugins) | source-verified README | merged ([PR #114](https://github.com/walkinglabs/awesome-deepseek-harness-plugins/pull/114)) |
479
+
480
+ Five more are in review (Zhiyuan-Fan, kejixiaoliang, beancookie, dshworks, losebird) and one
481
+ maintainer promised to include it (dshopen.org). The full table, including the auto-collected
482
+ topic feeds we have not verified, is [docs/DISTRIBUTION.md](docs/DISTRIBUTION.md).
483
+
484
+ ## Where the data lives
485
+
486
+ `$DSH_HOME/global-todo/todos.json`, or any absolute path you set:
487
+
488
+ ```yaml
489
+ # the profile's cordis.patch.yml
490
+ - id: global-todo
491
+ config:
492
+ storePath: /home/me/notes/global-todos.json # a synced or versioned folder
493
+ dispatchDir: /home/me/projects/thing # default cwd for a new session
494
+ requireExistingFiles: true # refuse a document that is not there yet
495
+ toolGroups: all # all (10 tools) | core (6: the list itself) | none
496
+ ```
497
+
498
+ The file is the artifact: plain JSON, one record per todo, written through a temp file and
499
+ `rename()`, with a write lock that serializes the human's and the agent's edits. A damaged
500
+ file is reported (`store-corrupt`) rather than silently reset, and a single mangled record
501
+ is skipped instead of taking the list down. Back it up by copying it.
502
+
503
+ ## Design decisions
504
+
505
+ **Attachments are references, not uploads.** A document is an absolute path that stays
506
+ where it is; an address is a URL. Nothing is copied into plugin storage, so a document
507
+ stays editable and diffable, the list stays small, and a dispatched agent opens the very
508
+ same path with its own tools. URLs carrying credentials are refused, because the list, the
509
+ audit trail and every prompt would otherwise keep a copy of the secret.
510
+
511
+ **Creation time is first-class.** `createdAt` is set once and never rewritten. The panel
512
+ shows it, the tools return it as both epoch and ISO, and the dispatch prompt includes it,
513
+ because "when did this land on my plate" is half of what a global todo is for.
514
+
515
+ **One write path.** The panel and the tools both call the same service functions, so a
516
+ validation rule, an audit entry or the write lock cannot exist on one side only.
517
+
518
+ **A failed hand-off is louder than a successful one.** Success and failure are both
519
+ recorded on the todo, and the panel shows the failure with its reason. A dispatch that
520
+ silently did nothing is worse than one that never happened.
521
+
522
+ **Optional services degrade, they do not crash.** `webServer`, `tools`, `agents`,
523
+ `agentPresets`, `workspaceRegistry` and `agentDefaultModel` are all reached through
524
+ `ctx.inject` or a guarded `ctx.get`. A lean composition loses exactly that surface.
525
+
526
+ See [docs/DESIGN.md](docs/DESIGN.md) for the data model and the rejected alternatives,
527
+ and [docs/ROADMAP.md](docs/ROADMAP.md) for what could come next.
528
+
529
+ ## Development
530
+
531
+ ```sh
532
+ npm run check # parse every file, assert the shipping shape, and — when a DSH
533
+ # profile is present — validate every tool schema against the
534
+ # Host's own validator
535
+ npm test # unit, contract and bundle tests, zero dependencies
536
+ npm run verify # static checks + tests
537
+ npm run check:contracts # the cross-field matrix a reviewer re-tests by hand:
538
+ # one invalid value, one answer, whichever field it lands on
539
+ npm run check:windows # the same idea on a real filesystem (Windows in CI)
540
+ npm run check:matrix # the running dsh against engines.dsh, then the manifest,
541
+ # bundle and tool-schema contracts (no preview writes)
542
+ ```
543
+
544
+ `engines.dsh` is the one version fact that matters: a Host outside it skips the whole bundle
545
+ instead of warning, so `check:matrix` is the smoke to run before widening the range.
546
+
547
+ There is **no build step**. The host half is plain ESM under `lib/`; the client half is a
548
+ classic script that registers itself with `window.__ModuleLoader__`, so no bundler,
549
+ TypeScript toolchain or CI artifact is involved. `lib/client.js` edits take effect on a
550
+ page refresh; host-half edits need a host restart.
551
+
552
+ ```sh
553
+ npm run preview # render the panel to preview/panel.html (+ detail and selection variants)
554
+ npm run screenshots # re-photograph those previews into docs/screenshots/, 1280×720
555
+ npm run tour # rebuild docs/screenshots/tour.gif from those PNGs (needs ffmpeg)
556
+ ```
557
+
558
+ The PNGs under `docs/screenshots/` ship with the package and are embedded in both
559
+ READMEs, so re-run `npm run screenshots` whenever the panel UI changes.
560
+
561
+ ```text
562
+ lib/
563
+ index.js host entry: store + service + dispatcher + the optional surfaces
564
+ store.js one JSON file: atomic writes, serialized updates, tolerant loads
565
+ model.js the todo entity: validation, patches, status, attachments, steps, review, notes
566
+ links.js attachment rules (pure: no filesystem access)
567
+ service.js the single write path, filters, sorting and the dispatch prompt
568
+ routes.js the HTTP surface for the panel
569
+ tools.js the ten agent tools
570
+ dispatch.js handing a todo to an agent session (auto / bound / new / current)
571
+ completion.js the clean-turn watcher behind automatic completion
572
+ capture.js the capture duplicate gate (title, paraphrase, recent done)
573
+ ingest.js the external door and its inbox rules
574
+ markdown.js Markdown export and tolerant import
575
+ report.js the weekly aggregate over completion records
576
+ reminders.js the due rule behind the reminder strip
577
+ settings.js host-side switches and the automatic-collect policy
578
+ metrics.js in-memory counters reported by /health
579
+ diagnostics.js the bounded client error log
580
+ errors.js typed failures with stable machine codes
581
+ client.js the browser half (panel + sidebar entry)
582
+ test/ unit, service, tool, route, dispatch, bundle and manifest tests
583
+ tools/ preview.mjs — render the panel to static HTML without a Host
584
+ screenshot.mjs — photograph those previews (borrows the profile's Playwright)
585
+ tour.mjs — assemble the README tour GIF from those PNGs
586
+ docs/ DESIGN.md (data model and rejected alternatives), ROADMAP.md
587
+ ```
588
+
589
+ Contributions are welcome — read [CONTRIBUTING.md](CONTRIBUTING.md) first.
590
+
591
+ ## Feedback
592
+
593
+ Questions, bug reports and ideas go to [Discussions](https://github.com/LUgABeEEE/dsh-global-todo/discussions).
594
+ When reporting a bug, include the DSH version, the plugin version from the panel footer, your OS and
595
+ the steps that reproduce it; a screenshot of the notice is worth more than a paragraph. Security
596
+ issues go through [SECURITY.md](SECURITY.md) instead, not the discussion board.
597
+
598
+ ## Security
599
+
600
+ The HTTP surface is local-only, the list is a local file, and no credential is ever stored.
601
+ Please report vulnerabilities as described in [SECURITY.md](SECURITY.md).
602
+
603
+ ## License
604
+
605
+ [MIT](LICENSE) © 2026 LUgABeEEE
606
+
607
+ A community plugin, not a DeepSeek project: the `dsh` prefix marks the ecosystem it plugs into, not
608
+ an endorsement. "DeepSeek" and "DeepSeek Harness" remain their owners' marks.