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 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 even though it looks like the natural
67
- // choice on Windows: the runtime's own tool inventory — the list
68
- // `tools.restrict()` prints when it refuses a name — includes
69
- // ops_glob/ops_grep/ops_inspect_local_file but NOT ops_bash, so naming it breaks
70
- // every child creation exactly the way `subagent` did. The dsh-ops shell
71
- // component publishes it through a different path than its file component. Add
72
- // it only after seeing it in your own environment's inventory line.
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.2",
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
- const NPM = process.env.DSH_RELEASE_NPM ?? 'npm';
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, options = {}) => {
66
- const result = spawnSync(command, args, { cwd: here, env, encoding: 'utf8', ...options });
67
- if (result.status !== 0 && options.allowFailure !== true) {
68
- throw new Error(`${command} ${args.join(' ')} failed (${result.status})\n${result.stdout ?? ''}${result.stderr ?? ''}`);
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 npm publish --registry ${OFFICIAL_REGISTRY}\n`);
158
+ process.stdout.write(` --dry-run: would run ${PUBLISHER} publish --registry ${OFFICIAL_REGISTRY}\n`);
155
159
  } else {
156
- const published = run(NPM, ['publish', '--registry', OFFICIAL_REGISTRY, '--access', 'public'], { allowFailure: true });
157
- process.stdout.write(` ${published.split('\n').filter((line) => /\+ |error|Publishing/.test(line)).join('\n ') || '(no output)'}\n`);
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 <= 20 && !live; attempt += 1) {
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 % 5 === 0) {
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. On this account publishing is STAGED: check for a pending\n' +
187
- ' approval on npmjs.com — `npm stage list` can report nothing while the\n' +
188
- ' registry still refuses the version.\n',
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
- const spec = `${manifest.name}@${version}`;
200
- const output = run('dsh', ['plugin', '--profile', profile, 'add', spec, '--registry', OFFICIAL_REGISTRY], {
201
- allowFailure: true,
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(` install reported ${installed ?? 'unknown'} — run it by hand if this matters:\n${output.trim()}\n`);
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');
@@ -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
- root.header?.agentPreset === 'root-planner',
144
- `root session is composed with root-planner (found "${root.header?.agentPreset ?? '?'}") — if this fails, the run proves nothing about the preset`,
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(session.header?.agentPreset).padEnd(14)} ` +
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
- session.header?.agentPreset === 'root-planner',
178
- `child ${String(session.header?.id).slice(0, 8)} at depth ${depth} inherits root-planner (found "${session.header?.agentPreset ?? '?'}")`,
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