dsh-checklists 0.5.1 → 0.6.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 +36 -1
- package/lib/index.js +60 -17
- package/lib/types/index.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ From a local build or tarball:
|
|
|
50
50
|
npm install
|
|
51
51
|
npm run build
|
|
52
52
|
npm pack
|
|
53
|
-
dsh plugin --profile dev add ./dsh-checklists-0.
|
|
53
|
+
dsh plugin --profile dev add ./dsh-checklists-0.6.0.tgz
|
|
54
54
|
dsh --profile dev --dump-config # the tool-checklist row should appear
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -68,6 +68,7 @@ Override the defaults on the plugin's entry in the profile's `cordis.patch.yml`:
|
|
|
68
68
|
maxDigestLines: 12 # lines of the injected digest
|
|
69
69
|
maxItemsPerList: 200 # per-list item cap
|
|
70
70
|
pruneDays: 14 # prune untouched sessions after N days (0 = never)
|
|
71
|
+
nudge: steer # steer | observe | off — see "Turn-end gate"
|
|
71
72
|
# storageRoot: "D:/data/dsh-checklists"
|
|
72
73
|
# templates:
|
|
73
74
|
# my-flow: ["Step 1", " Sub-step", "Step 2"]
|
|
@@ -113,8 +114,42 @@ the agent's edit.
|
|
|
113
114
|
|---|---|---|
|
|
114
115
|
| `/dsh-checklist/state` | GET | One session's state (read-only, `no-store`) |
|
|
115
116
|
| `/dsh-checklist/item` | POST | Edit an item's text, with a conflict guard |
|
|
117
|
+
| `/dsh-checklist/nudge` | GET | Recent turn-end gate decisions |
|
|
116
118
|
| `/dsh-checklist/client-probe` | GET | Client load diagnostics |
|
|
117
119
|
|
|
120
|
+
## Turn-end gate
|
|
121
|
+
|
|
122
|
+
A checklist only helps while it is true. The gate closes the most common gap:
|
|
123
|
+
a turn does real work, forgets to mark it, and the list is stale by the time you
|
|
124
|
+
look at it.
|
|
125
|
+
|
|
126
|
+
When a turn is about to close, the plugin checks exactly three things:
|
|
127
|
+
|
|
128
|
+
1. Did the turn run a tool that changes the workspace (`bash`, `edit`, `write`, …)?
|
|
129
|
+
2. Did it skip every checklist write?
|
|
130
|
+
3. Is anything still marked `doing`?
|
|
131
|
+
|
|
132
|
+
If all three hold, the agent gets one short steering message before the turn ends:
|
|
133
|
+
|
|
134
|
+
> Before you finish: N checklist item(s) are still marked "doing". If you
|
|
135
|
+
> completed any of them this turn, update them with checklist_check and say what
|
|
136
|
+
> proves it. If not, leave them as they are — do not mark work you did not do.
|
|
137
|
+
|
|
138
|
+
It never guesses **what** was completed — only that the bookkeeping was skipped.
|
|
139
|
+
At most one nudge per turn, and none when the turn was pure conversation or the
|
|
140
|
+
checklist already changed.
|
|
141
|
+
|
|
142
|
+
Inspect recent decisions at `GET /dsh-checklist/nudge`:
|
|
143
|
+
|
|
144
|
+
```jsonc
|
|
145
|
+
{ "mode": "steer", "entries": [
|
|
146
|
+
{ "source": "turn-stopping", "nudge": true, "steered": true,
|
|
147
|
+
"reason": "1 item(s) still doing after workspace changes", "doing": 1 }
|
|
148
|
+
] }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`nudge: observe` records without interrupting; `nudge: off` disables the gate.
|
|
152
|
+
|
|
118
153
|
## Language
|
|
119
154
|
|
|
120
155
|
The panel follows the harness locale: **Spanish** when `locale` starts with `es`,
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* The agent is the only writer. There is no client write path: the session
|
|
11
11
|
* document is the single source of truth.
|
|
12
12
|
*/
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
13
14
|
import { renderDigest } from './digest.js';
|
|
14
15
|
import { decideNudge, isChecklistWrite, MUTATING_TOOLS, NUDGE_ROUTE } from './gate.js';
|
|
15
16
|
import { countState } from './model.js';
|
|
@@ -19,6 +20,26 @@ import { ChecklistStore } from './store.js';
|
|
|
19
20
|
import { registerChecklistTools } from './tools.js';
|
|
20
21
|
export const name = 'dsh-checklist';
|
|
21
22
|
export const inject = ['tools'];
|
|
23
|
+
/**
|
|
24
|
+
* Build the steering message.
|
|
25
|
+
*
|
|
26
|
+
* Model-facing, so English. It states the observation, asks for evidence, and
|
|
27
|
+
* explicitly allows leaving an item untouched — a gate that demanded progress
|
|
28
|
+
* would push the model into marking work it never did.
|
|
29
|
+
*/
|
|
30
|
+
function buildSteerMessage(doing) {
|
|
31
|
+
return {
|
|
32
|
+
id: randomUUID(),
|
|
33
|
+
role: 'user',
|
|
34
|
+
content: [{
|
|
35
|
+
type: 'text',
|
|
36
|
+
text: `Before you finish: ${doing} checklist item(s) are still marked "doing". `
|
|
37
|
+
+ 'If you completed any of them this turn, update them with checklist_check and say what '
|
|
38
|
+
+ 'proves it. If not, leave them as they are — do not mark work you did not do.',
|
|
39
|
+
}],
|
|
40
|
+
source: { kind: 'user' },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
22
43
|
/**
|
|
23
44
|
* Resolve which session this prompt assembly belongs to.
|
|
24
45
|
*
|
|
@@ -46,11 +67,11 @@ export function apply(ctx, config = {}) {
|
|
|
46
67
|
if (config.pruneDays !== 0) {
|
|
47
68
|
void store.prune(config.pruneDays ?? 14).catch(() => undefined);
|
|
48
69
|
}
|
|
49
|
-
// --- Turn-end gate
|
|
50
|
-
// Watches one turn's tool activity and
|
|
51
|
-
// the checklist was left stale.
|
|
52
|
-
//
|
|
53
|
-
|
|
70
|
+
// --- Turn-end gate --------------------------------------------------------
|
|
71
|
+
// Watches one turn's tool activity and, as the turn closes, decides whether
|
|
72
|
+
// the checklist was left stale. In `steer` mode the agent gets a message
|
|
73
|
+
// before the turn is allowed to end; `observe` only records the decision.
|
|
74
|
+
const nudgeMode = config.nudge ?? 'steer';
|
|
54
75
|
const activity = new Map();
|
|
55
76
|
const gateLog = [];
|
|
56
77
|
const events = ctx;
|
|
@@ -69,7 +90,8 @@ export function apply(ctx, config = {}) {
|
|
|
69
90
|
current.checklistWrite = true;
|
|
70
91
|
activity.set(agentId, current);
|
|
71
92
|
}));
|
|
72
|
-
const evaluate = (
|
|
93
|
+
const evaluate = (agent, source) => {
|
|
94
|
+
const agentId = agent.id;
|
|
73
95
|
const current = activity.get(agentId) ?? { mutated: false, checklistWrite: false };
|
|
74
96
|
activity.set(agentId, { mutated: false, checklistWrite: false });
|
|
75
97
|
const doc = store.peek(agentId);
|
|
@@ -78,34 +100,55 @@ export function apply(ctx, config = {}) {
|
|
|
78
100
|
for (const list of doc.lists)
|
|
79
101
|
doing += countState(list.items, 'doing');
|
|
80
102
|
const decision = decideNudge(current, doing);
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
let steered = false;
|
|
104
|
+
let reason = decision.reason;
|
|
105
|
+
if (decision.nudge && nudgeMode === 'steer') {
|
|
106
|
+
try {
|
|
107
|
+
agent.steer?.(buildSteerMessage(doing));
|
|
108
|
+
steered = true;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
reason = `${reason}; steer failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Log every decision: an empty log must mean "the event never fired",
|
|
115
|
+
// never "the answer was no".
|
|
83
116
|
gateLog.push({
|
|
84
117
|
at: new Date().toISOString(),
|
|
85
118
|
sessionId: agentId,
|
|
86
119
|
source,
|
|
120
|
+
mode: nudgeMode,
|
|
87
121
|
nudge: decision.nudge,
|
|
88
|
-
|
|
122
|
+
steered,
|
|
123
|
+
reason,
|
|
89
124
|
doing,
|
|
90
125
|
});
|
|
91
126
|
if (gateLog.length > 20)
|
|
92
127
|
gateLog.shift();
|
|
93
128
|
};
|
|
94
129
|
ctx.effect(() => events.on('agent/turn-stopping', (payload) => {
|
|
130
|
+
if (nudgeMode === 'off')
|
|
131
|
+
return;
|
|
95
132
|
const event = payload;
|
|
96
133
|
if (typeof event.agent?.id === 'string')
|
|
97
|
-
evaluate(event.agent
|
|
134
|
+
evaluate(event.agent, 'turn-stopping');
|
|
98
135
|
}));
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// root listener.
|
|
136
|
+
// Fallback close signal. It evaluates only when `turn-stopping` left activity
|
|
137
|
+
// unconsumed, so it never duplicates a decision — but it still catches the
|
|
138
|
+
// turn if the primary event is ever missing.
|
|
103
139
|
ctx.effect(() => events.on('agent/status', (payload) => {
|
|
140
|
+
if (nudgeMode === 'off')
|
|
141
|
+
return;
|
|
104
142
|
const event = payload;
|
|
105
143
|
if (event.status !== 'idle')
|
|
106
144
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
const agent = event.agent;
|
|
146
|
+
if (!agent || typeof agent.id !== 'string')
|
|
147
|
+
return;
|
|
148
|
+
const current = activity.get(agent.id);
|
|
149
|
+
if (!current || (!current.mutated && !current.checklistWrite))
|
|
150
|
+
return;
|
|
151
|
+
evaluate(agent, 'status');
|
|
109
152
|
}));
|
|
110
153
|
let loggedScope = false;
|
|
111
154
|
ctx.inject(['systemPrompt'], (promptCtx) => {
|
|
@@ -161,7 +204,7 @@ export function apply(ctx, config = {}) {
|
|
|
161
204
|
kind: 'exact',
|
|
162
205
|
path: NUDGE_ROUTE,
|
|
163
206
|
handler: (req, res) => {
|
|
164
|
-
const body = Buffer.from(`${JSON.stringify({ mode:
|
|
207
|
+
const body = Buffer.from(`${JSON.stringify({ mode: nudgeMode, entries: gateLog }, null, 2)}\n`, 'utf8');
|
|
165
208
|
res.writeHead(200, {
|
|
166
209
|
'content-type': 'application/json; charset=utf-8',
|
|
167
210
|
'content-length': String(body.byteLength),
|
package/lib/types/index.d.ts
CHANGED
|
@@ -24,5 +24,12 @@ export interface Config {
|
|
|
24
24
|
pruneDays?: number;
|
|
25
25
|
/** Extra templates merged over the built-in ones. */
|
|
26
26
|
templates?: Record<string, string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* What the turn-end gate does when it finds a stale checklist.
|
|
29
|
+
* - `steer` (default): put a message in front of the agent before the turn ends.
|
|
30
|
+
* - `observe`: record the decision, never interrupt.
|
|
31
|
+
* - `off`: do not evaluate at all.
|
|
32
|
+
*/
|
|
33
|
+
nudge?: 'steer' | 'observe' | 'off';
|
|
27
34
|
}
|
|
28
35
|
export declare function apply(ctx: Context, config?: Config): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-checklists",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|