@godv61/dsh-task-engine 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.p0-test.mjs ADDED
@@ -0,0 +1,292 @@
1
+ // P0 acceptance test for dsh-task-engine 0.18.0: runs the pure engine +
2
+ // workflows functions and the registered `dev_task` tool against an in-memory
3
+ // fs, checking every P0 gate: capability binding, frozen snapshot, fail-closed
4
+ // unknown flow, core-rule shadow protection, and init three-phase flow.
5
+ import { readFileSync } from 'node:fs'
6
+ import { registerDevTask } from './lib/dev-task.js'
7
+ import { assertAdvance, checkFileScope, newTask, taskIdFromMessage, validateWorkflow } from './lib/engine.js'
8
+ import {
9
+ FLOW_PRESETS,
10
+ HIGH_RISK_REQUIRED_CAPABILITIES,
11
+ flowSatisfies,
12
+ resolveFlow,
13
+ } from './lib/workflows.js'
14
+
15
+ let passed = 0
16
+ function assert(cond, msg) {
17
+ if (!cond) {
18
+ console.error('FAIL:', msg)
19
+ throw new Error(msg)
20
+ }
21
+ passed++
22
+ }
23
+ async function assertThrows(fn, fragment, msg) {
24
+ try {
25
+ await fn()
26
+ } catch (err) {
27
+ const text = err instanceof Error ? err.message : String(err)
28
+ assert(text.includes(fragment), `${msg} (got: ${text})`)
29
+ return
30
+ }
31
+ throw new Error(`${msg}: expected throw containing "${fragment}"`)
32
+ }
33
+
34
+ function makeFs(initial = {}) {
35
+ const files = new Map(Object.entries(initial))
36
+ return {
37
+ async resolve(relPath, opts) {
38
+ const base = opts && opts.cwd ? String(opts.cwd).replace(/\\/g, '/') : ''
39
+ const key = base ? `${base}/${relPath}` : relPath
40
+ return { targetKey: key, displayPath: key }
41
+ },
42
+ async readText(target) {
43
+ return files.get(target.targetKey)
44
+ },
45
+ async writeText(target, content) {
46
+ files.set(target.targetKey, content)
47
+ },
48
+ _files: files,
49
+ }
50
+ }
51
+ function makeCtx(fs, approval) {
52
+ const tools = []
53
+ return {
54
+ fs,
55
+ tools: { register(tool) { tools.push(tool); return () => {} } },
56
+ get(service) {
57
+ if (service === 'approval') return approval
58
+ return undefined
59
+ },
60
+ _tools: tools,
61
+ }
62
+ }
63
+ async function registered(fs, approval) {
64
+ const ctx = makeCtx(fs, approval)
65
+ registerDevTask(ctx)
66
+ return ctx._tools[0].execute
67
+ }
68
+ const EXEC = { agent: undefined, callId: undefined, signal: undefined }
69
+ // A tool call whose session workspace is `cwd` (the harness process directory is
70
+ // deliberately different, exactly like a web session running in e2e-project).
71
+ function sessionExec(cwd) {
72
+ return { agent: { session: { header: { cwd } } }, callId: undefined, signal: undefined }
73
+ }
74
+
75
+ // ── 1. capability derivation and high-risk binding ────────────────────────
76
+ assert(flowSatisfies('standard', HIGH_RISK_REQUIRED_CAPABILITIES), 'standard satisfies high-risk capabilities')
77
+ assert(!flowSatisfies('agile', HIGH_RISK_REQUIRED_CAPABILITIES), 'agile lacks a high-risk capability')
78
+ assert(!flowSatisfies('minimal', HIGH_RISK_REQUIRED_CAPABILITIES), 'minimal lacks a high-risk capability')
79
+ assert(FLOW_PRESETS.standard.version === 1, 'preset carries a version')
80
+
81
+ // ── 2. unknown flow fails closed ───────────────────────────────────────────
82
+ const unknown = resolveFlow('nope')
83
+ assert(unknown.ok === false && unknown.code === 'UNKNOWN_FLOW', 'unknown flow returns UNKNOWN_FLOW')
84
+ assert(unknown.knownFlows.includes('standard'), 'known flow list is surfaced')
85
+
86
+ // ── 3. core bindings cannot be cancelled by an override ────────────────────
87
+ const merged = resolveFlow('standard', { stage_bindings: { '开发': { rules: [] } } })
88
+ assert(merged.ok && merged.config.stage_bindings['开发'].rules.includes('coding-conventions'),
89
+ 'emptying a stage override keeps the core rule')
90
+
91
+ // ── 4. newTask freezes the snapshot ────────────────────────────────────────
92
+ const snapshot = { flow: 'standard', version: 1, config: FLOW_PRESETS.standard.config }
93
+ const state = newTask({ id: 'T1', title: 'x', branch: 'main', work_size: 'standard', risk_level: 'standard', flow: snapshot })
94
+ assert(state.flow?.flow === 'standard' && state.flow.version === 1, 'newTask stores the frozen snapshot')
95
+ assert(state.stage === '需求评审', 'newTask starts at the frozen start stage')
96
+
97
+ // ── 5. high_risk create on minimal is rejected ─────────────────────────────
98
+ await assertThrows(
99
+ () => registered(makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'minimal' }) }))
100
+ .then(exe => exe({ operation: 'create', task_id: 'T2', title: 'x', branch: 'main', risk_level: 'high_risk' }, EXEC)),
101
+ 'high_risk',
102
+ 'high_risk on minimal is rejected',
103
+ )
104
+
105
+ // ── 6. a task keeps its frozen snapshot after eng.json changes ─────────────
106
+ {
107
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
108
+ const exe = await registered(fs)
109
+ const created = await exe({ operation: 'create', task_id: 'T3', title: 'x', branch: 'main' }, EXEC)
110
+ assert(created.includes('需求评审'), 'standard create starts at 需求评审')
111
+ fs._files.set('.dsh/eng.json', JSON.stringify({ flow: 'minimal' }))
112
+ const status = JSON.parse(await exe({ operation: 'status', task_id: 'T3' }, EXEC))
113
+ assert(status.flow?.flow === 'standard' && status.flow.version === 1, 'status re-reads the frozen snapshot')
114
+ assert(Array.isArray(status.legal_next) && status.legal_next.includes('设计'),
115
+ 'gates come from the frozen standard flow, not the live minimal flow')
116
+ }
117
+
118
+ // ── 7. unknown flow blocks state-changing operations ───────────────────────
119
+ await assertThrows(
120
+ () => registered(makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'nope' }) }))
121
+ .then(exe => exe({ operation: 'create', task_id: 'X', title: 'y', branch: 'z' }, EXEC)),
122
+ 'unknown flow',
123
+ 'create on an unknown flow fails closed',
124
+ )
125
+
126
+ // ── 8. a file missing its flow field fails closed ──────────────────────────
127
+ await assertThrows(
128
+ () => registered(makeFs({ '.dsh/eng.json': JSON.stringify({}) }))
129
+ .then(exe => exe({ operation: 'create', task_id: 'X', title: 'y', branch: 'z' }, EXEC)),
130
+ 'missing a "flow" field',
131
+ 'eng.json without a flow field is a config error',
132
+ )
133
+
134
+ // ── 9. init: inspect → propose → apply, no direct overwrite ────────────────
135
+ {
136
+ const fs = makeFs()
137
+ const exe = await registered(fs)
138
+ const insp = await exe({ operation: 'init', phase: 'inspect' }, EXEC)
139
+ assert(insp.includes('no AGENTS.md'), 'inspect on empty project prompts a scan')
140
+ const prop = await exe({ operation: 'init', phase: 'propose', content: '# 项目\n示例' }, EXEC)
141
+ assert(prop.includes('proposed create'), 'propose previews without writing')
142
+ assert(fs._files.get('AGENTS.md') === undefined, 'propose writes nothing')
143
+ const app = await exe({ operation: 'init', phase: 'apply', content: '# 项目\n示例' }, EXEC)
144
+ assert(app.includes('wrote ./AGENTS.md'), 'apply writes the file')
145
+ assert(fs._files.get('AGENTS.md') === '# 项目\n示例', 'apply persisted the draft')
146
+ }
147
+
148
+ // ── 10. init protects an existing governance file ─────────────────────────
149
+ {
150
+ const fs = makeFs({ 'AGENTS.md': '# 已有治理文件\n' })
151
+ const exe = await registered(fs)
152
+ await assertThrows(
153
+ () => exe({ operation: 'init', phase: 'apply', content: '# new' }, EXEC),
154
+ 'protected',
155
+ 'apply without overwrite on an existing file is rejected',
156
+ )
157
+ await assertThrows(
158
+ () => exe({ operation: 'init', phase: 'apply', content: '# new', overwrite: true }, EXEC),
159
+ 'approval',
160
+ 'overwrite requires an approval service',
161
+ )
162
+ }
163
+
164
+ // ── 11. a project cannot shadow a bundled core rule ────────────────────────
165
+ {
166
+ const bundled = readFileSync('./rules/security-redlines.md', 'utf8')
167
+ assert(bundled.includes('服务端'), 'bundled security-redlines exists as expected')
168
+ const fs = makeFs({
169
+ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }),
170
+ '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
171
+ })
172
+ const exe = await registered(fs)
173
+ await exe({ operation: 'create', task_id: 'T9', title: 'x', branch: 'main' }, EXEC)
174
+ const status = JSON.parse(await exe({ operation: 'status', task_id: 'T9' }, EXEC))
175
+ const sec = status.bindings.rules.find(r => r.name === 'security-redlines')
176
+ assert(sec !== undefined, 'security-redlines is disclosed at the start stage')
177
+ assert(!sec.content.includes('前端校验即可'), 'a project shadow of a core rule is ignored')
178
+ assert(sec.content.includes('服务端'), 'the bundled core rule body wins over a shadow')
179
+ }
180
+
181
+ // ── 12. session cwd drives every relative path (init regression) ──────────
182
+ {
183
+ const cwd = 'C:/users/ggbond/.dsh-verify/e2e-project'
184
+ const fs = makeFs({
185
+ [`${cwd}/AGENTS.md`]: '# e2e 项目治理文件\n两行\n',
186
+ 'D:/dsharness/deepseek-harness/AGENTS.md': '# DeepSeek Harness monorepo (packages/pnpm)\n',
187
+ })
188
+ const exe = await registered(fs)
189
+ const insp = await exe({ operation: 'init', phase: 'inspect' }, sessionExec(cwd))
190
+ assert(insp.includes('# e2e 项目治理文件'), 'inspect reads the session workspace AGENTS.md')
191
+ assert(!insp.includes('monorepo'), 'inspect does not read the harness repo AGENTS.md')
192
+ }
193
+
194
+ // ── 13. create writes the task record into the session workspace ──────────
195
+ {
196
+ const cwd = 'C:/users/ggbond/.dsh-verify/e2e-project'
197
+ const fs = makeFs({ [`${cwd}/.dsh/eng.json`]: JSON.stringify({ flow: 'standard' }) })
198
+ const exe = await registered(fs)
199
+ await exe({ operation: 'create', task_id: 'T12', title: 'x', branch: 'main' }, sessionExec(cwd))
200
+ assert(fs._files.get(`${cwd}/.dsh/task-T12.json`) !== undefined, 'task record lands in the session workspace')
201
+ assert(fs._files.get('.dsh/task-T12.json') === undefined, 'task record does not land in the backend base')
202
+ }
203
+
204
+ // ── 14. K: artifact ids must be unique across ALL stages ────────────────────
205
+ {
206
+ const withDup = { ...FLOW_PRESETS.standard.config, artifacts: [
207
+ { stage: '需求评审', id: 'doc', name: '需求', fields: ['scope'] },
208
+ { stage: '设计', id: 'doc', name: '设计', fields: ['approach'] },
209
+ ] }
210
+ const problems = validateWorkflow(withDup)
211
+ assert(problems.some(p => p.includes('duplicate artifact id "doc"')),
212
+ 'cross-stage duplicate artifact id is rejected')
213
+ }
214
+
215
+ // ── 15. K: record rejects an artifact owned by another stage ─────────────────
216
+ {
217
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
218
+ const exe = await registered(fs)
219
+ await exe({ operation: 'create', task_id: 'K1', title: 'x', branch: 'main' }, EXEC)
220
+ await assertThrows(
221
+ () => exe({ operation: 'record', task_id: 'K1', artifact: 'design', fields: { approach: 'a', risks: 'r', impact: 'i' } }, EXEC),
222
+ 'belongs to stage',
223
+ 'recording an artifact at a non-owning stage is rejected',
224
+ )
225
+ }
226
+
227
+ // ── 16. L/4.2: high-risk verification requires a REAL command receipt ────────
228
+ {
229
+ const cfg = FLOW_PRESETS.standard.config
230
+ const receipt = overrides => ({
231
+ command: 'npm test', exit_code: 0, timed_out: false, aborted: false,
232
+ started_at: '2026-01-01T00:00:00Z', finished_at: '2026-01-01T00:00:01Z',
233
+ stdout: '', stderr: '', ...overrides,
234
+ })
235
+ const mk = () => {
236
+ const s = newTask({ id: 'L1', title: 'x', branch: 'main', work_size: 'standard', risk_level: 'high_risk', flow: snapshot })
237
+ s.stage = '交付'
238
+ return s
239
+ }
240
+ let s = mk(); s.verification = { passed: true, evidence: ['单测通过'] }
241
+ assert(!assertAdvance(s, '代码审核', cfg).ok, 'high_risk with text-only evidence is blocked (no command receipt)')
242
+ s = mk(); s.verification = { passed: true, evidence: [], receipt: receipt({}) }
243
+ assert(assertAdvance(s, '代码审核', cfg).ok, 'high_risk with an exit-0 receipt passes the verified gate')
244
+ s = mk(); s.verification = { passed: true, evidence: [], receipt: receipt({ exit_code: 1 }) }
245
+ assert(!assertAdvance(s, '代码审核', cfg).ok, 'high_risk with a non-zero exit receipt is blocked')
246
+ s = mk(); s.verification = { passed: true, evidence: [], receipt: receipt({ timed_out: true }) }
247
+ assert(!assertAdvance(s, '代码审核', cfg).ok, 'high_risk with a timed-out receipt is blocked')
248
+ s = mk(); s.verification = { passed: true, evidence: [], receipt: receipt({ aborted: true }) }
249
+ assert(!assertAdvance(s, '代码审核', cfg).ok, 'high_risk with an aborted receipt is blocked')
250
+ }
251
+
252
+ // ── 17. J: task id extracted from the summary (the hook uses it, not a guess) ─
253
+ {
254
+ const cfg = FLOW_PRESETS.standard.config
255
+ assert(taskIdFromMessage('【GREET-001】【TASK】实现登录', cfg) === 'GREET-001', 'standard summary yields its task id')
256
+ assert(taskIdFromMessage('【GREET-001】【T1】实现登录', cfg) === 'GREET-001', 'item-label summary yields its task id')
257
+ assert(taskIdFromMessage('实现登录', cfg) === undefined, 'non-matching summary yields no task id')
258
+ assert(taskIdFromMessage('随意', FLOW_PRESETS.minimal.config) === undefined, 'pattern-less flow yields no task id')
259
+ }
260
+
261
+ // ── 18. N: engine bookkeeping files are exempt from file scope ───────────────
262
+ {
263
+ const s = newTask({ id: 'N1', title: 'x', branch: 'main', work_size: 'standard', risk_level: 'standard', flow: snapshot })
264
+ s.files = ['src/a.ts']
265
+ assert(checkFileScope(s, ['src/a.ts', '.dsh/task-N1.json', '.dsh/eng.json'], FLOW_PRESETS.standard.config).ok,
266
+ 'task record and eng.json do not violate file scope')
267
+ assert(!checkFileScope(s, ['src/b.ts'], FLOW_PRESETS.standard.config).ok,
268
+ 'an out-of-scope code file is still flagged')
269
+ }
270
+
271
+ // ── 19. H/I: hook reads raw-byte paths, NUL-separated, including deletions ───
272
+ {
273
+ const hook = readFileSync('./hooks/commit-msg', 'utf8')
274
+ assert(hook.includes('-c core.quotePath=false'), 'hook disables git octal path quoting')
275
+ assert(hook.includes('-z'), 'hook reads NUL-separated raw-byte paths')
276
+ assert(hook.includes('--diff-filter=ACMRDT'), 'hook includes deletions and type changes in the scope check')
277
+ }
278
+
279
+ // ── 20. 4.1/4.3: hook uses the frozen snapshot; writeInit guards overwrite ──
280
+ {
281
+ const hook = readFileSync('./hooks/commit-msg', 'utf8')
282
+ assert(hook.includes('config = state.flow.config'), 'hook checks the task frozen flow snapshot')
283
+ assert(hook.includes('function extractTaskId'), 'hook extracts the task id independent of the live config')
284
+ assert(!hook.includes('const FLOWS'), 'hook bundles the single source (workflows.ts), not a hand mirror')
285
+ }
286
+ {
287
+ const controller = readFileSync('./lib/controller.js', 'utf8')
288
+ assert(controller.includes('AGENTS.md 已存在且受保护'), 'workbench writeInit refuses to overwrite without an explicit intent')
289
+ assert(controller.includes('overwrite'), 'writeInit request carries an overwrite field')
290
+ }
291
+
292
+ console.log(`\nP0 acceptance: ${passed} checks passed`)
package/README.md CHANGED
@@ -9,7 +9,8 @@
9
9
  - **产物门**:每个阶段要交的产物 + 必填字段(需求说明 / 设计文档 / 评审记录…已固化在预设里),字段没填全 → 不让流转。
10
10
  - **提交门禁**:到检查点才能提交,消息必须匹配预设的提交格式,`manual` 策略永不自动提交。
11
11
  - **文件范围**:任务声明 `files`(本任务该改的文件),提交碰了范围外的文件 → 工具和 git 钩子都拒绝。
12
- - **skill / rule 挂载**:流程预设里每个阶段默认挂了「这个阶段用什么 skill」+「守哪条 rule」;项目可覆盖挂载,模型走到该阶段时,`dev_task` 只**渐进披露**这阶段挂载的内容,不一次性全给。内置一套通用库,也能在网页里新建自己的 skill/rule。
12
+ - **skill / rule 挂载**:流程预设里每个阶段默认挂了「这个阶段用什么 skill」+「守哪条 rule」;项目可在节点上**追加**自己的 skill/rule(内置绑定保留、不可移除,同名会被拒、只读内置版),模型走到该阶段时,`dev_task` 只**渐进披露**这阶段挂载的内容,不一次性全给。内置一套通用库,也能在网页里新建自己的 skill/rule。
13
+ - **边界(诚实说明)**:「需求确认」「方案确认」两扇门由**人工批准**点亮;但「实施项完成」「验证通过」「评审通过」是模型通过工具上报的状态,代码只校验这些状态在流程里自洽、格式/范围/绑定对齐,**不验证上报内容本身是否属实**——要硬保证请叠加外部 CI 或人工评审,不要把它当事实仲裁。
13
14
 
14
15
  ## 目录
15
16
 
@@ -74,7 +75,7 @@ dsh plugin --profile <name> add .
74
75
  name: '@godv61/dsh-task-engine/agent'
75
76
  ```
76
77
 
77
- 也可重跑 `dsh-task-engine-enable` 强制重建默认的 `eng`。
78
+ `dsh-task-engine-enable` 同样**只在 `eng` 不存在时创建**(已存在则直接退出、绝不覆盖);要重建请先删掉 `eng` 再重跑。
78
79
 
79
80
  ## 项目配置(选流程 + 挂 skill/rule)
80
81
 
@@ -93,7 +94,7 @@ dsh plugin --profile <name> add .
93
94
  - `flow`:选一套内置流程预设 —— `standard`(需求评审→设计→开发→交付→代码审核,含产物门+确认门)/ `agile`(需求→开发→交付→审查,四阶段少产物)/ `minimal`(开发→交付,只留提交门禁)。
94
95
  - `stage_bindings`:每个节点挂哪些 skill / rule(可整体省略,省略即用该预设自带的默认绑定)。
95
96
 
96
- **阶段图、流转守卫、产物字段、提交规则、验证证据开关全部固化在预设里,不再手工配置**。团队要改「提交消息格式」「交付要自测」这些约定,就覆盖内置的 `code-commit`/`code-verify` skill 和 `commit-conventions` rule(正文即规范),而不是改一堆参数。
97
+ **阶段图、流转守卫、产物字段、提交规则、验证证据开关全部固化在预设里,不再手工配置**。团队要改「提交消息格式」「交付要自测」这些约定,就新建一个项目级的新 skill/rule(用**新名字**,再挂到对应节点)——内置 skill/rule 的正文不可被同名覆盖(同名新建会被拒、解析同名只读内置版),新增的会与内置的一起挂在该节点上。
97
98
 
98
99
  `dev_task`(operation=config)随时查看当前生效配置;配置写错(未知 flow / 绑定到不存在的阶段 / 空名)会报错,不悄悄退回默认。
99
100
 
@@ -123,7 +124,7 @@ dsh plugin --profile <name> add .
123
124
  ```
124
125
 
125
126
  - **渐进披露**:模型走到某阶段时,`dev_task`(`status` / `advance`)只返回**这个阶段**挂载的 skill 名 + rule 内容;skill 用 DSH 的 `skill` 工具按名加载,rule 直接把正文给出。阶段推进到哪,才给哪。
126
- - **三层来源**:技能和规则都来自「内置 + 项目 + 用户」三层——内置(本包 `skills/`、`rules/`)、项目(`.dsh/skills/`、`.dsh/rules/`)、用户(`$DSH_HOME/skills/`、`$DSH_HOME/rules/`),同名时项目优先于用户优先于内置。
127
+ - **三层来源**:技能和规则都来自「内置 + 项目 + 用户」三层——内置(本包 `skills/`、`rules/`)、项目(`.dsh/skills/`、`.dsh/rules/`)、用户(`$DSH_HOME/skills/`、`$DSH_HOME/rules/`)。**同名时内置优先**:内置 skill/rule 不可被项目/用户同名覆盖(新建同名会被拒,解析同名只读内置版)。
127
128
  - **网页新建 / 查看 / 编辑 / 删除**:工作台「技能 / 规则」标签页里可新建、查看、编辑、删除项目级/用户级 skill 和 rule(内置只能查看、不可删改)。查看时正文以 markdown **富文本**渲染在宽弹窗里;编辑时正文保持纯文本。项目级写进 `.dsh/skills|rules/`(团队共享),用户级写进 `$DSH_HOME/skills|rules/`(个人全局),写好后自动出现在挂载清单里。
128
129
  - **内置库**:6 个节点技能(`requirement-analysis` / `solution-design` / `code-implement` / `code-verify` / `code-review` / `code-commit`)+ 3 条规则(`coding-conventions` / `commit-conventions` / `security-redlines`),开箱即用;想加项目专属的,用 DSH 原生技能机制或网页新建即可。
129
130
 
@@ -172,4 +173,6 @@ dsh plugin --profile <name> add .
172
173
  - **工程可靠性加固(0.18.0)**:① 高风险任务与流程能力绑定——`high_risk` 任务只能在具备「验证门 + 文件范围 + 评审门」能力的流程创建,选 `agile`/`minimal` 直接拒绝;② 任务创建时固化流程快照(预设 id + version + 完整配置),后续 `status`/`advance`/`verify`/`review`/`commit` 一律用快照,中途改 `.dsh/eng.json` 不再漂移在途任务的门禁;③ 未知流程失败关闭——`.dsh/eng.json` 缺 `flow` 或 `flow` 不在预设里一律报错(`UNKNOWN_FLOW` / 缺字段),不再静默回退 `standard`;④ 核心 skill/rule 不可取消、不可被同名覆盖——项目阶段绑定只能追加不能移除预设自带绑定,同名规则/技能读内置版、新建同名被拒;⑤ `init` 升级 `inspect → propose → apply` 三阶段,覆盖已有 `AGENTS.md` 需人工批准。
173
174
  - **会话工作目录修复(0.18.1)**:`dev_task` 的所有文件操作此前用 `fs.resolve(相对路径)` 不带 cwd,落到了 fs 后端默认目录(DSH 进程目录)而非会话工作区——在 web 会话里会把台账、配置、`AGENTS.md`、git 钩子写到/读到错误位置。改为从 `exec.agent.session.header.cwd` 取会话工作区并传给每个解析,补 4 项回归测试。
174
175
  - **工作台项目初始化(0.19.0)**:工作台新增置顶的「项目初始化」标签页——加载展示项目根 `AGENTS.md`、一键让 AI 扫描项目生成草稿(预览后再确认写回)、支持手动编辑与覆盖;Host 控制器新增 `readInit`/`writeInit`/`generateInit` 三个 Remote,`generateInit` 通过 `ctx.llm` + 默认模型在 Host 端直接生成,复用 `dev_task init` 的 200 行硬约束。
175
- - **使用手册跟进(0.19.1)**:随包发布的 `docs/manual.html` 补上工作台「项目初始化」(默认置顶标签页、项目根自动发现、AI 生成 150 秒超时 + 覆盖需人工确认),第 7 节标签页从「三个」改为「五个」;删除顶层已废弃的 `USER_GUIDE.html`(v0.9.1、无引用、不随包发布),README 目录结构描述同步为五标签页。
176
+ - **使用手册跟进(0.19.1)**:随包发布的 `docs/manual.html` 补上工作台「项目初始化」(默认置顶标签页、项目根自动发现、AI 生成 150 秒超时 + 覆盖需人工确认),第 7 节标签页从「三个」改为「五个」;安装章节补全 dsh CLI(非源码)安装方式与 pnpm 前置、`dsh plugin add` 的挂载机制;删除顶层已废弃的 `USER_GUIDE.html`(v0.9.1、无引用、不随包发布),README 目录结构描述同步为五标签页。
177
+ - **审计修复(0.19.2)**:补齐七项——① 提交钩子 `stagedFiles` 用 `core.quotePath=false` + `-z` 按 NUL 拆分(中文文件名不再被八进制转义误拒)并补 `D`(删除范围外文件也被拦);② 提交消息第一段改为 task id、钩子按 id 精确定位任务(不再按分支/mtime 猜,同分支多任务不再锁错);③ artifact id 全流程唯一 + `record` 校验产物属于当前阶段(堵越阶段复用);④ 高风险验证证据 `trim()` 后须非空(`evidence:[""]` 不再通过);⑤ README/手册加「诚实边界」,明说验证/评审/实施项是模型自报、需人工或 CI 兜底;⑥ 文档修正优先级(内置 &gt; 用户 &gt; 项目、内置不可覆盖)与 enable 措辞,`.dsh/task-*.json`/`eng.json` 豁免文件范围门;⑦ 提交钩子改用任务快照的 frozen 配置校验(对抗任务执行中改流程导致的配置漂移);⑧ 工作台 `writeInit` 对齐 `init` 保护(已有 `AGENTS.md` 时需显式 overwrite + 前端确认才覆盖);⑨ `.p0-test.mjs` 纳入发布包,装包后 `npm test` 可用。新增回归测试。
178
+ - **安全闭环(0.20.0)**:① 验证改真实命令回执——`dev_task verify` 新增 `command` 入参,引擎通过宿主 shell 服务真实运行该命令并落 `VerificationReceipt`(命令 / 退出码 / 超时 / 中止 / 起止时间 / stdout / stderr);`high_risk` 任务的 `verified` 门改为要求回执 `exit_code === 0` 且非超时 / 中止,纯文本 `passed` 声明不再放行(常规风险仍可用 `passed` + `evidence` 文本);② 文件范围检查补 `T` + 改用 `--name-status`——`--diff-filter=ACMRDT` 纳入类型变换,提交状态随范围拒绝一并报出,删除 / 类型变换 / 重命名的旧·新路径都受范围检查;③ 提交钩子改为从 `engine.ts` / `workflows.ts` 单一源打包生成(`build-hook.mjs`),彻底消除手写镜像漂移,未知流程在钩子侧同样 fail-closed(不再静默回退 `standard`)。
package/docs/manual.html CHANGED
@@ -80,22 +80,32 @@
80
80
  <p><strong>它不是常驻程序。</strong>只有你在「工程化开发引擎」预设的会话里提出开发请求时,才触发 <code>dev_task</code> 和这套门禁;换到别的预设,流程完全不介入。</p>
81
81
  </div>
82
82
  <p>三样东西分工明确:<strong>流程预设</strong>决定「走哪条流水线、有哪些守卫」,<strong>Skill</strong> 决定「这一站做什么」,<strong>Rule</strong> 决定「这一站守什么」。任务状态单独落盘,负责「跨会话恢复到哪一步」。</p>
83
+ <div class="callout warn">
84
+ <p><strong>诚实边界:</strong>阶段流转、提交格式、文件范围、消息里的任务绑定是<b>代码硬校验</b>(不匹配直接拒绝);<b>高风险任务的「验证通过」也是真实命令回执</b>——引擎实际运行 <code>verify</code> 命令、取退出码(<code>exit_code === 0</code> 且非超时/中止)才放行,不是模型自报。仍属模型自报、需人工或 CI 兜底的是:常规风险的验证声明、「评审通过」「实施项完成」,以及回执命令本身的覆盖面。只有「需求确认」「方案确认」两扇门由人工批准点亮。</p>
85
+ </div>
83
86
  </section>
84
87
 
85
88
  <section id="install">
86
89
  <h2>2. 安装与启用</h2>
87
- <h3>先装 DSH(二选一)</h3>
88
- <pre><code># 方式 A:npm 一条命令(需要 Node.js ≥ 22)
90
+ <h3>2.1 前置:Node.js ≥ 22 + pnpm</h3>
91
+ <pre><code>npm install -g pnpm # dsh 的 plugin 子命令底层转发给 pnpm,必须先装</code></pre>
92
+ <h3>2.2 先装 DSH(三选一)</h3>
93
+ <pre><code># 方式 A:npm 装 CLI(非源码,推荐给使用者)
94
+ npm install -g @deepseek-ai/dsh
95
+ dsh web
96
+
97
+ # 方式 B:npx 免安装直接跑
89
98
  npx @deepseek-ai/dsh web
90
99
 
91
- # 方式 B:从源码 clone
100
+ # 方式 C:从源码 clone(开发者)
92
101
  git clone https://github.com/deepseek-ai/deepseek-harness.git
93
102
  cd deepseek-harness
94
103
  pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
95
- <h3>再装引擎插件</h3>
104
+ <h3>2.3 把引擎插件装进 profile</h3>
96
105
  <pre><code>dsh plugin --profile web add @godv61/dsh-task-engine</code></pre>
97
- <p>重启 Web 后自动完成两件事:侧边栏多出「工程流程」工作台;预设列表多出「工程化开发引擎」。</p>
98
- <h3>启用</h3>
106
+ <p><code>dsh plugin --profile &lt;name&gt; add &lt;package&gt;</code> 会在 <code>$DSH_HOME/profiles/&lt;name&gt;/</code> 里执行 <code>pnpm add</code>,并自动识别插件声明的 <code>dsh.bundle.patch</code>,把它挂进该 profile 的插件层栈——<b>不要</b>手写 <code>npm i</code> 装到别处,那样不会挂载。</p>
107
+ <p>装完<b>重启 <code>dsh web</code></b> 生效,自动完成两件事:侧边栏多出「工程流程」工作台;预设列表多出「工程化开发引擎」。</p>
108
+ <h3>2.4 启用</h3>
99
109
  <p>新建会话 → 预设选「工程化开发引擎」。这个会话就挂上 <code>dev_task</code> + 内置技能 + 工程人设,开始按流程走。</p>
100
110
  <div class="callout"><p>想「正式开发」就选它;想「让 AI 自由探索」就选 <code>standard</code>。切换预设本身就是开关,零配置。</p></div>
101
111
  </section>
@@ -120,7 +130,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
120
130
  <tr><td><code>.dsh/skills/</code></td><td>项目级 skill(团队共享)</td><td>节点挂载命中时按需读取</td></tr>
121
131
  <tr><td><code>.dsh/rules/</code></td><td>项目级 rule(团队共享)</td><td>节点挂载命中时按需读取</td></tr>
122
132
  <tr><td><code>.dsh/task-*.json</code></td><td>当前任务的有效状态快照</td><td>确认、实施、验证、评审、切换阶段时更新</td></tr>
123
- <tr><td><code>$DSH_HOME/skills/ · rules/</code></td><td>用户级 skill / rule</td><td>同名时优先于项目级、内置</td></tr>
133
+ <tr><td><code>$DSH_HOME/skills/ · rules/</code></td><td>用户级 skill / rule</td><td>同名时让位于内置(内置 &gt; 用户 &gt; 项目)</td></tr>
124
134
  </tbody>
125
135
  </table>
126
136
  </div>
@@ -157,7 +167,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
157
167
  <tr><td><code>requirement-analysis</code></td><td>目标、验收、非目标,落需求说明,等人确认</td><td>编码</td></tr>
158
168
  <tr><td><code>solution-design</code></td><td>最小方案、技术基线、改动点,落设计文档</td><td>实现</td></tr>
159
169
  <tr><td><code>code-implement</code></td><td>实现、做「规格 + 质量」两阶段评审,都过才标完成</td><td>无关重构</td></tr>
160
- <tr><td><code>code-verify</code></td><td>按验收与风险验证、记录证据(高风险必须带证据)</td><td>掩盖失败或自动修复</td></tr>
170
+ <tr><td><code>code-verify</code></td><td>按验收与风险验证、记录结果(高风险必须跑真实命令回执)</td><td>掩盖失败或自动修复</td></tr>
161
171
  <tr><td><code>code-commit</code></td><td>校验阶段、范围、消息格式后提交</td><td>远程 push / 合并 / 发布</td></tr>
162
172
  <tr><td><code>code-review</code></td><td>评审变更,落结论与问题清单</td><td>代替实现或验证</td></tr>
163
173
  </tbody>
@@ -178,7 +188,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
178
188
  </tbody>
179
189
  </table>
180
190
  </div>
181
- <p>最重要的轻量原则:团队想改「提交消息格式」「交付要自测」这类约定,直接覆盖对应 rule / skill 的正文,而不是改一堆参数。</p>
191
+ <p>最重要的轻量原则:团队想改「提交消息格式」「交付要自测」这类约定,新建一个项目级规则 / 技能(用<b>新名字</b>)再挂到对应节点,而不是改一堆参数;内置 skill / rule 的正文<b>不可被同名覆盖</b>。</p>
182
192
  </section>
183
193
 
184
194
  <section id="configure">
@@ -198,7 +208,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
198
208
  }
199
209
  }</code></pre>
200
210
  <div class="callout warn"><p>页面实时校验:挂到不存在的阶段、空名等会红字提示并置灰保存;保存前主机再校验一遍。错误的配置存不进去。</p></div>
201
- <p>三层来源与优先级:同名 skill / rule,<strong>项目级 &gt; 用户级 &gt; 内置</strong>。想改内置某条,新建同名项目级版本即可顶替,内置留着兜底。</p>
211
+ <p>三层来源与优先级:同名 skill / rule,<strong>内置 &gt; 用户 &gt; 项目</strong>——内置不可被同名覆盖(新建同名会被拒,解析同名只读内置版)。要加团队约定,新建一个<b>不同名</b>的项目级 / 用户级资源再挂到节点上。</p>
202
212
  </section>
203
213
 
204
214
  <section id="init">
@@ -252,7 +262,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
252
262
  <tr><td>需求评审</td><td>拆需求、落「需求说明」</td><td>字段填全,且<b>人点「允许」确认需求</b></td></tr>
253
263
  <tr><td>设计</td><td>出最小方案、落「设计文档」</td><td>字段填全,且<b>人点「允许」确认方案</b></td></tr>
254
264
  <tr><td>开发</td><td>逐项实现,每项做规格 + 质量两阶段评审</td><td>实施项非空且全部 done(每项带两阶段评审)</td></tr>
255
- <tr><td>交付</td><td>验证、记证据,按门禁提交</td><td>验证通过(高风险必须带证据)</td></tr>
265
+ <tr><td>交付</td><td>验证、记证据,按门禁提交</td><td>验证通过(高风险必须真实命令回执,退出码 0)</td></tr>
256
266
  <tr><td>代码审核</td><td>评审变更,落「评审记录」</td><td>结论通过且字段填全</td></tr>
257
267
  <tr><td>完成</td><td>收尾</td><td>—</td></tr>
258
268
  </tbody>
@@ -280,7 +290,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
280
290
  <tr><td><code>risk_level</code></td><td>standard / high_risk,决定验证强度</td></tr>
281
291
  <tr><td><code>requirement_confirmed</code></td><td>需求是否已由人确认</td></tr>
282
292
  <tr><td><code>solution_confirmed</code></td><td>方案是否已由人确认</td></tr>
283
- <tr><td><code>verification</code></td><td>验证是否通过 + 证据</td></tr>
293
+ <tr><td><code>verification</code></td><td>验证是否通过 + 证据(高风险附真实命令回执)</td></tr>
284
294
  <tr><td><code>review</code></td><td>评审结论(通过 / 未过 + 问题)</td></tr>
285
295
  <tr><td><code>files</code></td><td>本任务允许修改的文件范围</td></tr>
286
296
  <tr><td><code>commits</code></td><td>已创建的提交记录</td></tr>
@@ -297,7 +307,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
297
307
  <li>实施中优先受影响模块的编译、目标测试与静态检查。</li>
298
308
  <li>差异没变时复用有效证据,不重复跑相同命令。</li>
299
309
  <li>无法联调外部系统时必须如实写「未联调」,不能把编译成功说成功能通过。</li>
300
- <li><code>high_risk</code> 任务过验证门必须附证据。</li>
310
+ <li><code>high_risk</code> 任务过验证门必须跑真实命令回执(引擎运行命令、取退出码,非自报通过)。</li>
301
311
  </ul>
302
312
  <h3>评审</h3>
303
313
  <p>开发阶段每项做「规格 + 质量」两阶段评审,都 pass 才标 done;缺评审或任一阶段 fail 会挡住「开发 → 交付」。</p>
@@ -336,7 +346,7 @@ risk_level: high_risk // 涉及鉴权,验证要加证据
336
346
  - 规格 ✓ 质量 ✓ → done
337
347
 
338
348
  ④ 交付
339
- - 验证:跑鉴权用例,记录通过证据(high_risk 必须)。
349
+ - 验证:跑鉴权用例,`verify` 传真实命令(high_risk 由引擎取退出码判定)。
340
350
  - 提交:【模块】【TASK】查询接口增加权限校验(只含本任务文件)
341
351
 
342
352
  ⑤ 代码审核
@@ -352,8 +362,8 @@ risk_level: high_risk // 涉及鉴权,验证要加证据
352
362
  <p>引擎只允许:读取仓库事实、修改<b>已确认任务范围内</b>的文件、按策略创建<b>范围受控的本地提交</b>。远程 push / 合并 / 发布、数据库写入,需人单独决定。</p>
353
363
  </div>
354
364
  <details open><summary>切换预设就是开关吗?</summary><div>是。选「工程化开发引擎」才挂 <code>dev_task</code> 和内置技能;选 <code>standard</code> 等其它预设则完全不介入。</div></details>
355
- <details><summary>想改内置 skill / rule 怎么办?</summary><div>不删内置(升级又回来)。新建一个<b>同名</b>的项目级 skill / rule,系统自动用你这份,内置留着兜底。</div></details>
356
- <details><summary>任务文档要不要提交?</summary><div>要。<code>.dsh/task-*.json</code> 是团队共享、跨会话恢复的任务事实,随分支提交;忽略它别人只能看到代码,看不到确认内容和进度。</div></details>
365
+ <details><summary>想改内置 skill / rule 怎么办?</summary><div>内置正文不可被同名覆盖(同名新建会被拒)。要加团队约定,新建一个<b>不同名</b>的项目级 skill / rule 再挂到对应节点,与内置并存。</div></details>
366
+ <details><summary>任务文档要不要提交?</summary><div>要。<code>.dsh/task-*.json</code> 是团队共享、跨会话恢复的任务事实,随分支提交——引擎已豁免 <code>.dsh/task-*.json</code> 与 <code>.dsh/eng.json</code>,不受文件范围门拦截;忽略它别人只能看到代码,看不到确认内容和进度。</div></details>
357
367
  <details><summary>一个会话能同时做两个需求吗?</summary><div>一个任务对应一份状态文档、一个分支。新需求保存当前进度后切新分支,切回原分支即可恢复。</div></details>
358
368
  <details><summary>提交被拒是怎么回事?</summary><div>通常是四类:没建 dev_task 任务、阶段没到提交检查点、消息格式不符、提交文件不在任务范围内。按拒绝原因修正即可。</div></details>
359
369
  <details><summary>「完成」等于上线了吗?</summary><div>不等于。<code>done</code> 只表示本地验证与必要评审通过;上线、合并、发布需人另行决定。</div></details>