dsh-checklists 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/lib/tools.js ADDED
@@ -0,0 +1,578 @@
1
+ /**
2
+ * The write surface: five tools the agent uses to keep the checklist current.
3
+ *
4
+ * Only the agent writes. There is no client write path, so the host document is
5
+ * the single source of truth and the sidebar (phase 2) is a read-only mirror.
6
+ */
7
+ import { defineTool } from '@deepseek-ai/dsh-tools';
8
+ import { buildItems, countItems, findList, flatten, ITEM_STATES, isItemState, mintId, nextItem, resolveRef, toLines, } from './model.js';
9
+ /** Ships with the plugin so a first run has something to instantiate. */
10
+ const DEFAULT_TEMPLATES = {
11
+ release: [
12
+ 'Define scope',
13
+ 'Implement changes',
14
+ ' Update tests',
15
+ ' Review diff',
16
+ 'Deploy to staging',
17
+ ' Smoke tests',
18
+ ' Rollback plan',
19
+ 'Deploy to production',
20
+ 'Announce release',
21
+ ],
22
+ feature: [
23
+ 'Understand the requirement',
24
+ 'Design the solution',
25
+ 'Implement',
26
+ ' Tests',
27
+ ' Docs',
28
+ 'Review and merge',
29
+ ],
30
+ debug: [
31
+ 'Reproduce the bug',
32
+ 'Isolate the cause',
33
+ 'Minimal fix',
34
+ ' Regression test',
35
+ 'Verify in staging',
36
+ ],
37
+ review: [
38
+ 'Read the diff',
39
+ 'Look for risks',
40
+ ' Security',
41
+ ' Performance',
42
+ 'Comment',
43
+ 'Approve or request changes',
44
+ ],
45
+ };
46
+ function sessionOf(exec) {
47
+ const id = exec.agent?.id;
48
+ if (typeof id !== 'string' || id.length === 0) {
49
+ throw new Error('checklist: this call has no session bound; checklists are per session');
50
+ }
51
+ return id;
52
+ }
53
+ function text(value) {
54
+ return typeof value === 'string' ? value : '';
55
+ }
56
+ export function registerChecklistTools(ctx, store, config = {}) {
57
+ const maxItems = config.maxItemsPerList ?? 200;
58
+ const allTemplates = async () => ({
59
+ ...DEFAULT_TEMPLATES,
60
+ ...(config.templates ?? {}),
61
+ ...(await store.readTemplates()),
62
+ });
63
+ ctx.effect(() => ctx.tools.register(defineTool({
64
+ name: 'checklist_create',
65
+ description: 'Create a named checklist for the CURRENT session. Use it when the user asks to track a multi-step '
66
+ + 'task, or when you are about to plan work that spans several steps and would otherwise be forgotten. '
67
+ + 'Items are plain strings; indent a line with two leading spaces to nest it under the previous item. '
68
+ + 'The checklist is injected into your context as a compact digest every turn, so you never need to '
69
+ + 're-read it unless you want the full tree (checklist_list).',
70
+ parameters: {
71
+ title: {
72
+ type: 'string',
73
+ required: true,
74
+ description: 'Short checklist title, unique within this session.',
75
+ },
76
+ items: {
77
+ type: 'array',
78
+ items: { type: 'string' },
79
+ description: 'Item texts. Two leading spaces nest under the previous item.',
80
+ },
81
+ template: {
82
+ type: 'string',
83
+ description: 'Optional template name: release, feature, debug, review, or one saved earlier.',
84
+ },
85
+ },
86
+ output: {
87
+ schema: {
88
+ type: 'object',
89
+ additionalProperties: false,
90
+ properties: {
91
+ listId: { type: 'string', required: true },
92
+ title: { type: 'string', required: true },
93
+ count: { type: 'integer', required: true },
94
+ },
95
+ },
96
+ render: (_args, value) => {
97
+ const result = value;
98
+ return [{ type: 'text', text: `Checklist "${result.title}" created with ${result.count} items (id ${result.listId}).` }];
99
+ },
100
+ },
101
+ timeoutMs: 10_000,
102
+ async execute(args, exec) {
103
+ const input = args;
104
+ const sessionId = sessionOf(exec);
105
+ return store.update(sessionId, async (doc) => {
106
+ const title = text(input.title).trim();
107
+ if (title.length === 0)
108
+ throw new Error('checklist_create: title must not be empty');
109
+ if (doc.lists.some((list) => list.title.toLowerCase() === title.toLowerCase())) {
110
+ throw new Error(`checklist_create: a checklist named "${title}" already exists in this session; `
111
+ + 'use checklist_list to read it, or pick another title');
112
+ }
113
+ const templateName = text(input.template).trim();
114
+ let templateLines;
115
+ if (templateName.length > 0) {
116
+ templateLines = (await allTemplates())[templateName];
117
+ if (!templateLines) {
118
+ const available = Object.keys(await allTemplates()).sort().join(', ');
119
+ throw new Error(`checklist_create: unknown template "${templateName}". Available: ${available}`);
120
+ }
121
+ }
122
+ const explicit = Array.isArray(input.items)
123
+ ? input.items.filter((item) => typeof item === 'string')
124
+ : [];
125
+ const items = buildItems([...(templateLines ?? []), ...explicit]);
126
+ if (items.length === 0) {
127
+ throw new Error('checklist_create: provide items or a template name');
128
+ }
129
+ const count = countItems(items).total;
130
+ if (count > maxItems) {
131
+ throw new Error(`checklist_create: ${count} items exceeds the per-list limit of ${maxItems}`);
132
+ }
133
+ const list = {
134
+ id: mintId('l', new Set(doc.lists.map((entry) => entry.id))),
135
+ title,
136
+ createdAt: new Date().toISOString(),
137
+ items,
138
+ };
139
+ doc.lists.push(list);
140
+ return { listId: list.id, title: list.title, count };
141
+ });
142
+ },
143
+ })));
144
+ ctx.effect(() => ctx.tools.register(defineTool({
145
+ name: 'checklist_check',
146
+ description: 'Set the state of one or more checklist items in the CURRENT session. Match items by id or by their '
147
+ + 'exact text (a unique prefix also works). Call it as soon as you finish, start, or block an item — '
148
+ + 'the digest and the sidebar update without the user asking.',
149
+ parameters: {
150
+ items: {
151
+ type: 'array',
152
+ items: { type: 'string' },
153
+ required: true,
154
+ description: 'Item ids or exact item texts to update.',
155
+ },
156
+ state: {
157
+ type: 'string',
158
+ required: true,
159
+ enum: [...ITEM_STATES],
160
+ description: 'New state for every matched item.',
161
+ },
162
+ listId: {
163
+ type: 'string',
164
+ description: 'Restrict matching to one checklist id.',
165
+ },
166
+ },
167
+ output: {
168
+ schema: {
169
+ type: 'object',
170
+ additionalProperties: false,
171
+ properties: {
172
+ state: { type: 'string', required: true },
173
+ updated: {
174
+ type: 'array',
175
+ required: true,
176
+ items: {
177
+ type: 'object',
178
+ additionalProperties: false,
179
+ properties: {
180
+ id: { type: 'string', required: true },
181
+ text: { type: 'string', required: true },
182
+ state: { type: 'string', required: true },
183
+ },
184
+ },
185
+ },
186
+ unresolved: {
187
+ type: 'array',
188
+ required: true,
189
+ items: { type: 'string' },
190
+ },
191
+ },
192
+ },
193
+ render: (_args, value) => {
194
+ const result = value;
195
+ const lines = result.updated.map((entry) => `· ${entry.text} → ${result.state}`);
196
+ if (result.unresolved.length > 0) {
197
+ lines.push(`No match: ${result.unresolved.join(', ')}`);
198
+ }
199
+ return [{ type: 'text', text: lines.join('\n') || 'No changes.' }];
200
+ },
201
+ },
202
+ timeoutMs: 10_000,
203
+ async execute(args, exec) {
204
+ const input = args;
205
+ const sessionId = sessionOf(exec);
206
+ const state = isItemState(input.state) ? input.state : undefined;
207
+ if (!state) {
208
+ throw new Error(`checklist_check: state must be one of ${ITEM_STATES.join(', ')}`);
209
+ }
210
+ const refs = Array.isArray(input.items)
211
+ ? input.items.filter((entry) => typeof entry === 'string')
212
+ : [];
213
+ if (refs.length === 0)
214
+ throw new Error('checklist_check: items must contain at least one id or text');
215
+ const listId = text(input.listId).trim() || undefined;
216
+ return store.update(sessionId, (doc) => {
217
+ const updated = [];
218
+ const unresolved = [];
219
+ const ambiguous = [];
220
+ for (const ref of refs) {
221
+ const found = resolveRef(doc.lists, ref, listId);
222
+ if (found.kind === 'hit') {
223
+ found.hit.item.state = state;
224
+ updated.push({ id: found.hit.item.id, text: found.hit.item.text, state });
225
+ }
226
+ else if (found.kind === 'ambiguous') {
227
+ ambiguous.push(ref);
228
+ }
229
+ else {
230
+ unresolved.push(ref);
231
+ }
232
+ }
233
+ if (updated.length === 0) {
234
+ const detail = ambiguous.length > 0 ? ` (ambiguous: ${ambiguous.join(', ')})` : '';
235
+ throw new Error(`checklist_check: no item matched ${refs.map((ref) => JSON.stringify(ref)).join(', ')}${detail}. `
236
+ + 'Call checklist_list for exact ids.');
237
+ }
238
+ return { state, updated, unresolved };
239
+ });
240
+ },
241
+ })));
242
+ ctx.effect(() => ctx.tools.register(defineTool({
243
+ name: 'checklist_list',
244
+ description: 'Read the full checklist tree of the CURRENT session, including item ids and states. '
245
+ + 'Use it when the digest is truncated or when you need exact ids to pass to checklist_check.',
246
+ parameters: {
247
+ listId: {
248
+ type: 'string',
249
+ description: 'Only return this checklist id.',
250
+ },
251
+ },
252
+ output: {
253
+ schema: { type: 'string' },
254
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
255
+ },
256
+ timeoutMs: 10_000,
257
+ async execute(args, exec) {
258
+ const input = args;
259
+ const sessionId = sessionOf(exec);
260
+ const listId = text(input.listId).trim() || undefined;
261
+ const doc = await store.prime(sessionId);
262
+ const lists = listId ? doc.lists.filter((list) => list.id === listId) : doc.lists;
263
+ if (lists.length === 0) {
264
+ return JSON.stringify({ sessionId, lists: [], note: 'No checklist exists in this session yet.' }, null, 2);
265
+ }
266
+ return JSON.stringify({ sessionId, lists }, null, 2);
267
+ },
268
+ })));
269
+ ctx.effect(() => ctx.tools.register(defineTool({
270
+ name: 'checklist_next',
271
+ description: 'Return the next checklist item to work on in the CURRENT session, in depth-first order. '
272
+ + 'Skips done subtrees and blocked ones; descends into an in-progress item before closing it. '
273
+ + 'Use it instead of re-reading the whole tree when you need to decide what to do next.',
274
+ parameters: {
275
+ listId: {
276
+ type: 'string',
277
+ description: 'Only search this checklist id.',
278
+ },
279
+ },
280
+ output: {
281
+ schema: {
282
+ type: 'object',
283
+ additionalProperties: false,
284
+ properties: {
285
+ found: { type: 'boolean', required: true },
286
+ listId: { type: 'string' },
287
+ listTitle: { type: 'string' },
288
+ itemId: { type: 'string' },
289
+ text: { type: 'string' },
290
+ path: { type: 'array', required: true, items: { type: 'string' } },
291
+ remaining: { type: 'integer', required: true },
292
+ blocked: { type: 'integer', required: true },
293
+ },
294
+ },
295
+ render: (_args, value) => {
296
+ const result = value;
297
+ if (!result.found) {
298
+ return [{ type: 'text', text: 'Nothing left to do in this session.' }];
299
+ }
300
+ const where = [...(result.path ?? []), result.text ?? ''].join(' › ');
301
+ const extra = result.blocked > 0 ? ` · ${result.blocked} blocked` : '';
302
+ return [{
303
+ type: 'text',
304
+ text: `Next: ${where} (${result.remaining} remaining${extra})`,
305
+ }];
306
+ },
307
+ },
308
+ timeoutMs: 10_000,
309
+ async execute(args, exec) {
310
+ const input = args;
311
+ const sessionId = sessionOf(exec);
312
+ const listId = text(input.listId).trim() || undefined;
313
+ const doc = await store.prime(sessionId);
314
+ const result = nextItem(doc.lists, listId);
315
+ if (!result.next) {
316
+ return { found: false, path: [], remaining: result.remaining, blocked: result.blocked };
317
+ }
318
+ const { list, item, path } = result.next;
319
+ return {
320
+ found: true,
321
+ listId: list.id,
322
+ listTitle: list.title,
323
+ itemId: item.id,
324
+ text: item.text,
325
+ path,
326
+ remaining: result.remaining,
327
+ blocked: result.blocked,
328
+ };
329
+ },
330
+ })));
331
+ ctx.effect(() => ctx.tools.register(defineTool({
332
+ name: 'checklist_edit',
333
+ description: 'Change the TEXT of one checklist item in the CURRENT session. Match it by id or exact text. '
334
+ + 'Use it to fix a typo or reword a step; it does not change the item state (use checklist_check for that).',
335
+ parameters: {
336
+ item: {
337
+ type: 'string',
338
+ required: true,
339
+ description: 'Item id or exact item text.',
340
+ },
341
+ text: {
342
+ type: 'string',
343
+ required: true,
344
+ description: 'New text for the item.',
345
+ },
346
+ listId: {
347
+ type: 'string',
348
+ description: 'Restrict matching to one checklist id.',
349
+ },
350
+ },
351
+ output: {
352
+ schema: {
353
+ type: 'object',
354
+ additionalProperties: false,
355
+ properties: {
356
+ id: { type: 'string', required: true },
357
+ from: { type: 'string', required: true },
358
+ to: { type: 'string', required: true },
359
+ },
360
+ },
361
+ render: (_args, value) => {
362
+ const result = value;
363
+ return [{ type: 'text', text: `"${result.from}" → "${result.to}"` }];
364
+ },
365
+ },
366
+ timeoutMs: 10_000,
367
+ async execute(args, exec) {
368
+ const input = args;
369
+ const sessionId = sessionOf(exec);
370
+ const ref = text(input.item).trim();
371
+ const next = text(input.text).trim();
372
+ if (ref.length === 0)
373
+ throw new Error('checklist_edit: item must not be empty');
374
+ if (next.length === 0)
375
+ throw new Error('checklist_edit: text must not be empty');
376
+ if (next.length > 500)
377
+ throw new Error('checklist_edit: text is longer than 500 characters');
378
+ const listId = text(input.listId).trim() || undefined;
379
+ return store.update(sessionId, (doc) => {
380
+ const found = resolveRef(doc.lists, ref, listId);
381
+ if (found.kind === 'ambiguous') {
382
+ const options = found.candidates
383
+ .map((entry) => `${entry.item.id} (${entry.list.title}: ${entry.item.text})`)
384
+ .join(', ');
385
+ throw new Error(`checklist_edit: "${ref}" is ambiguous; use an id: ${options}`);
386
+ }
387
+ if (found.kind === 'miss') {
388
+ throw new Error(`checklist_edit: no item matched "${ref}". Call checklist_list for exact ids.`);
389
+ }
390
+ const from = found.hit.item.text;
391
+ found.hit.item.text = next;
392
+ return { id: found.hit.item.id, from, to: next };
393
+ });
394
+ },
395
+ })));
396
+ ctx.effect(() => ctx.tools.register(defineTool({
397
+ name: 'checklist_add',
398
+ description: 'Append one or more items to an EXISTING checklist in the CURRENT session. '
399
+ + 'Items are plain strings; indent a line with two leading spaces to nest it under the previous new item. '
400
+ + 'Use `parent` to attach the new items under an existing item (by id or exact text) instead of the list root.',
401
+ parameters: {
402
+ listId: {
403
+ type: 'string',
404
+ required: true,
405
+ description: 'Checklist id, or its exact title.',
406
+ },
407
+ items: {
408
+ type: 'array',
409
+ required: true,
410
+ items: { type: 'string' },
411
+ description: 'Item texts to append. Two leading spaces nest under the previous new item.',
412
+ },
413
+ parent: {
414
+ type: 'string',
415
+ description: 'Existing item id or exact text to append under. Omit to append at the list root.',
416
+ },
417
+ },
418
+ output: {
419
+ schema: {
420
+ type: 'object',
421
+ additionalProperties: false,
422
+ properties: {
423
+ listId: { type: 'string', required: true },
424
+ title: { type: 'string', required: true },
425
+ added: { type: 'integer', required: true },
426
+ total: { type: 'integer', required: true },
427
+ },
428
+ },
429
+ render: (_args, value) => {
430
+ const result = value;
431
+ return [{
432
+ type: 'text',
433
+ text: `Added ${result.added} items to "${result.title}" (total ${result.total}).`,
434
+ }];
435
+ },
436
+ },
437
+ timeoutMs: 10_000,
438
+ async execute(args, exec) {
439
+ const input = args;
440
+ const sessionId = sessionOf(exec);
441
+ const ref = text(input.listId).trim();
442
+ if (ref.length === 0)
443
+ throw new Error('checklist_add: listId must not be empty');
444
+ const lines = Array.isArray(input.items)
445
+ ? input.items.filter((entry) => typeof entry === 'string')
446
+ : [];
447
+ if (lines.length === 0)
448
+ throw new Error('checklist_add: items must contain at least one string');
449
+ const parentRef = text(input.parent).trim();
450
+ return store.update(sessionId, (doc) => {
451
+ const list = findList(doc.lists, ref);
452
+ if (!list) {
453
+ const available = doc.lists.map((entry) => `${entry.id} (${entry.title})`).join(', ');
454
+ throw new Error(`checklist_add: no checklist "${ref}" in this session. `
455
+ + `Available: ${available.length > 0 ? available : 'none'}`);
456
+ }
457
+ // Ids are per list, so extend the set already in use or the new items
458
+ // would collide with existing ones.
459
+ const used = new Set(flatten(list.items).map((entry) => entry.item.id));
460
+ const fresh = buildItems(lines, used);
461
+ if (fresh.length === 0)
462
+ throw new Error('checklist_add: items produced no entries');
463
+ const added = countItems(fresh).total;
464
+ if (countItems(list.items).total + added > maxItems) {
465
+ throw new Error(`checklist_add: the list would exceed the limit of ${maxItems} items`);
466
+ }
467
+ if (parentRef.length > 0) {
468
+ const found = resolveRef([list], parentRef);
469
+ if (found.kind === 'ambiguous') {
470
+ const options = found.candidates.map((entry) => `${entry.item.id} (${entry.item.text})`).join(', ');
471
+ throw new Error(`checklist_add: parent "${parentRef}" is ambiguous; use an id: ${options}`);
472
+ }
473
+ if (found.kind === 'miss') {
474
+ throw new Error(`checklist_add: no item "${parentRef}" in "${list.title}". Call checklist_list for ids.`);
475
+ }
476
+ found.hit.item.children.push(...fresh);
477
+ }
478
+ else {
479
+ list.items.push(...fresh);
480
+ }
481
+ return { listId: list.id, title: list.title, added, total: countItems(list.items).total };
482
+ });
483
+ },
484
+ })));
485
+ ctx.effect(() => ctx.tools.register(defineTool({
486
+ name: 'checklist_delete',
487
+ description: 'Delete one checklist from the CURRENT session. The list and its items are gone; there is no archive. '
488
+ + 'Use it to clean up a finished or obsolete checklist, or to replace one (delete, then create). '
489
+ + 'Pass the id from checklist_list, or the exact title.',
490
+ parameters: {
491
+ listId: {
492
+ type: 'string',
493
+ required: true,
494
+ description: 'Checklist id, or its exact title.',
495
+ },
496
+ },
497
+ output: {
498
+ schema: {
499
+ type: 'object',
500
+ additionalProperties: false,
501
+ properties: {
502
+ listId: { type: 'string', required: true },
503
+ title: { type: 'string', required: true },
504
+ remaining: { type: 'integer', required: true },
505
+ },
506
+ },
507
+ render: (_args, value) => {
508
+ const result = value;
509
+ return [{ type: 'text', text: `Deleted checklist "${result.title}". ${result.remaining} left.` }];
510
+ },
511
+ },
512
+ timeoutMs: 10_000,
513
+ async execute(args, exec) {
514
+ const input = args;
515
+ const sessionId = sessionOf(exec);
516
+ const ref = text(input.listId).trim();
517
+ if (ref.length === 0)
518
+ throw new Error('checklist_delete: listId must not be empty');
519
+ return store.update(sessionId, (doc) => {
520
+ const target = findList(doc.lists, ref);
521
+ if (!target) {
522
+ const available = doc.lists.map((list) => `${list.id} (${list.title})`).join(', ');
523
+ throw new Error(`checklist_delete: no checklist "${ref}" in this session. `
524
+ + `Available: ${available.length > 0 ? available : 'none'}`);
525
+ }
526
+ doc.lists.splice(doc.lists.indexOf(target), 1);
527
+ return { listId: target.id, title: target.title, remaining: doc.lists.length };
528
+ });
529
+ },
530
+ })));
531
+ ctx.effect(() => ctx.tools.register(defineTool({
532
+ name: 'checklist_template_save',
533
+ description: 'Save one checklist of the CURRENT session as a reusable template available to future sessions. '
534
+ + 'Use it when the user says the same checklist will be needed again.',
535
+ parameters: {
536
+ name: {
537
+ type: 'string',
538
+ required: true,
539
+ description: 'Template name to create or overwrite.',
540
+ },
541
+ listId: {
542
+ type: 'string',
543
+ required: true,
544
+ description: 'Checklist id to copy.',
545
+ },
546
+ },
547
+ output: {
548
+ schema: {
549
+ type: 'object',
550
+ additionalProperties: false,
551
+ properties: {
552
+ name: { type: 'string', required: true },
553
+ items: { type: 'integer', required: true },
554
+ },
555
+ },
556
+ render: (_args, value) => {
557
+ const result = value;
558
+ return [{ type: 'text', text: `Template "${result.name}" saved with ${result.items} items.` }];
559
+ },
560
+ },
561
+ timeoutMs: 10_000,
562
+ async execute(args, exec) {
563
+ const input = args;
564
+ const sessionId = sessionOf(exec);
565
+ const name = text(input.name).trim();
566
+ if (name.length === 0)
567
+ throw new Error('checklist_template_save: name must not be empty');
568
+ const listId = text(input.listId).trim();
569
+ const doc = await store.prime(sessionId);
570
+ const list = doc.lists.find((entry) => entry.id === listId);
571
+ if (!list)
572
+ throw new Error(`checklist_template_save: no checklist with id "${listId}" in this session`);
573
+ const existing = await store.readTemplates();
574
+ await store.writeTemplates({ ...existing, [name]: toLines(list.items) });
575
+ return { name, items: countItems(list.items).total };
576
+ },
577
+ })));
578
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The compact checklist view injected into the model context every turn.
3
+ *
4
+ * This is the piece that removes the "ask me for the checklist again" loop:
5
+ * the digest is re-evaluated per assembly, so the model always sees current
6
+ * progress without anyone restating it.
7
+ *
8
+ * The rendering is deliberately small and stable — pending items first, a
9
+ * per-list line budget, and a hard total cap. A checklist that eats the context
10
+ * window would be self-defeating.
11
+ */
12
+ import { type SessionDoc } from './model.js';
13
+ export interface DigestOptions {
14
+ /** Hard cap on rendered body lines (header and rule excluded). Default 12. */
15
+ maxLines?: number;
16
+ /** Behavioral line shown last. */
17
+ rule?: string;
18
+ }
19
+ /**
20
+ * Render the session's checklists as a compact prompt block.
21
+ *
22
+ * Each checklist gets an equal share of the line budget. Giving the first list
23
+ * the whole budget starved the rest: with two lists the second showed a single
24
+ * line, and a third never appeared at all.
25
+ *
26
+ * @param doc - the session document, or undefined when nothing is cached.
27
+ * @param options - line budget and rule text.
28
+ * @returns the digest, or `''` when the session has no checklists.
29
+ */
30
+ export declare function renderDigest(doc: SessionDoc | undefined, options?: DigestOptions): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-checklist — session-scoped checklists for DeepSeek Harness.
3
+ *
4
+ * Two contributions:
5
+ * 1. Five tools (`checklist_*`) that let the agent create and update a tree
6
+ * of items for the current session.
7
+ * 2. A dynamic prompt context that renders a compact digest of that tree on
8
+ * every assembly, so the model never has to be reminded what is pending.
9
+ *
10
+ * The agent is the only writer. There is no client write path: the session
11
+ * document is the single source of truth.
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ export declare const name = "dsh-checklist";
15
+ export declare const inject: string[];
16
+ export interface Config {
17
+ /** Override the storage root (defaults to `$DSH_HOME/storages/dsh-checklist`). */
18
+ storageRoot?: string;
19
+ /** Body-line cap of the injected digest (default 12). */
20
+ maxDigestLines?: number;
21
+ /** Per-checklist item cap enforced by `checklist_create` (default 200). */
22
+ maxItemsPerList?: number;
23
+ /** Days after which untouched session files are pruned (default 14, 0 disables). */
24
+ pruneDays?: number;
25
+ /** Extra templates merged over the built-in ones. */
26
+ templates?: Record<string, string[]>;
27
+ }
28
+ export declare function apply(ctx: Context, config?: Config): void;