dsh-root-planner-preset 1.1.1 → 1.1.3
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/.github/CONTRIBUTING.md +30 -0
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/DEVELOPMENT.md +3 -1
- package/package.json +2 -2
- package/release.mjs +268 -0
- package/tools/inspect-sessions.mjs +0 -71
package/.github/CONTRIBUTING.md
CHANGED
|
@@ -82,6 +82,36 @@ script prints `SKIP` and explains why. That is a pass, not a failure.
|
|
|
82
82
|
- Keep `main` linear — the branch protection refuses merge commits, so rebase
|
|
83
83
|
rather than merge.
|
|
84
84
|
|
|
85
|
+
## Releasing
|
|
86
|
+
|
|
87
|
+
`node release.mjs <patch|minor|major|X.Y.Z>` does the whole sequence. Every step in
|
|
88
|
+
it exists because the manual version went wrong at least once:
|
|
89
|
+
|
|
90
|
+
1. regenerates `cordis.patch.yml` and refuses to continue unless
|
|
91
|
+
`verify-patch.mjs` passes;
|
|
92
|
+
2. bumps `package.json` and commits with a local-time stamp;
|
|
93
|
+
3. pushes (the owner bypasses branch protection; a collaborator's push is refused,
|
|
94
|
+
so open a pull request instead);
|
|
95
|
+
4. publishes to `https://registry.npmjs.org/` **explicitly** — the configured
|
|
96
|
+
registry is a read-only mirror that also lags behind, which is how an install
|
|
97
|
+
right after a release fails with *No matching version found*;
|
|
98
|
+
5. **polls the registry for the exact version** rather than trusting the exit code,
|
|
99
|
+
because publishing on this account is staged and `npm publish` can print success
|
|
100
|
+
without the version ever appearing;
|
|
101
|
+
6. upgrades the local profile so the machine runs what was published.
|
|
102
|
+
|
|
103
|
+
`--dry-run` walks the whole thing without changing anything; `--skip-install` and
|
|
104
|
+
`--skip-gh` drop the last two steps.
|
|
105
|
+
|
|
106
|
+
Run it from a terminal. Inside DSH's own sandbox its `spawn` calls fail with
|
|
107
|
+
`EPERM` on Windows — that is the sandbox's piped-stdio boundary, not a defect in
|
|
108
|
+
the script.
|
|
109
|
+
|
|
110
|
+
**A release is not a verification.** The script can put a version on npm while the
|
|
111
|
+
preset stays unproven. Restart `dsh web`, open a new session on Root Planner, and
|
|
112
|
+
confirm the *root* actually composed with it — `node tools/verify-session.mjs
|
|
113
|
+
<session-dir>` — before believing anything about behaviour.
|
|
114
|
+
|
|
85
115
|
## How `main` is protected
|
|
86
116
|
|
|
87
117
|
`main` requires the `verify` status check (strict: the branch must be up to date),
|
package/INSTALL.md
CHANGED
|
@@ -114,7 +114,7 @@ git clone https://github.com/XiaoOu2024/dsh-root-planner-preset
|
|
|
114
114
|
| 委派立刻返回**深度错误** | `maxDepth` 没设或设得太小 | 见第三节,设为 `2` |
|
|
115
115
|
| 子代理**什么都不能读** | 白名单过窄(比如你删过头了) | 至少保留 `read`、`glob`、`grep`、`ops_inspect_local_file`、`ops_glob`、`ops_grep` |
|
|
116
116
|
| 子代理**还是能改文件** | 根会话没选中 Root Planner(见第四节),或白名单被改坏了 | 先查会话文件的 `agentPreset` 字段是否 `root-planner`;再确认 `cordis.patch.yml` 里两处 `toolFilter.allow` 都**不含** `write`、`edit`、`ops_replace`、`ops_bash`、`pwsh`。改完必须重开会话 |
|
|
117
|
-
| 行为与人设描述**对不上**(比如"子代理能写"、"规则没生效") | 最常见的原因就是**根会话跑在别的预设上**,而它的子代理跑在 root-planner 上——两者的规则和权限本来就不一样 |
|
|
117
|
+
| 行为与人设描述**对不上**(比如"子代理能写"、"规则没生效") | 最常见的原因就是**根会话跑在别的预设上**,而它的子代理跑在 root-planner 上——两者的规则和权限本来就不一样 | 跑 `node tools/verify-session.mjs <会话目录>`,它会判定根与全部子会话的预设、深度与父子关系 |
|
|
118
118
|
| 改了 `persona.txt` 但没生效 | 会话与子代理都锁定启动时的插件版本 | 开**新会话**;必要时把 `preset-root-planner` 在插件列表里关掉再打开 |
|
|
119
119
|
| Windows 上校验报**人设不一致** | 换行符被 Git 改写了 | 确认 `.gitattributes` 存在且含 `* text=auto eol=lf`,重新克隆一次 |
|
|
120
120
|
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Then **one required step**: set **Settings → Subagents → depth limit** to **
|
|
|
78
78
|
|
|
79
79
|
Then **start a new session** and pick **Root Planner** in the preset selector. Full steps and troubleshooting are in [INSTALL.md](INSTALL.md).
|
|
80
80
|
|
|
81
|
-
> ⚠️ **The easiest mistake to make: the root session must actually be composed with Root Planner.** In one real trial the root session was still `standard`, so the root was bound by none of this preset while the subagents it spawned *were* `root-planner` — every rule and permission in the tree was mismatched, and a batch of confident conclusions about the preset followed from it. **To check:**
|
|
81
|
+
> ⚠️ **The easiest mistake to make: the root session must actually be composed with Root Planner.** In one real trial the root session was still `standard`, so the root was bound by none of this preset while the subagents it spawned *were* `root-planner` — every rule and permission in the tree was mismatched, and a batch of confident conclusions about the preset followed from it. **To check:** run `node tools/verify-session.mjs <session-dir>`, which reports the preset and depth of the root and every descendant, and fails loudly when the root is not `root-planner`.
|
|
82
82
|
|
|
83
83
|
## When it fits, and when it does not
|
|
84
84
|
|
package/README.zh.md
CHANGED
|
@@ -66,7 +66,7 @@ dsh plugin --profile web add github:XiaoOu2024/dsh-root-planner-preset
|
|
|
66
66
|
|
|
67
67
|
然后**开新会话**,在预设选择器里选 **Root Planner**。完整步骤与故障排查见 [INSTALL.md](INSTALL.md)。
|
|
68
68
|
|
|
69
|
-
> ⚠️ **最容易踩的坑:根会话必须真的选中 Root Planner。** 有人实测时根会话仍是 `standard`,于是根不受本预设约束、而它派出的子代理却是 `root-planner
|
|
69
|
+
> ⚠️ **最容易踩的坑:根会话必须真的选中 Root Planner。** 有人实测时根会话仍是 `standard`,于是根不受本预设约束、而它派出的子代理却是 `root-planner`——权限与规则整棵树错位,最后据此得出一批关于本预设的错误结论。**确认方法**:跑 `node tools/verify-session.mjs <会话目录>`,它会报出根与全部子会话的预设与深度,**根不是 root-planner 就直接判 FAIL**。
|
|
70
70
|
|
|
71
71
|
## 适合 / 不适合
|
|
72
72
|
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -27,7 +27,9 @@ Paths below are relative to the repository root; this file lives in `docs/`.
|
|
|
27
27
|
| `lib-yaml.mjs` | Resolves a YAML parser from the local DSH installation, so the scripts need no dependencies. |
|
|
28
28
|
| `verify-patch.mjs` | Parses the generated patch and checks persona integrity, role wiring, and the child mask. |
|
|
29
29
|
| `child-tools.mjs` | The child mask, defined once so the generator and the verifier cannot disagree. |
|
|
30
|
-
| `
|
|
30
|
+
| `release.mjs` | One-command release: regenerate, verify, bump, commit, push, publish to the **official** registry, confirm the version is live, upgrade the local profile. `--dry-run` changes nothing. Run it from a terminal, not from inside the DSH sandbox, whose piped-stdio boundary makes its `spawn` calls fail with `EPERM` on Windows. |
|
|
31
|
+
| `tools/verify-session.mjs` | Reads a real run's `.jsonl.zstd` logs (frame by frame — one decompress call returns 323 bytes of a 140 KB log) and checks that the **root** and every descendant composed with root-planner, at the expected depth. |
|
|
32
|
+
| `tools/extract-tool-inventory.mjs` | Pulls the runtime's own `known global tools:` line out of a log. That line, not memory, is what the child mask must be a subset of. |
|
|
31
33
|
| `verify-depth-override.mjs` | Checks that the profile patch layer still parses and that the `subagent` depth override landed in the composed profile. |
|
|
32
34
|
| `package.json` | Bundle manifest (`dsh.bundle.patch`). |
|
|
33
35
|
| `.gitattributes` | Pins LF; without it a Windows checkout breaks the byte-identity check. |
|
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.3",
|
|
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",
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"js-yaml": "^4.1.0"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
package/release.mjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// One-command release for this bundle.
|
|
2
|
+
//
|
|
3
|
+
// It exists because the manual sequence has four places that bite, all of them
|
|
4
|
+
// discovered the hard way in this repository:
|
|
5
|
+
//
|
|
6
|
+
// 1. `cordis.patch.yml` is GENERATED. Editing it by hand, or forgetting to
|
|
7
|
+
// regenerate after a persona edit, is the most common mistake — and CI only
|
|
8
|
+
// catches it after the push.
|
|
9
|
+
// 2. The configured registry is registry.npmmirror.com, a read-only MIRROR. A
|
|
10
|
+
// plain `npm publish` goes to the wrong place, and the mirror can lag the
|
|
11
|
+
// official registry by hours, so an install right after a release fails with
|
|
12
|
+
// "No matching version found".
|
|
13
|
+
// 3. Publishing this account goes through npm's STAGED publishing. `npm publish`
|
|
14
|
+
// can print a success line while the version never reaches the registry, so
|
|
15
|
+
// the exit code proves nothing. The only reliable check is asking the registry
|
|
16
|
+
// for the exact version, with retries: propagation is not instant.
|
|
17
|
+
// 4. The local profile must be upgraded explicitly, or the machine keeps running
|
|
18
|
+
// the previous version while you believe you are testing the new one.
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// node release.mjs patch # 1.1.1 -> 1.1.2, then release
|
|
22
|
+
// node release.mjs minor # 1.1.1 -> 1.2.0
|
|
23
|
+
// node release.mjs 2.0.0 # an explicit version
|
|
24
|
+
// node release.mjs patch -m "人设:…" # override the commit message body
|
|
25
|
+
//
|
|
26
|
+
// Flags:
|
|
27
|
+
// --dry-run do everything except commit, push, publish and install
|
|
28
|
+
// --skip-install do not upgrade the local DSH profile afterwards
|
|
29
|
+
// --skip-gh skip the GitHub CI wait (still pushes)
|
|
30
|
+
// --profile NAME DSH profile to upgrade (default: web)
|
|
31
|
+
|
|
32
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
34
|
+
import { homedir } from 'node:os';
|
|
35
|
+
import { dirname, join } from 'node:path';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
|
|
40
|
+
const PROXY = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? 'http://127.0.0.1:7897';
|
|
41
|
+
const GIT = process.env.DSH_RELEASE_GIT ?? 'git';
|
|
42
|
+
const GH = process.env.DSH_RELEASE_GH ?? 'gh';
|
|
43
|
+
const PUBLISHER = process.env.DSH_RELEASE_PUBLISHER ?? (process.platform === 'win32' ? 'pnpm' : 'npm');
|
|
44
|
+
|
|
45
|
+
const argv = process.argv.slice(2);
|
|
46
|
+
const dryRun = argv.includes('--dry-run');
|
|
47
|
+
const skipInstall = argv.includes('--skip-install');
|
|
48
|
+
const skipGh = argv.includes('--skip-gh');
|
|
49
|
+
const profileIndex = argv.indexOf('--profile');
|
|
50
|
+
const profile = profileIndex === -1 ? 'web' : argv[profileIndex + 1];
|
|
51
|
+
const messageIndex = argv.indexOf('-m');
|
|
52
|
+
const body = messageIndex === -1 ? undefined : argv[messageIndex + 1];
|
|
53
|
+
|
|
54
|
+
/** The version argument: the first token that is neither a flag nor a flag's value. */
|
|
55
|
+
const flagValues = new Set([profileIndex + 1, messageIndex + 1].filter((index) => index > 0));
|
|
56
|
+
const bump = argv.find((arg, index) => !arg.startsWith('-') && !flagValues.has(index));
|
|
57
|
+
|
|
58
|
+
if (bump === undefined) {
|
|
59
|
+
process.stderr.write('usage: node release.mjs <patch|minor|major|X.Y.Z> [-m "commit body"] [--dry-run] [--skip-install]\n');
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const env = { ...process.env, HTTPS_PROXY: PROXY, HTTP_PROXY: PROXY, GIT_TERMINAL_PROMPT: '0' };
|
|
64
|
+
const step = (text) => process.stdout.write(`\n\u25b6 ${text}\n`);
|
|
65
|
+
const run = (command, args, { allowFailure = false, shell = false, ...rest } = {}) => {
|
|
66
|
+
const result = spawnSync(command, args, { cwd: here, env, encoding: 'utf8', shell, ...rest });
|
|
67
|
+
// A spawn that never started reports status null and puts the reason in `error`;
|
|
68
|
+
// surfacing that by name is what turns a silent no-op into a diagnosable failure.
|
|
69
|
+
if (result.status !== 0 && !allowFailure) {
|
|
70
|
+
const reason = result.error ? `${result.error.code}: ${result.error.message}` : `exit ${result.status}`;
|
|
71
|
+
throw new Error(`${command} ${args.join(' ')} failed (${reason})\n${result.stdout ?? ''}${result.stderr ?? ''}`);
|
|
72
|
+
}
|
|
73
|
+
return `${result.stdout ?? ''}${result.stderr ?? ''}${result.error ? `\n${result.error.code}: ${result.error.message}` : ''}`;
|
|
74
|
+
};
|
|
75
|
+
const git = (...args) => run(GIT, args).trim();
|
|
76
|
+
|
|
77
|
+
/** Bump a semver string by one of patch/minor/major. */
|
|
78
|
+
function nextVersion(current, kind) {
|
|
79
|
+
if (/^\d+\.\d+\.\d+$/.test(kind)) return kind;
|
|
80
|
+
const [major, minor, patch] = current.split('.').map(Number);
|
|
81
|
+
if (kind === 'patch') return `${major}.${minor}.${patch + 1}`;
|
|
82
|
+
if (kind === 'minor') return `${major}.${minor + 1}.0`;
|
|
83
|
+
if (kind === 'major') return `${major + 1}.0.0`;
|
|
84
|
+
throw new Error(`unknown bump "${kind}" — use patch, minor, major, or an explicit X.Y.Z`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Preconditions ────────────────────────────────────────────────────────────
|
|
88
|
+
step('Preconditions');
|
|
89
|
+
const manifestPath = join(here, 'package.json');
|
|
90
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
91
|
+
const current = manifest.version;
|
|
92
|
+
const version = nextVersion(current, bump);
|
|
93
|
+
|
|
94
|
+
const dirty = git('status', '--porcelain');
|
|
95
|
+
if (dirty.length > 0) {
|
|
96
|
+
process.stdout.write(` uncommitted changes are present; they will be included in this release:\n${dirty.split('\n').map((l) => ` ${l}`).join('\n')}\n`);
|
|
97
|
+
}
|
|
98
|
+
const branch = git('rev-parse', '--abbrev-ref', 'HEAD');
|
|
99
|
+
if (branch !== 'main') throw new Error(`on branch "${branch}", expected main`);
|
|
100
|
+
process.stdout.write(` ${manifest.name} ${current} -> ${version}\n`);
|
|
101
|
+
|
|
102
|
+
// ── Build and verify ─────────────────────────────────────────────────────────
|
|
103
|
+
step('Regenerate the patch from persona.txt');
|
|
104
|
+
process.stdout.write(` ${run(process.execPath, ['build-patch.mjs']).trim()}\n`);
|
|
105
|
+
|
|
106
|
+
step('Verify the patch, the persona and the mask');
|
|
107
|
+
const patchReport = run(process.execPath, ['verify-patch.mjs']);
|
|
108
|
+
if (!patchReport.includes('ALL CHECKS PASSED')) throw new Error(`verify-patch.mjs did not pass:\n${patchReport}`);
|
|
109
|
+
process.stdout.write(' verify-patch.mjs: ALL CHECKS PASSED\n');
|
|
110
|
+
const depthReport = run(process.execPath, ['verify-depth-override.mjs'], { allowFailure: true });
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
depthReport.includes('ALL CHECKS PASSED')
|
|
113
|
+
? ' verify-depth-override.mjs: ALL CHECKS PASSED\n'
|
|
114
|
+
: ' verify-depth-override.mjs: SKIPped (no depth override on this machine)\n',
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ── Version and commit ───────────────────────────────────────────────────────
|
|
118
|
+
step(`Set version ${version}`);
|
|
119
|
+
if (dryRun) {
|
|
120
|
+
process.stdout.write(' --dry-run: not writing package.json\n');
|
|
121
|
+
} else {
|
|
122
|
+
const updated = readFileSync(manifestPath, 'utf8').replace(/"version": "[^"]+"/, `"version": "${version}"`);
|
|
123
|
+
if (updated === readFileSync(manifestPath, 'utf8')) throw new Error('could not find the version field to replace');
|
|
124
|
+
run(process.execPath, ['-e', `require('node:fs').writeFileSync(${JSON.stringify(manifestPath)}, ${JSON.stringify(updated)})`]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `yyyy-MM-dd HH:mm` in LOCAL time — the project's commit-message convention. */
|
|
128
|
+
function localStamp() {
|
|
129
|
+
const now = new Date();
|
|
130
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
131
|
+
return (
|
|
132
|
+
`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
|
|
133
|
+
`${pad(now.getHours())}:${pad(now.getMinutes())}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const commitMessage = `[Agent任务] ${localStamp()} | ${body ?? `发布 v${version}`}`;
|
|
138
|
+
|
|
139
|
+
step('Commit and push');
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
process.stdout.write(` --dry-run: would commit "${commitMessage}" and push to origin/main\n`);
|
|
142
|
+
} else {
|
|
143
|
+
git('add', '-A');
|
|
144
|
+
git('commit', '-m', commitMessage);
|
|
145
|
+
process.stdout.write(` committed ${git('rev-parse', '--short', 'HEAD')}\n`);
|
|
146
|
+
const pushed = run(GIT, ['push', 'origin', 'main'], { allowFailure: true });
|
|
147
|
+
// The owner bypasses branch protection; collaborators cannot, and will need a PR.
|
|
148
|
+
if (!/main -> main|Everything up-to-date/.test(pushed)) {
|
|
149
|
+
throw new Error(`push failed — if branch protection refused it, open a pull request instead:\n${pushed}`);
|
|
150
|
+
}
|
|
151
|
+
process.stdout.write(' pushed to origin/main\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Publish ──────────────────────────────────────────────────────────────────
|
|
155
|
+
step(`Publish with ${PUBLISHER} to the official registry (the configured one is a read-only mirror)`);
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
process.stdout.write(` --dry-run: would run ${PUBLISHER} publish --registry ${OFFICIAL_REGISTRY}\n`);
|
|
158
|
+
} else {
|
|
159
|
+
// Two Windows facts force the shape of this call, both learned by watching a
|
|
160
|
+
// publish silently do nothing:
|
|
161
|
+
// * `npm` and `pnpm` are shell shims, not executables — `npm` is an sh script
|
|
162
|
+
// (ENOENT), `npm.cmd`/`pnpm.cmd` refuse to spawn without a shell (EINVAL),
|
|
163
|
+
// and `npm.ps1` is PowerShell (EFTYPE). So the publisher is invoked WITH a
|
|
164
|
+
// shell, which is why every argument below must stay shell-safe.
|
|
165
|
+
// * The output must be shown even on failure. The first version filtered it
|
|
166
|
+
// down to matching lines and printed a blank line when the command never
|
|
167
|
+
// ran, hiding the real problem behind a successful-looking step.
|
|
168
|
+
const published = run(PUBLISHER, ['publish', '--registry', OFFICIAL_REGISTRY, '--access', 'public'], {
|
|
169
|
+
allowFailure: true,
|
|
170
|
+
shell: true,
|
|
171
|
+
});
|
|
172
|
+
const summary = published
|
|
173
|
+
.split('\n')
|
|
174
|
+
.map((line) => line.trim())
|
|
175
|
+
.filter((line) => line.length > 0 && !/^Progress:|^Packages:|^\+?$/.test(line));
|
|
176
|
+
process.stdout.write(`${summary.map((line) => ` ${line}`).join('\n') || ' (the publisher printed nothing — treat that as a failure)'}\n`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Confirm on the registry ──────────────────────────────────────────────────
|
|
180
|
+
step('Confirm the version is actually live (the exit code does not prove it)');
|
|
181
|
+
if (dryRun) {
|
|
182
|
+
process.stdout.write(' --dry-run: skipped\n');
|
|
183
|
+
} else {
|
|
184
|
+
// curl, not fetch: this machine reaches the network through a proxy that Node's
|
|
185
|
+
// global fetch does not read, so a fetch here would fail for a reason that has
|
|
186
|
+
// nothing to do with the release.
|
|
187
|
+
const probe = (url) =>
|
|
188
|
+
spawnSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', '-m', '20', '-x', PROXY, url], {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
}).stdout?.trim();
|
|
191
|
+
|
|
192
|
+
// Measured on this account: 1.1.2 was still 404 at 200s and live shortly after,
|
|
193
|
+
// so the window has to be generous or a successful release reads as a failure.
|
|
194
|
+
let live = false;
|
|
195
|
+
for (let attempt = 1; attempt <= 60 && !live; attempt += 1) {
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
197
|
+
const code = probe(`${OFFICIAL_REGISTRY}${manifest.name}/${version}`);
|
|
198
|
+
if (code === '200') {
|
|
199
|
+
live = true;
|
|
200
|
+
process.stdout.write(` ${manifest.name}@${version} is live on the registry\n`);
|
|
201
|
+
} else if (attempt % 6 === 0) {
|
|
202
|
+
process.stdout.write(` still not visible after ${attempt * 10}s (last HTTP ${code})\n`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!live) {
|
|
206
|
+
process.stderr.write(
|
|
207
|
+
'\n NOT VISIBLE after 10 minutes. Two known causes, in order of likelihood:\n' +
|
|
208
|
+
' 1. propagation lag — this registry answered 404 for over three minutes on a\n' +
|
|
209
|
+
' publish that had already succeeded, so re-check before concluding anything;\n' +
|
|
210
|
+
' 2. staged publishing — check npmjs.com for a pending approval. `npm stage list`\n' +
|
|
211
|
+
' reports nothing even when a version is held, so it is not the test.\n',
|
|
212
|
+
);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Upgrade the local profile ────────────────────────────────────────────────
|
|
218
|
+
step(`Upgrade the local "${profile}" profile so the machine runs what was published`);
|
|
219
|
+
if (dryRun || skipInstall) {
|
|
220
|
+
process.stdout.write(' skipped\n');
|
|
221
|
+
} else {
|
|
222
|
+
// `dsh` is a shell shim on Windows too, so it is invoked through Node rather
|
|
223
|
+
// than spawned directly. DSH_RELEASE_DSH overrides the entry point.
|
|
224
|
+
const dshEntry = process.env.DSH_RELEASE_DSH ?? 'D:/nodejs/npm_global/node_modules/@deepseek-ai/dsh/lib/bin.js';
|
|
225
|
+
const output = run(
|
|
226
|
+
process.execPath,
|
|
227
|
+
[dshEntry, 'plugin', '--profile', profile, 'add', `${manifest.name}@${version}`, '--registry', OFFICIAL_REGISTRY],
|
|
228
|
+
{ allowFailure: true },
|
|
229
|
+
);
|
|
230
|
+
const installed = (() => {
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(
|
|
233
|
+
readFileSync(join(homedir(), '.dsh', 'profiles', profile, 'node_modules', manifest.name, 'package.json'), 'utf8'),
|
|
234
|
+
).version;
|
|
235
|
+
} catch {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
})();
|
|
239
|
+
if (installed === version) {
|
|
240
|
+
process.stdout.write(` profile now runs ${manifest.name}@${installed}\n`);
|
|
241
|
+
} else {
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
` the profile reports ${installed ?? 'unknown'} rather than ${version} — install it by hand:\n` +
|
|
244
|
+
` dsh plugin --profile ${profile} add ${manifest.name}@${version} --registry ${OFFICIAL_REGISTRY}\n` +
|
|
245
|
+
`${output.trim().split('\n').slice(-6).map((line) => ` ${line}`).join('\n')}\n`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── CI ───────────────────────────────────────────────────────────────────────
|
|
251
|
+
if (!dryRun && !skipGh) {
|
|
252
|
+
step('Wait for CI on the pushed commit');
|
|
253
|
+
const sha = git('rev-parse', 'HEAD');
|
|
254
|
+
const waited = run(
|
|
255
|
+
GH,
|
|
256
|
+
['run', 'watch', sha, '--repo', 'XiaoOu2024/dsh-root-planner-preset', '--exit-status'],
|
|
257
|
+
{ allowFailure: true },
|
|
258
|
+
);
|
|
259
|
+
process.stdout.write(` ${waited.trim().split('\n').slice(-2).join('\n ') || '(no output)'}\n`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
step(dryRun ? 'Dry run complete — nothing was changed' : `Released ${manifest.name}@${version}`);
|
|
263
|
+
if (!dryRun) {
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
'\n Next: restart `dsh web`, open a new session on the Root Planner preset, then\n' +
|
|
266
|
+
' confirm it with tools/verify-session.mjs before trusting any behaviour.\n',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Forensics on the real root-planner test run: which preset did the root session
|
|
2
|
-
// compose with, and what tools did each level actually receive?
|
|
3
|
-
//
|
|
4
|
-
// Usage: node inspect-sessions.mjs <rootSessionDir> [childSessionDir ...]
|
|
5
|
-
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { zstdDecompressSync } from 'node:zlib';
|
|
9
|
-
|
|
10
|
-
/** Read one .jsonl.zstd session log into parsed events. */
|
|
11
|
-
function readSession(dir) {
|
|
12
|
-
const raw = zstdDecompressSync(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
13
|
-
return raw
|
|
14
|
-
.toString('utf8')
|
|
15
|
-
.split('\n')
|
|
16
|
-
.filter((line) => line.trim().length > 0)
|
|
17
|
-
.map((line) => {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(line);
|
|
20
|
-
} catch {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
.filter(Boolean);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
for (const dir of process.argv.slice(2)) {
|
|
28
|
-
const events = readSession(dir);
|
|
29
|
-
const types = new Map();
|
|
30
|
-
for (const event of events) types.set(event.type, (types.get(event.type) ?? 0) + 1);
|
|
31
|
-
|
|
32
|
-
process.stdout.write(`\n===== ${dir.split(/[\\/]/).pop()} — ${events.length} events =====\n`);
|
|
33
|
-
process.stdout.write(`event types: ${[...types.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([t, n]) => `${t}×${n}`).join(', ')}\n`);
|
|
34
|
-
|
|
35
|
-
// Session header: the composition that was actually in force.
|
|
36
|
-
for (const event of events) {
|
|
37
|
-
if (event.type === 'session/header' || event.type === 'session/start') {
|
|
38
|
-
const h = event.data ?? event;
|
|
39
|
-
process.stdout.write(
|
|
40
|
-
`header: origin=${h.header?.origin ?? h.origin} parent=${h.header?.parentSession ?? h.parentSession} ` +
|
|
41
|
-
`depth=${h.header?.delegationDepth ?? h.delegationDepth} preset=${h.preset ?? h.presetId ?? '(not in header)'}\n`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Tool names actually offered to this session, from its request payloads.
|
|
47
|
-
const offered = new Set();
|
|
48
|
-
for (const event of events) {
|
|
49
|
-
const message = event.data?.request?.messages ?? event.request?.messages;
|
|
50
|
-
if (!Array.isArray(message)) continue;
|
|
51
|
-
for (const entry of message) {
|
|
52
|
-
if (entry?.role !== 'system') continue;
|
|
53
|
-
const text = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
|
|
54
|
-
for (const match of text.matchAll(/"name":\s*"([a-z_0-9]+)"/g)) offered.add(match[1]);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (offered.size > 0) {
|
|
58
|
-
process.stdout.write(`tools seen in request payloads (${offered.size}): ${[...offered].sort().join(', ')}\n`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Any tool error mentioning a mask or an executor.
|
|
62
|
-
for (const event of events) {
|
|
63
|
-
const text = JSON.stringify(event);
|
|
64
|
-
for (const needle of ['unknown global tool', 'EXECUTOR_UNAVAILABLE', 'SubagentDepthError', 'exceeds maxDepth']) {
|
|
65
|
-
if (text.includes(needle)) {
|
|
66
|
-
const start = Math.max(0, text.indexOf(needle) - 220);
|
|
67
|
-
process.stdout.write(` [${needle}] …${text.slice(start, text.indexOf(needle) + 220)}…\n`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|