dsh-openwrite 0.2.28 → 0.2.34

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.
Files changed (125) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +49 -8
  3. package/THIRD_PARTY_NOTICES.md +4 -3
  4. package/companions.mjs +508 -0
  5. package/cordis.patch.yml +0 -2
  6. package/docs/CLOSED_ISSUE_FOLLOWUPS.md +1 -1
  7. package/docs/COMPANION_PLUGINS.md +142 -0
  8. package/docs/CONTEXT_SLOTS.md +1 -1
  9. package/docs/COST_MEASUREMENT.md +2 -2
  10. package/docs/DEVELOPMENT.md +6 -11
  11. package/docs/DSH_STORE.md +14 -2
  12. package/docs/EDITOR_SAFETY_REPAIRS.md +2 -2
  13. package/docs/INSTALL.md +112 -4
  14. package/docs/ISSUES_121_123.md +4 -4
  15. package/docs/ISSUES_140_142.md +58 -0
  16. package/docs/ISSUES_149_150_151.md +77 -0
  17. package/docs/ISSUE_123_FOLLOWUP.md +3 -2
  18. package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
  19. package/docs/LEARNING.md +1 -1
  20. package/docs/MOBILE_READINESS.md +62 -0
  21. package/docs/PLUGIN_MAINTENANCE.md +1 -3
  22. package/docs/RELEASE_ACCEPTANCE.md +20 -3
  23. package/docs/REVIEW_ROUND_22.md +67 -0
  24. package/docs/REVIEW_ROUND_23.md +48 -0
  25. package/docs/REVIEW_ROUND_25.md +51 -0
  26. package/docs/SELECTION_UPGRADE_REPAIRS.md +2 -2
  27. package/docs/TOOLS.md +181 -604
  28. package/docs/WORKFLOWS.md +5 -5
  29. package/package.json +17 -21
  30. package/packages/openwrite-bridge/lib/client.d.ts +0 -1
  31. package/packages/openwrite-bridge/lib/client.js +90 -9
  32. package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
  33. package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
  34. package/packages/openwrite-bridge/lib/dog-delivery.js +3 -1
  35. package/packages/openwrite-bridge/lib/dog-review.js +15 -3
  36. package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
  37. package/packages/openwrite-bridge/lib/domain.js +250 -29
  38. package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
  39. package/packages/openwrite-bridge/lib/host-llm.js +657 -0
  40. package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
  41. package/packages/openwrite-bridge/lib/host-web.js +166 -0
  42. package/packages/openwrite-bridge/lib/index.d.ts +4 -0
  43. package/packages/openwrite-bridge/lib/index.js +150 -6
  44. package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
  45. package/packages/openwrite-bridge/lib/managed-runtime.js +176 -34
  46. package/packages/openwrite-bridge/lib/tool-policy.js +37 -12
  47. package/packages/openwrite-bridge/lib/tools.js +1223 -1227
  48. package/packages/openwrite-bridge/package.json +7 -5
  49. package/packages/studio-panel/lib/client.js +123207 -121534
  50. package/packages/studio-panel/lib/client.js.map +1 -1
  51. package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
  52. package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
  53. package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
  54. package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
  55. package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
  56. package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
  57. package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
  58. package/packages/studio-panel/lib/types/client/VditorBody.d.ts +1 -1
  59. package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
  60. package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
  61. package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
  62. package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
  63. package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
  64. package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
  65. package/packages/studio-panel/package.json +5 -3
  66. package/plugin.mjs +159 -20
  67. package/preset-definition.mjs +144 -4
  68. package/presets/openwrite/agent.cordis.yml +7 -9
  69. package/presets/openwrite/ops-integration.mjs +28 -0
  70. package/presets/openwrite/preset.yml +2 -1
  71. package/presets/openwrite/skills/canon/SKILL.md +3 -3
  72. package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
  73. package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
  74. package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
  75. package/presets/openwrite/skills/export-book/SKILL.md +3 -3
  76. package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
  77. package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
  78. package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
  79. package/presets/openwrite/skills/learn/SKILL.md +3 -3
  80. package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
  81. package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
  82. package/presets/openwrite/skills/progress/SKILL.md +1 -1
  83. package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
  84. package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
  85. package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
  86. package/presets/openwrite/skills/style-system/SKILL.md +13 -6
  87. package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
  88. package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
  89. package/presets/openwrite/skills/world-query/SKILL.md +16 -11
  90. package/presets/openwrite/skills/write-next/SKILL.md +1 -1
  91. package/release/{openwrite-5.8.20-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
  92. package/release/runtime-manifest.json +4 -13
  93. package/release/source.json +21 -21
  94. package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
  95. package/scripts/maintenance.mjs +67 -7
  96. package/scripts/plugin-doctor.mjs +179 -0
  97. package/scripts/prepare.mjs +26 -4
  98. package/scripts/safe-upgrade.mjs +38 -5
  99. package/dog-tools.mjs +0 -13
  100. package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
  101. package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
  102. package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
  103. package/scripts/dog/delivery-stage.js +0 -32
  104. package/scripts/dog/import-chapter.js +0 -20
  105. package/scripts/dog/import-record.js +0 -35
  106. package/scripts/dog/review-dimension.js +0 -41
  107. package/scripts/dog/review-record.js +0 -42
  108. package/vendor/dsh-dog/LICENSE +0 -29
  109. package/vendor/dsh-dog/README.md +0 -114
  110. package/vendor/dsh-dog/UPSTREAM.md +0 -30
  111. package/vendor/dsh-dog/cordis.patch.yml +0 -14
  112. package/vendor/dsh-dog/lib/client.js +0 -2960
  113. package/vendor/dsh-dog/lib/client.js.map +0 -1
  114. package/vendor/dsh-dog/lib/core--byUPJL8.js +0 -2191
  115. package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
  116. package/vendor/dsh-dog/lib/core.d.ts +0 -2
  117. package/vendor/dsh-dog/lib/core.js +0 -2
  118. package/vendor/dsh-dog/lib/index.d.ts +0 -143
  119. package/vendor/dsh-dog/lib/index.js +0 -1230
  120. package/vendor/dsh-dog/package.json +0 -110
  121. package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
  122. package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
  123. package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
  124. package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
  125. package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ // Offline maintenance checks. Never reads credentials, starts a model, or edits profiles.
3
+ import { readFile, realpath } from 'node:fs/promises'
4
+ import { execFileSync } from 'node:child_process'
5
+ import { fileURLToPath, pathToFileURL } from 'node:url'
6
+ import { isAbsolute, join, resolve } from 'node:path'
7
+ import { homedir } from 'node:os'
8
+ import { load } from 'js-yaml'
9
+ import { inspectProfile } from './maintenance.mjs'
10
+ import { detectCompanions } from '../companions.mjs'
11
+
12
+ const root = fileURLToPath(new URL('../', import.meta.url))
13
+ const packageDirs = ['.', 'packages/openwrite-bridge', 'packages/studio-panel']
14
+ const isDsh = name => /^@deepseek-ai\/dsh(?:-|$)/.test(name)
15
+ const lockName = path => path.match(/(?:^|\/)node_modules\/(@deepseek-ai\/[^/]+)$/)?.[1]
16
+ const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
17
+ const exactVersion = value => /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(value ?? '')
18
+
19
+ export function auditVersions(manifest, lock, baseline, graph = {}) {
20
+ const errors = []
21
+ if (!exactVersion(baseline)) errors.push('DSH baseline must be an exact version')
22
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
23
+ const declared = manifest[field] ?? {}
24
+ const locked = lock.packages?.['']?.[field] ?? {}
25
+ for (const name of new Set([...Object.keys(declared), ...Object.keys(locked)])) {
26
+ if (declared[name] !== locked[name]) errors.push(`lock metadata differs: ${field}.${name}`)
27
+ // Build dependencies remain pinned; public peers may enumerate verified hosts.
28
+ const peerVersions = typeof declared[name] === 'string' ? declared[name].split(' || ') : []
29
+ const verified = manifest.dsh?.compatibility?.dshReleases ?? {}
30
+ const compatiblePeer = field === 'peerDependencies' && peerVersions.length > 0
31
+ && peerVersions.includes(graph[name] ?? baseline)
32
+ && peerVersions.every(value => exactVersion(value)
33
+ && (value === (graph[name] ?? baseline) || verified[value] === 'compatible'))
34
+ if (isDsh(name) && declared[name] !== undefined && declared[name] !== (graph[name] ?? baseline) && !compatiblePeer) {
35
+ errors.push(`unpinned DSH dependency: ${name}=${declared[name]}`)
36
+ }
37
+ if (isDsh(name) && declared[name] !== undefined
38
+ && (field !== 'peerDependencies' || !manifest.peerDependenciesMeta?.[name]?.optional)
39
+ && !lock.packages?.[`node_modules/${name}`]) {
40
+ errors.push(`required SDK missing from lock: ${name}`)
41
+ }
42
+ }
43
+ }
44
+ for (const [path, entry] of Object.entries(lock.packages ?? {})) {
45
+ const name = lockName(path)
46
+ if (name && isDsh(name) && entry.version !== (graph[name] ?? baseline)) {
47
+ errors.push(`DSH lock drift: ${name}=${entry.version}, expected ${baseline}`)
48
+ }
49
+ if (name === '@deepseek-ai/cordis' && entry.version !== (graph[name] ?? '4.0.2')) {
50
+ errors.push(`Cordis lock drift: ${entry.version}, expected ${graph[name] ?? '4.0.2'}`)
51
+ }
52
+ }
53
+ if (!lock.packages?.['']) errors.push('missing lockfile root metadata')
54
+ return errors
55
+ }
56
+
57
+ const exportPaths = value => typeof value === 'string' ? [value]
58
+ : value && typeof value === 'object' ? Object.values(value).flatMap(exportPaths) : []
59
+ const packagePath = path => path?.replace(/^\.\//, '')
60
+
61
+ export function auditBundle(manifest, files, patch) {
62
+ const errors = []
63
+ const available = new Set(files)
64
+ const required = [manifest.main, manifest.types, manifest.dsh?.bundle?.patch,
65
+ ...exportPaths(manifest.exports)]
66
+ if (!manifest.dsh?.bundle?.patch) errors.push('missing dsh.bundle.patch')
67
+ if (!manifest.main || !manifest.types) errors.push('missing host JS/types entry')
68
+ for (const path of required.filter(Boolean)) {
69
+ if (isAbsolute(path) || path.split(/[\\/]/).includes('..') || path.includes('\\')) {
70
+ errors.push(`entry escapes package: ${path}`)
71
+ } else if (!available.has(packagePath(path))) errors.push(`entry missing from tarball: ${path}`)
72
+ }
73
+ const rows = Array.isArray(patch) ? patch.flatMap(op => op?.insert ?? []) : []
74
+ if (rows.filter(row => row.name === manifest.name).length !== 1) {
75
+ errors.push('bundle patch must mount its host package exactly once')
76
+ }
77
+ const ids = rows.map(row => row.id)
78
+ if (ids.some(id => typeof id !== 'string' || !id) || new Set(ids).size !== ids.length) {
79
+ errors.push('bundle patch has missing/duplicate row ids')
80
+ }
81
+ if (manifest.dsh?.client || manifest.exports?.['./client']) {
82
+ if (manifest.dsh?.client?.platform !== 'web' || !manifest.exports?.['./client']) {
83
+ errors.push('web client needs platform=web and ./client export')
84
+ }
85
+ // The host route serves these on demand; a source-tree-only build can mask omissions.
86
+ for (const path of ['LICENSE', 'dist/index.css', 'dist/index.min.js',
87
+ 'dist/css/content-theme/dark.css', 'dist/css/content-theme/light.css',
88
+ 'dist/js/lute/lute.min.js', 'dist/js/icons/ant.js', 'dist/js/i18n/zh_CN.js']) {
89
+ if (!available.has(`vendor/vditor/${path}`)) errors.push(`editor asset missing from tarball: ${path}`)
90
+ }
91
+ }
92
+ return errors
93
+ }
94
+
95
+ async function checkProfiles(report) {
96
+ const home = process.env.DSH_HOME ?? join(process.env.HOME ?? '', '.dsh')
97
+ for (const profile of ['web', 'headless']) {
98
+ const dir = join(home, 'profiles', profile)
99
+ await report(`profile ${profile}`, async () => {
100
+ const manifest = await readJson(join(dir, 'package.json'))
101
+ const name = 'dsh-openwrite'
102
+ if (manifest.dsh?.profile?.bundles?.filter(n => n === name).length !== 1) throw new Error('standard bundle not installed exactly once')
103
+ const installed = await realpath(join(dir, 'node_modules', name))
104
+ const pkg = await readJson(join(installed, 'package.json'))
105
+ if (pkg.name !== name || !pkg.dsh?.bundle?.patch) throw new Error('invalid installed bundle')
106
+ await readFile(join(installed, 'release/runtime-manifest.json'))
107
+ const leftover = inspectProfile(manifest).leftover
108
+ if (leftover.length) throw new Error(`legacy packages still in bundles or dependencies (${leftover.join(', ')}); run openwrite-maintenance migrate --apply with dsh stopped`)
109
+ })
110
+ }
111
+ }
112
+
113
+ export async function doctor({ profiles = false } = {}) {
114
+ let failures = 0
115
+ const report = async (label, check) => {
116
+ try { await check(); console.log(`PASS ${label}`) }
117
+ catch (error) { failures++; console.error(`FAIL ${label}: ${error.message}`) }
118
+ }
119
+ const assertClean = errors => { if (errors.length) throw new Error(errors.slice(0, 8).join('; ') + (errors.length > 8 ? `; +${errors.length - 8} more` : '')) }
120
+ const rootManifest = await readJson(join(root, 'package.json'))
121
+ const compatibility = await readJson(join(root, 'release/sdk-compatibility.json'))
122
+ const baseline = rootManifest.devDependencies?.['@deepseek-ai/dsh']
123
+ console.log(`dsh-Openwrite doctor — DSH ${baseline}, Cordis 4.0.2 (offline)`)
124
+ await report('Node >= 22.19.0', () => {
125
+ const [major, minor] = process.versions.node.split('.').map(Number)
126
+ if (major < 22 || (major === 22 && minor < 19)) throw new Error(`found ${process.versions.node}`)
127
+ })
128
+ for (const relative of packageDirs) {
129
+ const dir = resolve(root, relative)
130
+ const manifest = await readJson(join(dir, 'package.json'))
131
+ const lock = await readJson(join(dir, 'package-lock.json'))
132
+ await report(`${relative}: manifest + lock compatibility`, () => assertClean(auditVersions(manifest, lock, baseline, compatibility.packages)))
133
+ await report(`${relative}: installed SDK matches lock`, async () => {
134
+ const errors = []
135
+ for (const [path, entry] of Object.entries(lock.packages ?? {})) {
136
+ const name = lockName(path)
137
+ if (!name || (!isDsh(name) && name !== '@deepseek-ai/cordis')) continue
138
+ try {
139
+ const installed = await readJson(join(dir, path, 'package.json'))
140
+ if (installed.version !== entry.version) errors.push(`${name}=${installed.version}, lock=${entry.version}`)
141
+ } catch { errors.push(`missing ${name}; run npm ci in ${relative}`) }
142
+ }
143
+ assertClean(errors)
144
+ })
145
+ if (relative === '.') continue
146
+ await report(`${manifest.name}: packed entries + patch + assets`, async () => {
147
+ const packed = JSON.parse(execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'],
148
+ { cwd: dir, encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] }))
149
+ const patch = load(await readFile(join(dir, manifest.dsh.bundle.patch), 'utf8'))
150
+ assertClean(auditBundle(manifest, packed[0].files.map(file => file.path), patch))
151
+ const host = await import(pathToFileURL(join(dir, manifest.main)).href)
152
+ if (host.name !== manifest.name || typeof host.apply !== 'function') throw new Error('host must export name + apply')
153
+ })
154
+ }
155
+ if (profiles) await checkProfiles(report)
156
+ await report('companion plugins', async () => {
157
+ const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
158
+ const companions = await detectCompanions(home)
159
+ const lines = []
160
+ if (companions.dog.installed) lines.push(`dsh-dog ${companions.dog.usable ? 'active' : `inactive (${companions.dog.reason})`}`)
161
+ if (companions.ops.installed) lines.push(`dsh-ops ${companions.ops.usable ? 'active' : `inactive (${companions.ops.reason})`}`)
162
+ if (companions.freeModel.installed) lines.push(`dsh-our-free-model ${companions.freeModel.usable ? 'active' : `inactive (${companions.freeModel.reason})`}`)
163
+ if (companions.host === null && (companions.dog.installed || companions.ops.installed || companions.freeModel.installed)) {
164
+ throw new Error(`host version unresolved; companion decisions stay fail-closed (${lines.join(', ')})`)
165
+ }
166
+ console.log(` ${lines.join(', ') || 'none installed'}`)
167
+ })
168
+ console.log(failures ? `${failures} check(s) failed.` : 'All checks passed. No model or live-service call was made.')
169
+ return failures ? 1 : 0
170
+ }
171
+
172
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
173
+ const unknown = process.argv.slice(2).filter(arg => arg !== '--profiles')
174
+ if (unknown.length) { console.error('Usage: node scripts/plugin-doctor.mjs [--profiles]'); process.exitCode = 2 }
175
+ else {
176
+ try { process.exitCode = await doctor({ profiles: process.argv.includes('--profiles') }) }
177
+ catch (error) { console.error(`FAIL ${error.message}`); process.exitCode = 1 }
178
+ }
179
+ }
@@ -1,21 +1,43 @@
1
1
  // Git installs build here only after the user authorizes this package in pnpm.
2
2
  // Precompiled Release tarballs need no install script or sibling checkout.
3
- import { access, readFile } from 'node:fs/promises'
3
+ import { access, readFile, stat } from 'node:fs/promises'
4
4
  import { spawnSync } from 'node:child_process'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  const root = fileURLToPath(new URL('../', import.meta.url))
7
7
  const manifest = JSON.parse(await readFile(new URL('../release/runtime-manifest.json', import.meta.url), 'utf8'))
8
8
  let built = false
9
9
  await access(new URL('../release/' + manifest.wheel.file, import.meta.url))
10
- for (const dir of ['packages/openwrite-bridge', 'packages/studio-panel', 'vendor/dsh-dog']) {
11
- try { await access(new URL(`../${dir}/lib/index.js`, import.meta.url)); await access(new URL(`../${dir}/lib/${dir.endsWith('openwrite-bridge') ? 'preset-tools.js' : 'client.js'}`, import.meta.url)); continue } catch {}
10
+ // The plugin version is baked into the client bundle as the preset identity
11
+ // (`openwrite-<version>`), so a package.json edited after the last build
12
+ // leaves the bundle naming the previous release. A build that only checks
13
+ // whether the outputs exist packs that stale identity, and the host then
14
+ // answers `preset "openwrite-<old>" not found`. Rebuild whenever the manifest
15
+ // is newer than the artifacts it feeds.
16
+ const manifestStamp = (await stat(new URL('../package.json', import.meta.url))).mtimeMs
17
+ for (const dir of ['packages/openwrite-bridge', 'packages/studio-panel']) {
18
+ const outputs = [
19
+ new URL(`../${dir}/lib/index.js`, import.meta.url),
20
+ new URL(`../${dir}/lib/${dir.endsWith('openwrite-bridge') ? 'preset-tools.js' : 'client.js'}`, import.meta.url),
21
+ ]
22
+ let newest = 0
23
+ try { for (const file of outputs) newest = Math.max(newest, (await stat(file)).mtimeMs) } catch { newest = 0 }
24
+ if (newest !== 0 && newest >= manifestStamp) continue
12
25
  built = true
13
26
  for (const args of [['ci', '--ignore-scripts', '--no-audit', '--no-fund'], ['run', 'build']]) {
14
27
  const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { cwd: root + dir, stdio: 'inherit', shell: process.platform === 'win32' })
15
28
  if (result.status !== 0) throw new Error(`${dir}: ${args.join(' ')} failed`)
16
29
  }
17
30
  }
18
- if (built) {
31
+ // Provenance must be refreshed whenever the source tree no longer matches the
32
+ // recorded hash, not merely when this run happened to build. Gating on `built`
33
+ // alone means "edit src/, then pack without rebuilding" publishes a tarball
34
+ // whose release/source.json still names the previous tree — and that file is
35
+ // gitignored, so release-report.mjs's dirty check cannot see it either.
36
+ let provenanceStale = true
37
+ try {
38
+ provenanceStale = !JSON.parse(await readFile(new URL('../release/source.json', import.meta.url), 'utf8')).sourceTreeSha256
39
+ } catch { /* missing or unreadable: regenerate */ }
40
+ if (built || provenanceStale) {
19
41
  const result = spawnSync(process.execPath, [fileURLToPath(new URL('./release-provenance.mjs', import.meta.url))], { cwd: root, stdio: 'inherit' })
20
42
  if (result.status !== 0) throw new Error('Release provenance generation failed')
21
43
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Offline profile upgrades retain the previous complete dependency tree until commit.
3
- import { cp, mkdir, readFile, readdir, rename, writeFile, open, access, readlink, symlink, unlink, realpath, lstat, rmdir, utimes } from 'node:fs/promises'
3
+ import { cp, mkdir, readFile, readdir, rename, writeFile, open, access, readlink, symlink, unlink, realpath, lstat, rmdir, rm, utimes } from 'node:fs/promises'
4
4
  import { spawn, spawnSync } from 'node:child_process'
5
5
  import { join, resolve, relative, isAbsolute, dirname, sep } from 'node:path'
6
6
  import { randomUUID, createHash } from 'node:crypto'
@@ -163,6 +163,16 @@ async function runInstall(home, profile, spec, log) {
163
163
  } finally { clearTimeout(timer); clearTimeout(drainTimer); clearTimeout(escalation); await writes }
164
164
  }
165
165
 
166
+ /**
167
+ * `.plugin-manager` is host-owned state, not ours: it must never be staged, or a
168
+ * POSIX upgrade would commit a snapshot of it that later upgrades then restore.
169
+ * Compared on a normalized relative path so the check is separator-agnostic —
170
+ * `path.join` yields `\` on win32, which a literal '/' test can never match.
171
+ */
172
+ function isHostManagedState(relativePath) {
173
+ return relativePath === '.plugin-manager' || relativePath.startsWith(`.plugin-manager${sep}`)
174
+ }
175
+
166
176
  function profileDirectory(home, profile) {
167
177
  if (!PROFILE.test(profile)) throw new Error('Invalid profile name')
168
178
  return join(resolve(home), 'profiles', profile)
@@ -175,7 +185,8 @@ export async function recoverUpgrade(home, operationId, options = {}) {
175
185
  const operation = JSON.parse(await readFile(join(root, 'operation.json'), 'utf8'))
176
186
  const target = profileDirectory(home, operation.profile)
177
187
  await (options.assertStopped || assertHostStopped)(home, operation.profile)
178
- const previous = join(root, 'previous-profile')
188
+
189
+ const previous = join(root, 'previous-profile')
179
190
  const lease = await acquireRecoveryLock(home)
180
191
  try {
181
192
  if (!await exists(target) && await exists(previous)) {
@@ -200,6 +211,9 @@ export async function recoverUpgrade(home, operationId, options = {}) {
200
211
  /** Stage a complete profile beside the active tree, validate, then swap while stopped. */
201
212
  export async function upgradeProfile(home, profile, spec, options = {}) {
202
213
  if (typeof spec !== 'string' || !spec || spec.startsWith('-') || /[\r\n]/.test(spec)) throw new Error('A package spec or Release tarball is required')
214
+ // A bare add can preserve a profile's old exact dependency/lock selection
215
+ // even after npm latest moved (#142). An upgrade must request a new version.
216
+ if (spec === 'dsh-openwrite') spec = 'dsh-openwrite@latest'
203
217
  const target = profileDirectory(home, profile)
204
218
  const stopped = options.assertStopped || assertHostStopped
205
219
  await stopped(home, profile)
@@ -222,7 +236,7 @@ export async function upgradeProfile(home, profile, spec, options = {}) {
222
236
  operation.old_manifest_sha256 = digest(oldManifest)
223
237
  await save(join(root, 'operation.json'), operation)
224
238
  await cp(target, staging, { recursive: true, dereference: false, verbatimSymlinks: true,
225
- filter: source => !source.includes('/.plugin-manager/') && !source.endsWith('/.plugin-manager') })
239
+ filter: source => !isHostManagedState(relative(target, source)) })
226
240
  await relocateProfileLinks(staging, target, staging, yaml)
227
241
  // A downloaded tarball may live in a disposable directory. Pin its bytes
228
242
  // below DSH_HOME before the installer writes file: references into locks.
@@ -266,7 +280,16 @@ export async function upgradeProfile(home, profile, spec, options = {}) {
266
280
  await save(join(root, 'operation.json'), operation)
267
281
  error.message += `; operation=${id}; log=${logPath}`
268
282
  throw error
269
- } finally { try { await log?.close() } finally { await release() } }
283
+ } finally {
284
+ try { await log?.close() } finally {
285
+ // The staging directory is a full profile copy including node_modules, so
286
+ // every failed upgrade used to leave hundreds of MB behind with nothing to
287
+ // reclaim it. On success it has been renamed onto the target, so removing
288
+ // it here is a no-op; on any failure path it is reclaimed.
289
+ try { await rm(staging, { recursive: true, force: true }) } catch { /* best effort */ }
290
+ await release()
291
+ }
292
+ }
270
293
  }
271
294
 
272
295
  /** Disable only an incomplete OpenWrite bundle so a stopped profile can boot for repair. */
@@ -297,7 +320,17 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1]
297
320
  const value = name => { const i = process.argv.indexOf(name); return i < 0 ? undefined : process.argv[i + 1] }
298
321
  const home = resolve(process.env.DSH_HOME || join(homedir(), '.dsh'))
299
322
  try {
300
- const profile = value('--profile') || 'web'
323
+ // `--profile` as the last argument yields undefined, and `|| 'web'` then
324
+ // silently operates on `web` and exits 0 - the same bug fixed in
325
+ // maintenance.mjs. A trailing flag is a usage error, not a default.
326
+ const profileIndex = process.argv.indexOf('--profile')
327
+ if (profileIndex >= 0) {
328
+ const named = process.argv[profileIndex + 1]
329
+ if (named === undefined || named === '') {
330
+ throw new Error('Usage: openwrite-safe-upgrade [--upgrade|--repair|--recover <id>] --profile <name> [--apply] [--package <path>]')
331
+ }
332
+ }
333
+ const profile = profileIndex < 0 ? 'web' : process.argv[profileIndex + 1]
301
334
  const result = value('--recover') ? await recoverUpgrade(home, value('--recover'))
302
335
  : process.argv.includes('--repair') ? await repairProfile(home, profile, process.argv.includes('--apply'))
303
336
  : await upgradeProfile(home, profile, value('--package'))
package/dog-tools.mjs DELETED
@@ -1,13 +0,0 @@
1
- export const name = '@dsh-novel/openwrite-dog-tools'
2
- export const inject = ['tools', 'openwriteDog']
3
- export function apply(ctx) {
4
- for (const tool of ctx.openwriteDog.tools) ctx.effect(() => ctx.tools.register(tool))
5
- ctx.effect(() => ctx.tools.guard(execution => {
6
- if (!execution.name.startsWith('dog_') || ['dog_status', 'dog_wait', 'dog_validate'].includes(execution.name)) return
7
- if (!execution.agent) return 'DOG_AGENT_SESSION_REQUIRED: DoG execution requires an Agent session; run this tool inside a writing Agent session.'
8
- if (execution.agent.ctx.get('planMode')?.get(execution.agent)?.active) return 'DOG_PLAN_MODE: DoG execution is disabled in plan mode; leave plan mode before running it.'
9
- const policy = ctx.get('sandboxPolicy')
10
- if (!policy) return 'DOG_SANDBOX_POLICY_UNAVAILABLE: The host sandbox policy service is unavailable; check the host configuration.'
11
- if (policy.resolve({ session: execution.agent.session }).mode === 'read-only') return 'DOG_READ_ONLY: DoG execution requires a writable session; the current sandbox is read-only.'
12
- }))
13
- }
@@ -1,42 +0,0 @@
1
- ---
2
- name: dog-delivery-query
3
- description: Query a chapter delivery DoG graph across manuscript, review, revision application, and re-review closure. Use after writing or reviewing a chapter, after revision actions, or when the user asks whether a chapter is truly ready to deliver.
4
- user-invocable: false
5
- ---
6
-
7
- Language: follow the author's current conversation language for responses and help; preserve the manuscript language. Translate these instructions when presenting them.
8
-
9
- # 章节交付 DoG 查询
10
-
11
- 章节写作、评审和修订工具会生成:
12
-
13
- `data/novels/{id}/data/dog/deliveries/{chapter}/dog-graph.json`
14
-
15
- 这张图查询六个阶段:`writing → review → revision → application → rereview → closure`。
16
- 它不替代 OpenWrite 的写章、六域累加评审或修订服务。
17
-
18
- ## 查询流程
19
-
20
- 1. 从最近一次 `novel_write_chapter`、`novel_review_chapter`、`novel_revision_*`
21
- 或 `novel_task_get` 结果的 `dog_delivery.graphPath` 找到图;conductor 输出使用
22
- `dog_delivery_graph_path`。
23
- 2. 依次调用 `dog_validate`、`dog_create`、`dog_run`、`dog_wait`,等待终态。
24
- 3. 用 `dog_status` 读取 `review`、`revision` 和 `closure` evidence,再决定领域动作。
25
- 4. 修订应用后必须重新调用 `novel_review_chapter`;只有复评对应当前正文 SHA 且门禁通过,
26
- `closure` 才能成为 `closed`。
27
-
28
- ## 状态解释
29
-
30
- - `review.missing`:正文存在但尚未评审。
31
- - `closure.review_failed`:当前正文已评审但未通过门禁,进入修订闭环。
32
- - `revision.proposal_pending`:提案已生成,等待作者确认应用或驳回。
33
- - `revision.applied_requires_rereview`:修订已写入正文,但不能视为问题已解决。
34
- - `closure.rereview_required`:必须对修订后的正文重新跑六域评审。
35
- - `closure.closed`:当前正文的最新评审通过,可以交付。
36
-
37
- ## 约束
38
-
39
- - DoG 只查询 canonical manuscript/review/revision 文件,不直接修改它们。
40
- - 不因提案状态为 `applied` 就宣称问题关闭;必须以当前正文 SHA 对应的复评为准。
41
- - `needs_human` 是等待评审、确认或复评,不得描述成成功。
42
- - 应用或驳回修订仍遵守 OpenWrite 创作 Agent 的用户确认边界。
@@ -1,29 +0,0 @@
1
- ---
2
- name: dog-import-query
3
- description: Query a smart-import DoG graph after a novel has been split into chapters. Use to inspect import completeness, AI precheck errors, and pending outline/asset/canonical setup.
4
- user-invocable: false
5
- ---
6
-
7
- Language: follow the author's current conversation language for responses and help; preserve the manuscript language. Translate these instructions when presenting them.
8
-
9
- # 拆书导入 DoG 验收
10
-
11
- `conductor/smart_import.py` 完成导入后会生成 `dog-graph.json`。这张图只验收导入
12
- 结果,不负责写入章节、资产或大纲。
13
-
14
- ## 工作流
15
-
16
- 1. 从导入输出中找到 `data/novels/{id}/data/dog/imports/{IMPORT_ID}/dog-graph.json`。
17
- 2. 依次调用 `dog_validate`、`dog_create`、`dog_run`、`dog_wait`,直到终态。
18
- 3. 用 `dog_status` 查看 `manifest` 和每个 `chapter-ch_*` 节点的 evidence。
19
- 4. 根节点通过后,读取 manifest 的 `construction.nextActions`,再调用规划和资产工具建立大纲、角色、世界观、进度和正典事件。
20
- 5. 资产/大纲建立完成后重新运行 `novel_review_chapter`,再用 `dog-review-query`
21
- 查询六域评审和 37 个 legacy leaf。
22
-
23
- ## 约束
24
-
25
- - DoG 失败只说明导入文件缺失、为空或 manifest 不一致;不要直接修改正文。
26
- - `status=partial|failed` 表示导入中途出错;先读取 manifest 的 `error` 和已完成的章节,再修复或重试导入。
27
- - `aiCheck.status=failed` 要如实报告,不能当成“无问题”。
28
- - `construction` 中的 `pending` 是后续建模待办,不是导入失败。
29
- - 不要让 DoG verifier 再调用 Studio 导入接口或重新读取原始整本小说。
@@ -1,42 +0,0 @@
1
- ---
2
- name: dog-review-query
3
- description: Query an existing hierarchical OpenWrite review-v2 artifact through dsh-dog. Use after novel_review_chapter or when inspecting six quality domains, evidence, coverage, blockers, delivery status, or the 37 legacy checks. Never rerun model review from DoG.
4
- user-invocable: false
5
- ---
6
-
7
- Language: follow the author's current conversation language for responses and help; preserve the manuscript language. Translate these instructions when presenting them.
8
-
9
- # 分层评审 DoG 查询
10
-
11
- 这个 Skill 只查询 OpenWrite 已物化的 review-v2 artifact。OpenWrite 的
12
- `novel_review_chapter` / conductor 是评审真源;DoG 读取 47 节点分层图:上下文、
13
- 六个质量域、硬门禁、确定性聚合和 37 个兼容查询 leaf。DoG 不调用评审模型。
14
-
15
- ## 工作流
16
-
17
- 1. 如果本轮还没有评审结果,先按 OpenWrite 写作流程调用 `novel_review_chapter`。
18
- 2. 找到评审输出目录中的 `dog-graph.json`。graph 与 `review.json`、`context.json`、
19
- `domain-*.json`、`gate.json`、`aggregate.json` 和 `dim_01.json` …
20
- `dim_37.json` 在同一目录。
21
- 3. 读取 graph 后依次调用 `dog_validate`、`dog_create`、`dog_run`、`dog_wait`。
22
- 所有节点均使用 programmatic `review-record` verifier;`dog_run` 后必须等到终态。
23
- 4. 用 `dog_status` 分别汇报 `qualityScore`、`coverage`、`gateStatus` 和
24
- `deliveryStatus`,再按需展开域、criterion、evidence、issue 和 legacy ID。
25
- `inconclusive` 表示覆盖不足、未请求或 artifact 不可判定,不能描述为通过。
26
- 5. 只有 OpenWrite 的审查问题才能进入 `novel_revisions_*` 回炉闭环。应用修订后
27
- 必须复评,再用 `dog-delivery-query` 确认 `closure.closed`。DoG 的 `failure`
28
- 只负责定位问题,不直接改正文或资产。
29
-
30
- ## 状态约定
31
-
32
- - `qualityScore` 是有可定位正文证据的正向累加分;issue 不直接扣分。
33
- - `coverage` 单独表达实际完成的评审范围;`inconclusive` 会降低覆盖率。
34
- - `gateStatus=blocked` 来自 hard-gate 或 critical finding,不改写质量分。
35
- - `deliveryStatus` 综合质量、覆盖、门禁和正文 SHA 新鲜度;它不是 `passed` 的别名。
36
- - 37 个 leaf 是 `legacy_check_ids` 查询入口,不是 37 个顶层计分维度。
37
-
38
- ## 边界
39
-
40
- - 不要用 DoG verifier 直接调用 Studio 写入接口。
41
- - 不要把 warning 自动升级成失败;先展示 evidence,由 `review_gate` 和作者决定。
42
- - 不要用 `dog_run` 生成、补写或刷新评审;artifact 缺失或 stale 时回到 OpenWrite 流程。
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env node
2
- // dsh-dog verifier for one materialized chapter-delivery stage.
3
-
4
- import { readFile } from 'node:fs/promises'
5
-
6
- const inputPath = process.argv[2]
7
- if (!inputPath) {
8
- process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
9
- process.exit(0)
10
- }
11
-
12
- try {
13
- const value = JSON.parse(await readFile(inputPath, 'utf8'))
14
- if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('delivery stage must be an object')
15
- if (!['delivery-stage', 'chapter-delivery'].includes(value.recordType)) throw new Error('record is not a delivery artifact')
16
- if (!['pass', 'fail', 'inconclusive'].includes(value.verdict)) throw new Error('delivery stage has an invalid verdict')
17
- process.stdout.write(JSON.stringify({
18
- verdict: value.verdict,
19
- evidence: {
20
- chapterId: value.chapterId,
21
- stage: value.stage,
22
- status: value.status ?? (value.readyForDelivery ? 'ready' : 'not_ready'),
23
- readyForDelivery: value.readyForDelivery,
24
- ...(value.evidence !== null && typeof value.evidence === 'object' ? value.evidence : {}),
25
- },
26
- }))
27
- } catch (error) {
28
- process.stdout.write(JSON.stringify({
29
- verdict: 'inconclusive',
30
- evidence: { error: error instanceof Error ? error.message : String(error) },
31
- }))
32
- }
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env node
2
- // dsh-dog verifier for one imported manuscript chapter.
3
-
4
- import { readFile } from 'node:fs/promises'
5
-
6
- const inputPath = process.argv[2]
7
- if (!inputPath) {
8
- process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
9
- process.exit(0)
10
- }
11
-
12
- try {
13
- const text = await readFile(inputPath, 'utf8')
14
- if (!text.trim()) throw new Error('chapter file is empty')
15
- const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? ''
16
- if (!heading) throw new Error('chapter file has no markdown title')
17
- process.stdout.write(JSON.stringify({ verdict: 'pass', evidence: { heading, bytes: Buffer.byteLength(text) } }))
18
- } catch (error) {
19
- process.stdout.write(JSON.stringify({ verdict: 'fail', evidence: { error: error instanceof Error ? error.message : String(error) } }))
20
- }
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
- // dsh-dog verifier for a smart-import manifest.
3
-
4
- import { readFile } from 'node:fs/promises'
5
-
6
- const inputPath = process.argv[2]
7
- if (!inputPath) {
8
- process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
9
- process.exit(0)
10
- }
11
-
12
- try {
13
- const value = JSON.parse(await readFile(inputPath, 'utf8'))
14
- if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('manifest must be an object')
15
- if (value.recordType !== 'smart-import' || !['completed', 'partial', 'failed'].includes(value.status)) throw new Error('manifest has an invalid status')
16
- if (!Number.isInteger(value.chapterCount) || !Array.isArray(value.chapters) || value.chapterCount !== value.chapters.length) {
17
- throw new Error('chapterCount does not match chapters')
18
- }
19
- const invalid = value.chapters.some(item => item === null || typeof item !== 'object' || typeof item.target !== 'string' || !item.target.endsWith('.md'))
20
- if (invalid) throw new Error('manifest contains an invalid chapter target')
21
- process.stdout.write(JSON.stringify({
22
- verdict: value.status === 'completed' ? 'pass' : 'fail',
23
- evidence: {
24
- importId: value.importId,
25
- status: value.status,
26
- error: value.error,
27
- source: value.source,
28
- chapterCount: value.chapterCount,
29
- aiCheck: value.aiCheck,
30
- construction: value.construction,
31
- },
32
- }))
33
- } catch (error) {
34
- process.stdout.write(JSON.stringify({ verdict: 'fail', evidence: { error: error instanceof Error ? error.message : String(error) } }))
35
- }
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
- // dsh-dog programmatic verifier for one materialized OpenWrite dimension.
3
-
4
- import { readFile } from 'node:fs/promises'
5
-
6
- const inputPath = process.argv[2]
7
- if (!inputPath) {
8
- process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
9
- process.exit(0)
10
- }
11
-
12
- try {
13
- const value = JSON.parse(await readFile(inputPath, 'utf8'))
14
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
15
- throw new Error('dimension record must be an object')
16
- }
17
- if (!['dimension', 'review-dimension'].includes(value.recordType)) {
18
- throw new Error('record is not a review dimension')
19
- }
20
- const verdict = value.verdict
21
- if (verdict !== 'pass' && verdict !== 'fail' && verdict !== 'inconclusive') {
22
- throw new Error('dimension record has an invalid verdict')
23
- }
24
- process.stdout.write(JSON.stringify({
25
- verdict,
26
- evidence: {
27
- chapterId: value.chapterId,
28
- dimension: value.dimension,
29
- name: value.name,
30
- status: value.status,
31
- criterionId: value.criterionId,
32
- issueCount: value.issueCount,
33
- issues: Array.isArray(value.issues) ? value.issues : [],
34
- },
35
- }))
36
- } catch (error) {
37
- process.stdout.write(JSON.stringify({
38
- verdict: 'inconclusive',
39
- evidence: { error: error instanceof Error ? error.message : String(error) },
40
- }))
41
- }
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env node
2
- // Programmatic verifier for hierarchical review context/domain/gate/aggregate records.
3
-
4
- import { readFile } from 'node:fs/promises'
5
-
6
- const inputPath = process.argv[2]
7
- if (!inputPath) {
8
- process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
9
- process.exit(0)
10
- }
11
-
12
- try {
13
- const value = JSON.parse(await readFile(inputPath, 'utf8'))
14
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
15
- throw new Error('review record must be an object')
16
- }
17
- if (!String(value.recordType ?? '').startsWith('review')) {
18
- throw new Error('recordType is not a review artifact')
19
- }
20
- if (!['pass', 'fail', 'inconclusive'].includes(value.verdict)) {
21
- throw new Error('review record has an invalid verdict')
22
- }
23
- process.stdout.write(JSON.stringify({
24
- verdict: value.verdict,
25
- evidence: {
26
- recordType: value.recordType,
27
- chapterId: value.chapterId,
28
- id: value.id,
29
- status: value.status ?? value.deliveryStatus ?? value.executionStatus,
30
- qualityScore: value.qualityScore,
31
- coverage: value.coverage,
32
- gateStatus: value.gateStatus,
33
- deliveryStatus: value.deliveryStatus,
34
- issueCount: Array.isArray(value.issues) ? value.issues.length : value.issueCount,
35
- },
36
- }))
37
- } catch (error) {
38
- process.stdout.write(JSON.stringify({
39
- verdict: 'inconclusive',
40
- evidence: { error: error instanceof Error ? error.message : String(error) },
41
- }))
42
- }