@godv61/dsh-task-engine 0.23.1 → 0.23.3
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/.p0-test.mjs +10 -10
- package/.workflow-test.mjs +274 -218
- package/README.md +10 -8
- package/cordis.patch.yml +10 -10
- package/defaults/eng.json +3 -3
- package/docs/CHANGELOG.md +55 -42
- package/docs/README.md +6 -4
- package/docs/configuration.md +4 -4
- package/docs/faq.md +6 -2
- package/docs/manual.html +9 -8
- package/docs/release-0.23.1.md +58 -58
- package/docs/release-0.23.2.md +21 -0
- package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +34 -34
- package/docs/testing/0.23.2/R02/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +56 -0
- package/docs/testing/0.23.2/R03/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +38 -0
- package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -0
- package/docs/testing/0.23.2//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +43 -0
- package/docs/workflow-regression.md +36 -36
- package/lib/client.js +46 -44
- package/lib/client.js.map +3 -3
- package/lib/dev-task.js +18 -8
- package/lib/dev-task.js.map +1 -1
- package/package.json +8 -8
- package/preset/agent.cordis.yml +21 -21
- package/preset/enable.mjs +87 -87
- package/preset/persona.md +4 -4
- package/preset/preset.yml +1 -1
- package/rules/coding-conventions.md +6 -6
- package/rules/commit-conventions.md +6 -6
- package/rules/security-redlines.md +5 -5
- package/skills/code-commit/SKILL.md +3 -3
- package/skills/code-implement/SKILL.md +10 -10
- package/skills/code-review/SKILL.md +2 -2
- package/skills/code-verify/SKILL.md +10 -8
- package/skills/eng-delivery/SKILL.md +8 -8
- package/skills/requirement-analysis/SKILL.md +4 -4
- package/skills/solution-design/SKILL.md +7 -7
package/.p0-test.mjs
CHANGED
|
@@ -101,7 +101,7 @@ function sessionExec(cwd) {
|
|
|
101
101
|
assert(flowSatisfies('standard', HIGH_RISK_REQUIRED_CAPABILITIES), 'standard satisfies high-risk capabilities')
|
|
102
102
|
assert(!flowSatisfies('agile', HIGH_RISK_REQUIRED_CAPABILITIES), 'agile lacks a high-risk capability')
|
|
103
103
|
assert(!flowSatisfies('minimal', HIGH_RISK_REQUIRED_CAPABILITIES), 'minimal lacks a high-risk capability')
|
|
104
|
-
assert(FLOW_PRESETS.standard.version === 2, 'standard preset carries the review-before-commit version')
|
|
104
|
+
assert(FLOW_PRESETS.standard.version === 2, 'standard preset carries the review-before-commit version')
|
|
105
105
|
|
|
106
106
|
// ── 2. unknown flow fails closed ───────────────────────────────────────────
|
|
107
107
|
const unknown = resolveFlow('nope')
|
|
@@ -135,7 +135,7 @@ await assertThrows(
|
|
|
135
135
|
assert(created.includes('需求评审'), 'standard create starts at 需求评审')
|
|
136
136
|
fs._files.set('.dsh/eng.json', JSON.stringify({ flow: 'minimal' }))
|
|
137
137
|
const status = JSON.parse(await exe({ operation: 'status', task_id: 'T3' }, EXEC))
|
|
138
|
-
assert(status.flow?.flow === 'standard' && status.flow.version === 2, 'status re-reads the frozen snapshot')
|
|
138
|
+
assert(status.flow?.flow === 'standard' && status.flow.version === 2, 'status re-reads the frozen snapshot')
|
|
139
139
|
assert(Array.isArray(status.legal_next) && status.legal_next.includes('设计'),
|
|
140
140
|
'gates come from the frozen standard flow, not the live minimal flow')
|
|
141
141
|
}
|
|
@@ -542,13 +542,13 @@ function memProbe(files) {
|
|
|
542
542
|
const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
|
|
543
543
|
const exe = await registered(fs)
|
|
544
544
|
await exe({ operation: 'create', task_id: 'V-3', title: 'x', branch: 'main' }, EXEC)
|
|
545
|
-
await assertThrows(() => exe({ operation: 'verify', task_id: 'V-3', passed: true }, EXEC),
|
|
546
|
-
'real validation command', 'new tasks refuse self-reported verification')
|
|
547
|
-
const legacy = JSON.parse(fs._files.get('.dsh/task-V-3.json'))
|
|
548
|
-
delete legacy.execution_version
|
|
549
|
-
fs._files.set('.dsh/task-V-3.json', JSON.stringify(legacy))
|
|
550
|
-
assert(JSON.parse(await exe({ operation: 'verify', task_id: 'V-3', passed: true }, EXEC)).ok,
|
|
551
|
-
'legacy tasks retain their verification contract')
|
|
545
|
+
await assertThrows(() => exe({ operation: 'verify', task_id: 'V-3', passed: true }, EXEC),
|
|
546
|
+
'real validation command', 'new tasks refuse self-reported verification')
|
|
547
|
+
const legacy = JSON.parse(fs._files.get('.dsh/task-V-3.json'))
|
|
548
|
+
delete legacy.execution_version
|
|
549
|
+
fs._files.set('.dsh/task-V-3.json', JSON.stringify(legacy))
|
|
550
|
+
assert(JSON.parse(await exe({ operation: 'verify', task_id: 'V-3', passed: true }, EXEC)).ok,
|
|
551
|
+
'legacy tasks retain their verification contract')
|
|
552
552
|
}
|
|
553
553
|
{
|
|
554
554
|
const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
|
|
@@ -736,4 +736,4 @@ function memProbe(files) {
|
|
|
736
736
|
assert(JSON.parse(fs._files.get('.dsh/task-CAS-RACE.json')).title === 'concurrent writer', 'CAS preserves the concurrent writer record')
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
-
console.log(`\nP0 acceptance: ${passed} checks passed`)
|
|
739
|
+
console.log(`\nP0 acceptance: ${passed} checks passed`)
|
package/.workflow-test.mjs
CHANGED
|
@@ -1,234 +1,290 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join, resolve, sep } from 'node:path'
|
|
6
|
-
import { registerDevTask } from './lib/dev-task.js'
|
|
7
|
-
import { newTask } from './lib/engine.js'
|
|
8
|
-
import { resolveFlow } from './lib/workflows.js'
|
|
9
|
-
import Controller from './lib/controller.js'
|
|
10
|
-
import { registerShippedSkills } from './lib/shipped-skills.js'
|
|
11
|
-
|
|
12
|
-
function fixture(stage = '开发', extra = {}) {
|
|
13
|
-
const cwd = resolve('test-project')
|
|
14
|
-
const config = resolveFlow('standard', { stage_bindings: { 完成: { skills: ['software-testing'] } } }).config
|
|
15
|
-
const state = newTask({ id: 'LIVE-1', title: 'EAM regression', branch: 'test', work_size: 'standard', risk_level: 'standard', flow: { flow: 'standard', version: 2, config }, root: cwd })
|
|
16
|
-
Object.assign(state, { stage, execution_version: 1, files: ['app.js'], requirement_confirmed: true, solution_confirmed: true, ...extra })
|
|
17
|
-
const records = new Map([[join(cwd, '.dsh/task-LIVE-1.json'), JSON.stringify(state)], [join(cwd, 'app.js'), 'source']])
|
|
18
|
-
const events = [], runs = []
|
|
19
|
-
const session = { id: 'test-session', header: { cwd }, snapshotEvents: () => events }
|
|
20
|
-
const policy = { mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }
|
|
21
|
-
const abort = new AbortController()
|
|
22
|
-
let execute
|
|
23
|
-
const key = (path, options) => join(options?.cwd ?? cwd, path)
|
|
24
|
-
const fs = {
|
|
25
|
-
resolve: async (path, options) => ({ targetKey: key(path, options) }),
|
|
26
|
-
readText: async target => records.get(target.targetKey),
|
|
27
|
-
listDir: async () => [...records.keys()].filter(path => /task-.+\.json$/.test(path)).map(path => ({ name: path.split(/[\\/]/).at(-1) })),
|
|
28
|
-
lstat: async (path, options) => records.has(key(path, options)) ? { version: 'v' } : undefined,
|
|
29
|
-
writeText: async (target, content) => { records.set(target.targetKey, content) },
|
|
30
|
-
}
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, resolve, sep } from 'node:path'
|
|
6
|
+
import { registerDevTask } from './lib/dev-task.js'
|
|
7
|
+
import { newTask } from './lib/engine.js'
|
|
8
|
+
import { resolveFlow } from './lib/workflows.js'
|
|
9
|
+
import Controller from './lib/controller.js'
|
|
10
|
+
import { registerShippedSkills } from './lib/shipped-skills.js'
|
|
11
|
+
|
|
12
|
+
function fixture(stage = '开发', extra = {}, services = {}) {
|
|
13
|
+
const cwd = resolve('test-project')
|
|
14
|
+
const config = resolveFlow('standard', { stage_bindings: { 完成: { skills: ['software-testing'] } } }).config
|
|
15
|
+
const state = newTask({ id: 'LIVE-1', title: 'EAM regression', branch: 'test', work_size: 'standard', risk_level: 'standard', flow: { flow: 'standard', version: 2, config }, root: cwd })
|
|
16
|
+
Object.assign(state, { stage, execution_version: 1, files: ['app.js'], requirement_confirmed: true, solution_confirmed: true, ...extra })
|
|
17
|
+
const records = new Map([[join(cwd, '.dsh/task-LIVE-1.json'), JSON.stringify(state)], [join(cwd, 'app.js'), 'source']])
|
|
18
|
+
const events = [], runs = []
|
|
19
|
+
const session = { id: 'test-session', header: { cwd }, snapshotEvents: () => events }
|
|
20
|
+
const policy = { mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }
|
|
21
|
+
const abort = new AbortController()
|
|
22
|
+
let execute
|
|
23
|
+
const key = (path, options) => join(options?.cwd ?? cwd, path)
|
|
24
|
+
const fs = {
|
|
25
|
+
resolve: async (path, options) => ({ targetKey: key(path, options) }),
|
|
26
|
+
readText: async target => records.get(target.targetKey),
|
|
27
|
+
listDir: async () => [...records.keys()].filter(path => /task-.+\.json$/.test(path)).map(path => ({ name: path.split(/[\\/]/).at(-1) })),
|
|
28
|
+
lstat: async (path, options) => records.has(key(path, options)) ? { version: 'v' } : undefined,
|
|
29
|
+
writeText: async (target, content) => { records.set(target.targetKey, content) },
|
|
30
|
+
}
|
|
31
31
|
const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
|
|
32
|
-
if (
|
|
33
|
-
if (name === '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
events.push({ type: 'tool/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await f.call({ operation: '
|
|
54
|
-
await f.call({ operation: '
|
|
55
|
-
await f.call({ operation: '
|
|
56
|
-
|
|
57
|
-
assert.equal(f.state().items[0].
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await f.call({ operation: '
|
|
67
|
-
await f.call({ operation: '
|
|
68
|
-
|
|
69
|
-
assert.equal(f.state().items[0].
|
|
70
|
-
|
|
71
|
-
assert.
|
|
72
|
-
assert.equal(f.state().items.
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await f.call({ operation: '
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
await f.call({ operation: '
|
|
83
|
-
await f.call({ operation: '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
32
|
+
if (Object.hasOwn(services, name)) return services[name]
|
|
33
|
+
if (name === 'sandboxPolicy') return { resolve(request) { assert.equal(request.session, session); return policy } }
|
|
34
|
+
if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async run(request) {
|
|
35
|
+
const fail = request.command === 'fail'
|
|
36
|
+
return { exitCode: fail ? 1 : 0, timedOut: false, aborted: false, sandbox: { mode: 'workspace-write', denied: false }, stdout: { text: request.command.startsWith('git -c core.quotepath=false log') ? 'abcdef1234567890\n【LIVE-1】【TASK】done\n\napp.js\n' : 'checks passed' }, stderr: { text: '' } }
|
|
37
|
+
} }
|
|
38
|
+
return undefined
|
|
39
|
+
} }
|
|
40
|
+
registerDevTask(ctx)
|
|
41
|
+
const exec = { agent: { session }, signal: abort.signal }
|
|
42
|
+
function load(name, success = true) {
|
|
43
|
+
const callId = `call-${events.length}`
|
|
44
|
+
events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
|
|
45
|
+
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: !success }] } } })
|
|
46
|
+
}
|
|
47
|
+
return { call: args => execute(JSON.parse(JSON.stringify({ task_id: 'LIVE-1', ...args })), exec), load, runs, policy, exec, records, cwd,
|
|
48
|
+
state: () => JSON.parse(records.get(join(cwd, '.dsh/task-LIVE-1.json'))) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('审查通过后更新状态并激活下一项,保留已完成项的审计', async () => {
|
|
52
|
+
const f = fixture()
|
|
53
|
+
await f.call({ operation: 'items', items: [{ id: 'A', title: 'first', status: 'doing' }, { id: 'B', title: 'second' }] })
|
|
54
|
+
await f.call({ operation: 'dispatch', item_id: 'A', description: 'implemented by worker' })
|
|
55
|
+
await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
|
|
56
|
+
await f.call({ operation: 'items', items: [{ id: 'A', title: 'first', status: 'done' }, { id: 'B', title: 'second', status: 'doing' }] })
|
|
57
|
+
assert.equal(f.state().items[0].review.spec.outcome, 'pass')
|
|
58
|
+
assert.equal(f.state().items[0].dispatch.description, 'implemented by worker')
|
|
59
|
+
await f.call({ operation: 'items', items: [{ id: 'A', title: 'changed', status: 'doing' }, { id: 'B', title: 'second', status: 'todo' }] })
|
|
60
|
+
assert.equal(f.state().items[0].review, undefined)
|
|
61
|
+
await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'x' }, { id: 'A', title: 'y' }] }), /unique/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静默丢审核', async () => {
|
|
65
|
+
const f = fixture()
|
|
66
|
+
await f.call({ operation: 'items', items: [{ id: 'A', title: 'reviewed models', status: 'doing' }] })
|
|
67
|
+
await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
|
|
68
|
+
await f.call({ operation: 'items', items: [{ id: 'A', status: 'done' }, { id: 'B', title: 'fix race', status: 'doing' }] })
|
|
69
|
+
assert.equal(f.state().items[0].title, 'reviewed models')
|
|
70
|
+
assert.equal(f.state().items[0].review.quality.outcome, 'pass')
|
|
71
|
+
await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'rewritten summary', status: 'done' }, { id: 'B', status: 'doing' }] }), /omit title|reopen/)
|
|
72
|
+
assert.equal(f.state().items[0].review.quality.outcome, 'pass')
|
|
73
|
+
assert.equal(f.state().items.length, 2)
|
|
74
|
+
await assert.rejects(f.call({ operation: 'items', items: [{ id: 'NEW' }] }), /title/)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('派发立即反映进行中,重派不复用旧审核且保持单一进行项', async () => {
|
|
78
|
+
const f = fixture()
|
|
79
|
+
await f.call({ operation: 'items', items: [{ id: 'A', title: 'models' }, { id: 'B', title: 'api' }] })
|
|
80
|
+
await f.call({ operation: 'dispatch', item_id: 'A', description: 'implement models' })
|
|
81
|
+
assert.equal(f.state().items[0].status, 'doing')
|
|
82
|
+
await assert.rejects(f.call({ operation: 'dispatch', item_id: 'B' }), /current doing item/)
|
|
83
|
+
await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
|
|
84
|
+
await f.call({ operation: 'dispatch', item_id: 'A', description: 'rework models' })
|
|
85
|
+
assert.equal(f.state().items[0].review, undefined)
|
|
86
|
+
})
|
|
87
|
+
|
|
87
88
|
test('无任务 id 的 status 发现当前工作区任务并支持分支过滤', async () => {
|
|
88
|
-
const f = fixture()
|
|
89
|
-
assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: undefined })).tasks[0].id, 'LIVE-1')
|
|
90
|
-
assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: undefined, branch: 'other' })).tasks.length, 0)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('状态明确区分内置节点门禁与附加技能命令回执,避免重复验证', async () => {
|
|
94
|
-
const f = fixture('代码审核')
|
|
95
|
-
const status = JSON.parse(await f.call({ operation: 'status' }))
|
|
96
|
-
assert.deepEqual(status.skill_obligations.find(row => row.stage === '代码审核').command_receipts_required, [])
|
|
97
|
-
assert.deepEqual(status.skill_obligations.find(row => row.stage === '完成').command_receipts_required, ['software-testing'])
|
|
89
|
+
const f = fixture()
|
|
90
|
+
assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: undefined })).tasks[0].id, 'LIVE-1')
|
|
91
|
+
assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: undefined, branch: 'other' })).tasks.length, 0)
|
|
98
92
|
})
|
|
99
93
|
|
|
100
|
-
test('
|
|
101
|
-
const f = fixture('
|
|
94
|
+
test('verify 升级审批不可用时不得先执行命令', async () => {
|
|
95
|
+
const f = fixture('交付')
|
|
102
96
|
const before = JSON.stringify(f.state())
|
|
103
|
-
|
|
104
|
-
assert.
|
|
105
|
-
assert.equal(JSON.stringify(f.state()), before)
|
|
106
|
-
await assert.rejects(f.call({ operation: 'record', artifact: 'requirement', fields: { scope: 'goal', 待确认取舍: 'question' } }), /status.artifact_requirements/)
|
|
97
|
+
await assert.rejects(f.call({ operation: 'verify', command: 'npm test', sandbox_permissions: 'danger-full-access', justification: 'Retry the denied test command' }), /approval/)
|
|
98
|
+
assert.equal(f.runs.length, 0, 'approval must precede shell execution')
|
|
107
99
|
assert.equal(JSON.stringify(f.state()), before)
|
|
108
|
-
await f.call({ operation: 'record', artifact: 'requirement', fields: { scope: '目标及待确认取舍', acceptance_criteria: ' ' } })
|
|
109
|
-
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, ['acceptance_criteria'])
|
|
110
|
-
await f.call({ operation: 'record', artifact: 'requirement', fields: { acceptance_criteria: '可执行验收' } })
|
|
111
|
-
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, [])
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('记录字段来自冻结流程,精简流程不硬编码标准字段,无记录阶段返回空列表', async () => {
|
|
115
|
-
const config = resolveFlow('agile').config
|
|
116
|
-
const f = fixture('需求', { flow: { flow: 'agile', version: 1, config } })
|
|
117
|
-
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements, [{ id: 'requirement', name: '需求说明', fields: ['scope'], missing_fields: ['scope'] }])
|
|
118
|
-
await f.call({ operation: 'record', artifact: 'requirement', fields: { scope: '目标、验收与疑问' } })
|
|
119
|
-
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, [])
|
|
120
|
-
assert.deepEqual(JSON.parse(await fixture('开发').call({ operation: 'status' })).artifact_requirements, [])
|
|
121
|
-
assert.deepEqual(JSON.parse(await fixture('设计').call({ operation: 'status' })).artifact_requirements[0].fields, ['approach', 'risks', 'impact'])
|
|
122
100
|
})
|
|
123
101
|
|
|
124
|
-
test('
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
102
|
+
test('verify 拒绝、取消及无效升级参数均不执行命令或改写台账', async () => {
|
|
103
|
+
for (const outcome of ['rejected', 'cancelled', 'unavailable']) {
|
|
104
|
+
const f = fixture('交付', {}, { approval: { request: async () => outcome } })
|
|
105
|
+
const before = JSON.stringify(f.state())
|
|
106
|
+
await assert.rejects(f.call({ operation: 'verify', command: 'npm test', sandbox_permissions: 'danger-full-access', justification: 'Retry denied command' }), /not approved/)
|
|
107
|
+
assert.equal(f.runs.length, 0)
|
|
108
|
+
assert.equal(JSON.stringify(f.state()), before)
|
|
109
|
+
}
|
|
110
|
+
for (const args of [{ sandbox_permissions: 'danger-full-access' }, { justification: 'orphan justification' }]) {
|
|
111
|
+
const f = fixture('交付')
|
|
112
|
+
await assert.rejects(f.call({ operation: 'verify', command: 'npm test', ...args }), /invalid escalation/)
|
|
113
|
+
assert.equal(f.runs.length, 0)
|
|
114
|
+
}
|
|
131
115
|
})
|
|
132
116
|
|
|
133
|
-
test('
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
117
|
+
test('验证命令审批先于执行,只授权本次命令且回执写入不重复审批', async () => {
|
|
118
|
+
for (const operation of ['verify', 'skill_result']) {
|
|
119
|
+
const approvals = []
|
|
120
|
+
const f = fixture('完成', {}, { approval: { request: async request => {
|
|
121
|
+
assert.equal(f.runs.length, 0)
|
|
122
|
+
approvals.push(request)
|
|
123
|
+
return 'allowed-once'
|
|
124
|
+
} } })
|
|
125
|
+
f.load('software-testing')
|
|
126
|
+
await f.call({ operation, command: 'npm test', skill_name: 'software-testing', evidence: ['real checks'], sandbox_permissions: 'danger-full-access', justification: 'Retry denied test command' })
|
|
127
|
+
assert.equal(approvals.length, 1)
|
|
128
|
+
assert.match(approvals[0].reason, /npm test/)
|
|
129
|
+
assert.equal(f.runs[0].sandboxPolicy.mode, 'danger-full-access')
|
|
130
|
+
assert.equal(f.runs[0].sandboxPolicy.workspaceRoot, f.cwd)
|
|
131
|
+
assert.equal(f.runs[0].sandboxPolicy.sessionId, f.policy.sessionId)
|
|
132
|
+
assert.equal(f.runs[0].signal, f.exec.signal)
|
|
133
|
+
assert.equal(f.policy.mode, 'workspace-write')
|
|
134
|
+
await f.call({ operation: 'verify', command: 'npm test' })
|
|
135
|
+
assert.equal(f.runs[1].sandboxPolicy, f.policy)
|
|
136
|
+
assert.equal(approvals.length, 1)
|
|
137
|
+
}
|
|
140
138
|
})
|
|
141
139
|
|
|
142
|
-
test('
|
|
143
|
-
const f = fixture('
|
|
144
|
-
f.load('code-review'); f.load('code-commit'); f.load('software-testing', false)
|
|
145
|
-
await assert.rejects(f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['tested'] }), /load skill/)
|
|
146
|
-
await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /software-testing/)
|
|
140
|
+
test('skill_result 审批拒绝时不执行命令、不写入技能回执', async () => {
|
|
141
|
+
const f = fixture('完成', {}, { approval: { request: async () => 'rejected' } })
|
|
147
142
|
f.load('software-testing')
|
|
148
|
-
await assert.rejects(f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', evidence: ['claimed pass'] }), /real validation command/)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test('绑定技能失败阻止完成;成功后仍须提交真实 Git 回执', async () => {
|
|
152
|
-
const f = fixture('代码审核', { review: { outcome: 'pass' }, artifacts: { review: { conclusion: 'pass', issues: 'none' } }, verification: { passed: true, evidence: ['checks'] } })
|
|
153
|
-
f.load('code-review'); f.load('code-commit'); f.load('software-testing')
|
|
154
|
-
await f.call({ operation: 'verify', command: 'check', evidence: ['real checks'] })
|
|
155
|
-
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'fail', evidence: ['scenario failed'] })
|
|
156
|
-
await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /skill_result/)
|
|
157
|
-
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['scenario passed'] })
|
|
158
|
-
await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /commit required/)
|
|
159
|
-
await f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done' })
|
|
160
|
-
await assert.rejects(f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done', hash: 'bbbbbbb' }), /does not match/)
|
|
161
|
-
await f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done', hash: 'abcdef1' })
|
|
162
|
-
await f.call({ operation: 'advance', target_stage: '完成' })
|
|
163
|
-
assert.equal(f.state().stage, '完成')
|
|
164
|
-
assert.equal(f.state().skill_results.完成['software-testing'].load_call_id.startsWith('call-'), true)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
test('验证后修改代码会使旧回执失效,必须重新验证', async () => {
|
|
168
|
-
const f = fixture('交付')
|
|
169
|
-
f.load('code-verify')
|
|
170
|
-
await f.call({ operation: 'verify', command: 'check' })
|
|
171
|
-
f.records.set(join(f.cwd, 'app.js'), 'modified source')
|
|
172
|
-
await assert.rejects(f.call({ operation: 'advance', target_stage: '代码审核' }), /stale/)
|
|
173
|
-
await f.call({ operation: 'verify', command: 'check' })
|
|
174
|
-
await f.call({ operation: 'advance', target_stage: '代码审核' })
|
|
175
|
-
assert.equal(f.state().stage, '代码审核')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test('status 提交提示与真实门禁一致,分别披露过期验证和附加技能回执且不改台账', async () => {
|
|
179
|
-
const f = fixture('代码审核', { review: { outcome: 'pass' }, artifacts: { review: { conclusion: 'pass', issues: 'none' } } })
|
|
180
|
-
await f.call({ operation: 'verify', command: 'check' })
|
|
181
|
-
assert.equal(JSON.parse(await f.call({ operation: 'status' })).commit.allowed, false)
|
|
182
|
-
f.load('code-review'); f.load('code-commit'); f.load('software-testing')
|
|
183
|
-
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['tested'] })
|
|
184
|
-
assert.equal(JSON.parse(await f.call({ operation: 'status' })).commit.allowed, true)
|
|
185
|
-
f.records.set(join(f.cwd, 'app.js'), 'changed after testing')
|
|
186
143
|
const before = JSON.stringify(f.state())
|
|
187
|
-
|
|
188
|
-
assert.equal(
|
|
189
|
-
assert.match(stale.commit.reason, /stale/)
|
|
190
|
-
assert.equal(stale.evidence_blockers.length, 2)
|
|
144
|
+
await assert.rejects(f.call({ operation: 'skill_result', skill_name: 'software-testing', command: 'npm test', evidence: ['test'], sandbox_permissions: 'danger-full-access', justification: 'Retry denied tests' }), /not approved/)
|
|
145
|
+
assert.equal(f.runs.length, 0)
|
|
191
146
|
assert.equal(JSON.stringify(f.state()), before)
|
|
192
|
-
await assert.rejects(f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done' }), /stale/)
|
|
193
|
-
await f.call({ operation: 'verify', command: 'check' })
|
|
194
|
-
const partial = JSON.parse(await f.call({ operation: 'status' }))
|
|
195
|
-
assert.equal(partial.commit.allowed, false)
|
|
196
|
-
assert.equal(partial.evidence_blockers.length, 1)
|
|
197
|
-
assert.match(partial.evidence_blockers[0], /software-testing/)
|
|
198
|
-
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['retested'] })
|
|
199
|
-
const current = JSON.parse(await f.call({ operation: 'status' }))
|
|
200
|
-
assert.equal(current.commit.allowed, true)
|
|
201
|
-
assert.deepEqual(current.evidence_blockers, [])
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('同名技能与规则在项目和个人目录各自保留管理入口', async t => {
|
|
205
|
-
const root = mkdtempSync(join(tmpdir(), 'engine-catalog-'))
|
|
206
|
-
const original = process.env.DSH_HOME
|
|
207
|
-
t.after(() => { if (original === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = original; assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep)); rmSync(root, { recursive: true, force: true }) })
|
|
208
|
-
process.env.DSH_HOME = join(root, 'user')
|
|
209
|
-
const project = join(root, 'project')
|
|
210
|
-
for (const base of [join(project, '.dsh'), process.env.DSH_HOME]) {
|
|
211
|
-
mkdirSync(join(base, 'skills/software-testing'), { recursive: true })
|
|
212
|
-
mkdirSync(join(base, 'rules'), { recursive: true })
|
|
213
|
-
writeFileSync(join(base, 'skills/software-testing/SKILL.md'), '\uFEFF---\r\nname: software-testing\r\ndescription: tests\r\n---\r\nRun tests.\r\n')
|
|
214
|
-
writeFileSync(join(base, 'rules/test-rule.md'), 'Test rule\n')
|
|
215
|
-
}
|
|
216
|
-
const receiver = { authorizedPath: async path => path }
|
|
217
|
-
const skills = await Controller.prototype.listSkills.call(receiver, project)
|
|
218
|
-
const rules = await Controller.prototype.listRules.call(receiver, project)
|
|
219
|
-
assert.deepEqual(skills.skills.filter(x => x.name === 'software-testing').map(x => x.source), ['project', 'user'])
|
|
220
|
-
assert.deepEqual(rules.rules.filter(x => x.name === 'test-rule').map(x => x.source), ['project', 'user'])
|
|
221
|
-
const read = await Controller.prototype.readSkill.call(receiver, { path: project, level: 'project', name: 'software-testing' })
|
|
222
|
-
assert.equal(read.ok, true)
|
|
223
|
-
assert.match(read.content, /Run tests/)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
test('安装包的七个内置技能实际注册且可在资源目录发现', async () => {
|
|
227
|
-
const registered = []
|
|
228
|
-
registerShippedSkills({ get: name => name === 'skills' ? { register: skill => { registered.push(skill); return () => {} } } : undefined })
|
|
229
|
-
const expected = ['code-commit', 'code-implement', 'code-review', 'code-verify', 'eng-delivery', 'requirement-analysis', 'solution-design']
|
|
230
|
-
assert.deepEqual(registered.map(skill => skill.name).sort(), expected)
|
|
231
|
-
assert.ok(registered.every(skill => skill.content.trim().length > 0))
|
|
232
|
-
const catalog = await Controller.prototype.listSkills.call({ authorizedPath: async path => path }, '')
|
|
233
|
-
assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(), expected)
|
|
234
147
|
})
|
|
148
|
+
|
|
149
|
+
test('状态明确区分内置节点门禁与附加技能命令回执,避免重复验证', async () => {
|
|
150
|
+
const f = fixture('代码审核')
|
|
151
|
+
const status = JSON.parse(await f.call({ operation: 'status' }))
|
|
152
|
+
assert.deepEqual(status.skill_obligations.find(row => row.stage === '代码审核').command_receipts_required, [])
|
|
153
|
+
assert.deepEqual(status.skill_obligations.find(row => row.stage === '完成').command_receipts_required, ['software-testing'])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('status 披露当前阶段记录字段和缺项,字段误用不写入且可按提示恢复', async () => {
|
|
157
|
+
const f = fixture('需求评审')
|
|
158
|
+
const before = JSON.stringify(f.state())
|
|
159
|
+
const status = JSON.parse(await f.call({ operation: 'status' }))
|
|
160
|
+
assert.deepEqual(status.artifact_requirements, [{ id: 'requirement', name: '需求说明', fields: ['scope', 'acceptance_criteria'], missing_fields: ['scope', 'acceptance_criteria'] }])
|
|
161
|
+
assert.equal(JSON.stringify(f.state()), before)
|
|
162
|
+
await assert.rejects(f.call({ operation: 'record', artifact: 'requirement', fields: { scope: 'goal', 待确认取舍: 'question' } }), /status.artifact_requirements/)
|
|
163
|
+
assert.equal(JSON.stringify(f.state()), before)
|
|
164
|
+
await f.call({ operation: 'record', artifact: 'requirement', fields: { scope: '目标及待确认取舍', acceptance_criteria: ' ' } })
|
|
165
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, ['acceptance_criteria'])
|
|
166
|
+
await f.call({ operation: 'record', artifact: 'requirement', fields: { acceptance_criteria: '可执行验收' } })
|
|
167
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, [])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('记录字段来自冻结流程,精简流程不硬编码标准字段,无记录阶段返回空列表', async () => {
|
|
171
|
+
const config = resolveFlow('agile').config
|
|
172
|
+
const f = fixture('需求', { flow: { flow: 'agile', version: 1, config } })
|
|
173
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements, [{ id: 'requirement', name: '需求说明', fields: ['scope'], missing_fields: ['scope'] }])
|
|
174
|
+
await f.call({ operation: 'record', artifact: 'requirement', fields: { scope: '目标、验收与疑问' } })
|
|
175
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, [])
|
|
176
|
+
assert.deepEqual(JSON.parse(await fixture('开发').call({ operation: 'status' })).artifact_requirements, [])
|
|
177
|
+
assert.deepEqual(JSON.parse(await fixture('设计').call({ operation: 'status' })).artifact_requirements[0].fields, ['approach', 'risks', 'impact'])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('可选内置技能回执不会因范围变更升级成额外流转门禁', async () => {
|
|
181
|
+
const f = fixture('需求评审', { artifacts: { requirement: { scope: 'device picker', acceptance_criteria: 'contract and selection' } } })
|
|
182
|
+
f.load('requirement-analysis')
|
|
183
|
+
await f.call({ operation: 'skill_result', skill_name: 'requirement-analysis', command: 'check', evidence: ['optional check'] })
|
|
184
|
+
await f.call({ operation: 'scope', files: ['app.js', 'new-vo.java'] })
|
|
185
|
+
await f.call({ operation: 'advance', target_stage: '设计' })
|
|
186
|
+
assert.equal(f.state().stage, '设计')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('验证使用会话策略与取消信号,并明确返回失败及沙箱信息', async () => {
|
|
190
|
+
const f = fixture('交付')
|
|
191
|
+
const result = JSON.parse(await f.call({ operation: 'verify', command: 'fail' }))
|
|
192
|
+
assert.equal(result.ok, false)
|
|
193
|
+
assert.equal(f.runs[0].sandboxPolicy, f.policy)
|
|
194
|
+
assert.equal(f.runs[0].signal, f.exec.signal)
|
|
195
|
+
assert.equal(result.verification.receipt.sandbox.mode, 'workspace-write')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('只声明已测试、或加载失败,均不能冒充执行绑定技能', async () => {
|
|
199
|
+
const f = fixture('代码审核')
|
|
200
|
+
f.load('code-review'); f.load('code-commit'); f.load('software-testing', false)
|
|
201
|
+
await assert.rejects(f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['tested'] }), /load skill/)
|
|
202
|
+
await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /software-testing/)
|
|
203
|
+
f.load('software-testing')
|
|
204
|
+
await assert.rejects(f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', evidence: ['claimed pass'] }), /real validation command/)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('绑定技能失败阻止完成;成功后仍须提交真实 Git 回执', async () => {
|
|
208
|
+
const f = fixture('代码审核', { review: { outcome: 'pass' }, artifacts: { review: { conclusion: 'pass', issues: 'none' } }, verification: { passed: true, evidence: ['checks'] } })
|
|
209
|
+
f.load('code-review'); f.load('code-commit'); f.load('software-testing')
|
|
210
|
+
await f.call({ operation: 'verify', command: 'check', evidence: ['real checks'] })
|
|
211
|
+
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'fail', evidence: ['scenario failed'] })
|
|
212
|
+
await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /skill_result/)
|
|
213
|
+
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['scenario passed'] })
|
|
214
|
+
await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /commit required/)
|
|
215
|
+
await f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done' })
|
|
216
|
+
await assert.rejects(f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done', hash: 'bbbbbbb' }), /does not match/)
|
|
217
|
+
await f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done', hash: 'abcdef1' })
|
|
218
|
+
await f.call({ operation: 'advance', target_stage: '完成' })
|
|
219
|
+
assert.equal(f.state().stage, '完成')
|
|
220
|
+
assert.equal(f.state().skill_results.完成['software-testing'].load_call_id.startsWith('call-'), true)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('验证后修改代码会使旧回执失效,必须重新验证', async () => {
|
|
224
|
+
const f = fixture('交付')
|
|
225
|
+
f.load('code-verify')
|
|
226
|
+
await f.call({ operation: 'verify', command: 'check' })
|
|
227
|
+
f.records.set(join(f.cwd, 'app.js'), 'modified source')
|
|
228
|
+
await assert.rejects(f.call({ operation: 'advance', target_stage: '代码审核' }), /stale/)
|
|
229
|
+
await f.call({ operation: 'verify', command: 'check' })
|
|
230
|
+
await f.call({ operation: 'advance', target_stage: '代码审核' })
|
|
231
|
+
assert.equal(f.state().stage, '代码审核')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('status 提交提示与真实门禁一致,分别披露过期验证和附加技能回执且不改台账', async () => {
|
|
235
|
+
const f = fixture('代码审核', { review: { outcome: 'pass' }, artifacts: { review: { conclusion: 'pass', issues: 'none' } } })
|
|
236
|
+
await f.call({ operation: 'verify', command: 'check' })
|
|
237
|
+
assert.equal(JSON.parse(await f.call({ operation: 'status' })).commit.allowed, false)
|
|
238
|
+
f.load('code-review'); f.load('code-commit'); f.load('software-testing')
|
|
239
|
+
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['tested'] })
|
|
240
|
+
assert.equal(JSON.parse(await f.call({ operation: 'status' })).commit.allowed, true)
|
|
241
|
+
f.records.set(join(f.cwd, 'app.js'), 'changed after testing')
|
|
242
|
+
const before = JSON.stringify(f.state())
|
|
243
|
+
const stale = JSON.parse(await f.call({ operation: 'status' }))
|
|
244
|
+
assert.equal(stale.commit.allowed, false)
|
|
245
|
+
assert.match(stale.commit.reason, /stale/)
|
|
246
|
+
assert.equal(stale.evidence_blockers.length, 2)
|
|
247
|
+
assert.equal(JSON.stringify(f.state()), before)
|
|
248
|
+
await assert.rejects(f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done' }), /stale/)
|
|
249
|
+
await f.call({ operation: 'verify', command: 'check' })
|
|
250
|
+
const partial = JSON.parse(await f.call({ operation: 'status' }))
|
|
251
|
+
assert.equal(partial.commit.allowed, false)
|
|
252
|
+
assert.equal(partial.evidence_blockers.length, 1)
|
|
253
|
+
assert.match(partial.evidence_blockers[0], /software-testing/)
|
|
254
|
+
await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['retested'] })
|
|
255
|
+
const current = JSON.parse(await f.call({ operation: 'status' }))
|
|
256
|
+
assert.equal(current.commit.allowed, true)
|
|
257
|
+
assert.deepEqual(current.evidence_blockers, [])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('同名技能与规则在项目和个人目录各自保留管理入口', async t => {
|
|
261
|
+
const root = mkdtempSync(join(tmpdir(), 'engine-catalog-'))
|
|
262
|
+
const original = process.env.DSH_HOME
|
|
263
|
+
t.after(() => { if (original === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = original; assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep)); rmSync(root, { recursive: true, force: true }) })
|
|
264
|
+
process.env.DSH_HOME = join(root, 'user')
|
|
265
|
+
const project = join(root, 'project')
|
|
266
|
+
for (const base of [join(project, '.dsh'), process.env.DSH_HOME]) {
|
|
267
|
+
mkdirSync(join(base, 'skills/software-testing'), { recursive: true })
|
|
268
|
+
mkdirSync(join(base, 'rules'), { recursive: true })
|
|
269
|
+
writeFileSync(join(base, 'skills/software-testing/SKILL.md'), '\uFEFF---\r\nname: software-testing\r\ndescription: tests\r\n---\r\nRun tests.\r\n')
|
|
270
|
+
writeFileSync(join(base, 'rules/test-rule.md'), 'Test rule\n')
|
|
271
|
+
}
|
|
272
|
+
const receiver = { authorizedPath: async path => path }
|
|
273
|
+
const skills = await Controller.prototype.listSkills.call(receiver, project)
|
|
274
|
+
const rules = await Controller.prototype.listRules.call(receiver, project)
|
|
275
|
+
assert.deepEqual(skills.skills.filter(x => x.name === 'software-testing').map(x => x.source), ['project', 'user'])
|
|
276
|
+
assert.deepEqual(rules.rules.filter(x => x.name === 'test-rule').map(x => x.source), ['project', 'user'])
|
|
277
|
+
const read = await Controller.prototype.readSkill.call(receiver, { path: project, level: 'project', name: 'software-testing' })
|
|
278
|
+
assert.equal(read.ok, true)
|
|
279
|
+
assert.match(read.content, /Run tests/)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('安装包的七个内置技能实际注册且可在资源目录发现', async () => {
|
|
283
|
+
const registered = []
|
|
284
|
+
registerShippedSkills({ get: name => name === 'skills' ? { register: skill => { registered.push(skill); return () => {} } } : undefined })
|
|
285
|
+
const expected = ['code-commit', 'code-implement', 'code-review', 'code-verify', 'eng-delivery', 'requirement-analysis', 'solution-design']
|
|
286
|
+
assert.deepEqual(registered.map(skill => skill.name).sort(), expected)
|
|
287
|
+
assert.ok(registered.every(skill => skill.content.trim().length > 0))
|
|
288
|
+
const catalog = await Controller.prototype.listSkills.call({ authorizedPath: async path => path }, '')
|
|
289
|
+
assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(), expected)
|
|
290
|
+
})
|
package/README.md
CHANGED
|
@@ -90,20 +90,22 @@ dsh plugin --profile web add @godv61/dsh-task-engine
|
|
|
90
90
|
| 完整操作说明 | [HTML 手册](docs/manual.html)(下载后在浏览器打开) |
|
|
91
91
|
| 当前能力与常见问题 | [常见问题](docs/faq.md) |
|
|
92
92
|
| 本地开发与验证 | [开发指南](docs/development.md) |
|
|
93
|
-
| 真实项目回归修复 | [0.23.1 发布说明](docs/release-0.23.1.md) · [回归迭代说明](docs/workflow-regression.md) |
|
|
94
|
-
| 版本变化与验证记录 | [更新日志](docs/CHANGELOG.md) · [0.23.1 测试说明](docs/testing/0.23.1/测试报告.md) |
|
|
93
|
+
| 真实项目回归修复 | [0.23.1 发布说明](docs/release-0.23.1.md) · [回归迭代说明](docs/workflow-regression.md) |
|
|
94
|
+
| 版本变化与验证记录 | [更新日志](docs/CHANGELOG.md) · [0.23.1 测试说明](docs/testing/0.23.1/测试报告.md) |
|
|
95
95
|
|
|
96
96
|
## 能力说明
|
|
97
97
|
|
|
98
|
-
`dev_task` 按配置检查阶段、产物和提交条件。0.23.1 新任务验证要求真实命令回执;附加技能的回执要求可在任务状态中查看,内置技能使用已有的记录、验证、审核和提交操作。任务保存创建时的流程快照,之后修改项目配置不会直接改变进行中的任务。
|
|
98
|
+
`dev_task` 按配置检查阶段、产物和提交条件。0.23.1 新任务验证要求真实命令回执;附加技能的回执要求可在任务状态中查看,内置技能使用已有的记录、验证、审核和提交操作。任务保存创建时的流程快照,之后修改项目配置不会直接改变进行中的任务。
|
|
99
|
+
|
|
100
|
+
审核结论、实施项完成情况和测试覆盖面仍需要你判断。本地提交钩子提供即时检查,不能替代人工审核或项目自己的 CI。详细说明见[常见问题](docs/faq.md)。
|
|
101
|
+
|
|
102
|
+
小修正允许主代理处理,仍须审核和验证。遇到沙箱权限拒绝应使用宿主审批或报告阻塞,不能靠反复切换命令规避。
|
|
99
103
|
|
|
100
|
-
审核结论、实施项完成情况和测试覆盖面仍需要你判断。本地提交钩子提供即时检查,不能替代人工审核或项目自己的 CI。详细说明见[常见问题](docs/faq.md)。
|
|
101
|
-
|
|
102
|
-
小修正允许主代理处理,仍须审核和验证。遇到沙箱权限拒绝应使用宿主审批或报告阻塞,不能靠反复切换命令规避。
|
|
103
|
-
|
|
104
104
|
状态查询会列出过期验证和附加技能回执;`commit.allowed` 同时检查这些阻塞,文件变化后先补验证再申请提交。
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
验证命令的单次权限重试先审批后执行,不改变会话权限;操作方式见[验证权限说明](docs/faq.md#验证命令被沙箱阻止怎么办)。
|
|
107
|
+
|
|
108
|
+
记录需求、方案或评审前,模型可从 `artifact_requirements` 获取当前阶段允许的字段和缺项,避免猜测字段名。
|
|
107
109
|
|
|
108
110
|
---
|
|
109
111
|
|