@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/.p0-test.mjs +265 -9
- package/README.md +4 -1
- package/docs/manual.html +16 -12
- package/hooks/commit-msg +411 -257
- package/lib/client.js.map +2 -2
- package/lib/dev-task.d.ts +7 -0
- package/lib/dev-task.js +199 -10
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +39 -0
- package/lib/engine.js +12 -7
- package/lib/engine.js.map +1 -1
- package/lib/hook.d.ts +12 -0
- package/lib/hook.js +288 -0
- package/lib/hook.js.map +1 -0
- package/lib/project.d.ts +66 -0
- package/lib/project.js +128 -0
- package/lib/project.js.map +1 -0
- package/lib/snapshot.d.ts +21 -0
- package/lib/snapshot.js +26 -0
- package/lib/snapshot.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,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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
99
|
-
const
|
|
100
|
-
return
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
280
|
+
return JSON.parse(import_node_fs.default.readFileSync(abs, "utf8"));
|
|
198
281
|
} 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)))
|
|
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 =
|
|
214
|
-
if (!
|
|
215
|
-
const out = []
|
|
216
|
-
const names =
|
|
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
|
|
219
|
-
if (
|
|
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(
|
|
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
|
|
354
|
+
return import_node_fs.default.statSync(file).mtimeMs;
|
|
235
355
|
} catch {
|
|
236
|
-
return 0
|
|
356
|
+
return 0;
|
|
237
357
|
}
|
|
238
358
|
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 +
|
|
453
|
+
refuse((checkpoint.reason ?? "commit checkpoint rejected") + "\uFF08\u5F53\u524D\u9636\u6BB5: " + state.stage + "\uFF09");
|
|
309
454
|
}
|
|
310
|
-
|
|
311
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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);
|