dsh-root-planner-preset 1.1.2 → 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 +53 -21
- 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,8 +39,9 @@ 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
|
-
|
|
43
|
-
const GH = process.env.DSH_RELEASE_GH ?? 'gh';
|
|
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';
|
|
44
|
+
const PUBLISHER = process.env.DSH_RELEASE_PUBLISHER ?? (process.platform === 'win32' ? 'pnpm' : 'npm');
|
|
44
45
|
|
|
45
46
|
const argv = process.argv.slice(2);
|
|
46
47
|
const dryRun = argv.includes('--dry-run');
|
|
@@ -62,12 +63,15 @@ if (bump === undefined) {
|
|
|
62
63
|
|
|
63
64
|
const env = { ...process.env, HTTPS_PROXY: PROXY, HTTP_PROXY: PROXY, GIT_TERMINAL_PROMPT: '0' };
|
|
64
65
|
const step = (text) => process.stdout.write(`\n\u25b6 ${text}\n`);
|
|
65
|
-
const run = (command, args,
|
|
66
|
-
const result = spawnSync(command, args, { cwd: here, env, encoding: 'utf8', ...
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const run = (command, args, { allowFailure = false, shell = false, ...rest } = {}) => {
|
|
67
|
+
const result = spawnSync(command, args, { cwd: here, env, encoding: 'utf8', shell, ...rest });
|
|
68
|
+
// A spawn that never started reports status null and puts the reason in `error`;
|
|
69
|
+
// surfacing that by name is what turns a silent no-op into a diagnosable failure.
|
|
70
|
+
if (result.status !== 0 && !allowFailure) {
|
|
71
|
+
const reason = result.error ? `${result.error.code}: ${result.error.message}` : `exit ${result.status}`;
|
|
72
|
+
throw new Error(`${command} ${args.join(' ')} failed (${reason})\n${result.stdout ?? ''}${result.stderr ?? ''}`);
|
|
69
73
|
}
|
|
70
|
-
return `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
74
|
+
return `${result.stdout ?? ''}${result.stderr ?? ''}${result.error ? `\n${result.error.code}: ${result.error.message}` : ''}`;
|
|
71
75
|
};
|
|
72
76
|
const git = (...args) => run(GIT, args).trim();
|
|
73
77
|
|
|
@@ -149,12 +153,28 @@ if (dryRun) {
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
// ── Publish ──────────────────────────────────────────────────────────────────
|
|
152
|
-
step(`Publish to the official registry (the configured one is a read-only mirror)`);
|
|
156
|
+
step(`Publish with ${PUBLISHER} to the official registry (the configured one is a read-only mirror)`);
|
|
153
157
|
if (dryRun) {
|
|
154
|
-
process.stdout.write(` --dry-run: would run
|
|
158
|
+
process.stdout.write(` --dry-run: would run ${PUBLISHER} publish --registry ${OFFICIAL_REGISTRY}\n`);
|
|
155
159
|
} else {
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
// Two Windows facts force the shape of this call, both learned by watching a
|
|
161
|
+
// publish silently do nothing:
|
|
162
|
+
// * `npm` and `pnpm` are shell shims, not executables — `npm` is an sh script
|
|
163
|
+
// (ENOENT), `npm.cmd`/`pnpm.cmd` refuse to spawn without a shell (EINVAL),
|
|
164
|
+
// and `npm.ps1` is PowerShell (EFTYPE). So the publisher is invoked WITH a
|
|
165
|
+
// shell, which is why every argument below must stay shell-safe.
|
|
166
|
+
// * The output must be shown even on failure. The first version filtered it
|
|
167
|
+
// down to matching lines and printed a blank line when the command never
|
|
168
|
+
// ran, hiding the real problem behind a successful-looking step.
|
|
169
|
+
const published = run(PUBLISHER, ['publish', '--registry', OFFICIAL_REGISTRY, '--access', 'public'], {
|
|
170
|
+
allowFailure: true,
|
|
171
|
+
shell: true,
|
|
172
|
+
});
|
|
173
|
+
const summary = published
|
|
174
|
+
.split('\n')
|
|
175
|
+
.map((line) => line.trim())
|
|
176
|
+
.filter((line) => line.length > 0 && !/^Progress:|^Packages:|^\+?$/.test(line));
|
|
177
|
+
process.stdout.write(`${summary.map((line) => ` ${line}`).join('\n') || ' (the publisher printed nothing — treat that as a failure)'}\n`);
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
// ── Confirm on the registry ──────────────────────────────────────────────────
|
|
@@ -170,22 +190,26 @@ if (dryRun) {
|
|
|
170
190
|
encoding: 'utf8',
|
|
171
191
|
}).stdout?.trim();
|
|
172
192
|
|
|
193
|
+
// Measured on this account: 1.1.2 was still 404 at 200s and live shortly after,
|
|
194
|
+
// so the window has to be generous or a successful release reads as a failure.
|
|
173
195
|
let live = false;
|
|
174
|
-
for (let attempt = 1; attempt <=
|
|
196
|
+
for (let attempt = 1; attempt <= 60 && !live; attempt += 1) {
|
|
175
197
|
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
176
198
|
const code = probe(`${OFFICIAL_REGISTRY}${manifest.name}/${version}`);
|
|
177
199
|
if (code === '200') {
|
|
178
200
|
live = true;
|
|
179
201
|
process.stdout.write(` ${manifest.name}@${version} is live on the registry\n`);
|
|
180
|
-
} else if (attempt %
|
|
202
|
+
} else if (attempt % 6 === 0) {
|
|
181
203
|
process.stdout.write(` still not visible after ${attempt * 10}s (last HTTP ${code})\n`);
|
|
182
204
|
}
|
|
183
205
|
}
|
|
184
206
|
if (!live) {
|
|
185
207
|
process.stderr.write(
|
|
186
|
-
'\n NOT VISIBLE
|
|
187
|
-
'
|
|
188
|
-
'
|
|
208
|
+
'\n NOT VISIBLE after 10 minutes. Two known causes, in order of likelihood:\n' +
|
|
209
|
+
' 1. propagation lag — this registry answered 404 for over three minutes on a\n' +
|
|
210
|
+
' publish that had already succeeded, so re-check before concluding anything;\n' +
|
|
211
|
+
' 2. staged publishing — check npmjs.com for a pending approval. `npm stage list`\n' +
|
|
212
|
+
' reports nothing even when a version is held, so it is not the test.\n',
|
|
189
213
|
);
|
|
190
214
|
process.exit(1);
|
|
191
215
|
}
|
|
@@ -196,10 +220,14 @@ step(`Upgrade the local "${profile}" profile so the machine runs what was publis
|
|
|
196
220
|
if (dryRun || skipInstall) {
|
|
197
221
|
process.stdout.write(' skipped\n');
|
|
198
222
|
} else {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
223
|
+
// `dsh` is a shell shim on Windows too, so it is invoked through Node rather
|
|
224
|
+
// than spawned directly. DSH_RELEASE_DSH overrides the entry point.
|
|
225
|
+
const dshEntry = process.env.DSH_RELEASE_DSH ?? 'D:/nodejs/npm_global/node_modules/@deepseek-ai/dsh/lib/bin.js';
|
|
226
|
+
const output = run(
|
|
227
|
+
process.execPath,
|
|
228
|
+
[dshEntry, 'plugin', '--profile', profile, 'add', `${manifest.name}@${version}`, '--registry', OFFICIAL_REGISTRY],
|
|
229
|
+
{ allowFailure: true },
|
|
230
|
+
);
|
|
203
231
|
const installed = (() => {
|
|
204
232
|
try {
|
|
205
233
|
return JSON.parse(
|
|
@@ -212,7 +240,11 @@ if (dryRun || skipInstall) {
|
|
|
212
240
|
if (installed === version) {
|
|
213
241
|
process.stdout.write(` profile now runs ${manifest.name}@${installed}\n`);
|
|
214
242
|
} else {
|
|
215
|
-
process.stdout.write(
|
|
243
|
+
process.stdout.write(
|
|
244
|
+
` the profile reports ${installed ?? 'unknown'} rather than ${version} — install it by hand:\n` +
|
|
245
|
+
` dsh plugin --profile ${profile} add ${manifest.name}@${version} --registry ${OFFICIAL_REGISTRY}\n` +
|
|
246
|
+
`${output.trim().split('\n').slice(-6).map((line) => ` ${line}`).join('\n')}\n`,
|
|
247
|
+
);
|
|
216
248
|
}
|
|
217
249
|
}
|
|
218
250
|
|
|
@@ -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
|
|