@godv61/dsh-task-engine 0.24.0 → 0.25.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 +150 -88
- package/.assessment-batch1.mjs +32 -12
- package/.e2e-presets.mjs +40 -5
- package/.enforce-test.mjs +139 -0
- package/.evidence-test.mjs +119 -0
- package/.filter-test.mjs +93 -0
- package/.freeze-test.mjs +110 -1
- package/.hook-test.mjs +8 -3
- package/.p0-test.mjs +141 -57
- package/.revision-test.mjs +17 -2
- package/.roundtrip-test.mjs +63 -0
- package/.workflow-test.mjs +31 -4
- package/README.md +4 -2
- package/docs/CHANGELOG.md +37 -0
- package/docs/configuration.md +3 -3
- package/hooks/commit-msg +93 -75
- package/lib/client.js +218 -81
- package/lib/client.js.map +1 -1
- package/lib/controller.d.ts +10 -2
- package/lib/controller.js +27 -2
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +94 -20
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +90 -0
- package/lib/engine.js +64 -0
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +14 -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 +75 -12
- package/lib/workflows.js +172 -96
- package/lib/workflows.js.map +1 -1
- package/package.json +9 -5
- package/scripts/verify-package.mjs +1 -1
package/docs/configuration.md
CHANGED
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
|
|
46
46
|
规则引用带来源(`bundled:` / `project:` / `user:`),因此同名资源不会被混淆。同一份规则可被多个技能引用,不需要复制正文;编辑共享规则时界面会显示受影响的技能。
|
|
47
47
|
|
|
48
|
-
[默认配置示例](../defaults/eng.json)
|
|
48
|
+
[默认配置示例](../defaults/eng.json)列出了标准流程采用推荐配置后的样子。
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
**预设不带任何绑定。** 一个只写了 `flow` 的配置就是字面意思:该流程的节点没有绑定,阶段仍按流程骨架流转。要一份现成的工程起点,在工作台点「采用推荐配置」,它会把技能、提交信息格式与产物字段**写入**你的配置一次;此后这些值就是你的普通配置,删掉不会补回,升级也不覆盖。
|
|
51
51
|
|
|
52
|
-
进入阶段后,`dev_task` 披露该阶段全部技能的规则内容。新任务离开阶段前会检查 Harness 的 skill
|
|
52
|
+
进入阶段后,`dev_task` 披露该阶段全部技能的规则内容。新任务离开阶段前会检查 Harness 的 skill 工具成功加载记录;技能还可以**声明自己需要哪类证据**(`command` / `artifact` / `review` / `manual` / `none`),引擎按各自形态检查对应记录——需求或设计类技能产出的是文档,不必运行一条无关命令。未声明时沿用命令回执。`status.skill_obligations` 中的 `command_receipts_required` 列出确实需要命令回执的技能。安装一个技能不会自动执行其中的脚本。
|
|
53
53
|
|
|
54
54
|
挂在终态(例如"完成")的技能在进入终态前执行。测试技能通常建议挂在"交付";现有"完成"绑定也会在审核阶段执行后才放行。标准流程 v2 在审核通过后提交,并核对真实 Git HEAD。修改文件或声明范围后,旧验证回执失效。
|
|
55
55
|
|
package/hooks/commit-msg
CHANGED
|
@@ -30,9 +30,6 @@ var import_node_fs = __toESM(require("node:fs"), 1);
|
|
|
30
30
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
31
31
|
|
|
32
32
|
// src/engine.ts
|
|
33
|
-
function formatResourceRef(ref) {
|
|
34
|
-
return `${ref.source}:${ref.name}`;
|
|
35
|
-
}
|
|
36
33
|
function artifactsForStage(stage, config2) {
|
|
37
34
|
return config2.artifacts.filter((artifact) => artifact.stage === stage);
|
|
38
35
|
}
|
|
@@ -172,19 +169,23 @@ var STANDARD = {
|
|
|
172
169
|
{ from: "\u4EA4\u4ED8", to: "\u4EE3\u7801\u5BA1\u6838", requires: ["verified"] },
|
|
173
170
|
{ from: "\u4EE3\u7801\u5BA1\u6838", to: "\u5B8C\u6210", requires: ["review_passed", "artifacts_present"] }
|
|
174
171
|
],
|
|
172
|
+
// No artifacts, no commit policy, no skills: the skeleton carries none of them.
|
|
173
|
+
artifacts: [],
|
|
174
|
+
commit: { policy: "task", message_pattern: "", message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42", checkpoints: ["\u4EE3\u7801\u5BA1\u6838"], file_scope: true },
|
|
175
|
+
high_risk_requires_verification: true
|
|
176
|
+
};
|
|
177
|
+
var STANDARD_RECOMMENDATION = {
|
|
178
|
+
commit: {
|
|
179
|
+
// Only the TEXT conventions. Policy, checkpoints and scope stay on the
|
|
180
|
+
// skeleton: they decide when a gate fires, not how a message reads.
|
|
181
|
+
message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
|
|
182
|
+
message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF08\u5982 GREET-001\uFF09\uFF0C\u5199\u7ED3\u679C\u4E0D\u5199\u7A7A\u6CDB\u52A8\u4F5C"
|
|
183
|
+
},
|
|
175
184
|
artifacts: [
|
|
176
185
|
{ stage: "\u9700\u6C42\u8BC4\u5BA1", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope", "acceptance_criteria"] },
|
|
177
186
|
{ stage: "\u8BBE\u8BA1", id: "design", name: "\u8BBE\u8BA1\u6587\u6863", fields: ["approach", "risks", "impact"] },
|
|
178
187
|
{ stage: "\u4EE3\u7801\u5BA1\u6838", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
|
|
179
188
|
],
|
|
180
|
-
commit: {
|
|
181
|
-
policy: "task",
|
|
182
|
-
message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
|
|
183
|
-
message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF08\u5982 GREET-001\uFF09\uFF0C\u5199\u7ED3\u679C\u4E0D\u5199\u7A7A\u6CDB\u52A8\u4F5C",
|
|
184
|
-
checkpoints: ["\u4EE3\u7801\u5BA1\u6838"],
|
|
185
|
-
file_scope: true
|
|
186
|
-
},
|
|
187
|
-
high_risk_requires_verification: true,
|
|
188
189
|
stage_bindings: {
|
|
189
190
|
"\u9700\u6C42\u8BC4\u5BA1": { skills: [bundled("requirement-analysis", "security-redlines")] },
|
|
190
191
|
"\u8BBE\u8BA1": { skills: [bundled("solution-design")] },
|
|
@@ -204,30 +205,38 @@ var AGILE = {
|
|
|
204
205
|
transitions: [
|
|
205
206
|
{ from: "\u9700\u6C42", to: "\u5F00\u53D1", requires: ["requirement_confirmation"] },
|
|
206
207
|
{ from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] },
|
|
208
|
+
// Entering 审查 is unconditional. A review guard on this edge would be
|
|
209
|
+
// circular — it would demand the verdict BEFORE the stage that produces it —
|
|
210
|
+
// and it would also block the commit at 交付, since a commit requires its
|
|
211
|
+
// stage's outgoing guards. What 审查 requires is declared as completion_guards
|
|
212
|
+
// instead, because a terminal stage has no outgoing edge to carry it.
|
|
207
213
|
{ from: "\u4EA4\u4ED8", to: "\u5BA1\u67E5", requires: [] }
|
|
208
214
|
],
|
|
215
|
+
artifacts: [],
|
|
216
|
+
commit: { policy: "item", message_pattern: "", message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42", checkpoints: ["\u4EA4\u4ED8"], file_scope: true },
|
|
217
|
+
high_risk_requires_verification: false,
|
|
218
|
+
// 审查 is terminal and IS the review, so there is no outgoing edge to carry
|
|
219
|
+
// the guard. Declaring it here is what makes a blocked review prevent
|
|
220
|
+
// completion; without it the stage could be reached and finished with the
|
|
221
|
+
// review failing, which is what the assessment found.
|
|
222
|
+
completion_guards: ["review_passed"]
|
|
223
|
+
};
|
|
224
|
+
var AGILE_RECOMMENDATION = {
|
|
225
|
+
commit: {
|
|
226
|
+
// `item` policy emits two label shapes: `T<n>` mid-flow and `TASK` at the
|
|
227
|
+
// closing checkpoint. The pattern accepts BOTH, so the label the engine hands
|
|
228
|
+
// the model always satisfies the rule that validates it.
|
|
229
|
+
message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
|
|
230
|
+
message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF1B\u5B9E\u65BD\u9879\u63D0\u4EA4\u7528 T1\u3001T2\uFF0C\u6536\u5C3E\u63D0\u4EA4\u7528 TASK"
|
|
231
|
+
},
|
|
209
232
|
artifacts: [
|
|
210
233
|
{ stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] },
|
|
211
234
|
// The 审查 stage binds code-review, whose instructions write this artifact with
|
|
212
|
-
// `record artifact=review`. Declaring it
|
|
213
|
-
//
|
|
214
|
-
//
|
|
235
|
+
// `record artifact=review`. Declaring it keeps that binding executable: the
|
|
236
|
+
// engine refuses an undeclared artifact, so a bound skill pointing at one would
|
|
237
|
+
// be a contract the recommendation made impossible to satisfy.
|
|
215
238
|
{ stage: "\u5BA1\u67E5", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
|
|
216
239
|
],
|
|
217
|
-
commit: {
|
|
218
|
-
// `item` policy emits two label shapes: `T<n>` for a mid-flow item commit and
|
|
219
|
-
// `TASK` for the closing commit at the checkpoint. The pattern below accepts
|
|
220
|
-
// BOTH, so the label the engine hands the model always satisfies the preset's
|
|
221
|
-
// own rule. Previously it accepted only `T\d+` while the checkpoint stage
|
|
222
|
-
// returned `TASK`, which made the closing commit impossible to phrase: the
|
|
223
|
-
// status line and the validator disagreed about the contract.
|
|
224
|
-
policy: "item",
|
|
225
|
-
message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
|
|
226
|
-
message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF1B\u5B9E\u65BD\u9879\u63D0\u4EA4\u7528 T1\u3001T2\uFF0C\u6536\u5C3E\u63D0\u4EA4\u7528 TASK",
|
|
227
|
-
checkpoints: ["\u4EA4\u4ED8"],
|
|
228
|
-
file_scope: true
|
|
229
|
-
},
|
|
230
|
-
high_risk_requires_verification: false,
|
|
231
240
|
stage_bindings: {
|
|
232
241
|
"\u9700\u6C42": { skills: [bundled("requirement-analysis", "security-redlines")] },
|
|
233
242
|
"\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
|
|
@@ -242,18 +251,13 @@ var MINIMAL = {
|
|
|
242
251
|
{ from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] }
|
|
243
252
|
],
|
|
244
253
|
artifacts: [],
|
|
245
|
-
commit: {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
checkpoints: ["\u4EA4\u4ED8"],
|
|
250
|
-
file_scope: false
|
|
251
|
-
},
|
|
252
|
-
high_risk_requires_verification: false,
|
|
254
|
+
commit: { policy: "task", message_pattern: "", message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42", checkpoints: ["\u4EA4\u4ED8"], file_scope: false },
|
|
255
|
+
high_risk_requires_verification: false
|
|
256
|
+
};
|
|
257
|
+
var MINIMAL_RECOMMENDATION = {
|
|
253
258
|
// A fast-change flow checks each change once, not twice under two headings a
|
|
254
259
|
// short change rarely distinguishes. The item still has to be reviewed — what
|
|
255
|
-
// drops is the duplicated verdict, which is where the weight actually was
|
|
256
|
-
// flow used to carry the same per-item audit burden as the full one.
|
|
260
|
+
// drops is the duplicated verdict, which is where the weight actually was.
|
|
257
261
|
review_depth: "single",
|
|
258
262
|
stage_bindings: {
|
|
259
263
|
"\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
|
|
@@ -269,54 +273,61 @@ function deriveCapabilities(config2) {
|
|
|
269
273
|
if (config2.commit.file_scope) caps.push("file_scope");
|
|
270
274
|
return caps;
|
|
271
275
|
}
|
|
272
|
-
function preset(id, version, label, description, config2) {
|
|
273
|
-
return {
|
|
276
|
+
function preset(id, version, label, description, config2, recommendation) {
|
|
277
|
+
return {
|
|
278
|
+
id,
|
|
279
|
+
version,
|
|
280
|
+
label,
|
|
281
|
+
description,
|
|
282
|
+
capabilities: deriveCapabilities(config2),
|
|
283
|
+
config: config2,
|
|
284
|
+
// exactOptionalPropertyTypes: omit rather than assign undefined.
|
|
285
|
+
...recommendation !== void 0 ? { recommendation } : {}
|
|
286
|
+
};
|
|
274
287
|
}
|
|
275
288
|
var FLOW_PRESETS = {
|
|
276
|
-
standard: preset("standard",
|
|
277
|
-
// Version
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
// Version
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
289
|
+
standard: preset("standard", 3, "\u5B8C\u6574\u7814\u53D1", "\u65B0\u529F\u80FD\u3001\u67B6\u6784\u6216\u8DE8\u6A21\u5757\u6539\u52A8\u3001\u9AD8\u98CE\u9669\u4EFB\u52A1\uFF1A\u9700\u6C42\u786E\u8BA4 \u2192 \u65B9\u6848\u786E\u8BA4 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u8BC1 \u2192 \u5BA1\u6838 \u2192 \u63D0\u4EA4", STANDARD, STANDARD_RECOMMENDATION),
|
|
290
|
+
// Version 4 for agile: 交付 → 审查 now requires review_passed, where it previously
|
|
291
|
+
// required nothing. That is a gate-semantics change, so a task created before it
|
|
292
|
+
// keeps its frozen version 3 config and behaves exactly as it did.
|
|
293
|
+
//
|
|
294
|
+
// Version 3 across all three: the preset is now a bare skeleton and its former
|
|
295
|
+
// built-in bindings, commit rule and artifacts are a separate recommendation a
|
|
296
|
+
// user adopts explicitly. Tasks created before this keep their frozen config and
|
|
297
|
+
// are unaffected; a project's existing config is likewise left exactly as it is.
|
|
298
|
+
agile: preset("agile", 4, "\u65E5\u5E38\u8FED\u4EE3", "\u76EE\u6807\u660E\u786E\u7684\u5E38\u89C4\u529F\u80FD\u4E0E\u7F3A\u9677\u4FEE\u590D\uFF1A\u76EE\u6807\u4E0E\u9A8C\u6536 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u6536\u4E0E\u5BA1\u67E5 \u2192 \u63D0\u4EA4", AGILE, AGILE_RECOMMENDATION),
|
|
299
|
+
minimal: preset("minimal", 3, "\u5FEB\u901F\u4FEE\u6539", "\u5C40\u90E8\u3001\u4F4E\u98CE\u9669\u3001\u65B9\u6848\u660E\u786E\u7684\u6539\u52A8\uFF1A\u4FEE\u6539 \u2192 \u68C0\u67E5\u4E0E\u63D0\u4EA4\uFF0C\u6BCF\u9879\u4E00\u6B21\u68C0\u67E5", MINIMAL, MINIMAL_RECOMMENDATION)
|
|
286
300
|
};
|
|
287
301
|
function dedupe(names) {
|
|
288
302
|
return [...new Set(names)];
|
|
289
303
|
}
|
|
290
|
-
function
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
seen.add(key);
|
|
302
|
-
skills.push(skill);
|
|
304
|
+
function applyProjectConfig(base, project) {
|
|
305
|
+
if (project === void 0) return base;
|
|
306
|
+
const next = { ...base };
|
|
307
|
+
if (project.stage_bindings !== void 0) {
|
|
308
|
+
const bindings = {};
|
|
309
|
+
for (const [stage, binding] of Object.entries(project.stage_bindings)) {
|
|
310
|
+
const entry = {};
|
|
311
|
+
if ((binding.skills ?? []).length > 0) entry.skills = [...binding.skills];
|
|
312
|
+
const legacy = dedupe(binding.legacy_rules ?? []);
|
|
313
|
+
if (legacy.length > 0) entry.legacy_rules = legacy;
|
|
314
|
+
bindings[stage] = entry;
|
|
303
315
|
}
|
|
304
|
-
|
|
305
|
-
const binding = {};
|
|
306
|
-
if (skills.length > 0) binding.skills = skills;
|
|
307
|
-
if (legacy.length > 0) binding.legacy_rules = legacy;
|
|
308
|
-
merged[stage] = binding;
|
|
309
|
-
changed = true;
|
|
316
|
+
next.stage_bindings = bindings;
|
|
310
317
|
}
|
|
311
|
-
if (
|
|
312
|
-
|
|
318
|
+
if (project.commit !== void 0) next.commit = project.commit;
|
|
319
|
+
if (project.artifacts !== void 0) next.artifacts = project.artifacts;
|
|
320
|
+
if (project.review_depth !== void 0) next.review_depth = project.review_depth;
|
|
321
|
+
if (project.commit_required !== void 0) next.commit_required = project.commit_required;
|
|
322
|
+
return next;
|
|
313
323
|
}
|
|
314
|
-
function resolveFlow(flow,
|
|
324
|
+
function resolveFlow(flow, project) {
|
|
315
325
|
const preset2 = FLOW_PRESETS[flow];
|
|
316
326
|
if (preset2 === void 0) {
|
|
317
327
|
return { ok: false, code: "UNKNOWN_FLOW", flow, knownFlows: Object.keys(FLOW_PRESETS) };
|
|
318
328
|
}
|
|
319
|
-
|
|
329
|
+
const { flow: _ignored, ...rest } = project ?? { flow };
|
|
330
|
+
return { ok: true, config: applyProjectConfig(preset2.config, { flow, ...rest }), preset: preset2 };
|
|
320
331
|
}
|
|
321
332
|
|
|
322
333
|
// src/snapshot.ts
|
|
@@ -471,8 +482,15 @@ function loadWorkflow(parsed) {
|
|
|
471
482
|
if (typeof flow !== "string" || flow.trim() === "") {
|
|
472
483
|
refuse('\u9879\u76EE .dsh/eng.json \u7F3A "flow" \u5B57\u6BB5\u2014\u2014\u8BBE\u4E3A standard|agile|minimal\uFF0C\u6216\u5220\u9664\u8BE5\u6587\u4EF6');
|
|
473
484
|
}
|
|
474
|
-
const
|
|
475
|
-
|
|
485
|
+
const project = {
|
|
486
|
+
flow,
|
|
487
|
+
...typeof parsed.stage_bindings === "object" && parsed.stage_bindings !== null && !Array.isArray(parsed.stage_bindings) ? { stage_bindings: parsed.stage_bindings } : {},
|
|
488
|
+
...typeof parsed.commit === "object" && parsed.commit !== null && !Array.isArray(parsed.commit) ? { commit: parsed.commit } : {},
|
|
489
|
+
...Array.isArray(parsed.artifacts) ? { artifacts: parsed.artifacts } : {},
|
|
490
|
+
...typeof parsed.review_depth === "string" ? { review_depth: parsed.review_depth } : {},
|
|
491
|
+
...typeof parsed.commit_required === "boolean" ? { commit_required: parsed.commit_required } : {}
|
|
492
|
+
};
|
|
493
|
+
const resolved = resolveFlow(flow, project);
|
|
476
494
|
if (!resolved.ok) {
|
|
477
495
|
refuse(`\u672A\u77E5\u6D41\u7A0B "${resolved.flow}"\uFF08\u5DF2\u77E5\u6D41\u7A0B\uFF1A${resolved.knownFlows.join("\u3001")}\uFF09`);
|
|
478
496
|
}
|