@deepblend/dsh-blender-preset 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/lib/deploy.js +217 -0
- package/lib/index.js +144 -0
- package/package.json +30 -0
- package/presets/deepblend/agent.cordis.yml +218 -0
- package/presets/deepblend/preset.yml +9 -0
- package/presets/deepblend/skills/deepblend-studio/SKILL.md +144 -0
- package/presets/deepblend-dev/agent.cordis.yml +285 -0
- package/presets/deepblend-dev/preset.yml +2 -0
package/lib/deploy.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepblend/dsh-blender-preset — the deployment rule.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS FILE IS
|
|
5
|
+
* -----------------
|
|
6
|
+
* One answer to "what files is a preset made of, and how does an installed copy
|
|
7
|
+
* differ from the shipped one". Three callers need that answer and must not be
|
|
8
|
+
* allowed to disagree:
|
|
9
|
+
*
|
|
10
|
+
* - `lib/index.js`, the plugin row that deploys the presets when the profile
|
|
11
|
+
* composes this package (the path a plugin-market user takes);
|
|
12
|
+
* - `deepblend/tools/install-presets.mjs`, the repository's operator tool;
|
|
13
|
+
* - `deepblend/tools/sync-plugin-presets.mjs`, which keeps the copy this
|
|
14
|
+
* package ships identical to `deepblend/presets/` (SPEC §5.2).
|
|
15
|
+
*
|
|
16
|
+
* A rule reachable only through one of them is a rule the other two can drift
|
|
17
|
+
* from silently — which is the failure this repository has paid for repeatedly
|
|
18
|
+
* (D38, D43, D57, D60: a list copied into a second place rots in the copy).
|
|
19
|
+
*
|
|
20
|
+
* WHY THE WHOLE DIRECTORY TRAVELS
|
|
21
|
+
* -------------------------------
|
|
22
|
+
* `REQUIRED_FILES` is what a preset cannot be without, not what is copied. The
|
|
23
|
+
* rest of the directory travels too: a preset's `skills/` directory is loaded
|
|
24
|
+
* through `customSkillDirs`, resolved against the composition's own base URL, so
|
|
25
|
+
* copying a fixed file list silently drops the skill the persona tells the model
|
|
26
|
+
* to load — the preset mounts, every tool arrives, and the skill is simply not
|
|
27
|
+
* in the catalog.
|
|
28
|
+
*
|
|
29
|
+
* THREE STATES, NOT TWO
|
|
30
|
+
* ---------------------
|
|
31
|
+
* `absent` (nothing of this preset is installed) is a state, not drift: that is
|
|
32
|
+
* every fresh machine, and reporting it as drift is what made the operator tool
|
|
33
|
+
* exit 1 on a machine where nothing was wrong. `partial` is drift, because a
|
|
34
|
+
* half-present preset is broken rather than absent.
|
|
35
|
+
*
|
|
36
|
+
* Owner: DeepBlend Studio — M6 (plugin-market packaging)
|
|
37
|
+
* Plane: Agent preset
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
41
|
+
import { homedir } from 'node:os'
|
|
42
|
+
import { join } from 'node:path'
|
|
43
|
+
import { fileURLToPath } from 'node:url'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The presets this package ships, resolved relative to this module.
|
|
47
|
+
*
|
|
48
|
+
* A URL rather than a path computed from `process.cwd()`: the package is
|
|
49
|
+
* installed into a profile's `node_modules` and the process may be started from
|
|
50
|
+
* anywhere, so its own location is the only stable anchor.
|
|
51
|
+
*/
|
|
52
|
+
export const PRESET_SOURCE = fileURLToPath(new URL('../presets/', import.meta.url))
|
|
53
|
+
|
|
54
|
+
/** The writable preset root under the harness home (`@deepseek-ai/dsh-agent-presets`). */
|
|
55
|
+
export const USER_PRESET_DIR = '.agent-presets'
|
|
56
|
+
|
|
57
|
+
/** Files a preset directory cannot be without. A preset missing one is not deployed at all. */
|
|
58
|
+
export const REQUIRED_FILES = ['preset.yml', 'agent.cordis.yml']
|
|
59
|
+
|
|
60
|
+
/** Directories inside a preset that are never part of it. */
|
|
61
|
+
export const IGNORED_DIRECTORIES = new Set(['node_modules', '.git'])
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The harness home this process would deploy into.
|
|
65
|
+
*
|
|
66
|
+
* `DSH_HOME` first because that is what the launcher itself honours, so a plugin
|
|
67
|
+
* running inside a deployment writes to the same home the deployment reads.
|
|
68
|
+
*
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function dshHome() {
|
|
72
|
+
return process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The default target root: `<DSH_HOME>/.agent-presets`. */
|
|
76
|
+
export function defaultTarget() {
|
|
77
|
+
return join(dshHome(), USER_PRESET_DIR)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every file under `directory`, as paths relative to it, in a stable order.
|
|
82
|
+
*
|
|
83
|
+
* Sorted because the result is compared between two trees: an unsorted walk
|
|
84
|
+
* makes a difference report depend on the filesystem's own order.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} directory
|
|
87
|
+
* @returns {string[]}
|
|
88
|
+
*/
|
|
89
|
+
export function filesUnder(directory) {
|
|
90
|
+
const found = []
|
|
91
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue
|
|
94
|
+
for (const nested of filesUnder(join(directory, entry.name))) found.push(join(entry.name, nested))
|
|
95
|
+
} else if (entry.isFile()) {
|
|
96
|
+
found.push(entry.name)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return found
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The preset ids a source root holds, in a stable order.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} [source]
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
export function presetIds(source = PRESET_SOURCE) {
|
|
109
|
+
if (!existsSync(source)) return []
|
|
110
|
+
return readdirSync(source)
|
|
111
|
+
.filter(name => statSync(join(source, name)).isDirectory())
|
|
112
|
+
.sort()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* What a preset's `preset.yml` and composition directory look like to a deployer.
|
|
117
|
+
*
|
|
118
|
+
* @typedef {object} PresetPlan
|
|
119
|
+
* @property {string} id
|
|
120
|
+
* @property {string} source
|
|
121
|
+
* @property {string} target
|
|
122
|
+
* @property {string[]} files every file the shipped copy has
|
|
123
|
+
* @property {string[]} missing shipped files the target does not have
|
|
124
|
+
* @property {string[]} drifted shipped files whose bytes differ
|
|
125
|
+
* @property {string[]} stale target files the shipped copy no longer has
|
|
126
|
+
* @property {boolean} absent the target holds none of this preset
|
|
127
|
+
* @property {string|null} problem why this preset cannot be deployed at all
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compare one shipped preset against one installed copy, writing nothing.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} input
|
|
134
|
+
* @param {string} input.source - the shipped preset directory.
|
|
135
|
+
* @param {string} input.target - where the preset would be installed.
|
|
136
|
+
* @param {string} [input.id] - the preset id, for the report.
|
|
137
|
+
* @returns {PresetPlan}
|
|
138
|
+
*/
|
|
139
|
+
export function planPreset({ source, target, id = source.split('/').pop() }) {
|
|
140
|
+
const missingRequired = REQUIRED_FILES.filter(file => !existsSync(join(source, file)))
|
|
141
|
+
if (missingRequired.length > 0) {
|
|
142
|
+
// A preset without its composition is not a preset; refusing beats writing half of one.
|
|
143
|
+
return {
|
|
144
|
+
id, source, target,
|
|
145
|
+
files: [], missing: [], drifted: [], stale: [], absent: !existsSync(target),
|
|
146
|
+
problem: `missing ${missingRequired.join(', ')} in the shipped copy`,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const files = filesUnder(source)
|
|
151
|
+
const targetFiles = existsSync(target) ? filesUnder(target) : []
|
|
152
|
+
const missing = []
|
|
153
|
+
const drifted = []
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const installed = join(target, file)
|
|
156
|
+
if (!existsSync(installed)) {
|
|
157
|
+
missing.push(file)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (readFileSync(join(source, file), 'utf8') !== readFileSync(installed, 'utf8')) drifted.push(file)
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
id, source, target, files, missing, drifted,
|
|
164
|
+
stale: targetFiles.filter(file => !files.includes(file)),
|
|
165
|
+
// "Nothing of this preset is here" — the state every fresh machine is in, and not drift.
|
|
166
|
+
absent: targetFiles.length === 0,
|
|
167
|
+
problem: null,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deploy one planned preset: write what differs, remove what the shipped copy no
|
|
173
|
+
* longer has, and leave everything else alone.
|
|
174
|
+
*
|
|
175
|
+
* The target is a user-owned root — a person may have authored presets of their
|
|
176
|
+
* own beside these — so this only ever touches the files of the preset it was
|
|
177
|
+
* given, and never the root itself.
|
|
178
|
+
*
|
|
179
|
+
* @param {PresetPlan} plan
|
|
180
|
+
* @returns {{written: string[], removed: string[]}}
|
|
181
|
+
*/
|
|
182
|
+
export function applyPreset(plan) {
|
|
183
|
+
if (plan.problem !== null) return { written: [], removed: [] }
|
|
184
|
+
const written = []
|
|
185
|
+
const removed = []
|
|
186
|
+
for (const stale of plan.stale) {
|
|
187
|
+
rmSync(join(plan.target, stale), { force: true })
|
|
188
|
+
removed.push(stale)
|
|
189
|
+
}
|
|
190
|
+
for (const file of [...plan.missing, ...plan.drifted]) {
|
|
191
|
+
const to = join(plan.target, file)
|
|
192
|
+
mkdirSync(join(to, '..'), { recursive: true })
|
|
193
|
+
copyFileSync(join(plan.source, file), to)
|
|
194
|
+
written.push(file)
|
|
195
|
+
}
|
|
196
|
+
return { written, removed }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Deploy every preset a source root holds, and report what each one needed.
|
|
201
|
+
*
|
|
202
|
+
* @param {object} [options]
|
|
203
|
+
* @param {string} [options.source] - the shipped preset root.
|
|
204
|
+
* @param {string} [options.target] - the installed preset root.
|
|
205
|
+
* @param {string[]} [options.ids] - restrict to these preset ids.
|
|
206
|
+
* @returns {Array<{id: string, plan: PresetPlan, written: string[], removed: string[]}>}
|
|
207
|
+
*/
|
|
208
|
+
export function deployPresets({ source = PRESET_SOURCE, target = defaultTarget(), ids } = {}) {
|
|
209
|
+
const wanted = ids === undefined ? presetIds(source) : ids
|
|
210
|
+
const reports = []
|
|
211
|
+
for (const id of wanted) {
|
|
212
|
+
const plan = planPreset({ source: join(source, id), target: join(target, id), id })
|
|
213
|
+
const { written, removed } = applyPreset(plan)
|
|
214
|
+
reports.push({ id, plan, written, removed })
|
|
215
|
+
}
|
|
216
|
+
return reports
|
|
217
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepblend/dsh-blender-preset
|
|
3
|
+
*
|
|
4
|
+
* The **Agent-preset plane** deliverable (SPEC §4.2, §5.2).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS PACKAGE EXISTS
|
|
7
|
+
* -----------------------
|
|
8
|
+
* The tools a session may see belong to an AGENT PRESET, not to the Host
|
|
9
|
+
* composition (SPEC §4.3): registering them process-wide would hand every
|
|
10
|
+
* session in the deployment the Blender toolset. That is why they are not in the
|
|
11
|
+
* bundle's patch.
|
|
12
|
+
*
|
|
13
|
+
* But a preset is a directory under `<DSH_HOME>/.agent-presets/`, and
|
|
14
|
+
* `dsh plugin add` installs a package — it does not write a user's preset root.
|
|
15
|
+
* So a plugin whose value IS its preset has to deliver one, and the ecosystem's
|
|
16
|
+
* pattern for that is a deployer row: the bundle mounts this package, and the
|
|
17
|
+
* package installs the preset it ships. `dsh-expert-orchestrator` is the same
|
|
18
|
+
* shape, and it is on the plugin list.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT TOUCHES
|
|
21
|
+
* ---------------
|
|
22
|
+
* Exactly `<DSH_HOME>/.agent-presets/<id>/` for the preset ids it ships, and
|
|
23
|
+
* nothing else. The root is a user-owned, writable directory — a person may have
|
|
24
|
+
* authored presets of their own beside these — so this never removes the root,
|
|
25
|
+
* never touches another preset, and never deletes a file that the shipped copy
|
|
26
|
+
* still has.
|
|
27
|
+
*
|
|
28
|
+
* The write is IDEMPOTENT and reports rather than insists: a file whose bytes
|
|
29
|
+
* already match is not rewritten, a stale file left by an earlier release is
|
|
30
|
+
* removed, and a home that cannot be written is logged and left alone. A plugin
|
|
31
|
+
* that throws while a profile composes would take the whole deployment down over
|
|
32
|
+
* a convenience copy, which is the wrong trade in both directions.
|
|
33
|
+
*
|
|
34
|
+
* WHY DEPLOYING IS THE DEFAULT
|
|
35
|
+
* ----------------------------
|
|
36
|
+
* An installed preset that has to be installed again by hand is the failure this
|
|
37
|
+
* exists to remove: `dsh plugin add` is the whole install, and the session list
|
|
38
|
+
* then shows "DeepBlend Studio". `deploy: false` is for a deployment that
|
|
39
|
+
* manages its preset root itself (a fleet, or a test), and it is honoured
|
|
40
|
+
* without a warning because it is a decision rather than a mistake.
|
|
41
|
+
*
|
|
42
|
+
* Owner: DeepBlend Studio — M6 (plugin-market packaging)
|
|
43
|
+
* Plane: Agent preset
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import z from '@deepseek-ai/schemastery'
|
|
47
|
+
|
|
48
|
+
import { PRESET_SOURCE, defaultTarget, deployPresets, presetIds } from './deploy.js'
|
|
49
|
+
|
|
50
|
+
/** Plugin name, surfaced in loader diagnostics. */
|
|
51
|
+
export const name = 'deepblend-blender-preset'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* No service is consumed and none is published.
|
|
55
|
+
*
|
|
56
|
+
* The deployment reads its own package and writes a directory, so there is
|
|
57
|
+
* nothing to wait for — and publishing a service from a row would make the
|
|
58
|
+
* preset that mounts it collide on a second session (SPEC §4.4). This row lives
|
|
59
|
+
* in the HOST composition precisely because it must happen once per process.
|
|
60
|
+
*/
|
|
61
|
+
export const inject = []
|
|
62
|
+
|
|
63
|
+
/** The shipped preset ids, for a composition that wants to name them. */
|
|
64
|
+
export const SHIPPED_PRESETS = Object.freeze(presetIds())
|
|
65
|
+
|
|
66
|
+
export const Config = z.object({
|
|
67
|
+
/** Deploy the shipped presets when this row composes. */
|
|
68
|
+
deploy: z.boolean().default(true),
|
|
69
|
+
/**
|
|
70
|
+
* Which preset ids to deploy. Empty means every preset this package ships,
|
|
71
|
+
* which is the right default: a preset added to the package should arrive
|
|
72
|
+
* without a composition change.
|
|
73
|
+
*/
|
|
74
|
+
presets: z.array(z.string()).default([]),
|
|
75
|
+
/** The preset root to deploy into; empty means `<DSH_HOME>/.agent-presets`. */
|
|
76
|
+
target: z.string().default(''),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/** The scope every line this row logs carries. */
|
|
80
|
+
export const LOG_SCOPE = 'deepblend-preset'
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deploy the shipped presets, and report what it took.
|
|
84
|
+
*
|
|
85
|
+
* Exported so the contract layer can drive it without composing a profile — the
|
|
86
|
+
* same reason `host/lib/frame-ledger.js` exports its measurement.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} config - the validated row configuration.
|
|
89
|
+
* @param {{info?: Function, warn?: Function}} [logger]
|
|
90
|
+
* @returns {{deployed: Array<{id: string, written: number, removed: number}>, skipped: string[], failed: string[]}}
|
|
91
|
+
*/
|
|
92
|
+
export function deployShippedPresets(config, logger) {
|
|
93
|
+
const target = config.target === '' ? defaultTarget() : config.target
|
|
94
|
+
const ids = config.presets.length === 0 ? presetIds(PRESET_SOURCE) : config.presets
|
|
95
|
+
const reports = []
|
|
96
|
+
const skipped = []
|
|
97
|
+
const failed = []
|
|
98
|
+
|
|
99
|
+
for (const id of ids) {
|
|
100
|
+
try {
|
|
101
|
+
const [report] = deployPresets({ source: PRESET_SOURCE, target, ids: [id] })
|
|
102
|
+
if (report === undefined) {
|
|
103
|
+
skipped.push(id)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (report.plan.problem !== null) {
|
|
107
|
+
// Refusing beats writing half a preset: a directory without its
|
|
108
|
+
// composition still occupies the id in the roster.
|
|
109
|
+
failed.push(`${id}: ${report.plan.problem}`)
|
|
110
|
+
logger?.warn?.(`${LOG_SCOPE}: ${id} was not deployed — ${report.plan.problem}`)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
reports.push({ id, written: report.written.length, removed: report.removed.length })
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
// A read-only or missing home is the operator's business, not a reason to
|
|
116
|
+
// fail the composition. The session list simply will not show the preset,
|
|
117
|
+
// and this line says why.
|
|
118
|
+
failed.push(`${id}: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
119
|
+
logger?.warn?.(`${LOG_SCOPE}: could not deploy ${id} into ${target}: ${String(cause)}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { deployed: reports, skipped, failed }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The row's apply: deploy once per composition, then stay out of the way.
|
|
127
|
+
*
|
|
128
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
129
|
+
* @param {z.infer<typeof Config>} config
|
|
130
|
+
*/
|
|
131
|
+
export function apply(ctx, config) {
|
|
132
|
+
if (!config.deploy) {
|
|
133
|
+
ctx.logger?.info?.(`${LOG_SCOPE}: deployment is switched off for this row; the preset root is left as it is`)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
const { deployed, failed } = deployShippedPresets(config, ctx.logger)
|
|
137
|
+
if (deployed.length === 0 && failed.length === 0) return
|
|
138
|
+
const summary = deployed
|
|
139
|
+
.map(entry => `${entry.id} (${entry.written} written, ${entry.removed} removed)`)
|
|
140
|
+
.join(', ')
|
|
141
|
+
if (deployed.length > 0) {
|
|
142
|
+
ctx.logger?.info?.(`${LOG_SCOPE}: deployed into ${config.target === '' ? defaultTarget() : config.target} — ${summary}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepblend/dsh-blender-preset",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DeepBlend Agent-preset plane: ships the DeepBlend Studio agent presets and the deployer row that installs them into the DSH home.",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./deploy": {
|
|
12
|
+
"default": "./lib/deploy.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"presets"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/pearjelly/deep-blend.git"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@deepseek-ai/cordis": ">=4.0.0 <5",
|
|
27
|
+
"@deepseek-ai/schemastery": ">=3.18.0 <4"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {}
|
|
30
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# The `deepblend` agent preset: DeepBlend Studio's PRODUCT mode.
|
|
2
|
+
#
|
|
3
|
+
# `deepblend-dev` is the mode this repository is developed in: a full coding agent
|
|
4
|
+
# with a shell, a filesystem and the ability to run the test suite. This file is
|
|
5
|
+
# what a USER runs, and its whole design is what it does NOT contain.
|
|
6
|
+
#
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
# SPEC §6.4 is the specification for this row set, and it is a CLOSED list:
|
|
9
|
+
#
|
|
10
|
+
# keep Persona · Agent Instructions · Plan · Ask User · Jobs Control ·
|
|
11
|
+
# DeepBlend Tool · DeepBlend Skill · Compaction · Present ·
|
|
12
|
+
# optionally read-only file tools
|
|
13
|
+
# remove any shell · any file write · any web download · any code execution ·
|
|
14
|
+
# the creator's `tool-cordis` · any Python execution tool
|
|
15
|
+
#
|
|
16
|
+
# Every row below is on that list, and every absence is a decision. The
|
|
17
|
+
# acceptance criteria M5 is checked against (`SPEC §20`) are all about absence:
|
|
18
|
+
# no shell, no arbitrary Python, no creator tool, all high-risk operations
|
|
19
|
+
# controlled. So the interesting content of this file is the comment explaining
|
|
20
|
+
# each row that is NOT here.
|
|
21
|
+
#
|
|
22
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
# WHY `tool-fs` IS ABSENT, AND `tool-fs-search` IS NOT
|
|
24
|
+
#
|
|
25
|
+
# SPEC calls for "optionally read-only file tools". `@deepseek-ai/dsh-tool-fs`
|
|
26
|
+
# registers FOUR tools in one row — `read`, `write`, `edit` and `read_image` —
|
|
27
|
+
# and its config surface is read limits only. There is no read-only subset to
|
|
28
|
+
# configure, so mounting it would hand this preset arbitrary file WRITE, which is
|
|
29
|
+
# the first thing SPEC §6.4 removes. Measured, not assumed:
|
|
30
|
+
#
|
|
31
|
+
# $ grep -ohE 'name: "[a-z_]+"' …/dsh-tool-fs/lib/index.js | sort -u
|
|
32
|
+
# name: "edit" name: "read" name: "read_image" name: "write"
|
|
33
|
+
#
|
|
34
|
+
# `@deepseek-ai/dsh-tool-fs-search` registers exactly `glob` and `grep` — listing
|
|
35
|
+
# paths and searching contents, both read-only — so THAT is the read-only file
|
|
36
|
+
# capability, and the model reaches every artifact it needs through the blender_*
|
|
37
|
+
# tools in any case: previews and contact sheets come back inside tool results as
|
|
38
|
+
# images (M2's whole design), not through a file reader.
|
|
39
|
+
#
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
# WHAT IS DELIBERATELY MISSING, AND WHY
|
|
42
|
+
#
|
|
43
|
+
# tool-bash / tool-pwsh arbitrary shell (SPEC §6.4, §11.1)
|
|
44
|
+
# tool-fs arbitrary file write (see above)
|
|
45
|
+
# tool-web arbitrary web download (SPEC §6.4)
|
|
46
|
+
# tool-cordis creator mode, mounts model-written JS
|
|
47
|
+
# tool-todo / tool-goal /
|
|
48
|
+
# command-goal not on the list. A goal is a
|
|
49
|
+
# long-running-objective feature; SPEC's closed
|
|
50
|
+
# set does not include it, and the least-surprise
|
|
51
|
+
# reading of "keep only these" is to keep only
|
|
52
|
+
# these. `command-compact` stays because it is
|
|
53
|
+
# part of the Compaction group SPEC requires.
|
|
54
|
+
# delegation / workflow / ralph subagents and workflows are not on the list
|
|
55
|
+
# either, and every one of them would be a way
|
|
56
|
+
# to reach a capability this preset just removed
|
|
57
|
+
# (a subagent runs a preset too).
|
|
58
|
+
#
|
|
59
|
+
# The last line is the one worth keeping in mind: this file is a permissions
|
|
60
|
+
# boundary, and a delegation row is a hole in it unless the delegated preset is
|
|
61
|
+
# equally narrow.
|
|
62
|
+
#
|
|
63
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
# THE SKILL TRAVELS INSIDE THIS DIRECTORY
|
|
65
|
+
#
|
|
66
|
+
# `skills/deepblend-studio/SKILL.md` is loaded by the `skill-filesystem` row
|
|
67
|
+
# below, which points at `skills/` resolved against `baseUrl` — the preset
|
|
68
|
+
# composition's own directory, per `@deepseek-ai/dsh-agent-presets`. That is the
|
|
69
|
+
# shipped convention (the deployment's `cordis` preset does exactly this for its
|
|
70
|
+
# two skills) and it means deploying this preset deploys its documentation: no
|
|
71
|
+
# second location to keep in step.
|
|
72
|
+
#
|
|
73
|
+
# `install-presets.mjs` copies the whole directory rather than a fixed file list
|
|
74
|
+
# for that reason.
|
|
75
|
+
|
|
76
|
+
# ── identity ────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
# The product persona. `{{model}}` and `{{cwd}}` resolve from the agent's own
|
|
79
|
+
# route and workspace.
|
|
80
|
+
- id: persona
|
|
81
|
+
name: '@deepseek-ai/dsh-persona'
|
|
82
|
+
config:
|
|
83
|
+
prefix: >-
|
|
84
|
+
You are DeepBlend Studio, a 3D animation agent powered by the {{model}} model.
|
|
85
|
+
You plan, inspect and control Blender through the blender_* tools: a scene is
|
|
86
|
+
a SceneSpec document, every accepted change is an immutable revision, and you
|
|
87
|
+
look at a scene with previews before you commit hours to a final render.
|
|
88
|
+
Load the deepblend-studio skill before starting real work.
|
|
89
|
+
suffix: Your working directory is {{cwd}}.
|
|
90
|
+
|
|
91
|
+
- id: agent-instructions
|
|
92
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
93
|
+
config:
|
|
94
|
+
maxBytes: 65536
|
|
95
|
+
|
|
96
|
+
# ── plan mode (SPEC §6.4 "Plan") ─────────────────────────────────────────────
|
|
97
|
+
#
|
|
98
|
+
# Plan state is per-agent by nature, so the entry-local realm is the correct
|
|
99
|
+
# lifetime rather than a workaround: without it a row would publish into the root
|
|
100
|
+
# realm and `dsh-agent-presets` would reject the mount.
|
|
101
|
+
- id: planning
|
|
102
|
+
name: cordis:group
|
|
103
|
+
group: true
|
|
104
|
+
isolate:
|
|
105
|
+
planMode: true
|
|
106
|
+
config:
|
|
107
|
+
- id: plan-mode
|
|
108
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
109
|
+
config:
|
|
110
|
+
section: |
|
|
111
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
112
|
+
|
|
113
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
114
|
+
|
|
115
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
116
|
+
|
|
117
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
118
|
+
|
|
119
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
120
|
+
|
|
121
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
122
|
+
|
|
123
|
+
# ── the questions a person must answer (SPEC §6.4 "Ask User") ────────────────
|
|
124
|
+
|
|
125
|
+
- id: tool-ask-user
|
|
126
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
127
|
+
|
|
128
|
+
# ── background work (SPEC §6.4 "Jobs Control") ───────────────────────────────
|
|
129
|
+
#
|
|
130
|
+
# Only the model-facing controls. The task REGISTRY stays in the host composition,
|
|
131
|
+
# where `tool-bash` and the render reconciler resolve it — an entry-local realm
|
|
132
|
+
# here would be invisible to them, and the tools would answer "background jobs
|
|
133
|
+
# unavailable" while sitting in the catalog. A long final render is exactly the
|
|
134
|
+
# work this preset must be able to collect and stop.
|
|
135
|
+
- id: tool-jobs
|
|
136
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
137
|
+
|
|
138
|
+
# ── the DeepBlend skill (SPEC §6.4 "DeepBlend Skill") ────────────────────────
|
|
139
|
+
|
|
140
|
+
# The skill REGISTRY is a host service layered per scope; these rows register
|
|
141
|
+
# into THIS preset's layer, so they need no realm. `customSkillDirs` is the one
|
|
142
|
+
# `!!js` expression in this file, and it works here — unlike in the bundle patch,
|
|
143
|
+
# which is composed by a loader whose context has no `process` (M0 §4.2, and the
|
|
144
|
+
# header of `bundle/cordis.patch.yml`). A preset composition IS read by `Include`,
|
|
145
|
+
# which rewrites `baseUrl` to the composition's own directory; the deployment's
|
|
146
|
+
# shipped `cordis` preset uses the same expression for its own two skills.
|
|
147
|
+
- id: skill-filesystem
|
|
148
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
149
|
+
config:
|
|
150
|
+
customSkillDirs:
|
|
151
|
+
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
|
|
152
|
+
|
|
153
|
+
- id: tool-skill
|
|
154
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
155
|
+
|
|
156
|
+
# ── read-only file access (SPEC §6.4 "可选只读文件工具") ──────────────────────
|
|
157
|
+
#
|
|
158
|
+
# `glob` and `grep` only — see the header for why `tool-fs` cannot be narrowed
|
|
159
|
+
# and is therefore absent altogether.
|
|
160
|
+
- id: tool-fs-search
|
|
161
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
162
|
+
config:
|
|
163
|
+
sampleOverCapGlobResults: false
|
|
164
|
+
|
|
165
|
+
# ── compaction (SPEC §6.4 "Compaction") ──────────────────────────────────────
|
|
166
|
+
#
|
|
167
|
+
# `compaction-basic` reads `toolResultPruner` through `ctx.get`, so the pruner
|
|
168
|
+
# must share this realm rather than sit outside it. `tokenMeter` is deliberately
|
|
169
|
+
# NOT here: the meter stays on the host plane and owns the context-meter units the
|
|
170
|
+
# browser reads for every session, so it must not come and go with a preset.
|
|
171
|
+
- id: compaction
|
|
172
|
+
name: cordis:group
|
|
173
|
+
group: true
|
|
174
|
+
isolate:
|
|
175
|
+
compaction: true
|
|
176
|
+
toolResultPruner: true
|
|
177
|
+
config:
|
|
178
|
+
- id: compaction-basic
|
|
179
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
180
|
+
|
|
181
|
+
- id: command-compact
|
|
182
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
183
|
+
|
|
184
|
+
- id: tool-result-pruner
|
|
185
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
186
|
+
config:
|
|
187
|
+
thresholdChars: 8192
|
|
188
|
+
headChars: 4096
|
|
189
|
+
tailChars: 1024
|
|
190
|
+
|
|
191
|
+
# ── deliverables (SPEC §6.4 "Present/Deliverable") ───────────────────────────
|
|
192
|
+
#
|
|
193
|
+
# How a user receives an artifact the agent produced. It needs `fs` and
|
|
194
|
+
# `sessionProjections` from the host; it registers into `tools` and provides
|
|
195
|
+
# nothing, so it needs no realm.
|
|
196
|
+
- id: present
|
|
197
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
198
|
+
|
|
199
|
+
# ── DeepBlend Studio ────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
# The model-visible DeepBlend tools. This row CONSUMES the `blenderStudio`
|
|
202
|
+
# service published by the `@deepblend/dsh-blender-bundle` HOST composition — it
|
|
203
|
+
# publishes nothing, so it needs no realm and is safe to mount per session
|
|
204
|
+
# (SPEC §4.3/§4.4).
|
|
205
|
+
#
|
|
206
|
+
# WHAT THE CATALOG IS IS NOT WRITTEN HERE. The tool names, the count and the
|
|
207
|
+
# per-milestone groupings live in `packages/deepblend/tool/lib/`, and
|
|
208
|
+
# `composition/tool-plane-m3.e2e.mjs` owns the current total. This block used to
|
|
209
|
+
# be the kind of comment that goes on stating a count after the next milestone
|
|
210
|
+
# changed it — the copy nobody runs is the one that rots (D38, D43, D60) — so it
|
|
211
|
+
# points at the package instead.
|
|
212
|
+
#
|
|
213
|
+
# `blender_asset_ingest` is still absent: its host service and approval boundary
|
|
214
|
+
# are M5 work (SPEC §11). So is `blender_revision_restore`, which SPEC §11 lists
|
|
215
|
+
# and two of this repository's own documents tell a user to call — see
|
|
216
|
+
# `milestone-status.md` §16 for the measured gap.
|
|
217
|
+
- id: deepblend-tool
|
|
218
|
+
name: '@deepblend/dsh-blender-tool'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# WHY THERE IS NO `order` KEY, when every preset the deployment ships declares one.
|
|
2
|
+
#
|
|
3
|
+
# The roster sorts by `order` and treats an absent one as `Number.POSITIVE_INFINITY`
|
|
4
|
+
# (`@deepseek-ai/dsh-agent-presets`, "ordered by id" is the tie-break), so this preset appears after the four
|
|
5
|
+
# built-ins and among any other unordered ones by id. That is the intended position for a plugin's preset, and it
|
|
6
|
+
# is the more robust spelling: a declared number would tie with a future built-in that picks the same one, while
|
|
7
|
+
# an absent one always follows the deployment's own. Measured by reading the sort, not assumed.
|
|
8
|
+
name: DeepBlend Studio
|
|
9
|
+
description: 用 DeepSeek-Flash 规划、检查并控制 Blender 产出 3D 动画的专用 Agent:只会看场景、改场景、渲预览、判读测量并交付视频,没有 Shell、没有任意文件写入、没有任意 Python。
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deepblend-studio
|
|
3
|
+
description: Use when planning or producing a 3D animation with DeepBlend Studio — setting up a shot or product scene, bringing the user's own 3D model into a project, changing a Blender scene through SceneSpec patches, rendering and judging a preview, repairing a scene that measures badly, running, checking, cancelling or resuming a long final render, or delivering a finished video. Also use when a previous session's revision has to be restored.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DeepBlend Studio
|
|
7
|
+
|
|
8
|
+
You drive a Blender workbench. The scene is not a `.blend` file you edit — it is a
|
|
9
|
+
**SceneSpec** document, and every accepted change compiles that document into a new,
|
|
10
|
+
immutable **revision**. Everything you do is one of: read a revision, propose a patch
|
|
11
|
+
that becomes a revision, render a revision, or judge what a render measured.
|
|
12
|
+
|
|
13
|
+
## The cost asymmetry decides everything
|
|
14
|
+
|
|
15
|
+
Two kinds of render exist and they differ by three orders of magnitude:
|
|
16
|
+
|
|
17
|
+
| | `blender_preview_render` / `blender_preview_views` | `blender_final_render` |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| What it is for | looking at the scene | producing the deliverable |
|
|
20
|
+
| Resolution / engine | 640×360, fast engine, few samples | whatever the `final` profile says (typically 1920×1080, Cycles, hundreds of samples) |
|
|
21
|
+
| Measured cost | seconds for the whole image | **19.6–41.4 s for ONE frame** on the machine this was built on |
|
|
22
|
+
| A typical job | — | 450 frames ≈ **3.4 hours** |
|
|
23
|
+
|
|
24
|
+
So: **look with previews, and never start a final render to find out what something
|
|
25
|
+
looks like.** A final render is the last step of a decision you have already made,
|
|
26
|
+
not a way to make one. If you catch yourself wanting "just one frame to check", that
|
|
27
|
+
is a preview.
|
|
28
|
+
|
|
29
|
+
A final render is also the one operation that may need approval (SPEC §15.1), and it
|
|
30
|
+
is the one that can outlive the process. Treat starting it as a commitment — and know
|
|
31
|
+
the way out: **`blender_job_cancel`** stops it and *measures* that the renderer is gone
|
|
32
|
+
rather than assuming the signal landed. Cancelling keeps every frame already written, so
|
|
33
|
+
a cancelled render is resumable with `blender_final_render {resumeJobId}`, and a partially
|
|
34
|
+
written frame is re-rendered rather than kept. Cancel a render you no longer want; never
|
|
35
|
+
start a second one beside it, because a project can only have one delivery render at a
|
|
36
|
+
time (`RENDER_JOB_CONFLICT`).
|
|
37
|
+
|
|
38
|
+
## The order that works
|
|
39
|
+
|
|
40
|
+
1. **`blender_capabilities`** — what this Blender can actually do: engines, formats,
|
|
41
|
+
GPU. Call it once per session before promising anything. It reports absence as
|
|
42
|
+
data (`installed: false` plus a warning), never as a crash.
|
|
43
|
+
2. **`blender_project_create`** — a project owns a SceneSpec, a revision history and
|
|
44
|
+
the render output. The first revision is `r0001`.
|
|
45
|
+
3. **`blender_asset_ingest`** — only if the user brought a model file. It is a two-step
|
|
46
|
+
contract and the second step is easy to forget; see "Bringing in a model" below.
|
|
47
|
+
4. **`blender_scene_get`** — read before writing. Patching without reading is how a
|
|
48
|
+
stale `baseRevision` produces a refusal you then have to diagnose.
|
|
49
|
+
5. **`blender_scene_patch`** — the ONLY way to change a scene. There is no free-form
|
|
50
|
+
edit and no script. See "Revision discipline" below.
|
|
51
|
+
6. **`blender_preview_render` / `blender_preview_views`** — look at it. `preview_views`
|
|
52
|
+
renders several declared views in one Blender launch and returns a contact sheet.
|
|
53
|
+
7. **`blender_visual_review`** — ask for a measured judgement (see "Who decides what").
|
|
54
|
+
8. **`blender_visual_autofix`** — hand the loop a goal and let it iterate, when the
|
|
55
|
+
problem is one the measurements can see.
|
|
56
|
+
9. **`blender_final_render`** — only once the preview is right. It returns as soon as
|
|
57
|
+
the job is durable, not when it finishes.
|
|
58
|
+
10. **`blender_job_status`** — poll it, or come back to it in a later turn. A render
|
|
59
|
+
that outlives the process is still there: the store is on disk.
|
|
60
|
+
11. **`blender_export`** — publish the delivery package when you want it verified and
|
|
61
|
+
described rather than merely finished.
|
|
62
|
+
|
|
63
|
+
`blender_scene_validate` fits anywhere: it checks a scene, or a patch you have not
|
|
64
|
+
committed yet (`dryRun`), without touching the project. Use it when a patch is refused
|
|
65
|
+
and the reason is not obvious.
|
|
66
|
+
|
|
67
|
+
## Bringing in a model the user already has
|
|
68
|
+
|
|
69
|
+
`blender_asset_ingest` copies a file into the project; it **does not touch the scene**.
|
|
70
|
+
Two steps follow it, and a scene that skips the second one has an asset nobody uses:
|
|
71
|
+
|
|
72
|
+
1. `blender_asset_ingest {projectId, sourcePath}` — a local file, no approval. Pass
|
|
73
|
+
`sourceUrl` instead and the call **pauses to ask the operator**, because it leaves
|
|
74
|
+
the machine. Use `sourcePath` whenever the file is already there.
|
|
75
|
+
2. `blender_scene_patch {op: "asset.add", asset: {id, type, path, sha256}}` — declare it,
|
|
76
|
+
using the `assetId`, project-relative `path` and `sha256` the ingest returned. This is
|
|
77
|
+
what commits it: one accepted patch, one new revision, like every other change.
|
|
78
|
+
3. `blender_scene_patch {op: "entity.add", …}` with `type: "asset-instance"` and that
|
|
79
|
+
`assetId` — now something in the scene uses it.
|
|
80
|
+
|
|
81
|
+
`blender_scene_validate` is the check that the format is one this Blender build can
|
|
82
|
+
import. Setting `license` on anything you downloaded is worth the argument: an asset
|
|
83
|
+
whose provenance is unrecorded is one nobody can safely ship.
|
|
84
|
+
|
|
85
|
+
## Revision discipline
|
|
86
|
+
|
|
87
|
+
- **One successful patch = one immutable revision.** Nothing rewrites history.
|
|
88
|
+
- **A failed patch changes nothing.** The current revision's directory is never opened
|
|
89
|
+
for writing, so a refusal costs you a message, not your work.
|
|
90
|
+
- **Retrying is safe.** An omitted idempotency key is derived from the patch itself, so
|
|
91
|
+
an accidental resend returns the first result instead of making a second revision.
|
|
92
|
+
Pass an explicit key only when you deliberately want the same operations twice.
|
|
93
|
+
- **Going back is `blender_revision_restore`**, which moves the pointer forward to a
|
|
94
|
+
copy — it does not delete anything. Use it when a change measured worse.
|
|
95
|
+
- **Long renders resume; they do not restart.** `blender_final_render` with a
|
|
96
|
+
`resumeJobId` renders only the frames that are missing or corrupt. The frames on disk
|
|
97
|
+
are the authority, not the job record, so a render killed mid-frame resumes correctly.
|
|
98
|
+
Never start a fresh render of a job that already has frames.
|
|
99
|
+
- **A render you no longer want is cancelled, not abandoned.** `blender_job_cancel` takes
|
|
100
|
+
the job id, stops the renderer and reports whether the process is actually gone.
|
|
101
|
+
|
|
102
|
+
## Who decides what
|
|
103
|
+
|
|
104
|
+
This is the part most likely to be got wrong, and the workbench is built to make it
|
|
105
|
+
hard to get wrong:
|
|
106
|
+
|
|
107
|
+
- **The score and the issues are measured, not reported.** The host computes them
|
|
108
|
+
deterministically from the rendered pixels — composition, exposure, occlusion. The
|
|
109
|
+
same scene scores the same twice, and a contract test asserts that.
|
|
110
|
+
- **You are the only one who can see meaning.** The measurements cannot tell a watch
|
|
111
|
+
dial that is *supposed* to be bright from one that is blown out, or a screen that is
|
|
112
|
+
*supposed* to be hidden from one that is occluded by mistake. When a scene measures
|
|
113
|
+
well and still looks wrong, that judgement is yours to state, with the view and the
|
|
114
|
+
object named.
|
|
115
|
+
- **`blender_visual_review` returns three things separately**: the measured score,
|
|
116
|
+
the measured issues, and what the vision model *reported*. A reported finding is kept
|
|
117
|
+
only if the view exists, the category is in the closed set, and the evidence is not
|
|
118
|
+
empty. Never present a reported finding as a measurement.
|
|
119
|
+
- **Autofix only adopts a fix that measurably improved the score.** A round that did not
|
|
120
|
+
improve is rolled back — the revision stays in history, but the project does not move
|
|
121
|
+
to a worse version. If the loop stops short of the goal it hands back the open issues
|
|
122
|
+
with their measurements and the revisions it tried; that handover is the state to
|
|
123
|
+
continue from, not a failure to re-run.
|
|
124
|
+
|
|
125
|
+
## Reading the evidence you get back
|
|
126
|
+
|
|
127
|
+
- Preview results carry the **contact sheet** as an image. That image is what the vision
|
|
128
|
+
model sees, so you and it are looking at the same thing.
|
|
129
|
+
- An issue names **the number that triggered it** and the view it was measured in. Quote
|
|
130
|
+
those when you explain a problem; "it looks off" is not something a user can act on.
|
|
131
|
+
- A revision manifest records what was done, by whom, and which previews it produced.
|
|
132
|
+
`blender_project_get` summarises the history — read it before proposing a change that
|
|
133
|
+
depends on what already happened.
|
|
134
|
+
|
|
135
|
+
## Practical rules
|
|
136
|
+
|
|
137
|
+
- Prefer **one patch that changes one thing**. A patch that moves the camera, the lights
|
|
138
|
+
and a material at once cannot be scored back to a cause.
|
|
139
|
+
- Give every revision a **note**: the model reading the history later is usually you.
|
|
140
|
+
- Do not promise a resolution, a duration or a frame count before `blender_capabilities`
|
|
141
|
+
and the project's own render profile have told you what is possible.
|
|
142
|
+
- The workbench UI shows the same authoritative state you are reading. If a user says the
|
|
143
|
+
panel disagrees with you, the panel is reading the host and the host is reading disk —
|
|
144
|
+
find the difference rather than assuming either side.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# The `deepblend-dev` agent preset: DeepBlend Studio's development mode.
|
|
2
|
+
#
|
|
3
|
+
# It was copied from the deployment's `standard` preset (SPEC §6.2: a custom preset
|
|
4
|
+
# starts as a copy of an existing one) and differs from it in exactly one row —
|
|
5
|
+
# `deepblend-tool`, at the end of this file. The header used to still say it WAS the
|
|
6
|
+
# `standard` preset, which is the kind of inherited sentence nothing ever re-reads;
|
|
7
|
+
# it is corrected here because this file is now the repository's source of truth and
|
|
8
|
+
# `deepblend/tools/install-presets.mjs` deploys it.
|
|
9
|
+
#
|
|
10
|
+
# This file is an AGENT-PLANE composition. The roster mounts it ONCE under a
|
|
11
|
+
# standing scope; every session naming it joins by scope parentage, so the
|
|
12
|
+
# tools and prompt sections registered here cover each joined agent while a
|
|
13
|
+
# session's own state stays keyed per Session/Agent inside the plugins. The
|
|
14
|
+
# host composition (`base.cordis.yml` + `web.cordis.yml`) keeps everything a
|
|
15
|
+
# preset must not own: the registries themselves, the sandbox and approval
|
|
16
|
+
# stack, persistence, and the model route.
|
|
17
|
+
#
|
|
18
|
+
# A service row here MUST sit inside a group carrying an `isolate` realm.
|
|
19
|
+
# Without one it publishes into the root realm, where it is process-global —
|
|
20
|
+
# another preset publishing the same name collides, and a host reader would
|
|
21
|
+
# resolve one preset's instance for every session; `dsh-agent-presets` rejects
|
|
22
|
+
# that at mount. `true` means an entry-local realm: this standing mount's own
|
|
23
|
+
# private instance, apart from every other preset's. (A shared label does NOT
|
|
24
|
+
# pool instances — `provide()` throws on the second registration under the
|
|
25
|
+
# same realm symbol; labels join REALMS, and are not what this file needs.)
|
|
26
|
+
|
|
27
|
+
# ── identity ────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
# The preset's own persona, shadowing the deployment default for this agent.
|
|
30
|
+
# `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.
|
|
31
|
+
- id: persona
|
|
32
|
+
name: '@deepseek-ai/dsh-persona'
|
|
33
|
+
config:
|
|
34
|
+
suffix: Your working directory is {{cwd}}.
|
|
35
|
+
prefix: >-
|
|
36
|
+
You are a coding agent powered by the {{model}} model.
|
|
37
|
+
|
|
38
|
+
- id: agent-instructions
|
|
39
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
40
|
+
config:
|
|
41
|
+
maxBytes: 65536
|
|
42
|
+
|
|
43
|
+
# ── shell ───────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
|
46
|
+
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
|
47
|
+
# the criterion for host-plane ownership — injection resolves before any session
|
|
48
|
+
# exists, so there is no agent to key by. Behind a preset realm those variables
|
|
49
|
+
# never reached the model's shell at all. Both shell tools consume the host
|
|
50
|
+
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
|
51
|
+
# host-plane too.
|
|
52
|
+
- id: tool-bash
|
|
53
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
54
|
+
disabled: !!js process.platform === 'win32'
|
|
55
|
+
|
|
56
|
+
- id: tool-pwsh
|
|
57
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
58
|
+
disabled: !!js process.platform !== 'win32'
|
|
59
|
+
|
|
60
|
+
# ── filesystem ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
# Both register into the host `tools` registry and provide nothing, so
|
|
63
|
+
# they need no realm. The `fs` service and its policy stay in the host.
|
|
64
|
+
- id: tool-fs
|
|
65
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
66
|
+
|
|
67
|
+
- id: tool-fs-search
|
|
68
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
69
|
+
config:
|
|
70
|
+
sampleOverCapGlobResults: false
|
|
71
|
+
|
|
72
|
+
# ── background jobs ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
|
75
|
+
# its producers sit outside any realm this file could put it in — `tool-bash`
|
|
76
|
+
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
|
77
|
+
# to every sibling row, so `run_in_background` would answer "background jobs
|
|
78
|
+
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
|
79
|
+
# owning agent anyway, so one host instance serves every session. What a preset
|
|
80
|
+
# chooses is whether its agent can collect and stop background work at all.
|
|
81
|
+
- id: tool-jobs
|
|
82
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
83
|
+
|
|
84
|
+
# ── skills ──────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
# The skill REGISTRY lives in the host composition and is layered per scope:
|
|
87
|
+
# these rows register into THIS preset's layer of it, so they need no realm.
|
|
88
|
+
# `skill-filesystem` contributes local-root discovery for agents on this preset, and
|
|
89
|
+
# `tool-skill` gives them the catalog and loader; the merged catalog also
|
|
90
|
+
# carries whatever the deployment registered globally (repository plugins).
|
|
91
|
+
- id: skill-filesystem
|
|
92
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
93
|
+
|
|
94
|
+
- id: tool-skill
|
|
95
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
96
|
+
|
|
97
|
+
# ── goals ───────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
# The goal service and session driver stay on the host plane, where the Gateway
|
|
100
|
+
# can resolve them. The human command and model-facing tool register into this
|
|
101
|
+
# preset's scoped layers.
|
|
102
|
+
- id: command-goal
|
|
103
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
104
|
+
|
|
105
|
+
- id: tool-goal
|
|
106
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
107
|
+
|
|
108
|
+
# ── plan mode ───────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
# Plan state is per-agent by nature, so an entry-local realm is not a
|
|
111
|
+
# workaround here — it is the correct lifetime.
|
|
112
|
+
- id: planning
|
|
113
|
+
name: cordis:group
|
|
114
|
+
group: true
|
|
115
|
+
isolate:
|
|
116
|
+
planMode: true
|
|
117
|
+
config:
|
|
118
|
+
- id: plan-mode
|
|
119
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
120
|
+
config:
|
|
121
|
+
section: |
|
|
122
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
123
|
+
|
|
124
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
125
|
+
|
|
126
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
127
|
+
|
|
128
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
129
|
+
|
|
130
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
131
|
+
|
|
132
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
133
|
+
|
|
134
|
+
# ── compaction ──────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
|
137
|
+
# share this realm rather than sit outside it.
|
|
138
|
+
#
|
|
139
|
+
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
|
|
140
|
+
# plane, and the rows here resolve that one instance. It takes no configuration,
|
|
141
|
+
# keys every fold by Session, and owns the context-meter projection units the
|
|
142
|
+
# browser reads for every session — behind a realm those units would come and go
|
|
143
|
+
# with whichever presets happen to be mounted. What a preset chooses is whether
|
|
144
|
+
# its agent compacts at all, which is `compaction-basic` below.
|
|
145
|
+
- id: compaction
|
|
146
|
+
name: cordis:group
|
|
147
|
+
group: true
|
|
148
|
+
isolate:
|
|
149
|
+
compaction: true
|
|
150
|
+
toolResultPruner: true
|
|
151
|
+
config:
|
|
152
|
+
- id: compaction-basic
|
|
153
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
154
|
+
|
|
155
|
+
- id: command-compact
|
|
156
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
157
|
+
|
|
158
|
+
- id: tool-result-pruner
|
|
159
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
160
|
+
config:
|
|
161
|
+
thresholdChars: 8192
|
|
162
|
+
headChars: 4096
|
|
163
|
+
tailChars: 1024
|
|
164
|
+
|
|
165
|
+
# ── delegation and workflows ────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
# The `subagents` registry and its spawn/fork backends live in the HOST
|
|
168
|
+
# composition: the registry is a process singleton whose cross-session queries
|
|
169
|
+
# the api-proxy serves to the browser, and a provider name may only be
|
|
170
|
+
# registered once. This preset contributes the delegation TOOLS, which resolve
|
|
171
|
+
# that host registry.
|
|
172
|
+
#
|
|
173
|
+
# `workflows` is different — nothing outside an agent reads it — so every row
|
|
174
|
+
# that reaches it shares one entry-local realm here, and a consumer left
|
|
175
|
+
# outside would resolve a host registry this preset does not populate.
|
|
176
|
+
- id: delegation
|
|
177
|
+
name: cordis:group
|
|
178
|
+
group: true
|
|
179
|
+
isolate:
|
|
180
|
+
workflowEngine: true
|
|
181
|
+
config:
|
|
182
|
+
- id: tool-subagent-control
|
|
183
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
184
|
+
|
|
185
|
+
- id: tool-subagent-list-agents
|
|
186
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
187
|
+
|
|
188
|
+
- id: tool-subagent
|
|
189
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
190
|
+
config:
|
|
191
|
+
provider: spawn
|
|
192
|
+
toolName: subagent
|
|
193
|
+
modelSelectionSettings: true
|
|
194
|
+
backgroundMode: continuable
|
|
195
|
+
|
|
196
|
+
# Fork omits model selection so provider/model stay equal to the parent and
|
|
197
|
+
# the inherited history remains eligible for KV Cache reuse. This preset
|
|
198
|
+
# keeps fork continuable; parent and child inherit the same messaging tool,
|
|
199
|
+
# while the parent id and return guidance follow the inherited history.
|
|
200
|
+
- id: tool-subagent-fork
|
|
201
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
202
|
+
config:
|
|
203
|
+
provider: fork
|
|
204
|
+
toolName: subagent_fork
|
|
205
|
+
backgroundMode: continuable
|
|
206
|
+
|
|
207
|
+
# Production dsh does not install these optional providers. Install the
|
|
208
|
+
# matching Bundle in this Profile and restart the Host, then copy this
|
|
209
|
+
# preset and remove `disabled` from the matching tool row. Host availability
|
|
210
|
+
# alone grants no tool.
|
|
211
|
+
- id: tool-subagent-codex
|
|
212
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
213
|
+
disabled: true
|
|
214
|
+
config:
|
|
215
|
+
provider: codex
|
|
216
|
+
toolName: subagent_codex
|
|
217
|
+
backgroundMode: one-shot
|
|
218
|
+
maxDepth: provider-managed
|
|
219
|
+
|
|
220
|
+
- id: tool-subagent-claude-code
|
|
221
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
222
|
+
disabled: true
|
|
223
|
+
config:
|
|
224
|
+
provider: claude-code
|
|
225
|
+
toolName: subagent_claude_code
|
|
226
|
+
backgroundMode: one-shot
|
|
227
|
+
maxDepth: provider-managed
|
|
228
|
+
|
|
229
|
+
- id: workflow-worker-thread
|
|
230
|
+
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
|
231
|
+
config:
|
|
232
|
+
provider: spawn
|
|
233
|
+
|
|
234
|
+
- id: tool-workflow
|
|
235
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
236
|
+
|
|
237
|
+
- id: tool-ralph
|
|
238
|
+
name: '@deepseek-ai/dsh-tool-ralph'
|
|
239
|
+
config:
|
|
240
|
+
subagentProvider: spawn
|
|
241
|
+
maxRounds: 64
|
|
242
|
+
|
|
243
|
+
# ── remaining model-facing rows ─────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
- id: tool-ask-user
|
|
246
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
247
|
+
|
|
248
|
+
- id: tool-todo
|
|
249
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
250
|
+
config:
|
|
251
|
+
allowParallelInProgress: true
|
|
252
|
+
|
|
253
|
+
# The `web` service and its search provider stay in the host composition; only
|
|
254
|
+
# the model-facing tool is per-session.
|
|
255
|
+
- id: tool-web
|
|
256
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
257
|
+
config:
|
|
258
|
+
fetch: true
|
|
259
|
+
searchTimeoutMs: 60000
|
|
260
|
+
|
|
261
|
+
- id: present
|
|
262
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
263
|
+
|
|
264
|
+
# ── DeepBlend Studio ────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
# The model-visible DeepBlend tools. This row CONSUMES the `blenderStudio`
|
|
267
|
+
# service published by the `@deepblend/dsh-blender-bundle` Host composition —
|
|
268
|
+
# it publishes nothing, so it needs no `isolate` realm and is safe to mount
|
|
269
|
+
# per session (SPEC §4.3/§4.4).
|
|
270
|
+
#
|
|
271
|
+
# WHAT THE CATALOG IS IS NOT WRITTEN HERE. The tool names, the count and the
|
|
272
|
+
# per-milestone groupings live in `packages/deepblend/tool/lib/`, and each
|
|
273
|
+
# milestone's suite asserts that its own batch is complete and usable
|
|
274
|
+
# (`composition/tool-plane-m3.e2e.mjs` owns the current total).
|
|
275
|
+
#
|
|
276
|
+
# This block used to enumerate the tools and state a count, and it went on stating
|
|
277
|
+
# it after the next milestone registered the ones it described as not yet arrived.
|
|
278
|
+
# The copy nobody runs is always the one that rots (architecture-decisions D38,
|
|
279
|
+
# D43, D60), so it is pointed at rather than restated. What DOES belong here is
|
|
280
|
+
# the rule that outlives every milestone: a tool the model can see is a promise
|
|
281
|
+
# the runtime must keep. As of M5 that list is COMPLETE — every tool SPEC §11 names is
|
|
282
|
+
# registered — so the rule no longer has an example of something held back, which is
|
|
283
|
+
# the state it was written to reach.
|
|
284
|
+
- id: deepblend-tool
|
|
285
|
+
name: '@deepblend/dsh-blender-tool'
|