@godv61/dsh-task-engine 0.19.2 → 0.22.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,473 @@
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
- },
53
+ function guardSatisfied(guard, state2, config2) {
54
+ switch (guard) {
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
+ });
78
+ }
79
+ }
96
80
  }
97
-
98
- function loadWorkflow(parsed) {
99
- const flow = parsed && typeof parsed.flow === 'string' ? parsed.flow : 'standard'
100
- return FLOWS[flow] || FLOWS.standard
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
+ };
101
89
  }
102
-
103
- // Mirrors engine.guardSatisfied.
104
- function guardSatisfied(guard, state, config) {
105
- 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
- })
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" };
94
+ }
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" };
101
+ }
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}` };
123
106
  }
124
- default: return false
125
107
  }
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 };
126
118
  }
127
119
 
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' }
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"] }
133
150
  }
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' }
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"] }
140
176
  }
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 }
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"] }
144
196
  }
145
- return { allowed: false, reason: 'no commit is due at this stage under the current policy' }
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;
146
206
  }
147
-
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' }
207
+ function preset(id, version, label, description, config2) {
208
+ return { id, version, label, description, capabilities: deriveCapabilities(config2), config: config2 };
209
+ }
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)];
217
+ }
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;
157
231
  }
232
+ if (!changed) return base;
233
+ return { ...base, stage_bindings: merged };
158
234
  }
159
-
160
- // Mirrors engine.checkFileScope + engine.isEngineMeta.
161
- function isEngineMeta(file) {
162
- return /^\.dsh\/(task-[^/]+\.json|eng\.json)$/.test(file)
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) };
239
+ }
240
+ return { ok: true, config: mergeBindings(preset2.config, override?.stage_bindings), preset: preset2 };
163
241
  }
164
242
 
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 }
243
+ // src/snapshot.ts
244
+ var import_node_crypto = require("node:crypto");
245
+ function hashText(text) {
246
+ return (0, import_node_crypto.createHash)("sha256").update(text).digest("hex");
173
247
  }
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 []
187
- }
248
+ function hashConfig(config2) {
249
+ return hashText(JSON.stringify(config2));
188
250
  }
189
251
 
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 = ''
252
+ // src/project.ts
253
+ var TYPE_MARKERS = {
254
+ node: ["package.json"],
255
+ java: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"],
256
+ python: ["pyproject.toml", "requirements.txt", "setup.py"],
257
+ go: ["go.mod"],
258
+ rust: ["Cargo.toml"]
259
+ };
260
+ var DEFAULT_RISK_POLICY = {
261
+ // `.dsh/task-*.json` and `.dsh/eng.json` are protected by the snapshot hash and
262
+ // the engine-meta exemption, not by this list; `.dsh/rules` / `.dsh/skills`
263
+ // hold regular project content and stay committable by normal tasks.
264
+ sensitive_paths: [".git", ".env", ".credentials.yaml", "credentials", "secrets"],
265
+ risky_operations: ["D", "T"]
266
+ };
267
+ var ALL_MARKERS = Object.values(TYPE_MARKERS).flat();
268
+
269
+ // src/hook.ts
270
+ var messageFile = process.argv[2];
271
+ function refuse(reason) {
272
+ console.error("");
273
+ console.error("dsh-task-engine \u63D0\u4EA4\u95E8\u7981\u62D2\u7EDD: " + reason);
274
+ 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");
275
+ console.error("");
276
+ process.exit(1);
277
+ }
278
+ function readJson(abs) {
196
279
  try {
197
- raw = fs.readFileSync(configPath, 'utf8')
280
+ return JSON.parse(import_node_fs.default.readFileSync(abs, "utf8"));
198
281
  } 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)))
282
+ return void 0;
206
283
  }
207
- config = loadWorkflow(parsed)
208
- } else {
209
- config = loadWorkflow(undefined)
210
284
  }
211
-
285
+ function normalizeTask(task) {
286
+ return {
287
+ schema: 1,
288
+ id: String(task.id ?? ""),
289
+ title: String(task.title ?? ""),
290
+ branch: String(task.branch ?? ""),
291
+ work_size: task.work_size ?? "standard",
292
+ risk_level: task.risk_level ?? "standard",
293
+ stage: String(task.stage ?? ""),
294
+ requirement_confirmed: task.requirement_confirmed === true,
295
+ solution_confirmed: task.solution_confirmed === true,
296
+ items: Array.isArray(task.items) ? task.items : [],
297
+ verification: task.verification ?? { passed: false, evidence: [] },
298
+ review: task.review ?? { outcome: "pending" },
299
+ artifacts: task.artifacts ?? {},
300
+ files: Array.isArray(task.files) ? task.files : [],
301
+ commits: Array.isArray(task.commits) ? task.commits : [],
302
+ ...task.flow !== void 0 ? { flow: task.flow } : {}
303
+ };
304
+ }
212
305
  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'))
306
+ const dir = import_node_path.default.join(process.cwd(), ".dsh");
307
+ if (!import_node_fs.default.existsSync(dir)) return [];
308
+ const out = [];
309
+ const names = import_node_fs.default.readdirSync(dir).filter((name) => name.startsWith("task-") && name.endsWith(".json"));
217
310
  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 })
311
+ const json = readJson(import_node_path.default.join(dir, name));
312
+ if (json !== void 0 && json.id !== void 0) out.push({ file: import_node_path.default.join(dir, name), task: normalizeTask(json) });
220
313
  }
221
- return out
314
+ return out;
222
315
  }
223
-
224
316
  function currentBranch() {
225
317
  try {
226
- return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim()
318
+ return (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
227
319
  } catch {
228
- return ''
320
+ return "";
321
+ }
322
+ }
323
+ function stagedEntries() {
324
+ try {
325
+ const raw = (0, import_node_child_process.execSync)("git -c core.quotePath=false diff --cached --name-status -z --diff-filter=ACMRDT", { encoding: "utf8" });
326
+ const parts = raw.split("\0");
327
+ const entries2 = [];
328
+ let i = 0;
329
+ while (i < parts.length) {
330
+ const status = parts[i];
331
+ if (status === void 0 || status === "") {
332
+ i++;
333
+ continue;
334
+ }
335
+ if (status.startsWith("R") || status.startsWith("C")) {
336
+ const from = parts[i + 1];
337
+ const to = parts[i + 2];
338
+ if (from !== void 0 && from !== "") entries2.push({ status, path: from });
339
+ if (to !== void 0 && to !== "") entries2.push({ status, path: to });
340
+ i += 3;
341
+ } else {
342
+ const path2 = parts[i + 1];
343
+ if (path2 !== void 0 && path2 !== "") entries2.push({ status, path: path2 });
344
+ i += 2;
345
+ }
346
+ }
347
+ return entries2;
348
+ } catch {
349
+ return [];
229
350
  }
230
351
  }
231
-
232
352
  function fileMtime(file) {
233
353
  try {
234
- return fs.statSync(file).mtimeMs
354
+ return import_node_fs.default.statSync(file).mtimeMs;
235
355
  } catch {
236
- return 0
356
+ return 0;
237
357
  }
238
358
  }
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
359
+ function pickTask(tasks2, branch) {
360
+ if (tasks2.length === 0) return void 0;
361
+ const byBranch = tasks2.find(({ task }) => task.branch === branch);
362
+ if (byBranch) return byBranch.task;
363
+ if (tasks2.length === 1) return tasks2[0].task;
364
+ const sorted = [...tasks2].sort((a, b) => fileMtime(b.file) - fileMtime(a.file));
365
+ return sorted[0].task;
249
366
  }
250
-
251
- function readCommitMessage() {
252
- return fs.readFileSync(messageFile, 'utf8')
253
- .split('\n')
254
- .filter(line => !line.startsWith('#'))
255
- .join('\n')
256
- .trim()
367
+ function readCommitMessage(file) {
368
+ return import_node_fs.default.readFileSync(file, "utf8").split("\n").filter((line) => !line.startsWith("#")).join("\n").trim();
257
369
  }
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
370
+ function extractTaskId(message2) {
371
+ const match = /^【([^【】\s]+)】/.exec(message2);
372
+ return match ? match[1] : void 0;
373
+ }
374
+ function isEngineMeta2(file) {
375
+ return /^\.dsh\/(task-[^/]+\.json|eng\.json)$/.test(file);
376
+ }
377
+ function riskyPaths(entries2) {
378
+ return entries2.filter((entry) => !isEngineMeta2(entry.path)).filter((entry) => DEFAULT_RISK_POLICY.sensitive_paths.some((prefix) => entry.path === prefix || entry.path.startsWith(`${prefix}/`))).map((entry) => entry.path);
379
+ }
380
+ function riskVerified(state2) {
381
+ const receipt = state2.verification.receipt;
382
+ return state2.risk_level === "high_risk" && receipt !== void 0 && receipt.exit_code === 0 && !receipt.timed_out && !receipt.aborted;
383
+ }
384
+ function defaultConfig() {
385
+ const resolved = resolveFlow("standard");
386
+ if (!resolved.ok) throw new Error('built-in "standard" preset is missing');
387
+ return resolved.config;
388
+ }
389
+ function loadWorkflow(parsed) {
390
+ if (parsed === void 0) return defaultConfig();
391
+ const flow = parsed.flow;
392
+ if (typeof flow !== "string" || flow.trim() === "") {
393
+ 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
+ }
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);
397
+ if (!resolved.ok) {
398
+ refuse(`\u672A\u77E5\u6D41\u7A0B "${resolved.flow}"\uFF08\u5DF2\u77E5\u6D41\u7A0B\uFF1A${resolved.knownFlows.join("\u3001")}\uFF09`);
399
+ }
400
+ return resolved.config;
401
+ }
402
+ var cwd = process.cwd();
403
+ var configPath = import_node_path.default.join(cwd, ".dsh", "eng.json");
404
+ var config;
405
+ if (import_node_fs.default.existsSync(configPath)) {
406
+ let raw = "";
407
+ try {
408
+ raw = import_node_fs.default.readFileSync(configPath, "utf8");
409
+ } catch {
410
+ raw = "";
411
+ }
412
+ let parsed;
413
+ try {
414
+ parsed = JSON.parse(raw);
415
+ } catch (error) {
416
+ refuse("invalid JSON in .dsh/eng.json: " + (error instanceof Error ? error.message : String(error)));
417
+ }
418
+ config = loadWorkflow(parsed);
419
+ } else {
420
+ config = loadWorkflow(void 0);
266
421
  }
267
-
268
- // --- the gate ---
269
422
  if (!messageFile) {
270
- process.exit(0)
423
+ process.exit(0);
271
424
  }
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)
425
+ var message = readCommitMessage(messageFile);
426
+ var tasks = loadTasks();
427
+ var taskId = extractTaskId(message);
428
+ var state;
429
+ if (taskId !== void 0) {
430
+ const found = tasks.find(({ task }) => task.id === taskId);
283
431
  if (!found) {
284
- refuse(`提交消息里的任务 id "${taskId}" 找不到对应的任务记录(.dsh/task-<id>.json)`)
432
+ 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
433
  }
286
- state = found.task
434
+ state = found.task;
287
435
  } else {
288
- state = pickTask(tasks, currentBranch())
436
+ state = pickTask(tasks, currentBranch());
289
437
  }
290
438
  if (!state) {
291
- refuse('没有找到 dev_task 任务记录(.dsh/task-*.json)——请先 dev_task operation=create 建立任务')
439
+ 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
440
  }
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
441
+ if (state.flow?.hash !== void 0 && hashConfig(state.flow.config) !== state.flow.hash) {
442
+ refuse(`\u4EFB\u52A1 ${state.id} \u7684\u6D41\u7A0B\u5FEB\u7167 hash \u4E0D\u5339\u914D\u2014\u2014task \u8BB0\u5F55\u5728\u521B\u5EFA\u540E\u88AB\u6539\u52A8\u8FC7\uFF0C\u4FEE\u590D\u6216\u91CD\u5EFA\u4EFB\u52A1\u540E\u518D\u63D0\u4EA4`);
299
443
  }
300
-
301
- const verdict = validateCommitMessage(message, config)
444
+ if (state.flow !== void 0 && state.flow.config !== void 0) {
445
+ config = state.flow.config;
446
+ }
447
+ var verdict = validateCommitMessage(message, config);
302
448
  if (!verdict.ok) {
303
- refuse(verdict.reason)
449
+ refuse(verdict.errors?.[0] ?? "commit summary does not match the configured pattern");
304
450
  }
305
-
306
- const checkpoint = commitCheckpoint(state, config)
451
+ var checkpoint = commitCheckpoint(state, config);
307
452
  if (!checkpoint.allowed) {
308
- refuse(checkpoint.reason + '(当前阶段: ' + state.stage + ')')
453
+ refuse((checkpoint.reason ?? "commit checkpoint rejected") + "\uFF08\u5F53\u524D\u9636\u6BB5: " + state.stage + "\uFF09");
309
454
  }
310
-
311
- const scope = checkFileScope(state, stagedFiles(), config)
455
+ var entries = stagedEntries();
456
+ var scope = checkFileScope(state, entries.map((entry) => entry.path), config);
312
457
  if (!scope.ok) {
313
458
  if (scope.noScope) {
314
- refuse('任务没有声明文件范围(files 为空)——先用 dev_task operation=scope 声明本任务涉及的文件')
459
+ 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
460
  }
316
- refuse('提交了任务范围之外的文件: ' + scope.outside.join(', '))
461
+ const described = scope.outside.map((outside) => {
462
+ const entry = entries.find((candidate) => candidate.path === outside);
463
+ return entry === void 0 ? outside : `${entry.status} ${outside}`;
464
+ });
465
+ refuse("\u63D0\u4EA4\u4E86\u4EFB\u52A1\u8303\u56F4\u4E4B\u5916\u7684\u6587\u4EF6: " + described.join(", "));
317
466
  }
318
-
319
- process.exit(0)
467
+ var sensitive = riskyPaths(entries);
468
+ if (sensitive.length > 0 && !riskVerified(state)) {
469
+ refuse(
470
+ "\u63D0\u4EA4\u89E6\u53CA\u654F\u611F\u8DEF\u5F84: " + sensitive.join(", ") + " \u2014 \u8FD9\u7C7B\u53D8\u66F4\u8981\u6C42\u4EFB\u52A1\u58F0\u660E\u4E3A high_risk\uFF0C\u4E14\u9A8C\u8BC1\u5FC5\u987B\u662F\u771F\u5B9E\u547D\u4EE4\u56DE\u6267\uFF08exit 0\uFF09"
471
+ );
472
+ }
473
+ process.exit(0);