@godv61/dsh-task-engine 0.19.2 → 0.20.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
@@ -1,319 +1,428 @@
1
1
  #!/usr/bin/env node
2
- // dsh-task-engine commit-msg hook — mechanical commit gate.
3
- //
4
- // Installed into `.git/hooks/commit-msg` by `dev_task` (operation=install_hook).
5
- // Self-contained mirror of src/engine.ts (`commitCheckpoint` + `validateCommitMessage`
6
- // + `guardSatisfied`). KEEP IN SYNC with that source when the gate semantics change.
7
- //
8
- // Policy: refuse any `git commit` that is not backed by a dev_task record sitting
9
- // at a commit-allowed checkpoint whose message matches the configured pattern.
10
- 'use strict'
11
2
 
12
- const fs = require('node:fs')
13
- const path = require('node:path')
14
- const { execSync } = require('node:child_process')
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
15
26
 
16
- const messageFile = process.argv[2]
27
+ // src/hook.ts
28
+ var import_node_child_process = require("node:child_process");
29
+ var import_node_fs = __toESM(require("node:fs"), 1);
30
+ var import_node_path = __toESM(require("node:path"), 1);
17
31
 
18
- function refuse(reason) {
19
- console.error('')
20
- console.error('dsh-task-engine 提交门禁拒绝: ' + reason)
21
- console.error('请先用 dev_task 走完前置阶段(或修正提交消息),再提交。')
22
- console.error('')
23
- process.exit(1)
32
+ // src/engine.ts
33
+ function artifactsForStage(stage, config2) {
34
+ return config2.artifacts.filter((artifact) => artifact.stage === stage);
24
35
  }
25
-
26
- function readJson(abs) {
27
- try {
28
- return JSON.parse(fs.readFileSync(abs, 'utf8'))
29
- } catch {
30
- return undefined
36
+ function todosBlockers(state2) {
37
+ if (state2.items.length === 0) return ["no implementation items"];
38
+ const blockers = [];
39
+ for (const item of state2.items) {
40
+ if (item.status !== "done") {
41
+ blockers.push(`item "${item.id}" is ${item.status}`);
42
+ continue;
43
+ }
44
+ if (item.review === void 0) {
45
+ blockers.push(`item "${item.id}" is done but has no two-stage review`);
46
+ continue;
47
+ }
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`);
31
50
  }
51
+ return blockers;
32
52
  }
33
-
34
- // Mirrors src/workflows.ts FLOW_CONFIGS — keep in sync.
35
- const FLOWS = {
36
- standard: {
37
- stages: ['需求评审', '设计', '开发', '交付', '代码审核', '完成'],
38
- start_stage: '需求评审',
39
- transitions: [
40
- { from: '需求评审', to: '设计', requires: ['requirement_confirmation', 'artifacts_present'] },
41
- { from: '设计', to: '开发', requires: ['solution_confirmation', 'artifacts_present'] },
42
- { from: '开发', to: '交付', requires: ['todos_done'] },
43
- { from: '交付', to: '代码审核', requires: ['verified'] },
44
- { from: '代码审核', to: '完成', requires: ['review_passed', 'artifacts_present'] },
45
- ],
46
- artifacts: [
47
- { stage: '需求评审', id: 'requirement', name: '需求说明', fields: ['scope', 'acceptance_criteria'] },
48
- { stage: '设计', id: 'design', name: '设计文档', fields: ['approach', 'risks', 'impact'] },
49
- { stage: '代码审核', id: 'review', name: '评审记录', fields: ['conclusion', 'issues'] },
50
- ],
51
- commit: {
52
- policy: 'task',
53
- message_pattern: '^【(\\S+)】【(?:TASK|T\\d+)】.+',
54
- message_hint: '【<task_id>】【TASK/T1】说明',
55
- checkpoints: ['交付'],
56
- file_scope: true,
57
- },
58
- high_risk_requires_verification: true,
59
- },
60
- agile: {
61
- stages: ['需求', '开发', '交付', '审查'],
62
- start_stage: '需求',
63
- transitions: [
64
- { from: '需求', to: '开发', requires: ['requirement_confirmation'] },
65
- { from: '开发', to: '交付', requires: ['todos_done'] },
66
- { from: '交付', to: '审查', requires: [] },
67
- ],
68
- artifacts: [
69
- { stage: '需求', id: 'requirement', name: '需求说明', fields: ['scope'] },
70
- ],
71
- commit: {
72
- policy: 'item',
73
- message_pattern: '^【(\\S+)】【T\\d+】.+',
74
- message_hint: '【<task_id>】【T1】说明',
75
- checkpoints: ['交付'],
76
- file_scope: true,
77
- },
78
- high_risk_requires_verification: false,
79
- },
80
- minimal: {
81
- stages: ['开发', '交付'],
82
- start_stage: '开发',
83
- transitions: [
84
- { from: '开发', to: '交付', requires: ['todos_done'] },
85
- ],
86
- artifacts: [],
87
- commit: {
88
- policy: 'task',
89
- message_pattern: '',
90
- message_hint: '无格式要求',
91
- checkpoints: ['交付'],
92
- file_scope: false,
93
- },
94
- high_risk_requires_verification: false,
95
- },
96
- }
97
-
98
- function loadWorkflow(parsed) {
99
- const flow = parsed && typeof parsed.flow === 'string' ? parsed.flow : 'standard'
100
- return FLOWS[flow] || FLOWS.standard
101
- }
102
-
103
- // Mirrors engine.guardSatisfied.
104
- function guardSatisfied(guard, state, config) {
53
+ function guardSatisfied(guard, state2, config2) {
105
54
  switch (guard) {
106
- case 'requirement_confirmation': return state.requirement_confirmed === true
107
- case 'solution_confirmation': return state.solution_confirmed === true
108
- case 'todos_done':
109
- return Array.isArray(state.items) && state.items.length > 0 && state.items.every(item => item.status === 'done')
110
- case 'verified':
111
- if (!state.verification || state.verification.passed !== true) return false
112
- return config.high_risk_requires_verification && state.risk_level === 'high_risk'
113
- ? Array.isArray(state.verification.evidence) && state.verification.evidence.some(evidence => evidence.trim() !== '')
114
- : true
115
- case 'review_passed': return state.review && state.review.outcome === 'pass'
116
- case 'artifacts_present': {
117
- const recorded = state.artifacts || {}
118
- return (config.artifacts || []).filter(artifact => artifact.stage === state.stage).every(artifact => {
119
- const fields = recorded[artifact.id]
120
- if (!fields) return false
121
- return (artifact.fields || []).every(field => typeof fields[field] === 'string' && fields[field].trim() !== '')
122
- })
55
+ case "requirement_confirmation":
56
+ return state2.requirement_confirmed;
57
+ case "solution_confirmation":
58
+ return state2.solution_confirmed;
59
+ case "todos_done":
60
+ return todosBlockers(state2).length === 0;
61
+ case "verified": {
62
+ if (!state2.verification.passed) return false;
63
+ if (config2.high_risk_requires_verification && state2.risk_level === "high_risk") {
64
+ const receipt = state2.verification.receipt;
65
+ return receipt !== void 0 && receipt.exit_code === 0 && !receipt.timed_out && !receipt.aborted;
66
+ }
67
+ return true;
68
+ }
69
+ case "review_passed":
70
+ return state2.review.outcome === "pass";
71
+ case "artifacts_present": {
72
+ const required = artifactsForStage(state2.stage, config2);
73
+ return required.every((artifact) => {
74
+ const recorded = state2.artifacts[artifact.id];
75
+ if (recorded === void 0) return false;
76
+ return artifact.fields.every((field) => recorded[field] !== void 0 && recorded[field].trim() !== "");
77
+ });
123
78
  }
124
- default: return false
125
79
  }
126
80
  }
127
-
128
- // Mirrors engine.commitCheckpoint.
129
- function commitCheckpoint(state, config) {
130
- const { policy, checkpoints } = config.commit
131
- if (policy === 'manual') {
132
- return { allowed: false, reason: 'commit policy is manual — await an explicit user instruction' }
81
+ function validateCommitMessage(message2, config2) {
82
+ if (config2.commit.message_pattern === "") return { ok: true };
83
+ const re = new RegExp(config2.commit.message_pattern);
84
+ if (re.test(message2)) return { ok: true };
85
+ return {
86
+ ok: false,
87
+ errors: [`commit summary "${message2}" does not match ${config2.commit.message_hint}`]
88
+ };
89
+ }
90
+ function commitCheckpoint(state2, config2) {
91
+ const { policy, checkpoints } = config2.commit;
92
+ if (policy === "manual") {
93
+ return { allowed: false, reason: "commit policy is manual \u2014 await an explicit user instruction" };
133
94
  }
134
- if (checkpoints.includes(state.stage)) {
135
- const ready = config.transitions
136
- .filter(transition => transition.from === state.stage)
137
- .every(transition => (transition.requires ?? []).every(guard => guardSatisfied(guard, state, config)))
138
- if (!ready) return { allowed: false, reason: 'checkpoint stage reached but its outgoing guards are unmet' }
139
- return { allowed: true, label: 'TASK' }
95
+ if (checkpoints.includes(state2.stage)) {
96
+ const ready = config2.transitions.filter((transition) => transition.from === state2.stage).every((transition) => (transition.requires ?? []).every((g) => guardSatisfied(g, state2, config2)));
97
+ if (!ready) {
98
+ return { allowed: false, reason: "checkpoint stage reached but its outgoing guards are unmet" };
99
+ }
100
+ return { allowed: true, label: "TASK" };
140
101
  }
141
- if (policy === 'item') {
142
- const done = (state.items ?? []).filter(item => item.status === 'done').length
143
- if (done > 0) return { allowed: true, label: 'T' + done }
102
+ if (policy === "item") {
103
+ const done = state2.items.filter((item) => item.status === "done").length;
104
+ if (done > 0) {
105
+ return { allowed: true, label: `T${done}` };
106
+ }
144
107
  }
145
- return { allowed: false, reason: 'no commit is due at this stage under the current policy' }
108
+ return { allowed: false, reason: "no commit is due at this stage under the current policy" };
109
+ }
110
+ function isEngineMeta(file) {
111
+ return /^\.dsh\/(task-[^/]+\.json|eng\.json)$/.test(file);
112
+ }
113
+ function checkFileScope(state2, committing, config2) {
114
+ if (!config2.commit.file_scope) return { ok: true, outside: [], noScope: false };
115
+ if (state2.files.length === 0) return { ok: false, outside: [], noScope: true };
116
+ const outside = committing.filter((file) => !state2.files.includes(file) && !isEngineMeta(file));
117
+ return outside.length === 0 ? { ok: true, outside: [], noScope: false } : { ok: false, outside, noScope: false };
146
118
  }
147
119
 
148
- // Mirrors engine.validateCommitMessage.
149
- function validateCommitMessage(message, config) {
150
- if (config.commit.message_pattern === '') return { ok: true }
151
- try {
152
- return new RegExp(config.commit.message_pattern).test(message)
153
- ? { ok: true }
154
- : { ok: false, reason: 'commit summary does not match ' + config.commit.message_hint }
155
- } catch {
156
- return { ok: false, reason: 'commit.message_pattern is not a valid regular expression' }
120
+ // src/workflows.ts
121
+ var STANDARD = {
122
+ stages: ["\u9700\u6C42\u8BC4\u5BA1", "\u8BBE\u8BA1", "\u5F00\u53D1", "\u4EA4\u4ED8", "\u4EE3\u7801\u5BA1\u6838", "\u5B8C\u6210"],
123
+ start_stage: "\u9700\u6C42\u8BC4\u5BA1",
124
+ transitions: [
125
+ { from: "\u9700\u6C42\u8BC4\u5BA1", to: "\u8BBE\u8BA1", requires: ["requirement_confirmation", "artifacts_present"] },
126
+ { from: "\u8BBE\u8BA1", to: "\u5F00\u53D1", requires: ["solution_confirmation", "artifacts_present"] },
127
+ { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] },
128
+ { from: "\u4EA4\u4ED8", to: "\u4EE3\u7801\u5BA1\u6838", requires: ["verified"] },
129
+ { from: "\u4EE3\u7801\u5BA1\u6838", to: "\u5B8C\u6210", requires: ["review_passed", "artifacts_present"] }
130
+ ],
131
+ artifacts: [
132
+ { stage: "\u9700\u6C42\u8BC4\u5BA1", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope", "acceptance_criteria"] },
133
+ { stage: "\u8BBE\u8BA1", id: "design", name: "\u8BBE\u8BA1\u6587\u6863", fields: ["approach", "risks", "impact"] },
134
+ { stage: "\u4EE3\u7801\u5BA1\u6838", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
135
+ ],
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: ["\u4EA4\u4ED8"],
141
+ file_scope: true
142
+ },
143
+ high_risk_requires_verification: true,
144
+ 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", "code-commit"], rules: ["coding-conventions", "commit-conventions"] },
149
+ "\u4EE3\u7801\u5BA1\u6838": { skills: ["code-review"], rules: ["security-redlines"] }
150
+ }
151
+ };
152
+ var AGILE = {
153
+ stages: ["\u9700\u6C42", "\u5F00\u53D1", "\u4EA4\u4ED8", "\u5BA1\u67E5"],
154
+ start_stage: "\u9700\u6C42",
155
+ transitions: [
156
+ { from: "\u9700\u6C42", to: "\u5F00\u53D1", requires: ["requirement_confirmation"] },
157
+ { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] },
158
+ { from: "\u4EA4\u4ED8", to: "\u5BA1\u67E5", requires: [] }
159
+ ],
160
+ artifacts: [
161
+ { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] }
162
+ ],
163
+ 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
169
+ },
170
+ high_risk_requires_verification: false,
171
+ 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"] }
157
176
  }
177
+ };
178
+ var MINIMAL = {
179
+ stages: ["\u5F00\u53D1", "\u4EA4\u4ED8"],
180
+ start_stage: "\u5F00\u53D1",
181
+ transitions: [
182
+ { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] }
183
+ ],
184
+ 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,
193
+ stage_bindings: {
194
+ "\u5F00\u53D1": { skills: ["code-implement"] },
195
+ "\u4EA4\u4ED8": { skills: ["code-commit"], rules: ["commit-conventions"] }
196
+ }
197
+ };
198
+ function deriveCapabilities(config2) {
199
+ const caps = [];
200
+ const guards = new Set(config2.transitions.flatMap((t) => t.requires ?? []));
201
+ if (guards.has("verified")) caps.push("verification_gate");
202
+ if (guards.has("review_passed")) caps.push("review_gate");
203
+ if (guards.has("artifacts_present")) caps.push("artifact_gate");
204
+ if (config2.commit.file_scope) caps.push("file_scope");
205
+ return caps;
158
206
  }
159
-
160
- // Mirrors engine.checkFileScope + engine.isEngineMeta.
161
- function isEngineMeta(file) {
162
- return /^\.dsh\/(task-[^/]+\.json|eng\.json)$/.test(file)
207
+ function preset(id, version, label, description, config2) {
208
+ return { id, version, label, description, capabilities: deriveCapabilities(config2), config: config2 };
163
209
  }
164
-
165
- function checkFileScope(state, committing, config) {
166
- if (!config.commit.file_scope) return { ok: true, outside: [], noScope: false }
167
- const files = Array.isArray(state.files) ? state.files : []
168
- if (files.length === 0) return { ok: false, outside: [], noScope: true }
169
- const outside = committing.filter(file => !files.includes(file) && !isEngineMeta(file))
170
- return outside.length === 0
171
- ? { ok: true, outside: [], noScope: false }
172
- : { ok: false, outside, noScope: false }
210
+ var FLOW_PRESETS = {
211
+ standard: preset("standard", 1, "\u6807\u51C6\u7814\u53D1", "\u9700\u6C42\u8BC4\u5BA1 \u2192 \u8BBE\u8BA1 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u4EE3\u7801\u5BA1\u6838\uFF0C\u542B\u4EA7\u7269\u95E8 + \u786E\u8BA4\u95E8", 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)
214
+ };
215
+ function dedupe(names) {
216
+ return [...new Set(names)];
173
217
  }
174
-
175
- function stagedFiles() {
176
- try {
177
- // `-z` emits raw bytes (no core.quotePath octal escaping) NUL-separated, so
178
- // non-ASCII filenames survive the scope comparison verbatim; `quotePath=false`
179
- // is a belt-and-braces fallback. `D` is included so a deletion outside the
180
- // declared scope is caught too, not just adds/renames/modifies.
181
- return execSync('git -c core.quotePath=false diff --cached --name-only -z --diff-filter=ACMRD', { encoding: 'utf8' })
182
- .split('\0')
183
- .map(line => line.trim())
184
- .filter(Boolean)
185
- } catch {
186
- return []
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;
231
+ }
232
+ if (!changed) return base;
233
+ return { ...base, stage_bindings: merged };
234
+ }
235
+ function resolveFlow(flow, override) {
236
+ const preset2 = FLOW_PRESETS[flow];
237
+ if (preset2 === void 0) {
238
+ return { ok: false, code: "UNKNOWN_FLOW", flow, knownFlows: Object.keys(FLOW_PRESETS) };
187
239
  }
240
+ return { ok: true, config: mergeBindings(preset2.config, override?.stage_bindings), preset: preset2 };
188
241
  }
189
242
 
190
- // --- gather project state ---
191
- const cwd = process.cwd()
192
- const configPath = path.join(cwd, '.dsh', 'eng.json')
193
- let config
194
- if (fs.existsSync(configPath)) {
195
- let raw = ''
243
+ // src/hook.ts
244
+ var messageFile = process.argv[2];
245
+ function refuse(reason) {
246
+ console.error("");
247
+ console.error("dsh-task-engine \u63D0\u4EA4\u95E8\u7981\u62D2\u7EDD: " + reason);
248
+ console.error("\u8BF7\u5148\u7528 dev_task \u8D70\u5B8C\u524D\u7F6E\u9636\u6BB5\uFF08\u6216\u4FEE\u6B63\u63D0\u4EA4\u6D88\u606F\uFF09\uFF0C\u518D\u63D0\u4EA4\u3002");
249
+ console.error("");
250
+ process.exit(1);
251
+ }
252
+ function readJson(abs) {
196
253
  try {
197
- raw = fs.readFileSync(configPath, 'utf8')
254
+ return JSON.parse(import_node_fs.default.readFileSync(abs, "utf8"));
198
255
  } catch {
199
- raw = ''
200
- }
201
- let parsed
202
- try {
203
- parsed = JSON.parse(raw)
204
- } catch (error) {
205
- refuse('invalid JSON in .dsh/eng.json: ' + (error && error.message ? error.message : String(error)))
256
+ return void 0;
206
257
  }
207
- config = loadWorkflow(parsed)
208
- } else {
209
- config = loadWorkflow(undefined)
210
258
  }
211
-
259
+ function normalizeTask(task) {
260
+ return {
261
+ schema: 1,
262
+ id: String(task.id ?? ""),
263
+ title: String(task.title ?? ""),
264
+ branch: String(task.branch ?? ""),
265
+ work_size: task.work_size ?? "standard",
266
+ risk_level: task.risk_level ?? "standard",
267
+ stage: String(task.stage ?? ""),
268
+ requirement_confirmed: task.requirement_confirmed === true,
269
+ solution_confirmed: task.solution_confirmed === true,
270
+ items: Array.isArray(task.items) ? task.items : [],
271
+ verification: task.verification ?? { passed: false, evidence: [] },
272
+ review: task.review ?? { outcome: "pending" },
273
+ artifacts: task.artifacts ?? {},
274
+ files: Array.isArray(task.files) ? task.files : [],
275
+ commits: Array.isArray(task.commits) ? task.commits : [],
276
+ ...task.flow !== void 0 ? { flow: task.flow } : {}
277
+ };
278
+ }
212
279
  function loadTasks() {
213
- const dir = path.join(cwd, '.dsh')
214
- if (!fs.existsSync(dir)) return []
215
- const out = []
216
- const names = fs.readdirSync(dir).filter(name => name.startsWith('task-') && name.endsWith('.json'))
280
+ const dir = import_node_path.default.join(process.cwd(), ".dsh");
281
+ if (!import_node_fs.default.existsSync(dir)) return [];
282
+ const out = [];
283
+ const names = import_node_fs.default.readdirSync(dir).filter((name) => name.startsWith("task-") && name.endsWith(".json"));
217
284
  for (const name of names) {
218
- const task = readJson(path.join(dir, name))
219
- if (task && task.id) out.push({ file: path.join(dir, name), task })
285
+ const json = readJson(import_node_path.default.join(dir, name));
286
+ if (json !== void 0 && json.id !== void 0) out.push({ file: import_node_path.default.join(dir, name), task: normalizeTask(json) });
220
287
  }
221
- return out
288
+ return out;
222
289
  }
223
-
224
290
  function currentBranch() {
225
291
  try {
226
- return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim()
292
+ return (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
227
293
  } catch {
228
- return ''
294
+ return "";
295
+ }
296
+ }
297
+ function stagedEntries() {
298
+ try {
299
+ const raw = (0, import_node_child_process.execSync)("git -c core.quotePath=false diff --cached --name-status -z --diff-filter=ACMRDT", { encoding: "utf8" });
300
+ const parts = raw.split("\0");
301
+ const entries2 = [];
302
+ let i = 0;
303
+ while (i < parts.length) {
304
+ const status = parts[i];
305
+ if (status === void 0 || status === "") {
306
+ i++;
307
+ continue;
308
+ }
309
+ if (status.startsWith("R") || status.startsWith("C")) {
310
+ const from = parts[i + 1];
311
+ const to = parts[i + 2];
312
+ if (from !== void 0 && from !== "") entries2.push({ status, path: from });
313
+ if (to !== void 0 && to !== "") entries2.push({ status, path: to });
314
+ i += 3;
315
+ } else {
316
+ const path2 = parts[i + 1];
317
+ if (path2 !== void 0 && path2 !== "") entries2.push({ status, path: path2 });
318
+ i += 2;
319
+ }
320
+ }
321
+ return entries2;
322
+ } catch {
323
+ return [];
229
324
  }
230
325
  }
231
-
232
326
  function fileMtime(file) {
233
327
  try {
234
- return fs.statSync(file).mtimeMs
328
+ return import_node_fs.default.statSync(file).mtimeMs;
235
329
  } catch {
236
- return 0
330
+ return 0;
237
331
  }
238
332
  }
239
-
240
- // Fallback only for pattern-less flows (e.g. minimal), which carry no task id in
241
- // the summary. Pattern flows locate the exact task by id via parseTaskId below.
242
- function pickTask(tasks, branch) {
243
- if (tasks.length === 0) return undefined
244
- const byBranch = tasks.find(({ task }) => task.branch === branch)
245
- if (byBranch) return byBranch.task
246
- if (tasks.length === 1) return tasks[0].task
247
- const sorted = [...tasks].sort((a, b) => fileMtime(b.file) - fileMtime(a.file))
248
- return sorted[0].task
333
+ function pickTask(tasks2, branch) {
334
+ if (tasks2.length === 0) return void 0;
335
+ const byBranch = tasks2.find(({ task }) => task.branch === branch);
336
+ if (byBranch) return byBranch.task;
337
+ if (tasks2.length === 1) return tasks2[0].task;
338
+ const sorted = [...tasks2].sort((a, b) => fileMtime(b.file) - fileMtime(a.file));
339
+ return sorted[0].task;
249
340
  }
250
-
251
- function readCommitMessage() {
252
- return fs.readFileSync(messageFile, 'utf8')
253
- .split('\n')
254
- .filter(line => !line.startsWith('#'))
255
- .join('\n')
256
- .trim()
341
+ function readCommitMessage(file) {
342
+ return import_node_fs.default.readFileSync(file, "utf8").split("\n").filter((line) => !line.startsWith("#")).join("\n").trim();
257
343
  }
258
-
259
- // Extract the task id from the summary's leading 【…】 field. Deliberately
260
- // config-independent (unlike engine.taskIdFromMessage): the hook must locate the
261
- // task BEFORE it knows that task's frozen config, so it cannot use the config's
262
- // pattern to parse the id. Pattern-less flows (minimal) simply carry no 【…】 here.
263
- function extractTaskId(message) {
264
- const match = /^【([^【】\s]+)】/.exec(message)
265
- return match ? match[1] : undefined
344
+ function extractTaskId(message2) {
345
+ const match = /^【([^【】\s]+)】/.exec(message2);
346
+ return match ? match[1] : void 0;
347
+ }
348
+ function defaultConfig() {
349
+ const resolved = resolveFlow("standard");
350
+ if (!resolved.ok) throw new Error('built-in "standard" preset is missing');
351
+ return resolved.config;
352
+ }
353
+ function loadWorkflow(parsed) {
354
+ if (parsed === void 0) return defaultConfig();
355
+ const flow = parsed.flow;
356
+ if (typeof flow !== "string" || flow.trim() === "") {
357
+ refuse('\u9879\u76EE .dsh/eng.json \u7F3A "flow" \u5B57\u6BB5\u2014\u2014\u8BBE\u4E3A standard|agile|minimal\uFF0C\u6216\u5220\u9664\u8BE5\u6587\u4EF6');
358
+ }
359
+ const override = typeof parsed.stage_bindings === "object" && parsed.stage_bindings !== null && !Array.isArray(parsed.stage_bindings) ? { stage_bindings: parsed.stage_bindings } : void 0;
360
+ const resolved = resolveFlow(flow, override);
361
+ if (!resolved.ok) {
362
+ refuse(`\u672A\u77E5\u6D41\u7A0B "${resolved.flow}"\uFF08\u5DF2\u77E5\u6D41\u7A0B\uFF1A${resolved.knownFlows.join("\u3001")}\uFF09`);
363
+ }
364
+ return resolved.config;
365
+ }
366
+ var cwd = process.cwd();
367
+ var configPath = import_node_path.default.join(cwd, ".dsh", "eng.json");
368
+ var config;
369
+ if (import_node_fs.default.existsSync(configPath)) {
370
+ let raw = "";
371
+ try {
372
+ raw = import_node_fs.default.readFileSync(configPath, "utf8");
373
+ } catch {
374
+ raw = "";
375
+ }
376
+ let parsed;
377
+ try {
378
+ parsed = JSON.parse(raw);
379
+ } catch (error) {
380
+ refuse("invalid JSON in .dsh/eng.json: " + (error instanceof Error ? error.message : String(error)));
381
+ }
382
+ config = loadWorkflow(parsed);
383
+ } else {
384
+ config = loadWorkflow(void 0);
266
385
  }
267
-
268
- // --- the gate ---
269
386
  if (!messageFile) {
270
- process.exit(0)
387
+ process.exit(0);
271
388
  }
272
-
273
- const message = readCommitMessage()
274
- const tasks = loadTasks()
275
-
276
- // Locate the task by id from the summary first; only then read its frozen config.
277
- // No branch/mtime guessing for pattern flows, so sibling tasks on one branch
278
- // cannot be cross-locked.
279
- const taskId = extractTaskId(message)
280
- let state
281
- if (taskId !== undefined) {
282
- const found = tasks.find(({ task }) => task.id === taskId)
389
+ var message = readCommitMessage(messageFile);
390
+ var tasks = loadTasks();
391
+ var taskId = extractTaskId(message);
392
+ var state;
393
+ if (taskId !== void 0) {
394
+ const found = tasks.find(({ task }) => task.id === taskId);
283
395
  if (!found) {
284
- refuse(`提交消息里的任务 id "${taskId}" 找不到对应的任务记录(.dsh/task-<id>.json)`)
396
+ refuse(`\u63D0\u4EA4\u6D88\u606F\u91CC\u7684\u4EFB\u52A1 id "${taskId}" \u627E\u4E0D\u5230\u5BF9\u5E94\u7684\u4EFB\u52A1\u8BB0\u5F55\uFF08.dsh/task-<id>.json\uFF09`);
285
397
  }
286
- state = found.task
398
+ state = found.task;
287
399
  } else {
288
- state = pickTask(tasks, currentBranch())
400
+ state = pickTask(tasks, currentBranch());
289
401
  }
290
402
  if (!state) {
291
- refuse('没有找到 dev_task 任务记录(.dsh/task-*.json)——请先 dev_task operation=create 建立任务')
403
+ refuse("\u6CA1\u6709\u627E\u5230 dev_task \u4EFB\u52A1\u8BB0\u5F55\uFF08.dsh/task-*.json\uFF09\u2014\u2014\u8BF7\u5148 dev_task operation=create \u5EFA\u7ACB\u4EFB\u52A1");
292
404
  }
293
-
294
- // 4.1 fix: check this task against its frozen flow snapshot, not the live
295
- // .dsh/eng.json (which may have drifted mid-task). Fall back to the live config
296
- // only for legacy records that predate the snapshot field.
297
- if (state.flow && state.flow.config) {
298
- config = state.flow.config
405
+ if (state.flow !== void 0 && state.flow.config !== void 0) {
406
+ config = state.flow.config;
299
407
  }
300
-
301
- const verdict = validateCommitMessage(message, config)
408
+ var verdict = validateCommitMessage(message, config);
302
409
  if (!verdict.ok) {
303
- refuse(verdict.reason)
410
+ refuse(verdict.errors?.[0] ?? "commit summary does not match the configured pattern");
304
411
  }
305
-
306
- const checkpoint = commitCheckpoint(state, config)
412
+ var checkpoint = commitCheckpoint(state, config);
307
413
  if (!checkpoint.allowed) {
308
- refuse(checkpoint.reason + '(当前阶段: ' + state.stage + ')')
414
+ refuse((checkpoint.reason ?? "commit checkpoint rejected") + "\uFF08\u5F53\u524D\u9636\u6BB5: " + state.stage + "\uFF09");
309
415
  }
310
-
311
- const scope = checkFileScope(state, stagedFiles(), config)
416
+ var entries = stagedEntries();
417
+ var scope = checkFileScope(state, entries.map((entry) => entry.path), config);
312
418
  if (!scope.ok) {
313
419
  if (scope.noScope) {
314
- refuse('任务没有声明文件范围(files 为空)——先用 dev_task operation=scope 声明本任务涉及的文件')
420
+ refuse("\u4EFB\u52A1\u6CA1\u6709\u58F0\u660E\u6587\u4EF6\u8303\u56F4\uFF08files \u4E3A\u7A7A\uFF09\u2014\u2014\u5148\u7528 dev_task operation=scope \u58F0\u660E\u672C\u4EFB\u52A1\u6D89\u53CA\u7684\u6587\u4EF6");
315
421
  }
316
- refuse('提交了任务范围之外的文件: ' + scope.outside.join(', '))
422
+ const described = scope.outside.map((outside) => {
423
+ const entry = entries.find((candidate) => candidate.path === outside);
424
+ return entry === void 0 ? outside : `${entry.status} ${outside}`;
425
+ });
426
+ refuse("\u63D0\u4EA4\u4E86\u4EFB\u52A1\u8303\u56F4\u4E4B\u5916\u7684\u6587\u4EF6: " + described.join(", "));
317
427
  }
318
-
319
- process.exit(0)
428
+ process.exit(0);