@godv61/dsh-task-engine 0.29.3 → 0.30.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 (85) hide show
  1. package/.adaptive-test.mjs +380 -185
  2. package/.codex-project-test.mjs +18 -18
  3. package/.evidence-test.mjs +14 -1
  4. package/.hook-test.mjs +19 -19
  5. package/.resource-test.mjs +17 -0
  6. package/.roundtrip-test.mjs +8 -3
  7. package/.sonar-credential-test.mjs +38 -0
  8. package/.sonarlint-local-test.mjs +71 -0
  9. package/.workflow-test.mjs +56 -9
  10. package/README.md +35 -112
  11. package/docs/development.md +45 -53
  12. package/docs/manual.html +140 -84
  13. package/hooks/commit-msg +19 -1
  14. package/lib/adaptive.js +1 -1
  15. package/lib/adaptive.js.map +1 -1
  16. package/lib/client.js +297 -9
  17. package/lib/client.js.map +1 -1
  18. package/lib/controller.d.ts +44 -0
  19. package/lib/controller.js +73 -0
  20. package/lib/controller.js.map +1 -1
  21. package/lib/dev-task.d.ts +2 -0
  22. package/lib/dev-task.js +342 -42
  23. package/lib/dev-task.js.map +1 -1
  24. package/lib/engine.d.ts +5 -1
  25. package/lib/engine.js +6 -0
  26. package/lib/engine.js.map +1 -1
  27. package/lib/hook.js +3 -3
  28. package/lib/hook.js.map +1 -1
  29. package/lib/project-init.d.ts +4 -0
  30. package/lib/project-init.js +21 -2
  31. package/lib/project-init.js.map +1 -1
  32. package/lib/skill-audit.js +9 -1
  33. package/lib/skill-audit.js.map +1 -1
  34. package/lib/sonar-credential.d.ts +17 -0
  35. package/lib/sonar-credential.js +17 -0
  36. package/lib/sonar-credential.js.map +1 -0
  37. package/lib/sonar-report.d.ts +4 -0
  38. package/lib/sonar-report.js +53 -0
  39. package/lib/sonar-report.js.map +1 -0
  40. package/lib/sonar.d.ts +35 -2
  41. package/lib/sonar.js +63 -4
  42. package/lib/sonar.js.map +1 -1
  43. package/lib/sonarlint-local.d.ts +17 -0
  44. package/lib/sonarlint-local.js +317 -0
  45. package/lib/sonarlint-local.js.map +1 -0
  46. package/lib/verification-tests.d.ts +10 -0
  47. package/lib/verification-tests.js +15 -0
  48. package/lib/verification-tests.js.map +1 -0
  49. package/package.json +6 -4
  50. package/preset/enable.mjs +2 -2
  51. package/preset/persona.md +4 -4
  52. package/scripts/verify-dsh-compat.mjs +14 -14
  53. package/scripts/verify-package.mjs +10 -8
  54. package/skills/architecture-design/SKILL.md +11 -11
  55. package/skills/code-development/SKILL.md +11 -11
  56. package/skills/code-review/SKILL.md +12 -10
  57. package/skills/eng-delivery/SKILL.md +10 -7
  58. package/skills/requirements-analysis/SKILL.md +11 -11
  59. package/skills/task-orchestration/SKILL.md +11 -11
  60. package/skills/test-validation/SKILL.md +11 -9
  61. package/docs/BRIEF-FOR-REVIEW.md +0 -163
  62. package/docs/CHANGELOG.md +0 -407
  63. package/docs/README.md +0 -42
  64. package/docs/adaptive-workflows.md +0 -76
  65. package/docs/assets/workflow-banner.svg +0 -29
  66. package/docs/configuration.md +0 -80
  67. package/docs/faq.md +0 -59
  68. package/docs/getting-started.md +0 -55
  69. package/docs/listing/godv61__dsh-task-engine.yml +0 -6
  70. package/docs/listing/submission.md +0 -84
  71. package/docs/manual-legacy.html +0 -380
  72. package/docs/releases/0.23.0.md +0 -32
  73. package/docs/releases/0.23.1.md +0 -58
  74. package/docs/releases/0.23.2.md +0 -21
  75. package/docs/resource-install.md +0 -64
  76. package/docs/roadmap.md +0 -33
  77. package/docs/testing/0.23.0//346/265/213/350/257/225/346/211/247/350/241/214/350/256/260/345/275/225.md +0 -189
  78. package/docs/testing/0.23.0//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -42
  79. package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -34
  80. package/docs/testing/0.23.1//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -41
  81. 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 +0 -56
  82. 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 +0 -38
  83. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -65
  84. 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 +0 -43
  85. package/docs/workflow-regression.md +0 -36
@@ -29,7 +29,7 @@ function diskFs() {
29
29
  }
30
30
  }
31
31
 
32
- test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
32
+ test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
33
33
  const root = fixture(t)
34
34
  const body = '---\nname: same\ndescription: Codex method\n---\nCodex body v1\n'
35
35
  put(join(root, '.agents/skills/same/SKILL.md'), body)
@@ -42,23 +42,23 @@ test('Codex project Skills are discovered separately; new DSH Skills stay in .ds
42
42
  const created = await Controller.prototype.writeSkill.call(receiver, { level: 'project', name: 'new-one', description: 'New DSH method', whenToUse: '', content: 'body', path: root })
43
43
  assert.equal(created.ok, true)
44
44
  assert.equal(readFileSync(join(root, '.dsh/skills/new-one/SKILL.md'), 'utf8').includes('body'), true)
45
- })
46
-
47
- test('a project Skill can shadow a bundled meta Skill without replacing an existing project copy', async t => {
48
- const root = fixture(t)
49
- const receiver = { authorizedPath: async path => path }
50
- const request = { level: 'project', name: 'requirements-analysis', description: 'Team requirements method',
51
- whenToUse: 'During requirement analysis', content: 'Project method', path: root, createOnly: true }
52
- const first = await Controller.prototype.writeSkill.call(receiver, request)
53
- assert.equal(first.ok, true)
54
- const file = join(root, '.dsh/skills/requirements-analysis/SKILL.md')
55
- assert.match(readFileSync(file, 'utf8'), /Project method/)
56
- const second = await Controller.prototype.writeSkill.call(receiver, { ...request, content: 'Should not replace' })
57
- assert.equal(second.ok, false)
58
- assert.match(readFileSync(file, 'utf8'), /Project method/)
59
- const catalog = await Controller.prototype.listSkills.call(receiver, root)
60
- assert.deepEqual(catalog.skills.filter(skill => skill.name === 'requirements-analysis').map(skill => skill.ref.source).sort(), ['bundled', 'project'])
61
- })
45
+ })
46
+
47
+ test('a project Skill can shadow a bundled meta Skill without replacing an existing project copy', async t => {
48
+ const root = fixture(t)
49
+ const receiver = { authorizedPath: async path => path }
50
+ const request = { level: 'project', name: 'requirements-analysis', description: 'Team requirements method',
51
+ whenToUse: 'During requirement analysis', content: 'Project method', path: root, createOnly: true }
52
+ const first = await Controller.prototype.writeSkill.call(receiver, request)
53
+ assert.equal(first.ok, true)
54
+ const file = join(root, '.dsh/skills/requirements-analysis/SKILL.md')
55
+ assert.match(readFileSync(file, 'utf8'), /Project method/)
56
+ const second = await Controller.prototype.writeSkill.call(receiver, { ...request, content: 'Should not replace' })
57
+ assert.equal(second.ok, false)
58
+ assert.match(readFileSync(file, 'utf8'), /Project method/)
59
+ const catalog = await Controller.prototype.listSkills.call(receiver, root)
60
+ assert.deepEqual(catalog.skills.filter(skill => skill.name === 'requirements-analysis').map(skill => skill.ref.source).sort(), ['bundled', 'project'])
61
+ })
62
62
 
63
63
  test('a bound Codex Skill loads its attached DSH Rule and reads edits next time', async t => {
64
64
  const root = fixture(t)
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import test from 'node:test'
9
9
  import assert from 'node:assert/strict'
10
- import { needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
10
+ import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
11
11
  import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
12
12
 
13
13
  /** A session that has loaded the named skills. */
@@ -21,6 +21,19 @@ function sessionWith(names) {
21
21
  return { id: 's', snapshotEvents: () => events }
22
22
  }
23
23
 
24
+ test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
25
+ const events = [
26
+ { type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
27
+ { type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
28
+ { type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
29
+ { type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
30
+ ]
31
+ const loaded = loadedSkills({ snapshotEvents: () => events })
32
+ assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
33
+ assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
34
+ assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
35
+ })
36
+
24
37
  /** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
25
38
  function withEvidence(evidence) {
26
39
  const adopted = adoptRecommendation('standard')
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,21 +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
- })
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
- })
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
+ })
@@ -131,3 +131,20 @@ test('ledger rejects malformed records instead of pretending there are no tasks'
131
131
  await assert.rejects(()=>controller.readTasks('/project'),/任务记录/)
132
132
  }
133
133
  })
134
+ test('ledger exposes Sonar findings without project credentials',async()=>{
135
+ const {default:Controller}=await import('./lib/controller.js')
136
+ const controller=Object.create(Controller.prototype)
137
+ controller.authorizedPath=async p=>p
138
+ controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-sample.json'}],readText:async()=>JSON.stringify({
139
+ id:'sample',title:'Sample',stage:'代码审核',branch:'feature/sample',items:[],
140
+ sonar_policy:{source:'ide-local',token_env:'SONAR_TOKEN',host_url:'https://sonar.example',project_key:'sample'},
141
+ sonar_audit:{gate:'ERROR',checked_at:'2026-10-03T00:00:00Z',target:'main',
142
+ findings:[{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3}],
143
+ blocking:[{key:'issue-1'}],uncovered_files:[]},
144
+ })})
145
+ const {tasks}=await controller.readTasks('/project')
146
+ assert.equal(tasks[0].sonar.audit.gate,'ERROR')
147
+ assert.equal(tasks[0].sonar.audit.blocking_count,1)
148
+ assert.deepEqual(tasks[0].sonar.audit.findings[0],{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3})
149
+ assert.equal(JSON.stringify(tasks).includes('SONAR_TOKEN'),false)
150
+ })
@@ -7,18 +7,23 @@
7
7
  */
8
8
  import test from 'node:test'
9
9
  import assert from 'node:assert/strict'
10
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
10
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
11
11
  import { tmpdir } from 'node:os'
12
12
  import { dirname, join, resolve, sep } from 'node:path'
13
13
  import { resolveFlow, adoptRecommendation, compactProjectConfig, materializeBundledReferences } from './lib/workflows.js'
14
14
  import Controller from './lib/controller.js'
15
15
  import { assertAdvance, validateWorkflow } from './lib/engine.js'
16
- import { TYPERT_REMOTE } from './src/client/remote.ts'
16
+ // The npm package ships the built client, not its TypeScript source. Keep the
17
+ // source codec assertion in repository tests while allowing shipped tests to run.
18
+ const remoteSource = new URL('./src/client/remote.ts', import.meta.url)
19
+ const hasRemoteSource = existsSync(remoteSource)
20
+ const TYPERT_REMOTE = hasRemoteSource ? (await import(remoteSource.href)).TYPERT_REMOTE : undefined
17
21
 
18
22
  /** The fields a project config can carry, so a round-trip is checked on all of them. */
19
23
  const FIELDS = ['flow', 'stage_bindings', 'commit', 'artifacts', 'review_depth', 'commit_required']
20
24
 
21
- test('browser remote codec retains source-qualified bindings and all saved fields', () => {
25
+ test('browser remote codec retains source-qualified bindings and all saved fields',
26
+ { skip: hasRemoteSource ? false : 'TypeScript source is excluded from the npm package' }, () => {
22
27
  const write = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'write')
23
28
  const read = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'read')
24
29
  const adopted = compactProjectConfig(adoptRecommendation('minimal'))
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { resolveSonarToken, sonarCredentialRef } from './lib/sonar-credential.js'
4
+ import Controller from './lib/controller.js'
5
+
6
+ test('Sonar credential references stay distinct for different workspaces', () => {
7
+ const qms = sonarCredentialRef('D:/projects/qms')
8
+ const eam = sonarCredentialRef('D:/projects/eam')
9
+ assert.match(qms, /^DSH_TASK_ENGINE_SONAR_[A-F0-9]{24}$/)
10
+ assert.notEqual(qms, eam)
11
+ assert.equal(qms, sonarCredentialRef('D:/projects/qms'))
12
+ assert.ok(!qms.includes('qms'))
13
+ if (process.platform === 'win32') assert.equal(qms, sonarCredentialRef('d:/PROJECTS/QMS'))
14
+ })
15
+
16
+ test('Sonar token management keeps each workspace value private', async () => {
17
+ const stored = new Map()
18
+ const credentials = {
19
+ describe: async ref => ({ configured: stored.has(ref), writable: true, source: stored.has(ref) ? 'file' : undefined }),
20
+ set: async (ref, value) => { stored.set(ref, value) },
21
+ unset: async ref => { stored.delete(ref) },
22
+ resolve: async ref => stored.has(ref) ? { value: stored.get(ref), source: 'file' } : undefined,
23
+ }
24
+ const controller = Object.create(Controller.prototype)
25
+ controller.authorizedPath = async path => path
26
+ Object.defineProperty(controller, 'ctx', { value: { get: name => name === 'credentials' ? credentials : undefined } })
27
+ const qms = 'D:/projects/qms'
28
+ const eam = 'D:/projects/eam'
29
+ const result = await controller.setSonarToken({ path: qms, token: 'qms-secret' })
30
+ assert.equal(result.configured, true)
31
+ assert.ok(!JSON.stringify(result).includes('qms-secret'))
32
+ assert.equal((await controller.describeSonarToken(eam)).configured, false)
33
+ assert.equal((await credentials.resolve(sonarCredentialRef(qms))).value, 'qms-secret')
34
+ assert.equal(await resolveSonarToken(credentials, qms, 'old-env-token'), 'qms-secret')
35
+ assert.equal(await resolveSonarToken(credentials, eam, 'old-env-token'), 'old-env-token')
36
+ await controller.unsetSonarToken(qms)
37
+ assert.equal((await controller.describeSonarToken(qms)).configured, false)
38
+ })
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { test } from 'node:test'
7
+ import { changedLines, missingLocalAuditScope, uncoveredLocalFiles } from './lib/sonarlint-local.js'
8
+ import { isIncludedAuditPath, validAuditIncludePaths } from './lib/sonar.js'
9
+ import { renderSonarReport, sonarReportPath } from './lib/sonar-report.js'
10
+
11
+ test('project scan roots mirror Maven backend scope without including the Vue app', () => {
12
+ const roots = ['fp-bussiness', 'fp-ruoyi', 'fp-ruoyi-spring-boot', 'pom.xml']
13
+ assert.equal(validAuditIncludePaths(roots), true)
14
+ assert.equal(isIncludedAuditPath('fp-bussiness/src/main/java/A.java', roots), true)
15
+ assert.equal(isIncludedAuditPath('fp-ruoyi-ui/src/views/auth.vue', roots), false)
16
+ assert.equal(validAuditIncludePaths(['../outside']), false)
17
+ assert.equal(validAuditIncludePaths(['C:/outside']), false)
18
+ })
19
+
20
+ test('one review gets a credential-free Markdown report path and issue details', () => {
21
+ const audit = { analysis_id: 'abc12345-0000', checked_at: '2026-10-03T10:11:12.000Z',
22
+ target: 'qdm_gw_chemical_260930', gate: 'ERROR', findings: [{key:'x',rule:'java:S103',
23
+ severity:'MAJOR',file:'A.java',line:12,message:'Line too long'}], blocking:[{key:'x'}],
24
+ scanned_files:['A.java'],uncovered_files:[] }
25
+ const path = sonarReportPath('QMS-1', audit)
26
+ assert.match(path, /^\.dsh\/reviews\/QMS-1\/2026-/)
27
+ const report = renderSonarReport('QMS-1', {source:'ide-local',project_key:'project',include_paths:['fp-bussiness']}, audit)
28
+ assert.match(report, /java:S103/)
29
+ assert.match(report, /A.java:12/)
30
+ assert.equal(report.includes('token-secret'),false)
31
+ })
32
+
33
+ test('unsupported files block only when the project has active rules for their language', () => {
34
+ const paths = ['migration.sql', 'settings.json', 'script.py']
35
+ const profiles = [
36
+ { language: 'java', activeRuleCount: 66 },
37
+ { language: 'json', activeRuleCount: 0 },
38
+ { language: 'py', activeRuleCount: 3 },
39
+ ]
40
+ assert.deepEqual(uncoveredLocalFiles(paths, profiles), ['script.py'])
41
+ assert.deepEqual(uncoveredLocalFiles(paths, [...profiles, { language: 'plsql', activeRuleCount: 5 }]),
42
+ ['migration.sql', 'script.py'])
43
+ })
44
+
45
+ test('local audit detects changed backend files omitted from the task scope', () => {
46
+ assert.deepEqual(missingLocalAuditScope(['fp-bussiness/A.java', 'fp-bussiness/B.java'],
47
+ ['fp-bussiness/A.java', 'fp-ruoyi-ui/view.vue']), ['fp-bussiness/B.java'])
48
+ assert.deepEqual(missingLocalAuditScope(['fp-bussiness/A.java'], ['./FP-BUSSINESS\\A.java']), [])
49
+ })
50
+
51
+ test('local Sonar audit considers Git new code but excludes generated DSH records', async () => {
52
+ const root = mkdtempSync(join(tmpdir(), 'dsh-sonarlint-diff-test-'))
53
+ const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' })
54
+ try {
55
+ git('init', '--quiet')
56
+ writeFileSync(join(root, 'Sample.java'), 'class Sample {}\n')
57
+ git('add', 'Sample.java')
58
+ git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '--quiet', '-m', 'baseline')
59
+ writeFileSync(join(root, 'Sample.java'), 'class Sample {}\n// new code\n')
60
+ writeFileSync(join(root, 'change.sql'), 'SELECT 1;\n')
61
+ mkdirSync(join(root, '.dsh'))
62
+ writeFileSync(join(root, '.dsh', 'task.json'), '{"generated":true}\n')
63
+ const changed = await changedLines(root, 'HEAD')
64
+ assert.deepEqual(changed.map(file => file.path).sort(), ['Sample.java', 'change.sql'])
65
+ assert.deepEqual([...changed.find(file => file.path === 'Sample.java').lines], [2])
66
+ assert.equal(changed.find(file => file.path === 'change.sql').newFile, true)
67
+ assert.deepEqual((await changedLines(root, 'HEAD', ['Sample.java'])).map(file => file.path), ['Sample.java'])
68
+ } finally {
69
+ rmSync(root, { recursive: true, force: true })
70
+ }
71
+ })
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join, resolve, sep } from 'node:path'
6
6
  import { registerDevTask } from './lib/dev-task.js'
7
- import { newTask, validateWorkflow } from './lib/engine.js'
7
+ import { newTask, validateWorkflow, verificationBlockers } from './lib/engine.js'
8
8
  import { adoptRecommendation, resolveFlow, retainStageBindings } from './lib/workflows.js'
9
9
  import Controller from './lib/controller.js'
10
10
  import { registerShippedSkills } from './lib/shipped-skills.js'
@@ -71,9 +71,10 @@ function fixture(stage = '开发', extra = {}, services = {}) {
71
71
  const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
72
72
  if (Object.hasOwn(services, name)) return services[name]
73
73
  if (name === 'sandboxPolicy') return { resolve(request) { assert.equal(request.session, session); return policy } }
74
- if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async run(request) {
74
+ if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async execute(request) {
75
75
  const fail = request.command === 'fail'
76
- 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: '' } }
76
+ const outcome = { 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: '' } }
77
+ return { result: async () => outcome }
77
78
  } }
78
79
  return undefined
79
80
  } }
@@ -206,6 +207,52 @@ test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静
206
207
  await assert.rejects(f.call({ operation: 'items', items: [{ id: 'NEW' }] }), /title/)
207
208
  })
208
209
 
210
+ test('重排实施项时不静默丢弃已完成项和审查记录', async () => {
211
+ const f = fixture()
212
+ await f.call({ operation: 'items', items: [
213
+ { id: 'I1', title: 'models', status: 'doing' },
214
+ { id: 'I2', title: 'mapper', status: 'todo' },
215
+ ] })
216
+ await f.call({ operation: 'dispatch', item_id: 'I1', description: 'direct implementation' })
217
+ await f.call({ operation: 'review_item', item_id: 'I1', spec_outcome: 'pass', quality_outcome: 'pass' })
218
+ await f.call({ operation: 'items', items: [{ id: 'I1', status: 'done' }, { id: 'I2' }] })
219
+ await f.call({ operation: 'items', items: [{ id: 'I3', title: 'service', status: 'todo' }] })
220
+ assert.deepEqual(f.state().items.map(item => item.id), ['I1', 'I2', 'I3'])
221
+ assert.equal(f.state().items[0].status, 'done')
222
+ assert.equal(f.state().items[0].review.quality.outcome, 'pass')
223
+ assert.equal(f.state().items[0].dispatch.description, 'direct implementation')
224
+ await f.call({ operation: 'items', items_mode: 'replace', items: [{ id: 'I3', title: 'service', status: 'todo' }] })
225
+ assert.deepEqual(f.state().items.map(item => item.id), ['I3', 'I1'])
226
+ })
227
+
228
+ test('Maven verify rejects zero or missing Surefire tests even when the command exits zero', async () => {
229
+ let output = 'Tests run: 0, Failures: 0, Errors: 0, Skipped: 0'
230
+ const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
231
+ exitCode: 0, timedOut: false, aborted: false, stdout: { text: output }, stderr: { text: '' },
232
+ }) }) }
233
+ const f = fixture('交付', {}, { shell })
234
+ await f.call({ operation: 'verify', command: 'mvn -q test' })
235
+ assert.equal(f.state().verification.passed, false)
236
+ assert.equal(f.state().verification.receipt.test_summary.count, 0)
237
+ output = 'BUILD SUCCESS'
238
+ await f.call({ operation: 'verify', command: 'mvn -q test' })
239
+ assert.equal(f.state().verification.passed, false)
240
+ assert.equal(f.state().verification.receipt.test_summary.count, null)
241
+ output = 'Tests run: 23, Failures: 0, Errors: 0, Skipped: 0'
242
+ await f.call({ operation: 'verify', command: 'mvn test' })
243
+ assert.equal(f.state().verification.passed, true)
244
+ assert.equal(f.state().verification.receipt.test_summary.count, 23)
245
+ })
246
+
247
+ test('a previously stored Maven zero-test pass is blocked by current workflow gates', () => {
248
+ const f = fixture('代码审核')
249
+ const state = f.state()
250
+ state.verification = { passed: true, evidence: [], receipt: { command: 'mvn test', exit_code: 0,
251
+ timed_out: false, aborted: false, started_at: 'now', finished_at: 'now',
252
+ stdout: 'Tests run: 0, Failures: 0, Errors: 0, Skipped: 0', stderr: '' } }
253
+ assert.match(verificationBlockers(state, state.flow.config).join(' '), /no nonzero Surefire/)
254
+ })
255
+
209
256
  test('派发立即反映进行中,重派不复用旧审核且保持单一进行项', async () => {
210
257
  const f = fixture()
211
258
  await f.call({ operation: 'items', items: [{ id: 'A', title: 'models' }, { id: 'B', title: 'api' }] })
@@ -411,14 +458,14 @@ test('同名技能与规则在项目和个人目录各自保留管理入口', as
411
458
  assert.match(read.content, /Run tests/)
412
459
  })
413
460
 
414
- test('安装包注册会话编排和元技能,阶段目录只隐藏会话编排技能', async () => {
461
+ test('安装包注册会话编排和元技能,阶段目录只隐藏会话编排技能', async () => {
415
462
  const registered = []
416
463
  registerShippedSkills({ get: name => name === 'skills' ? { register: skill => { registered.push(skill); return () => {} } } : undefined })
417
- const expected = ['architecture-design', 'code-development', 'code-review', 'eng-delivery',
418
- 'requirements-analysis', 'task-orchestration', 'test-validation']
464
+ const expected = ['architecture-design', 'code-development', 'code-review', 'eng-delivery',
465
+ 'requirements-analysis', 'task-orchestration', 'test-validation']
419
466
  assert.deepEqual(registered.map(skill => skill.name).sort(), expected)
420
467
  assert.ok(registered.every(skill => skill.content.trim().length > 0))
421
468
  const catalog = await Controller.prototype.listSkills.call({ authorizedPath: async path => path }, '')
422
- assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(),
423
- expected.filter(name => name !== 'eng-delivery'))
424
- })
469
+ assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(),
470
+ expected.filter(name => name !== 'eng-delivery'))
471
+ })
package/README.md CHANGED
@@ -1,114 +1,37 @@
1
- <p align="center">
2
- <img src="docs/assets/workflow-banner.svg" alt="DSH Task Engine:DeepSeek Harness 的个人工程流程工作台,标准流程从需求评审走向完成" width="100%" />
3
- </p>
4
-
5
- <h1 align="center">DSH Task Engine</h1>
1
+ # DSH Task Engine
6
2
 
7
- > `0.29.0` 新增自适应流程、项目技能初始化与可选 SonarQube CI 审核。SonarQube 集成目前读取提交后的 CI 分析结果,不提供未提交代码的本地 Sonar 审核。
8
-
9
- <p align="center">按每个需求选择工程路径,用项目 Skill 与 Rule 复用团队开发规范。</p>
10
- <p align="center"><sub>Task-scoped engineering workflows for DeepSeek Harness.</sub></p>
11
-
12
- <p align="center">
13
- <a href="https://www.npmjs.com/package/@godv61/dsh-task-engine"><img src="https://img.shields.io/npm/v/%40godv61%2Fdsh-task-engine?style=flat-square&amp;color=238636" alt="npm version" /></a>
14
- <a href="https://github.com/godv61/dsh-task-engine/actions/workflows/verify.yml"><img src="https://github.com/godv61/dsh-task-engine/actions/workflows/verify.yml/badge.svg" alt="Package verification CI" /></a>
15
- <a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome DSH Plugin" /></a>
16
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-64748b?style=flat-square" alt="MIT license" /></a>
17
- </p>
18
-
19
- <p align="center">
20
- <a href="#快速开始">快速开始</a> ·
21
- <a href="#工作台里有什么">工作台</a> ·
22
- <a href="docs/README.md">使用文档</a> ·
23
- <a href="docs/roadmap.md">功能规划</a> ·
24
- <a href="docs/CHANGELOG.md">更新日志</a>
25
- </p>
26
-
27
- ## 为什么用它
28
-
29
- 让 AI 在持续开发的项目中处理不同需求时,按这次需求的复杂度选择低、中、高、超高四档任务流程。元技能规定交接产物,项目 Skill/Rule 承载团队编码规范;同一项目下的多个会话可以各自推进不同任务。
30
-
31
- - **知道下一步做什么**:任务创建时选择复杂度并冻结流程,当前阶段的条件和产物清楚可查。
32
- - **复用自己的工作方法**:把技能和规则安装到项目或个人目录,将规则配置在技能下,再把技能挂到节点上;同一技能在任何节点都使用同一套约束。
33
- - **找得到过程记录**:任务台账集中查看阶段、实施项、验证与审核状态。
34
-
35
- ## 快速开始
36
-
37
- 需要已安装 DeepSeek Harness 和 pnpm。插件装入你使用的 Web profile;下面以 `web` 为例。
38
-
39
- ```sh
40
- dsh plugin --profile web add @godv61/dsh-task-engine
41
- ```
42
-
43
- 1. 重启 Harness Web,打开侧边栏的 **工程任务**。
44
- 2. 选择工作区,在 **自适应流程** 中查看四档路径,按需挂载项目技能。
45
- 3. 新建会话,选择 **工程化开发引擎** 预设,描述要完成的开发任务。
46
-
47
- 看到“工程任务”入口和“工程化开发引擎”会话预设,就说明工作台与任务工具已接入。使用 Harness 源码启动的安装方式见[安装与启用](docs/getting-started.md)。
48
-
49
- > 工程化会话先分析需求复杂度,再以 `dev_task assess` 预览并创建任务;`.dsh/meta.json` 只配置元技能挂载和可选 SonarQube 审核,不强迫项目所有会话走同一条流程。
50
-
51
- ## 工作台里有什么
52
-
53
- | 页面 | 你可以做什么 |
54
- | :--- | :--- |
55
- | **项目初始化** | 分开管理 `AGENTS.md` 与项目 Skill/Rule。后者可在页面复制初始化请求,发给“工程化开发引擎”会话后由 `dev_task init_project` 扫描、预览并应用。 |
56
- | **自适应流程** | 查看四档任务路径、元技能绑定与可选 SonarQube 审核配置。 |
57
- | **任务台账** | 查看实施、验证和审核记录,按关键词、阶段或风险筛选。 |
58
- | **技能** | 安装、编辑技能,并集中维护每个技能唯一的规则列表。 |
59
- | **规则** | 选择 Markdown 文件安装规则,维护项目或个人开发约定。 |
60
-
61
- 插件内置会话编排 Skill `eng-delivery` 和六个通用元技能 Skill。项目知识、领域编码方法和 Rule 由使用者创建或通过 init 生成,可放在项目或个人目录;同名项目 Skill 在新任务中覆盖用户级 Skill。
3
+ 在 DeepSeek Harness 中按**每个需求**组织开发:模型评估低、中、高、超高四档复杂度,任务按相应元技能推进;项目 Skill 与 Rule 保存团队约定。可选的 SonarQube 审核只在代码审核阶段运行。
62
4
 
63
- 新路径的四档顺序、元技能交接契约、`init_project` 和可选 SonarQube 审核见[自适应工程任务](docs/adaptive-workflows.md)。
64
-
65
- **流程与工作方法各有职责。** 四档流程规定阶段与门禁,并在相应阶段加载内置元技能;项目业务技能和规则由使用者配置。同一条规则可由多个技能共享。点击元技能的“配置核心 Skill 的 Rule”,内置或用户级 Skill 可在保存时生成同名项目副本,再挂载项目 Rule。任务的流程与资源引用在创建时确定;Skill/Rule 正文在每次交互读取最新版本,创建时副本仅用于审计。[自适应流程说明](docs/adaptive-workflows.md)
66
-
67
- 已有任务仍按创建时的快照运行;旧版 `.dsh/eng.json` 也继续可读。工作台只展示自适应流程,旧版配置细节保存在[历史文档](docs/configuration.md)。
68
-
69
- ## 把自己的技能和规则带进来
70
-
71
- **选择文件 → 检查预览 → 选择范围 → 确认安装 → 挂到阶段。**
72
-
73
- 技能选择含 `SKILL.md` 的文件夹,规则选择一个 `.md` 文件。预览会显示名称、正文、文件数、体积和目标路径;目标目录中已有同名资源不会被覆盖。项目级 Skill 可与内置 Skill 同名,以项目版本优先生效。
74
-
75
- | 安装范围 | 存放位置 | 用途 |
76
- | :--- | :--- | :--- |
77
- | 项目 | 工作区 `.dsh/skills`、`.dsh/rules` | 当前项目的工作方法与约定;工作台新建资源默认写在这里。 |
78
- | 个人 | `$DSH_HOME/skills`、`$DSH_HOME/rules` | 在这台电脑上的多个项目间复用。 |
79
-
80
- 工作台也会发现同一项目 `.agents/skills/<名称>/SKILL.md` 中的 Codex 项目技能,可将它绑定到流程节点,并在技能下配置 DSH 规则;它的正文仍留在原目录。运行时使用 `dev_task` 的 `load_skill` 操作读取该技能及其规则,下一次读取会取得文件的最新内容。[Codex 项目技能说明](docs/configuration.md#技能与规则)
81
-
82
- 技能的脚本、模板和附件会一并保留。文件格式、大小限制和故障处理见[技能与规则安装指南](docs/resource-install.md)。
83
-
84
- ## 使用文档
85
-
86
- | 想了解什么 | 从这里开始 |
87
- | :--- | :--- |
88
- | 安装、启用与第一次使用 | [快速上手](docs/getting-started.md) |
89
- | 任务级流程、元技能与 SonarQube | [自适应工程任务](docs/adaptive-workflows.md) |
90
- | 旧项目配置与阶段绑定 | [流程配置](docs/configuration.md) |
91
- | 技能文件夹与 Markdown 规则 | [资源安装](docs/resource-install.md) |
92
- | 完整操作说明 | [HTML 手册](docs/manual.html)(下载后在浏览器打开) |
93
- | 当前能力与常见问题 | [常见问题](docs/faq.md) |
94
- | 本地开发与验证 | [开发指南](docs/development.md) |
95
- | 真实项目回归修复 | [0.23.1 发布说明](docs/releases/0.23.1.md) · [回归迭代说明](docs/workflow-regression.md) |
96
- | 版本变化与验证记录 | [更新日志](docs/CHANGELOG.md) · [0.23.1 测试说明](docs/testing/0.23.1/测试报告.md) |
97
-
98
- ## 能力说明
99
-
100
- `dev_task` 按配置检查阶段、产物和提交条件。新任务的验证要求真实命令回执;技能可按自身配置要求命令、产物、审核、人工批准或无需额外证据,执行义务可在任务状态中查看。任务保存创建时的流程与资源引用;之后修改流程配置不会改变进行中任务的状态机,编辑所引用资源的正文会从下一次交互生效。
101
-
102
- 审核结论、实施项完成情况和测试覆盖面仍需要你判断。本地提交钩子提供即时检查,不能替代人工审核或项目自己的 CI。详细说明见[常见问题](docs/faq.md)。
103
-
104
- 小修正允许主代理处理,仍须审核和验证。遇到沙箱权限拒绝应使用宿主审批或报告阻塞,不能靠反复切换命令规避。
105
-
106
- 状态查询会列出过期验证和附加技能回执;`commit.allowed` 同时检查这些阻塞,文件变化后先补验证再申请提交。
107
-
108
- 验证命令的单次权限重试先审批后执行,不改变会话权限;操作方式见[验证权限说明](docs/faq.md#验证命令被沙箱阻止怎么办)。
109
-
110
- 记录需求、方案或评审前,模型可从 `artifact_requirements` 获取当前阶段允许的字段和缺项,避免猜测字段名。
111
-
112
- ---
113
-
114
- [MIT License](LICENSE) · [反馈问题](https://github.com/godv61/dsh-task-engine/issues) · Built for DeepSeek Harness
5
+ ## 安装
6
+
7
+ 把插件安装到实际使用的 Web profile(下面以 `web` 为例),重启 DSH Web:
8
+
9
+ ```sh
10
+ dsh plugin --profile web add @godv61/dsh-task-engine
11
+ ```
12
+
13
+ 使用 DSH 源码启动时,在 DSH 根目录执行 `pnpm dsh plugin --profile web add @godv61/dsh-task-engine`,再运行 `pnpm dsh web --no-open`。重启后,侧边栏应出现“工程任务”,新会话应能选择“工程化开发引擎”。普通会话不会因为某个项目配置了流程而自动进入该流程。
14
+
15
+ ## 从一个项目开始
16
+
17
+ 1. 在“工程任务”选择工作区,进入“项目初始化”,复制初始化请求到该项目的“工程化开发引擎”会话。`init_project` 依次扫描、预览、应用项目地图、技术栈及开发 Skill/Rule。项目地图概述**整个仓库**,需求细节写进具体任务。
18
+ 2. 检查生成的 `.dsh/skills/`、`.dsh/rules/` 和 `.dsh/meta.json`,按需在“自适应流程”把项目 Skill 挂到元技能。同名项目 Skill 优先于用户级和内置 Skill。
19
+ 3. 在会话中描述需求。模型用 `dev_task assess` 评估复杂度,再创建独立任务。高、超高任务按实现先后顺序编排实施项,不按人员分包。
20
+ 4. 在“任务台账”查看阶段、实施项、测试和审核。`items` 默认按 ID 增量更新;要有意重排或移除未开始的项目时,显式使用 `items_mode=replace`。
21
+ 5. 开发完成后运行真实验证。Maven 测试命令须在输出中证明至少运行一个测试;零测试不能作为通过。审核通过后,按任务门禁完成提交。
22
+
23
+ ## 可选 SonarQube
24
+
25
+ 在当前项目的“自适应流程”页单独填写服务地址、项目 Key、扫描方式和 Token。Token 保存在本机 DSH 项目凭据中,不写入仓库;不需要 Sonar 的项目保持开关关闭。
26
+
27
+ - **本地规则审核 `ide-local`**:使用项目 Quality Profile 中可本地执行的规则检查相对 Git 参考分支新增的代码;无需先提交或推送。审核会拒绝任务范围漏登的新增文件。对疑似误报,可逐条附理由和证据,请求人工批准;原始告警保留,代码变化后需重新审核。
28
+ - **CI 结果 `ci`**:读取本次 CI 分析的 CE task ID,按服务端新代码规则和 Quality Gate 判断。
29
+ - **上传式本机扫描 `local`**:本机扫描并上传分析;需要支持分支分析的 SonarQube 版本。
30
+
31
+ 每次审核都在项目 `.dsh/reviews/<任务 ID>/` 留下 Markdown 报告,任务台账可查看问题与处理状态。`ide-local` 的结果不能代替服务端完整 Quality Gate。真实且可复用的修复案例可以经预览后生成项目 Rule;误报不会自动变成 Rule。
32
+
33
+ ## 文档
34
+
35
+ 详细安装、首次初始化、四档流程、Skill/Rule、任务命令、Sonar 配置与排障见唯一的[使用手册](docs/manual.html)。插件开发与发版检查见[开发指南](docs/development.md)。仓库历史版本可通过 Git 记录查看;当前文档只描述现行功能。
36
+
37
+ [MIT License](LICENSE) · [问题反馈](https://github.com/godv61/dsh-task-engine/issues)