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 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.3",
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
- 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';
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');
@@ -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