@godv61/dsh-task-engine 0.23.9 → 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/hooks/commit-msg CHANGED
@@ -33,8 +33,9 @@ var import_node_path = __toESM(require("node:path"), 1);
33
33
  function artifactsForStage(stage, config2) {
34
34
  return config2.artifacts.filter((artifact) => artifact.stage === stage);
35
35
  }
36
- function todosBlockers(state2) {
36
+ function todosBlockers(state2, config2) {
37
37
  if (state2.items.length === 0) return ["no implementation items"];
38
+ const depth = config2?.review_depth ?? "two-stage";
38
39
  const blockers = [];
39
40
  for (const item of state2.items) {
40
41
  if (item.status !== "done") {
@@ -42,11 +43,13 @@ function todosBlockers(state2) {
42
43
  continue;
43
44
  }
44
45
  if (item.review === void 0) {
45
- blockers.push(`item "${item.id}" is done but has no two-stage review`);
46
+ blockers.push(`item "${item.id}" is done but has no review`);
46
47
  continue;
47
48
  }
48
- if (item.review.spec.outcome !== "pass") blockers.push(`item "${item.id}" spec review is not pass`);
49
- if (item.review.quality.outcome !== "pass") blockers.push(`item "${item.id}" quality review is not pass`);
49
+ if (item.review.spec?.outcome !== "pass") blockers.push(`item "${item.id}" spec review is not pass`);
50
+ if (depth === "two-stage" && item.review.quality?.outcome !== "pass") {
51
+ blockers.push(`item "${item.id}" quality review is not pass`);
52
+ }
50
53
  }
51
54
  return blockers;
52
55
  }
@@ -57,7 +60,7 @@ function guardSatisfied(guard, state2, config2) {
57
60
  case "solution_confirmation":
58
61
  return state2.solution_confirmed;
59
62
  case "todos_done":
60
- return todosBlockers(state2).length === 0;
63
+ return todosBlockers(state2, config2).length === 0;
61
64
  case "verified": {
62
65
  if (!state2.verification.passed) return false;
63
66
  if (config2.high_risk_requires_verification && state2.risk_level === "high_risk") {
@@ -87,11 +90,45 @@ function validateCommitMessage(message2, config2) {
87
90
  errors: [`commit summary "${message2}" does not match ${config2.commit.message_hint}`]
88
91
  };
89
92
  }
93
+ function verificationHeldStages(config2) {
94
+ const held = /* @__PURE__ */ new Set();
95
+ const queue = config2.transitions.filter((transition) => (transition.requires ?? []).includes("verified")).map((transition) => transition.to);
96
+ while (queue.length > 0) {
97
+ const stage = queue.shift();
98
+ if (held.has(stage)) continue;
99
+ held.add(stage);
100
+ for (const transition of config2.transitions) {
101
+ if (transition.from === stage && !held.has(transition.to)) queue.push(transition.to);
102
+ }
103
+ }
104
+ return held;
105
+ }
106
+ function verificationBlockers(state2, config2) {
107
+ if (state2.execution_version !== 1) return [];
108
+ if (!verificationHeldStages(config2).has(state2.stage)) return [];
109
+ const blockers = [];
110
+ if (!state2.verification.passed) {
111
+ blockers.push("this flow requires a passing verification at this stage, and the current verification is not passing");
112
+ }
113
+ if (config2.high_risk_requires_verification && state2.risk_level === "high_risk") {
114
+ const receipt = state2.verification.receipt;
115
+ if (receipt === void 0) {
116
+ blockers.push("a high-risk task needs a real command receipt, and none is recorded");
117
+ } else if (receipt.exit_code !== 0 || receipt.timed_out || receipt.aborted) {
118
+ blockers.push("a high-risk task needs a command that exited 0 without timing out or aborting");
119
+ }
120
+ }
121
+ return blockers;
122
+ }
90
123
  function commitCheckpoint(state2, config2) {
91
124
  const { policy, checkpoints } = config2.commit;
92
125
  if (policy === "manual") {
93
126
  return { allowed: false, reason: "commit policy is manual \u2014 await an explicit user instruction" };
94
127
  }
128
+ const verification = verificationBlockers(state2, config2);
129
+ if (verification.length > 0) {
130
+ return { allowed: false, reason: verification.join("; ") };
131
+ }
95
132
  if (checkpoints.includes(state2.stage)) {
96
133
  const ready = config2.transitions.filter((transition) => transition.from === state2.stage).every((transition) => (transition.requires ?? []).every((g) => guardSatisfied(g, state2, config2)));
97
134
  if (!ready) {
@@ -118,6 +155,10 @@ function checkFileScope(state2, committing, config2) {
118
155
  }
119
156
 
120
157
  // src/workflows.ts
158
+ function bundled(skill, ...rules) {
159
+ const ref = (name) => ({ source: "bundled", name });
160
+ return { skill: ref(skill), rules: rules.map(ref) };
161
+ }
121
162
  var STANDARD = {
122
163
  stages: ["\u9700\u6C42\u8BC4\u5BA1", "\u8BBE\u8BA1", "\u5F00\u53D1", "\u4EA4\u4ED8", "\u4EE3\u7801\u5BA1\u6838", "\u5B8C\u6210"],
123
164
  start_stage: "\u9700\u6C42\u8BC4\u5BA1",
@@ -128,25 +169,34 @@ var STANDARD = {
128
169
  { from: "\u4EA4\u4ED8", to: "\u4EE3\u7801\u5BA1\u6838", requires: ["verified"] },
129
170
  { from: "\u4EE3\u7801\u5BA1\u6838", to: "\u5B8C\u6210", requires: ["review_passed", "artifacts_present"] }
130
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
+ },
131
184
  artifacts: [
132
185
  { stage: "\u9700\u6C42\u8BC4\u5BA1", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope", "acceptance_criteria"] },
133
186
  { stage: "\u8BBE\u8BA1", id: "design", name: "\u8BBE\u8BA1\u6587\u6863", fields: ["approach", "risks", "impact"] },
134
187
  { stage: "\u4EE3\u7801\u5BA1\u6838", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
135
188
  ],
136
- commit: {
137
- policy: "task",
138
- message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
139
- 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",
140
- checkpoints: ["\u4EE3\u7801\u5BA1\u6838"],
141
- file_scope: true
142
- },
143
- high_risk_requires_verification: true,
144
189
  stage_bindings: {
145
- "\u9700\u6C42\u8BC4\u5BA1": { skills: ["requirement-analysis"], rules: ["security-redlines"] },
146
- "\u8BBE\u8BA1": { skills: ["solution-design"] },
147
- "\u5F00\u53D1": { skills: ["code-implement"], rules: ["coding-conventions"] },
148
- "\u4EA4\u4ED8": { skills: ["code-verify"], rules: ["coding-conventions"] },
149
- "\u4EE3\u7801\u5BA1\u6838": { skills: ["code-review", "code-commit"], rules: ["security-redlines", "commit-conventions"] }
190
+ "\u9700\u6C42\u8BC4\u5BA1": { skills: [bundled("requirement-analysis", "security-redlines")] },
191
+ "\u8BBE\u8BA1": { skills: [bundled("solution-design")] },
192
+ "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
193
+ "\u4EA4\u4ED8": { skills: [bundled("code-verify")] },
194
+ "\u4EE3\u7801\u5BA1\u6838": {
195
+ skills: [
196
+ bundled("code-review", "coding-conventions", "security-redlines"),
197
+ bundled("code-commit", "commit-conventions")
198
+ ]
199
+ }
150
200
  }
151
201
  };
152
202
  var AGILE = {
@@ -155,24 +205,43 @@ var AGILE = {
155
205
  transitions: [
156
206
  { from: "\u9700\u6C42", to: "\u5F00\u53D1", requires: ["requirement_confirmation"] },
157
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.
158
213
  { from: "\u4EA4\u4ED8", to: "\u5BA1\u67E5", requires: [] }
159
214
  ],
160
- artifacts: [
161
- { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] }
162
- ],
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 = {
163
225
  commit: {
164
- policy: "item",
165
- message_pattern: "^\u3010(\\S+)\u3011\u3010T\\d+\u3011.+",
166
- message_hint: "\u3010<task_id>\u3011\u3010T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id",
167
- checkpoints: ["\u4EA4\u4ED8"],
168
- file_scope: true
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"
169
231
  },
170
- high_risk_requires_verification: false,
232
+ artifacts: [
233
+ { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] },
234
+ // The 审查 stage binds code-review, whose instructions write this artifact with
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.
238
+ { stage: "\u5BA1\u67E5", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
239
+ ],
171
240
  stage_bindings: {
172
- "\u9700\u6C42": { skills: ["requirement-analysis"] },
173
- "\u5F00\u53D1": { skills: ["code-implement"], rules: ["coding-conventions"] },
174
- "\u4EA4\u4ED8": { skills: ["code-verify", "code-commit"], rules: ["commit-conventions"] },
175
- "\u5BA1\u67E5": { skills: ["code-review"] }
241
+ "\u9700\u6C42": { skills: [bundled("requirement-analysis", "security-redlines")] },
242
+ "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
243
+ "\u4EA4\u4ED8": { skills: [bundled("code-verify"), bundled("code-commit", "commit-conventions")] },
244
+ "\u5BA1\u67E5": { skills: [bundled("code-review", "coding-conventions", "security-redlines")] }
176
245
  }
177
246
  };
178
247
  var MINIMAL = {
@@ -182,17 +251,17 @@ var MINIMAL = {
182
251
  { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] }
183
252
  ],
184
253
  artifacts: [],
185
- commit: {
186
- policy: "task",
187
- message_pattern: "",
188
- message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42",
189
- checkpoints: ["\u4EA4\u4ED8"],
190
- file_scope: false
191
- },
192
- 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 = {
258
+ // A fast-change flow checks each change once, not twice under two headings a
259
+ // short change rarely distinguishes. The item still has to be reviewed — what
260
+ // drops is the duplicated verdict, which is where the weight actually was.
261
+ review_depth: "single",
193
262
  stage_bindings: {
194
- "\u5F00\u53D1": { skills: ["code-implement"] },
195
- "\u4EA4\u4ED8": { skills: ["code-commit"], rules: ["commit-conventions"] }
263
+ "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
264
+ "\u4EA4\u4ED8": { skills: [bundled("code-commit", "commit-conventions")] }
196
265
  }
197
266
  };
198
267
  function deriveCapabilities(config2) {
@@ -204,40 +273,61 @@ function deriveCapabilities(config2) {
204
273
  if (config2.commit.file_scope) caps.push("file_scope");
205
274
  return caps;
206
275
  }
207
- function preset(id, version, label, description, config2) {
208
- 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
+ };
209
287
  }
210
288
  var FLOW_PRESETS = {
211
- standard: preset("standard", 2, "\u6807\u51C6\u7814\u53D1", "\u9700\u6C42\u8BC4\u5BA1 \u2192 \u8BBE\u8BA1 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u4EE3\u7801\u5BA1\u6838\uFF0C\u5BA1\u6838\u540E\u63D0\u4EA4", STANDARD),
212
- agile: preset("agile", 1, "\u654F\u6377\u8F7B\u91CF", "\u9700\u6C42 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u5BA1\u67E5\uFF0C\u56DB\u9636\u6BB5\u3001\u5C11\u4EA7\u7269", AGILE),
213
- minimal: preset("minimal", 1, "\u7EAF\u4EE3\u7801", "\u5F00\u53D1 \u2192 \u4EA4\u4ED8\uFF0C\u4E24\u9636\u6BB5\uFF0C\u53EA\u7559\u63D0\u4EA4\u95E8\u7981", 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)
214
300
  };
215
301
  function dedupe(names) {
216
302
  return [...new Set(names)];
217
303
  }
218
- function mergeBindings(base, override) {
219
- if (override === void 0) return base;
220
- let changed = false;
221
- const merged = { ...base.stage_bindings };
222
- for (const [stage, addition] of Object.entries(override)) {
223
- const existing = merged[stage];
224
- const skills = dedupe([...existing?.skills ?? [], ...addition.skills ?? []]);
225
- const rules = dedupe([...existing?.rules ?? [], ...addition.rules ?? []]);
226
- const binding = {};
227
- if (skills.length > 0) binding.skills = skills;
228
- if (rules.length > 0) binding.rules = rules;
229
- merged[stage] = binding;
230
- changed = true;
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;
315
+ }
316
+ next.stage_bindings = bindings;
231
317
  }
232
- if (!changed) return base;
233
- 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;
234
323
  }
235
- function resolveFlow(flow, override) {
324
+ function resolveFlow(flow, project) {
236
325
  const preset2 = FLOW_PRESETS[flow];
237
326
  if (preset2 === void 0) {
238
327
  return { ok: false, code: "UNKNOWN_FLOW", flow, knownFlows: Object.keys(FLOW_PRESETS) };
239
328
  }
240
- 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 };
241
331
  }
242
332
 
243
333
  // src/snapshot.ts
@@ -392,8 +482,15 @@ function loadWorkflow(parsed) {
392
482
  if (typeof flow !== "string" || flow.trim() === "") {
393
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');
394
484
  }
395
- const override = typeof parsed.stage_bindings === "object" && parsed.stage_bindings !== null && !Array.isArray(parsed.stage_bindings) ? { stage_bindings: parsed.stage_bindings } : void 0;
396
- 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);
397
494
  if (!resolved.ok) {
398
495
  refuse(`\u672A\u77E5\u6D41\u7A0B "${resolved.flow}"\uFF08\u5DF2\u77E5\u6D41\u7A0B\uFF1A${resolved.knownFlows.join("\u3001")}\uFF09`);
399
496
  }