acmer-coach 2.6.0 → 2.6.2

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/cordis.patch.yml CHANGED
@@ -1,10 +1,155 @@
1
1
  # acmer-coach 组合层(bundle 形态)。
2
2
  #
3
- # ⚠️ 两条路**不能并存**:这份 insert 和 profile 的 cordis.patch.yml 里再来
4
- # 一条 insert,插件就会被挂两次。profile 那边现在是空的(留了说明)。
5
- #
6
- # ⚠️ id 必须和 index.js 导出的 `name` 逐字相同,name 必须和**包名**相同 ——
7
- # 对不上不会报错,工具会**静默不注册**,界面上完全看不出区别。
3
+ # 教练实例与小鲸预设由此统一声明,不在 profile 重复 insert 相同条目。
4
+ # 已有个人 xiaojing 预设使用不同 id,可继续并存。
5
+ # 教练实例保留 acmer-coach 的 id/name;原生预设使用独立稳定 id。
8
6
  - insert:
9
7
  - id: acmer-coach
10
8
  name: 'acmer-coach'
9
+
10
+ # 原生 standard 预设配置快照(dsh 0.2.0-rc.2);升级宿主时同步清单。
11
+ - insert:
12
+ - id: preset-acmer-coach-xiaojing
13
+ name: '@deepseek-ai/dsh-agent-preset'
14
+ config:
15
+ id: acmer-coach-xiaojing
16
+ name: 小鲸教练
17
+ description: 鲸鱼娘算法教练,保留原生标准预设的工具、计划、压缩与委派能力。
18
+ order: 20
19
+ plugins:
20
+ - id: persona
21
+ name: '@deepseek-ai/dsh-persona'
22
+ config:
23
+ suffix: Your working directory is {{cwd}}.
24
+ prefix: 你是小鲸教练。默认角色风格遵循 coach:persona,教学、来源与证据遵循 coach:rules;优先尊重用户当前偏好和宿主自定义风格。
25
+ - id: agent-instructions
26
+ name: '@deepseek-ai/dsh-agent-instructions'
27
+ config:
28
+ maxBytes: 65536
29
+ - id: tool-bash
30
+ name: '@deepseek-ai/dsh-tool-bash'
31
+ disabled: !!js process.platform === 'win32'
32
+ - id: tool-pwsh
33
+ name: '@deepseek-ai/dsh-tool-pwsh'
34
+ disabled: !!js process.platform !== 'win32'
35
+ - id: tool-fs
36
+ name: '@deepseek-ai/dsh-tool-fs'
37
+ - id: tool-fs-search
38
+ name: '@deepseek-ai/dsh-tool-fs-search'
39
+ config:
40
+ sampleOverCapGlobResults: false
41
+ - id: tool-jobs
42
+ name: '@deepseek-ai/dsh-tool-jobs'
43
+ - id: skill-filesystem
44
+ name: '@deepseek-ai/dsh-skill-filesystem'
45
+ - id: tool-skill
46
+ name: '@deepseek-ai/dsh-tool-skill'
47
+ - id: command-goal
48
+ name: '@deepseek-ai/dsh-command-goal'
49
+ - id: tool-goal
50
+ name: '@deepseek-ai/dsh-tool-goal'
51
+ - id: planning
52
+ name: cordis:group
53
+ group: true
54
+ isolate:
55
+ planMode: true
56
+ config:
57
+ - id: plan-mode
58
+ name: '@deepseek-ai/dsh-plan-mode'
59
+ config:
60
+ section: |
61
+ 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.
62
+
63
+ 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.
64
+
65
+ 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.
66
+
67
+ 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.
68
+
69
+ 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.
70
+
71
+ 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.
72
+ - id: compaction
73
+ name: cordis:group
74
+ group: true
75
+ isolate:
76
+ compaction: true
77
+ toolResultPruner: true
78
+ config:
79
+ - id: compaction-basic
80
+ name: '@deepseek-ai/dsh-compaction-basic'
81
+ - id: command-compact
82
+ name: '@deepseek-ai/dsh-command-compact'
83
+ - id: tool-result-pruner
84
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
85
+ config:
86
+ thresholdChars: 8192
87
+ headChars: 4096
88
+ tailChars: 1024
89
+ - id: delegation
90
+ name: cordis:group
91
+ group: true
92
+ isolate:
93
+ workflowEngine: true
94
+ config:
95
+ - id: tool-subagent-control
96
+ name: '@deepseek-ai/dsh-tool-subagent-control'
97
+ - id: tool-subagent-list-agents
98
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
99
+ - id: tool-subagent
100
+ name: '@deepseek-ai/dsh-tool-subagent'
101
+ config:
102
+ provider: spawn
103
+ toolName: subagent
104
+ modelSelectionSettings: true
105
+ backgroundMode: continuable
106
+ - id: tool-subagent-fork
107
+ name: '@deepseek-ai/dsh-tool-subagent'
108
+ config:
109
+ provider: fork
110
+ toolName: subagent_fork
111
+ backgroundMode: continuable
112
+ - id: tool-subagent-codex
113
+ name: '@deepseek-ai/dsh-tool-subagent'
114
+ disabled: true
115
+ config:
116
+ provider: codex
117
+ toolName: subagent_codex
118
+ backgroundMode: one-shot
119
+ maxDepth: provider-managed
120
+ - id: tool-subagent-claude-code
121
+ name: '@deepseek-ai/dsh-tool-subagent'
122
+ disabled: true
123
+ config:
124
+ provider: claude-code
125
+ toolName: subagent_claude_code
126
+ backgroundMode: one-shot
127
+ maxDepth: provider-managed
128
+ - id: workflow-ptc
129
+ name: '@deepseek-ai/dsh-workflow-ptc'
130
+ config:
131
+ provider: spawn
132
+ - id: tool-workflow
133
+ name: '@deepseek-ai/dsh-tool-workflow'
134
+ - id: tool-ralph
135
+ name: '@deepseek-ai/dsh-tool-ralph'
136
+ disabled: true
137
+ config:
138
+ subagentProvider: spawn
139
+ maxRounds: 64
140
+ - id: tool-ask-user
141
+ name: '@deepseek-ai/dsh-tool-ask-user'
142
+ - id: tool-todo
143
+ name: '@deepseek-ai/dsh-tool-todo'
144
+ config:
145
+ allowParallelInProgress: true
146
+ - id: tool-web
147
+ name: '@deepseek-ai/dsh-tool-web'
148
+ config:
149
+ fetch: true
150
+ searchTimeoutMs: 60000
151
+ - id: present
152
+ name: '@deepseek-ai/dsh-tool-present'
153
+ - id: tool-plugin-manager
154
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
155
+ disabled: true
@@ -19,7 +19,15 @@ function sources(value, label) {
19
19
  assert.ok(typeof source.title === 'string' && source.title.trim(), `${label}: source title required`)
20
20
  const url = new URL(source.url)
21
21
  assert.equal(url.protocol, 'https:', `${label}: public HTTPS source required`)
22
- assert.ok(!url.username && !url.password && !url.search, `${label}: no account credentials or query data`)
22
+ assert.ok(!url.username && !url.password, `${label}: no account credentials`)
23
+ if (url.search) {
24
+ assert.equal(url.hostname, 'contest.ucup.ac', `${label}: only public official attachments may use query parameters`)
25
+ assert.equal(url.pathname, '/download.php')
26
+ assert.deepEqual([...url.searchParams.keys()].sort(), ['id', 'r', 'type'])
27
+ assert.equal(url.searchParams.get('type'), 'attachments')
28
+ assert.match(url.searchParams.get('id'), /^\d+$/)
29
+ assert.match(url.searchParams.get('r'), /^\d+$/)
30
+ }
23
31
  assert.ok(url.pathname !== '/', `${label}: link to evidence, not a homepage`)
24
32
  }
25
33
  }
@@ -35,27 +43,54 @@ test('contest evidence exposes actual reviewed samples, never a complete syllabu
35
43
  const store = createStrategyStore({ dataDir: dir, competenciesPath: fileURLToPath(new URL('COMPETENCIES.yaml', knowledge)) })
36
44
  const profiles = store.target('profiles').availableProfiles
37
45
  const regional = profiles.find(profile => profile.id === 'regional-diagnostic')
38
- assert.equal(regional.coverage.sampledProblems, 19)
39
- assert.equal(regional.coverage.reviewedSolutions, 19)
40
- assert.deepEqual(regional.coverage.years, [2022, 2023])
46
+ assert.equal(regional.coverage.sampledProblems, 70)
47
+ assert.equal(regional.coverage.reviewedSolutions, 70)
48
+ assert.deepEqual(regional.coverage.years, [2022, 2023, 2024, 2025])
41
49
  assert.equal(regional.coverage.isSyllabus, false)
42
50
  assert.deepEqual(regional.samples.filter(sample => sample.year === 2022).map(sample => sample.problem).sort(), [...'ABCDEFGHIJKLM'])
43
- assert.equal(new Set(regional.samples.map(sample => `${sample.contest}:${sample.year}:${sample.problem}`)).size, 19)
51
+ assert.equal(new Set(regional.samples.map(sample => `${sample.contest}:${sample.year}:${sample.problem}`)).size, 70)
52
+ const additions = [
53
+ ["Xi'an", 2025, 'ABCDEFGHIJKLM', 'contest.ucup.ac', '/download.php'],
54
+ ['Hangzhou', 2024, 'ABCDEFGHIJKLM', 'contest.ucup.ac', '/download.php'],
55
+ ['Shanghai', 2025, 'ABCDEFGHIJKLM', 'ucup.ac', '/tutorials/tutorials-4-12-zh.pdf'],
56
+ ['Hong Kong', 2025, 'ABCDEFGHIJKL', 'ucup.ac', '/tutorials/tutorials-4-14-en.pdf'],
57
+ ]
58
+ for (const [city, year, letters, host, pathname] of additions) {
59
+ const samples = regional.samples.filter(sample => sample.contest === city && sample.year === year)
60
+ assert.deepEqual(samples.map(sample => sample.problem).sort(), [...letters], `${city}: reviewed full paper`)
61
+ for (const sample of samples) {
62
+ const url = new URL(sample.sourceUrl)
63
+ assert.equal(url.hostname, host)
64
+ assert.equal(url.pathname, pathname)
65
+ assert.match(url.hash, /^#page=[1-9]\d*$/)
66
+ if (city === "Xi'an" || city === 'Hangzhou') {
67
+ assert.equal(url.searchParams.get('type'), 'attachments')
68
+ assert.equal(url.searchParams.get('id'), city === "Xi'an" ? '2562' : '1893')
69
+ assert.equal(url.searchParams.get('r'), '1')
70
+ }
71
+ }
72
+ }
44
73
  assert.match(regional.title, /中国|亚洲/)
45
74
  for (const sample of regional.samples) {
46
75
  assert.ok(sample.model && sample.solutionReviewed)
47
- assert.match(sample.sourceUrl, sample.year === 2022
76
+ if (sample.contest === 'Nanjing') assert.match(sample.sourceUrl, sample.year === 2022
48
77
  ? /^https:\/\/sua\.ac\/wiki\/2022-icpc-nanjing\/tutorial-sketch-zh\.pdf#page=\d+$/
49
78
  : /^https:\/\/universal-cup-website\.qoj\.ac\/files\/ucup\/tutorials\/tutorials-2-11\.pdf#page=\d+$/)
50
79
  assert.ok(sample.competencyIds.every(id => competencies.some(card => card.id === id)))
51
80
  }
52
81
  const xian = profiles.find(profile => profile.id === 'xian-pending')
53
- assert.equal(xian.coverage.sampledProblems, 0)
54
- assert.equal(xian.coverage.reviewedSolutions, 0)
82
+ assert.equal(xian.coverage.sampledProblems, 13)
83
+ assert.equal(xian.coverage.reviewedSolutions, 13)
84
+ assert.deepEqual(xian.coverage.years, [2025])
85
+ assert.deepEqual(xian.samples, regional.samples.filter(sample => sample.contest === "Xi'an"))
55
86
  assert.equal(xian.coverage.isSyllabus, false)
56
87
  assert.equal(store.target('set', { expectedRevision: 0, contest: '区域赛', date: '2026-10-17', result: '稳定', profileId: regional.id, weeklyHours: 25,
57
88
  priorities: [{ id: 'graph-modeling', weight: 10 }] }).ok, true)
58
- assert.equal(store.focus().profile.coverage.sampledProblems, 19)
89
+ assert.equal(store.focus().profile.coverage.sampledProblems, 70)
90
+ assert.equal(store.target('set', { expectedRevision: 1, contest: '西安区域赛', date: '2026-10-17', result: '稳定', profileId: 'xian-pending', weeklyHours: 25,
91
+ priorities: [{ id: 'graph-modeling', weight: 10 }] }).ok, true)
92
+ assert.equal(store.focus().profile.coverage.reviewedSolutions, 13)
93
+ assert.equal(store.focus().profile.id, 'xian-pending')
59
94
  assert.equal(profiles.find(profile => profile.id === 'nwerc-reference').coverage.reviewedSolutions, 13)
60
95
  } finally { rmSync(dir, { recursive: true, force: true }) }
61
96
  })
@@ -0,0 +1,236 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import { syncBuiltinESMExports } from 'node:module'
5
+ import { tmpdir } from 'node:os'
6
+ import { dirname, join } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { parse } from 'yaml'
9
+
10
+ const personaPath = new URL('../assets/persona/xiaojing-coach.md', import.meta.url)
11
+ const rulesPath = new URL('../assets/rules/coach-rules.md', import.meta.url)
12
+ let imports = 0
13
+
14
+ async function fixture(t) {
15
+ const dir = fs.mkdtempSync(join(tmpdir(), 'coach-persona-'))
16
+ const previous = Object.fromEntries(['COACH_DATA_DIR', 'COACH_DB', 'COACH_RULES_DIR'].map(key => [key, process.env[key]]))
17
+ process.env.COACH_DATA_DIR = dir
18
+ process.env.COACH_DB = join(dir, 'unused.db')
19
+ delete process.env.COACH_RULES_DIR
20
+ t.after(() => {
21
+ for (const [key, value] of Object.entries(previous)) {
22
+ if (value === undefined) delete process.env[key]
23
+ else process.env[key] = value
24
+ }
25
+ fs.rmSync(dir, { recursive: true, force: true })
26
+ })
27
+ const { apply } = await import(`../index.js?persona-check=${++imports}`)
28
+ const tools = new Map(), hooks = new Map(), sections = []
29
+ const ctx = {
30
+ tools: { register: tool => tools.set(tool.name, tool) },
31
+ get: name => name === 'systemPrompt' ? { section: section => sections.push(section) } : undefined,
32
+ on: (name, handler) => hooks.set(name, handler),
33
+ inject() {},
34
+ }
35
+ return { dir, apply, ctx, tools, hooks, sections }
36
+ }
37
+
38
+ function denyReads(t, denied) {
39
+ const read = fs.readFileSync
40
+ const mocked = t.mock.method(fs, 'readFileSync', (path, ...args) => {
41
+ if (denied(String(path).replaceAll('\\', '/'))) throw Object.assign(new Error('fixture missing file'), { code: 'ENOENT' })
42
+ return read(path, ...args)
43
+ })
44
+ syncBuiltinESMExports()
45
+ return () => { mocked.mock.restore(); syncBuiltinESMExports() }
46
+ }
47
+
48
+ function assertTools(f) {
49
+ assert.equal(f.tools.size, 31)
50
+ assert.ok(f.tools.has('coach_status'))
51
+ assert.ok(f.tools.has('coach_note'))
52
+ }
53
+
54
+ test('apply loads packaged static persona before custom style without local personas', async t => {
55
+ const f = await fixture(t)
56
+ const restore = denyReads(t, path => path.includes('/personas/'))
57
+ try { f.apply(f.ctx) } finally { restore() }
58
+ const persona = f.sections.find(section => section.name === 'coach:persona')
59
+ assert.ok(persona, 'packaged persona must load through the public apply entry')
60
+ assert.equal(persona.text, fs.readFileSync(personaPath, 'utf8'))
61
+ assert.equal(persona.order, -100)
62
+ const rules = f.sections.find(section => section.name === 'coach:rules')
63
+ assert.equal(rules.text, fs.readFileSync(rulesPath, 'utf8'))
64
+ assert.equal(rules.order, 400)
65
+ const custom = { name: 'deployment:persona-prefix', order: 0, text: '自定义风格' }
66
+ assert.deepEqual([...f.sections, custom].sort((a, b) => a.order - b.order).map(section => section.name),
67
+ ['coach:persona', 'deployment:persona-prefix', 'coach:rules'])
68
+ assertTools(f)
69
+ })
70
+
71
+ test('missing packaged persona leaves rules and tools available', async t => {
72
+ const f = await fixture(t)
73
+ const restore = denyReads(t, path => path.endsWith('/assets/persona/xiaojing-coach.md'))
74
+ try { f.apply(f.ctx) } finally { restore() }
75
+ assert.deepEqual(f.sections.map(section => section.name), ['coach:rules'])
76
+ assert.equal(f.sections[0].text, fs.readFileSync(rulesPath, 'utf8'))
77
+ assertTools(f)
78
+ })
79
+
80
+ test('rejected persona section leaves rules and tools available', async t => {
81
+ const f = await fixture(t)
82
+ f.ctx.get = () => ({ section(section) {
83
+ if (section.name === 'coach:persona') throw new Error('host rejects persona section')
84
+ f.sections.push(section)
85
+ } })
86
+ f.apply(f.ctx)
87
+ assert.deepEqual(f.sections.map(section => section.name), ['coach:rules'])
88
+ assert.equal(f.sections[0].text, fs.readFileSync(rulesPath, 'utf8'))
89
+ assertTools(f)
90
+ })
91
+
92
+ test('missing rules leaves packaged persona and tools available', async t => {
93
+ const f = await fixture(t)
94
+ const restore = denyReads(t, path => path.endsWith('/assets/rules/coach-rules.md'))
95
+ try { f.apply(f.ctx) } finally { restore() }
96
+ assert.deepEqual(f.sections.map(section => section.name), ['coach:persona'])
97
+ assert.equal(f.sections[0].text, fs.readFileSync(personaPath, 'utf8'))
98
+ assertTools(f)
99
+ })
100
+
101
+ test('rejected rules section leaves packaged persona and tools available', async t => {
102
+ const f = await fixture(t)
103
+ f.ctx.get = () => ({ section(section) {
104
+ if (section.name === 'coach:rules') throw new Error('host rejects rules section')
105
+ f.sections.push(section)
106
+ } })
107
+ f.apply(f.ctx)
108
+ assert.deepEqual(f.sections.map(section => section.name), ['coach:persona'])
109
+ assert.equal(f.sections[0].text, fs.readFileSync(personaPath, 'utf8'))
110
+ assertTools(f)
111
+ })
112
+
113
+ for (const unavailable of ['missing service', 'missing get', 'throwing get']) {
114
+ test(`all 31 tools load with ${unavailable}`, async t => {
115
+ const f = await fixture(t)
116
+ if (unavailable === 'missing get') delete f.ctx.get
117
+ else f.ctx.get = () => {
118
+ if (unavailable === 'throwing get') throw new Error('service unavailable')
119
+ return undefined
120
+ }
121
+ assert.doesNotThrow(() => f.apply(f.ctx))
122
+ assert.deepEqual(f.sections, [])
123
+ assertTools(f)
124
+ const result = await f.tools.get('coach_target').execute({ action: 'profiles' })
125
+ assert.equal(result.ok, true)
126
+ assert.ok(result.availableProfiles.length)
127
+ })
128
+ }
129
+
130
+ test('persona is read once into static sections, never messages or training ledgers', async t => {
131
+ const f = await fixture(t)
132
+ fs.writeFileSync(join(f.dir, 'MAP.yaml'), 'meta: {version: 1}\nnodes: [{id: fixture-node, name: 测试节点}]\n')
133
+ fs.writeFileSync(join(f.dir, 'PROGRESS.yaml'), 'version: 1\ncursor: fixture-node\nnodes: {}\npending: null\n')
134
+ const snapshot = () => fs.readdirSync(f.dir).sort().map(name => [name, fs.readFileSync(join(f.dir, name), 'utf8')])
135
+ const before = snapshot()
136
+ const reads = []
137
+ const restore = denyReads(t, path => {
138
+ if (path.endsWith('/assets/persona/xiaojing-coach.md') || path.endsWith('/assets/rules/coach-rules.md')) reads.push(path)
139
+ return false
140
+ })
141
+ try {
142
+ f.apply(f.ctx)
143
+ assert.equal(f.sections.length, 2)
144
+ assert.ok(f.sections.every(section => typeof section.text === 'string'))
145
+ const initial = structuredClone(f.sections)
146
+ const hook = f.hooks.get('agent/pre-step')
147
+ for (const step of [1, 2, 1]) {
148
+ const messages = [{ role: 'user', content: [{ type: 'text', text: '继续' }] }]
149
+ const result = await hook({ messages, step }, async () => ({ kind: 'enter', messages }))
150
+ assert.deepEqual(result.messages[0], messages[0])
151
+ for (const message of result.messages.slice(1)) {
152
+ assert.ok(message.content.every(block => block.type === 'text' && block.text.startsWith('<coach_state>\n')))
153
+ }
154
+ assert.ok(!JSON.stringify(result.messages).includes('鲸鱼娘'))
155
+ }
156
+ assert.deepEqual(f.sections, initial)
157
+ assert.equal(reads.filter(path => path.endsWith('/xiaojing-coach.md')).length, 1)
158
+ assert.equal(reads.filter(path => path.endsWith('/coach-rules.md')).length, 1)
159
+ } finally { restore() }
160
+ assert.deepEqual(snapshot(), before)
161
+ assertTools(f)
162
+ })
163
+
164
+ test('bundle declares a native 小鲸教练 preset with the stock standard capability composition', async () => {
165
+ const patches = parse(fs.readFileSync(new URL('../cordis.patch.yml', import.meta.url), 'utf8'), {
166
+ customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: value => value }],
167
+ })
168
+ const entries = patches.flatMap(patch => patch.insert ?? [])
169
+ const preset = entries.find(entry => entry.config?.id === 'acmer-coach-xiaojing')
170
+ assert.ok(preset, 'installing the bundle must declare the selectable native 小鲸教练 preset')
171
+ assert.equal(preset.id, 'preset-acmer-coach-xiaojing')
172
+ assert.equal(preset.name, '@deepseek-ai/dsh-agent-preset')
173
+ const config = preset.config
174
+ assert.equal(config.name, '小鲸教练')
175
+ assert.equal(entries.filter(entry => entry.name === 'acmer-coach').length, 1)
176
+ assert.ok(!entries.some(entry => entry.id === 'agent-preset-registry'), 'bundle must not change the selected default')
177
+ const persona = config.plugins.find(entry => entry.id === 'persona')
178
+ assert.equal(persona.name, '@deepseek-ai/dsh-persona')
179
+ assert.ok(persona.config.prefix.includes('coach:persona'))
180
+ assert.ok(persona.config.prefix.includes('coach:rules'))
181
+ assert.ok(persona.config.prefix.length < 200, 'reuse packaged persona without duplicating its full text')
182
+ assert.equal(persona.config.suffix, 'Your working directory is {{cwd}}.')
183
+ const ids = config.plugins.map(entry => entry.id)
184
+ for (const id of ['tool-bash', 'tool-pwsh', 'tool-fs', 'tool-fs-search', 'tool-jobs', 'skill-filesystem', 'tool-skill',
185
+ 'planning', 'compaction', 'delegation', 'tool-ask-user', 'tool-todo', 'tool-web', 'present']) assert.ok(ids.includes(id), id)
186
+ const delegation = config.plugins.find(entry => entry.id === 'delegation')
187
+ for (const id of ['tool-subagent-codex', 'tool-subagent-claude-code', 'tool-ralph']) {
188
+ assert.equal(delegation.config.find(entry => entry.id === id).disabled, true, `${id} stays disabled`)
189
+ }
190
+ assert.equal(config.plugins.find(entry => entry.id === 'tool-bash').disabled, "process.platform === 'win32'")
191
+ assert.equal(config.plugins.find(entry => entry.id === 'tool-pwsh').disabled, "process.platform !== 'win32'")
192
+ assert.equal(config.plugins.find(entry => entry.id === 'tool-plugin-manager').disabled, true)
193
+ assert.ok(!JSON.stringify(config).match(/[A-Z]:[\\/]|accountId|userId|handle|rating/), 'preset must contain no private profile paths or account data')
194
+ })
195
+
196
+ test('packaged preset passes the installed native loader and configuration schemas', async t => {
197
+ let boot, presets, registry, loader
198
+ try {
199
+ [boot, presets, registry, loader] = await Promise.all([
200
+ import('@deepseek-ai/dsh-app-boot'), import('@deepseek-ai/dsh-agent-preset'),
201
+ import('@deepseek-ai/dsh-agent-preset-registry'), import('@deepseek-ai/cordis-plugin-loader'),
202
+ ])
203
+ } catch (error) {
204
+ if (error.code !== 'ERR_MODULE_NOT_FOUND') throw error
205
+ t.skip('native preset packages are supplied by the dsh host; portable bundle checks still run')
206
+ return
207
+ }
208
+ const entries = boot.composeEntries([boot.loadOverlayPatches('dsh', fileURLToPath(new URL('../cordis.patch.yml', import.meta.url)))])
209
+ const row = entries.find(entry => entry.config?.id === 'acmer-coach-xiaojing')
210
+ assert.ok(row)
211
+ const config = presets.default.Config(row.config)
212
+ assert.equal(registry.entryListProblem(config.plugins), undefined)
213
+ const enabledShell = config.plugins.filter(entry => ['tool-bash', 'tool-pwsh'].includes(entry.id))
214
+ .filter(entry => !loader.interpolate({}, entry.disabled)).map(entry => entry.id)
215
+ assert.deepEqual(enabledShell, [process.platform === 'win32' ? 'tool-pwsh' : 'tool-bash'])
216
+ async function validate(rows) {
217
+ for (const entry of rows) {
218
+ if (entry.group) { await validate(entry.config); continue }
219
+ if (loader.interpolate({}, entry.disabled)) continue
220
+ const plugin = await import(entry.name)
221
+ const schema = plugin.Config ?? plugin.default?.Config
222
+ if (schema) assert.doesNotThrow(() => schema(entry.config ?? {}), entry.name)
223
+ if (plugin.resolveConfig) assert.doesNotThrow(() => plugin.resolveConfig(entry.config ?? {}), entry.name)
224
+ }
225
+ }
226
+ await validate(config.plugins)
227
+ let standardPath
228
+ try { standardPath = join(dirname(fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-web-app/package.json'))), 'presets', 'standard.patch.yml') }
229
+ catch (error) { if (error.code !== 'ERR_MODULE_NOT_FOUND') throw error }
230
+ if (standardPath && fs.existsSync(standardPath)) {
231
+ const stock = boot.composeEntries([boot.loadOverlayPatches('dsh', standardPath)]).find(entry => entry.config?.id === 'standard')
232
+ const actual = structuredClone(config.plugins)
233
+ actual.find(entry => entry.id === 'persona').config.prefix = stock.config.plugins.find(entry => entry.id === 'persona').config.prefix
234
+ assert.deepEqual(actual, stock.config.plugins, 'stock capability snapshot differs only in the persona prefix')
235
+ }
236
+ })
package/dev/run-all.sh CHANGED
@@ -10,6 +10,7 @@
10
10
  # curriculum-evidence 编排证据目录、引用与事件身份
11
11
  # strategy-check 比赛目标、VP 证据、复盘与重点选择
12
12
  # teaching 续课笔记、学生原话与进度保留
13
+ # persona 随包人设与静态提示词注入
13
14
  # parity-check 和线上那份的保真度对比
14
15
  #
15
16
  # 主验收跑的是**已安装副本**(不是源码)—— 那才是运行时真正加载的东西。
@@ -69,6 +70,7 @@ run "strategy" node --test dev/strategy-check.mjs
69
70
  run "catalog" node --test dev/catalog-check.mjs
70
71
  run "coach-contract" node --test dev/coaching-contract-check.mjs
71
72
  run "teaching" node --test dev/teaching-check.mjs
73
+ run "persona" node --test dev/persona-check.mjs
72
74
  run "grading" node --test dev/grading-check.mjs
73
75
  run "training" node --test dev/training-check.mjs
74
76
  run "VP-client" node dev/vp-client-check.mjs
package/dev.sh CHANGED
@@ -35,7 +35,7 @@ done
35
35
  # 从副本目录跑 `bash dev.sh` 走的是另一条路(`dsh plugin remove/add`,而那条
36
36
  # 路不可靠),和这份的行为不一样。换句话说,**同一个命令,从哪个目录按回车
37
37
  # 决定跑哪份脚本** —— 两份 dev.sh 就是两份真相,正是这份文件在防的那种坑。
38
- SYNC_FILES='index.js package.json verify.mjs verify-shape.mjs progress-cli.mjs build-node-meta.mjs cordis.patch.yml README.md lib/client.js lib/vp-client.js lib/workspace-migration.js lib/setup.js lib/curriculum.js lib/curriculum-evidence.js lib/curriculum-planning.js lib/strategy.js lib/teaching.js assets/knowledge/COMPETENCIES.yaml assets/knowledge/NODE_SCOPES.yaml assets/knowledge/CONTEST_PROFILES.yaml assets/knowledge/vp.html assets/knowledge/skilltree.html assets/rules/coach-rules.md dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs dev/teaching-check.mjs dev/grading-check.mjs dev/training-check.mjs coach/skilltree_template.html dev/run-all.sh docs/长期课程机制.md docs/比赛与教学依据.md docs/设计方针.md dev.sh'
38
+ SYNC_FILES='index.js package.json verify.mjs verify-shape.mjs progress-cli.mjs build-node-meta.mjs cordis.patch.yml README.md lib/client.js lib/vp-client.js lib/workspace-migration.js lib/setup.js lib/curriculum.js lib/curriculum-evidence.js lib/curriculum-planning.js lib/strategy.js lib/teaching.js assets/knowledge/COMPETENCIES.yaml assets/knowledge/NODE_SCOPES.yaml assets/knowledge/CONTEST_PROFILES.yaml assets/knowledge/vp.html assets/knowledge/skilltree.html assets/rules/coach-rules.md assets/persona/xiaojing-coach.md dev/persona-check.mjs dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs dev/teaching-check.mjs dev/grading-check.mjs dev/training-check.mjs coach/skilltree_template.html dev/run-all.sh docs/长期课程机制.md docs/比赛与教学依据.md docs/设计方针.md docs/china-contest-sources.md dev.sh'
39
39
  STALE=''
40
40
  for f in $SYNC_FILES; do
41
41
  for d in "${DSTS[@]}"; do
@@ -120,7 +120,7 @@ EVIDENCE_RC=${EVIDENCE_RC:-0}
120
120
 
121
121
  echo
122
122
  echo '── 策略、教学目录、真实工具与账号数据回归 ──'
123
- node --test "$SRC_BASH/dev/strategy-check.mjs" "$SRC_BASH/dev/catalog-check.mjs" "$SRC_BASH/dev/coaching-contract-check.mjs" "$SRC_BASH/dev/teaching-check.mjs" "$SRC_BASH/dev/grading-check.mjs" "$SRC_BASH/dev/training-check.mjs" "$SRC_BASH/dev/workspace-migration-check.mjs" "$SRC_BASH/dev/vp-client-check.mjs" "$SRC_BASH/dev/vp-sync-check.mjs" || CONTRACT_RC=$?
123
+ node --test "$SRC_BASH/dev/strategy-check.mjs" "$SRC_BASH/dev/catalog-check.mjs" "$SRC_BASH/dev/coaching-contract-check.mjs" "$SRC_BASH/dev/teaching-check.mjs" "$SRC_BASH/dev/persona-check.mjs" "$SRC_BASH/dev/grading-check.mjs" "$SRC_BASH/dev/training-check.mjs" "$SRC_BASH/dev/workspace-migration-check.mjs" "$SRC_BASH/dev/vp-client-check.mjs" "$SRC_BASH/dev/vp-sync-check.mjs" || CONTRACT_RC=$?
124
124
  CONTRACT_RC=${CONTRACT_RC:-0}
125
125
 
126
126
  LEDGER_AFTER=$(ledger_fp)