@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.
Files changed (37) hide show
  1. package/.p0-test.mjs +10 -10
  2. package/.workflow-test.mjs +274 -218
  3. package/README.md +10 -8
  4. package/cordis.patch.yml +10 -10
  5. package/defaults/eng.json +3 -3
  6. package/docs/CHANGELOG.md +55 -42
  7. package/docs/README.md +6 -4
  8. package/docs/configuration.md +4 -4
  9. package/docs/faq.md +6 -2
  10. package/docs/manual.html +9 -8
  11. package/docs/release-0.23.1.md +58 -58
  12. package/docs/release-0.23.2.md +21 -0
  13. package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +34 -34
  14. 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
  15. 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
  16. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -0
  17. 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
  18. package/docs/workflow-regression.md +36 -36
  19. package/lib/client.js +46 -44
  20. package/lib/client.js.map +3 -3
  21. package/lib/dev-task.js +18 -8
  22. package/lib/dev-task.js.map +1 -1
  23. package/package.json +8 -8
  24. package/preset/agent.cordis.yml +21 -21
  25. package/preset/enable.mjs +87 -87
  26. package/preset/persona.md +4 -4
  27. package/preset/preset.yml +1 -1
  28. package/rules/coding-conventions.md +6 -6
  29. package/rules/commit-conventions.md +6 -6
  30. package/rules/security-redlines.md +5 -5
  31. package/skills/code-commit/SKILL.md +3 -3
  32. package/skills/code-implement/SKILL.md +10 -10
  33. package/skills/code-review/SKILL.md +2 -2
  34. package/skills/code-verify/SKILL.md +10 -8
  35. package/skills/eng-delivery/SKILL.md +8 -8
  36. package/skills/requirement-analysis/SKILL.md +4 -4
  37. 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`)
@@ -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 (name === 'sandboxPolicy') return { resolve(request) { assert.equal(request.session, session); return policy } }
33
- if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async run(request) {
34
- const fail = request.command === 'fail'
35
- 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: '' } }
36
- } }
37
- return undefined
38
- } }
39
- registerDevTask(ctx)
40
- const exec = { agent: { session }, signal: abort.signal }
41
- function load(name, success = true) {
42
- const callId = `call-${events.length}`
43
- events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
44
- events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: !success }] } } })
45
- }
46
- return { call: args => execute(JSON.parse(JSON.stringify({ task_id: 'LIVE-1', ...args })), exec), load, runs, policy, exec, records, cwd,
47
- state: () => JSON.parse(records.get(join(cwd, '.dsh/task-LIVE-1.json'))) }
48
- }
49
-
50
- test('审查通过后更新状态并激活下一项,保留已完成项的审计', async () => {
51
- const f = fixture()
52
- await f.call({ operation: 'items', items: [{ id: 'A', title: 'first', status: 'doing' }, { id: 'B', title: 'second' }] })
53
- await f.call({ operation: 'dispatch', item_id: 'A', description: 'implemented by worker' })
54
- await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
55
- await f.call({ operation: 'items', items: [{ id: 'A', title: 'first', status: 'done' }, { id: 'B', title: 'second', status: 'doing' }] })
56
- assert.equal(f.state().items[0].review.spec.outcome, 'pass')
57
- assert.equal(f.state().items[0].dispatch.description, 'implemented by worker')
58
- await f.call({ operation: 'items', items: [{ id: 'A', title: 'changed', status: 'doing' }, { id: 'B', title: 'second', status: 'todo' }] })
59
- assert.equal(f.state().items[0].review, undefined)
60
- await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'x' }, { id: 'A', title: 'y' }] }), /unique/)
61
- })
62
-
63
- test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静默丢审核', async () => {
64
- const f = fixture()
65
- await f.call({ operation: 'items', items: [{ id: 'A', title: 'reviewed models', status: 'doing' }] })
66
- await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
67
- await f.call({ operation: 'items', items: [{ id: 'A', status: 'done' }, { id: 'B', title: 'fix race', status: 'doing' }] })
68
- assert.equal(f.state().items[0].title, 'reviewed models')
69
- assert.equal(f.state().items[0].review.quality.outcome, 'pass')
70
- await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'rewritten summary', status: 'done' }, { id: 'B', status: 'doing' }] }), /omit title|reopen/)
71
- assert.equal(f.state().items[0].review.quality.outcome, 'pass')
72
- assert.equal(f.state().items.length, 2)
73
- await assert.rejects(f.call({ operation: 'items', items: [{ id: 'NEW' }] }), /title/)
74
- })
75
-
76
- test('派发立即反映进行中,重派不复用旧审核且保持单一进行项', async () => {
77
- const f = fixture()
78
- await f.call({ operation: 'items', items: [{ id: 'A', title: 'models' }, { id: 'B', title: 'api' }] })
79
- await f.call({ operation: 'dispatch', item_id: 'A', description: 'implement models' })
80
- assert.equal(f.state().items[0].status, 'doing')
81
- await assert.rejects(f.call({ operation: 'dispatch', item_id: 'B' }), /current doing item/)
82
- await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
83
- await f.call({ operation: 'dispatch', item_id: 'A', description: 'rework models' })
84
- assert.equal(f.state().items[0].review, undefined)
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('status 披露当前阶段记录字段和缺项,字段误用不写入且可按提示恢复', async () => {
101
- const f = fixture('需求评审')
94
+ test('verify 升级审批不可用时不得先执行命令', async () => {
95
+ const f = fixture('交付')
102
96
  const before = JSON.stringify(f.state())
103
- const status = JSON.parse(await f.call({ operation: 'status' }))
104
- assert.deepEqual(status.artifact_requirements, [{ id: 'requirement', name: '需求说明', fields: ['scope', 'acceptance_criteria'], missing_fields: ['scope', 'acceptance_criteria'] }])
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('可选内置技能回执不会因范围变更升级成额外流转门禁', async () => {
125
- const f = fixture('需求评审', { artifacts: { requirement: { scope: 'device picker', acceptance_criteria: 'contract and selection' } } })
126
- f.load('requirement-analysis')
127
- await f.call({ operation: 'skill_result', skill_name: 'requirement-analysis', command: 'check', evidence: ['optional check'] })
128
- await f.call({ operation: 'scope', files: ['app.js', 'new-vo.java'] })
129
- await f.call({ operation: 'advance', target_stage: '设计' })
130
- assert.equal(f.state().stage, '设计')
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('验证使用会话策略与取消信号,并明确返回失败及沙箱信息', async () => {
134
- const f = fixture('交付')
135
- const result = JSON.parse(await f.call({ operation: 'verify', command: 'fail' }))
136
- assert.equal(result.ok, false)
137
- assert.equal(f.runs[0].sandboxPolicy, f.policy)
138
- assert.equal(f.runs[0].signal, f.exec.signal)
139
- assert.equal(result.verification.receipt.sandbox.mode, 'workspace-write')
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('只声明已测试、或加载失败,均不能冒充执行绑定技能', async () => {
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
- const stale = JSON.parse(await f.call({ operation: 'status' }))
188
- assert.equal(stale.commit.allowed, false)
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
- 记录需求、方案或评审前,模型可从 `artifact_requirements` 获取当前阶段允许的字段和缺项,避免猜测字段名。
106
+ 验证命令的单次权限重试先审批后执行,不改变会话权限;操作方式见[验证权限说明](docs/faq.md#验证命令被沙箱阻止怎么办)。
107
+
108
+ 记录需求、方案或评审前,模型可从 `artifact_requirements` 获取当前阶段允许的字段和缺项,避免猜测字段名。
107
109
 
108
110
  ---
109
111