dsh-global-todo 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.9.0] — 2026-10-20
2
+
3
+ Competitive ground: the hand-off now has a return path, capture carries its source, and the
4
+ first screen says what the plugin is for.
5
+
6
+ - **Session read-back.** Every `assistant/message` updates a bounded in-memory tail (200 sessions,
7
+ 400 chars); `GET /session?id=` returns it with `alive` / `dir`, and the detail shows 「会话在跑 /
8
+ 已休眠」, the last thing the conversation said, and a button to open it. No disk read per row, no
9
+ new persistence.
10
+ - **Evidence on captured items.** `evidence` is the sentence an item came from, quoted verbatim
11
+ (≤300 chars), asked for by the capture prompt and shown under 「原话」 in the detail. A todo you
12
+ typed yourself simply has none.
13
+ - **A landing page instead of a manual.** Both READMEs open with three sentences, a 20-second
14
+ state tour (`docs/screenshots/tour.gif`, built reproducibly by `npm run tour`), and the install
15
+ command — the reference material moved below the fold.
16
+
1
17
  ## [0.8.0] — 2026-10-20
2
18
 
3
19
  A todo can carry its plan, and the plan is what the agent receives.
package/README.md CHANGED
@@ -1,12 +1,24 @@
1
1
  # dsh-global-todo
2
2
 
3
- **One todo list for the whole DeepSeek Harness, owned by you and written by your agent too.**
3
+ **A cross-session todo list for DeepSeek Harness: one file you own, written by you and your
4
+ agent, and handed to a conversation in one click.**
4
5
 
5
- Every entry records when it was added, carries a description, can attach any number of
6
- documents and addresses, and can be handed to an agent session with one click — so
7
- "continue this" is a button, not a paragraph of context you retype.
6
+ Three things it does that a session-local plan cannot:
8
7
 
9
- [中文说明](README.zh.md)
8
+ 1. **One JSON file is the whole list.** `cat`, diff, commit it, put it on a NAS, carry it to
9
+ another machine. Nothing lives inside a session log.
10
+ 2. **Hand-off is a button, not a paragraph.** Four modes, the working directory checked, and
11
+ the todo's own **steps** arrive as a numbered plan.
12
+ 3. **An agent saying "done" is a claim.** It lands in **待确认 / Needs review** until a person
13
+ confirms it — or sends it back to the list.
14
+
15
+ ![The panel in use: list, detail, steps, review, inbox, reminders, selection](docs/screenshots/tour.gif)
16
+
17
+ ```sh
18
+ dsh plugin add dsh-global-todo
19
+ ```
20
+
21
+ [中文说明](README.zh.md) · [Every state has a baseline image](#screenshots) · [Design notes](docs/DESIGN.md)
10
22
 
11
23
  ---
12
24
 
package/README.zh.md CHANGED
@@ -1,11 +1,20 @@
1
1
  # dsh-global-todo · 全局待办
2
2
 
3
- **一份属于你的全局待办清单,Agent 和你读写同一份。**
3
+ **一份跨会话的待办清单:一个属于你的文件,你和 Agent 读写同一份,一键交给某个对话继续干。**
4
4
 
5
- 每条都会记下**加入时间**,可以带描述,可以挂**任意多个相关文档和地址**,
6
- 还能**一键交给 Agent 继续干**——「接着做这个」是一个按钮,而不是一段你每次都要重打的上下文。
5
+ 三件会话级计划做不到的事:
7
6
 
8
- [English](README.md)
7
+ 1. **一个 JSON 文件就是全部**。能 `cat`、能 diff、能提交进仓库、能放 NAS、换机带走——不藏在任何会话日志里。
8
+ 2. **交接是一个按钮,不是一段话**。四种模式、会先检查工作目录,待办自己的**步骤**会作为编号计划一起交出去。
9
+ 3. **Agent 说「做完了」不算数**。它先进**待确认**,人点头才算完成,或者被退回清单。
10
+
11
+ ![面板实况:列表、详情、步骤、待确认、收件箱、提醒、批量选择](docs/screenshots/tour.gif)
12
+
13
+ ```sh
14
+ dsh plugin add dsh-global-todo
15
+ ```
16
+
17
+ [English](README.md) · [每个条件状态都有基线图](#截图) · [设计说明](docs/DESIGN.md)
9
18
 
10
19
  ---
11
20
 
package/docs/DESIGN.md CHANGED
@@ -57,6 +57,37 @@ $DSH_HOME/global-todo/todos.json ← { version: 1, todos: [ … ] }
57
57
  version, and a future migration step would run before the first write, keeping the old
58
58
  file as a backup.
59
59
 
60
+ ## Reading the conversation back
61
+
62
+ Handing work over used to be one-way: the panel knew a conversation was bound, and nothing about
63
+ what came of it. The read-back closes that loop with the cheapest mechanism available.
64
+
65
+ - **Where the text comes from.** The plugin already listens on `session/event`; every
66
+ `assistant/message` also updates a bounded cache (200 sessions, 400 characters each). Nothing is
67
+ read from disk per row and nothing is persisted — a stale tail is better than a slow panel, and
68
+ the panel says when it is dormant.
69
+ - **Why not read the session log on demand.** Opening a row is the most common click in the panel.
70
+ A per-row disk read would make it wait on I/O, and a conversation that the Host had unloaded
71
+ answers nothing anyway.
72
+ - **The route is separate** (`GET /session?id=`): `/list` is polled, a tail is not, and folding
73
+ them together would read every session on every poll. A missing tail is a normal answer, not an
74
+ error.
75
+ - **What the person sees:** 会话在跑 / 会话已休眠, the last thing it said (three lines, full text in
76
+ the tooltip), and a button that opens that conversation.
77
+
78
+ ## The sentence an item came from
79
+
80
+ A captured todo is somebody's summary of what a person said, which is exactly the step where
81
+ meaning drifts. So capture now carries `evidence`: the sentence, quoted verbatim, at most 300
82
+ characters, stored on the record and shown under 「原话」 in the detail (a quiet blockquote, not a
83
+ badge — it is reference material, not a status).
84
+
85
+ - The capture prompt asks for it in as many words: quote, do not paraphrase.
86
+ - A todo the person typed themselves has no evidence, and that is not an error — the field is
87
+ empty rather than absent, so the panel never has to guess.
88
+ - Too long is refused, like every other text field: evidence that is not the sentence is not
89
+ evidence.
90
+
60
91
  ## A plan inside the todo
61
92
 
62
93
  A todo is one line, but the work behind it is often five steps. Rather than grow a second object
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/lib/client.js CHANGED
@@ -50,7 +50,7 @@ window.__ModuleLoader__.load({
50
50
  * different mechanisms (restart vs. hot reload), so "which one am I looking
51
51
  * at" needs an answer that the bundle itself carries.
52
52
  */
53
- const UI_REV = 'ui47'
53
+ const UI_REV = 'ui48'
54
54
  /** Last language resolved for the shell; module-level so a fetch failure can be worded too. */
55
55
  let activeLang = 'zh'
56
56
 
@@ -299,6 +299,13 @@ window.__ModuleLoader__.load({
299
299
  'detail.links': '相关文档与地址',
300
300
  'detail.completion': '完成记录',
301
301
  'detail.notes': '进展记录',
302
+ 'detail.readback': '会话回读',
303
+ 'detail.evidence': '原话',
304
+ 'readback.none': '这条还没有交给过会话。',
305
+ 'readback.alive': '会话在跑',
306
+ 'readback.dormant': '会话已休眠',
307
+ 'readback.silent': '这个会话还没说过话。',
308
+ 'readback.open': '打开会话',
302
309
  'note.placeholder': '记一条进展:做到哪了、卡在哪、需要谁做什么',
303
310
  'note.add': '记录进展',
304
311
  'note.by.agent': 'Agent',
@@ -599,6 +606,13 @@ window.__ModuleLoader__.load({
599
606
  'group.conversation': 'Conversation',
600
607
  'detail.completion': 'Completion',
601
608
  'detail.notes': 'Progress notes',
609
+ 'detail.readback': 'Session read-back',
610
+ 'detail.evidence': 'In the author’s words',
611
+ 'readback.none': 'This has never been handed to a conversation.',
612
+ 'readback.alive': 'Conversation is running',
613
+ 'readback.dormant': 'Conversation is dormant',
614
+ 'readback.silent': 'This conversation has not said anything yet.',
615
+ 'readback.open': 'Open conversation',
602
616
  'note.placeholder': 'Add a note: where this stands, what is blocking it, who needs to act',
603
617
  'note.add': 'Add note',
604
618
  'note.by.agent': 'Agent',
@@ -1295,6 +1309,14 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1295
1309
  .gt-links { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
1296
1310
  .gt-link { display: flex; gap: 8px; align-items: center; padding: 4px 0; min-width: 0; }
1297
1311
  .gt-link-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
1312
+ .gt-evidence { margin: 0; padding: 2px 0 2px 10px; border-left: 2px solid var(--dsw-alias-border-l2);
1313
+ color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px;
1314
+ white-space: pre-wrap; word-break: break-word; }
1315
+ .gt-readback { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
1316
+ .gt-readback-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
1317
+ .gt-readback-text { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary);
1318
+ display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
1319
+ word-break: break-word; }
1298
1320
  .gt-textarea-steps { min-height: 76px; }
1299
1321
  .gt-steps { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; }
1300
1322
  .gt-step { display: flex; align-items: flex-start; gap: 8px; padding: 3px 0; }
@@ -1636,6 +1658,9 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1636
1658
  // viewport; the click looked like it did nothing. Bring the row into view
1637
1659
  // when it opens, without yanking the page when it closes.
1638
1660
  const rootRef = useRef(null)
1661
+ // What the bound conversation last said. Fetched once per open, because the row is
1662
+ // the only place the question "did it get anywhere" is asked.
1663
+ const [tail, setTail] = useState(null)
1639
1664
  const [linkValue, setLinkValue] = useState('')
1640
1665
  const [linkTitle, setLinkTitle] = useState('')
1641
1666
  const [draft, setDraft] = useState({
@@ -1645,6 +1670,26 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1645
1670
  priority: todo.priority,
1646
1671
  })
1647
1672
 
1673
+ useEffect(() => {
1674
+ if (open !== true || todo.sessionId === '') {
1675
+ setTail(null)
1676
+ return undefined
1677
+ }
1678
+ let alive = true
1679
+ call('session', { query: { id: todo.sessionId } })
1680
+ .then((data) => {
1681
+ // A conversation this process has not seen speak answers null: that is not an
1682
+ // error, it is the normal case for an old hand-off.
1683
+ if (alive) setTail(data?.session ?? null)
1684
+ })
1685
+ .catch(() => {
1686
+ if (alive) setTail(null)
1687
+ })
1688
+ return () => {
1689
+ alive = false
1690
+ }
1691
+ }, [open, todo.sessionId])
1692
+
1648
1693
  // Re-seed the edit draft whenever the record changes underneath us.
1649
1694
  useEffect(() => {
1650
1695
  setDraft({
@@ -2087,6 +2132,37 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2087
2132
  h(
2088
2133
  'div',
2089
2134
  { className: 'gt-section' },
2135
+ h('div', { className: 'gt-section-title' }, t('detail.readback')),
2136
+ tail === null
2137
+ ? h('div', { className: 'gt-muted' }, t('readback.none'))
2138
+ : h(
2139
+ 'div',
2140
+ { className: 'gt-readback' },
2141
+ h('div', { className: 'gt-readback-meta' },
2142
+ tail.alive === true ? t('readback.alive') : t('readback.dormant'),
2143
+ tail.tail?.at === undefined ? null : h('span', null, ' · ' + absoluteTime(tail.tail.at)),
2144
+ ),
2145
+ tail.tail === null || tail.tail === undefined
2146
+ ? h('div', { className: 'gt-muted' }, t('readback.silent'))
2147
+ : h('div', { className: 'gt-readback-text', title: tail.tail.text }, tail.tail.text),
2148
+ typeof ctx?.uiWorkspace?.openSession === 'function'
2149
+ ? h(
2150
+ 'button',
2151
+ {
2152
+ type: 'button',
2153
+ className: 'gt-btn gt-btn-quiet',
2154
+ onClick: () => {
2155
+ try {
2156
+ ctx.uiWorkspace.openSession(todo.sessionId)
2157
+ } catch {
2158
+ /* opening a conversation is a nicety, never a failure */
2159
+ }
2160
+ },
2161
+ },
2162
+ t('readback.open'),
2163
+ )
2164
+ : null,
2165
+ ),
2090
2166
  h('div', { className: 'gt-section-title' }, t('detail.notes')),
2091
2167
  (todo.notes ?? []).length === 0
2092
2168
  ? h('div', { className: 'gt-muted' }, t('note.none'))
@@ -2135,6 +2211,14 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2135
2211
  ),
2136
2212
  ),
2137
2213
  ),
2214
+ todo.evidence === '' || todo.evidence === undefined
2215
+ ? null
2216
+ : h(
2217
+ 'div',
2218
+ { className: 'gt-section' },
2219
+ h('div', { className: 'gt-section-title' }, t('detail.evidence')),
2220
+ h('blockquote', { className: 'gt-evidence' }, todo.evidence),
2221
+ ),
2138
2222
  todo.completion === null || todo.completion === undefined
2139
2223
  ? null
2140
2224
  : h(
package/lib/completion.js CHANGED
@@ -67,7 +67,7 @@ const VERDICT_TOOLS = Object.freeze([
67
67
  * @param {unknown} message
68
68
  * @returns {string}
69
69
  */
70
- function messageText(message) {
70
+ export function messageText(message) {
71
71
  const content = message?.content
72
72
  if (!Array.isArray(content)) return ''
73
73
  return content
package/lib/index.js CHANGED
@@ -23,7 +23,7 @@
23
23
  import { readFile, writeFile } from 'node:fs/promises'
24
24
  import path from 'node:path'
25
25
 
26
- import { createCompletionWatcher } from './completion.js'
26
+ import { createCompletionWatcher, messageText } from './completion.js'
27
27
  import { createDispatcher } from './dispatch.js'
28
28
  import { createSettings, explainAutoCollect, idleSessions, retryDelayMs } from './settings.js'
29
29
  import { createDiagnostics } from './diagnostics.js'
@@ -36,7 +36,7 @@ import { createStore, resolveStorePath } from './store.js'
36
36
  import { createTools } from './tools.js'
37
37
 
38
38
  /** Must equal `package.json`'s version; `npm run check` enforces that. */
39
- export const VERSION = '0.8.0'
39
+ export const VERSION = '0.9.0'
40
40
 
41
41
  /** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
42
42
  export const name = 'dsh-global-todo'
@@ -132,6 +132,21 @@ export function apply(ctx, config) {
132
132
  // reached the wrong conversation once already.
133
133
  const recentFile = path.join(path.dirname(file), 'recent-session.json')
134
134
  const recentSession = { id: '', watching: typeof ctx?.on === 'function', file: recentFile }
135
+ // What a bound conversation last said, remembered while its events stream by.
136
+ // Read-back has to be cheap: the panel asks on every opened row, and going to the
137
+ // session log per row would make opening a todo wait on disk. Bounded, text-only,
138
+ // never persisted — a stale tail is better than no answer, and it costs no write.
139
+ const SESSION_TAIL_LIMIT = 200
140
+ const sessionTails = new Map()
141
+ const rememberTail = (sessionId, text, at) => {
142
+ if (sessionId === '' || text === '') return
143
+ if (sessionTails.size >= SESSION_TAIL_LIMIT && !sessionTails.has(sessionId)) {
144
+ // Oldest insertion first: a Map keeps insertion order, so one delete is enough.
145
+ const oldest = sessionTails.keys().next()
146
+ if (oldest.done !== true) sessionTails.delete(oldest.value)
147
+ }
148
+ sessionTails.set(sessionId, { text: text.slice(0, 400), at, length: text.length })
149
+ }
135
150
 
136
151
  /** Best-effort: losing this file costs a guess, never a write. */
137
152
  const rememberSession = (sessionId) => {
@@ -158,6 +173,11 @@ export function apply(ctx, config) {
158
173
  try {
159
174
  const session = first?.id !== undefined && first?.header !== undefined ? first : second
160
175
  const event = typeof first?.type === 'string' ? first : typeof second?.type === 'string' ? second : undefined
176
+ // Every assistant message updates the read-back cache; only a person typing
177
+ // moves the recent-session pointer.
178
+ if (event !== undefined && event.type === 'assistant/message' && typeof session?.id === 'string') {
179
+ rememberTail(session.id, messageText(event.data?.message), Date.now())
180
+ }
161
181
  if (event === undefined || event.type !== 'user/message') return
162
182
  // `source.kind === 'user'` means a person typed it: model turns, tool
163
183
  // results and plugin injections all carry a different kind.
@@ -520,6 +540,7 @@ export function apply(ctx, config) {
520
540
  tools: toolState,
521
541
  completion: watcherState,
522
542
  target: recentSession,
543
+ sessionTail: (sessionId) => sessionTails.get(sessionId) ?? null,
523
544
  settings,
524
545
  autoCollect: autoCollectState,
525
546
  reminders: reminderState,
package/lib/model.js CHANGED
@@ -56,6 +56,9 @@ export const LIMITS = Object.freeze({
56
56
  // item should be split, and a cap keeps the dispatch prompt from becoming a document.
57
57
  steps: 20,
58
58
  step: 200,
59
+ // The sentence the item came from. A captured todo without its source reads as an
60
+ // opinion later; with it, the person can check whether it was heard correctly.
61
+ evidence: 300,
59
62
  })
60
63
 
61
64
  /**
@@ -208,6 +211,8 @@ export function createTodo(input, options = {}) {
208
211
  // An optional plan inside the todo: the steps a person or an agent broke it into.
209
212
  // Shipped to the agent as a numbered list, so hand-off stops being one paragraph.
210
213
  steps: normalizeSteps(raw.steps),
214
+ // Where it came from, in the author's own words: empty means "typed here".
215
+ evidence: normalizeText(raw.evidence, '原话', LIMITS.evidence, 'invalid-evidence'),
211
216
  createdAt: now,
212
217
  updatedAt: now,
213
218
  createdBy: by,
@@ -312,6 +317,7 @@ export function normalizeStored(raw, options = {}) {
312
317
  }
313
318
  })(),
314
319
  links,
320
+ evidence: typeof raw.evidence === 'string' ? raw.evidence.slice(0, LIMITS.evidence) : '',
315
321
  steps: (() => {
316
322
  try {
317
323
  return normalizeSteps(raw.steps)
@@ -577,6 +583,13 @@ export function applyPatch(todo, patch, options = {}) {
577
583
  changed.push('tags')
578
584
  }
579
585
  }
586
+ if (patch.evidence !== undefined) {
587
+ const evidence = normalizeText(patch.evidence, '原话', LIMITS.evidence, 'invalid-evidence')
588
+ if (evidence !== todo.evidence) {
589
+ todo.evidence = evidence
590
+ changed.push('evidence')
591
+ }
592
+ }
580
593
  if (patch.steps !== undefined) {
581
594
  const steps = normalizeSteps(patch.steps)
582
595
  const same =
package/lib/routes.js CHANGED
@@ -334,6 +334,28 @@ export function createRoutes(deps) {
334
334
  return { todo: service.annotateLinks(await service.get(id)) }
335
335
  }),
336
336
 
337
+ /**
338
+ * What a bound conversation last said, plus whether it is still around.
339
+ *
340
+ * Deliberately a separate route: the list is polled and a hand-off's tail is not,
341
+ * so folding this into /list would read every session on every poll. A missing
342
+ * tail is the normal answer for a conversation this process has not seen speak.
343
+ */
344
+ exact('GET', `${ROUTE_BASE}/session`, async (request) => {
345
+ const id = params(request).get('id') ?? ''
346
+ if (id === '') return { session: null }
347
+ const tailOf = typeof deps.sessionTail === 'function' ? deps.sessionTail : () => null
348
+ let alive = false
349
+ let dir = ''
350
+ try {
351
+ alive = dispatcher?.sessionAlive?.(id) === true
352
+ dir = dispatcher?.sessionDir?.(id) ?? ''
353
+ } catch {
354
+ /* a missing dispatcher is not an error here */
355
+ }
356
+ return { session: { id, alive, dir, tail: tailOf(id) } }
357
+ }),
358
+
337
359
  /**
338
360
  * The whole list as JSON.
339
361
  *
package/lib/service.js CHANGED
@@ -605,6 +605,9 @@ export function createService(options) {
605
605
  '该记的:等对方回复、待确认的决定、答应别人的事、还没收尾且需要另开一个会话继续的一步。',
606
606
  '每条必要时补描述、挂上相关文档**绝对路径**或网址,并标级别(P0 紧急 / P1 高 / P2 常规 / P3 低)。',
607
607
  '',
608
+ '**带上原话**(重要):每条用 `evidence` 逐字引用用户说的那一句(最多 200 字,不要改写、不要总结)。',
609
+ '以后看到这条待办的人,要能一眼核对「有没有听错」;没有原话的抓取,只是一条无从核对的转述。',
610
+ '',
608
611
  '记完用**一句话**汇总(例如「记了 2 条:X 与 Y」),不要逐条复述。',
609
612
  ].join('\n')
610
613
  }
package/lib/tools.js CHANGED
@@ -70,9 +70,17 @@ const LINKS_INPUT = {
70
70
  }
71
71
 
72
72
  /** One todo's worth of input, shared by `add` and `capture`. */
73
+ const EVIDENCE_INPUT = {
74
+ type: 'string',
75
+ description:
76
+ `原话(可选,最多 ${LIMITS.evidence} 字):用户说的那**一句**,逐字引用,用来让人核对有没有听错。`
77
+ + '抓取时几乎总该带;自己顺手记一条时可以不写。',
78
+ }
79
+
73
80
  const ITEM_PROPERTIES = {
74
81
  title: { type: 'string', description: `一句话说清要做什么(最多 ${LIMITS.title} 字)` },
75
82
  description: { type: 'string', description: '补充说明、验收标准、已知的坑' },
83
+ evidence: EVIDENCE_INPUT,
76
84
  links: LINKS_INPUT,
77
85
  tags: { type: 'array', items: { type: 'string' }, description: `标签,最多 ${LIMITS.tags} 个` },
78
86
  priority: { type: 'string', enum: [...PRIORITIES], description: 'P0 紧急 / P1 高 / P2 常规(缺省)/ P3 低' },
@@ -120,6 +128,7 @@ const TODO_SHAPE = { type: 'object',
120
128
  priority: { type: 'string', enum: [...PRIORITIES] },
121
129
  dueAt: { oneOf: [{ type: 'string' }, { type: 'null' }] },
122
130
  tags: { type: 'array', items: { type: 'string' } },
131
+ evidence: { type: 'string', description: '原话(空串表示是自己记的,不是抓的)' },
123
132
  steps: STEPS_SHAPE,
124
133
  links: {
125
134
  type: 'array',
@@ -181,6 +190,7 @@ export function presentTodo(todo, now = Date.now()) {
181
190
  priority: todo.priority,
182
191
  dueAt: todo.dueAt,
183
192
  tags: [...todo.tags],
193
+ evidence: todo.evidence ?? '',
184
194
  steps: (todo.steps ?? []).map((step) => ({ text: step.text, done: step.done === true })),
185
195
  links: todo.links.map((link) => ({ ...link })),
186
196
  createdAt: todo.createdAt,
@@ -332,6 +342,7 @@ export function createTools(service, options = {}) {
332
342
  description: { type: 'string', description: '补充说明、验收标准、已知的坑' },
333
343
  links: LINKS_INPUT,
334
344
  steps: STEPS_INPUT,
345
+ evidence: EVIDENCE_INPUT,
335
346
  tags: { type: 'array', items: { type: 'string' }, description: `标签,最多 ${LIMITS.tags} 个` },
336
347
  priority: { type: 'string', enum: [...PRIORITIES], description: 'P0 紧急 / P1 高 / P2 常规(缺省)/ P3 低' },
337
348
  dueAt: { type: 'string', description: 'ISO-8601 截止时间,例如 2026-10-20T10:00 或 2026-10-20' },
@@ -574,6 +585,7 @@ export function createTools(service, options = {}) {
574
585
  tags: { type: 'array', items: { type: 'string' } },
575
586
  links: LINKS_INPUT,
576
587
  steps: STEPS_INPUT,
588
+ evidence: EVIDENCE_INPUT,
577
589
  },
578
590
  required: ['id'],
579
591
  additionalProperties: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-global-todo",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "icon": "./docs/icon.svg",
5
5
  "description": "Global, cross-session todos for DeepSeek Harness: one list owned by the human and the agent together, with creation time, attached documents and links, and one-click hand-off to an agent session. 全局待办:跨会话一份清单,记录加入时间,可挂文档与地址,可一键交给 Agent 继续干。",
6
6
  "type": "module",
@@ -58,6 +58,7 @@
58
58
  "verify": "npm run check && npm run check:render && npm test",
59
59
  "preview": "node tools/preview.mjs",
60
60
  "screenshots": "npm run preview && node tools/screenshot.mjs",
61
+ "tour": "node tools/tour.mjs",
61
62
  "prepack": "npm run verify",
62
63
  "check:render": "node tools/preview.mjs --check",
63
64
  "check:live": "node tools/live-check.mjs",