dsh-checklists 0.3.3 → 0.5.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/README.md +2 -1
- package/lib/gate.js +55 -0
- package/lib/index.js +62 -0
- package/lib/model.js +54 -7
- package/lib/tools.js +73 -1
- package/lib/types/gate.d.ts +37 -0
- package/lib/types/model.d.ts +23 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ If the agent forgets to mark something, you say so in chat and it fixes it.
|
|
|
28
28
|
| `checklist_edit` | Changes an item's text (not its state) |
|
|
29
29
|
| `checklist_list` | Full tree with ids and states, for when the digest is truncated |
|
|
30
30
|
| `checklist_delete` | Deletes a list (by id or exact title) |
|
|
31
|
+
| `checklist_remove_item` | Removes one item and everything nested under it |
|
|
31
32
|
| `checklist_rename` | Renames a list; titles stay unique within the session |
|
|
32
33
|
| `checklist_template_save` | Saves a list as a reusable template for future sessions |
|
|
33
34
|
|
|
@@ -49,7 +50,7 @@ From a local build or tarball:
|
|
|
49
50
|
npm install
|
|
50
51
|
npm run build
|
|
51
52
|
npm pack
|
|
52
|
-
dsh plugin --profile dev add ./dsh-checklists-0.
|
|
53
|
+
dsh plugin --profile dev add ./dsh-checklists-0.4.0.tgz
|
|
53
54
|
dsh --profile dev --dump-config # the tool-checklist row should appear
|
|
54
55
|
```
|
|
55
56
|
|
package/lib/gate.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-end gate: decides whether a turn is closing with the checklist stale.
|
|
3
|
+
*
|
|
4
|
+
* Kept pure on purpose — the host event wiring lives in `index.ts`, so the rule
|
|
5
|
+
* itself is testable without a running harness and can be reasoned about by
|
|
6
|
+
* reading one function.
|
|
7
|
+
*
|
|
8
|
+
* The gate never guesses *what* was completed. It only notices the shape of the
|
|
9
|
+
* problem: real work happened, the checklist was not touched, and something is
|
|
10
|
+
* still marked as in progress.
|
|
11
|
+
*/
|
|
12
|
+
/** Observation endpoint for the gate's last decisions. */
|
|
13
|
+
export const NUDGE_ROUTE = '/dsh-checklist/nudge';
|
|
14
|
+
/**
|
|
15
|
+
* Tools whose result can change the workspace. `run_code` is included because
|
|
16
|
+
* the PTC transport can reach any tool behind it.
|
|
17
|
+
*/
|
|
18
|
+
export const MUTATING_TOOLS = new Set([
|
|
19
|
+
'bash',
|
|
20
|
+
'pwsh',
|
|
21
|
+
'write',
|
|
22
|
+
'edit',
|
|
23
|
+
'str_replace_editor',
|
|
24
|
+
'apply_patch',
|
|
25
|
+
'run_code',
|
|
26
|
+
]);
|
|
27
|
+
/** Tools that already keep a checklist current. */
|
|
28
|
+
const CHECKLIST_WRITES = new Set([
|
|
29
|
+
'checklist_create',
|
|
30
|
+
'checklist_add',
|
|
31
|
+
'checklist_check',
|
|
32
|
+
'checklist_edit',
|
|
33
|
+
'checklist_remove_item',
|
|
34
|
+
'checklist_delete',
|
|
35
|
+
'checklist_rename',
|
|
36
|
+
]);
|
|
37
|
+
export function isChecklistWrite(name) {
|
|
38
|
+
return CHECKLIST_WRITES.has(name);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decide whether the closing turn should be challenged.
|
|
42
|
+
*
|
|
43
|
+
* @param activity - what the turn did.
|
|
44
|
+
* @param doingCount - items currently in `doing` across the session.
|
|
45
|
+
* @returns the decision and a short human-readable reason, logged either way.
|
|
46
|
+
*/
|
|
47
|
+
export function decideNudge(activity, doingCount) {
|
|
48
|
+
if (!activity.mutated)
|
|
49
|
+
return { nudge: false, reason: 'no mutating tool calls' };
|
|
50
|
+
if (activity.checklistWrite)
|
|
51
|
+
return { nudge: false, reason: 'checklist already updated' };
|
|
52
|
+
if (doingCount === 0)
|
|
53
|
+
return { nudge: false, reason: 'nothing in progress' };
|
|
54
|
+
return { nudge: true, reason: `${doingCount} item(s) still doing after workspace changes` };
|
|
55
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* document is the single source of truth.
|
|
12
12
|
*/
|
|
13
13
|
import { renderDigest } from './digest.js';
|
|
14
|
+
import { decideNudge, isChecklistWrite, MUTATING_TOOLS, NUDGE_ROUTE } from './gate.js';
|
|
15
|
+
import { countState } from './model.js';
|
|
14
16
|
import { serveProbe, PROBE_ROUTE } from './probe.js';
|
|
15
17
|
import { serveItemUpdate, serveState, ITEM_ROUTE, STATE_ROUTE } from './route.js';
|
|
16
18
|
import { ChecklistStore } from './store.js';
|
|
@@ -44,6 +46,52 @@ export function apply(ctx, config = {}) {
|
|
|
44
46
|
if (config.pruneDays !== 0) {
|
|
45
47
|
void store.prune(config.pruneDays ?? 14).catch(() => undefined);
|
|
46
48
|
}
|
|
49
|
+
// --- Turn-end gate (observation mode) ------------------------------------
|
|
50
|
+
// Watches one turn's tool activity and decides, as the turn closes, whether
|
|
51
|
+
// the checklist was left stale. This release only records the decision — the
|
|
52
|
+
// event is not exercised yet — so enabling `steer` is a one-line follow-up
|
|
53
|
+
// once the log shows it fires when it should.
|
|
54
|
+
const activity = new Map();
|
|
55
|
+
const gateLog = [];
|
|
56
|
+
const events = ctx;
|
|
57
|
+
ctx.effect(() => events.on('tools/result', (payload) => {
|
|
58
|
+
const event = payload;
|
|
59
|
+
const toolName = event.exec?.name;
|
|
60
|
+
const agentId = event.exec?.agent?.id;
|
|
61
|
+
if (typeof toolName !== 'string' || typeof agentId !== 'string')
|
|
62
|
+
return;
|
|
63
|
+
const current = activity.get(agentId) ?? { mutated: false, checklistWrite: false };
|
|
64
|
+
if (MUTATING_TOOLS.has(toolName))
|
|
65
|
+
current.mutated = true;
|
|
66
|
+
if (isChecklistWrite(toolName))
|
|
67
|
+
current.checklistWrite = true;
|
|
68
|
+
activity.set(agentId, current);
|
|
69
|
+
}));
|
|
70
|
+
ctx.effect(() => events.on('agent/turn-stopping', (payload) => {
|
|
71
|
+
const event = payload;
|
|
72
|
+
const agentId = event.agent?.id;
|
|
73
|
+
if (typeof agentId !== 'string')
|
|
74
|
+
return;
|
|
75
|
+
const current = activity.get(agentId) ?? { mutated: false, checklistWrite: false };
|
|
76
|
+
activity.set(agentId, { mutated: false, checklistWrite: false });
|
|
77
|
+
const doc = store.peek(agentId);
|
|
78
|
+
let doing = 0;
|
|
79
|
+
if (doc)
|
|
80
|
+
for (const list of doc.lists)
|
|
81
|
+
doing += countState(list.items, 'doing');
|
|
82
|
+
const decision = decideNudge(current, doing);
|
|
83
|
+
if (!decision.nudge && process.env.DSH_CHECKLIST_DEBUG !== '1')
|
|
84
|
+
return;
|
|
85
|
+
gateLog.push({
|
|
86
|
+
at: new Date().toISOString(),
|
|
87
|
+
sessionId: agentId,
|
|
88
|
+
nudge: decision.nudge,
|
|
89
|
+
reason: decision.reason,
|
|
90
|
+
doing,
|
|
91
|
+
});
|
|
92
|
+
if (gateLog.length > 20)
|
|
93
|
+
gateLog.shift();
|
|
94
|
+
}));
|
|
47
95
|
let loggedScope = false;
|
|
48
96
|
ctx.inject(['systemPrompt'], (promptCtx) => {
|
|
49
97
|
promptCtx.systemPrompt.context({
|
|
@@ -93,5 +141,19 @@ export function apply(ctx, config = {}) {
|
|
|
93
141
|
serveProbe(req, res);
|
|
94
142
|
},
|
|
95
143
|
}), 'dsh-checklist: client probe route');
|
|
144
|
+
// What the turn-end gate decided, for verification before it steers.
|
|
145
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
146
|
+
kind: 'exact',
|
|
147
|
+
path: NUDGE_ROUTE,
|
|
148
|
+
handler: (req, res) => {
|
|
149
|
+
const body = Buffer.from(`${JSON.stringify({ mode: 'observe', entries: gateLog }, null, 2)}\n`, 'utf8');
|
|
150
|
+
res.writeHead(200, {
|
|
151
|
+
'content-type': 'application/json; charset=utf-8',
|
|
152
|
+
'content-length': String(body.byteLength),
|
|
153
|
+
'cache-control': 'no-store',
|
|
154
|
+
});
|
|
155
|
+
res.end(body);
|
|
156
|
+
},
|
|
157
|
+
}), 'dsh-checklist: nudge log route');
|
|
96
158
|
});
|
|
97
159
|
}
|
package/lib/model.js
CHANGED
|
@@ -9,12 +9,25 @@ export const ITEM_STATES = ['todo', 'doing', 'done', 'blocked'];
|
|
|
9
9
|
export function emptyDoc(sessionId) {
|
|
10
10
|
return { version: 1, sessionId, lists: [] };
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Mint the next `<prefix>N` identifier.
|
|
14
|
+
*
|
|
15
|
+
* Numbers start after the highest one already in `used`, so a removed item's id
|
|
16
|
+
* is not handed to the next arrival: a freed `i3` stays dead instead of
|
|
17
|
+
* silently aliasing a different item. The high-water mark is not persisted, so
|
|
18
|
+
* removing the newest item and adding another can still reuse that id — fully
|
|
19
|
+
* closing the hole needs a stored counter, which means a schema change.
|
|
20
|
+
*/
|
|
13
21
|
export function mintId(prefix, used) {
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
let highest = 0;
|
|
23
|
+
for (const id of used) {
|
|
24
|
+
if (!id.startsWith(prefix))
|
|
25
|
+
continue;
|
|
26
|
+
const suffix = Number(id.slice(prefix.length));
|
|
27
|
+
if (Number.isInteger(suffix) && suffix > highest)
|
|
28
|
+
highest = suffix;
|
|
29
|
+
}
|
|
30
|
+
return `${prefix}${highest + 1}`;
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
20
33
|
* Build an item tree from plain lines.
|
|
@@ -80,6 +93,16 @@ export function countItems(items) {
|
|
|
80
93
|
}
|
|
81
94
|
return { total, done };
|
|
82
95
|
}
|
|
96
|
+
/** Count items in one state across a tree. */
|
|
97
|
+
export function countState(items, state) {
|
|
98
|
+
let total = 0;
|
|
99
|
+
for (const item of items) {
|
|
100
|
+
if (item.state === state)
|
|
101
|
+
total += 1;
|
|
102
|
+
total += countState(item.children, state);
|
|
103
|
+
}
|
|
104
|
+
return total;
|
|
105
|
+
}
|
|
83
106
|
/**
|
|
84
107
|
* Resolve one model-supplied reference against the session's checklists.
|
|
85
108
|
*
|
|
@@ -89,14 +112,17 @@ export function countItems(items) {
|
|
|
89
112
|
*
|
|
90
113
|
* @param lists - the session's checklists.
|
|
91
114
|
* @param ref - a model-supplied item id, text, or prefix.
|
|
92
|
-
* @param listId - optional checklist id to search within.
|
|
115
|
+
* @param listId - optional checklist id **or exact title** to search within.
|
|
93
116
|
* @returns the hit, the ambiguity, or a miss.
|
|
94
117
|
*/
|
|
95
118
|
export function resolveRef(lists, ref, listId) {
|
|
96
119
|
const needles = ref.trim().toLowerCase();
|
|
97
120
|
if (needles.length === 0)
|
|
98
121
|
return { kind: 'miss' };
|
|
99
|
-
|
|
122
|
+
// Accept a title here as well as an id: every other tool does, and a model
|
|
123
|
+
// that just created "Release 2.3" will pass that back, not "l1".
|
|
124
|
+
const scope = listId ? findList(lists, listId) : undefined;
|
|
125
|
+
const scoped = listId ? (scope ? [scope] : []) : lists;
|
|
100
126
|
const entries = [];
|
|
101
127
|
for (const list of scoped) {
|
|
102
128
|
for (const { item } of flatten(list.items))
|
|
@@ -147,6 +173,27 @@ export function findItemById(lists, id) {
|
|
|
147
173
|
}
|
|
148
174
|
return undefined;
|
|
149
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Remove one item (and its whole subtree) by exact id.
|
|
178
|
+
*
|
|
179
|
+
* Mutates the array in place, so callers must pass a document they own — the
|
|
180
|
+
* store's `update` already hands out a clone.
|
|
181
|
+
*
|
|
182
|
+
* @param items - sibling list to search, at any depth.
|
|
183
|
+
* @param id - exact item id.
|
|
184
|
+
* @returns the removed node, or `undefined` when no item has that id.
|
|
185
|
+
*/
|
|
186
|
+
export function removeItemById(items, id) {
|
|
187
|
+
const index = items.findIndex((entry) => entry.id === id);
|
|
188
|
+
if (index >= 0)
|
|
189
|
+
return items.splice(index, 1)[0];
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
const removed = removeItemById(item.children, id);
|
|
192
|
+
if (removed)
|
|
193
|
+
return removed;
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
150
197
|
/**
|
|
151
198
|
* Validate a checklist rename before applying it.
|
|
152
199
|
*
|
package/lib/tools.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the single source of truth and the sidebar (phase 2) is a read-only mirror.
|
|
6
6
|
*/
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
-
import { buildItems, checkRename, countItems, findList, flatten, ITEM_STATES, isItemState, mintId, nextItem, resolveRef, toLines, } from './model.js';
|
|
8
|
+
import { buildItems, checkRename, countItems, findList, flatten, ITEM_STATES, isItemState, mintId, nextItem, removeItemById, resolveRef, toLines, } from './model.js';
|
|
9
9
|
/** Ships with the plugin so a first run has something to instantiate. */
|
|
10
10
|
const DEFAULT_TEMPLATES = {
|
|
11
11
|
release: [
|
|
@@ -542,6 +542,78 @@ export function registerChecklistTools(ctx, store, config = {}) {
|
|
|
542
542
|
});
|
|
543
543
|
},
|
|
544
544
|
})));
|
|
545
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
546
|
+
name: 'checklist_remove_item',
|
|
547
|
+
description: 'Remove one item — and everything nested under it — from a checklist in the CURRENT session. '
|
|
548
|
+
+ 'Match it by id or exact text. Use it to drop work that no longer applies; '
|
|
549
|
+
+ 'to change the item instead, use checklist_edit.',
|
|
550
|
+
parameters: {
|
|
551
|
+
listId: {
|
|
552
|
+
type: 'string',
|
|
553
|
+
required: true,
|
|
554
|
+
description: 'Checklist id, or its exact title.',
|
|
555
|
+
},
|
|
556
|
+
item: {
|
|
557
|
+
type: 'string',
|
|
558
|
+
required: true,
|
|
559
|
+
description: 'Item id or exact item text.',
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
output: {
|
|
563
|
+
schema: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
additionalProperties: false,
|
|
566
|
+
properties: {
|
|
567
|
+
listId: { type: 'string', required: true },
|
|
568
|
+
itemId: { type: 'string', required: true },
|
|
569
|
+
text: { type: 'string', required: true },
|
|
570
|
+
removed: { type: 'integer', required: true },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
render: (_args, value) => {
|
|
574
|
+
const result = value;
|
|
575
|
+
const extra = result.removed > 1 ? ` (+${result.removed - 1} nested)` : '';
|
|
576
|
+
return [{ type: 'text', text: `Removed "${result.text}"${extra}.` }];
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
timeoutMs: 10_000,
|
|
580
|
+
async execute(args, exec) {
|
|
581
|
+
const input = args;
|
|
582
|
+
const sessionId = sessionOf(exec);
|
|
583
|
+
const listRef = text(input.listId).trim();
|
|
584
|
+
const itemRef = text(input.item).trim();
|
|
585
|
+
if (listRef.length === 0)
|
|
586
|
+
throw new Error('checklist_remove_item: listId must not be empty');
|
|
587
|
+
if (itemRef.length === 0)
|
|
588
|
+
throw new Error('checklist_remove_item: item must not be empty');
|
|
589
|
+
return store.update(sessionId, (doc) => {
|
|
590
|
+
const list = findList(doc.lists, listRef);
|
|
591
|
+
if (!list) {
|
|
592
|
+
const available = doc.lists.map((entry) => `${entry.id} (${entry.title})`).join(', ');
|
|
593
|
+
throw new Error(`checklist_remove_item: no checklist "${listRef}" in this session. `
|
|
594
|
+
+ `Available: ${available.length > 0 ? available : 'none'}`);
|
|
595
|
+
}
|
|
596
|
+
const found = resolveRef([list], itemRef);
|
|
597
|
+
if (found.kind === 'ambiguous') {
|
|
598
|
+
const options = found.candidates.map((entry) => `${entry.item.id} (${entry.item.text})`).join(', ');
|
|
599
|
+
throw new Error(`checklist_remove_item: "${itemRef}" is ambiguous; use an id: ${options}`);
|
|
600
|
+
}
|
|
601
|
+
if (found.kind === 'miss') {
|
|
602
|
+
throw new Error(`checklist_remove_item: no item "${itemRef}" in "${list.title}". Call checklist_list for ids.`);
|
|
603
|
+
}
|
|
604
|
+
const removed = removeItemById(list.items, found.hit.item.id);
|
|
605
|
+
if (!removed) {
|
|
606
|
+
throw new Error(`checklist_remove_item: item "${itemRef}" disappeared from "${list.title}"`);
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
listId: list.id,
|
|
610
|
+
itemId: removed.id,
|
|
611
|
+
text: removed.text,
|
|
612
|
+
removed: countItems([removed]).total,
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
},
|
|
616
|
+
})));
|
|
545
617
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
546
618
|
name: 'checklist_delete',
|
|
547
619
|
description: 'Delete one checklist from the CURRENT session. The list and its items are gone; there is no archive. '
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-end gate: decides whether a turn is closing with the checklist stale.
|
|
3
|
+
*
|
|
4
|
+
* Kept pure on purpose — the host event wiring lives in `index.ts`, so the rule
|
|
5
|
+
* itself is testable without a running harness and can be reasoned about by
|
|
6
|
+
* reading one function.
|
|
7
|
+
*
|
|
8
|
+
* The gate never guesses *what* was completed. It only notices the shape of the
|
|
9
|
+
* problem: real work happened, the checklist was not touched, and something is
|
|
10
|
+
* still marked as in progress.
|
|
11
|
+
*/
|
|
12
|
+
export interface TurnActivity {
|
|
13
|
+
/** The turn ran a tool that can change the workspace. */
|
|
14
|
+
mutated: boolean;
|
|
15
|
+
/** The turn already wrote to a checklist. */
|
|
16
|
+
checklistWrite: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Observation endpoint for the gate's last decisions. */
|
|
19
|
+
export declare const NUDGE_ROUTE = "/dsh-checklist/nudge";
|
|
20
|
+
export interface NudgeDecision {
|
|
21
|
+
nudge: boolean;
|
|
22
|
+
reason: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tools whose result can change the workspace. `run_code` is included because
|
|
26
|
+
* the PTC transport can reach any tool behind it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MUTATING_TOOLS: ReadonlySet<string>;
|
|
29
|
+
export declare function isChecklistWrite(name: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Decide whether the closing turn should be challenged.
|
|
32
|
+
*
|
|
33
|
+
* @param activity - what the turn did.
|
|
34
|
+
* @param doingCount - items currently in `doing` across the session.
|
|
35
|
+
* @returns the decision and a short human-readable reason, logged either way.
|
|
36
|
+
*/
|
|
37
|
+
export declare function decideNudge(activity: TurnActivity, doingCount: number): NudgeDecision;
|
package/lib/types/model.d.ts
CHANGED
|
@@ -25,7 +25,15 @@ export interface SessionDoc {
|
|
|
25
25
|
lists: Checklist[];
|
|
26
26
|
}
|
|
27
27
|
export declare function emptyDoc(sessionId: string): SessionDoc;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Mint the next `<prefix>N` identifier.
|
|
30
|
+
*
|
|
31
|
+
* Numbers start after the highest one already in `used`, so a removed item's id
|
|
32
|
+
* is not handed to the next arrival: a freed `i3` stays dead instead of
|
|
33
|
+
* silently aliasing a different item. The high-water mark is not persisted, so
|
|
34
|
+
* removing the newest item and adding another can still reuse that id — fully
|
|
35
|
+
* closing the hole needs a stored counter, which means a schema change.
|
|
36
|
+
*/
|
|
29
37
|
export declare function mintId(prefix: string, used: ReadonlySet<string>): string;
|
|
30
38
|
/**
|
|
31
39
|
* Build an item tree from plain lines.
|
|
@@ -53,6 +61,8 @@ export interface ItemCounts {
|
|
|
53
61
|
done: number;
|
|
54
62
|
}
|
|
55
63
|
export declare function countItems(items: readonly ChecklistItem[]): ItemCounts;
|
|
64
|
+
/** Count items in one state across a tree. */
|
|
65
|
+
export declare function countState(items: readonly ChecklistItem[], state: ItemState): number;
|
|
56
66
|
export interface RefHit {
|
|
57
67
|
list: Checklist;
|
|
58
68
|
item: ChecklistItem;
|
|
@@ -75,7 +85,7 @@ export type RefResult = {
|
|
|
75
85
|
*
|
|
76
86
|
* @param lists - the session's checklists.
|
|
77
87
|
* @param ref - a model-supplied item id, text, or prefix.
|
|
78
|
-
* @param listId - optional checklist id to search within.
|
|
88
|
+
* @param listId - optional checklist id **or exact title** to search within.
|
|
79
89
|
* @returns the hit, the ambiguity, or a miss.
|
|
80
90
|
*/
|
|
81
91
|
export declare function resolveRef(lists: readonly Checklist[], ref: string, listId?: string): RefResult;
|
|
@@ -97,6 +107,17 @@ export declare function findItemById(lists: readonly Checklist[], id: string): {
|
|
|
97
107
|
list: Checklist;
|
|
98
108
|
item: ChecklistItem;
|
|
99
109
|
} | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Remove one item (and its whole subtree) by exact id.
|
|
112
|
+
*
|
|
113
|
+
* Mutates the array in place, so callers must pass a document they own — the
|
|
114
|
+
* store's `update` already hands out a clone.
|
|
115
|
+
*
|
|
116
|
+
* @param items - sibling list to search, at any depth.
|
|
117
|
+
* @param id - exact item id.
|
|
118
|
+
* @returns the removed node, or `undefined` when no item has that id.
|
|
119
|
+
*/
|
|
120
|
+
export declare function removeItemById(items: ChecklistItem[], id: string): ChecklistItem | undefined;
|
|
100
121
|
export type RenameCheck = {
|
|
101
122
|
kind: 'ok';
|
|
102
123
|
list: Checklist;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-checklists",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Session-scoped checklists for DeepSeek Harness: the agent tracks work as a tree, the model always sees a compact digest, and a read-only sidebar tab mirrors it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|