@godv61/dsh-task-engine 0.27.0 → 0.27.1
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 +11 -6
- package/.assessment-batch1.mjs +14 -11
- package/.codex-project-test.mjs +80 -0
- package/.enforce-test.mjs +4 -5
- package/.evidence-test.mjs +3 -3
- package/.filter-test.mjs +6 -8
- package/.freeze-test.mjs +20 -11
- package/.p0-test.mjs +21 -24
- package/.roundtrip-test.mjs +16 -28
- package/.workflow-test.mjs +6 -8
- package/README.md +8 -6
- package/defaults/eng.json +1 -97
- package/docs/CHANGELOG.md +7 -0
- package/docs/configuration.md +12 -13
- package/docs/faq.md +1 -1
- package/docs/manual.html +16 -34
- package/docs/resource-install.md +5 -3
- package/hooks/commit-msg +4 -33
- package/lib/client.js +94 -108
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +4 -3
- package/lib/controller.js +12 -6
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +33 -27
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +2 -3
- package/lib/engine.js +2 -2
- package/lib/engine.js.map +1 -1
- package/lib/shipped-skills.d.ts +2 -4
- package/lib/shipped-skills.js +2 -4
- package/lib/shipped-skills.js.map +1 -1
- package/lib/skill-audit.d.ts +4 -4
- package/lib/skill-audit.js +19 -14
- package/lib/skill-audit.js.map +1 -1
- package/lib/workflows.d.ts +3 -4
- package/lib/workflows.js +2 -41
- package/lib/workflows.js.map +1 -1
- package/package.json +4 -4
- package/preset/agent.cordis.yml +1 -1
- package/preset/persona.md +1 -1
- package/scripts/verify-package.mjs +3 -1
- package/skills/eng-delivery/SKILL.md +4 -4
- package/rules/coding-conventions.md +0 -7
- package/rules/commit-conventions.md +0 -6
- package/rules/security-redlines.md +0 -6
- package/skills/code-commit/SKILL.md +0 -13
- package/skills/code-implement/SKILL.md +0 -24
- package/skills/code-review/SKILL.md +0 -13
- package/skills/code-verify/SKILL.md +0 -20
- package/skills/requirement-analysis/SKILL.md +0 -16
- package/skills/solution-design/SKILL.md +0 -19
package/.acceptance.mjs
CHANGED
|
@@ -42,9 +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}
|
|
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 !== '')
|
|
48
53
|
const replaced = resolveFlow(id, { ...adopted, stage_bindings: {} }).config
|
|
49
54
|
check(`${id} 清空后为空(不补回)`, Object.keys(replaced.stage_bindings ?? {}).length === 0)
|
|
50
55
|
const mine = resolveFlow(id, {
|
|
@@ -101,13 +106,13 @@ for (const id of ['standard', 'agile', 'minimal']) {
|
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
console.log('')
|
|
104
|
-
console.log('=== 7. defaults/eng.json
|
|
109
|
+
console.log('=== 7. defaults/eng.json 是空绑定配置示例 ===')
|
|
105
110
|
const doc = JSON.parse(readFileSync('defaults/eng.json', 'utf8'))
|
|
106
111
|
const merged = resolveFlow(doc.flow, doc)
|
|
107
112
|
check('defaults/eng.json 可解析', merged.ok)
|
|
108
113
|
if (merged.ok) {
|
|
109
114
|
check('defaults/eng.json 无验证问题', validateWorkflow(merged.config).length === 0)
|
|
110
|
-
check('defaults/eng.json
|
|
115
|
+
check('defaults/eng.json 不含技能', Object.keys(merged.config.stage_bindings ?? {}).length === 0)
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
console.log('')
|
|
@@ -148,4 +153,4 @@ check('三者范围互不相同', new Set([r.join(), s.join(), d.join()]).size =
|
|
|
148
153
|
|
|
149
154
|
console.log('')
|
|
150
155
|
console.log(failures === 0 ? '✅ 全部验收项通过' : `❌ ${failures} 项未通过`)
|
|
151
|
-
process.exit(failures === 0 ? 0 : 1)
|
|
156
|
+
process.exit(failures === 0 ? 0 : 1)
|
package/.assessment-batch1.mjs
CHANGED
|
@@ -320,16 +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
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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)
|
|
328
330
|
const shared = reviewRules.filter(rule => implementRules.includes(rule))
|
|
329
331
|
assert.ok(shared.length > 0,
|
|
330
332
|
'coding-conventions and security-redlines apply to both implementing and reviewing code, so at least one rule must be shared by reference')
|
|
331
|
-
assert.ok(shared.includes('
|
|
332
|
-
`
|
|
333
|
+
assert.ok(shared.includes('project:shared-rule'),
|
|
334
|
+
`the shared project rule is one reference; saw ${JSON.stringify(shared)}`)
|
|
333
335
|
})
|
|
334
336
|
|
|
335
337
|
test('batch3: legacy stage-level rules stay in force and are named as unassigned', async () => {
|
|
@@ -337,16 +339,17 @@ test('batch3: legacy stage-level rules stay in force and are named as unassigned
|
|
|
337
339
|
// invent an answer the config never had; dropping them would lose a constraint.
|
|
338
340
|
const f = fixture('standard', '开发', {}, {})
|
|
339
341
|
const state = f.state()
|
|
340
|
-
state.flow.config.stage_bindings = { 开发: { skills: [], legacy_rules: ['
|
|
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')
|
|
341
344
|
f.records.set(join(f.cwd, '.dsh/task-LIVE-1.json'), JSON.stringify(state))
|
|
342
345
|
const status = JSON.parse(await f.call({ operation: 'status' }))
|
|
343
346
|
assert.ok(
|
|
344
|
-
(status.unassigned_legacy_rules ?? []).includes('
|
|
347
|
+
(status.unassigned_legacy_rules ?? []).includes('shared-rule'),
|
|
345
348
|
`the legacy rule must be reported as unassigned; saw ${JSON.stringify(status.unassigned_legacy_rules)}`,
|
|
346
349
|
)
|
|
347
350
|
// Still enforced: it resolves and appears among the rules in force.
|
|
348
351
|
assert.ok(
|
|
349
|
-
(status.rules ?? []).some(rule => rule.name === '
|
|
352
|
+
(status.rules ?? []).some(rule => rule.name === 'shared-rule'),
|
|
350
353
|
`a legacy rule must keep applying; saw ${JSON.stringify(status.rules)}`,
|
|
351
354
|
)
|
|
352
355
|
})
|
|
@@ -420,4 +423,4 @@ test('P2: an unknown review_depth is a configuration error, not a silent downgra
|
|
|
420
423
|
const problems = validateWorkflow(config)
|
|
421
424
|
assert.ok(problems.some(p => /review_depth/u.test(p)),
|
|
422
425
|
`a typo must be reported; saw ${JSON.stringify(problems)}`)
|
|
423
|
-
})
|
|
426
|
+
})
|
|
@@ -0,0 +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
|
+
})
|
package/.enforce-test.mjs
CHANGED
|
@@ -15,14 +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 =
|
|
19
|
-
|
|
20
|
-
rules: [...entry.rules, { source: 'project', name: 'mine' }],
|
|
21
|
-
}))
|
|
18
|
+
const skills = [{ skill: { source: 'project', name: 'my-implementation' },
|
|
19
|
+
rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
|
|
22
20
|
const stage_bindings = { ...adopted.stage_bindings, '开发': { skills } }
|
|
23
21
|
const records = new Map([
|
|
24
22
|
[join(cwd, '.dsh/eng.json'), JSON.stringify({ ...adopted, stage_bindings })],
|
|
25
|
-
[join(cwd, '.dsh/rules/mine.md'), 'RULE'],
|
|
23
|
+
[join(cwd, '.dsh/rules/mine.md'), 'RULE'],
|
|
24
|
+
[join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '---\nname: my-implementation\n---\nImplement.'],
|
|
26
25
|
[join(cwd, 'a.js'), 'source'],
|
|
27
26
|
])
|
|
28
27
|
const events = []
|
package/.evidence-test.mjs
CHANGED
|
@@ -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 = [
|
|
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
|
-
//
|
|
46
|
-
assert.equal(needsSkillReceipt('code-implement'),
|
|
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)
|
package/.filter-test.mjs
CHANGED
|
@@ -83,11 +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:
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
assert.ok((binding.skills ?? []).length > 0)
|
|
93
|
-
})
|
|
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,13 +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
|
|
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([
|
|
19
26
|
// A project that adopted the shipped recommendation, which is what gives it
|
|
20
27
|
// skills and rules to freeze. `{flow}` alone adopts nothing and therefore has
|
|
21
28
|
// nothing to freeze — a legitimate state, but not the one under test.
|
|
22
|
-
[join(cwd, '.dsh/eng.json'), JSON.stringify(
|
|
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'],
|
|
23
33
|
[join(cwd, 'a.js'), 'source'],
|
|
24
34
|
])
|
|
25
35
|
const events = []
|
|
@@ -75,13 +85,13 @@ test('freeze: a frozen body carries its content and a hash of that content', asy
|
|
|
75
85
|
})
|
|
76
86
|
|
|
77
87
|
test('freeze: shared resources are stored once, not duplicated per reference', async () => {
|
|
78
|
-
//
|
|
88
|
+
// One project rule is referenced by two skills; the body is stored once.
|
|
79
89
|
const state = await created('standard')
|
|
80
90
|
const resources = state.flow.resources ?? []
|
|
81
91
|
const hashes = resources.map(r => r.hash)
|
|
82
92
|
assert.equal(new Set(hashes).size, hashes.length,
|
|
83
93
|
'identical bodies must not be stored more than once')
|
|
84
|
-
assert.ok(resources.some(r => r.ref.name === '
|
|
94
|
+
assert.ok(resources.some(r => r.ref.name === 'shared-rule'),
|
|
85
95
|
'the shared rule must be present')
|
|
86
96
|
})
|
|
87
97
|
|
|
@@ -103,14 +113,13 @@ test('freeze: every preset freezes a complete, non-empty set', async () => {
|
|
|
103
113
|
async function atDevelopmentWithProjectRule() {
|
|
104
114
|
const cwd = resolve('freeze-live')
|
|
105
115
|
const adopted = adoptRecommendation('standard')
|
|
106
|
-
const skills =
|
|
107
|
-
|
|
108
|
-
rules: [...entry.rules, { source: 'project', name: 'mine' }],
|
|
109
|
-
}))
|
|
116
|
+
const skills = [{ skill: { source: 'project', name: 'my-implementation' },
|
|
117
|
+
rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
|
|
110
118
|
const project = { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }
|
|
111
119
|
const records = new Map([
|
|
112
120
|
[join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
|
|
113
|
-
[join(cwd, '.dsh/rules/mine.md'), 'ORIGINAL RULE'],
|
|
121
|
+
[join(cwd, '.dsh/rules/mine.md'), 'ORIGINAL RULE'],
|
|
122
|
+
[join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '# Implementation skill'],
|
|
114
123
|
[join(cwd, 'a.js'), 'source'],
|
|
115
124
|
])
|
|
116
125
|
let execute
|
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(adopted.stage_bindings
|
|
156
|
-
'adopting the recommendation
|
|
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
|
|
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: '
|
|
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 === '
|
|
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
|
|
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,21 +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
|
|
271
|
-
assert(bundled.includes('服务端'), 'bundled security-redlines exists as expected')
|
|
272
|
-
const fs = makeFs({
|
|
270
|
+
const fs = makeFs({
|
|
273
271
|
// A project file with the same NAME but a different body. Under the old
|
|
274
272
|
// bare-name lookup this would have replaced the bundled rule silently.
|
|
275
273
|
'.dsh/eng.json': JSON.stringify({
|
|
276
274
|
flow: 'standard',
|
|
277
275
|
stage_bindings: {
|
|
278
276
|
'需求评审': { skills: [{
|
|
279
|
-
skill: { source: '
|
|
277
|
+
skill: { source: 'project', name: 'requirement-analysis' },
|
|
280
278
|
rules: [{ source: 'project', name: 'security-redlines' }],
|
|
281
279
|
}] },
|
|
282
280
|
},
|
|
283
281
|
}),
|
|
284
|
-
'.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
|
|
282
|
+
'.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
|
|
283
|
+
'.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
|
|
285
284
|
})
|
|
286
285
|
const exe = await registered(fs)
|
|
287
286
|
await exe({ operation: 'create', task_id: 'T9', title: 'x', branch: 'main' }, EXEC)
|
|
@@ -292,26 +291,24 @@ await assertThrows(
|
|
|
292
291
|
assert(sec.content.includes('前端校验即可'), 'the PROJECT body is what gets disclosed, because the reference names the project layer')
|
|
293
292
|
}
|
|
294
293
|
|
|
295
|
-
// ── 11b. a bundled
|
|
294
|
+
// ── 11b. a deleted bundled rule is reported as missing ───────────────────────
|
|
296
295
|
{
|
|
297
296
|
const fs = makeFs({
|
|
298
297
|
'.dsh/eng.json': JSON.stringify({
|
|
299
298
|
flow: 'standard',
|
|
300
299
|
stage_bindings: {
|
|
301
300
|
'需求评审': { skills: [{
|
|
302
|
-
skill: { source: '
|
|
301
|
+
skill: { source: 'project', name: 'requirement-analysis' },
|
|
303
302
|
rules: [{ source: 'bundled', name: 'security-redlines' }],
|
|
304
303
|
}] },
|
|
305
304
|
},
|
|
306
305
|
}),
|
|
307
|
-
'.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
|
|
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 exe({ operation: 'create', task_id: 'T10', title: 'x', branch: 'main' }, EXEC)
|
|
311
|
-
|
|
312
|
-
const sec = status.bindings.rules.find(r => r.name === 'security-redlines')
|
|
313
|
-
assert(sec !== undefined && sec.source === 'bundled', 'a bundled reference resolves to the bundled layer')
|
|
314
|
-
assert(sec.content.includes('服务端'), 'the bundled body is used, untouched by the same-named project file')
|
|
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')
|
|
315
312
|
}
|
|
316
313
|
|
|
317
314
|
// ── 12. session cwd drives every relative path (init regression) ──────────
|
|
@@ -602,10 +599,10 @@ function memProbe(files) {
|
|
|
602
599
|
const exe = await registered(fs)
|
|
603
600
|
await exe({ operation: 'create', task_id: 'FP-1', title: 'x', branch: 'main' }, EXEC)
|
|
604
601
|
const status = JSON.parse(await exe({ operation: 'status', task_id: 'FP-1' }, EXEC))
|
|
605
|
-
assert(status.bindings_drift === undefined, 'fresh task
|
|
606
|
-
const record = JSON.parse(fs._files.get('.dsh/task-FP-1.json'))
|
|
607
|
-
assert(
|
|
608
|
-
'create
|
|
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')
|
|
609
606
|
}
|
|
610
607
|
|
|
611
608
|
// ── 26. 0.22-C: multi-language verify command chain + parallel tasks ────────
|
package/.roundtrip-test.mjs
CHANGED
|
@@ -88,17 +88,11 @@ test('one user skill profile is shared across workspaces and cannot depend on a
|
|
|
88
88
|
}
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
test('recommendation adoption
|
|
91
|
+
test('recommendation adoption contains no bundled resources', () => {
|
|
92
92
|
const { config, copies } = materializeBundledReferences(adoptRecommendation('standard'), 'project')
|
|
93
|
-
assert.
|
|
94
|
-
assert.
|
|
95
|
-
|
|
96
|
-
assert.ok(refs.every(ref => ref.source === 'project'))
|
|
97
|
-
const rules = Object.values(config.skill_profiles).flatMap(profile => profile.rules)
|
|
98
|
-
assert.ok(rules.every(ref => ref.source === 'project'))
|
|
99
|
-
const shared = rules.filter(ref => ref.name === 'security-redlines-copy')
|
|
100
|
-
assert.equal(shared.length, 3)
|
|
101
|
-
assert.equal(copies.filter(copy => copy.kind === 'rule' && copy.name === 'security-redlines').length, 1)
|
|
93
|
+
assert.deepEqual(copies, [])
|
|
94
|
+
assert.deepEqual(config.stage_bindings, {})
|
|
95
|
+
assert.equal(JSON.stringify(config).includes('bundled:'), false)
|
|
102
96
|
assert.deepEqual(validateWorkflow(resolveFlow('standard', config).config), [])
|
|
103
97
|
})
|
|
104
98
|
|
|
@@ -112,7 +106,7 @@ test('project migration leaves a global user skill profile and its bundled rules
|
|
|
112
106
|
assert.deepEqual(copies, [])
|
|
113
107
|
})
|
|
114
108
|
|
|
115
|
-
test('
|
|
109
|
+
test('recommendation saves only editable project configuration, without resource copies', async () => {
|
|
116
110
|
const root = mkdtempSync(join(tmpdir(), 'dsh-adopt-'))
|
|
117
111
|
assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
|
|
118
112
|
const fs = {
|
|
@@ -126,39 +120,31 @@ test('adoption saves editable project copies, preserves edited files on repeat,
|
|
|
126
120
|
const first = await Controller.prototype.write.call(receiver,
|
|
127
121
|
{ path: root, ...recommended, materialize_bundled: 'project' })
|
|
128
122
|
assert.equal(first.ok, true)
|
|
129
|
-
assert.equal(first.adoption.created.length,
|
|
123
|
+
assert.equal(first.adoption.created.length, 0)
|
|
130
124
|
const file = join(root, '.dsh', 'eng.json')
|
|
131
125
|
const saved = JSON.parse(readFileSync(file, 'utf8'))
|
|
132
126
|
assert.equal(JSON.stringify(saved).includes('"bundled"'), false)
|
|
133
|
-
const shared = join(root, '.dsh', 'rules', 'security-redlines-copy.md')
|
|
134
|
-
assert.ok(readFileSync(shared, 'utf8').length > 0)
|
|
135
|
-
writeFileSync(shared, 'user edited shared rule\n')
|
|
136
127
|
const second = await Controller.prototype.write.call(receiver,
|
|
137
128
|
{ path: root, ...recommended, materialize_bundled: 'project' })
|
|
138
129
|
assert.equal(second.ok, true)
|
|
139
130
|
assert.equal(second.adoption.created.length, 0)
|
|
140
|
-
assert.equal(second.adoption.reused.length,
|
|
141
|
-
assert.equal(readFileSync(shared, 'utf8'), 'user edited shared rule\n')
|
|
131
|
+
assert.equal(second.adoption.reused.length, 0)
|
|
142
132
|
const loaded = await Controller.prototype.read.call(receiver, root)
|
|
143
133
|
assert.equal(loaded.ok, true)
|
|
144
|
-
|
|
145
|
-
.filter(rule => rule.name === 'security-redlines-copy')
|
|
146
|
-
assert.equal(sharedRefs.length, 3)
|
|
147
|
-
writeFileSync(file, JSON.stringify(compactProjectConfig(recommended)))
|
|
148
|
-
const legacy = await Controller.prototype.read.call(receiver, root)
|
|
149
|
-
assert.equal(legacy.ok, true)
|
|
150
|
-
assert.ok(JSON.stringify(legacy.config).includes('"bundled"'))
|
|
134
|
+
assert.deepEqual(loaded.config.stage_bindings, {})
|
|
151
135
|
} finally {
|
|
152
136
|
rmSync(root, { recursive: true, force: true })
|
|
153
137
|
}
|
|
154
138
|
})
|
|
155
139
|
|
|
156
140
|
test('round-trip: stages store references while skill rules have one owner', () => {
|
|
157
|
-
const adopted = adoptRecommendation('standard')
|
|
141
|
+
const adopted = { ...adoptRecommendation('standard'), stage_bindings: {
|
|
142
|
+
'开发': { skill_refs: [{ source: 'project', name: 'my-implementation' }] },
|
|
143
|
+
}, skill_profiles: { 'project:my-implementation': { rules: [{ source: 'project', name: 'my-rule' }] } } }
|
|
158
144
|
const compact = compactProjectConfig(adopted)
|
|
159
145
|
assert.ok(compact.stage_bindings['开发'].skill_refs.length > 0)
|
|
160
146
|
assert.equal(compact.stage_bindings['开发'].skills, undefined)
|
|
161
|
-
assert.ok(compact.skill_profiles['
|
|
147
|
+
assert.ok(compact.skill_profiles['project:my-implementation'].rules.length > 0)
|
|
162
148
|
const readBack = resolveFlow('standard', JSON.parse(JSON.stringify(compact)))
|
|
163
149
|
assert.deepEqual(readBack.config.stage_bindings, resolveFlow('standard', adopted).config.stage_bindings)
|
|
164
150
|
const custom = { flow: 'standard', stage_bindings: {
|
|
@@ -177,13 +163,15 @@ test('remote write stores canonical skill profiles and read expands them', async
|
|
|
177
163
|
writeText: async (_target, text) => { stored = text },
|
|
178
164
|
}
|
|
179
165
|
const receiver = { authorizedPath: async path => path, fs: () => fs }
|
|
180
|
-
const adopted = adoptRecommendation('standard')
|
|
166
|
+
const adopted = { ...adoptRecommendation('standard'), stage_bindings: {
|
|
167
|
+
'开发': { skill_refs: [{ source: 'project', name: 'my-implementation' }] },
|
|
168
|
+
}, skill_profiles: { 'project:my-implementation': { rules: [{ source: 'project', name: 'my-rule' }] } } }
|
|
181
169
|
const written = await Controller.prototype.write.call(receiver, { path: 'project', ...adopted })
|
|
182
170
|
assert.equal(written.ok, true)
|
|
183
171
|
const onDisk = JSON.parse(stored)
|
|
184
172
|
assert.ok(onDisk.stage_bindings['开发'].skill_refs.length > 0)
|
|
185
173
|
assert.equal(onDisk.stage_bindings['开发'].skills, undefined)
|
|
186
|
-
assert.ok(onDisk.skill_profiles['
|
|
174
|
+
assert.ok(onDisk.skill_profiles['project:my-implementation'].rules.length > 0)
|
|
187
175
|
const loaded = await Controller.prototype.read.call(receiver, 'project')
|
|
188
176
|
assert.equal(loaded.ok, true)
|
|
189
177
|
assert.deepEqual(loaded.config.stage_bindings, written.config.stage_bindings)
|
package/.workflow-test.mjs
CHANGED
|
@@ -309,11 +309,9 @@ test('记录字段来自冻结流程,精简流程不硬编码标准字段,
|
|
|
309
309
|
assert.deepEqual(JSON.parse(await fixture('设计').call({ operation: 'status' })).artifact_requirements[0].fields, ['approach', 'risks', 'impact'])
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
-
test('
|
|
313
|
-
const f = fixture('需求评审', { artifacts: { requirement: { scope: 'device picker', acceptance_criteria: 'contract and selection' } } })
|
|
314
|
-
f.
|
|
315
|
-
await f.call({ operation: 'skill_result', skill_name: 'requirement-analysis', command: 'check', evidence: ['optional check'] })
|
|
316
|
-
await f.call({ operation: 'scope', files: ['app.js', 'new-vo.java'] })
|
|
312
|
+
test('空绑定流程不会因范围变更产生额外技能门禁', async () => {
|
|
313
|
+
const f = fixture('需求评审', { artifacts: { requirement: { scope: 'device picker', acceptance_criteria: 'contract and selection' } } })
|
|
314
|
+
await f.call({ operation: 'scope', files: ['app.js', 'new-vo.java'] })
|
|
317
315
|
await f.call({ operation: 'advance', target_stage: '设计' })
|
|
318
316
|
assert.equal(f.state().stage, '设计')
|
|
319
317
|
})
|
|
@@ -413,12 +411,12 @@ test('同名技能与规则在项目和个人目录各自保留管理入口', as
|
|
|
413
411
|
assert.match(read.content, /Run tests/)
|
|
414
412
|
})
|
|
415
413
|
|
|
416
|
-
test('
|
|
414
|
+
test('安装包只注册会话编排技能,阶段目录不展示它', async () => {
|
|
417
415
|
const registered = []
|
|
418
416
|
registerShippedSkills({ get: name => name === 'skills' ? { register: skill => { registered.push(skill); return () => {} } } : undefined })
|
|
419
|
-
const expected = ['
|
|
417
|
+
const expected = ['eng-delivery']
|
|
420
418
|
assert.deepEqual(registered.map(skill => skill.name).sort(), expected)
|
|
421
419
|
assert.ok(registered.every(skill => skill.content.trim().length > 0))
|
|
422
420
|
const catalog = await Controller.prototype.listSkills.call({ authorizedPath: async path => path }, '')
|
|
423
|
-
assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(),
|
|
421
|
+
assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(), [])
|
|
424
422
|
})
|
package/README.md
CHANGED
|
@@ -56,9 +56,9 @@ dsh plugin --profile web add @godv61/dsh-task-engine
|
|
|
56
56
|
| **技能** | 安装、编辑技能,并集中维护每个技能唯一的规则列表。 |
|
|
57
57
|
| **规则** | 选择 Markdown 文件安装规则,维护项目或个人开发约定。 |
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
插件只内置负责会话编排的 `eng-delivery`。阶段技能和规则由使用者创建或安装,可放在项目或个人目录;安装、删除都会明确展示目标位置。
|
|
60
60
|
|
|
61
|
-
**流程与工作方法是两回事。**
|
|
61
|
+
**流程与工作方法是两回事。** 流程只决定工作怎么流转(阶段顺序与门禁),预设不自带任何技能或提交格式。要用什么技能、遵守哪些规则,都由你配置。「采用推荐配置」只填写可修改的提交文本、产物字段和评审深度,不改变技能与规则绑定。同一条规则可由多个技能共享。用户级技能及其规则配置由所有项目和会话共用。任务的流程与资源引用在创建时确定;Skill/Rule 正文在每次交互读取最新版本,创建时副本仅用于审计。[配置细节](docs/configuration.md)
|
|
62
62
|
|
|
63
63
|
## 选择适合这次任务的流程
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ dsh plugin --profile web add @godv61/dsh-task-engine
|
|
|
68
68
|
| **日常迭代** `agile` | 需求 → 开发 → 交付 → 审查 | 目标明确的日常开发任务。 |
|
|
69
69
|
| **快速修改** `minimal` | 开发 → 交付 | 已明确做法的小改动。 |
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
阶段顺序和检查条件由预设固定;技能与规则由用户配置。空绑定也能运行流程。需要提交文本和产物字段的起点时可显式采用推荐配置。**可视化自定义流程不在计划内**,详见[功能规划](docs/roadmap.md)。
|
|
72
72
|
|
|
73
73
|
## 把自己的技能和规则带进来
|
|
74
74
|
|
|
@@ -78,9 +78,11 @@ dsh plugin --profile web add @godv61/dsh-task-engine
|
|
|
78
78
|
|
|
79
79
|
| 安装范围 | 存放位置 | 用途 |
|
|
80
80
|
| :--- | :--- | :--- |
|
|
81
|
-
| 项目 | 工作区 `.dsh/skills`、`.dsh/rules` |
|
|
82
|
-
| 个人 | `$DSH_HOME/skills`、`$DSH_HOME/rules` | 在这台电脑上的多个项目间复用。 |
|
|
83
|
-
|
|
81
|
+
| 项目 | 工作区 `.dsh/skills`、`.dsh/rules` | 当前项目的工作方法与约定;工作台新建资源默认写在这里。 |
|
|
82
|
+
| 个人 | `$DSH_HOME/skills`、`$DSH_HOME/rules` | 在这台电脑上的多个项目间复用。 |
|
|
83
|
+
|
|
84
|
+
工作台也会发现同一项目 `.agents/skills/<名称>/SKILL.md` 中的 Codex 项目技能,可将它绑定到流程节点,并在技能下配置 DSH 规则;它的正文仍留在原目录。运行时使用 `dev_task` 的 `load_skill` 操作读取该技能及其规则,下一次读取会取得文件的最新内容。[Codex 项目技能说明](docs/configuration.md#技能与规则)
|
|
85
|
+
|
|
84
86
|
技能的脚本、模板和附件会一并保留。文件格式、大小限制和故障处理见[技能与规则安装指南](docs/resource-install.md)。
|
|
85
87
|
|
|
86
88
|
## 使用文档
|