dsh-global-todo 0.0.0-stage → 0.3.2

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 ADDED
@@ -0,0 +1,491 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.2] — 2026-10-19
11
+
12
+ No code change. `0.3.1` was published to npm by hand, so it carries no provenance attestation;
13
+ this release republishes the same source tree through the repository's `publish.yml` workflow,
14
+ which authenticates with OIDC (Trusted Publishing) instead of a token. From here on a release is
15
+ a tag: npm signs the attestation that ties the tarball to this commit and this workflow.
16
+
17
+ ## [0.3.1] — 2026-10-17
18
+
19
+ A compatibility release rather than a feature one: DeepSeek Harness moved to `0.2.1-rc.2`, and
20
+ the plugin needed no code change to follow it — but three of its own notes described the old
21
+ Host, and a note that is out of date is what the next change gets planned around.
22
+
23
+ ### Changed
24
+
25
+ - **Verified against DSH `0.2.1-rc.2`, and the picker notes now say what is true.** Nothing in
26
+ the plugin had to change: the tool schemas still pass the Host's own validator, every service
27
+ it injects is still there, and the client half touches only `slots` / `locale` / `effect` /
28
+ `uiWorkspace`, whose contract did not move. What moved is the Host — 0.2.1 mounts its own
29
+ directory picker, so the panel's `选择` is now the shell's native dialog and the built-in
30
+ browser is a fallback rather than the normal case. Three comments and one notice said
31
+ otherwise and have been corrected: a note that still claims a capability is missing is worse
32
+ than no note, because the next reader plans around it.
33
+
34
+ ## [0.3.0] — 2026-10-17
35
+
36
+ The round after 0.2.0: `/health` grew counters and a JSON export/import round trip, an agent
37
+ gained an honest way to reopen a todo it had finished by mistake, and automatic collect learned
38
+ both when to shut up — one question per message a person wrote — and where never to speak at all,
39
+ a workspace somebody exempted. The quiet auto-close from the middle of that round was removed
40
+ again the same night it shipped; the reason is written down in `DESIGN.md`.
41
+
42
+ ### Added
43
+
44
+ - **`/health` now reports what the plugin has been doing.** In-process counters for the list
45
+ (`created`, `removed`, `restored`, `imports`, `noted`, `changes`, `size`, `peak`) and for
46
+ hand-offs (`attempts`, `ok`, `failed`, `byMode`, `successRate`), plus a per-tool call count that
47
+ includes failures. The existing fields are untouched; `metrics` is `null` when the plugin is
48
+ mounted without a store. Deliberately **in-memory**: it describes this process, and a counter
49
+ that survives a restart is a second source of truth for facts the store already owns.
50
+ - **The list can be exported and imported as JSON.** `GET /export` returns the store's own
51
+ document — not the read view, so the runtime annotations (`missing`, `sessionDir`) are absent —
52
+ and `POST /import` accepts it back with `replace` or `merge` semantics (`merge` overwrites by id
53
+ and appends new ones). A single unreadable record fails the whole request with a 400 rather than
54
+ writing half of it. The panel gained 导出 / 导入 buttons; the import path defaults to `merge` so
55
+ choosing the wrong file cannot empty the list.
56
+ - The tool count in the docs now matches the tree: **ten** `global_todo_*` tools
57
+ (`add`, `capture`, `list`, `get`, `update`, `link`, `note`, `done`, `reopen`, `remove`), the
58
+ same list `/health.tools.registered` reports. `ROADMAP.md` was re-audited against the code and
59
+ now separates what has landed (with `file:line` citations) from what has not.
60
+ - **`global_todo_reopen` gives a wrongly-finished todo an honest way back.** It does exactly
61
+ one thing — `done` / `cancelled` → `open` — records the act as the agent's (with an optional
62
+ `reason` in the activity trail) and **refuses** an item that was never finished, instead of
63
+ reporting a repair that did not happen. Until now the only agent-side path was `status` on
64
+ `global_todo_done`, i.e. a capability filed under the tool whose name says the opposite, so a
65
+ data repair reached for the HTTP route instead — and was filed as the person's work.
66
+ - **A workspace can be exempted from automatic collect.** The `Exempt` action on a conversation's
67
+ group header takes that conversation's whole **working directory** out of scope, and
68
+ `/health.settings` reports the list so the panel can show which places are out. It is keyed on
69
+ the directory rather than on a session id because the conversations that need it are the ones a
70
+ machine drives — a chat companion's room — and such a session is recreated with a new id when it
71
+ restarts, so an id-keyed exemption would quietly stop applying. Subdirectories count; a shared
72
+ prefix does not, and a conversation whose directory nobody reported is not treated as exempt.
73
+ The directory is remembered from the conversation's own events rather than looked up when the
74
+ timer runs, because these conversations are idle most of the time and an idle conversation is
75
+ one the Host may have unloaded — where a live lookup answers `''`, and the exemption would stop
76
+ applying to exactly the conversations it exists for.
77
+
78
+ ### Fixed
79
+
80
+ - **The panel's `/update` and `/status` routes no longer need a lie to accept a write.** Both
81
+ hardcoded `by: 'human'`, so a script repairing data on an agent's behalf filed the change as
82
+ the person's own — the four reopens from the mis-close repair are recorded that way, and the
83
+ only thing saying otherwise was a sentence in a note. Both routes now take an explicit
84
+ `by: 'agent'` and still default to `human`; any other value is a `400`, because quietly
85
+ folding a typo into `human` is the same lie with better manners.
86
+
87
+ - **Automatic collect no longer asks the same conversation over and over.** The question the plugin
88
+ sends *is* a message in the conversation, so it restarted the plugin's own quiet clock, and the
89
+ answer to it ended a clean turn — which put the same conversation straight back in the queue. With
90
+ `cooldownMinutes` on the same half hour as `idleMinutes`, the cooldown had always expired by the
91
+ time the quiet window opened, so one conversation was asked every half hour, all night, with
92
+ nobody there. A conversation is now asked **once per message a person wrote in it**: only
93
+ `source.kind === 'user'` re-arms it, and the plugin's own traffic never does. A restarted Host has
94
+ forgotten the timestamps and so stays quiet, which is the direction to fail in.
95
+
96
+ - **A quiet conversation no longer has its open todos closed for it.** Yesterday's helper read
97
+ "this conversation has gone quiet" as "its work is finished" and closed every open todo that
98
+ conversation had written, under a template summary. On its first night it closed eight that were
99
+ not finished — one waiting on a person, one waiting on an event that had not happened yet — and
100
+ the only thing that told them apart was that summary, because there was no evidence behind it.
101
+ The signal is not weak, it points the wrong way: a conversation falls quiet *because* it is
102
+ waiting, so the items that matter most are the ones it gets wrong. Only paths that carry evidence
103
+ close a todo now — the agent's own `global_todo_done`, the hand-off watcher, or a person in the
104
+ panel — and the module that held the heuristic is gone.
105
+
106
+ ## [0.2.0] — 2026-10-15
107
+
108
+ The first published release. 0.1.0 was cut in the repository but never reached a
109
+ registry, so this is the version to install.
110
+
111
+ ### Fixed
112
+
113
+ - **A settings write that landed during startup was silently undone.** The Host reads
114
+ `settings.json` without awaiting the read — it cannot block its own mount on a file — and that
115
+ read replaces the whole settings state. A panel toggle, or a cooldown booking from an
116
+ automatic ask, that arrived while the read was still in flight was therefore overwritten by
117
+ the file that read had just loaded. Writes now wait for the startup read to finish.
118
+ - **The diagnostics log is trimmed without ever being seen empty.** The bound was enforced by
119
+ rewriting `client-errors.log` in place, and `writeFile` truncates its target before it writes,
120
+ so a reader that landed in that window got 0 bytes — the CI reader that expected the newest
121
+ report got `''`. The trim now writes a sibling file and renames it over the log, which is
122
+ atomic, and a failed trim leaves no litter beside the log.
123
+
124
+ ### Fixed
125
+
126
+ - **The hand-off picker now mirrors the shell's own visibility rule.** It was offering
127
+ conversations the sidebar deliberately hides: **subagent children** (`origin === 'subagent'`),
128
+ **blank sessions** that are not the one in front of the user, and archived sessions whose
129
+ archived set came from a source the picker read too narrowly. The rule is copied from
130
+ `sessionVisible` in the shipped workspace package — the only authority on it — including its one
131
+ exception: a blank session *is* offered while it is the current one. The picker also reports the
132
+ shape of the session snapshot it sees to `/health.clientErrors` once per mount, because a preview
133
+ fixture written by the same hand as the filter can only ever confirm itself.
134
+
135
+ - **A long working directory no longer breaks its own label.** `工作目录:` is a flex child of the
136
+ directory pill, so when the row overflowed it shrank and wrapped mid-word — and the third
137
+ button (`浏览`) was enough to push it over. Only the path yields now: it already ellipsises,
138
+ while the label, the hint and the buttons keep their size.
139
+
140
+ ### Added
141
+
142
+ - **A folder browser, because the shell's picker is not always mounted.** `uiWorkspace.pickDirectory()`
143
+ delegates to a directory-picking Remote namespace that this deployment does not have, so it
144
+ throws instead of opening anything (the panel had been reduced to asking you to type a path).
145
+ The Host now serves `GET /api/global-todo/dirs` — visible sub-folders, sorted, capped, with the
146
+ parent — and `选择` walks it as a real picker (`浏览`: current path, `上一级`, folder buttons,
147
+ `用这个目录`). The path field stays for pasting, and the failure is still reported to
148
+ `/health.clientErrors`.
149
+
150
+ ### Changed
151
+
152
+ - **A quiet conversation has its finished work ticked off for it.** Asking an agent to close what
153
+ it finished works most of the time, which is not the same as a tidy list — so when a conversation
154
+ that *wrote* todos has been quiet for the idle window after a clean turn, the Host closes the ones
155
+ it owns, with a summary saying why ("插件按「做完就划掉」结束了它写下的这条"), and it is reversible.
156
+ It touches only that conversation's own todos, and it is deliberately independent of the
157
+ automatic-collect switch: tidying is not the same feature as asking for more.
158
+ - **`/health` reports `idleMinutes`** (and the settings write echoes it). The rule in force has to
159
+ be observable — a check that could not see it reported "not deployed" for code that was running.
160
+
161
+
162
+ - **Automatic collect now waits for quiet, instead of asking at every turn end.** That was the
163
+ design mistake behind "too frequent": a session being worked in ends turns every few minutes, so
164
+ the question arrived mid-work, repeatedly. A clean turn now only *marks* the conversation as a
165
+ candidate and records **when it went quiet**; a one-minute timer asks when nothing has been said
166
+ for `idleMinutes` (default **30**) — and any new message, or the start of a new turn, restarts
167
+ that clock. Asking is also still gated on the conversation being the one the person is in, the
168
+ turn having ended cleanly, and the per-session cooldown. Not knowing how long it has been quiet
169
+ counts as "not yet" (fail closed).
170
+ - **Captured items are refined and merged rather than transcribed.** The capture prompt now asks
171
+ for one entry per matter, merges scattered points into a single entry's description, targets
172
+ "1–3 entries, not 8", and refuses to record what the list already has. The capture tool enforces
173
+ the last part: an item whose normalised title matches an unfinished entry is skipped with
174
+ 「清单里已有同名的未完成条目」 instead of being registered again — which is how the list kept
175
+ filling with near-duplicates.
176
+
177
+
178
+ - **A calmer panel.** The header had grown two rows of controls that said the same things
179
+ twice: the three count chips *were* the status filter, and the add form sat open above the
180
+ list forever. Now there are three rows with one job each — **title + summary + `添加`**,
181
+ **filters · grouping · search** in a single row, then a quiet **context line** (working
182
+ directory, hand-off target, `选择`, `自动获取`). Counts moved into the summary line as plain
183
+ text, every "pick one of these" control uses one segmented style (chips are now only facts
184
+ about a todo: tags, levels, notes), and the add form opens from `添加` instead of occupying
185
+ the top of the list.
186
+
187
+ - **The test suite waits for results, not for the clock.** The remaining fixed sleeps
188
+ (`settle()`, `setTimeout`) are gone: the auto-collect, restart and dispatch tests now poll for
189
+ the state their assertions are about, with a bounded timeout that names what it waited for. A
190
+ sleep is a test that reports on the machine, and one of them was already losing that race on
191
+ the slower CI runner.
192
+ - **CI runs `actions/checkout@v7` and `actions/setup-node@v7`**, which are already on Node 24,
193
+ so the Node 20 deprecation warnings are gone. Automatic npm caching is off explicitly: this
194
+ plugin has no dependencies and no lockfile to cache.
195
+
196
+ - **The repository is publishable as it stands.** Two things stood between this tree and a
197
+ public one. The machine-path guard in `scripts/check.mjs` only looked at the shipped halves,
198
+ so a preview fixture, a test constant and a screenshot caption each carried the author's real
199
+ home directory while the guard still reported `OK`; it now covers every committable file and
200
+ allows exactly one placeholder, `/home/me/…`. And `publish.yml` asked Node 22.19 for npm
201
+ 10.9.3, which cannot do OIDC at all — trusted publishing needs npm 11.5.1 or later — so the
202
+ job installs that floor before it publishes. The internal research reports left `docs/`, and
203
+ the fixtures and screenshots were regenerated from the same neutral data.
204
+
205
+ ## [0.1.0] — 2026-10-14
206
+
207
+ The first release: one JSON list a person and an agent both write to, eight agent tools, a
208
+ panel that hands work over and records how it finished.
209
+
210
+ ### Fixed
211
+
212
+ - **「选择」工作目录不再以一条无解的错误结束.** `uiWorkspace.pickDirectory()` exists, but it
213
+ delegates to a directory-picking **Remote namespace** that is not mounted in every
214
+ deployment, where calling it throws a bare
215
+ `Cannot read properties of undefined (reading 'directoryPicker')`. The panel now falls back
216
+ to a **path field** (`手动填` → paste an absolute path → `使用`), says plainly why, and reports
217
+ the throw to the Host diagnostics so it is visible in `/health.clientErrors` instead of only
218
+ in someone's browser.
219
+ - **Both capture actions and the automatic-collect switch reported an error even though they
220
+ worked.** The handlers ended with `void refresh()` — a function that does not exist — so a
221
+ successful request was immediately followed by a `ReferenceError` that the handler's own catch
222
+ turned into an error banner. The write had already landed, which is also why the switch appeared
223
+ impossible to turn off: the panel's copy of the Host settings was only read at mount, so the chip
224
+ never moved and the next click sent the same value again. Health is now refreshed on the poll and
225
+ updated optimistically on write, and `void refresh()` is gone.
226
+ - **A guard against that class of bug**: `tools/scan-client-calls.mjs` scans the shipped client for
227
+ calls to functions nobody defined (comments and strings removed first; destructuring, parameters
228
+ and method shorthand recognised), wired into `npm run check` and covered by its own test. Every
229
+ other check passes such a mistake, and the preview harness cannot see it — an error caught inside
230
+ a handler only appears on a real re-render.
231
+ - **The toolbar's own "collect from this chat" button is gone.** The action lives on each
232
+ conversation's group header, where its subject is unambiguous; a second copy in the toolbar only
233
+ gave the wrong one to click.
234
+ - **The Host's own tooling is no longer locked out by the connection fence.** Deferring to the
235
+ fence fixed LAN access but made *every* credential-less request a 401, because the fence
236
+ authenticates browsers — so `curl`, scripts on the Host and other agents lost access to a list
237
+ they are the main writers of. The fence now judges **browsers** (anything carrying a cookie or
238
+ an `Origin`), while a local, credential-less caller is judged by the loopback rule — the guard
239
+ that existed before the fence did. A fence that throws still fails closed, and a cross-site
240
+ request is never rescued by the carve-out. `/health` reports `fence.localScriptsAdmitted`.
241
+
242
+ - **The panel works from the LAN and from a tunnel again.** The Host's connection fence was read
243
+ once at mount time, when it can still be absent, so every request fell through to the plugin's
244
+ own loopback check. That check is stricter than the Host's trusted-host list: a browser anywhere
245
+ but the Host machine got `403 只接受本机请求` on **every** call — list, picker and both capture
246
+ actions — while `curl` from the Host looked perfectly healthy. The fence is now resolved per
247
+ request, its verdict is honoured verbatim, and `/health` reports which fence is actually in
248
+ force so a silent downgrade cannot hide again.
249
+
250
+ ### Added
251
+
252
+ - **`全选` in selection mode.** Tick everything the panel is currently showing (collapsed groups
253
+ included — their todos are still what the user means), and the same control becomes
254
+ `取消全选` once everything is selected.
255
+
256
+ - **Batch assignment (「一键分配」)**. Turn on 「选择」 in the toolbar, tick a few todos, and one
257
+ click sends each of them to the best target available for it — **the conversation that wrote
258
+ it**, then a conversation in the **same workspace** (a candidate the panel works out from its
259
+ own recency and archive data, which the Host cannot see), then a **target you choose** for the
260
+ rest. Those are an attempt order, not a preference: a target that cannot be loaded is the
261
+ reason the next one exists. Whatever still has nowhere to go is reported back and offered a
262
+ picker instead of being sent somewhere nobody chose, and one failure does not abort the batch.
263
+ - **An automatic collect switch** (panel chip `自动获取`, `POST /api/global-todo/settings`). When
264
+ it is on, a conversation you are in that finishes a clean turn and has not been asked for a
265
+ while is asked once whether anything needs following up. It is off until a person turns it on,
266
+ the switch lives on the Host (so it works with the panel closed and is reported by `/health`),
267
+ and every reason *not* to ask is explicit: an aborted/failed turn, a conversation nobody is in,
268
+ a session asked inside the cooldown, or the feature being off. The cooldown is booked before
269
+ the question is asked, so a repeated event or a failed request cannot turn into a loop.
270
+ - **Stronger proactive guidance in the system prompt.** The section now lists the concrete
271
+ triggers for recording something on one's own initiative (a cross-conversation follow-up, "I'll
272
+ do X then Y", a leftover step at the end of a turn), says plainly that asking the user whether
273
+ to record it is wrong, and points at `global_todo_note` for reporting a blocker instead of
274
+ rewriting the description.
275
+
276
+ ### Changed
277
+
278
+ - **"Collect from this chat" lives on the conversation it collects from.** A global toolbar
279
+ button could not say *which* conversation it meant, so the action now sits on each
280
+ conversation's group header in the list (`抓取`), and asking a group to collect files the
281
+ results under that conversation. The toolbar keeps a fallback, shown *only* when the
282
+ conversation in front of the user has no group yet — otherwise there would be no way to
283
+ collect from it.
284
+ - **Collecting from a conversation that is not running now resumes it** (the same rule a
285
+ hand-off follows). It used to look the session up in the live registry and die on
286
+ `undefined` for any conversation that was not currently loaded.
287
+
288
+ ### Added
289
+
290
+ - **The hand-off picker shows the conversations you actually use, newest first.** Its order is
291
+ the sidebar's own recency key (`updatedAt`), so the conversation you were last in is at the
292
+ top and stale ones sink, and **archived conversations are left out entirely** — an archived
293
+ conversation is not an answer to "where should this work go". Conversations that share a
294
+ title get a short id suffix so two options can be told apart, and if the current target is no
295
+ longer listed (archived since it was chosen) it is shown as `(已归档)` rather than silently
296
+ displaying a different conversation as selected.
297
+ - **Progress notes** (`global_todo_note`, `POST /api/global-todo/note`): an append-only trail of
298
+ who said what and when, capped per todo. An agent reporting "here is where I am stuck" no
299
+ longer has to rewrite the user's description, and — like the other verdict tools — writing a
300
+ note makes automatic completion stand down rather than contradict a stated blocker. The row
301
+ shows the count, the detail shows the trail and a box to add one.
302
+ - **Dead attachments are flagged where you see them.** File attachments that no longer exist
303
+ are marked red on the row and labelled in the detail. The flag is computed on read and never
304
+ stored, so a file that comes back stops being flagged and a stale flag cannot outlive the
305
+ problem.
306
+ - **Search syntax**: `tag:发布` / `#运维`, `is:overdue|active|due|open|doing|done`, `has:link`,
307
+ `has:note`, `level:p0|p1|p2|p3`, plus free words, which are combined with AND and also match
308
+ note text. A plain search still behaves exactly as before.
309
+ - **A new conversation is placed where the work lives.** A hand-off that opens a session now
310
+ joins the workspace owning the conversation that wrote the todo (or the one it was handed
311
+ from), and starts in that workspace's directory — so the agent and the sidebar agree
312
+ instead of the session landing beside unrelated ones. The panel also lists the workspaces,
313
+ so a new conversation can be aimed at one explicitly (`+ new conversation · <workspace>`);
314
+ a named workspace outranks the inherited one. Only when neither is known does the working
315
+ directory decide.
316
+ - **A pre-commit credential audit inside `npm run check`**: the rule the project used to
317
+ apply only to shipped files now runs over every file a commit would carry (51 of them,
318
+ with the same exclusions `.gitignore` applies), and it asserts that the local artifacts
319
+ (`node_modules/`, `preview/`, `todos.json`, backups) cannot reach a commit at all. A
320
+ credential leaked in a test or a doc is exactly as leaked as one in `lib/`.
321
+ - **`npm run check:live`** (`tools/live-check.mjs`): verifies the wired Host rather than
322
+ doubles — health surfaces, a delete → undo round trip compared field by field, and the
323
+ `sessionAlive`/`sessionDir` a row needs to show whether its conversation is running. It
324
+ cleans up its temporary record even when it fails, and stays out of `verify` because it
325
+ needs a reachable Host.
326
+ - **Levels P0–P3.** `urgent` joins the existing three levels (`P0 紧急 / P1 高 / P2 常规 /
327
+ P3 低`), so no migration is needed: `high`/`normal`/`low` stay valid and `normal` stays the
328
+ default. The badge marks P0 in the error colour to separate it from P1.
329
+ - **`POST /api/global-todo/capture` and the panel's 「从本对话抓取」 button**: the panel
330
+ injects a capture request into the conversation the user is in, which then records its
331
+ follow-ups with `global_todo_capture`. It is a request for work, not a hand-off: nothing is
332
+ recorded against a todo and no completion is expected.
333
+ - **Grouping dimensions.** The panel groups by `conversation` (default), `time` or `level`.
334
+ Conversation mode merges everything one agent conversation submitted into a single entry
335
+ that carries the count, collapsing it until asked — a list that several 「抓一下待办」 runs
336
+ filled up stops reading as a wall of rows. Items with no source conversation gather under
337
+ 「手动添加」, and the row level (high / normal / low) is visible as a badge and as the
338
+ highest level of a conversation group.
339
+ - **A render check** (`npm run check:render`, part of `verify`): the client half has no unit
340
+ seam, so the generated panel HTML is asserted instead — merged conversations stay one
341
+ entry, single-item ones keep their row, levels and all three grouping buttons render, and
342
+ no literal colour leaks into the markup.
343
+ - **Automatic completion.** A handed-off session that closes its turn cleanly finishes its
344
+ todo by itself: the closing assistant message becomes `completion.summary` and the files
345
+ that turn changed become `completion.artifacts`. Two guards keep it honest — only a
346
+ session this plugin just dispatched to is eligible, and `error` / `aborted` /
347
+ `interrupted` turns leave the todo open. `lib/completion.js` watches `session/event`
348
+ (`assistant/message`, `workspace/changes`, `turn/end`) and resolves file lists through
349
+ the `workspaceChanges` recorder when an event does not carry them.
350
+ - `global_todo_done` accepts `summary` and `artifacts`, so an agent can write the record
351
+ itself; the completion prompt asks for them, and "don't mark it done if you couldn't" is
352
+ stated in the hand-off message.
353
+ - The panel renders the completion: 完成于 on the row, artifact chips next to the
354
+ attachments, and a completion section in the detail.
355
+ - `global_todo_capture`: record a batch of todos in one call, with per-entry validation so
356
+ one bad attachment does not lose the rest.
357
+ - Proactive-capture guidance in the system prompt: when the agent should record something
358
+ on its own initiative, when it must not, and 「抓一下待办」 as the phrase that asks it to
359
+ scan the conversation and capture a batch. The boundary is stated in the prompt too:
360
+ record, never start work.
361
+ - `POST /api/global-todo/restore` and the panel's undo: a deleted todo comes back
362
+ verbatim — same id, creation time, attachments and history — and a record whose id
363
+ is taken is refused rather than merged.
364
+ - `sessionAlive` on every listed todo, derived from the live agent registry on read, so
365
+ the panel can say 「会话在跑」 instead of 「已绑定会话」.
366
+ - `npm run preview`: renders the panel (and an opened detail view) to static HTML with
367
+ no Host and no dependencies, for UI work and review.
368
+
369
+ ### Fixed
370
+
371
+ - **The receipt's jump button did nothing.** Client services arrive through
372
+ `ctx.inject([...])`, not `ctx.get(...)`: the directory marks `uiWorkspace` as a hard
373
+ dependency for exactly that reason. It is now obtained by injection, and the button is
374
+ only rendered when `openSession` really exists — a button that does nothing is worse than
375
+ no button.
376
+ - **A hand-off still landed in the wrong conversation.** Inference about "the conversation
377
+ you are in" was wrong twice, so it is no longer the mechanism: the toolbar now lists the
378
+ conversations and the user picks one (`交给:[ 对话名 ▾ ]`). An explicit pick is sent with
379
+ `authoritative: true`, which the Host honours verbatim; the list marks the conversation the
380
+ Host would have chosen on its own, `自动` keeps the old behaviour, and a pick is remembered.
381
+ - **A hand-off pulled the next hand-off with it.** The "conversation you were last in" came
382
+ from `api-session/activity`, which reports *any* user-authored durable message — and a
383
+ hand-off is one: the plugin injects a user-role message into its target. So every dispatch
384
+ moved "the current conversation" to the conversation it had just dispatched to, and the
385
+ next click followed the previous click instead of the person. The tracker now reads the
386
+ session log and keeps only `user/message` events whose `source.kind` is `user`, so model
387
+ turns, tool results and plugin injections cannot move it. The id is also kept next to the
388
+ data (`recent-session.json`), because the tracker lives in memory and this plugin restarts
389
+ with the Host: an empty tracker falls back to the panel's stale guess, which is how a
390
+ hand-off reached the wrong conversation. Covered by two smoke tests — the message sequence,
391
+ and a restart that must not forget.
392
+ - **A hand-off could start work silently in another conversation.** The panel said nothing
393
+ after a successful dispatch, so a person staring at the todo list saw "no reaction" while
394
+ the agent in a different conversation was already working. The panel now names the target
395
+ and offers to open it, and the pre-click label uses the conversation the Host will actually
396
+ pick (`/health`'s recent-session answer) instead of the panel's own guess.
397
+ - **The panel's guess about "the conversation on screen" outranked the real one.** While the
398
+ panel is the main view, its idea of the current session is whatever the view retained
399
+ earlier — hours stale in practice, and it sent work to the wrong conversation. The activity
400
+ feed (a real user message) now wins; a caller that genuinely knows — a button rendered
401
+ inside a conversation — opts in with `authoritative: true`.
402
+ - **Auto-completion recorded an empty artifact list.** `workspace/changes` is not appended
403
+ with `turn/end`: the recorder enqueues a turn-end snapshot and diff when the turn closes
404
+ and announces the result in a later event. Completing on `turn/end` therefore always saw
405
+ no files. The record is now written when that summary arrives, with a bounded wait so a
406
+ turn that changed nothing still completes, and a new turn settles the previous wait.
407
+ - **A stated blocker was overwritten by an automatic "done".** Auto-completion now backs off
408
+ when the agent used a verdict tool of its own (`global_todo_done`, `global_todo_update`,
409
+ `global_todo_remove`) during the turn: an agent that says "this is blocked, here is why"
410
+ must not be contradicted by a heuristic. Read-only tools (`list`, `get`) do not count.
411
+ - **「交给 Agent」 did not mean "hand it to the conversation I am in".** The default was
412
+ `auto`, which reused the session an earlier click had created — so a second click sent the
413
+ work back to that side conversation instead of the chat on screen, and the first click
414
+ opened a session nobody asked for. The panel now defaults to `current`, shows the target
415
+ before the click, and the Host resolves "current" from the `api-session/activity` feed
416
+ (the conversation the user last wrote in), falling back to the conversation the todo was
417
+ written from. `auto` now means bound → current → new, and an unresolvable target fails
418
+ with a reason instead of silently creating a conversation.
419
+ - **A hand-off created sessions with no working directory.** `meta.cwd` was only set when
420
+ `dispatchDir` was configured, so the Host filed the new sessions under `_no-cwd` and the
421
+ agent inside them could do nothing. The directory is now resolved in order — what the
422
+ caller names, the directory the todo was written from, the bound conversation's
423
+ directory, then `dispatchDir` — and a hand-off with none of them is refused with an
424
+ actionable message instead of quietly creating a dead session.
425
+ - A live conversation with no working directory is flagged on its row, and `auto` no longer
426
+ reuses one: it opens a fresh session in a resolved directory instead, so a single click
427
+ recovers a todo that was handed to a dead-end conversation.
428
+
429
+ ### Added
430
+
431
+ - Every todo written through a tool records `originDir` and `originSessionId`, read from
432
+ the calling agent in `execute(args, exec)`.
433
+ - The panel's 工作目录 control: follows the conversation you are in (read from the shell's
434
+ `useSessions` state), or a directory you pick with the Host's picker — remembered across
435
+ sessions and overridable per dispatch.
436
+ - **A hot reload kept the previous stylesheet.** The panel's `<style>` tag was reused
437
+ when it already existed, so new markup ran against the old rules: class names the old
438
+ stylesheet never knew rendered with no styles at all — a panel that looked "unchanged"
439
+ while its code was already new. The tag is now replaced (and stamped with the build
440
+ revision), with a bundle test for the reload path.
441
+ - **The agent tools were missing entirely.** Their output schemas used
442
+ `type: ['string', 'null']`, which the Host's schema subset does not accept, so
443
+ registration was rejected for all seven tools. Nullability is now `oneOf`, and
444
+ `test/schema.test.mjs` plus the Host-validator step in `npm run check` keep the
445
+ class of failure out.
446
+ - The `/health` payload reports the tool registration state, which is what surfaced
447
+ the failure in a live Host.
448
+
449
+ ### Changed
450
+
451
+ - Rows now keep `▶ 交给 Agent` visible at all times (hiding the only way to hand work to an
452
+ agent behind a hover made a lone todo look inert), list the first attachments as chips
453
+ (a link opens, a document copies its path, `+N` for the rest), and separate the meta line
454
+ with `·`.
455
+ - Row cards use the raised surface and a heavier title, so the list keeps its structure in
456
+ a light theme; the row action is an outline button instead of a filled one.
457
+ - Panel redesign: urgency grouping with collapsible sections, hover-revealed row actions
458
+ behind a menu, count chips that double as filters, a search field with a `/` hint,
459
+ attachment rows with 打开 / 复制路径, an inline edit section with a dirty-state save,
460
+ and a footer that names the keyboard shortcuts.
461
+ - The word "attachment" now covers addresses and documents consistently in the UI.
462
+
463
+ ### Added
464
+
465
+ - Global todo list persisted as one JSON file (`$DSH_HOME/global-todo/todos.json`
466
+ by default), written atomically and serialized so the human and the agent can
467
+ never lose each other's change.
468
+ - Creation time on every entry: `createdAt` is set once, never edited, and shown
469
+ in the panel, in the tool output and in every dispatch prompt.
470
+ - Attachments as references — any number of documents (absolute paths) and
471
+ addresses (http/https) per todo, with URL credentials refused; documents stay
472
+ where they are instead of being copied into plugin storage.
473
+ - Seven agent tools: `global_todo_add`, `global_todo_list`, `global_todo_get`,
474
+ `global_todo_update`, `global_todo_link`, `global_todo_done`,
475
+ `global_todo_remove`.
476
+ - Web panel in the sidebar with quick add, filters, text search, a detail view
477
+ showing the activity trail, and inline attachment editing.
478
+ - One-click hand-off to an agent session: `auto` (reuse the bound conversation,
479
+ otherwise start one), `new`, `bound` and `current` modes, with the outcome —
480
+ success or the reason for failure — recorded on the todo.
481
+ - HTTP API (`/api/global-todo/*`) fenced by the host connection fence, with a
482
+ strict loopback fallback and real status codes.
483
+ - Optional-service degradation: with no `webServer`, `tools`, `agents` or
484
+ `agentPresets` service, the plugin loses exactly that surface instead of
485
+ failing to load.
486
+
487
+ [Unreleased]: https://github.com/LUgABeEEE/dsh-global-todo/compare/v0.3.1...HEAD
488
+ [0.3.1]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.3.1
489
+ [0.3.0]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.3.0
490
+ [0.2.0]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.2.0
491
+ [0.1.0]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.1.0
@@ -0,0 +1,58 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a
6
+ harassment-free experience for everyone, regardless of age, body size, visible or invisible
7
+ disability, ethnicity, sex characteristics, gender identity and expression, level of
8
+ experience, education, socio-economic status, nationality, personal appearance, race,
9
+ religion, or sexual identity and orientation.
10
+
11
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse,
12
+ inclusive, and healthy community.
13
+
14
+ ## Our standards
15
+
16
+ Examples of behavior that contributes to a positive environment:
17
+
18
+ - Demonstrating empathy and kindness toward other people
19
+ - Being respectful of differing opinions, viewpoints, and experiences
20
+ - Giving and gracefully accepting constructive feedback
21
+ - Accepting responsibility and apologizing to those affected by our mistakes, and learning
22
+ from the experience
23
+ - Focusing on what is best not just for us as individuals, but for the overall community
24
+
25
+ Examples of unacceptable behavior:
26
+
27
+ - The use of sexualized language or imagery, and sexual attention or advances of any kind
28
+ - Trolling, insulting or derogatory comments, and personal or political attacks
29
+ - Public or private harassment
30
+ - Publishing others' private information, such as a physical or email address, without
31
+ their explicit permission
32
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
33
+
34
+ ## Enforcement responsibilities
35
+
36
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable
37
+ behavior and will take appropriate and fair corrective action in response to any behavior
38
+ that they deem inappropriate, threatening, offensive, or harmful.
39
+
40
+ ## Scope
41
+
42
+ This Code of Conduct applies within all community spaces, and also applies when an
43
+ individual is officially representing the community in public spaces.
44
+
45
+ ## Enforcement
46
+
47
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the
48
+ maintainer through the contact route in [SECURITY.md](SECURITY.md). All complaints will be
49
+ reviewed and investigated promptly and fairly, and the privacy and security of the reporter
50
+ will be respected.
51
+
52
+ ## Attribution
53
+
54
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1,
55
+ available at
56
+ <https://www.contributor-covenant.org/version/2/1/code_of_conduct.html>.
57
+
58
+ [homepage]: https://www.contributor-covenant.org
@@ -0,0 +1,101 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project is small on purpose: no build step, no runtime
4
+ dependencies, and a test suite that runs in a couple of seconds. Please keep it that way.
5
+ 中文亦可,Issue 和 PR 用中文写没问题。
6
+
7
+ ## Getting set up
8
+
9
+ ```sh
10
+ git clone https://github.com/LUgABeEEE/dsh-global-todo.git
11
+ cd dsh-global-todo
12
+ npm run verify # static checks + the whole test suite
13
+ dsh plugin --profile web add "$PWD" # optional: try it in a live host
14
+ ```
15
+
16
+ There is nothing to install: the plugin has no runtime and no development dependencies.
17
+ Node 22.19 or newer is all you need.
18
+
19
+ ## The loop
20
+
21
+ 1. **Open an issue first** for anything larger than a bug fix — a new field, a new route, a
22
+ new dispatch mode. Design conversations are cheaper than review cycles.
23
+ 2. Keep the change to one concern.
24
+ 3. Add or extend tests. A behaviour change without a test will be asked for one.
25
+ 4. Run `npm run verify`.
26
+ 5. Open a PR and fill in the template.
27
+
28
+ ## What the tests are for
29
+
30
+ | File | Protects |
31
+ |---|---|
32
+ | `test/links.test.mjs` | attachment rules, including the credential refusal |
33
+ | `test/model.test.mjs` | the entity: validation, patches, activity, tolerant loads |
34
+ | `test/store.test.mjs` | atomic writes, the write lock, corrupt-file behaviour |
35
+ | `test/service.test.mjs` | filters, sorting, the shared write path |
36
+ | `test/tools.test.mjs` | tool schemas and every tool's success and failure shape |
37
+ | `test/routes.test.mjs` | HTTP status codes, the fence, body limits |
38
+ | `test/dispatch.test.mjs` | all four dispatch modes, preset mounting, recorded failures |
39
+ | `test/bundle.test.mjs` | the browser half: registration, slots, theme tokens, route contract |
40
+ | `test/manifest.test.mjs` | `package.json`, the patch row, exports, version agreement |
41
+ | `test/schema.test.mjs` | the Host's tool-schema subset — a schema outside it is rejected at registration, silently |
42
+
43
+ ## House rules
44
+
45
+ - **Plain ESM, no dependencies.** No bundler, no TypeScript, no `@deepseek-ai/*` import in
46
+ `lib/`. Raw JSON Schema for tool parameters; raw `webServer.register` for routes. This is
47
+ what keeps the plugin loading in lean compositions and across `0.2.x` releases.
48
+ - **Comments explain why, not what.** A non-obvious guard should say which failure it
49
+ prevents. Restating a line of code in prose is noise.
50
+ - **User-facing strings need both languages.** Client dictionaries live in `lib/client.js`
51
+ (`zh` and `en`); host-side messages are Chinese, matching the rest of the ecosystem.
52
+ - **Errors are typed.** Raise `TodoError` with a code from `lib/errors.js` rather than
53
+ throwing a bare `Error`; the HTTP layer maps codes to statuses and the tests assert them.
54
+ - **Never write outside your own surface.** The client half contributes through slots and
55
+ its own stylesheet tag; the host half registers routes, tools and one prompt section.
56
+ - **No secrets, ever.** The store is local and the plugin stores no credentials. The URL
57
+ rule that refuses `user:password@` exists for this reason — do not relax it.
58
+
59
+ ## Releasing
60
+
61
+ Facts checked on 2026-10-14, so nobody has to re-derive them:
62
+
63
+ - the npm name `dsh-global-todo` is **still free** (`registry.npmjs.org/dsh-global-todo` → 404);
64
+ - the repository `LUgABeEEE/dsh-global-todo` exists and holds the first commit
65
+ (`2a6f716`), so the `repository` field in `package.json` now points at something real;
66
+ CI has run green on both matrix legs (node 22.19.0 and 24.x).
67
+
68
+ The order that avoids publishing a package pointing at nothing:
69
+
70
+ 1. **`npm run verify`** — static checks plus the whole suite. This is also what CI runs.
71
+ 2. **First commit.** The repository has none yet, so there is no history to preserve:
72
+ `git init` and commit with the identity used across this machine —
73
+ `git -c user.name=LUgABeEEE -c user.email=LUgABeEEE@users.noreply.github.com commit`.
74
+ Before committing, confirm nothing secret is staged:
75
+ `git diff --cached | rg -i "apiKey|password|token|PRIVATE KEY"`.
76
+ `npm run verify` already performs the equivalent audit over every committable file, so a
77
+ green run is the evidence — and it also asserts that `.gitignore` keeps the local
78
+ artifacts (`node_modules/`, `preview/`, `todos.json`, backups) out of the commit.
79
+ 3. **Push and watch CI for real.** The workflow matrix is Node 22.19 and 24; it has only
80
+ ever been simulated locally, so the first green run is the first real evidence.
81
+ 4. **Publish with Trusted Publishing (OIDC).** `.github/workflows/publish.yml` is ready: it
82
+ verifies, then publishes with provenance when a `v*` tag is pushed or the workflow is
83
+ dispatched by hand. Before its first run, the npm side needs the binding — on the package
84
+ settings page add a trusted publisher with:
85
+ - repository `LUgABeEEE/dsh-global-todo`
86
+ - workflow file `publish.yml`
87
+ - environment: none (or one you create and also name here)
88
+
89
+ No long-lived token is involved, which is the point: nothing to leak from a laptop. Check
90
+ what would ship with `npm pack --dry-run`.
91
+ 5. **Cut the version.** Move CHANGELOG's `Unreleased` block under
92
+ `## 0.1.0 — <date>`, keep `package.json`'s `version` and `VERSION` in `lib/index.js`
93
+ equal (a test enforces it), and tag the commit.
94
+ 6. **Ask for a listing.** The plugin index (`awesome-dsh-plugin`) takes a PR: one line plus
95
+ a link, with `docs/screenshots/panel.png` as the picture.
96
+
97
+ ## Reporting a bug
98
+
99
+ Include the plugin version (Settings → Plugins, or `GET /api/global-todo/health`), the DSH
100
+ version, what you did, what happened, and what you expected. A redacted snippet of
101
+ `todos.json` helps when the problem is about a record.