@godv61/dsh-task-engine 0.27.1 → 0.29.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.
Files changed (63) hide show
  1. package/.acceptance.mjs +11 -11
  2. package/.adaptive-test.mjs +189 -0
  3. package/.assessment-batch1.mjs +14 -14
  4. package/.codex-project-test.mjs +80 -80
  5. package/.enforce-test.mjs +12 -12
  6. package/.evidence-test.mjs +8 -8
  7. package/.filter-test.mjs +6 -6
  8. package/.freeze-test.mjs +44 -44
  9. package/.hook-test.mjs +19 -2
  10. package/.p0-test.mjs +21 -21
  11. package/.preset-test.mjs +11 -1
  12. package/.revision-test.mjs +16 -16
  13. package/.roundtrip-test.mjs +247 -247
  14. package/.workflow-test.mjs +122 -120
  15. package/README.md +33 -27
  16. package/cordis.patch.yml +158 -9
  17. package/docs/CHANGELOG.md +44 -30
  18. package/docs/README.md +6 -5
  19. package/docs/adaptive-workflows.md +72 -0
  20. package/docs/configuration.md +25 -25
  21. package/docs/development.md +1 -1
  22. package/docs/faq.md +14 -6
  23. package/docs/getting-started.md +8 -5
  24. package/docs/manual-legacy.html +380 -0
  25. package/docs/manual.html +124 -378
  26. package/docs/resource-install.md +5 -5
  27. package/docs/roadmap.md +8 -9
  28. package/hooks/commit-msg +39 -22
  29. package/lib/adaptive.d.ts +15 -0
  30. package/lib/adaptive.js +54 -0
  31. package/lib/adaptive.js.map +1 -0
  32. package/lib/client.js +719 -433
  33. package/lib/client.js.map +3 -3
  34. package/lib/controller.d.ts +50 -0
  35. package/lib/controller.js +132 -0
  36. package/lib/controller.js.map +1 -1
  37. package/lib/dev-task.js +413 -18
  38. package/lib/dev-task.js.map +1 -1
  39. package/lib/engine.d.ts +20 -0
  40. package/lib/engine.js +4 -1
  41. package/lib/engine.js.map +1 -1
  42. package/lib/hook.js +50 -29
  43. package/lib/hook.js.map +1 -1
  44. package/lib/project-init.d.ts +23 -0
  45. package/lib/project-init.js +94 -0
  46. package/lib/project-init.js.map +1 -0
  47. package/lib/skill-audit.js +8 -7
  48. package/lib/skill-audit.js.map +1 -1
  49. package/lib/sonar.d.ts +33 -0
  50. package/lib/sonar.js +80 -0
  51. package/lib/sonar.js.map +1 -0
  52. package/package.json +15 -16
  53. package/preset/agent.cordis.yml +3 -3
  54. package/preset/enable.mjs +2 -2
  55. package/preset/persona.md +4 -2
  56. package/scripts/verify-package.mjs +9 -5
  57. package/skills/architecture-design/SKILL.md +11 -0
  58. package/skills/code-development/SKILL.md +11 -0
  59. package/skills/code-review/SKILL.md +11 -0
  60. package/skills/eng-delivery/SKILL.md +19 -15
  61. package/skills/requirements-analysis/SKILL.md +11 -0
  62. package/skills/task-orchestration/SKILL.md +11 -0
  63. package/skills/test-validation/SKILL.md +11 -0
package/.freeze-test.mjs CHANGED
@@ -13,23 +13,23 @@ import { registerDevTask } from './lib/dev-task.js'
13
13
  import { hashText } from './lib/snapshot.js'
14
14
 
15
15
  /** Create one task through the real tool and return its frozen snapshot. */
16
- async function created(flow) {
17
- const cwd = resolve('freeze-project')
18
- const stage = { standard: '需求评审', agile: '需求', minimal: '开发' }[flow]
19
- const shared = { source: 'project', name: 'shared-rule' }
20
- const adopted = adoptRecommendation(flow)
21
- const project = { ...adopted, stage_bindings: { [stage]: { skills: [
22
- { skill: { source: 'project', name: 'first-skill' }, rules: [shared], evidence: 'none' },
23
- { skill: { source: 'project', name: 'second-skill' }, rules: [shared], evidence: 'none' },
24
- ] } } }
25
- const records = new Map([
16
+ async function created(flow) {
17
+ const cwd = resolve('freeze-project')
18
+ const stage = { standard: '需求评审', agile: '需求', minimal: '开发' }[flow]
19
+ const shared = { source: 'project', name: 'shared-rule' }
20
+ const adopted = adoptRecommendation(flow)
21
+ const project = { ...adopted, stage_bindings: { [stage]: { skills: [
22
+ { skill: { source: 'project', name: 'first-skill' }, rules: [shared], evidence: 'none' },
23
+ { skill: { source: 'project', name: 'second-skill' }, rules: [shared], evidence: 'none' },
24
+ ] } } }
25
+ const records = new Map([
26
26
  // A project that adopted the shipped recommendation, which is what gives it
27
27
  // skills and rules to freeze. `{flow}` alone adopts nothing and therefore has
28
28
  // nothing to freeze — a legitimate state, but not the one under test.
29
- [join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
30
- [join(cwd, '.dsh/skills/first-skill/SKILL.md'), '# First skill'],
31
- [join(cwd, '.dsh/skills/second-skill/SKILL.md'), '# Second skill'],
32
- [join(cwd, '.dsh/rules/shared-rule.md'), '# Shared rule'],
29
+ [join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
30
+ [join(cwd, '.dsh/skills/first-skill/SKILL.md'), '# First skill'],
31
+ [join(cwd, '.dsh/skills/second-skill/SKILL.md'), '# Second skill'],
32
+ [join(cwd, '.dsh/rules/shared-rule.md'), '# Shared rule'],
33
33
  [join(cwd, 'a.js'), 'source'],
34
34
  ])
35
35
  const events = []
@@ -85,13 +85,13 @@ test('freeze: a frozen body carries its content and a hash of that content', asy
85
85
  })
86
86
 
87
87
  test('freeze: shared resources are stored once, not duplicated per reference', async () => {
88
- // One project rule is referenced by two skills; the body is stored once.
88
+ // One project rule is referenced by two skills; the body is stored once.
89
89
  const state = await created('standard')
90
90
  const resources = state.flow.resources ?? []
91
91
  const hashes = resources.map(r => r.hash)
92
92
  assert.equal(new Set(hashes).size, hashes.length,
93
93
  'identical bodies must not be stored more than once')
94
- assert.ok(resources.some(r => r.ref.name === 'shared-rule'),
94
+ assert.ok(resources.some(r => r.ref.name === 'shared-rule'),
95
95
  'the shared rule must be present')
96
96
  })
97
97
 
@@ -113,13 +113,13 @@ test('freeze: every preset freezes a complete, non-empty set', async () => {
113
113
  async function atDevelopmentWithProjectRule() {
114
114
  const cwd = resolve('freeze-live')
115
115
  const adopted = adoptRecommendation('standard')
116
- const skills = [{ skill: { source: 'project', name: 'my-implementation' },
117
- rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
116
+ const skills = [{ skill: { source: 'project', name: 'my-implementation' },
117
+ rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
118
118
  const project = { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }
119
119
  const records = new Map([
120
120
  [join(cwd, '.dsh/eng.json'), JSON.stringify(project)],
121
- [join(cwd, '.dsh/rules/mine.md'), 'ORIGINAL RULE'],
122
- [join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '# Implementation skill'],
121
+ [join(cwd, '.dsh/rules/mine.md'), 'ORIGINAL RULE'],
122
+ [join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '# Implementation skill'],
123
123
  [join(cwd, 'a.js'), 'source'],
124
124
  ])
125
125
  let execute
@@ -159,29 +159,29 @@ async function atDevelopmentWithProjectRule() {
159
159
  }
160
160
  }
161
161
 
162
- test('live: a running task reads the latest rule body at the same reference', async () => {
163
- const f = await atDevelopmentWithProjectRule()
164
- const before = await f.disclosed()
165
- assert.equal(before.mine?.content, 'ORIGINAL RULE')
166
-
167
- f.records.set(f.rule(), 'CHANGED RULE')
168
- const after = await f.disclosed()
169
- assert.equal(after.mine?.content, 'CHANGED RULE',
170
- 'the next interaction must disclose the edited body')
171
- assert.ok((after.status.stale_source_rules ?? []).some(r => r.includes('edited')),
172
- 'the difference from the creation-time audit copy must be visible')
173
- assert.match(after.status.resource_update_notice, /current source body is in force/u)
174
- })
175
-
176
- test('live: deleting a source rule makes the binding unresolved', async () => {
177
- const f = await atDevelopmentWithProjectRule()
178
- f.records.delete(f.rule())
179
- const { status, mine } = await f.disclosed()
180
- assert.equal(mine, undefined)
181
- assert.ok(status.missing_rules.includes('project:mine'))
182
- assert.ok((status.stale_source_rules ?? []).some(r => r.includes('deleted')),
183
- 'the deletion is also reported against the creation-time audit copy')
184
- })
162
+ test('live: a running task reads the latest rule body at the same reference', async () => {
163
+ const f = await atDevelopmentWithProjectRule()
164
+ const before = await f.disclosed()
165
+ assert.equal(before.mine?.content, 'ORIGINAL RULE')
166
+
167
+ f.records.set(f.rule(), 'CHANGED RULE')
168
+ const after = await f.disclosed()
169
+ assert.equal(after.mine?.content, 'CHANGED RULE',
170
+ 'the next interaction must disclose the edited body')
171
+ assert.ok((after.status.stale_source_rules ?? []).some(r => r.includes('edited')),
172
+ 'the difference from the creation-time audit copy must be visible')
173
+ assert.match(after.status.resource_update_notice, /current source body is in force/u)
174
+ })
175
+
176
+ test('live: deleting a source rule makes the binding unresolved', async () => {
177
+ const f = await atDevelopmentWithProjectRule()
178
+ f.records.delete(f.rule())
179
+ const { status, mine } = await f.disclosed()
180
+ assert.equal(mine, undefined)
181
+ assert.ok(status.missing_rules.includes('project:mine'))
182
+ assert.ok((status.stale_source_rules ?? []).some(r => r.includes('deleted')),
183
+ 'the deletion is also reported against the creation-time audit copy')
184
+ })
185
185
 
186
186
  test('freeze: an unfrozen task falls back to live resolution and reports a missing file', async () => {
187
187
  // Tasks created before freezing have no snapshot. They must keep working, and a
@@ -200,4 +200,4 @@ test('freeze: an unfrozen task falls back to live resolution and reports a missi
200
200
  assert.equal(mine, undefined, 'with no snapshot and no file, the rule cannot resolve')
201
201
  assert.ok((status.missing_rules ?? []).includes('project:mine'),
202
202
  'it is reported as missing so the gate can block on it')
203
- })
203
+ })
package/.hook-test.mjs CHANGED
@@ -113,7 +113,7 @@ async function standardConfig() {
113
113
  return resolved.config
114
114
  }
115
115
 
116
- test('a task at its checkpoint with the right message commits', async () => {
116
+ test('a task at its checkpoint with the right message commits', async () => {
117
117
  const config = await standardConfig()
118
118
  const root = repo('allow', approvedTask({ flow: { flow: 'standard', version: 2, config } }))
119
119
  try {
@@ -193,4 +193,21 @@ test('touching a sensitive path without a high-risk receipt is refused', async (
193
193
  } finally {
194
194
  rmSync(root, { recursive: true, force: true })
195
195
  }
196
- })
196
+ })
197
+
198
+ test('adaptive task can make its first CI commit before Sonar review', async () => {
199
+ const { adaptiveWorkflow } = await import('./lib/adaptive.js')
200
+ const config = adaptiveWorkflow('medium')
201
+ const root = repo('adaptive-sonar', approvedTask({
202
+ stage: '测试',
203
+ review: { outcome: 'pending' },
204
+ sonar_policy: { enabled: true, host_url: 'https://sonar.example.test', project_key: 'example', mode: 'branch', token_env: 'SONAR_TOKEN' },
205
+ flow: { flow: 'adaptive-medium', version: 1, config },
206
+ }))
207
+ try {
208
+ const result = tryCommit(root, 'first CI scan')
209
+ assert.equal(result.allowed, true, `the commit must precede its Sonar analysis: ${result.stderr}`)
210
+ } finally {
211
+ rmSync(root, { recursive: true, force: true })
212
+ }
213
+ })
package/.p0-test.mjs CHANGED
@@ -151,21 +151,21 @@ const emptied = resolveFlow('standard', { flow: 'standard', stage_bindings: { '
151
151
  assert(emptied.ok && (emptied.config.stage_bindings['开发'].skills ?? []).length === 0,
152
152
  'a project that binds nothing to a stage keeps it empty; nothing is merged back in')
153
153
 
154
- const adopted = adoptedFlow('standard')
155
- assert(Object.keys(adopted.stage_bindings ?? {}).length === 0,
156
- 'adopting the recommendation does not supply skills')
154
+ const adopted = adoptedFlow('standard')
155
+ assert(Object.keys(adopted.stage_bindings ?? {}).length === 0,
156
+ 'adopting the recommendation does not supply skills')
157
157
 
158
- // And a user who gives a project skill their own rule list gets that list — the
158
+ // And a user who gives a project skill their own rule list gets that list — the
159
159
  // defect this replaces was that the entry was skipped as a duplicate reference.
160
160
  const ownRules = resolveFlow('standard', {
161
161
  flow: 'standard',
162
162
  stage_bindings: {
163
- '开发': { skills: [{ skill: { source: 'project', name: 'my-implementation' }, rules: [{ source: 'project', name: 'only-mine' }] }] },
163
+ '开发': { skills: [{ skill: { source: 'project', name: 'my-implementation' }, rules: [{ source: 'project', name: 'only-mine' }] }] },
164
164
  },
165
165
  }).config
166
- const implementBinding = ownRules.stage_bindings['开发'].skills.find(entry => entry.skill.name === 'my-implementation')
166
+ const implementBinding = ownRules.stage_bindings['开发'].skills.find(entry => entry.skill.name === 'my-implementation')
167
167
  assert(implementBinding.rules.length === 1 && implementBinding.rules[0].name === 'only-mine',
168
- 'a project rule list for a project skill is used as given, not discarded as a duplicate')
168
+ 'a project rule list for a project skill is used as given, not discarded as a duplicate')
169
169
 
170
170
  // ── 4. newTask freezes the snapshot ────────────────────────────────────────
171
171
  const snapshot = { flow: 'standard', version: 1, config: adoptedFlow('standard') }
@@ -267,20 +267,20 @@ await assertThrows(
267
267
  // is no chain to walk and no shadowing to defend against: the same name in two
268
268
  // layers is two distinct resources, and the binding says which one it means.
269
269
  {
270
- const fs = makeFs({
270
+ const fs = makeFs({
271
271
  // A project file with the same NAME but a different body. Under the old
272
272
  // bare-name lookup this would have replaced the bundled rule silently.
273
273
  '.dsh/eng.json': JSON.stringify({
274
274
  flow: 'standard',
275
275
  stage_bindings: {
276
276
  '需求评审': { skills: [{
277
- skill: { source: 'project', name: 'requirement-analysis' },
277
+ skill: { source: 'project', name: 'requirement-analysis' },
278
278
  rules: [{ source: 'project', name: 'security-redlines' }],
279
279
  }] },
280
280
  },
281
281
  }),
282
- '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
283
- '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
282
+ '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
283
+ '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
284
284
  })
285
285
  const exe = await registered(fs)
286
286
  await exe({ operation: 'create', task_id: 'T9', title: 'x', branch: 'main' }, EXEC)
@@ -291,24 +291,24 @@ await assertThrows(
291
291
  assert(sec.content.includes('前端校验即可'), 'the PROJECT body is what gets disclosed, because the reference names the project layer')
292
292
  }
293
293
 
294
- // ── 11b. a deleted bundled rule is reported as missing ───────────────────────
294
+ // ── 11b. a deleted bundled rule is reported as missing ───────────────────────
295
295
  {
296
296
  const fs = makeFs({
297
297
  '.dsh/eng.json': JSON.stringify({
298
298
  flow: 'standard',
299
299
  stage_bindings: {
300
300
  '需求评审': { skills: [{
301
- skill: { source: 'project', name: 'requirement-analysis' },
301
+ skill: { source: 'project', name: 'requirement-analysis' },
302
302
  rules: [{ source: 'bundled', name: 'security-redlines' }],
303
303
  }] },
304
304
  },
305
305
  }),
306
- '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
307
- '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
306
+ '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
307
+ '.dsh/skills/requirement-analysis/SKILL.md': '# Project requirement analysis',
308
308
  })
309
309
  const exe = await registered(fs)
310
- await assertThrows(() => exe({ operation: 'create', task_id: 'T10', title: 'x', branch: 'main' }, EXEC),
311
- 'unreadable', 'a reference to a removed bundled rule fails closed')
310
+ await assertThrows(() => exe({ operation: 'create', task_id: 'T10', title: 'x', branch: 'main' }, EXEC),
311
+ 'unreadable', 'a reference to a removed bundled rule fails closed')
312
312
  }
313
313
 
314
314
  // ── 12. session cwd drives every relative path (init regression) ──────────
@@ -599,10 +599,10 @@ function memProbe(files) {
599
599
  const exe = await registered(fs)
600
600
  await exe({ operation: 'create', task_id: 'FP-1', title: 'x', branch: 'main' }, EXEC)
601
601
  const status = JSON.parse(await exe({ operation: 'status', task_id: 'FP-1' }, EXEC))
602
- assert(status.bindings_drift === undefined, 'fresh task has no bundled-rule drift marker')
603
- const record = JSON.parse(fs._files.get('.dsh/task-FP-1.json'))
604
- assert(record.bindings_fingerprint === undefined,
605
- 'create does not record a fingerprint for removed bundled rules')
602
+ assert(status.bindings_drift === undefined, 'fresh task has no bundled-rule drift marker')
603
+ const record = JSON.parse(fs._files.get('.dsh/task-FP-1.json'))
604
+ assert(record.bindings_fingerprint === undefined,
605
+ 'create does not record a fingerprint for removed bundled rules')
606
606
  }
607
607
 
608
608
  // ── 26. 0.22-C: multi-language verify command chain + parallel tasks ────────
package/.preset-test.mjs CHANGED
@@ -158,4 +158,14 @@ test('the rewritten persona row keeps valid indentation for both shapes', () =>
158
158
  const legacy = applyPersona(PERSONA_FIXTURE_LEGACY, 'ENGINEER').text
159
159
  assert.match(legacy, /- id: persona\n {2}name: '@deepseek-ai\/dsh-persona'\n {2}config:\n {4}text: [|>]-\n {6}ENGINEER\n/u,
160
160
  'the legacy row is indented correctly')
161
- })
161
+ })
162
+
163
+ test('DSH 0.2 bundle declares the engineering preset', () => {
164
+ const patch = readFileSync(resolve('cordis.patch.yml'), 'utf8')
165
+ assert.match(patch, /id: preset-eng/u)
166
+ assert.match(patch, /name: '@deepseek-ai\/dsh-agent-preset'/u)
167
+ assert.match(patch, /id: eng/u)
168
+ assert.match(patch, /engineering-delivery coding agent/u)
169
+ assert.match(patch, /id: task-engine-agent/u)
170
+ assert.match(patch, /name: '@godv61\/dsh-task-engine\/agent'/u)
171
+ })
@@ -92,7 +92,7 @@ test('rework: fixing a defect keeps the agreed requirement and solution', () =>
92
92
  assert.deepEqual(state.commits, [], 'the commit described a superseded tree')
93
93
  })
94
94
 
95
- test('rework: clearing evidence returns an item to unfinished, not to done-without-audit', () => {
95
+ test('rework: clearing evidence returns an item to unfinished, not to done-without-audit', () => {
96
96
  // A cleared review means the item is no longer proven done. Leaving it `done`
97
97
  // would let todos_done pass on work whose audit had just been discarded.
98
98
  const state = loadedTask()
@@ -111,18 +111,18 @@ test('rework: history records what was invalidated, for audit', () => {
111
111
  assert.equal(state.revisions[0].from, '代码审核')
112
112
  assert.equal(state.revisions[0].to, '设计')
113
113
  assert.deepEqual(state.revisions[0].invalidated, invalidatedBy('solution'))
114
- })
115
-
116
- test('rework: skill results from revised stages are invalidated', () => {
117
- const state = loadedTask()
118
- state.flow = { flow: 'standard', version: 3, config: adoptedFlow('standard') }
119
- state.skill_results = {
120
- '需求评审': { analysis: { evidence: ['still agreed'] } },
121
- '开发': { implementation: { evidence: ['old implementation'] } },
122
- '代码审核': { audit: { evidence: ['old audit'] } },
123
- }
124
- applyRevision(state, rework('defect', '开发'))
125
- assert.ok(state.skill_results['需求评审'])
126
- assert.equal(state.skill_results['开发'], undefined)
127
- assert.equal(state.skill_results['代码审核'], undefined)
128
- })
114
+ })
115
+
116
+ test('rework: skill results from revised stages are invalidated', () => {
117
+ const state = loadedTask()
118
+ state.flow = { flow: 'standard', version: 3, config: adoptedFlow('standard') }
119
+ state.skill_results = {
120
+ '需求评审': { analysis: { evidence: ['still agreed'] } },
121
+ '开发': { implementation: { evidence: ['old implementation'] } },
122
+ '代码审核': { audit: { evidence: ['old audit'] } },
123
+ }
124
+ applyRevision(state, rework('defect', '开发'))
125
+ assert.ok(state.skill_results['需求评审'])
126
+ assert.equal(state.skill_results['开发'], undefined)
127
+ assert.equal(state.skill_results['代码审核'], undefined)
128
+ })