@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.
- package/.acceptance.mjs +101 -39
- package/.assessment-batch1.mjs +32 -12
- package/.e2e-presets.mjs +40 -5
- package/.enforce-test.mjs +139 -0
- package/.evidence-test.mjs +118 -0
- package/.filter-test.mjs +93 -0
- package/.freeze-test.mjs +110 -1
- package/.hook-test.mjs +195 -190
- package/.p0-test.mjs +130 -46
- package/.preset-test.mjs +160 -160
- package/.revision-test.mjs +32 -3
- package/.roundtrip-test.mjs +178 -0
- package/.workflow-test.mjs +117 -4
- package/README.md +10 -8
- package/cordis.patch.yml +10 -10
- package/defaults/eng.json +70 -22
- package/docs/BRIEF-FOR-REVIEW.md +162 -162
- package/docs/CHANGELOG.md +49 -0
- package/docs/README.md +40 -40
- package/docs/configuration.md +14 -11
- package/docs/development.md +1 -1
- package/docs/faq.md +4 -4
- package/docs/listing/godv61__dsh-task-engine.yml +5 -5
- package/docs/listing/submission.md +83 -83
- package/docs/manual.html +31 -26
- package/docs/releases/0.23.2.md +21 -21
- package/docs/roadmap.md +34 -34
- 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
- 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
- package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -65
- package/hooks/commit-msg +159 -72
- package/lib/client.js +963 -571
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +11 -2
- package/lib/controller.js +33 -5
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +170 -28
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +105 -0
- package/lib/engine.js +93 -5
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +17 -4
- package/lib/hook.js.map +1 -1
- package/lib/skill-audit.d.ts +14 -3
- package/lib/skill-audit.js +66 -3
- package/lib/skill-audit.js.map +1 -1
- package/lib/workflows.d.ts +78 -12
- package/lib/workflows.js +258 -94
- package/lib/workflows.js.map +1 -1
- package/package.json +9 -5
- package/preset/agent.cordis.yml +21 -21
- package/preset/enable.mjs +87 -87
- package/preset/persona.md +4 -4
- package/preset/preset.yml +1 -1
- package/rules/coding-conventions.md +6 -6
- package/rules/commit-conventions.md +6 -6
- package/rules/security-redlines.md +5 -5
- package/scripts/verify-dsh-compat.mjs +144 -144
- package/scripts/verify-package.mjs +1 -1
- 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
|
-
|
|
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.
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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
|
|
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
|
-
|
|
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, '
|
|
229
|
-
assert(
|
|
230
|
-
assert(sec.content.includes('
|
|
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(
|
|
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 = { ...
|
|
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(
|
|
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 =
|
|
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 =
|
|
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('随意',
|
|
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'],
|
|
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'],
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|