@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.
@@ -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 工具成功加载记录;附加技能还须记录 `skill_result`(技能名、执行场景、真实验收命令回执)。`status.skill_obligations` 中的 `command_receipts_required` 列出需要回执的技能;七个内置技能使用已有的记录、验证、审核和提交操作,无需重复登记附加回执。安装一个技能不会自动执行其中的脚本。
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 here keeps that binding executable:
213
- // the engine refuses an undeclared artifact, so a bound skill pointing at one
214
- // would be a contract the preset itself made impossible to satisfy.
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
- policy: "task",
247
- message_pattern: "",
248
- message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42",
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: this
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 { id, version, label, description, capabilities: deriveCapabilities(config2), config: config2 };
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", 2, "\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),
277
- // Version 2: the closing-commit label shape was unified with the message pattern,
278
- // and the `review` artifact the bound code-review skill writes was declared. Tasks
279
- // created before this keep their frozen version 1 config and are unaffected.
280
- agile: preset("agile", 2, "\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),
281
- // Version 2: per-item review depth is declared as `single` instead of inheriting
282
- // the full flow's two verdicts per item. Tasks created before this keep their
283
- // frozen version 1 config, which has no `review_depth` and therefore reads as
284
- // two-stage exactly as it behaved.
285
- minimal: preset("minimal", 2, "\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)
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 mergeBindings(base, override) {
291
- if (override === void 0) return base;
292
- let changed = false;
293
- const merged = { ...base.stage_bindings };
294
- for (const [stage, addition] of Object.entries(override)) {
295
- const existing = merged[stage];
296
- const skills = [...existing?.skills ?? []];
297
- const seen = new Set(skills.map((skill) => formatResourceRef(skill.skill)));
298
- for (const skill of addition.skills ?? []) {
299
- const key = formatResourceRef(skill.skill);
300
- if (seen.has(key)) continue;
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
- const legacy = dedupe([...existing?.legacy_rules ?? [], ...addition.legacy_rules ?? []]);
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 (!changed) return base;
312
- return { ...base, stage_bindings: merged };
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, override) {
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
- return { ok: true, config: mergeBindings(preset2.config, override?.stage_bindings), preset: preset2 };
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 override = typeof parsed.stage_bindings === "object" && parsed.stage_bindings !== null && !Array.isArray(parsed.stage_bindings) ? { stage_bindings: parsed.stage_bindings } : void 0;
475
- const resolved = resolveFlow(flow, override);
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
  }