dsh-plugin-upgrade 0.1.0

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/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "dsh-plugin-upgrade",
3
+ "version": "0.1.0",
4
+ "description": "Plugin-author upgrade skill for DeepSeek Harness: a version-locked 0.1.3-alpha.1 -> 0.1.5-alpha.1 version card plus a zero-dependency seam scanner (V3 session format, assistant/message.stream, SessionHandleReadResult, ctx.agent, Inbox, SubprocessHandle.pid, SystemPrompt persona, PTC rename, EpochHeader.system, and the tsconfig stale-path false green), packaged as a bundle skill and an npx CLI.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/PerryLink/dsh-plugin-upgrade.git"
8
+ },
9
+ "homepage": "https://github.com/PerryLink/dsh-plugin-upgrade#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/PerryLink/dsh-plugin-upgrade/issues"
12
+ },
13
+ "author": "PerryLink",
14
+ "private": false,
15
+ "type": "module",
16
+ "main": "./index.mjs",
17
+ "types": "./types.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./types.d.ts",
21
+ "default": "./index.mjs"
22
+ },
23
+ "./scan": {
24
+ "types": "./types.d.ts",
25
+ "default": "./lib/scan.mjs"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "bin": {
30
+ "dsh-plugin-upgrade-scan": "./scripts/scan-0.1.5.mjs"
31
+ },
32
+ "files": [
33
+ "index.mjs",
34
+ "types.d.ts",
35
+ "lib",
36
+ "scripts",
37
+ "skills",
38
+ "cordis.patch.yml",
39
+ "CHANGELOG.md",
40
+ "SECURITY.md",
41
+ "AGENTS.md",
42
+ "THIRD_PARTY_NOTICES.md",
43
+ "README.md",
44
+ "README.zh.md",
45
+ "README.es.md",
46
+ "README.pt.md",
47
+ "README.hi.md",
48
+ "LICENSE"
49
+ ],
50
+ "sideEffects": false,
51
+ "dsh": {
52
+ "bundle": {
53
+ "patch": "./cordis.patch.yml"
54
+ }
55
+ },
56
+ "keywords": [
57
+ "dsh",
58
+ "dsh-plugin",
59
+ "deepseek-harness",
60
+ "deepseek",
61
+ "cordis",
62
+ "plugin-upgrade",
63
+ "migration",
64
+ "skill",
65
+ "version-card",
66
+ "scanner"
67
+ ],
68
+ "engines": {
69
+ "node": "^22.19.0 || >=24.0.0"
70
+ },
71
+ "packageManager": "pnpm@11.7.0",
72
+ "peerDependencies": {
73
+ "@deepseek-ai/cordis": "^4.0.2",
74
+ "@deepseek-ai/dsh-skill": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
75
+ "@deepseek-ai/schemastery": "^3.18.2"
76
+ },
77
+ "devDependencies": {
78
+ "@deepseek-ai/cordis": "^4.0.2",
79
+ "@deepseek-ai/dsh-skill": "0.1.5-alpha.1",
80
+ "@deepseek-ai/schemastery": "^3.18.2"
81
+ },
82
+ "scripts": {
83
+ "test": "node --test \"test/*.test.mjs\"",
84
+ "scan": "node scripts/scan-0.1.5.mjs",
85
+ "check:readmes": "node scripts/check-readme-sync.mjs",
86
+ "verify:self-contained": "node scripts/verify-self-contained.mjs",
87
+ "verify:artifacts": "node scripts/verify-artifacts.mjs",
88
+ "verify": "npm test && node scripts/check-readme-sync.mjs && node scripts/verify-self-contained.mjs && node scripts/verify-artifacts.mjs"
89
+ },
90
+ "license": "Apache-2.0",
91
+ "funding": {
92
+ "type": "individual",
93
+ "url": "https://github.com/sponsors/PerryLink"
94
+ }
95
+ }
@@ -0,0 +1,21 @@
1
+ // Print the CHANGELOG.md section for one version, for GitHub Release notes.
2
+ // Usage: node scripts/changelog-section.mjs <x.y.z>
3
+ import { readFileSync } from 'node:fs'
4
+ import { dirname, join, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const version = process.argv[2]
8
+ if (!version) {
9
+ console.error('usage: node scripts/changelog-section.mjs <x.y.z>')
10
+ process.exit(2)
11
+ }
12
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
13
+ const lines = readFileSync(join(root, 'CHANGELOG.md'), 'utf8').split(/\r?\n/)
14
+ const start = lines.findIndex(l => l.startsWith(`## [${version}]`))
15
+ if (start < 0) {
16
+ console.error(`CHANGELOG.md has no "## [${version}]" section`)
17
+ process.exit(1)
18
+ }
19
+ const rest = lines.slice(start + 1)
20
+ const end = rest.findIndex(l => l.startsWith('## ['))
21
+ process.stdout.write(`${rest.slice(0, end < 0 ? rest.length : end).join('\n').trim()}\n`)
@@ -0,0 +1,33 @@
1
+ // Five-language README sync gate: every README must carry the same number of
2
+ // `## ` sections as the English source and state the install command.
3
+ // Usage: node scripts/check-readme-sync.mjs
4
+ import { readFileSync, existsSync } from 'node:fs'
5
+ import { dirname, join, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
9
+ const FILES = ['README.md', 'README.zh.md', 'README.es.md', 'README.pt.md', 'README.hi.md']
10
+ const INSTALL_COMMAND = 'dsh plugin --profile web add dsh-plugin-upgrade'
11
+ const failures = []
12
+ const read = (file) => {
13
+ const p = join(root, file)
14
+ if (!existsSync(p)) { failures.push(`${file} is missing`); return '' }
15
+ return readFileSync(p, 'utf8')
16
+ }
17
+ const sectionCount = text => (text.match(/^## /gmu) ?? []).length
18
+
19
+ const contents = FILES.map(read)
20
+ const expected = sectionCount(contents[0])
21
+ for (let i = 1; i < FILES.length; i++) {
22
+ if (contents[i] === '') continue
23
+ const count = sectionCount(contents[i])
24
+ if (count !== expected) failures.push(`${FILES[i]}: ${count} '## ' sections, expected ${expected}`)
25
+ if (!contents[i].includes(INSTALL_COMMAND)) failures.push(`${FILES[i]}: missing the install command`)
26
+ }
27
+ if (!contents[0].includes(INSTALL_COMMAND)) failures.push('README.md: missing the install command')
28
+ if (failures.length) {
29
+ console.error('readme-sync: FAIL')
30
+ for (const f of failures) console.error(' ' + f)
31
+ process.exit(1)
32
+ }
33
+ console.log(`readme-sync: all ${FILES.length} READMEs share ${expected} sections and the install command`)
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI wrapper for the packaged seam scanner.
4
+ *
5
+ * Usage:
6
+ * node scripts/scan-0.1.5.mjs [--repo <path>] [--json <out.json>] [--seams M1,S3] [--quiet]
7
+ * npx --package dsh-plugin-upgrade dsh-plugin-upgrade-scan --repo <path>
8
+ *
9
+ * Exit codes: 0 = no error-severity hit, 1 = at least one error-severity hit,
10
+ * 2 = usage or scan failure. The implementation lives in ../lib/scan.mjs
11
+ * so the plugin, the CLI and the tests share one seam catalog.
12
+ */
13
+ import { main } from '../lib/scan.mjs'
14
+
15
+ process.exit(main(process.argv.slice(2)))
@@ -0,0 +1,36 @@
1
+ // Sweep every plugin repo under a workspace root and write the evidence table.
2
+ // Usage: node sweep-all.mjs [<workspaceRoot>] [<out.md>]
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { scanRepo } from '../lib/scan.mjs'
6
+
7
+ const ROOT = path.resolve(process.argv[2] || '.')
8
+ const OUT = path.resolve(process.argv[3] || path.join(ROOT, 'scan-0.1.5-sweep.md'))
9
+ const SKIP = new Set(['adp-list', 'audit-dsh-infinite-gen-2', 'pan17-dsh-wechat', 'dsh-autotier', 'dsh-personal-directive'])
10
+ const dirs = fs.readdirSync(ROOT, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
11
+ .filter(n => !n.startsWith('_') && !n.startsWith('.') && !SKIP.has(n))
12
+ .filter(n => fs.existsSync(path.join(ROOT, n, 'package.json'))).sort()
13
+
14
+ const rows = []
15
+ for (const n of dirs) {
16
+ const r = scanRepo(path.join(ROOT, n))
17
+ const errors = r.hits.filter(h => h.severity === 'error')
18
+ const warns = r.hits.filter(h => h.severity === 'warn')
19
+ rows.push({ repo: n, files: r.files, errors: errors.length, warns: warns.length, errorSeams: [...new Set(errors.map(h => h.seam))].join(',') || '-' })
20
+ }
21
+ const L = []
22
+ L.push('# scan-0.1.5 · workspace sweep evidence')
23
+ L.push('')
24
+ L.push(`Generated ${new Date().toISOString()} · scanner \`scripts/scan-0.1.5.mjs\` · workspace \`${ROOT}\``)
25
+ L.push('')
26
+ L.push('All repos below were adapted to `0.1.5-alpha.1` by the 2026-09-09 wave. Error-severity hits are expected to be **zero**; warn-severity hits are heuristic leads for manual review (S1/S2/S10).')
27
+ L.push('')
28
+ L.push('| repo | files | errors | warns | error seams |')
29
+ L.push('|---|---|---|---|---|')
30
+ for (const r of rows) L.push(`| ${r.repo} | ${r.files} | ${r.errors} | ${r.warns} | ${r.errorSeams} |`)
31
+ L.push('')
32
+ const withErr = rows.filter(r => r.errors > 0)
33
+ L.push(`**Totals**: ${rows.length} repos · ${rows.reduce((a, r) => a + r.files, 0)} files · ${rows.reduce((a, r) => a + r.errors, 0)} error hits · ${rows.reduce((a, r) => a + r.warns, 0)} warn hits · repos with errors: ${withErr.length ? withErr.map(r => r.repo).join(', ') : 'none'}`)
34
+ fs.mkdirSync(path.dirname(OUT), { recursive: true })
35
+ fs.writeFileSync(OUT, L.join('\n'), 'utf8')
36
+ console.log(L.join('\n'))
@@ -0,0 +1,86 @@
1
+ // verify-artifacts: pack the package into a temp directory and prove the published
2
+ // tarball carries the plugin entry, the skill bundle, the CLI and the patch layer,
3
+ // and that the entry imports under plain Node.
4
+ // Usage: node scripts/verify-artifacts.mjs
5
+ import { execFileSync } from 'node:child_process'
6
+ import { mkdtempSync, readdirSync, rmSync, existsSync, readFileSync, symlinkSync, mkdirSync, writeFileSync } from 'node:fs'
7
+ import { dirname, join, resolve } from 'node:path'
8
+ import { tmpdir } from 'node:os'
9
+ import { fileURLToPath, pathToFileURL } from 'node:url'
10
+
11
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
12
+ const staging = mkdtempSync(join(tmpdir(), 'dsh-plugin-upgrade-pack-'))
13
+ const failures = []
14
+ try {
15
+ execFileSync('npm', ['pack', '--pack-destination', staging], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: true })
16
+ const tgz = readdirSync(staging).find(f => f.endsWith('.tgz'))
17
+ if (!tgz) throw new Error('npm pack produced no tarball')
18
+ const extract = join(staging, 'x')
19
+ execFileSync('tar', ['-xzf', join(staging, tgz), '-C', staging], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
20
+ const pkgRoot = join(staging, 'package')
21
+ if (!existsSync(pkgRoot)) { rmSync(extract, { recursive: true, force: true }); failures.push('tarball has no package/ root') }
22
+
23
+ const required = [
24
+ 'index.mjs',
25
+ 'cordis.patch.yml',
26
+ 'lib/scan.mjs',
27
+ 'scripts/scan-0.1.5.mjs',
28
+ 'skills/plugin-upgrade-015/SKILL.md',
29
+ 'skills/plugin-upgrade-015/scripts/scan-0.1.5.mjs',
30
+ 'skills/plugin-upgrade-015/references/v0.1.3-alpha.1-to-v0.1.5-alpha.1.md',
31
+ 'README.md',
32
+ 'CHANGELOG.md',
33
+ 'LICENSE',
34
+ ]
35
+ for (const rel of required) if (!existsSync(join(pkgRoot, rel))) failures.push(`tarball is missing ${rel}`)
36
+
37
+ // The packaged entry must import without the harness present. It imports the
38
+ // declared peer @deepseek-ai/schemastery, so lend the extracted tree this
39
+ // repo's installed peers through a directory link instead of reinstalling.
40
+ try {
41
+ const nm = join(pkgRoot, 'node_modules')
42
+ if (!existsSync(nm) && existsSync(join(root, 'node_modules'))) {
43
+ symlinkSync(join(root, 'node_modules'), nm, process.platform === 'win32' ? 'junction' : 'dir')
44
+ }
45
+ const entryUrl = pathToFileURL(join(pkgRoot, 'index.mjs')).href
46
+ const out = execFileSync(process.execPath, ['-e', `import(${JSON.stringify(entryUrl)}).then(m => console.log('exports:' + ['name','inject','Config','apply'].filter(k => k in m).join(',')))`], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
47
+ if (!/exports:name,inject,Config,apply/.test(out)) failures.push(`entry export surface unexpected: ${out.trim()}`)
48
+ } catch (error) {
49
+ failures.push(`packaged entry failed to import: ${error instanceof Error ? String(error.stderr || error.message).slice(0, 200) : String(error)}`)
50
+ }
51
+
52
+ // The packaged SKILL.md must keep its corridor frontmatter.
53
+ const skill = readFileSync(join(pkgRoot, 'skills/plugin-upgrade-015/SKILL.md'), 'utf8')
54
+ if (!/^name:\s*plugin-upgrade-015\s*$/m.test(skill)) failures.push('packaged SKILL.md lost its frontmatter name')
55
+
56
+ // cordis.patch.yml must stay a top-level YAML ARRAY of loader patch entries:
57
+ // a mapping (`insert:` at column 0) mounts nothing and dsh reports
58
+ // "must be a top-level YAML array of loader patch entries" at profile load.
59
+ const patch = readFileSync(join(pkgRoot, 'cordis.patch.yml'), 'utf8')
60
+ const body = patch.split(/\r?\n/).filter(l => l.trim() !== '' && !l.trim().startsWith('#'))
61
+ if (!body[0]?.startsWith('- ')) failures.push(`cordis.patch.yml is not a top-level YAML array (starts with ${JSON.stringify(body[0]?.slice(0, 30))})`)
62
+ if (!body.some(l => /^-\s+insert:/.test(l))) failures.push('cordis.patch.yml has no top-level `- insert:` entry')
63
+ if (!body.some(l => /name:\s*dsh-plugin-upgrade\s*$/.test(l))) failures.push('cordis.patch.yml does not insert the dsh-plugin-upgrade row')
64
+
65
+ // The skill-relative scanner entry must work from inside the tarball, because
66
+ // the skill body resolves `./scripts/...` against the skill directory.
67
+ const probe = join(staging, 'bad-probe')
68
+ mkdirSync(probe, { recursive: true })
69
+ writeFileSync(join(probe, 'index.ts'), "ctx.on('tool/code-dispatch', () => {})\n")
70
+ try {
71
+ execFileSync(process.execPath, [join(pkgRoot, 'skills/plugin-upgrade-015/scripts/scan-0.1.5.mjs'), '--repo', probe, '--quiet'], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
72
+ failures.push('packaged skill-relative scanner exited 0 on a real seam')
73
+ } catch (error) {
74
+ if (error.status !== 1) failures.push(`packaged skill-relative scanner exited ${error.status}, expected 1`)
75
+ }
76
+
77
+ if (failures.length) {
78
+ console.error('artifacts: FAIL')
79
+ for (const f of failures) console.error(' ' + f)
80
+ process.exitCode = 1
81
+ } else {
82
+ console.log(`artifacts: OK (${required.length} required files present, entry imports, skill frontmatter intact)`)
83
+ }
84
+ } finally {
85
+ rmSync(staging, { recursive: true, force: true })
86
+ }
@@ -0,0 +1,59 @@
1
+ // verify-self-contained: every bare import in this package must resolve from the
2
+ // declared dependency set, and no import may point outside the package root.
3
+ // Usage: node scripts/verify-self-contained.mjs
4
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
5
+ import { dirname, join, resolve, relative, isAbsolute } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
9
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
10
+ const declared = new Set([
11
+ ...Object.keys(pkg.dependencies ?? {}),
12
+ ...Object.keys(pkg.devDependencies ?? {}),
13
+ ...Object.keys(pkg.peerDependencies ?? {}),
14
+ ...Object.keys(pkg.optionalDependencies ?? {}),
15
+ ])
16
+ const BUILTIN = /^(node:|[a-z]+$)/
17
+ const SKIP = new Set(['node_modules', '.git', 'fixtures'])
18
+
19
+ function* walk(dir) {
20
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
21
+ if (e.isDirectory()) { if (!SKIP.has(e.name)) yield* walk(join(dir, e.name)) }
22
+ else if (/\.(mjs|cjs|js)$/.test(e.name)) yield join(dir, e.name)
23
+ }
24
+ }
25
+
26
+ const problems = []
27
+ for (const file of walk(root)) {
28
+ // Strip comments and template literals first: this script itself contains
29
+ // `import('${...}')` inside a template literal, which is not an import edge.
30
+ const text = readFileSync(file, 'utf8')
31
+ .replace(/\/\*[\s\S]*?\*\//g, '')
32
+ .replace(/^[ \t]*\/\/.*$/gm, '')
33
+ .replace(/`(?:[^`\\]|\\.)*`/g, '``')
34
+ for (const m of text.matchAll(/(?:from|import\()\s*['"]([^'"]+)['"]/g)) {
35
+ const spec = m[1]
36
+ if (spec.startsWith('.')) {
37
+ const target = resolve(dirname(file), spec)
38
+ const rel = relative(root, target)
39
+ if (rel.startsWith('..') || isAbsolute(rel)) problems.push(`${relative(root, file)}: relative import escapes the package: ${spec}`)
40
+ else if (!existsSync(target) && !existsSync(`${target}.mjs`) && !existsSync(join(target, 'index.mjs'))) problems.push(`${relative(root, file)}: relative import does not exist: ${spec}`)
41
+ continue
42
+ }
43
+ if (BUILTIN.test(spec)) continue
44
+ const base = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0]
45
+ if (!declared.has(base)) problems.push(`${relative(root, file)}: bare import not declared in package.json: ${spec}`)
46
+ }
47
+ }
48
+
49
+ // The packaged skill and its references must exist for the plugin to mount.
50
+ for (const required of ['skills/plugin-upgrade-015/SKILL.md', 'skills/plugin-upgrade-015/references/v0.1.3-alpha.1-to-v0.1.5-alpha.1.md', 'cordis.patch.yml']) {
51
+ if (!existsSync(join(root, required))) problems.push(`missing packaged asset: ${required}`)
52
+ }
53
+
54
+ if (problems.length) {
55
+ console.error('self-contained: FAIL')
56
+ for (const p of problems) console.error(' ' + p)
57
+ process.exit(1)
58
+ }
59
+ console.log('self-contained: all imports resolve within the package and from declared dependencies')
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: plugin-upgrade-015
3
+ description: Migrate a DeepSeek Harness plugin repo from the 0.1.3-alpha.1 line to 0.1.5-alpha.1. Runs a zero-dependency seam scanner (V3 session format, assistant/message.stream, SessionHandleReadResult, ctx.agent, Inbox, SubprocessHandle.pid, SystemPrompt persona, PTC rename, EpochHeader.system, and the tsconfig stale-path false-green), then walks the fix-and-verify loop with real-host smoke.
4
+ whenToUse: Use when a DSH plugin must support @deepseek-ai/dsh 0.1.5-alpha.1 (or any host line in >=0.1.3-alpha.1 <0.1.5-alpha.2) while keeping the 0.1.2-rc.1 peer band working. Not for 0.1.1→0.1.2 migrations (use the community convergence skill) and not for the DSH user-facing upgrade/repair path.
5
+ metadata:
6
+ corridor: "0.1.3-alpha.1 -> 0.1.5-alpha.1"
7
+ host-baseline: "0.1.5-alpha.1 (checkout 19d2e38480, tag dsh-v0.1.5-alpha.1 5dda764ed3)"
8
+ evidence: "40 plugin repos, 2026-09-09 wave"
9
+ status: "published"
10
+ user-invocable: true
11
+ ---
12
+
13
+ # Plugin upgrade · 0.1.3-alpha.1 → 0.1.5-alpha.1
14
+
15
+ You are migrating **one plugin repository** to the DSH `0.1.5-alpha.1` line. The goal is not "make typecheck pass" — it is "prove the plugin still works on the target host". Local gates are necessary but not sufficient: two failure classes survive a green gate (stale-type false green, and tests mocked against the old shape).
16
+
17
+ ## Hard rules
18
+
19
+ 1. **Run the scanner first.** `node ./scripts/scan-0.1.5.mjs --repo <repo>` — it prints `file:line` facts. Do not trust a green `typecheck` before M1 is cleared.
20
+ 2. **M1 is a blocker, not a warning.** If `tsconfig` `paths` do not resolve, TypeScript silently falls back to published types and every other signal is fake. Fix the paths, then re-run; new red is real signal.
21
+ 3. **Only adapt what the card says changed.** Do not refactor beyond the card; keep the old peer band working (the repo must still boot on `0.1.2-rc.1`).
22
+ 4. **Behavior change ⇒ test change ⇒ docs change, in one commit.** Five-language READMEs and CHANGELOG move with the code.
23
+ 5. **Real-host smoke is the exit criterion.** Temp `DSH_HOME` (mkdtemp) + target CLI + `dsh plugin --profile web add <tarball>` + `--dump-config`. Never touch the user's real `~/.dsh`.
24
+
25
+ ## Loop
26
+
27
+ 1. **Identify** — record: repo, current version, peer band, target host tag, node/pnpm, and whether the repo has client half / tracked `lib/`.
28
+ 2. **Baseline** — run the repo's own gate chain and *record pre-existing failures*; never let them be counted as migration regressions.
29
+ 3. **Scan** — run `./scripts/scan-0.1.5.mjs`; load only the version card facts that hit.
30
+ 4. **Plan** — group by host/client/config/distribution; list files, why they change, tests, rollback point. Get confirmation before editing.
31
+ 5. **Adapt + verify per module** — fix, run the module's tests, keep commits conventional and independently revertable.
32
+ 6. **Prove** — full gate chain + real-host smoke (+ resume round-trip for log writers, + browser assertion for client halves). Report done / not-hit / pre-existing / unverified / rollback.
33
+
34
+ ## Reference
35
+
36
+ - `./references/v0.1.3-alpha.1-to-v0.1.5-alpha.1.md` — the version card: 10 seams with host path + commit + minimal fix + regression, and the 4 that no community PR covers yet (S3/S8/S9/M1).
37
+ - `./scripts/scan-0.1.5.mjs` — the detector (zero dependency, `file:line`, exit 1 on error-severity hits).
38
+ - The package's own test suite (`node --test`) — synthetic bad/good fixtures plus a live negative on a repo already adapted by this wave.
@@ -0,0 +1,90 @@
1
+ # Version card · `0.1.3-alpha.1` → `0.1.5-alpha.1`
2
+
3
+ > 适用:任何 `@deepseek-ai/dsh-*` peer 落在 `>=0.1.2-rc.1 <0.2.0`、且需要同时支持 `0.1.5-alpha.1` 的插件仓。
4
+ > 状态:**本地留档(未发布)**。证据来自 2026-09-09 批次 40 个真实插件仓的实测升级波(宿主 checkout `0.1.5-alpha.1`,HEAD `19d2e38480`;官方 tag `dsh-v0.1.5-alpha.1` = `5dda764ed3`)。
5
+ > 证据目录:`_scratch/batch-2026-09-09/`(`COMPLETION-REPORT.md` §2、`cards/*.md`、`probe/*.json`、`doctor/`、`charset/`、`phase-e/`)。
6
+
7
+ ## 0. 这一跳与其它跳的关系
8
+
9
+ | 走廊 | 社区状态(2026-09-09 实测) |
10
+ |---|---|
11
+ | `0.1.0-rc.8 → 0.1.3-alpha.2` | 合流仓 `oh-my-dsh/dsh-plugin-upgrade-skill` 已合入 |
12
+ | `0.1.3-alpha.2 → 0.1.5-alpha.1` | **两条 open PR 已认领**(#195 18 卡 / #197 6 卡),均未合入 |
13
+ | 本卡覆盖 | `0.1.3-alpha.1 → 0.1.5-alpha.1`,**只补已被实测、但上述 PR 未覆盖的 4 类**(S3 / S8 / S9 / M1),其余 seam 作为交叉核对 |
14
+
15
+ **本卡的四类独家内容**:S3(`assistant/message.stream`)、S8(`SessionHandleReadResult`)、S9(`SystemPrompt.persona` → `personaPrefix`)、**M1(tsconfig 陈旧路径导致 typecheck 静默假绿)**。
16
+
17
+ ## 1. M1 · tsconfig 陈旧路径 → 本地门禁假绿(**最高优先级,独家**)
18
+
19
+ | 项 | 内容 |
20
+ |---|---|
21
+ | 旧假设 | `tsconfig.json` 的 `paths` 把 `@deepseek-ai/*` 指到本地 harness checkout,`pnpm run typecheck` 即"对最新版编译" |
22
+ | 触发 | 工作区搬迁(2026-09-07)后 `../../../packages|vendor` 不再指向 checkout(实测解析到 `D:\Projects\packages`,不存在) |
23
+ | 真实行为 | TypeScript 对**解析失败的 path 映射静默回退**到 `node_modules` 的已发布类型 → 门禁全绿,但编译的不是目标版本 |
24
+ | 实测规模 | 40 仓中 **11 仓**命中;修复路径后**3 仓立刻暴露真实 TS 错误**(原本"全绿") |
25
+ | 正确写法 | 相对仓库根:`../../../../deepseek-harness/packages/<pkg>/lib/types/index.d.ts`(及 `vendor/...`) |
26
+ | 检测 | `node scripts/scan-0.1.5.mjs --seams M1`:逐条解析 `paths`,目标不存在即报 error |
27
+ | 回归 | 修复后 `pnpm run typecheck` 必须仍绿;**若变红,那才是真实适配缺口**(先修代码,不要回退路径) |
28
+ | 剩余风险 | 路径依赖本地 checkout 布局;CI 侧应改用 `typecheck:ci`(清空 paths)做已发布线回归,两把尺子都要绿 |
29
+
30
+ ## 2. S3 · `assistant/message` 必填 `stream`(**独家**)
31
+
32
+ | 项 | 内容 |
33
+ |---|---|
34
+ | 目标契约 | `packages/core/session/src/index.ts` 的 `assertAssistantSettlementShape` 要求 `Array.isArray(data.stream)` |
35
+ | 谁受影响 | **写入会话日志的一方**(导入器/转换器/eval 产物生成器),不是只读消费者 |
36
+ | 失败形态 | 日志写入成功、`Session.fromRestore` 抛 `seed assistant/message at index N has invalid settlement fields` → **会话不可续聊**(比 typecheck 红更隐蔽) |
37
+ | 实测案例 | `dsh-claude-move`:handle 基线导入成功但 resume 失败;修复 = `normalizeHandleEvents` 在格式版本 ≥2 时为 `assistant/message` 补 `stream: []`(legacy 与版本 <2 路径零改动) |
38
+ | 注意 | 这不是 0.1.5 新增:0.1.3-alpha.1 的旧断言同样会抛,只是 0.1.2-rc.1 线无该字段;**升级到 alpha 线后成为阻断项** |
39
+ | 检测 | 扫描器 S3:文件出现 `assistant/message` + 会话写入迹象、且全文无 `stream:` → error |
40
+ | 回归 | L6 级往返测试:新日志可 resume、缺 `stream` 形状被拒、legacy 可读回 |
41
+
42
+ ## 3. S8 · `SessionHandleReadResult`(**独家**)
43
+
44
+ | 项 | 内容 |
45
+ |---|---|
46
+ | 目标契约 | `SessionHandle.read()` 返回 `{ eventState, events, ... }`(`packages/session/session-persistence/src/*`,commit `9b78f99dec`) |
47
+ | 旧假设 | `read()` 返回 `SessionEvent[]`(直接 `.filter()` / `.findLast()` / 当数组用) |
48
+ | 实测案例 | `dsh-background-agents` 生产代码 `src/tools.ts:713`(`TS2740: missing length, concat, join, slice…`)+ 两处测试;`dsh-output-styles` 的 `scripts/verify-session-log.mjs` 直接 TypeError |
49
+ | 正确写法 | `const { events } = await handle.read()` 或 `(await handle.read()).events` |
50
+ | 检测 | 扫描器 S8:`await X.read()` 出现但全文无 `.events`/解构解包 → error |
51
+ | 回归 | 测试夹具改成 `.events`;verify 脚本用同一契约 |
52
+
53
+ ## 4. S9 · `SystemPrompt` 配置改名(**独家**)
54
+
55
+ | 项 | 内容 |
56
+ |---|---|
57
+ | 目标契约 | `packages/core/system-prompt/src/index.ts` 的 Config:`includeHarnessIdentity?` / `includeRuntimeContext?` / `personaPrefix?` / `personaSuffix?` / `toolOrder?`(commit `40792330c0`) |
58
+ | 旧假设 | `{ persona: '' }` |
59
+ | 实测案例 | `dsh-data-quality`、`dsh-industry-research`、`dsh-research-report`、`dsh-fast` 的测试夹具(`TS2353: 'persona' does not exist in type 'Config'`) |
60
+ | 正确写法 | `{ personaPrefix: '' }`;**不要**用 `includeHarnessIdentity: false` 代替——那会删掉 `harness:identity` 段,语义不等价 |
61
+ | 官方先例 | `apps/cli/tests/profiles/headless/tests/harness.ts:59` |
62
+ | 检测 | 扫描器 S9:`{ persona:` 且文件含 `SystemPrompt` → error |
63
+ | 回归 | typecheck + typecheck:ci 双绿 |
64
+
65
+ ## 5. 其余 seam(交叉核对用,细节见 COMPLETION-REPORT §2)
66
+
67
+ | seam | 一句话 | 影响仓(实测) |
68
+ |---|---|---|
69
+ | S1 | 会话格式 V3 + 日志世代名 `session.v3.jsonl.zstd` | 日志读写方;硬编码旧名会静默失效 |
70
+ | S2 | `EpochHeader.system` 移除,系统提示词进 `system/message` | auto-review / fast / observe / output-styles |
71
+ | S4 | `tool/code-dispatch` → `tool/ptc-dispatch`(V3 迁移映射) | doublecheck(4 处)/ observe(2 处) |
72
+ | S5 | `ctx.agent` 移除 | wechat(唯一命中;替代 = setup 第 2 参) |
73
+ | S6 | `Inbox` 类型化(不可构造) | fund-research / score / test-drive(夹具) |
74
+ | S7 | `SubprocessHandle.pid` 移除 | click / talk / score / test-drive / lsp-actions |
75
+ | S10 | 事件词表 fail-closed、`append` 无 `ignorable` 写入通道 | permission-rules(版本预检未覆盖新线 → 审计行未盖章 → 会话不可恢复) |
76
+
77
+ ## 6. 强制验证顺序(本地门禁是必要非充分)
78
+
79
+ 1. `scan-0.1.5.mjs` 出 `file:line` 事实(先看 M1:假绿不排掉,后面全是幻觉)。
80
+ 2. `pnpm install --no-frozen-lockfile` → `typecheck`(对 checkout)+ `typecheck:ci`(对已发布线)双绿。
81
+ 3. `test` + `build` + 仓内 `verify:*` + `pack`。
82
+ 4. **真实宿主冒烟**:临时 `DSH_HOME`(`%TEMP%` mkdtemp)+ 目标版 CLI → `dsh plugin --profile web add <tgz>` → `--dump-config` 出现插件行。
83
+ 5. 写会话日志的插件追加 **resume 往返**(S3);有 client 半的追加真实浏览器断言(L9)。
84
+ 6. 记录:改动面、未命中项、迁移前既有失败、未验证边界、回滚点。
85
+
86
+ ## 7. 剩余风险与边界
87
+
88
+ - 版本卡会过期:`0.1.5-alpha.1` 之后仍有新跳;本卡只对 `0.1.3-alpha.1 → 0.1.5-alpha.1` 负责。
89
+ - 扫描器是启发式:S2/S7/S10 为 warn 级,需人工复核;M1/S3/S8/S9 为 error 级且已在合成夹具 + 真实仓双向验证。
90
+ - 未覆盖:真机 L9(浏览器)与 L11(真实模型)在无凭据/无浏览器环境下未执行。
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Skill-relative scanner entry.
4
+ *
5
+ * The skill body resolves its relative paths against this skill's own directory
6
+ * (the `resourceBase`), so `./scripts/scan-0.1.5.mjs` must exist here as well as
7
+ * at the package root. Both are thin wrappers over the single implementation in
8
+ * `<package>/lib/scan.mjs`, so the skill, the CLI and the tests share one catalog.
9
+ *
10
+ * Usage: node ./scripts/scan-0.1.5.mjs --repo <path>
11
+ */
12
+ import { main } from '../../../lib/scan.mjs'
13
+
14
+ process.exit(main(process.argv.slice(2)))
package/types.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Public type surface for dsh-plugin-upgrade.
3
+ * @module dsh-plugin-upgrade
4
+ */
5
+
6
+ /** Plugin configuration (Schemastery-backed; every field is a deployment choice). */
7
+ export interface Config {
8
+ /** Register the packaged skill (default true). */
9
+ enabled?: boolean
10
+ /** Skill name published to the model catalog (default 'plugin-upgrade-015'). */
11
+ skillName?: string
12
+ /** Skill root inside the package; must contain `<skillName>/SKILL.md`. */
13
+ skillsRoot?: string
14
+ /** Mark the skill user-invocable in addition to model-invocable (default true). */
15
+ userInvocable?: boolean
16
+ }
17
+
18
+ /** One seam hit produced by the scanner. */
19
+ export interface SeamHit {
20
+ /** Seam id: S1..S10 or M1. */
21
+ seam: string
22
+ /** 'error' fails the scan; 'warn' is a manual-review lead. */
23
+ severity: 'error' | 'warn'
24
+ /** Absolute path of the file carrying the hit. */
25
+ file: string
26
+ /** 1-based line number. */
27
+ line: number
28
+ /** Trimmed source line (truncated to 200 characters). */
29
+ snippet: string
30
+ /** Human-readable explanation, including the suggested action for M1. */
31
+ detail: string
32
+ }
33
+
34
+ /** Scanner report for one repository. */
35
+ export interface ScanReport {
36
+ repo: string
37
+ scannedAt: string
38
+ files: number
39
+ hits: SeamHit[]
40
+ bySeam: Record<string, number>
41
+ }
42
+
43
+ /** Scan a repository directory for the 0.1.3-alpha.1 -> 0.1.5-alpha.1 seams. */
44
+ export declare function scanRepo(repoDir: string, options?: { seams?: string[] }): ScanReport
45
+
46
+ /** Render a report for a terminal. */
47
+ export declare function render(report: ScanReport): string
48
+
49
+ /** CLI entry point; returns the process exit code. */
50
+ export declare function main(argv: string[]): number
51
+
52
+ /** Split a SKILL.md frontmatter block into its routing fields and body. */
53
+ export declare function splitFrontmatter(text: string): { description?: string, whenToUse?: string, body: string }
54
+
55
+ /** Read and validate a packaged skill bundle (fails loud). */
56
+ export declare function readSkillBundle(skillsRoot: string, skillName: string): {
57
+ frontmatterName: string
58
+ description?: string
59
+ whenToUse?: string
60
+ body: string
61
+ skillDir: string
62
+ }
63
+
64
+ export declare const name: string
65
+ export declare const inject: string[]
66
+ export declare const Config: unknown
67
+ export declare function apply(ctx: unknown, config?: Config): void