@godv61/dsh-task-engine 0.27.1 → 0.28.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/.acceptance.mjs CHANGED
@@ -42,14 +42,14 @@ check('选流程不产生绑定', Object.keys(resolveFlow('standard', { flow: 's
42
42
 
43
43
  console.log('')
44
44
  console.log('=== 3. 采用后完全可编辑、可删除 ===')
45
- for (const id of ['standard', 'agile', 'minimal']) {
46
- const adopted = adoptRecommendation(id)
47
- check(`${id} 采用后仍不绑定业务技能`, Object.keys(adopted.stage_bindings ?? {}).length === 0)
48
- const retained = adoptRecommendation(id, { flow: id, stage_bindings: {
49
- 开发: { skill_refs: [{ source: 'project', name: 'mine' }] },
50
- } })
51
- check(`${id} 采用推荐文本时保留用户技能`, retained.stage_bindings.开发.skill_refs[0].name === 'mine')
52
- if (id !== 'minimal') check(`${id} 推荐提交文本实际写入`, retained.commit.message_pattern !== '')
45
+ for (const id of ['standard', 'agile', 'minimal']) {
46
+ const adopted = adoptRecommendation(id)
47
+ check(`${id} 采用后仍不绑定业务技能`, Object.keys(adopted.stage_bindings ?? {}).length === 0)
48
+ const retained = adoptRecommendation(id, { flow: id, stage_bindings: {
49
+ 开发: { skill_refs: [{ source: 'project', name: 'mine' }] },
50
+ } })
51
+ check(`${id} 采用推荐文本时保留用户技能`, retained.stage_bindings.开发.skill_refs[0].name === 'mine')
52
+ if (id !== 'minimal') check(`${id} 推荐提交文本实际写入`, retained.commit.message_pattern !== '')
53
53
  const replaced = resolveFlow(id, { ...adopted, stage_bindings: {} }).config
54
54
  check(`${id} 清空后为空(不补回)`, Object.keys(replaced.stage_bindings ?? {}).length === 0)
55
55
  const mine = resolveFlow(id, {
@@ -106,13 +106,13 @@ for (const id of ['standard', 'agile', 'minimal']) {
106
106
  }
107
107
 
108
108
  console.log('')
109
- console.log('=== 7. defaults/eng.json 是空绑定配置示例 ===')
109
+ console.log('=== 7. defaults/eng.json 是空绑定配置示例 ===')
110
110
  const doc = JSON.parse(readFileSync('defaults/eng.json', 'utf8'))
111
111
  const merged = resolveFlow(doc.flow, doc)
112
112
  check('defaults/eng.json 可解析', merged.ok)
113
113
  if (merged.ok) {
114
114
  check('defaults/eng.json 无验证问题', validateWorkflow(merged.config).length === 0)
115
- check('defaults/eng.json 不含技能', Object.keys(merged.config.stage_bindings ?? {}).length === 0)
115
+ check('defaults/eng.json 不含技能', Object.keys(merged.config.stage_bindings ?? {}).length === 0)
116
116
  }
117
117
 
118
118
  console.log('')
@@ -153,4 +153,4 @@ check('三者范围互不相同', new Set([r.join(), s.join(), d.join()]).size =
153
153
 
154
154
  console.log('')
155
155
  console.log(failures === 0 ? '✅ 全部验收项通过' : `❌ ${failures} 项未通过`)
156
- process.exit(failures === 0 ? 0 : 1)
156
+ process.exit(failures === 0 ? 0 : 1)
@@ -320,18 +320,18 @@ test('batch3: the same skill carries identical rules wherever it is bound', () =
320
320
  test('batch3: a rule shared by two skills is one resource, referenced twice', () => {
321
321
  // Reuse must not duplicate the rule body: two skills referencing one rule point
322
322
  // at the same source-qualified reference.
323
- const sharedRule = { source: 'project', name: 'shared-rule' }
324
- const config = resolveFlow('standard', { flow: 'standard', stage_bindings: {
325
- '开发': { skills: [{ skill: { source: 'project', name: 'implement' }, rules: [sharedRule] }] },
326
- '代码审核': { skills: [{ skill: { source: 'project', name: 'review' }, rules: [sharedRule] }] },
327
- } }).config
328
- const reviewRules = config.stage_bindings['代码审核'].skills[0].rules.map(formatResourceRef)
329
- const implementRules = config.stage_bindings['开发'].skills[0].rules.map(formatResourceRef)
323
+ const sharedRule = { source: 'project', name: 'shared-rule' }
324
+ const config = resolveFlow('standard', { flow: 'standard', stage_bindings: {
325
+ '开发': { skills: [{ skill: { source: 'project', name: 'implement' }, rules: [sharedRule] }] },
326
+ '代码审核': { skills: [{ skill: { source: 'project', name: 'review' }, rules: [sharedRule] }] },
327
+ } }).config
328
+ const reviewRules = config.stage_bindings['代码审核'].skills[0].rules.map(formatResourceRef)
329
+ const implementRules = config.stage_bindings['开发'].skills[0].rules.map(formatResourceRef)
330
330
  const shared = reviewRules.filter(rule => implementRules.includes(rule))
331
331
  assert.ok(shared.length > 0,
332
332
  'coding-conventions and security-redlines apply to both implementing and reviewing code, so at least one rule must be shared by reference')
333
- assert.ok(shared.includes('project:shared-rule'),
334
- `the shared project rule is one reference; saw ${JSON.stringify(shared)}`)
333
+ assert.ok(shared.includes('project:shared-rule'),
334
+ `the shared project rule is one reference; saw ${JSON.stringify(shared)}`)
335
335
  })
336
336
 
337
337
  test('batch3: legacy stage-level rules stay in force and are named as unassigned', async () => {
@@ -339,17 +339,17 @@ test('batch3: legacy stage-level rules stay in force and are named as unassigned
339
339
  // invent an answer the config never had; dropping them would lose a constraint.
340
340
  const f = fixture('standard', '开发', {}, {})
341
341
  const state = f.state()
342
- state.flow.config.stage_bindings = { 开发: { skills: [], legacy_rules: ['shared-rule'] } }
343
- f.records.set(join(f.cwd, '.dsh/rules/shared-rule.md'), '# shared legacy rule')
342
+ state.flow.config.stage_bindings = { 开发: { skills: [], legacy_rules: ['shared-rule'] } }
343
+ f.records.set(join(f.cwd, '.dsh/rules/shared-rule.md'), '# shared legacy rule')
344
344
  f.records.set(join(f.cwd, '.dsh/task-LIVE-1.json'), JSON.stringify(state))
345
345
  const status = JSON.parse(await f.call({ operation: 'status' }))
346
346
  assert.ok(
347
- (status.unassigned_legacy_rules ?? []).includes('shared-rule'),
347
+ (status.unassigned_legacy_rules ?? []).includes('shared-rule'),
348
348
  `the legacy rule must be reported as unassigned; saw ${JSON.stringify(status.unassigned_legacy_rules)}`,
349
349
  )
350
350
  // Still enforced: it resolves and appears among the rules in force.
351
351
  assert.ok(
352
- (status.rules ?? []).some(rule => rule.name === 'shared-rule'),
352
+ (status.rules ?? []).some(rule => rule.name === 'shared-rule'),
353
353
  `a legacy rule must keep applying; saw ${JSON.stringify(status.rules)}`,
354
354
  )
355
355
  })
@@ -423,4 +423,4 @@ test('P2: an unknown review_depth is a configuration error, not a silent downgra
423
423
  const problems = validateWorkflow(config)
424
424
  assert.ok(problems.some(p => /review_depth/u.test(p)),
425
425
  `a typo must be reported; saw ${JSON.stringify(problems)}`)
426
- })
426
+ })
@@ -1,80 +1,80 @@
1
- import test from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { dirname, join, resolve, sep } from 'node:path'
6
- import Controller from './lib/controller.js'
7
- import { registerDevTask } from './lib/dev-task.js'
8
- import { loadedSkills, skillBlockers } from './lib/skill-audit.js'
9
- import { resolveFlow } from './lib/workflows.js'
10
- import { validateWorkflow } from './lib/engine.js'
11
-
12
- function fixture(t) {
13
- const root = mkdtempSync(join(tmpdir(), 'dsh-codex-project-'))
14
- assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
15
- t.after(() => rmSync(root, { recursive: true, force: true }))
16
- return root
17
- }
18
- function put(path, body) {
19
- mkdirSync(dirname(path), { recursive: true })
20
- writeFileSync(path, body)
21
- }
22
- function diskFs() {
23
- return {
24
- resolve: async (path, options) => resolve(options?.cwd ?? '', path),
25
- readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
26
- lstat: async (path, options) => { try { return { version: String(statSync(resolve(options?.cwd ?? '', path)).mtimeMs) } } catch { return undefined } },
27
- writeText: async (path, body) => put(path, body),
28
- listDir: async path => readdirSync(path, { withFileTypes: true }).map(entry => ({ name: entry.name })),
29
- }
30
- }
31
-
32
- test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
33
- const root = fixture(t)
34
- const body = '---\nname: same\ndescription: Codex method\n---\nCodex body v1\n'
35
- put(join(root, '.agents/skills/same/SKILL.md'), body)
36
- put(join(root, '.dsh/skills/same/SKILL.md'), '---\nname: same\ndescription: DSH method\n---\nDSH body\n')
37
- const receiver = { authorizedPath: async path => path }
38
- const catalog = await Controller.prototype.listSkills.call(receiver, root)
39
- assert.deepEqual(catalog.skills.filter(skill => skill.name === 'same').map(skill => skill.ref.source).sort(), ['codex-project', 'project'])
40
- const read = await Controller.prototype.readSkill.call(receiver, { level: 'codex-project', name: 'same', path: root })
41
- assert.equal(read.content, 'Codex body v1')
42
- const created = await Controller.prototype.writeSkill.call(receiver, { level: 'project', name: 'new-one', description: 'New DSH method', whenToUse: '', content: 'body', path: root })
43
- assert.equal(created.ok, true)
44
- assert.equal(readFileSync(join(root, '.dsh/skills/new-one/SKILL.md'), 'utf8').includes('body'), true)
45
- })
46
-
47
- test('a bound Codex Skill loads its attached DSH Rule and reads edits next time', async t => {
48
- const root = fixture(t)
49
- const skillFile = join(root, '.agents/skills/sample/SKILL.md')
50
- const ruleFile = join(root, '.dsh/rules/shared.md')
51
- put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v1\n')
52
- put(ruleFile, 'Rule v1')
53
- const ref = { source: 'codex-project', name: 'sample' }
54
- const rule = { source: 'project', name: 'shared' }
55
- const project = { flow: 'minimal', stage_bindings: { '开发': { skill_refs: [ref] } }, skill_profiles: { 'codex-project:sample': { rules: [rule], evidence: 'none' } } }
56
- put(join(root, '.dsh/eng.json'), JSON.stringify(project))
57
- assert.deepEqual(validateWorkflow(resolveFlow('minimal', project).config), [])
58
- const fs = diskFs(), tools = []
59
- registerDevTask({ fs, tools: { register(tool) { tools.push(tool) } }, get() { return undefined } })
60
- const execute = tools[0].execute
61
- const events = []
62
- const session = { id: 'test-session', header: { cwd: root }, snapshotEvents: () => events }
63
- const run = (args, callId = 'call') => execute(args, { agent: { session }, callId, signal: undefined })
64
- await run({ operation: 'create', task_id: 'T1', title: 'Test', branch: 'main' })
65
- const first = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-1')
66
- assert.match(first, /Skill v1/)
67
- assert.match(first, /Rule v1/)
68
- events.push({ type: 'tool/call', data: { name: 'dev_task', callId: 'load-1', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }) } })
69
- events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: 'load-1', isError: false }] } } })
70
- assert.equal(loadedSkills(session).get('T1#codex-project:sample'), 'load-1')
71
- const state = JSON.parse(readFileSync(join(root, '.dsh/task-T1.json'), 'utf8'))
72
- assert.deepEqual(skillBlockers(state, state.flow.config, session), [])
73
- assert.match(skillBlockers({ ...state, id: 'T2' }, state.flow.config, session).join('; '), /load_skill/,
74
- 'loading a Skill for one task must not satisfy another task with a different Rule profile')
75
- put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v2\n')
76
- put(ruleFile, 'Rule v2')
77
- const second = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-2')
78
- assert.match(second, /Skill v2/)
79
- assert.match(second, /Rule v2/)
80
- })
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { dirname, join, resolve, sep } from 'node:path'
6
+ import Controller from './lib/controller.js'
7
+ import { registerDevTask } from './lib/dev-task.js'
8
+ import { loadedSkills, skillBlockers } from './lib/skill-audit.js'
9
+ import { resolveFlow } from './lib/workflows.js'
10
+ import { validateWorkflow } from './lib/engine.js'
11
+
12
+ function fixture(t) {
13
+ const root = mkdtempSync(join(tmpdir(), 'dsh-codex-project-'))
14
+ assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
15
+ t.after(() => rmSync(root, { recursive: true, force: true }))
16
+ return root
17
+ }
18
+ function put(path, body) {
19
+ mkdirSync(dirname(path), { recursive: true })
20
+ writeFileSync(path, body)
21
+ }
22
+ function diskFs() {
23
+ return {
24
+ resolve: async (path, options) => resolve(options?.cwd ?? '', path),
25
+ readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
26
+ lstat: async (path, options) => { try { return { version: String(statSync(resolve(options?.cwd ?? '', path)).mtimeMs) } } catch { return undefined } },
27
+ writeText: async (path, body) => put(path, body),
28
+ listDir: async path => readdirSync(path, { withFileTypes: true }).map(entry => ({ name: entry.name })),
29
+ }
30
+ }
31
+
32
+ test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
33
+ const root = fixture(t)
34
+ const body = '---\nname: same\ndescription: Codex method\n---\nCodex body v1\n'
35
+ put(join(root, '.agents/skills/same/SKILL.md'), body)
36
+ put(join(root, '.dsh/skills/same/SKILL.md'), '---\nname: same\ndescription: DSH method\n---\nDSH body\n')
37
+ const receiver = { authorizedPath: async path => path }
38
+ const catalog = await Controller.prototype.listSkills.call(receiver, root)
39
+ assert.deepEqual(catalog.skills.filter(skill => skill.name === 'same').map(skill => skill.ref.source).sort(), ['codex-project', 'project'])
40
+ const read = await Controller.prototype.readSkill.call(receiver, { level: 'codex-project', name: 'same', path: root })
41
+ assert.equal(read.content, 'Codex body v1')
42
+ const created = await Controller.prototype.writeSkill.call(receiver, { level: 'project', name: 'new-one', description: 'New DSH method', whenToUse: '', content: 'body', path: root })
43
+ assert.equal(created.ok, true)
44
+ assert.equal(readFileSync(join(root, '.dsh/skills/new-one/SKILL.md'), 'utf8').includes('body'), true)
45
+ })
46
+
47
+ test('a bound Codex Skill loads its attached DSH Rule and reads edits next time', async t => {
48
+ const root = fixture(t)
49
+ const skillFile = join(root, '.agents/skills/sample/SKILL.md')
50
+ const ruleFile = join(root, '.dsh/rules/shared.md')
51
+ put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v1\n')
52
+ put(ruleFile, 'Rule v1')
53
+ const ref = { source: 'codex-project', name: 'sample' }
54
+ const rule = { source: 'project', name: 'shared' }
55
+ const project = { flow: 'minimal', stage_bindings: { '开发': { skill_refs: [ref] } }, skill_profiles: { 'codex-project:sample': { rules: [rule], evidence: 'none' } } }
56
+ put(join(root, '.dsh/eng.json'), JSON.stringify(project))
57
+ assert.deepEqual(validateWorkflow(resolveFlow('minimal', project).config), [])
58
+ const fs = diskFs(), tools = []
59
+ registerDevTask({ fs, tools: { register(tool) { tools.push(tool) } }, get() { return undefined } })
60
+ const execute = tools[0].execute
61
+ const events = []
62
+ const session = { id: 'test-session', header: { cwd: root }, snapshotEvents: () => events }
63
+ const run = (args, callId = 'call') => execute(args, { agent: { session }, callId, signal: undefined })
64
+ await run({ operation: 'create', task_id: 'T1', title: 'Test', branch: 'main' })
65
+ const first = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-1')
66
+ assert.match(first, /Skill v1/)
67
+ assert.match(first, /Rule v1/)
68
+ events.push({ type: 'tool/call', data: { name: 'dev_task', callId: 'load-1', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }) } })
69
+ events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: 'load-1', isError: false }] } } })
70
+ assert.equal(loadedSkills(session).get('T1#codex-project:sample'), 'load-1')
71
+ const state = JSON.parse(readFileSync(join(root, '.dsh/task-T1.json'), 'utf8'))
72
+ assert.deepEqual(skillBlockers(state, state.flow.config, session), [])
73
+ assert.match(skillBlockers({ ...state, id: 'T2' }, state.flow.config, session).join('; '), /load_skill/,
74
+ 'loading a Skill for one task must not satisfy another task with a different Rule profile')
75
+ put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v2\n')
76
+ put(ruleFile, 'Rule v2')
77
+ const second = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-2')
78
+ assert.match(second, /Skill v2/)
79
+ assert.match(second, /Rule v2/)
80
+ })
package/.enforce-test.mjs CHANGED
@@ -15,13 +15,13 @@ import { completionBlockers, terminalRequirements, resourceBlockers } from './li
15
15
  async function project() {
16
16
  const cwd = resolve('enforce-project')
17
17
  const adopted = adoptRecommendation('standard')
18
- const skills = [{ skill: { source: 'project', name: 'my-implementation' },
19
- rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
18
+ const skills = [{ skill: { source: 'project', name: 'my-implementation' },
19
+ rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
20
20
  const stage_bindings = { ...adopted.stage_bindings, '开发': { skills } }
21
21
  const records = new Map([
22
22
  [join(cwd, '.dsh/eng.json'), JSON.stringify({ ...adopted, stage_bindings })],
23
- [join(cwd, '.dsh/rules/mine.md'), 'RULE'],
24
- [join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '---\nname: my-implementation\n---\nImplement.'],
23
+ [join(cwd, '.dsh/rules/mine.md'), 'RULE'],
24
+ [join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '---\nname: my-implementation\n---\nImplement.'],
25
25
  [join(cwd, 'a.js'), 'source'],
26
26
  ])
27
27
  const events = []
@@ -85,13 +85,13 @@ test('enforce: a task WITHOUT a snapshot cannot advance while a bound rule is un
85
85
  )
86
86
  })
87
87
 
88
- test('enforce: a task WITH a snapshot blocks when its live source rule is deleted', async () => {
89
- const f = await project()
90
- await satistfyDevelopment(f)
91
- f.records.delete(f.rule())
92
- await assert.rejects(() => f.call({ operation: 'advance', target_stage: '交付' }), /resolves nowhere/u)
93
- assert.equal(f.current().stage, '开发')
94
- })
88
+ test('enforce: a task WITH a snapshot blocks when its live source rule is deleted', async () => {
89
+ const f = await project()
90
+ await satistfyDevelopment(f)
91
+ f.records.delete(f.rule())
92
+ await assert.rejects(() => f.call({ operation: 'advance', target_stage: '交付' }), /resolves nowhere/u)
93
+ assert.equal(f.current().stage, '开发')
94
+ })
95
95
 
96
96
  test('enforce: completion requires the review and verification the flow declared', async () => {
97
97
  // Arriving at the last stage used to be enough. The agile flow declares a review
@@ -131,4 +131,4 @@ test('enforce: the resource blocker names the stage, the reference and the fix',
131
131
  assert.match(blockers[0], /Restore the file|remove the binding/u,
132
132
  'a blocker must say what to do, not only what is wrong')
133
133
  assert.deepEqual(resourceBlockers([], '开发'), [], 'a resolvable stage has no blocker')
134
- })
134
+ })
@@ -24,7 +24,7 @@ function sessionWith(names) {
24
24
  /** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
25
25
  function withEvidence(evidence) {
26
26
  const adopted = adoptRecommendation('standard')
27
- const skills = [{ skill: { source: 'project', name: 'doc-skill' }, rules: [], ...(evidence !== undefined ? { evidence } : {}) }]
27
+ const skills = [{ skill: { source: 'project', name: 'doc-skill' }, rules: [], ...(evidence !== undefined ? { evidence } : {}) }]
28
28
  return resolveFlow('standard', { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }).config
29
29
  }
30
30
 
@@ -42,8 +42,8 @@ function state(extra = {}) {
42
42
  }
43
43
 
44
44
  test('evidence: needsSkillReceipt honours a declared non-command kind', () => {
45
- // No name-based exemption remains; a declared kind overrides the default.
46
- assert.equal(needsSkillReceipt('code-implement'), true, 'skill names do not exempt command evidence')
45
+ // No name-based exemption remains; a declared kind overrides the default.
46
+ assert.equal(needsSkillReceipt('code-implement'), true, 'skill names do not exempt command evidence')
47
47
  assert.equal(needsSkillReceipt('doc-skill'), true, 'an undeclared extra skill defaults to a command receipt')
48
48
  assert.equal(needsSkillReceipt('doc-skill', 'command'), true)
49
49
  assert.equal(needsSkillReceipt('doc-skill', 'artifact'), false)
@@ -56,8 +56,8 @@ test('evidence: a document-producing skill is satisfied by a recorded artifact',
56
56
  // The blocker is the point: without an artifact it blocks, and with one it does
57
57
  // not — so the declared kind is genuinely enforced rather than merely accepted.
58
58
  //
59
- // Mounted on 设计, which the adopted setup declares an artifact for. Missing
60
- // declarations and incomplete fields must both remain blockers.
59
+ // Mounted on 设计, which the adopted setup declares an artifact for. Missing
60
+ // declarations and incomplete fields must both remain blockers.
61
61
  const adopted = adoptRecommendation('standard')
62
62
  const config = resolveFlow('standard', {
63
63
  ...adopted,
@@ -78,7 +78,7 @@ test('evidence: a document-producing skill is satisfied by a recorded artifact',
78
78
  // Record what the stage declares and the blocker clears.
79
79
  const declared = config.artifacts.find(artifact => artifact.stage === '设计')
80
80
  assert.ok(declared !== undefined, 'the standard flow declares an artifact at 设计')
81
- const cleared = skillBlockers(atDesign({ artifacts: { [declared.id]: Object.fromEntries(declared.fields.map(field => [field, 'checked'])) } }), config, session)
81
+ const cleared = skillBlockers(atDesign({ artifacts: { [declared.id]: Object.fromEntries(declared.fields.map(field => [field, 'checked'])) } }), config, session)
82
82
  assert.deepEqual(cleared.filter(b => b.includes('doc-skill')), [],
83
83
  'a recorded artifact satisfies artifact evidence')
84
84
  })
@@ -98,7 +98,7 @@ test('evidence: manual evidence needs an explicit statement, none needs nothing'
98
98
  const session = sessionWith(['code-implement', 'doc-skill'])
99
99
  assert.ok(skillBlockers(state(), manual, session).some(b => b.includes('manual evidence')),
100
100
  'manual evidence is absent until something is recorded')
101
- const recorded = state({ skill_results: { '开发': { 'doc-skill': { evidence: ['reviewed by the lead'], approved: true } } } })
101
+ const recorded = state({ skill_results: { '开发': { 'doc-skill': { evidence: ['reviewed by the lead'], approved: true } } } })
102
102
  assert.deepEqual(skillBlockers(recorded, manual, session).filter(b => b.includes('doc-skill')), [],
103
103
  'a recorded statement satisfies manual evidence')
104
104
 
@@ -115,4 +115,4 @@ test('evidence: an undeclared extra skill still owes a command receipt', () => {
115
115
  const blockers = skillBlockers(state(), config, session)
116
116
  assert.ok(blockers.some(b => b.includes('validation command')),
117
117
  `an undeclared skill must still owe a command receipt; got ${JSON.stringify(blockers)}`)
118
- })
118
+ })
package/.filter-test.mjs CHANGED
@@ -83,9 +83,9 @@ test('filter: only-selected combined with a query still keeps what is in force',
83
83
  assert.ok(!shown.some(e => e.name === 'code-implement'), 'an unbound non-matching skill is dropped')
84
84
  })
85
85
 
86
- test('filter: a fresh project has no prebound business skill', () => {
87
- const adopted = adoptRecommendation('standard')
88
- const resolved = resolveFlow('standard', adopted).config
89
- const binding = bindingsForStage('开发', resolved)
90
- assert.equal(binding, undefined)
91
- })
86
+ test('filter: a fresh project has no prebound business skill', () => {
87
+ const adopted = adoptRecommendation('standard')
88
+ const resolved = resolveFlow('standard', adopted).config
89
+ const binding = bindingsForStage('开发', resolved)
90
+ assert.equal(binding, undefined)
91
+ })
package/.freeze-test.mjs CHANGED
@@ -13,23 +13,23 @@ import { registerDevTask } from './lib/dev-task.js'
13
13
  import { hashText } from './lib/snapshot.js'
14
14
 
15
15
  /** Create one task through the real tool and return its frozen snapshot. */
16
- async function created(flow) {
17
- const cwd = resolve('freeze-project')
18
- const stage = { standard: '需求评审', agile: '需求', minimal: '开发' }[flow]
19
- const shared = { source: 'project', name: 'shared-rule' }
20
- const adopted = adoptRecommendation(flow)
21
- const project = { ...adopted, stage_bindings: { [stage]: { skills: [
22
- { skill: { source: 'project', name: 'first-skill' }, rules: [shared], evidence: 'none' },
23
- { skill: { source: 'project', name: 'second-skill' }, rules: [shared], evidence: 'none' },
24
- ] } } }
25
- const records = new Map([
16
+ async function created(flow) {
17
+ const cwd = resolve('freeze-project')
18
+ const stage = { standard: '需求评审', agile: '需求', minimal: '开发' }[flow]
19
+ const shared = { source: 'project', name: 'shared-rule' }
20
+ const adopted = adoptRecommendation(flow)
21
+ const project = { ...adopted, stage_bindings: { [stage]: { skills: [
22
+ { skill: { source: 'project', name: 'first-skill' }, rules: [shared], evidence: 'none' },
23
+ { skill: { source: 'project', name: 'second-skill' }, rules: [shared], evidence: 'none' },
24
+ ] } } }
25
+ const records = new Map([
26
26
  // A project that adopted the shipped recommendation, which is what gives it
27
27
  // skills and rules to freeze. `{flow}` alone adopts nothing and therefore has
28
28
  // nothing to freeze — a legitimate state, but not the one under test.
29
- [join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
30
- [join(cwd, '.dsh/skills/first-skill/SKILL.md'), '# First skill'],
31
- [join(cwd, '.dsh/skills/second-skill/SKILL.md'), '# Second skill'],
32
- [join(cwd, '.dsh/rules/shared-rule.md'), '# Shared rule'],
29
+ [join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
30
+ [join(cwd, '.dsh/skills/first-skill/SKILL.md'), '# First skill'],
31
+ [join(cwd, '.dsh/skills/second-skill/SKILL.md'), '# Second skill'],
32
+ [join(cwd, '.dsh/rules/shared-rule.md'), '# Shared rule'],
33
33
  [join(cwd, 'a.js'), 'source'],
34
34
  ])
35
35
  const events = []
@@ -85,13 +85,13 @@ test('freeze: a frozen body carries its content and a hash of that content', asy
85
85
  })
86
86
 
87
87
  test('freeze: shared resources are stored once, not duplicated per reference', async () => {
88
- // One project rule is referenced by two skills; the body is stored once.
88
+ // One project rule is referenced by two skills; the body is stored once.
89
89
  const state = await created('standard')
90
90
  const resources = state.flow.resources ?? []
91
91
  const hashes = resources.map(r => r.hash)
92
92
  assert.equal(new Set(hashes).size, hashes.length,
93
93
  'identical bodies must not be stored more than once')
94
- assert.ok(resources.some(r => r.ref.name === 'shared-rule'),
94
+ assert.ok(resources.some(r => r.ref.name === 'shared-rule'),
95
95
  'the shared rule must be present')
96
96
  })
97
97
 
@@ -113,13 +113,13 @@ test('freeze: every preset freezes a complete, non-empty set', async () => {
113
113
  async function atDevelopmentWithProjectRule() {
114
114
  const cwd = resolve('freeze-live')
115
115
  const adopted = adoptRecommendation('standard')
116
- const skills = [{ skill: { source: 'project', name: 'my-implementation' },
117
- rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
116
+ const skills = [{ skill: { source: 'project', name: 'my-implementation' },
117
+ rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
118
118
  const project = { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }
119
119
  const records = new Map([
120
120
  [join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
121
- [join(cwd, '.dsh/rules/mine.md'), 'ORIGINAL RULE'],
122
- [join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '# Implementation skill'],
121
+ [join(cwd, '.dsh/rules/mine.md'), 'ORIGINAL RULE'],
122
+ [join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '# Implementation skill'],
123
123
  [join(cwd, 'a.js'), 'source'],
124
124
  ])
125
125
  let execute
@@ -159,29 +159,29 @@ async function atDevelopmentWithProjectRule() {
159
159
  }
160
160
  }
161
161
 
162
- test('live: a running task reads the latest rule body at the same reference', async () => {
163
- const f = await atDevelopmentWithProjectRule()
164
- const before = await f.disclosed()
165
- assert.equal(before.mine?.content, 'ORIGINAL RULE')
166
-
167
- f.records.set(f.rule(), 'CHANGED RULE')
168
- const after = await f.disclosed()
169
- assert.equal(after.mine?.content, 'CHANGED RULE',
170
- 'the next interaction must disclose the edited body')
171
- assert.ok((after.status.stale_source_rules ?? []).some(r => r.includes('edited')),
172
- 'the difference from the creation-time audit copy must be visible')
173
- assert.match(after.status.resource_update_notice, /current source body is in force/u)
174
- })
175
-
176
- test('live: deleting a source rule makes the binding unresolved', async () => {
177
- const f = await atDevelopmentWithProjectRule()
178
- f.records.delete(f.rule())
179
- const { status, mine } = await f.disclosed()
180
- assert.equal(mine, undefined)
181
- assert.ok(status.missing_rules.includes('project:mine'))
182
- assert.ok((status.stale_source_rules ?? []).some(r => r.includes('deleted')),
183
- 'the deletion is also reported against the creation-time audit copy')
184
- })
162
+ test('live: a running task reads the latest rule body at the same reference', async () => {
163
+ const f = await atDevelopmentWithProjectRule()
164
+ const before = await f.disclosed()
165
+ assert.equal(before.mine?.content, 'ORIGINAL RULE')
166
+
167
+ f.records.set(f.rule(), 'CHANGED RULE')
168
+ const after = await f.disclosed()
169
+ assert.equal(after.mine?.content, 'CHANGED RULE',
170
+ 'the next interaction must disclose the edited body')
171
+ assert.ok((after.status.stale_source_rules ?? []).some(r => r.includes('edited')),
172
+ 'the difference from the creation-time audit copy must be visible')
173
+ assert.match(after.status.resource_update_notice, /current source body is in force/u)
174
+ })
175
+
176
+ test('live: deleting a source rule makes the binding unresolved', async () => {
177
+ const f = await atDevelopmentWithProjectRule()
178
+ f.records.delete(f.rule())
179
+ const { status, mine } = await f.disclosed()
180
+ assert.equal(mine, undefined)
181
+ assert.ok(status.missing_rules.includes('project:mine'))
182
+ assert.ok((status.stale_source_rules ?? []).some(r => r.includes('deleted')),
183
+ 'the deletion is also reported against the creation-time audit copy')
184
+ })
185
185
 
186
186
  test('freeze: an unfrozen task falls back to live resolution and reports a missing file', async () => {
187
187
  // Tasks created before freezing have no snapshot. They must keep working, and a
@@ -200,4 +200,4 @@ test('freeze: an unfrozen task falls back to live resolution and reports a missi
200
200
  assert.equal(mine, undefined, 'with no snapshot and no file, the rule cannot resolve')
201
201
  assert.ok((status.missing_rules ?? []).includes('project:mine'),
202
202
  'it is reported as missing so the gate can block on it')
203
- })
203
+ })
package/.p0-test.mjs CHANGED
@@ -151,21 +151,21 @@ const emptied = resolveFlow('standard', { flow: 'standard', stage_bindings: { '
151
151
  assert(emptied.ok && (emptied.config.stage_bindings['开发'].skills ?? []).length === 0,
152
152
  'a project that binds nothing to a stage keeps it empty; nothing is merged back in')
153
153
 
154
- const adopted = adoptedFlow('standard')
155
- assert(Object.keys(adopted.stage_bindings ?? {}).length === 0,
156
- 'adopting the recommendation does not supply skills')
154
+ const adopted = adoptedFlow('standard')
155
+ assert(Object.keys(adopted.stage_bindings ?? {}).length === 0,
156
+ 'adopting the recommendation does not supply skills')
157
157
 
158
- // And a user who gives a project skill their own rule list gets that list — the
158
+ // And a user who gives a project skill their own rule list gets that list — the
159
159
  // defect this replaces was that the entry was skipped as a duplicate reference.
160
160
  const ownRules = resolveFlow('standard', {
161
161
  flow: 'standard',
162
162
  stage_bindings: {
163
- '开发': { skills: [{ skill: { source: 'project', name: 'my-implementation' }, rules: [{ source: 'project', name: 'only-mine' }] }] },
163
+ '开发': { skills: [{ skill: { source: 'project', name: 'my-implementation' }, rules: [{ source: 'project', name: 'only-mine' }] }] },
164
164
  },
165
165
  }).config
166
- const implementBinding = ownRules.stage_bindings['开发'].skills.find(entry => entry.skill.name === 'my-implementation')
166
+ const implementBinding = ownRules.stage_bindings['开发'].skills.find(entry => entry.skill.name === 'my-implementation')
167
167
  assert(implementBinding.rules.length === 1 && implementBinding.rules[0].name === 'only-mine',
168
- 'a project rule list for a project skill is used as given, not discarded as a duplicate')
168
+ 'a project rule list for a project skill is used as given, not discarded as a duplicate')
169
169
 
170
170
  // ── 4. newTask freezes the snapshot ────────────────────────────────────────
171
171
  const snapshot = { flow: 'standard', version: 1, config: adoptedFlow('standard') }
@@ -267,20 +267,20 @@ await assertThrows(
267
267
  // is no chain to walk and no shadowing to defend against: the same name in two
268
268
  // layers is two distinct resources, and the binding says which one it means.
269
269
  {
270
- const fs = makeFs({
270
+ const fs = makeFs({
271
271
  // A project file with the same NAME but a different body. Under the old
272
272
  // bare-name lookup this would have replaced the bundled rule silently.
273
273
  '.dsh/eng.json': JSON.stringify({
274
274
  flow: 'standard',
275
275
  stage_bindings: {
276
276
  '需求评审': { skills: [{
277
- skill: { source: 'project', name: 'requirement-analysis' },
277
+ skill: { source: 'project', name: 'requirement-analysis' },
278
278
  rules: [{ source: 'project', name: 'security-redlines' }],
279
279
  }] },
280
280
  },
281
281
  }),
282
- '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
283
- '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
282
+ '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
283
+ '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
284
284
  })
285
285
  const exe = await registered(fs)
286
286
  await exe({ operation: 'create', task_id: 'T9', title: 'x', branch: 'main' }, EXEC)
@@ -291,24 +291,24 @@ await assertThrows(
291
291
  assert(sec.content.includes('前端校验即可'), 'the PROJECT body is what gets disclosed, because the reference names the project layer')
292
292
  }
293
293
 
294
- // ── 11b. a deleted bundled rule is reported as missing ───────────────────────
294
+ // ── 11b. a deleted bundled rule is reported as missing ───────────────────────
295
295
  {
296
296
  const fs = makeFs({
297
297
  '.dsh/eng.json': JSON.stringify({
298
298
  flow: 'standard',
299
299
  stage_bindings: {
300
300
  '需求评审': { skills: [{
301
- skill: { source: 'project', name: 'requirement-analysis' },
301
+ skill: { source: 'project', name: 'requirement-analysis' },
302
302
  rules: [{ source: 'bundled', name: 'security-redlines' }],
303
303
  }] },
304
304
  },
305
305
  }),
306
- '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
307
- '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
306
+ '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
307
+ '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
308
308
  })
309
309
  const exe = await registered(fs)
310
- await assertThrows(() => exe({ operation: 'create', task_id: 'T10', title: 'x', branch: 'main' }, EXEC),
311
- 'unreadable', 'a reference to a removed bundled rule fails closed')
310
+ await assertThrows(() => exe({ operation: 'create', task_id: 'T10', title: 'x', branch: 'main' }, EXEC),
311
+ 'unreadable', 'a reference to a removed bundled rule fails closed')
312
312
  }
313
313
 
314
314
  // ── 12. session cwd drives every relative path (init regression) ──────────
@@ -599,10 +599,10 @@ function memProbe(files) {
599
599
  const exe = await registered(fs)
600
600
  await exe({ operation: 'create', task_id: 'FP-1', title: 'x', branch: 'main' }, EXEC)
601
601
  const status = JSON.parse(await exe({ operation: 'status', task_id: 'FP-1' }, EXEC))
602
- assert(status.bindings_drift === undefined, 'fresh task has no bundled-rule drift marker')
603
- const record = JSON.parse(fs._files.get('.dsh/task-FP-1.json'))
604
- assert(record.bindings_fingerprint === undefined,
605
- 'create does not record a fingerprint for removed bundled rules')
602
+ assert(status.bindings_drift === undefined, 'fresh task has no bundled-rule drift marker')
603
+ const record = JSON.parse(fs._files.get('.dsh/task-FP-1.json'))
604
+ assert(record.bindings_fingerprint === undefined,
605
+ 'create does not record a fingerprint for removed bundled rules')
606
606
  }
607
607
 
608
608
  // ── 26. 0.22-C: multi-language verify command chain + parallel tasks ────────
package/.preset-test.mjs CHANGED
@@ -158,4 +158,14 @@ test('the rewritten persona row keeps valid indentation for both shapes', () =>
158
158
  const legacy = applyPersona(PERSONA_FIXTURE_LEGACY, 'ENGINEER').text
159
159
  assert.match(legacy, /- id: persona\n {2}name: '@deepseek-ai\/dsh-persona'\n {2}config:\n {4}text: [|>]-\n {6}ENGINEER\n/u,
160
160
  'the legacy row is indented correctly')
161
- })
161
+ })
162
+
163
+ test('DSH 0.2 bundle declares the engineering preset', () => {
164
+ const patch = readFileSync(resolve('cordis.patch.yml'), 'utf8')
165
+ assert.match(patch, /id: preset-eng/u)
166
+ assert.match(patch, /name: '@deepseek-ai\/dsh-agent-preset'/u)
167
+ assert.match(patch, /id: eng/u)
168
+ assert.match(patch, /engineering-delivery coding agent/u)
169
+ assert.match(patch, /id: task-engine-agent/u)
170
+ assert.match(patch, /name: '@godv61\/dsh-task-engine\/agent'/u)
171
+ })