@godv61/dsh-task-engine 0.24.0 → 0.26.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 (60) hide show
  1. package/.acceptance.mjs +101 -39
  2. package/.assessment-batch1.mjs +32 -12
  3. package/.e2e-presets.mjs +40 -5
  4. package/.enforce-test.mjs +139 -0
  5. package/.evidence-test.mjs +118 -0
  6. package/.filter-test.mjs +93 -0
  7. package/.freeze-test.mjs +110 -1
  8. package/.hook-test.mjs +195 -190
  9. package/.p0-test.mjs +130 -46
  10. package/.preset-test.mjs +160 -160
  11. package/.revision-test.mjs +32 -3
  12. package/.roundtrip-test.mjs +178 -0
  13. package/.workflow-test.mjs +117 -4
  14. package/README.md +10 -8
  15. package/cordis.patch.yml +10 -10
  16. package/defaults/eng.json +70 -22
  17. package/docs/BRIEF-FOR-REVIEW.md +162 -162
  18. package/docs/CHANGELOG.md +49 -0
  19. package/docs/README.md +40 -40
  20. package/docs/configuration.md +14 -11
  21. package/docs/development.md +1 -1
  22. package/docs/faq.md +4 -4
  23. package/docs/listing/godv61__dsh-task-engine.yml +5 -5
  24. package/docs/listing/submission.md +83 -83
  25. package/docs/manual.html +31 -26
  26. package/docs/releases/0.23.2.md +21 -21
  27. package/docs/roadmap.md +34 -34
  28. 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 -56
  29. 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 -38
  30. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -65
  31. package/hooks/commit-msg +159 -72
  32. package/lib/client.js +963 -571
  33. package/lib/client.js.map +3 -3
  34. package/lib/controller.d.ts +11 -2
  35. package/lib/controller.js +33 -5
  36. package/lib/controller.js.map +1 -1
  37. package/lib/dev-task.js +170 -28
  38. package/lib/dev-task.js.map +1 -1
  39. package/lib/engine.d.ts +105 -0
  40. package/lib/engine.js +93 -5
  41. package/lib/engine.js.map +1 -1
  42. package/lib/hook.js +17 -4
  43. package/lib/hook.js.map +1 -1
  44. package/lib/skill-audit.d.ts +14 -3
  45. package/lib/skill-audit.js +66 -3
  46. package/lib/skill-audit.js.map +1 -1
  47. package/lib/workflows.d.ts +78 -12
  48. package/lib/workflows.js +258 -94
  49. package/lib/workflows.js.map +1 -1
  50. package/package.json +9 -5
  51. package/preset/agent.cordis.yml +21 -21
  52. package/preset/enable.mjs +87 -87
  53. package/preset/persona.md +4 -4
  54. package/preset/preset.yml +1 -1
  55. package/rules/coding-conventions.md +6 -6
  56. package/rules/commit-conventions.md +6 -6
  57. package/rules/security-redlines.md +5 -5
  58. package/scripts/verify-dsh-compat.mjs +144 -144
  59. package/scripts/verify-package.mjs +1 -1
  60. package/skills/code-verify/SKILL.md +3 -3
package/.p0-test.mjs CHANGED
@@ -6,12 +6,29 @@ import { readFileSync } from 'node:fs'
6
6
  import { registerDevTask } from './lib/dev-task.js'
7
7
  import { assertAdvance, checkFileScope, newTask, taskIdFromMessage, validateWorkflow } from './lib/engine.js'
8
8
  import {
9
+
10
+
9
11
  FLOW_PRESETS,
10
12
  HIGH_RISK_REQUIRED_CAPABILITIES,
11
13
  flowSatisfies,
12
14
  resolveFlow,
15
+ adoptRecommendation,
13
16
  } from './lib/workflows.js'
14
17
 
18
+ /**
19
+ * The preset as a project would actually run it: the skeleton plus the shipped
20
+ * recommendation, adopted exactly the way a user adopts it. A test that asserts
21
+ * on bindings, commit rules or artifacts wants this, because those no longer come
22
+ * from the preset itself.
23
+ * @param {string} id - preset id.
24
+ * @returns {import('./lib/engine.js').WorkflowConfig} the adopted config.
25
+ */
26
+ function adoptedFlow(id, extra) {
27
+ const base = adoptRecommendation(id)
28
+ if (base === undefined) throw new Error('unknown preset: ' + id)
29
+ return resolveFlow(id, { ...base, ...extra }).config
30
+ }
31
+
15
32
  let passed = 0
16
33
  function assert(cond, msg) {
17
34
  if (!cond) {
@@ -101,34 +118,64 @@ function sessionExec(cwd) {
101
118
  assert(flowSatisfies('standard', HIGH_RISK_REQUIRED_CAPABILITIES), 'standard satisfies high-risk capabilities')
102
119
  assert(!flowSatisfies('agile', HIGH_RISK_REQUIRED_CAPABILITIES), 'agile lacks a high-risk capability')
103
120
  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')
121
+ // Version 3 is the skeleton/recommendation split: the preset now carries only
122
+ // state control, and its former bindings, commit text and artifacts are a
123
+ // separate recommendation a user adopts. The version number is what makes an
124
+ // already-created task keep its frozen config instead of silently changing.
125
+ assert(FLOW_PRESETS.standard.version === 3, 'standard preset carries the skeleton/recommendation version')
126
+ // Versions are per-preset because they record per-preset changes: agile moved
127
+ // again when 审查 gained a completion guard, and a task created before that
128
+ // keeps its frozen version and behaves exactly as it did.
129
+ assert(FLOW_PRESETS.standard.version === 3, 'standard carries the skeleton/recommendation version')
130
+ assert(FLOW_PRESETS.minimal.version === 3, 'minimal carries the skeleton/recommendation version')
131
+ assert(FLOW_PRESETS.agile.version === 4, 'agile moved again for the completion guard')
105
132
 
106
133
  // ── 2. unknown flow fails closed ───────────────────────────────────────────
134
+ // `resolveFlow` is called directly here: this test is about the fail-closed
135
+ // result, and the adoption helper throws instead of returning one.
107
136
  const unknown = resolveFlow('nope')
108
137
  assert(unknown.ok === false && unknown.code === 'UNKNOWN_FLOW', 'unknown flow returns UNKNOWN_FLOW')
109
138
  assert(unknown.knownFlows.includes('standard'), 'known flow list is surfaced')
110
139
 
111
- // ── 3. core skill bindings cannot be cancelled by an override ──────────────
112
- // Rules moved onto the skill, so the invariant is about the SKILL surviving an
113
- // override and that skill's own rule list staying intact. A stage has no rule
114
- // list left to empty.
115
- const merged = resolveFlow('standard', { stage_bindings: { '开发': {} } })
116
- assert(merged.ok
117
- && merged.config.stage_bindings['开发'].skills.some(entry => entry.skill.name === 'code-implement'),
118
- 'an empty stage override keeps the core skill binding')
119
- const implementBinding = merged.config.stage_bindings['开发'].skills.find(entry => entry.skill.name === 'code-implement')
120
- assert(implementBinding.rules.map(rule => rule.name).includes('coding-conventions'),
121
- 'a skill keeps its own rules; there is no stage-level rule list for an override to empty')
140
+ // ── 3. the user's bindings are taken as given, not merged with a preset's ────
141
+ // The old invariant here was "a preset's core skill survives any override", which
142
+ // meant a project could never drop a shipped skill and a project's own rule list
143
+ // for that skill was discarded because the skill reference already existed. The
144
+ // contract is now the opposite: what the user configured IS the configuration,
145
+ // and the preset offers its setup only through explicit adoption.
146
+ const bare = resolveFlow('standard', { flow: 'standard' })
147
+ assert(bare.ok && Object.keys(bare.config.stage_bindings ?? {}).length === 0,
148
+ 'the preset skeleton carries no bindings of its own')
149
+
150
+ const emptied = resolveFlow('standard', { flow: 'standard', stage_bindings: { '开发': { skills: [] } } })
151
+ assert(emptied.ok && (emptied.config.stage_bindings['开发'].skills ?? []).length === 0,
152
+ 'a project that binds nothing to a stage keeps it empty; nothing is merged back in')
153
+
154
+ const adopted = adoptedFlow('standard')
155
+ assert(adopted.stage_bindings['开发'].skills.some(entry => entry.skill.name === 'code-implement'),
156
+ 'adopting the recommendation is what supplies the shipped skills')
157
+
158
+ // And a user who gives a shipped skill their own rule list gets that list — the
159
+ // defect this replaces was that the entry was skipped as a duplicate reference.
160
+ const ownRules = resolveFlow('standard', {
161
+ flow: 'standard',
162
+ stage_bindings: {
163
+ '开发': { skills: [{ skill: { source: 'bundled', name: 'code-implement' }, rules: [{ source: 'project', name: 'only-mine' }] }] },
164
+ },
165
+ }).config
166
+ const implementBinding = ownRules.stage_bindings['开发'].skills.find(entry => entry.skill.name === 'code-implement')
167
+ assert(implementBinding.rules.length === 1 && implementBinding.rules[0].name === 'only-mine',
168
+ 'a project rule list for a shipped skill is used as given, not discarded as a duplicate')
122
169
 
123
170
  // ── 4. newTask freezes the snapshot ────────────────────────────────────────
124
- const snapshot = { flow: 'standard', version: 1, config: FLOW_PRESETS.standard.config }
171
+ const snapshot = { flow: 'standard', version: 1, config: adoptedFlow('standard') }
125
172
  const state = newTask({ id: 'T1', title: 'x', branch: 'main', work_size: 'standard', risk_level: 'standard', flow: snapshot })
126
173
  assert(state.flow?.flow === 'standard' && state.flow.version === 1, 'newTask stores the frozen snapshot')
127
174
  assert(state.stage === '需求评审', 'newTask starts at the frozen start stage')
128
175
 
129
176
  // ── 5. high_risk create on minimal is rejected ─────────────────────────────
130
177
  await assertThrows(
131
- () => registered(makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'minimal' }) }))
178
+ () => registered(makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('minimal')) }))
132
179
  .then(exe => exe({ operation: 'create', task_id: 'T2', title: 'x', branch: 'main', risk_level: 'high_risk' }, EXEC)),
133
180
  'high_risk',
134
181
  'high_risk on minimal is rejected',
@@ -136,13 +183,13 @@ await assertThrows(
136
183
 
137
184
  // ── 6. a task keeps its frozen snapshot after eng.json changes ─────────────
138
185
  {
139
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
186
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
140
187
  const exe = await registered(fs)
141
188
  const created = await exe({ operation: 'create', task_id: 'T3', title: 'x', branch: 'main' }, EXEC)
142
189
  assert(created.includes('需求评审'), 'standard create starts at 需求评审')
143
- fs._files.set('.dsh/eng.json', JSON.stringify({ flow: 'minimal' }))
190
+ fs._files.set('.dsh/eng.json', JSON.stringify(adoptRecommendation('minimal')))
144
191
  const status = JSON.parse(await exe({ operation: 'status', task_id: 'T3' }, EXEC))
145
- assert(status.flow?.flow === 'standard' && status.flow.version === 2, 'status re-reads the frozen snapshot')
192
+ assert(status.flow?.flow === 'standard' && status.flow.version === 3, 'status re-reads the frozen snapshot')
146
193
  assert(Array.isArray(status.legal_next) && status.legal_next.includes('设计'),
147
194
  'gates come from the frozen standard flow, not the live minimal flow')
148
195
  }
@@ -213,21 +260,58 @@ await assertThrows(
213
260
  )
214
261
  }
215
262
 
216
- // ── 11. a project cannot shadow a bundled core rule ────────────────────────
263
+ // ── 11. a bound rule resolves from the layer the reference names ────────────
264
+ // Rules used to be looked up by bare name through a precedence chain, so a
265
+ // project file named like a bundled one silently replaced the shipped body.
266
+ // A reference now names its layer (`bundled:` / `project:` / `user:`), so there
267
+ // is no chain to walk and no shadowing to defend against: the same name in two
268
+ // layers is two distinct resources, and the binding says which one it means.
217
269
  {
218
270
  const bundled = readFileSync('./rules/security-redlines.md', 'utf8')
219
271
  assert(bundled.includes('服务端'), 'bundled security-redlines exists as expected')
220
272
  const fs = makeFs({
221
- '.dsh/eng.json': JSON.stringify({ flow: 'standard' }),
273
+ // A project file with the same NAME but a different body. Under the old
274
+ // bare-name lookup this would have replaced the bundled rule silently.
275
+ '.dsh/eng.json': JSON.stringify({
276
+ flow: 'standard',
277
+ stage_bindings: {
278
+ '需求评审': { skills: [{
279
+ skill: { source: 'bundled', name: 'requirement-analysis' },
280
+ rules: [{ source: 'project', name: 'security-redlines' }],
281
+ }] },
282
+ },
283
+ }),
222
284
  '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
223
285
  })
224
286
  const exe = await registered(fs)
225
287
  await exe({ operation: 'create', task_id: 'T9', title: 'x', branch: 'main' }, EXEC)
226
288
  const status = JSON.parse(await exe({ operation: 'status', task_id: 'T9' }, EXEC))
227
289
  const sec = status.bindings.rules.find(r => r.name === 'security-redlines')
228
- assert(sec !== undefined, 'security-redlines is disclosed at the start stage')
229
- assert(!sec.content.includes('前端校验即可'), 'a project shadow of a core rule is ignored')
230
- assert(sec.content.includes('服务端'), 'the bundled core rule body wins over a shadow')
290
+ assert(sec !== undefined, 'the bound project rule is disclosed at the start stage')
291
+ assert(sec.source === 'project', 'the disclosed rule reports the layer it was resolved from')
292
+ assert(sec.content.includes('前端校验即可'), 'the PROJECT body is what gets disclosed, because the reference names the project layer')
293
+ }
294
+
295
+ // ── 11b. a bundled reference keeps resolving the bundled body ───────────────
296
+ {
297
+ const fs = makeFs({
298
+ '.dsh/eng.json': JSON.stringify({
299
+ flow: 'standard',
300
+ stage_bindings: {
301
+ '需求评审': { skills: [{
302
+ skill: { source: 'bundled', name: 'requirement-analysis' },
303
+ rules: [{ source: 'bundled', name: 'security-redlines' }],
304
+ }] },
305
+ },
306
+ }),
307
+ '.dsh/rules/security-redlines.md': '# 弱化的安全红线,前端校验即可',
308
+ })
309
+ const exe = await registered(fs)
310
+ await exe({ operation: 'create', task_id: 'T10', title: 'x', branch: 'main' }, EXEC)
311
+ const status = JSON.parse(await exe({ operation: 'status', task_id: 'T10' }, EXEC))
312
+ const sec = status.bindings.rules.find(r => r.name === 'security-redlines')
313
+ assert(sec !== undefined && sec.source === 'bundled', 'a bundled reference resolves to the bundled layer')
314
+ assert(sec.content.includes('服务端'), 'the bundled body is used, untouched by the same-named project file')
231
315
  }
232
316
 
233
317
  // ── 12. session cwd drives every relative path (init regression) ──────────
@@ -246,7 +330,7 @@ await assertThrows(
246
330
  // ── 13. create writes the task record into the session workspace ──────────
247
331
  {
248
332
  const cwd = 'C:/users/ggbond/.dsh-verify/e2e-project'
249
- const fs = makeFs({ [`${cwd}/.dsh/eng.json`]: JSON.stringify({ flow: 'standard' }) })
333
+ const fs = makeFs({ [`${cwd}/.dsh/eng.json`]: JSON.stringify(adoptRecommendation('standard')) })
250
334
  const exe = await registered(fs)
251
335
  await exe({ operation: 'create', task_id: 'T12', title: 'x', branch: 'main' }, sessionExec(cwd))
252
336
  assert(fs._files.get(`${cwd}/.dsh/task-T12.json`) !== undefined, 'task record lands in the session workspace')
@@ -255,7 +339,7 @@ await assertThrows(
255
339
 
256
340
  // ── 14. K: artifact ids must be unique across ALL stages ────────────────────
257
341
  {
258
- const withDup = { ...FLOW_PRESETS.standard.config, artifacts: [
342
+ const withDup = { ...adoptedFlow('standard'), artifacts: [
259
343
  { stage: '需求评审', id: 'doc', name: '需求', fields: ['scope'] },
260
344
  { stage: '设计', id: 'doc', name: '设计', fields: ['approach'] },
261
345
  ] }
@@ -266,7 +350,7 @@ await assertThrows(
266
350
 
267
351
  // ── 15. K: record rejects an artifact owned by another stage ─────────────────
268
352
  {
269
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
353
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
270
354
  const exe = await registered(fs)
271
355
  await exe({ operation: 'create', task_id: 'K1', title: 'x', branch: 'main' }, EXEC)
272
356
  await assertThrows(
@@ -278,7 +362,7 @@ await assertThrows(
278
362
 
279
363
  // ── 16. L/4.2: high-risk verification requires a REAL command receipt ────────
280
364
  {
281
- const cfg = FLOW_PRESETS.standard.config
365
+ const cfg = adoptedFlow('standard')
282
366
  const receipt = overrides => ({
283
367
  command: 'npm test', exit_code: 0, timed_out: false, aborted: false,
284
368
  started_at: '2026-01-01T00:00:00Z', finished_at: '2026-01-01T00:00:01Z',
@@ -303,20 +387,20 @@ await assertThrows(
303
387
 
304
388
  // ── 17. J: task id extracted from the summary (the hook uses it, not a guess) ─
305
389
  {
306
- const cfg = FLOW_PRESETS.standard.config
390
+ const cfg = adoptedFlow('standard')
307
391
  assert(taskIdFromMessage('【GREET-001】【TASK】实现登录', cfg) === 'GREET-001', 'standard summary yields its task id')
308
392
  assert(taskIdFromMessage('【GREET-001】【T1】实现登录', cfg) === 'GREET-001', 'item-label summary yields its task id')
309
393
  assert(taskIdFromMessage('实现登录', cfg) === undefined, 'non-matching summary yields no task id')
310
- assert(taskIdFromMessage('随意', FLOW_PRESETS.minimal.config) === undefined, 'pattern-less flow yields no task id')
394
+ assert(taskIdFromMessage('随意', adoptedFlow('minimal')) === undefined, 'pattern-less flow yields no task id')
311
395
  }
312
396
 
313
397
  // ── 18. N: engine bookkeeping files are exempt from file scope ───────────────
314
398
  {
315
399
  const s = newTask({ id: 'N1', title: 'x', branch: 'main', work_size: 'standard', risk_level: 'standard', flow: snapshot })
316
400
  s.files = ['src/a.ts']
317
- assert(checkFileScope(s, ['src/a.ts', '.dsh/task-N1.json', '.dsh/eng.json'], FLOW_PRESETS.standard.config).ok,
401
+ assert(checkFileScope(s, ['src/a.ts', '.dsh/task-N1.json', '.dsh/eng.json'], adoptedFlow('standard')).ok,
318
402
  'task record and eng.json do not violate file scope')
319
- assert(!checkFileScope(s, ['src/b.ts'], FLOW_PRESETS.standard.config).ok,
403
+ assert(!checkFileScope(s, ['src/b.ts'], adoptedFlow('standard')).ok,
320
404
  'an out-of-scope code file is still flagged')
321
405
  }
322
406
 
@@ -447,7 +531,7 @@ function memProbe(files) {
447
531
  // ── 23. 0.22-A: snapshot hash integrity ────────────────────────────────────
448
532
  {
449
533
  const { hashConfig } = await import('./lib/snapshot.js')
450
- const config = FLOW_PRESETS.standard.config
534
+ const config = adoptedFlow('standard')
451
535
  const h1 = hashConfig(config)
452
536
  const h2 = hashConfig(JSON.parse(JSON.stringify(config)))
453
537
  assert(h1 === h2 && /^[0-9a-f]{64}$/.test(h1), 'hashConfig is a deterministic sha256')
@@ -456,7 +540,7 @@ function memProbe(files) {
456
540
  assert(hashConfig(tampered) !== h1, 'editing the frozen config changes the hash')
457
541
  }
458
542
  {
459
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
543
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
460
544
  const exe = await registered(fs)
461
545
  await exe({ operation: 'create', task_id: 'HASH-1', title: 'x', branch: 'main' }, EXEC)
462
546
  const record = JSON.parse(fs._files.get('.dsh/task-HASH-1.json'))
@@ -491,7 +575,7 @@ function memProbe(files) {
491
575
 
492
576
  // ── 25. 0.22-B: risk downgrade approval + bindings fingerprint ─────────────
493
577
  {
494
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
578
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
495
579
  const exe = await registered(fs)
496
580
  await exe({ operation: 'create', task_id: 'RISK-1', title: 'x', branch: 'main', risk_level: 'high_risk' }, EXEC)
497
581
  await assertThrows(
@@ -504,7 +588,7 @@ function memProbe(files) {
504
588
  }
505
589
  {
506
590
  const approval = { async request() { return 'allowed-once' } }
507
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
591
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
508
592
  const exe = await registered(fs, approval)
509
593
  await exe({ operation: 'create', task_id: 'RISK-2', title: 'x', branch: 'main', risk_level: 'high_risk' }, EXEC)
510
594
  assert((await exe({ operation: 'set_risk', task_id: 'RISK-2', risk_level: 'standard' }, EXEC)).includes('risk_level set to standard'),
@@ -514,7 +598,7 @@ function memProbe(files) {
514
598
  'downgrade is recorded in the audit trail')
515
599
  }
516
600
  {
517
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
601
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
518
602
  const exe = await registered(fs)
519
603
  await exe({ operation: 'create', task_id: 'FP-1', title: 'x', branch: 'main' }, EXEC)
520
604
  const status = JSON.parse(await exe({ operation: 'status', task_id: 'FP-1' }, EXEC))
@@ -536,7 +620,7 @@ function memProbe(files) {
536
620
  )
537
621
  }
538
622
  {
539
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }), 'package.json': '{}' })
623
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')), 'package.json': '{}' })
540
624
  const exe = await registered(fs)
541
625
  await exe({ operation: 'create', task_id: 'V-2', title: 'x', branch: 'main' }, EXEC)
542
626
  await assertThrows(
@@ -546,7 +630,7 @@ function memProbe(files) {
546
630
  )
547
631
  }
548
632
  {
549
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
633
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
550
634
  const exe = await registered(fs)
551
635
  await exe({ operation: 'create', task_id: 'V-3', title: 'x', branch: 'main' }, EXEC)
552
636
  await assertThrows(() => exe({ operation: 'verify', task_id: 'V-3', passed: true }, EXEC),
@@ -558,7 +642,7 @@ function memProbe(files) {
558
642
  'legacy tasks retain their verification contract')
559
643
  }
560
644
  {
561
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
645
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
562
646
  const exe = await registered(fs)
563
647
  await exe({ operation: 'create', task_id: 'A-1', title: 'x', branch: 'main' }, EXEC)
564
648
  await exe({ operation: 'create', task_id: 'B-2', title: 'y', branch: 'main' }, EXEC)
@@ -594,7 +678,7 @@ function memProbe(files) {
594
678
 
595
679
  // ── 29. dev_task writes carry the per-call sandbox policy ───────────────────
596
680
  {
597
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
681
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
598
682
  let seenPolicy
599
683
  const origWrite = fs.writeText
600
684
  fs.writeText = async (target, content, expected, signal, sandboxPolicy) => {
@@ -616,7 +700,7 @@ function memProbe(files) {
616
700
 
617
701
  // ── 30. 0.22.x: sandbox escalation requires a human one-shot approval ───────
618
702
  {
619
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
703
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
620
704
  let seenPolicy
621
705
  const origWrite = fs.writeText
622
706
  fs.writeText = async (target, content, expected, signal, sandboxPolicy) => {
@@ -634,7 +718,7 @@ function memProbe(files) {
634
718
  }
635
719
 
636
720
  {
637
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
721
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
638
722
  const exe = await registered(fs) // no approval service
639
723
  await assertThrows(
640
724
  () => exe({
@@ -648,7 +732,7 @@ function memProbe(files) {
648
732
 
649
733
  // ── 31. 0.22.x: set_risk cannot raise risk past the flow's capabilities ─────
650
734
  {
651
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'minimal' }) })
735
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('minimal')) })
652
736
  const exe = await registered(fs)
653
737
  await exe({ operation: 'create', task_id: 'SR-1', title: 'x', branch: 'main' }, EXEC)
654
738
  await assertThrows(
@@ -658,7 +742,7 @@ function memProbe(files) {
658
742
  )
659
743
  }
660
744
  {
661
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
745
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
662
746
  const exe = await registered(fs)
663
747
  await exe({ operation: 'create', task_id: 'SR-2', title: 'x', branch: 'main' }, EXEC)
664
748
  assert((await exe({ operation: 'set_risk', task_id: 'SR-2', risk_level: 'high_risk' }, EXEC)).includes('high_risk'),
@@ -674,7 +758,7 @@ function memProbe(files) {
674
758
 
675
759
  // ── 33. 0.23: verify runs in the task's recorded project root ──────────────
676
760
  {
677
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }), 'package.json': '{}' })
761
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')), 'package.json': '{}' })
678
762
  const seenWorkdirs = []
679
763
  const shell = {
680
764
  resolve(request) { return { command: request.command, workdir: request.workdir ?? '', timeoutMs: request.timeoutMs } },
@@ -694,7 +778,7 @@ function memProbe(files) {
694
778
 
695
779
  // ── 34. 0.23: revision increments on every write (CAS foundation) ───────────
696
780
  {
697
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
781
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
698
782
  const exe = await registered(fs)
699
783
  await exe({ operation: 'create', task_id: 'CAS-1', title: 'x', branch: 'main' }, EXEC)
700
784
  const r1 = JSON.parse(fs._files.get('.dsh/task-CAS-1.json')).revision
@@ -724,7 +808,7 @@ function memProbe(files) {
724
808
 
725
809
  // A second writer between content read and write must not be silently overwritten.
726
810
  {
727
- const fs = makeFs({ '.dsh/eng.json': JSON.stringify({ flow: 'standard' }) })
811
+ const fs = makeFs({ '.dsh/eng.json': JSON.stringify(adoptRecommendation('standard')) })
728
812
  const exe = await registered(fs)
729
813
  await exe({ operation: 'create', task_id: 'CAS-RACE', title: 'race', branch: 'main' }, EXEC)
730
814
  const originalRead = fs.readText.bind(fs)