dsh-root-planner-preset 1.1.3 → 1.1.4
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/child-tools.mjs +19 -7
- package/package.json +1 -1
- package/release.mjs +2 -1
- package/tools/inspect-preset-events.mjs +85 -0
- package/tools/verify-session.mjs +54 -7
package/child-tools.mjs
CHANGED
|
@@ -63,13 +63,25 @@ export const CHILD_TOOLS = [
|
|
|
63
63
|
// Running commands. `pwsh` is the one shell this profile reports as a global
|
|
64
64
|
// tool, and background jobs are switched off for it in the preset (see above).
|
|
65
65
|
//
|
|
66
|
-
// `ops_bash` is deliberately NOT here
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
66
|
+
// `ops_bash` is deliberately NOT here, and it is not an oversight — it CANNOT be
|
|
67
|
+
// here, for the same reason `subagent` cannot:
|
|
68
|
+
//
|
|
69
|
+
// * dsh-ops publishes it per AGENT, not globally. `lib/session-shells.js`
|
|
70
|
+
// mounts a child plugin on each `agent.ctx` and registers the tool there
|
|
71
|
+
// (`entry.ctx.tools.register(definition)`), then walks the agents again to
|
|
72
|
+
// `tools.restrict({ deny: [BASH_TOOL] })` on the ones it may not serve. So it
|
|
73
|
+
// never appears in the runtime's global inventory, and naming it in a mask
|
|
74
|
+
// fails child creation the way `subagent` did.
|
|
75
|
+
// * It cannot be granted because it does not need granting. `allowed(agent)`
|
|
76
|
+
// tests the agent's OWN session policy — full access, or a confinement
|
|
77
|
+
// provider available — and a child qualifies on the same terms as the root,
|
|
78
|
+
// so a child receives `ops_bash` from dsh-ops by itself. That project's own
|
|
79
|
+
// manual-validation notes say it plainly: a restricted session does not
|
|
80
|
+
// publish `ops_bash`; it appears only with full access, `publishBashTool`,
|
|
81
|
+
// a resolvable bash payload and an available subprocess service.
|
|
82
|
+
//
|
|
83
|
+
// The consequence worth knowing: a child's shell can differ from the root's
|
|
84
|
+
// because of its own permission mode, not because of anything this mask says.
|
|
73
85
|
'pwsh',
|
|
74
86
|
'todo_write',
|
|
75
87
|
'ask_user_question',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-root-planner-preset",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH agent preset that outsources file reading to strictly read-only subagents, while the root agent keeps the plan and owns every file mutation.",
|
|
6
6
|
"license": "MIT",
|
package/release.mjs
CHANGED
|
@@ -39,7 +39,8 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
39
39
|
const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
|
|
40
40
|
const PROXY = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? 'http://127.0.0.1:7897';
|
|
41
41
|
const GIT = process.env.DSH_RELEASE_GIT ?? 'git';
|
|
42
|
-
|
|
42
|
+
// `gh` is not installed on PATH on this machine; it lives in a standalone extract.
|
|
43
|
+
const GH = process.env.DSH_RELEASE_GH ?? 'D:/Tools/gh/2.102.0/bin/gh.exe';
|
|
43
44
|
const PUBLISHER = process.env.DSH_RELEASE_PUBLISHER ?? (process.platform === 'win32' ? 'pnpm' : 'npm');
|
|
44
45
|
|
|
45
46
|
const argv = process.argv.slice(2);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Reports the preset-related events in one session log — the durable evidence of
|
|
2
|
+
// which preset a session was composed with, and whether a selection was attempted.
|
|
3
|
+
//
|
|
4
|
+
// Usage: node tools/inspect-preset-events.mjs <session-dir>
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
9
|
+
|
|
10
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
11
|
+
function firstFrameEnd(buf) {
|
|
12
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
13
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
14
|
+
}
|
|
15
|
+
return buf.length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
19
|
+
async function decompressAll(buf) {
|
|
20
|
+
let rest = buf;
|
|
21
|
+
let text = '';
|
|
22
|
+
while (rest.length > 0) {
|
|
23
|
+
const slice = rest.subarray(0, firstFrameEnd(rest));
|
|
24
|
+
try {
|
|
25
|
+
text += await new Promise((resolve, reject) => {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
const dec = createZstdDecompress();
|
|
28
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
29
|
+
dec.on('error', reject);
|
|
30
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
31
|
+
dec.end(slice);
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (slice.length >= rest.length) break;
|
|
37
|
+
rest = rest.subarray(slice.length);
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dir = process.argv[2];
|
|
43
|
+
if (dir === undefined) {
|
|
44
|
+
process.stderr.write('usage: node tools/inspect-preset-events.mjs <session-dir>\n');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
49
|
+
const events = text
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter((line) => line.trim().length > 0)
|
|
52
|
+
.flatMap((line) => {
|
|
53
|
+
try {
|
|
54
|
+
return [JSON.parse(line)];
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
process.stdout.write(`${events.length} events, ${text.length} chars\n\n`);
|
|
61
|
+
|
|
62
|
+
const header = events[0];
|
|
63
|
+
process.stdout.write(`header: id=${header?.id}\n agentPreset=${header?.agentPreset}\n origin=${header?.origin} depth=${header?.delegationDepth}\n`);
|
|
64
|
+
|
|
65
|
+
process.stdout.write('\npreset-related events:\n');
|
|
66
|
+
let found = 0;
|
|
67
|
+
for (const event of events) {
|
|
68
|
+
const type = String(event.type ?? '');
|
|
69
|
+
if (!/preset/i.test(type)) continue;
|
|
70
|
+
found += 1;
|
|
71
|
+
process.stdout.write(` #${events.indexOf(event)} ${type} ${JSON.stringify(event.data ?? event).slice(0, 220)}\n`);
|
|
72
|
+
}
|
|
73
|
+
if (found === 0) process.stdout.write(' (none — no agent-preset/selected event was ever appended)\n');
|
|
74
|
+
|
|
75
|
+
process.stdout.write('\nuser messages (first 3):\n');
|
|
76
|
+
let users = 0;
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
const content = event.data?.message?.content ?? event.message?.content;
|
|
79
|
+
const role = event.data?.message?.role ?? event.message?.role;
|
|
80
|
+
if (role !== 'user' || typeof content !== 'string') continue;
|
|
81
|
+
users += 1;
|
|
82
|
+
if (users > 3) break;
|
|
83
|
+
process.stdout.write(` #${events.indexOf(event)} ${JSON.stringify(content.slice(0, 160))}\n`);
|
|
84
|
+
}
|
|
85
|
+
if (users === 0) process.stdout.write(' (no user messages recorded yet)\n');
|
package/tools/verify-session.mjs
CHANGED
|
@@ -58,7 +58,7 @@ async function decompressAll(buf) {
|
|
|
58
58
|
return text;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** Read one session directory into {header, events}. */
|
|
61
|
+
/** Read one session directory into {header, events, text}. */
|
|
62
62
|
async function readSession(dir) {
|
|
63
63
|
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
64
64
|
const events = text
|
|
@@ -71,9 +71,17 @@ async function readSession(dir) {
|
|
|
71
71
|
return [];
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
-
return { header: events[0], events };
|
|
74
|
+
return { header: events[0], events, text };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A phrase only this preset's persona contains, so finding it in a session proves
|
|
79
|
+
* that session was composed with root-planner. Chosen against the alternative of
|
|
80
|
+
* matching "You are a delegated subagent", which DSH injects into EVERY subagent's
|
|
81
|
+
* runtime context and therefore proves nothing.
|
|
82
|
+
*/
|
|
83
|
+
const PERSONA_MARKER = 'the same rules govern every node';
|
|
84
|
+
|
|
77
85
|
const argv = process.argv.slice(2);
|
|
78
86
|
const quiet = argv.includes('--quiet');
|
|
79
87
|
const expectDepthIndex = argv.indexOf('--expect-depth');
|
|
@@ -139,11 +147,48 @@ const check = (ok, label) => {
|
|
|
139
147
|
|
|
140
148
|
if (!quiet) process.stdout.write(`root session: ${root.header?.id}\n`);
|
|
141
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Which preset a session is ACTUALLY running.
|
|
152
|
+
*
|
|
153
|
+
* The session header's `agentPreset` is written when the session is created, with
|
|
154
|
+
* the default preset, and the client then switches preset through
|
|
155
|
+
* `agentPresets.select()` — which appends an `agent-preset/selected` event and
|
|
156
|
+
* recomposes the agent, but never rewrites the header. Judging a run by the header
|
|
157
|
+
* reports "standard" for a session that is genuinely running something else, and
|
|
158
|
+
* this tool made exactly that mistake twice before the prompt text settled it.
|
|
159
|
+
*
|
|
160
|
+
* Identity is therefore taken from the strongest available evidence, in order:
|
|
161
|
+
* 1. this preset's persona prose appearing in the session's own content — only a
|
|
162
|
+
* session composed with root-planner can contain it;
|
|
163
|
+
* 2. the recorded `agent-preset/selected` event;
|
|
164
|
+
* 3. the header, which is the creation default and the weakest of the three.
|
|
165
|
+
* @param session - one read session.
|
|
166
|
+
* @returns the resolved preset id and what it was resolved from.
|
|
167
|
+
*/
|
|
168
|
+
function resolvePreset(session) {
|
|
169
|
+
if ((session.text ?? '').includes(PERSONA_MARKER)) {
|
|
170
|
+
return { preset: 'root-planner', evidence: 'persona text in the session' };
|
|
171
|
+
}
|
|
172
|
+
for (const event of session.events) {
|
|
173
|
+
if (event.type === 'agent-preset/selected' && typeof event.data?.agentPreset === 'string') {
|
|
174
|
+
return { preset: event.data.agentPreset, evidence: 'agent-preset/selected event' };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { preset: session.header?.agentPreset, evidence: 'session header (creation default)' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rootPreset = resolvePreset(root);
|
|
142
181
|
check(
|
|
143
|
-
|
|
144
|
-
`root session
|
|
182
|
+
rootPreset.preset === 'root-planner',
|
|
183
|
+
`root session runs root-planner (found "${rootPreset.preset}" via ${rootPreset.evidence})`,
|
|
145
184
|
);
|
|
146
185
|
check(root.header?.delegationDepth === 0, `root is at depth 0 (found ${root.header?.delegationDepth})`);
|
|
186
|
+
if (!quiet && root.header?.agentPreset !== rootPreset.preset) {
|
|
187
|
+
process.stdout.write(
|
|
188
|
+
`note: the header says "${root.header?.agentPreset}" while the session runs "${rootPreset.preset}" ` +
|
|
189
|
+
`(${rootPreset.evidence}) — the header is the creation default, not the running preset\n`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
147
192
|
|
|
148
193
|
const children = sessions.filter((session) => session.header?.parentSession === root.header?.id);
|
|
149
194
|
const byId = new Map(sessions.map((session) => [session.header?.id, session]));
|
|
@@ -160,8 +205,9 @@ const depthOf = (session) => {
|
|
|
160
205
|
if (!quiet) {
|
|
161
206
|
process.stdout.write(`\n${sessions.length} session(s) total:\n`);
|
|
162
207
|
for (const session of sessions) {
|
|
208
|
+
const preset = resolvePreset(session);
|
|
163
209
|
process.stdout.write(
|
|
164
|
-
` ${String(session.header?.id).slice(0, 20)} preset=${String(
|
|
210
|
+
` ${String(session.header?.id).slice(0, 20)} preset=${String(preset.preset).padEnd(14)} ` +
|
|
165
211
|
`depth=${depthOf(session)} parent=${String(session.header?.parentSession ?? '(root)').slice(0, 20)}\n`,
|
|
166
212
|
);
|
|
167
213
|
}
|
|
@@ -173,9 +219,10 @@ check(descendants.length > 0, 'the root actually delegated (at least one child s
|
|
|
173
219
|
|
|
174
220
|
for (const session of descendants) {
|
|
175
221
|
const depth = depthOf(session);
|
|
222
|
+
const preset = resolvePreset(session);
|
|
176
223
|
check(
|
|
177
|
-
|
|
178
|
-
`child ${String(session.header?.id).slice(0, 8)} at depth ${depth}
|
|
224
|
+
preset.preset === 'root-planner',
|
|
225
|
+
`child ${String(session.header?.id).slice(0, 8)} at depth ${depth} runs root-planner (found "${preset.preset}" via ${preset.evidence})`,
|
|
179
226
|
);
|
|
180
227
|
}
|
|
181
228
|
|