@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 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'
@@ -0,0 +1,2 @@
1
+ name: DeepBlend 开发模式
2
+ description: 开发与调试 DeepBlend Studio 的完整编码 Agent:在标准模式全部能力之上提供 Blender 能力检测工具,用于开发 Blender Runtime、检查项目文件、执行测试与查看日志产物。