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/README.md +24 -4
- package/assets/knowledge/CONTEST_PROFILES.yaml +166 -97
- package/assets/persona/xiaojing-coach.md +75 -0
- package/assets/rules/coach-rules.md +2 -0
- package/cordis.patch.yml +150 -5
- package/dev/catalog-check.mjs +44 -9
- package/dev/persona-check.mjs +236 -0
- package/dev/run-all.sh +2 -0
- package/dev.sh +2 -2
- package/docs/china-contest-sources.md +423 -0
- package/docs//346/257/224/350/265/233/344/270/216/346/225/231/345/255/246/344/276/235/346/215/256.md +23 -6
- package/index.js +11 -5
- package/package.json +3 -3
package/cordis.patch.yml
CHANGED
|
@@ -1,10 +1,155 @@
|
|
|
1
1
|
# acmer-coach 组合层(bundle 形态)。
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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
|
package/dev/catalog-check.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
39
|
-
assert.equal(regional.coverage.reviewedSolutions,
|
|
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,
|
|
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,
|
|
54
|
-
assert.equal(xian.coverage.reviewedSolutions,
|
|
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,
|
|
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)
|