@k0t0vich/meta-agents-template 0.1.3 → 0.1.4
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/CHANGELOG.md +15 -0
- package/README.md +52 -10
- package/agents.md +67 -16
- package/package.json +1 -1
- package/template/.github/workflows/gitflow-lite-verify.yml +63 -0
- package/template/.meta-agents/config/project-context.yaml +7 -0
- package/template/.meta-agents/config/roles.yaml +5 -1
- package/template/.meta-agents/config/system.yaml +125 -3
- package/template/.meta-agents/config/trackers.yaml +1 -0
- package/template/.meta-agents/prompts/agile-manager.md +19 -1
- package/template/.meta-agents/prompts/clarifier.md +2 -0
- package/template/.meta-agents/prompts/mr-review-agent.md +26 -0
- package/template/.meta-agents/prompts/reviewer-judge.md +3 -1
- package/template/.meta-agents/prompts/status-agent.md +27 -0
- package/template/.meta-agents/scripts/run-mr-review-gate.mjs +488 -0
- package/template/.meta-agents/scripts/run-review-gate.mjs +54 -1
- package/template/.meta-agents/scripts/sync-status.mjs +620 -1
- package/template/.meta-agents/scripts/task-branch-router.mjs +530 -0
- package/template/.meta-agents/scripts/tracker/provider-lock.mjs +104 -0
- package/template/.meta-agents/scripts/tracker-gateway.mjs +199 -0
- package/template/.meta-agents/scripts/verify-branch-strategy.mjs +103 -0
- package/template/.meta-agents/scripts/verify-commit-link.mjs +27 -13
- package/template/.meta-agents/scripts/verify-governance.mjs +37 -12
- package/template/.meta-agents/templates/agent-work-contract.md +8 -1
- package/template/.meta-agents/templates/task-template.md +7 -1
- package/template/README.md +43 -6
- package/template/agents.md +67 -16
- package/template/package.json +6 -1
- package/template/tracker-command-template.md +122 -28
- package/tracker-command-template.md +109 -15
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Role: MR Review Agent
|
|
2
|
+
Goal: выполнить обязательный pre-merge review gate для MR/PR с фокусом на task linkage, PRD evidence и соответствие Git Flow Lite.
|
|
3
|
+
|
|
4
|
+
When to run:
|
|
5
|
+
- после `COMMIT_BY_NAME` и `push`;
|
|
6
|
+
- до merge в целевую ветку (`develop` для feature, `main` для release/hotfix).
|
|
7
|
+
|
|
8
|
+
Output format:
|
|
9
|
+
1. MR context (branch/base/PR/commit scope).
|
|
10
|
+
2. Task linkage summary.
|
|
11
|
+
3. PRD evidence summary (в PR body и/или изменённых markdown-артефактах).
|
|
12
|
+
4. Critical findings.
|
|
13
|
+
5. Risks.
|
|
14
|
+
6. Recommendation: `PASS_CANDIDATE`/`FAIL`.
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
- финальный `PASS_CONFIRMED` только после явного `MR Review Approved: yes` от пользователя;
|
|
18
|
+
- при `FAIL` merge блокируется;
|
|
19
|
+
- для `feature/*` target branch обязан быть `develop`;
|
|
20
|
+
- для `release/*|hotfix/*` target branch обязан быть `main`;
|
|
21
|
+
- в отчёте обязательно фиксировать task refs и состояние PRD evidence.
|
|
22
|
+
|
|
23
|
+
Tooling:
|
|
24
|
+
- `npm run meta:mr-review`
|
|
25
|
+
- `npm run meta:mr-review-approve`
|
|
26
|
+
- или `npm run meta:ops -- --command RUN_MR_REVIEW_GATE --payload '{...}'`
|
|
@@ -2,6 +2,8 @@ Role: Reviewer/Judge Agent
|
|
|
2
2
|
Goal: независимая приёмка по критериям и обязательный pre-commit review gate.
|
|
3
3
|
Output format: что сделано, критические замечания, потенциальные риски, рекомендация PASS_CANDIDATE/FAIL.
|
|
4
4
|
Rule: финальный PASS_CONFIRMED только после явного `Review Approved: yes` от пользователя.
|
|
5
|
+
Handoff rule: после pre-commit PASS_CONFIRMED задача передаётся в `MR Review Agent` для pre-merge gate.
|
|
5
6
|
Status rule: перед review gate задача должна быть в статусе `REVIEW`; статус `READY` выставляется только после `PASS_CONFIRMED`.
|
|
6
|
-
Delivery rule: `READY` требует commit+push
|
|
7
|
+
Delivery rule: `READY` требует commit+push на `feature/*|release/*|hotfix/*` (или `codex/*` эквиваленты) и PR в целевую ветку; `DONE` требует интеграцию в `main` и back-merge в `develop` для `release/*|hotfix/*`; `PUBLISH` только после фактической публикации.
|
|
8
|
+
Branch rule: при несоответствии ветки Git Flow Lite (`main`, `develop`, `feature/*`, `release/*`, `hotfix/*`, `codex/*`) review gate должен сигнализировать FAIL.
|
|
7
9
|
Output: accepted/rejected + findings.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Role: Status Agent
|
|
2
|
+
Goal: выдавать короткий и точный статус-срез проекта по запросам вида `статус`, `что в процессе`, `процесс`, `где мы`.
|
|
3
|
+
|
|
4
|
+
Routing rule:
|
|
5
|
+
- Если запрос про текущее состояние/прогресс без явной команды реализации, выбирается `Status Agent`.
|
|
6
|
+
|
|
7
|
+
Data source priority:
|
|
8
|
+
1. locked tracker provider (`.meta-agents/config/project-context.yaml` + `trackers.yaml`).
|
|
9
|
+
2. Для `github`: GitHub tracker snapshot (через `gh`) — источник истины.
|
|
10
|
+
3. Для `local`: `tasks/*` — источник истины.
|
|
11
|
+
4. Git (`branch`, uncommitted changes) всегда включается.
|
|
12
|
+
|
|
13
|
+
Output format (обязательный):
|
|
14
|
+
1. Trackers used (locked provider + доступные providers).
|
|
15
|
+
2. Current sprint.
|
|
16
|
+
3. Current task in progress.
|
|
17
|
+
4. Branch context (`branch`, `branch type`, `branch task ref`, `task/branch alignment`).
|
|
18
|
+
5. Git working tree summary (modified/staged, untracked, ahead/behind).
|
|
19
|
+
6. Короткая сводка рисков/неопределённостей.
|
|
20
|
+
|
|
21
|
+
Tooling:
|
|
22
|
+
- Основной путь: `npm run meta:status`.
|
|
23
|
+
- Для machine-readable вывода можно использовать `npm run meta:status -- --json`.
|
|
24
|
+
- Для веточного preflight по конкретной задаче: `npm run meta:task-start -- --task <TASK-ID|#issue> --slug <slug>`.
|
|
25
|
+
|
|
26
|
+
Rule:
|
|
27
|
+
- В `github` режиме локальные `tasks/*` не считать авторитетным источником по умолчанию; рассматривать только как cache/legacy artifacts.
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { resolveTrackerForCommand } from "./tracker/provider-lock.mjs";
|
|
6
|
+
|
|
7
|
+
const REQUIRED_PRD_SECTIONS = ["### Описание", "### Проверяемость", "### Что сделано"];
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const options = {
|
|
11
|
+
approve: false,
|
|
12
|
+
tracker: "",
|
|
13
|
+
pr: "",
|
|
14
|
+
base: "",
|
|
15
|
+
json: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
19
|
+
const arg = argv[i];
|
|
20
|
+
if (arg === "--approve") {
|
|
21
|
+
const value = (argv[i + 1] || "").trim().toLowerCase();
|
|
22
|
+
options.approve = value === "yes" || value === "true" || value === "1";
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === "--tracker") {
|
|
27
|
+
options.tracker = (argv[i + 1] || "").trim().toLowerCase();
|
|
28
|
+
i += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg === "--pr") {
|
|
32
|
+
options.pr = (argv[i + 1] || "").trim();
|
|
33
|
+
i += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--base") {
|
|
37
|
+
options.base = (argv[i + 1] || "").trim();
|
|
38
|
+
i += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === "--json") {
|
|
42
|
+
options.json = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function run(command, args, allowFailure = false) {
|
|
51
|
+
try {
|
|
52
|
+
return execFileSync(command, args, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
}).trim();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (allowFailure) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJson(command, args, allowFailure = false) {
|
|
66
|
+
const raw = run(command, args, allowFailure);
|
|
67
|
+
if (!raw) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return JSON.parse(raw);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function classifyBranch(branch) {
|
|
74
|
+
if (/^main$/.test(branch)) {
|
|
75
|
+
return "main";
|
|
76
|
+
}
|
|
77
|
+
if (/^develop$/.test(branch)) {
|
|
78
|
+
return "develop";
|
|
79
|
+
}
|
|
80
|
+
if (/^feature\/.+/.test(branch) || /^codex\/feature\/.+/.test(branch)) {
|
|
81
|
+
return "feature";
|
|
82
|
+
}
|
|
83
|
+
if (/^release\/.+/.test(branch) || /^codex\/release\/.+/.test(branch)) {
|
|
84
|
+
return "release";
|
|
85
|
+
}
|
|
86
|
+
if (/^hotfix\/.+/.test(branch) || /^codex\/hotfix\/.+/.test(branch)) {
|
|
87
|
+
return "hotfix";
|
|
88
|
+
}
|
|
89
|
+
return "unknown";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function defaultBaseByBranchType(branchType) {
|
|
93
|
+
if (branchType === "hotfix") {
|
|
94
|
+
return "main";
|
|
95
|
+
}
|
|
96
|
+
return "develop";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractTaskRefs(text) {
|
|
100
|
+
const source = String(text || "");
|
|
101
|
+
const refs = new Set();
|
|
102
|
+
|
|
103
|
+
const issueMatches = source.match(/#\d+/g) || [];
|
|
104
|
+
for (const ref of issueMatches) {
|
|
105
|
+
refs.add(ref);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const taskMatches = source.match(/\b[A-Z][A-Z0-9_]*-\d+\b/g) || [];
|
|
109
|
+
for (const ref of taskMatches) {
|
|
110
|
+
refs.add(ref.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return refs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasTestFiles(files) {
|
|
117
|
+
return files.some(
|
|
118
|
+
(file) =>
|
|
119
|
+
/(^|\/)(tests?|__tests__)\//i.test(file) ||
|
|
120
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/i.test(file),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function readPrdEvidenceFromFiles(files) {
|
|
125
|
+
const evidence = [];
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
if (!file.endsWith(".md")) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const abs = path.resolve(process.cwd(), file);
|
|
132
|
+
let content = "";
|
|
133
|
+
try {
|
|
134
|
+
content = await fs.readFile(abs, "utf8");
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const hasAll = REQUIRED_PRD_SECTIONS.every((section) => content.includes(section));
|
|
140
|
+
if (hasAll) {
|
|
141
|
+
evidence.push(`${file}: ${REQUIRED_PRD_SECTIONS.join(", ")}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const found = REQUIRED_PRD_SECTIONS.filter((section) => content.includes(section));
|
|
146
|
+
if (found.length > 0) {
|
|
147
|
+
evidence.push(`${file}: partial -> ${found.join(", ")}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return evidence;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseGitLog(range) {
|
|
155
|
+
const format = "%H%x1f%s%x1f%b%x1e";
|
|
156
|
+
const raw = run("git", ["log", `--pretty=format:${format}`, range], true);
|
|
157
|
+
if (!raw) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return raw
|
|
162
|
+
.split("\x1e")
|
|
163
|
+
.map((entry) => entry.trim())
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.map((entry) => {
|
|
166
|
+
const parts = entry.split("\x1f");
|
|
167
|
+
return {
|
|
168
|
+
oid: parts[0] || "",
|
|
169
|
+
title: parts[1] || "",
|
|
170
|
+
body: parts.slice(2).join("\x1f") || "",
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ensureUnique(values) {
|
|
176
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectLocalContext(options) {
|
|
180
|
+
const branch = run("git", ["rev-parse", "--abbrev-ref", "HEAD"], true) || "unknown";
|
|
181
|
+
const branchType = classifyBranch(branch);
|
|
182
|
+
const baseRef = options.base || defaultBaseByBranchType(branchType);
|
|
183
|
+
const range = `${baseRef}..HEAD`;
|
|
184
|
+
const commits = parseGitLog(range);
|
|
185
|
+
const files = run("git", ["diff", "--name-only", range], true)
|
|
186
|
+
.split("\n")
|
|
187
|
+
.map((line) => line.trim())
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
source: "git",
|
|
192
|
+
pr: {
|
|
193
|
+
number: options.pr || "",
|
|
194
|
+
title: "",
|
|
195
|
+
url: "",
|
|
196
|
+
state: "LOCAL",
|
|
197
|
+
baseRefName: baseRef,
|
|
198
|
+
headRefName: branch,
|
|
199
|
+
body: "",
|
|
200
|
+
},
|
|
201
|
+
branch,
|
|
202
|
+
branchType,
|
|
203
|
+
baseRefName: baseRef,
|
|
204
|
+
commits,
|
|
205
|
+
files,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function collectGithubContext(options) {
|
|
210
|
+
run("gh", ["--version"], false);
|
|
211
|
+
const authStatus = run("gh", ["auth", "status"], true);
|
|
212
|
+
if (!authStatus) {
|
|
213
|
+
throw new Error("gh auth status unavailable");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let prNumber = options.pr;
|
|
217
|
+
if (!prNumber) {
|
|
218
|
+
const self = readJson("gh", ["pr", "view", "--json", "number"], true);
|
|
219
|
+
prNumber = self?.number ? String(self.number) : "";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!prNumber) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const pr = readJson("gh", [
|
|
227
|
+
"pr",
|
|
228
|
+
"view",
|
|
229
|
+
prNumber,
|
|
230
|
+
"--json",
|
|
231
|
+
"number,title,url,state,baseRefName,headRefName,body,commits,files",
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const commits = (pr?.commits || []).map((commit) => ({
|
|
235
|
+
oid: commit.oid || "",
|
|
236
|
+
title: commit.messageHeadline || "",
|
|
237
|
+
body: commit.messageBody || "",
|
|
238
|
+
}));
|
|
239
|
+
const files = (pr?.files || []).map((file) => file.path).filter(Boolean);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
source: "github",
|
|
243
|
+
pr: {
|
|
244
|
+
number: String(pr?.number || ""),
|
|
245
|
+
title: pr?.title || "",
|
|
246
|
+
url: pr?.url || "",
|
|
247
|
+
state: pr?.state || "",
|
|
248
|
+
baseRefName: pr?.baseRefName || "",
|
|
249
|
+
headRefName: pr?.headRefName || "",
|
|
250
|
+
body: pr?.body || "",
|
|
251
|
+
},
|
|
252
|
+
branch: pr?.headRefName || run("git", ["rev-parse", "--abbrev-ref", "HEAD"], true) || "unknown",
|
|
253
|
+
branchType: classifyBranch(pr?.headRefName || ""),
|
|
254
|
+
baseRefName: pr?.baseRefName || "",
|
|
255
|
+
commits,
|
|
256
|
+
files,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function gatherTaskRefs(context) {
|
|
261
|
+
const refs = new Set();
|
|
262
|
+
|
|
263
|
+
for (const ref of extractTaskRefs(context.pr.title)) {
|
|
264
|
+
refs.add(ref);
|
|
265
|
+
}
|
|
266
|
+
for (const ref of extractTaskRefs(context.pr.body)) {
|
|
267
|
+
refs.add(ref);
|
|
268
|
+
}
|
|
269
|
+
for (const commit of context.commits) {
|
|
270
|
+
for (const ref of extractTaskRefs(`${commit.title}\n${commit.body}`)) {
|
|
271
|
+
refs.add(ref);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const branchIssue = String(context.branch || "").match(/(?:^|\/)(\d+)(?:-|$)/);
|
|
276
|
+
if (branchIssue) {
|
|
277
|
+
refs.add(`#${branchIssue[1]}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const branchTask = String(context.branch || "").match(/([A-Z][A-Z0-9_]*-\d+)/);
|
|
281
|
+
if (branchTask) {
|
|
282
|
+
refs.add(branchTask[1].toUpperCase());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return Array.from(refs);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function evaluateBranchPolicy(context, critical) {
|
|
289
|
+
const branchType = context.branchType;
|
|
290
|
+
if (branchType === "unknown" || branchType === "main") {
|
|
291
|
+
critical.push(`branch-policy -> unsupported merge flow branch '${context.branch}'`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (branchType === "feature" && context.baseRefName !== "develop") {
|
|
296
|
+
critical.push(
|
|
297
|
+
`branch-policy -> feature branch '${context.branch}' must target develop, got '${context.baseRefName || "unknown"}'`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if ((branchType === "release" || branchType === "hotfix") && context.baseRefName !== "main") {
|
|
301
|
+
critical.push(
|
|
302
|
+
`branch-policy -> ${branchType} branch '${context.branch}' must target main, got '${context.baseRefName || "unknown"}'`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function main() {
|
|
308
|
+
const options = parseArgs(process.argv.slice(2));
|
|
309
|
+
let trackerProvider = "";
|
|
310
|
+
try {
|
|
311
|
+
trackerProvider = resolveTrackerForCommand({
|
|
312
|
+
requestedTracker: options.tracker,
|
|
313
|
+
cwd: process.cwd(),
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error(`MR Review Gate FAIL: ${error.message}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let context = null;
|
|
321
|
+
let sourceNote = "";
|
|
322
|
+
|
|
323
|
+
if (trackerProvider === "github") {
|
|
324
|
+
try {
|
|
325
|
+
context = collectGithubContext(options);
|
|
326
|
+
if (!context) {
|
|
327
|
+
sourceNote = "No PR found via GitHub; fallback to local git range analysis.";
|
|
328
|
+
context = collectLocalContext(options);
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
sourceNote = `GitHub PR query failed (${error.message}); fallback to local git range analysis.`;
|
|
332
|
+
context = collectLocalContext(options);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
context = collectLocalContext(options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!context) {
|
|
339
|
+
console.error("MR Review Gate FAIL: unable to collect MR context");
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const critical = [];
|
|
344
|
+
const risks = [];
|
|
345
|
+
|
|
346
|
+
if (context.pr.state && context.pr.state !== "OPEN" && context.pr.state !== "LOCAL") {
|
|
347
|
+
critical.push(`pr-state -> expected OPEN/LOCAL, got '${context.pr.state}'`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (context.commits.length === 0) {
|
|
351
|
+
critical.push("mr-context -> no commits detected for review range");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
evaluateBranchPolicy(context, critical);
|
|
355
|
+
|
|
356
|
+
const taskRefs = gatherTaskRefs(context);
|
|
357
|
+
if (taskRefs.length === 0) {
|
|
358
|
+
critical.push("task-linkage -> no task/issue references detected in PR/commits/branch");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const prdEvidence = await readPrdEvidenceFromFiles(context.files);
|
|
362
|
+
const prHasPrdSections = REQUIRED_PRD_SECTIONS.every((section) =>
|
|
363
|
+
String(context.pr.body || "").includes(section),
|
|
364
|
+
);
|
|
365
|
+
if (prdEvidence.length === 0 && !prHasPrdSections) {
|
|
366
|
+
critical.push(
|
|
367
|
+
"prd-evidence -> no PRD sections found in changed markdown files or PR body",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (context.files.length > 0 && !hasTestFiles(context.files)) {
|
|
372
|
+
risks.push("Changes detected without explicit test files in MR diff.");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (sourceNote) {
|
|
376
|
+
risks.push(sourceNote);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const report = {
|
|
380
|
+
trackerProvider,
|
|
381
|
+
source: context.source,
|
|
382
|
+
pr: context.pr,
|
|
383
|
+
branch: context.branch,
|
|
384
|
+
branchType: context.branchType,
|
|
385
|
+
baseRefName: context.baseRefName,
|
|
386
|
+
commitsCount: context.commits.length,
|
|
387
|
+
changedFilesCount: context.files.length,
|
|
388
|
+
taskRefs: ensureUnique(taskRefs),
|
|
389
|
+
prdEvidence: prHasPrdSections
|
|
390
|
+
? ["PR body contains full PRD sections: Описание, Проверяемость, Что сделано", ...prdEvidence]
|
|
391
|
+
: prdEvidence,
|
|
392
|
+
critical,
|
|
393
|
+
risks,
|
|
394
|
+
recommendation: critical.length === 0 ? "PASS_CANDIDATE" : "FAIL",
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
if (options.json) {
|
|
398
|
+
console.log(JSON.stringify(report, null, 2));
|
|
399
|
+
} else {
|
|
400
|
+
console.log("# MR Review Gate Report");
|
|
401
|
+
console.log("");
|
|
402
|
+
console.log("## MR Context");
|
|
403
|
+
console.log(`- tracker provider: ${trackerProvider}`);
|
|
404
|
+
console.log(`- source: ${report.source}`);
|
|
405
|
+
console.log(`- branch: ${report.branch} (${report.branchType})`);
|
|
406
|
+
console.log(`- base: ${report.baseRefName || "unknown"}`);
|
|
407
|
+
console.log(`- PR: ${report.pr.number || "not detected"} ${report.pr.title || ""}`.trim());
|
|
408
|
+
if (report.pr.url) {
|
|
409
|
+
console.log(`- PR url: ${report.pr.url}`);
|
|
410
|
+
}
|
|
411
|
+
console.log(`- commits: ${report.commitsCount}`);
|
|
412
|
+
console.log(`- changed files: ${report.changedFilesCount}`);
|
|
413
|
+
|
|
414
|
+
console.log("");
|
|
415
|
+
console.log("## Task Linkage");
|
|
416
|
+
if (report.taskRefs.length === 0) {
|
|
417
|
+
console.log("- none");
|
|
418
|
+
} else {
|
|
419
|
+
for (const ref of report.taskRefs) {
|
|
420
|
+
console.log(`- ${ref}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log("");
|
|
425
|
+
console.log("## PRD Evidence");
|
|
426
|
+
if (report.prdEvidence.length === 0) {
|
|
427
|
+
console.log("- none");
|
|
428
|
+
} else {
|
|
429
|
+
for (const item of report.prdEvidence.slice(0, 20)) {
|
|
430
|
+
console.log(`- ${item}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log("");
|
|
435
|
+
console.log("## Critical Findings");
|
|
436
|
+
if (report.critical.length === 0) {
|
|
437
|
+
console.log("- none");
|
|
438
|
+
} else {
|
|
439
|
+
for (const item of report.critical) {
|
|
440
|
+
console.log(`- ${item}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
console.log("");
|
|
445
|
+
console.log("## Risks");
|
|
446
|
+
if (report.risks.length === 0) {
|
|
447
|
+
console.log("- none");
|
|
448
|
+
} else {
|
|
449
|
+
for (const item of report.risks) {
|
|
450
|
+
console.log(`- ${item}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log("");
|
|
455
|
+
console.log("## Recommendation");
|
|
456
|
+
console.log(`- ${report.recommendation}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (report.critical.length > 0) {
|
|
460
|
+
if (!options.json) {
|
|
461
|
+
console.log("");
|
|
462
|
+
console.log("## Decision");
|
|
463
|
+
console.log("- FAIL");
|
|
464
|
+
}
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!options.approve) {
|
|
469
|
+
if (!options.json) {
|
|
470
|
+
console.log("");
|
|
471
|
+
console.log("## Decision");
|
|
472
|
+
console.log("- PENDING_USER_CONFIRMATION");
|
|
473
|
+
console.log("- Требуется явное подтверждение пользователя: MR Review Approved = yes");
|
|
474
|
+
}
|
|
475
|
+
process.exit(2);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!options.json) {
|
|
479
|
+
console.log("");
|
|
480
|
+
console.log("## Decision");
|
|
481
|
+
console.log("- PASS_CONFIRMED");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
main().catch((error) => {
|
|
486
|
+
console.error(`MR Review Gate FAIL: ${error.message}`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { resolveTrackerForCommand } from "./tracker/provider-lock.mjs";
|
|
4
5
|
|
|
5
6
|
function parseArgs(argv) {
|
|
6
7
|
const options = {
|
|
7
8
|
approve: false,
|
|
9
|
+
tracker: "",
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -13,6 +15,11 @@ function parseArgs(argv) {
|
|
|
13
15
|
const value = (argv[index + 1] || "").trim().toLowerCase();
|
|
14
16
|
options.approve = value === "yes" || value === "true" || value === "1";
|
|
15
17
|
index += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (arg === "--tracker") {
|
|
21
|
+
options.tracker = (argv[index + 1] || "").trim().toLowerCase();
|
|
22
|
+
index += 1;
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -21,7 +28,10 @@ function parseArgs(argv) {
|
|
|
21
28
|
|
|
22
29
|
function git(args, allowFailure = false) {
|
|
23
30
|
try {
|
|
24
|
-
const output = execFileSync("git", args, {
|
|
31
|
+
const output = execFileSync("git", args, {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
});
|
|
25
35
|
return output.trim();
|
|
26
36
|
} catch (error) {
|
|
27
37
|
if (allowFailure) {
|
|
@@ -45,6 +55,25 @@ function matchTestFile(filePath) {
|
|
|
45
55
|
return /(^|\/)(tests?|__tests__)\//i.test(filePath) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(filePath);
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
function classifyBranch(branch) {
|
|
59
|
+
if (/^main$/.test(branch)) {
|
|
60
|
+
return "main";
|
|
61
|
+
}
|
|
62
|
+
if (/^develop$/.test(branch)) {
|
|
63
|
+
return "develop";
|
|
64
|
+
}
|
|
65
|
+
if (/^feature\/.+/.test(branch) || /^codex\/feature\/.+/.test(branch)) {
|
|
66
|
+
return "feature";
|
|
67
|
+
}
|
|
68
|
+
if (/^release\/.+/.test(branch) || /^codex\/release\/.+/.test(branch)) {
|
|
69
|
+
return "release";
|
|
70
|
+
}
|
|
71
|
+
if (/^hotfix\/.+/.test(branch) || /^codex\/hotfix\/.+/.test(branch)) {
|
|
72
|
+
return "hotfix";
|
|
73
|
+
}
|
|
74
|
+
return "unknown";
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
async function scanMarkers(files) {
|
|
49
78
|
const markers = [];
|
|
50
79
|
// Match common marker formats while ignoring status arrows in workflow lines.
|
|
@@ -70,9 +99,22 @@ async function scanMarkers(files) {
|
|
|
70
99
|
|
|
71
100
|
async function main() {
|
|
72
101
|
const options = parseArgs(process.argv.slice(2));
|
|
102
|
+
let trackerProvider = "";
|
|
103
|
+
try {
|
|
104
|
+
trackerProvider = resolveTrackerForCommand({
|
|
105
|
+
requestedTracker: options.tracker,
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Review Gate FAIL: ${error.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
73
113
|
const stagedFiles = splitLines(git(["diff", "--cached", "--name-only"], true));
|
|
74
114
|
const recentFiles = splitLines(git(["show", "--name-only", "--pretty=", "HEAD"], true));
|
|
75
115
|
const files = stagedFiles.length > 0 ? stagedFiles : recentFiles;
|
|
116
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], true) || "unknown";
|
|
117
|
+
const branchType = classifyBranch(branch);
|
|
76
118
|
|
|
77
119
|
if (files.length === 0) {
|
|
78
120
|
console.log("Review Gate FAIL");
|
|
@@ -93,10 +135,21 @@ async function main() {
|
|
|
93
135
|
if (sourceTouched && !testsTouched) {
|
|
94
136
|
risks.push("Source/template changes detected without explicit test changes.");
|
|
95
137
|
}
|
|
138
|
+
if (branchType === "develop") {
|
|
139
|
+
risks.push("Changes are on develop branch; prefer feature/release/hotfix branches for task-level work.");
|
|
140
|
+
}
|
|
141
|
+
if (branchType === "unknown") {
|
|
142
|
+
critical.push(`branch-policy -> unsupported branch '${branch}' for Git Flow Lite`);
|
|
143
|
+
}
|
|
144
|
+
if (branchType === "main") {
|
|
145
|
+
critical.push("branch-policy -> direct review/commit flow on main is blocked in Git Flow Lite");
|
|
146
|
+
}
|
|
96
147
|
|
|
97
148
|
console.log("# Review Gate Report");
|
|
98
149
|
console.log("");
|
|
99
150
|
console.log("## Что сделано");
|
|
151
|
+
console.log(`- tracker provider: ${trackerProvider}`);
|
|
152
|
+
console.log(`- branch: ${branch} (${branchType})`);
|
|
100
153
|
for (const file of files) {
|
|
101
154
|
console.log(`- changed: ${file}`);
|
|
102
155
|
}
|