@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/.p0-test.mjs +19 -9
- package/README.md +2 -1
- package/docs/manual.html +6 -6
- package/hooks/commit-msg +367 -258
- package/lib/client.js.map +2 -2
- package/lib/dev-task.js +44 -4
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +20 -0
- package/lib/engine.js +10 -7
- package/lib/engine.js.map +1 -1
- package/lib/hook.d.ts +12 -0
- package/lib/hook.js +258 -0
- package/lib/hook.js.map +1 -0
- package/package.json +2 -2
- package/skills/code-verify/SKILL.md +1 -1
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
case
|
|
109
|
-
return
|
|
110
|
-
case
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
135
|
-
const ready =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return { allowed: true, label:
|
|
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 ===
|
|
142
|
-
const done =
|
|
143
|
-
if (done > 0)
|
|
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:
|
|
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
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
254
|
+
return JSON.parse(import_node_fs.default.readFileSync(abs, "utf8"));
|
|
198
255
|
} catch {
|
|
199
|
-
|
|
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 =
|
|
214
|
-
if (!
|
|
215
|
-
const out = []
|
|
216
|
-
const names =
|
|
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
|
|
219
|
-
if (
|
|
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(
|
|
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
|
|
328
|
+
return import_node_fs.default.statSync(file).mtimeMs;
|
|
235
329
|
} catch {
|
|
236
|
-
return 0
|
|
330
|
+
return 0;
|
|
237
331
|
}
|
|
238
332
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
const
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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 +
|
|
414
|
+
refuse((checkpoint.reason ?? "commit checkpoint rejected") + "\uFF08\u5F53\u524D\u9636\u6BB5: " + state.stage + "\uFF09");
|
|
309
415
|
}
|
|
310
|
-
|
|
311
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|