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.
- package/LICENSE +1 -1
- package/README.md +49 -8
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/companions.mjs +508 -0
- package/cordis.patch.yml +0 -2
- package/docs/CLOSED_ISSUE_FOLLOWUPS.md +1 -1
- package/docs/COMPANION_PLUGINS.md +142 -0
- package/docs/CONTEXT_SLOTS.md +1 -1
- package/docs/COST_MEASUREMENT.md +2 -2
- package/docs/DEVELOPMENT.md +6 -11
- package/docs/DSH_STORE.md +14 -2
- package/docs/EDITOR_SAFETY_REPAIRS.md +2 -2
- package/docs/INSTALL.md +112 -4
- package/docs/ISSUES_121_123.md +4 -4
- package/docs/ISSUES_140_142.md +58 -0
- package/docs/ISSUES_149_150_151.md +77 -0
- package/docs/ISSUE_123_FOLLOWUP.md +3 -2
- package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
- package/docs/LEARNING.md +1 -1
- package/docs/MOBILE_READINESS.md +62 -0
- package/docs/PLUGIN_MAINTENANCE.md +1 -3
- package/docs/RELEASE_ACCEPTANCE.md +20 -3
- package/docs/REVIEW_ROUND_22.md +67 -0
- package/docs/REVIEW_ROUND_23.md +48 -0
- package/docs/REVIEW_ROUND_25.md +51 -0
- package/docs/SELECTION_UPGRADE_REPAIRS.md +2 -2
- package/docs/TOOLS.md +181 -604
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +17 -21
- package/packages/openwrite-bridge/lib/client.d.ts +0 -1
- package/packages/openwrite-bridge/lib/client.js +90 -9
- package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
- package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
- package/packages/openwrite-bridge/lib/dog-delivery.js +3 -1
- package/packages/openwrite-bridge/lib/dog-review.js +15 -3
- package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
- package/packages/openwrite-bridge/lib/domain.js +250 -29
- package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
- package/packages/openwrite-bridge/lib/host-llm.js +657 -0
- package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
- package/packages/openwrite-bridge/lib/host-web.js +166 -0
- package/packages/openwrite-bridge/lib/index.d.ts +4 -0
- package/packages/openwrite-bridge/lib/index.js +150 -6
- package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
- package/packages/openwrite-bridge/lib/managed-runtime.js +176 -34
- package/packages/openwrite-bridge/lib/tool-policy.js +37 -12
- package/packages/openwrite-bridge/lib/tools.js +1223 -1227
- package/packages/openwrite-bridge/package.json +7 -5
- package/packages/studio-panel/lib/client.js +123207 -121534
- package/packages/studio-panel/lib/client.js.map +1 -1
- package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
- package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
- package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
- package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/VditorBody.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
- package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
- package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
- package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
- package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
- package/packages/studio-panel/package.json +5 -3
- package/plugin.mjs +159 -20
- package/preset-definition.mjs +144 -4
- package/presets/openwrite/agent.cordis.yml +7 -9
- package/presets/openwrite/ops-integration.mjs +28 -0
- package/presets/openwrite/preset.yml +2 -1
- package/presets/openwrite/skills/canon/SKILL.md +3 -3
- package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
- package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
- package/presets/openwrite/skills/export-book/SKILL.md +3 -3
- package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
- package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
- package/presets/openwrite/skills/learn/SKILL.md +3 -3
- package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
- package/presets/openwrite/skills/progress/SKILL.md +1 -1
- package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
- package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
- package/presets/openwrite/skills/style-system/SKILL.md +13 -6
- package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
- package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
- package/presets/openwrite/skills/world-query/SKILL.md +16 -11
- package/presets/openwrite/skills/write-next/SKILL.md +1 -1
- package/release/{openwrite-5.8.20-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +4 -13
- package/release/source.json +21 -21
- package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
- package/scripts/maintenance.mjs +67 -7
- package/scripts/plugin-doctor.mjs +179 -0
- package/scripts/prepare.mjs +26 -4
- package/scripts/safe-upgrade.mjs +38 -5
- package/dog-tools.mjs +0 -13
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
- package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
- package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
- package/scripts/dog/delivery-stage.js +0 -32
- package/scripts/dog/import-chapter.js +0 -20
- package/scripts/dog/import-record.js +0 -35
- package/scripts/dog/review-dimension.js +0 -41
- package/scripts/dog/review-record.js +0 -42
- package/vendor/dsh-dog/LICENSE +0 -29
- package/vendor/dsh-dog/README.md +0 -114
- package/vendor/dsh-dog/UPSTREAM.md +0 -30
- package/vendor/dsh-dog/cordis.patch.yml +0 -14
- package/vendor/dsh-dog/lib/client.js +0 -2960
- package/vendor/dsh-dog/lib/client.js.map +0 -1
- package/vendor/dsh-dog/lib/core--byUPJL8.js +0 -2191
- package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
- package/vendor/dsh-dog/lib/core.d.ts +0 -2
- package/vendor/dsh-dog/lib/core.js +0 -2
- package/vendor/dsh-dog/lib/index.d.ts +0 -143
- package/vendor/dsh-dog/lib/index.js +0 -1230
- package/vendor/dsh-dog/package.json +0 -110
- package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
- package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
- package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
- package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
- package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
package/plugin.mjs
CHANGED
|
@@ -3,23 +3,122 @@ import { createHash, randomUUID } from 'node:crypto'
|
|
|
3
3
|
import { lock } from 'proper-lockfile'
|
|
4
4
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
-
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import {
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { legacyPresetIds, legacyAliasName, presetDefinition, referencedPresetIds } from './preset-definition.mjs'
|
|
9
|
+
import { detectCompanions, OPENWRITE_OPS_OPTIONS, summarizeOpsPlan } from './companions.mjs'
|
|
8
10
|
|
|
9
11
|
export const name = 'dsh-openwrite'
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Which companion plugins are installed and loadable here, for the workbench's
|
|
15
|
+
* companion section. Best-effort by design: an unreadable home reports nothing
|
|
16
|
+
* rather than failing the route.
|
|
17
|
+
*/
|
|
18
|
+
async function loadOpsResolver(home, entry) {
|
|
19
|
+
// The companion may live in a profile whose node_modules is not on this
|
|
20
|
+
// plugin's resolution path (dsh-openwrite can be a symlinked dev install), so
|
|
21
|
+
// resolve from the profile that actually owns dsh-ops first.
|
|
22
|
+
const candidates = []
|
|
23
|
+
if (typeof entry.profile === 'string' && entry.profile !== '') {
|
|
24
|
+
try {
|
|
25
|
+
const fromProfile = createRequire(join(home, 'profiles', entry.profile, 'package.json'))
|
|
26
|
+
candidates.push(fromProfile.resolve('dsh-ops/integration'))
|
|
27
|
+
} catch { /* fall through to the normal resolution path */ }
|
|
28
|
+
}
|
|
29
|
+
candidates.push('dsh-ops/integration')
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
try {
|
|
32
|
+
const module = await import(candidate.startsWith('/') ? pathToFileURL(candidate).href : candidate)
|
|
33
|
+
if (typeof module.resolveIntegrationConfig === 'function') return module.resolveIntegrationConfig
|
|
34
|
+
} catch { /* try the next candidate */ }
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function opsPlan(home, entry) {
|
|
40
|
+
if (!entry.usable) return null
|
|
41
|
+
try {
|
|
42
|
+
// Read-only normalization: the same options the preset row uses. A dsh-ops
|
|
43
|
+
// release without this export (or one that changed the option names) is
|
|
44
|
+
// reported as no plan rather than failing the companion inventory.
|
|
45
|
+
const resolveIntegrationConfig = await loadOpsResolver(home, entry)
|
|
46
|
+
if (resolveIntegrationConfig === null) return null
|
|
47
|
+
return summarizeOpsPlan(resolveIntegrationConfig({ ...OPENWRITE_OPS_OPTIONS }))
|
|
48
|
+
} catch { return null }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function companionSummary(home) {
|
|
52
|
+
try {
|
|
53
|
+
const companions = await detectCompanions(home)
|
|
54
|
+
const describe = entry => ({ installed: entry.installed, usable: entry.usable, version: entry.version, reason: entry.reason })
|
|
55
|
+
return {
|
|
56
|
+
host: companions.host,
|
|
57
|
+
dog: describe(companions.dog),
|
|
58
|
+
ops: { ...describe(companions.ops), plan: await opsPlan(home, companions.ops) },
|
|
59
|
+
freeModel: describe(companions.freeModel),
|
|
20
60
|
}
|
|
61
|
+
} catch { return null }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The host's own default-model selection, read from the service the host
|
|
66
|
+
* exposes to plugins (`ctx.agentDefaultModel`).
|
|
67
|
+
*
|
|
68
|
+
* The API key is deliberately NOT part of this answer. The selection is a fact
|
|
69
|
+
* the author already sees in the host's own settings; the key is a secret that
|
|
70
|
+
* must never travel to a browser, and the host keeps it in its credentials
|
|
71
|
+
* service rather than in this selection at all.
|
|
72
|
+
*
|
|
73
|
+
* A host that does not compose the service, or one whose selection is not
|
|
74
|
+
* configured yet, answers `available: false` instead of throwing: the settings
|
|
75
|
+
* page shows "not configured", which is true, rather than failing to open.
|
|
76
|
+
*/
|
|
77
|
+
function hostModelSelection(ctx) {
|
|
78
|
+
const empty = { available: false, provider: '', model: '', reasoningEffort: '' }
|
|
79
|
+
let service
|
|
80
|
+
try { service = ctx.get('agentDefaultModel') } catch { return empty }
|
|
81
|
+
if (service === undefined || service === null) return empty
|
|
82
|
+
let selection
|
|
83
|
+
try { selection = service.currentSelection() } catch { return empty }
|
|
84
|
+
const provider = typeof selection?.provider === 'string' ? selection.provider : ''
|
|
85
|
+
const model = typeof selection?.model === 'string' ? selection.model : ''
|
|
86
|
+
const reasoningEffort = typeof selection?.reasoningEffort === 'string' ? selection.reasoningEffort : ''
|
|
87
|
+
// A selection with no provider or no model names no model: report it as
|
|
88
|
+
// unconfigured rather than as a selection of two empty strings.
|
|
89
|
+
return { available: provider !== '' && model !== '', provider, model, reasoningEffort }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every non-hidden file of a tree, keyed by its path relative to the root. */
|
|
93
|
+
async function listFiles(dir, prefix = '', found = new Map()) {
|
|
94
|
+
for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
95
|
+
// Editor and OS noise is not an author edit: a `.DS_Store` must not make the
|
|
96
|
+
// next activation refuse to start.
|
|
97
|
+
if (entry.name.startsWith('.')) continue
|
|
98
|
+
const key = prefix + entry.name
|
|
99
|
+
const path = join(dir, entry.name)
|
|
100
|
+
if (entry.isDirectory()) await listFiles(path, key + '/', found)
|
|
101
|
+
else found.set(key, path)
|
|
102
|
+
}
|
|
103
|
+
return found
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Digest of a managed preset directory, over the files the package ships only.
|
|
108
|
+
* A foreign file (the author's note, a skill they added, a dangling symlink) is
|
|
109
|
+
* not a modification: counting it would make one stray file brick the plugin.
|
|
110
|
+
*/
|
|
111
|
+
async function digestShipped(source, destination) {
|
|
112
|
+
const shipped = await listFiles(source)
|
|
113
|
+
const present = await listFiles(destination)
|
|
114
|
+
const hash = createHash('sha256')
|
|
115
|
+
for (const key of [...shipped.keys()].sort()) {
|
|
116
|
+
const path = present.get(key)
|
|
117
|
+
if (path === undefined) { hash.update(key + '\u0000missing'); continue }
|
|
118
|
+
hash.update(key)
|
|
119
|
+
try { hash.update(await readFile(path)) }
|
|
120
|
+
catch (error) { if (error.code !== 'ENOENT') hash.update(key + '\u0000unreadable') }
|
|
21
121
|
}
|
|
22
|
-
await visit(dir)
|
|
23
122
|
return hash.digest('hex')
|
|
24
123
|
}
|
|
25
124
|
|
|
@@ -39,12 +138,13 @@ export async function apply(ctx) {
|
|
|
39
138
|
const release = await lock(leases, { realpath: false, stale: 30_000, retries: { retries: 30, minTimeout: 100, maxTimeout: 500 } })
|
|
40
139
|
try { return await operation() } finally { await release() }
|
|
41
140
|
}
|
|
42
|
-
const expected = await digestTree(source)
|
|
43
141
|
const marker = join(destination, '.openwrite-managed.json')
|
|
44
142
|
await withLock(async () => {
|
|
45
143
|
const existing = await readFile(marker, 'utf8').then(JSON.parse).catch(() => null)
|
|
46
|
-
if (existing
|
|
47
|
-
|
|
144
|
+
if (existing) {
|
|
145
|
+
if (existing.source !== await digestShipped(source, destination)) {
|
|
146
|
+
throw new Error(`OpenWrite preset ${id} was modified; copy it to a custom preset before reinstalling this version`)
|
|
147
|
+
}
|
|
48
148
|
}
|
|
49
149
|
if (!existing) {
|
|
50
150
|
if (await lstat(destination).then(() => true).catch(error => { if (error.code === 'ENOENT') return false; throw error })) {
|
|
@@ -54,7 +154,8 @@ export async function apply(ctx) {
|
|
|
54
154
|
await mkdir(staging, { recursive: true })
|
|
55
155
|
try {
|
|
56
156
|
await cp(source, staging, { recursive: true })
|
|
57
|
-
|
|
157
|
+
const digest = await digestShipped(source, staging)
|
|
158
|
+
await writeFile(join(staging, '.openwrite-managed.json'), JSON.stringify({ source: digest, tree: digest, version }))
|
|
58
159
|
await rename(staging, destination)
|
|
59
160
|
} finally { await rm(staging, { recursive: true, force: true }) }
|
|
60
161
|
}
|
|
@@ -79,9 +180,16 @@ export async function apply(ctx) {
|
|
|
79
180
|
if (typeof presetCtx.agentPresets.register !== 'function') return
|
|
80
181
|
const definition = await presetDefinition(version, true)
|
|
81
182
|
yield await presetCtx.agentPresets.register(definition)
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
183
|
+
const discovered = (await readdir(join(home, '.agent-presets')).catch(() => []))
|
|
184
|
+
.filter(name => /^openwrite-\d+-\d+-\d+(?:-[a-z0-9-]+)?$/.test(name))
|
|
185
|
+
// An alias keeps a session written by an older release resumable. Registering
|
|
186
|
+
// every released id instead floods the new-session selector with entries no
|
|
187
|
+
// session references, so the alias set follows the session store and only
|
|
188
|
+
// falls back to the durable list when that store cannot be read. This
|
|
189
|
+
// release's own identity is excluded: it is registered as the stable preset
|
|
190
|
+
// and its directory belongs to this plugin, so an alias for it is noise.
|
|
191
|
+
const referenced = await referencedPresetIds(home)
|
|
192
|
+
const ids = new Set([...(referenced ?? legacyPresetIds), ...discovered].filter(candidate => candidate !== id))
|
|
85
193
|
for (const legacyId of ids) {
|
|
86
194
|
// An author-supplied definition wins; readDocument checks presence without
|
|
87
195
|
// waiting for host activation (list/resolve would deadlock startup).
|
|
@@ -90,8 +198,39 @@ export async function apply(ctx) {
|
|
|
90
198
|
catch (error) { if (error?.code !== 'agent-preset/not-found') throw error }
|
|
91
199
|
}
|
|
92
200
|
yield await presetCtx.agentPresets.register({ ...definition, id: legacyId,
|
|
93
|
-
order: 1000, name:
|
|
201
|
+
order: 1000, name: legacyAliasName(legacyId, definition.name),
|
|
94
202
|
})
|
|
95
203
|
}
|
|
96
204
|
})
|
|
205
|
+
// Companion status for the workbench's companion section, and the host's own
|
|
206
|
+
// default-model selection, so OpenWrite's page in the host's settings dialog
|
|
207
|
+
// can show which model dsh itself would use. The companion route reports
|
|
208
|
+
// which implementation serves DoG and whether the optional integrations are
|
|
209
|
+
// live; it never changes what the preset mounts. The model route carries no
|
|
210
|
+
// key: the selection is a browser-visible fact, the key is not. Like every
|
|
211
|
+
// other /studio-panel/* route both sit behind the host's
|
|
212
|
+
// browser-authentication fence: without it any local process could read the
|
|
213
|
+
// host version, the companion inventory and the host's model selection.
|
|
214
|
+
ctx.inject(['webServer'], webCtx => webCtx.effect(() => {
|
|
215
|
+
webCtx.webServer.register({
|
|
216
|
+
kind: 'exact', path: '/studio-panel/companions', handler: async (req, res) => {
|
|
217
|
+
const respond = (code, data) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)) }
|
|
218
|
+
const connection = webCtx.get('connection')
|
|
219
|
+
const rejection = connection ? connection.requestRejection({ headers: req.headers }) : 503
|
|
220
|
+
if (rejection !== undefined) { respond(rejection, { error: 'dsh browser authentication required', message: 'dsh browser authentication required', code: 'AUTHENTICATION_REQUIRED' }); return }
|
|
221
|
+
if (req.method !== 'GET') { respond(405, { error: 'Method not allowed', message: 'Method not allowed', code: 'METHOD_NOT_ALLOWED' }); return }
|
|
222
|
+
respond(200, await companionSummary(home) ?? { host: null, dog: { installed: false, usable: false, version: null, reason: null }, ops: { installed: false, usable: false, version: null, reason: null }, freeModel: { installed: false, usable: false, version: null, reason: null } })
|
|
223
|
+
},
|
|
224
|
+
}, 'openwrite: companion status')
|
|
225
|
+
webCtx.webServer.register({
|
|
226
|
+
kind: 'exact', path: '/studio-panel/host-model', handler: async (req, res) => {
|
|
227
|
+
const respond = (code, data) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)) }
|
|
228
|
+
const connection = webCtx.get('connection')
|
|
229
|
+
const rejection = connection ? connection.requestRejection({ headers: req.headers }) : 503
|
|
230
|
+
if (rejection !== undefined) { respond(rejection, { error: 'dsh browser authentication required', message: 'dsh browser authentication required', code: 'AUTHENTICATION_REQUIRED' }); return }
|
|
231
|
+
if (req.method !== 'GET') { respond(405, { error: 'Method not allowed', message: 'Method not allowed', code: 'METHOD_NOT_ALLOWED' }); return }
|
|
232
|
+
respond(200, hostModelSelection(ctx))
|
|
233
|
+
},
|
|
234
|
+
}, 'openwrite: host model selection')
|
|
235
|
+
}))
|
|
97
236
|
}
|
package/preset-definition.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { open, readFile, readdir, stat, mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
import { fileURLToPath } from 'node:url'
|
|
3
6
|
import { load, Type, JSON_SCHEMA } from 'js-yaml'
|
|
7
|
+
import { detectCompanions } from './companions.mjs'
|
|
8
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
4
9
|
|
|
5
10
|
// Preserve Loader expressions; evaluating them here loses the preset's baseUrl
|
|
6
11
|
// and entry-local realm. This is the host's entry-list !!js wire representation.
|
|
@@ -12,16 +17,150 @@ const schema = JSON_SCHEMA.extend(new Type('tag:yaml.org,2002:js', {
|
|
|
12
17
|
export const stablePresetId = 'openwrite'
|
|
13
18
|
|
|
14
19
|
// These released identities are durable session references, even after their
|
|
15
|
-
// package-managed directories have been removed by older plugin versions.
|
|
16
|
-
|
|
20
|
+
// package-managed directories have been removed by older plugin versions. They
|
|
21
|
+
// are the safety net used when the session store cannot be read; the live alias
|
|
22
|
+
// set follows the sessions that actually reference them (see below).
|
|
23
|
+
/**
|
|
24
|
+
* Every released identity on the current `major.minor` line.
|
|
25
|
+
*
|
|
26
|
+
* Derived from the package version rather than a hardcoded count: `{ length: 29 }`
|
|
27
|
+
* could only ever express `0.2.x`, so a move to 0.3.0 or 1.0.0 structurally could
|
|
28
|
+
* not produce the new identity and the durable-resume safety net would silently
|
|
29
|
+
* omit it.
|
|
30
|
+
*/
|
|
31
|
+
const releaseVersion = JSON.parse(await readFile(new URL('package.json', import.meta.url), 'utf8')).version
|
|
32
|
+
const releaseLine = /^(\d+)\.(\d+)\.(\d+)/.exec(String(releaseVersion || ''))
|
|
33
|
+
export const legacyPresetIds = releaseLine === null
|
|
34
|
+
? []
|
|
35
|
+
: Array.from({ length: Number(releaseLine[3]) }, (_, i) => `openwrite-${releaseLine[1]}-${releaseLine[2]}-${i + 1}`)
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
/** One released versioned preset identity, including any pre-release suffix. */
|
|
38
|
+
const versionedPresetId = /^openwrite-\d+-\d+-\d+(?:-[a-z0-9-]+)?$/
|
|
39
|
+
|
|
40
|
+
/** Session log artifacts, in both encodings the host persistence backend writes. */
|
|
41
|
+
const sessionLogSuffixes = ['.jsonl.zstd', '.jsonl']
|
|
42
|
+
|
|
43
|
+
/** A session header is the first frame of its log; a bounded prefix reads it. */
|
|
44
|
+
const headerPrefixBytes = 256 * 1024
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Name an alias after the release it restores. A bare id is an internal detail
|
|
48
|
+
* that leaves dozens of identically prefixed entries in the preset selector.
|
|
49
|
+
*/
|
|
50
|
+
export function legacyAliasName(id, name) {
|
|
51
|
+
if (!versionedPresetId.test(id)) return `${name}(历史会话 ${id})`
|
|
52
|
+
return `${name}(旧版 ${id.replace(/^openwrite-/, '').replaceAll('-', '.')} · 仅用于恢复历史会话)`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function collectSessionLogs(root) {
|
|
56
|
+
const logs = []
|
|
57
|
+
const walk = async directory => {
|
|
58
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
59
|
+
const path = join(directory, entry.name)
|
|
60
|
+
if (entry.isDirectory()) await walk(path)
|
|
61
|
+
else if (sessionLogSuffixes.some(suffix => entry.name.endsWith(suffix))) logs.push(path)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await walk(root)
|
|
65
|
+
return logs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A digest over every log's identity, not just {count, newest mtime}: deleting
|
|
70
|
+
* one session and adding another in the same second keeps that pair unchanged
|
|
71
|
+
* and would serve a stale alias set (a missing id, a lingering one). Stats only,
|
|
72
|
+
* so this stays cheap enough to re-derive on every start.
|
|
73
|
+
*/
|
|
74
|
+
async function fingerprintLogs(logs) {
|
|
75
|
+
const hash = createHash('sha256')
|
|
76
|
+
const rows = []
|
|
77
|
+
for (const path of logs) {
|
|
78
|
+
let info
|
|
79
|
+
try { info = await stat(path) } catch { continue }
|
|
80
|
+
rows.push(`${path}\u0000${Math.round(info.mtimeMs)}\u0000${info.size}\n`)
|
|
81
|
+
}
|
|
82
|
+
rows.sort()
|
|
83
|
+
for (const row of rows) hash.update(row)
|
|
84
|
+
return { files: rows.length, digest: hash.digest('hex') }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The `agentPreset` a stored session header carries, or undefined when unreadable. */
|
|
88
|
+
async function headerPresetId(path) {
|
|
89
|
+
const handle = await open(path, 'r')
|
|
90
|
+
try {
|
|
91
|
+
const buffer = Buffer.alloc(headerPrefixBytes)
|
|
92
|
+
const { bytesRead } = await handle.read(buffer, 0, headerPrefixBytes, 0)
|
|
93
|
+
const head = buffer.subarray(0, bytesRead)
|
|
94
|
+
// node:zlib decodes the leading frame and ignores the appended event frames.
|
|
95
|
+
const text = path.endsWith('.zstd') ? zstdDecompressSync(head).toString('utf8') : head.toString('utf8')
|
|
96
|
+
const header = JSON.parse(text.split('\n', 1)[0])
|
|
97
|
+
return typeof header?.agentPreset === 'string' ? header.agentPreset : undefined
|
|
98
|
+
} finally {
|
|
99
|
+
await handle.close()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The versioned preset ids stored sessions still reference. Registering every
|
|
105
|
+
* released id instead would fill the new-session selector with entries no
|
|
106
|
+
* session can resume, so the alias set is derived from the session store.
|
|
107
|
+
* Returns null when that store cannot be read at all, which keeps the durable
|
|
108
|
+
* alias list rather than silently dropping historical session resume.
|
|
109
|
+
*/
|
|
110
|
+
export async function referencedPresetIds(home) {
|
|
111
|
+
const root = join(home, 'sessions')
|
|
112
|
+
let logs
|
|
113
|
+
try {
|
|
114
|
+
logs = await collectSessionLogs(root)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code !== 'ENOENT') return null
|
|
117
|
+
logs = []
|
|
118
|
+
}
|
|
119
|
+
// fingerprintLogs skips logs it cannot stat (a dangling symlink, or one the
|
|
120
|
+
// host prunes mid-scan); a walk that cannot even be listed is the only case
|
|
121
|
+
// that falls back to the durable list.
|
|
122
|
+
const fingerprint = await fingerprintLogs(logs).catch(() => null)
|
|
123
|
+
if (!fingerprint) return null
|
|
124
|
+
const cache = join(home, 'openwrite', 'preset-aliases.json')
|
|
125
|
+
const cached = await readFile(cache, 'utf8').then(JSON.parse).catch(() => null)
|
|
126
|
+
if (cached?.fingerprint?.files === fingerprint.files && cached.fingerprint.digest === fingerprint.digest) {
|
|
127
|
+
return new Set((cached.ids ?? []).filter(id => versionedPresetId.test(id)))
|
|
128
|
+
}
|
|
129
|
+
const ids = new Set()
|
|
130
|
+
for (const path of logs) {
|
|
131
|
+
const id = await headerPresetId(path).catch(() => undefined)
|
|
132
|
+
if (id && versionedPresetId.test(id)) ids.add(id)
|
|
133
|
+
}
|
|
134
|
+
await mkdir(join(home, 'openwrite'), { recursive: true }).catch(() => {})
|
|
135
|
+
await writeFile(cache, JSON.stringify({ fingerprint, ids: [...ids] }) + '\n').catch(() => {})
|
|
136
|
+
return ids
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Mount dsh-ops into this preset when the companion is installed, its declared
|
|
141
|
+
* host range covers the running host, and it exposes the integration entry.
|
|
142
|
+
*/
|
|
143
|
+
async function adaptCompanions(plugins, options) {
|
|
144
|
+
if (plugins.some(row => row.id === 'openwrite-ops')) return
|
|
145
|
+
let companions = null
|
|
146
|
+
try {
|
|
147
|
+
companions = await detectCompanions(resolveDshHome(), options)
|
|
148
|
+
} catch { /* an unreadable home leaves the preset exactly as shipped */ }
|
|
149
|
+
if (!companions?.ops?.usable) return
|
|
150
|
+
plugins.push({ id: 'openwrite-ops', name: 'dsh-openwrite/ops-integration' })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function presetDefinition(version, modern = false, options = {}) {
|
|
19
154
|
const baseUrl = new URL('./presets/openwrite/', import.meta.url)
|
|
20
155
|
const plugins = load(await readFile(new URL('agent.cordis.yml', baseUrl), 'utf8'), { schema })
|
|
21
156
|
const metadata = load(await readFile(new URL('preset.yml', baseUrl), 'utf8'))
|
|
22
157
|
if (modern) {
|
|
23
158
|
const adapt = rows => {
|
|
24
159
|
for (const row of rows) {
|
|
160
|
+
// The workflow row is renamed for hosts that ship the PTC variant. This
|
|
161
|
+
// repo's node_modules carries an older host without that package, so the
|
|
162
|
+
// resolution gate treats it as expected-absent rather than failing (see
|
|
163
|
+
// scripts/preset-smoke.mjs).
|
|
25
164
|
if (row.name === '@deepseek-ai/dsh-workflow-worker-thread') {
|
|
26
165
|
row.name = '@deepseek-ai/dsh-workflow-ptc'
|
|
27
166
|
}
|
|
@@ -33,5 +172,6 @@ export async function presetDefinition(version, modern = false) {
|
|
|
33
172
|
}
|
|
34
173
|
adapt(plugins)
|
|
35
174
|
}
|
|
175
|
+
await adaptCompanions(plugins, options)
|
|
36
176
|
return { ...metadata, id: stablePresetId, order: -1, plugins }
|
|
37
177
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
outlines and assets unless the author asks to translate them. Chinese skill instructions
|
|
21
21
|
describe procedures; they do not require Chinese output. Translate their headings and help
|
|
22
22
|
when presenting them. In English conversations, use English throughout the creative loop.
|
|
23
|
-
Writing tasks return a task_id immediately; poll
|
|
23
|
+
Writing tasks return a task_id immediately; poll novel_task(action="get") until terminal and present
|
|
24
24
|
cancellation as requested until the task reports cancelled. Resume saved drafts via run_id_v2.
|
|
25
25
|
|
|
26
26
|
你是 OpenWrite 创作 Agent,运行在 DeepSeek Harness(dsh)的统一 OpenWrite 预设中。
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
→ rereview 闭环。低质量、硬门禁失败或上下文不完整时先报告原因,不强行交付。
|
|
67
67
|
修改已有正文、人物、世界、正典、伏笔或其他项目文档时,先用 novel_search / novel_status
|
|
68
68
|
定位正式实体并读取最新版本。正文、正典和其他普通文档的每个独立修改分别调用
|
|
69
|
-
|
|
70
|
-
修改分别调用
|
|
69
|
+
novel_change_plan(action="document", step="preview");大纲、资产、创作重点、伏笔和写作目标的每个独立
|
|
70
|
+
修改分别调用 novel_change_plan(action="structured", step="preview")。向用户展示返回的 before/after 与来源版本;
|
|
71
71
|
只有用户明确确认后,才把原 preview_token 原样传给 action="apply", confirm=true,不得重新生成路径、
|
|
72
72
|
锚点或内容。用户拒绝时用 action="reject";源文件冲突时用 action="retry" 生成新预览;
|
|
73
73
|
已应用项需要撤销时,只能把返回的 undo_preview_token 传给 action="undo", confirm=true。
|
|
74
|
-
|
|
74
|
+
novel_doc(action="write") 是已获明确授权的完整文档写入兼容入口,不得用它绕过逐项预览。
|
|
75
75
|
完成写入后可用 novel_trace_list 核对来源、预算、模型调用、工具调用和领域变更。Trace 只保留摘要、
|
|
76
76
|
revision 与用量,作品正文、提示原文、模型原文、凭据和推理内容不得写入 trace。
|
|
77
77
|
普通讨论、分析和未确认建议不得写入资产。
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
或阶段仍在大纲滚动期但用户明确要求“根据现在/当前大纲写下一章”、
|
|
82
82
|
“就按这个大纲写”或等价表达,先向用户确认大纲范围,再执行章节预检与写作。
|
|
83
83
|
用户要求修改大纲时,先用 novel_outline_read 定位结构,再用
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
novel_change_plan(action="structured", step="preview", change_kind="outline") 对一个结构操作逐项预览和确认;不要用
|
|
85
|
+
novel_doc(action="write") 绕过大纲门控,也不要整篇覆盖 src/outline.md。novel_outline_edit、
|
|
86
|
+
novel_assets(action="update")、novel_focus、novel_foreshadowing 和 novel_writing_targets 仅在用户本轮已经
|
|
87
87
|
明确授权对应的精确变更时作为直接写入兼容入口使用。
|
|
88
88
|
用户要求删除已写正文或章节时,先向用户说明将删除的章节与影响并得到明确同意。
|
|
89
89
|
单章使用 novel_chapter_delete;清理连续尾段时使用 novel_chapter_delete_batch,
|
|
@@ -335,5 +335,3 @@
|
|
|
335
335
|
- id: openwrite-tools
|
|
336
336
|
name: dsh-openwrite/tools
|
|
337
337
|
|
|
338
|
-
- id: openwrite-dog-tools
|
|
339
|
-
name: dsh-openwrite/dog-tools
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// dsh-ops preset integration. The row that mounts this module is only added
|
|
2
|
+
// when dsh-ops is installed, its declared host range covers the running host,
|
|
3
|
+
// and it exposes `dsh-ops/integration`. The import is still defensive: a
|
|
4
|
+
// companion that changed its entry between detection and load must degrade to
|
|
5
|
+
// a no-op row rather than take the whole preset composition down.
|
|
6
|
+
export const name = '@dsh-novel/openwrite-ops'
|
|
7
|
+
|
|
8
|
+
export async function apply(ctx) {
|
|
9
|
+
let integration
|
|
10
|
+
try {
|
|
11
|
+
({ integration } = await import('dsh-ops/integration'))
|
|
12
|
+
} catch {
|
|
13
|
+
ctx.effect(() => () => {}, 'openwrite-ops: dsh-ops integration unavailable')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
// Bounded file tools plus progressive discovery for the OpenWrite preset.
|
|
17
|
+
// Official file tools stay available (`replaceOfficial: false`), no shell is
|
|
18
|
+
// published (`shell: false`), and the preset's own guidance is not replaced.
|
|
19
|
+
const plugin = integration({
|
|
20
|
+
preset: 'openwrite',
|
|
21
|
+
files: true,
|
|
22
|
+
discovery: true,
|
|
23
|
+
presentation: 'native',
|
|
24
|
+
replaceOfficial: false,
|
|
25
|
+
shell: false,
|
|
26
|
+
})
|
|
27
|
+
return plugin.apply(ctx)
|
|
28
|
+
}
|
|
@@ -9,9 +9,9 @@ Language: follow the author's current conversation language for responses and he
|
|
|
9
9
|
|
|
10
10
|
接到已有 `novel_*`。人物、地点、世界观以 OpenWrite 资产为准。
|
|
11
11
|
|
|
12
|
-
1. `
|
|
13
|
-
2. 指定实体用 `
|
|
12
|
+
1. `novel_assets(action="list"` 列出人物/世界资产(含名称与别名)。
|
|
13
|
+
2. 指定实体用 `novel_assets(action="read", kind="character"|"world"|"progression", id=…)`。
|
|
14
14
|
3. 按名称或别名找不到时 `novel_search`(scope 可 `assets`)。
|
|
15
15
|
4. 关系与状态漂移用 `novel_continuity`。
|
|
16
16
|
|
|
17
|
-
向作者报告规范名、别名、摘要。改设定走 `
|
|
17
|
+
向作者报告规范名、别名、摘要。改设定走 `novel_change_plan(action="structured"(change_kind="asset")`;普通讨论不得写入。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delivery-artifact-query
|
|
3
|
+
description: Query a chapter delivery record 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
|
+
# 章节交付状态查询
|
|
10
|
+
|
|
11
|
+
章节写作、评审和修订工具会直接返回交付状态,并把同一份记录落盘:
|
|
12
|
+
`data/novels/{id}/data/dog/deliveries/{chapter}/dog-graph.json`。
|
|
13
|
+
它覆盖六个阶段:`writing → review → revision → application → rereview → closure`。
|
|
14
|
+
它不替代 OpenWrite 的写章、六域累加评审或修订服务。
|
|
15
|
+
|
|
16
|
+
## 查询流程
|
|
17
|
+
|
|
18
|
+
1. 从最近一次 `novel_write_chapter`、`novel_review_chapter`、`novel_revision`
|
|
19
|
+
或 `novel_task(action="get")` 结果里的 `dog_delivery` 读状态;需要完整阶段明细时再读
|
|
20
|
+
上面的 `dog-graph.json`(`stages` 里有每一阶段的 verdict 与 evidence)。
|
|
21
|
+
2. 按 `closure` 的状态给结论(见下表),不要自行推断。
|
|
22
|
+
3. 修订应用后必须重新调用 `novel_review_chapter`;只有复评对应当前正文 SHA 且门禁通过,
|
|
23
|
+
`closure` 才能成为 `closed`。
|
|
24
|
+
|
|
25
|
+
## 状态解释
|
|
26
|
+
|
|
27
|
+
- `review.missing`:正文存在但尚未评审。
|
|
28
|
+
- `closure.review_failed`:当前正文已评审但未通过门禁,进入修订闭环。
|
|
29
|
+
- `revision.proposal_pending`:提案已生成,等待作者确认应用或驳回。
|
|
30
|
+
- `revision.applied_requires_rereview`:修订已写入正文,但不能视为问题已解决。
|
|
31
|
+
- `closure.rereview_required`:必须对修订后的正文重新跑六域评审。
|
|
32
|
+
- `closure.closed`:当前正文的最新评审通过,可以交付。
|
|
33
|
+
|
|
34
|
+
`readyForDelivery=true` 等价于「当前正文 SHA 的最新复评通过且门禁通过」,
|
|
35
|
+
可以直接作为交付结论;为 `false` 时说明上面某一项未满足。
|
|
36
|
+
|
|
37
|
+
## 约束
|
|
38
|
+
|
|
39
|
+
- 只查询 canonical manuscript/review/revision 文件,不直接修改它们。
|
|
40
|
+
- 不因提案状态为 `applied` 就宣称问题关闭;必须以当前正文 SHA 对应的复评为准。
|
|
41
|
+
- 记录缺失时回到 OpenWrite 流程重新评审,不要把上一次的结论说成最新。
|
|
@@ -19,14 +19,15 @@ Language: follow the author's current conversation language for responses and he
|
|
|
19
19
|
3. `data/style/composed.md` — 当前作品风格约束
|
|
20
20
|
4. `src/outline.md` / canonical packet — 本章戏剧位置与章节目标
|
|
21
21
|
|
|
22
|
-
如果需要完整判断某章对白是否失真,优先结合 `
|
|
22
|
+
如果需要完整判断某章对白是否失真,优先结合 `novel_review_chapter` 或 `novel_chapter_work` 的章节 packet,而不是只看几行台词。
|
|
23
23
|
|
|
24
24
|
## 可用工具
|
|
25
25
|
|
|
26
26
|
| 工具 | 说明 |
|
|
27
27
|
|------|------|
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
28
|
+
| `novel_manuscript_metrics(chapter_id=…)` | 章节可量化信号:字数、句长、对白占比等 |
|
|
29
|
+
| `novel_review_chapter(chapter_id=…, domains=["prose","character"])` | 对整章做审查,可发现台词失真、解释性对话、套路对白 |
|
|
30
|
+
| `novel_chapter_work(chapter_id=…)` | 章节工作 brief:戏剧位置、出场角色、章节目标 |
|
|
30
31
|
|
|
31
32
|
## 什么时候用
|
|
32
33
|
|
|
@@ -37,26 +38,24 @@ Language: follow the author's current conversation language for responses and he
|
|
|
37
38
|
|
|
38
39
|
## 工作方式
|
|
39
40
|
|
|
40
|
-
### 1.
|
|
41
|
+
### 1. 读取章节信号
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
先定位章节,再读可量化指标:
|
|
43
44
|
|
|
44
45
|
```text
|
|
45
46
|
用户: 分析一下林月在第六章的对话风格
|
|
46
47
|
|
|
47
|
-
AI:
|
|
48
|
-
[COMMAND] extract_dialogue_fingerprint {
|
|
49
|
-
"chapter_id": "ch_006",
|
|
50
|
-
"character_names": ["林月"]
|
|
51
|
-
}
|
|
48
|
+
AI: 调用 novel_manuscript_metrics(chapter_id="ch_006")
|
|
52
49
|
```
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
重点观察返回里的:
|
|
55
52
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
|
|
53
|
+
- 字数与对白占比:这一章是谁在说话
|
|
54
|
+
- 句长相关指标:偏短还是偏长
|
|
55
|
+
- 出场角色:林月是否真的有戏份
|
|
56
|
+
|
|
57
|
+
没有工具会直接给出"对白指纹";上面的指标加上正文摘录就是判断依据。
|
|
58
|
+
需要整章判断时再调用 `novel_review_chapter(chapter_id="ch_006", domains=["prose","character"])`。
|
|
60
59
|
|
|
61
60
|
### 2. 结合角色档案复核
|
|
62
61
|
|
|
@@ -163,6 +162,6 @@ AI:
|
|
|
163
162
|
|
|
164
163
|
## 注意
|
|
165
164
|
|
|
166
|
-
- `
|
|
167
|
-
- `
|
|
165
|
+
- `novel_manuscript_metrics` 提供可量化信号,判断"说话指纹"要靠它加上正文摘录
|
|
166
|
+
- `novel_review_chapter` 适合看“对白放在整章里是否成立”
|
|
168
167
|
- 真要改对白,必须结合人物文档和章节戏剧位置一起判断
|