@k0t0vich/meta-agents-template 0.1.8 → 0.1.10
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 +16 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/cli.mjs +20 -6
- package/src/init.mjs +41 -3
- package/template/.meta-agents/scripts/task-branch-router.mjs +16 -5
- package/template/.meta-agents/scripts/tracker/github.mjs +549 -4
- package/template/README.md +2 -2
- package/template/agents.md +2 -2
- package/template/tracker-command-template.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.10 - 2026-03-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Implemented GitHub TrackerGateway adapter operations instead of `not implemented` stubs (`CREATE_TASK`, `SET_STATUS`, `ASSIGN_SPRINT`, `COMMIT_BY_NAME`).
|
|
9
|
+
- Added safe `dryRun` support for GitHub tracker operations to validate flows without side effects.
|
|
10
|
+
- Added smoke regression check for `meta:ops CREATE_TASK` (dry-run) to prevent future regressions where command exists but adapter is stubbed.
|
|
11
|
+
|
|
12
|
+
## 0.1.9 - 2026-03-20
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- `meta-agents init` now creates `AGENTS.md` by default for new projects, so agent runtimes that expect uppercase bootstrap filename can load rules reliably.
|
|
16
|
+
- Existing projects with pre-created `agents.md` remain non-destructive: init still updates only managed links block instead of replacing full file.
|
|
17
|
+
- Bootstrap block now includes explicit required instruction to read canonical rules from npm-linked template files.
|
|
18
|
+
- Branch context protection for `meta:task-start` now checks `AGENTS.md` with fallback to `agents.md`.
|
|
19
|
+
- Local smoke now validates `AGENTS.md` presence and required bootstrap instruction in empty-folder install scenario.
|
|
20
|
+
|
|
5
21
|
## 0.1.8 - 2026-03-20
|
|
6
22
|
|
|
7
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ npx -y @k0t0vich/meta-agents-template init . --yes --tracker github
|
|
|
31
31
|
Важно:
|
|
32
32
|
- если пользователь дал URL `https://www.npmjs.com/package/@k0t0vich/meta-agents-template`, извлекай имя пакета `@k0t0vich/meta-agents-template` и ставь именно его;
|
|
33
33
|
- не запускай `npm i -D ...` в папке без `package.json`, иначе npm может установить пакет в родительский проект.
|
|
34
|
-
- `meta-agents init` по умолчанию работает non-destructive: в корне создаёт/обновляет только project-specific конфиги `.meta-agents/config/project-context.yaml` (имя проекта + tracker) и `.meta-agents/config/trackers.yaml`, аккуратно merge-ит `meta:*` scripts в `package.json` и
|
|
34
|
+
- `meta-agents init` по умолчанию работает non-destructive: в корне создаёт/обновляет только project-specific конфиги `.meta-agents/config/project-context.yaml` (имя проекта + tracker) и `.meta-agents/config/trackers.yaml`, аккуратно merge-ит `meta:*` scripts в `package.json` и добавляет/обновляет bootstrap-блок в `AGENTS.md` (или в уже существующий `agents.md`) вместо полной перезаписи.
|
|
35
35
|
- `--force` нужен только если надо перезаписать уже существующие project-specific конфиги (`project-context.yaml`, `trackers.yaml`) и мигрировать legacy `meta:*` scripts.
|
|
36
36
|
|
|
37
37
|
## One-line для агентного запуска
|
|
@@ -48,7 +48,7 @@ mkdir -p my-project && cd my-project && npm init -y && npm i -D @k0t0vich/meta-a
|
|
|
48
48
|
Проверка, что шаблон и правила действительно развернуты:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
test -f agents.md && test -f .meta-agents/config/project-context.yaml && test -f node_modules/@k0t0vich/meta-agents-template/template/agents.md && echo "meta-agents template ready"
|
|
51
|
+
(test -f AGENTS.md || test -f agents.md) && test -f .meta-agents/config/project-context.yaml && test -f node_modules/@k0t0vich/meta-agents-template/template/agents.md && echo "meta-agents template ready"
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## Диалог при `init`
|
|
@@ -64,7 +64,7 @@ test -f agents.md && test -f .meta-agents/config/project-context.yaml && test -f
|
|
|
64
64
|
my-project/
|
|
65
65
|
package.json
|
|
66
66
|
package-lock.json
|
|
67
|
-
agents.md
|
|
67
|
+
AGENTS.md # или agents.md, если файл уже существовал
|
|
68
68
|
.meta-agents/
|
|
69
69
|
config/
|
|
70
70
|
project-context.yaml
|
|
@@ -76,7 +76,7 @@ my-project/
|
|
|
76
76
|
template/.meta-agents/scripts/*.mjs
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
`
|
|
79
|
+
Файл `AGENTS.md` (или уже существующий `agents.md`) не перезаписывается целиком: `init` добавляет/обновляет в нём только managed bootstrap-блок со ссылками на canonical rules из npm-пакета.
|
|
80
80
|
|
|
81
81
|
## Канонические команды
|
|
82
82
|
0. `VERIFY_GOVERNANCE_GATE`
|
|
@@ -153,7 +153,7 @@ npm run meta:status
|
|
|
153
153
|
`meta:ops` принудительно проверяет tracker provider lock и блокирует выполнение, если переданный `--tracker` не совпадает с зафиксированным provider проекта.
|
|
154
154
|
`meta:branch` валидирует ветку по Git Flow Lite (`main`, `develop`, `feature/*`, `release/*`, `hotfix/*`).
|
|
155
155
|
`meta:task-start` делает branch-routing preflight: сравнивает задачу с текущей веткой, показывает dirty/ahead блокеры и готовит маршрут (`stay_on_current_branch` или `create_new_branch`).
|
|
156
|
-
`meta:task-start` также делает context-protection check для `agents.md` между текущей и базовой веткой и требует явного подтверждения при diff.
|
|
156
|
+
`meta:task-start` также делает context-protection check для `AGENTS.md` (fallback: `agents.md`) между текущей и базовой веткой и требует явного подтверждения при diff.
|
|
157
157
|
Именование branch task ref: использовать GitHub issue ref в формате `issue-<number>`.
|
|
158
158
|
`meta:mr-review` формирует сводный pre-merge отчёт по MR/PR, `meta:mr-review-approve` фиксирует финальный PASS после подтверждения пользователя.
|
|
159
159
|
`meta:status` отдаёт единый статус-срез: trackers used, current sprint, current task, current branch context, git uncommitted и summary.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -279,12 +279,26 @@ export async function main(argv) {
|
|
|
279
279
|
if (result.config.skipped.length > 0) {
|
|
280
280
|
console.log(`Config files kept: ${result.config.skipped.join(", ")}`);
|
|
281
281
|
}
|
|
282
|
-
if (result.agents.created) {
|
|
283
|
-
console.log(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
if (result.agents.created.length > 0) {
|
|
283
|
+
console.log(
|
|
284
|
+
`Agent bootstrap created: ${result.agents.created
|
|
285
|
+
.map((item) => path.basename(item))
|
|
286
|
+
.join(", ")}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (result.agents.updated.length > 0) {
|
|
290
|
+
console.log(
|
|
291
|
+
`Agent bootstrap updated: ${result.agents.updated
|
|
292
|
+
.map((item) => path.basename(item))
|
|
293
|
+
.join(", ")}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (result.agents.created.length === 0 && result.agents.updated.length === 0) {
|
|
297
|
+
console.log(
|
|
298
|
+
`Agent bootstrap already up to date: ${result.agents.unchanged
|
|
299
|
+
.map((item) => path.basename(item))
|
|
300
|
+
.join(", ")}`,
|
|
301
|
+
);
|
|
288
302
|
}
|
|
289
303
|
if (result.git.actions.length > 0) {
|
|
290
304
|
console.log(`Git actions: ${result.git.actions.join("; ")}`);
|
package/src/init.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
13
13
|
".js",
|
|
14
14
|
]);
|
|
15
15
|
const ROOT_CONFIG_FILES = ["project-context.yaml", "trackers.yaml"];
|
|
16
|
+
const DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
|
16
17
|
|
|
17
18
|
const AGENTS_LINK_BLOCK_START = "<!-- meta-agents-template:links:start -->";
|
|
18
19
|
const AGENTS_LINK_BLOCK_END = "<!-- meta-agents-template:links:end -->";
|
|
@@ -219,6 +220,8 @@ function buildAgentsLinkBlock(packageName) {
|
|
|
219
220
|
const lines = [
|
|
220
221
|
AGENTS_LINK_BLOCK_START,
|
|
221
222
|
"## Meta Agents Template (npm-linked)",
|
|
223
|
+
"ОБЯЗАТЕЛЬНО: перед выполнением любой команды сначала прочитай canonical rules по ссылкам ниже.",
|
|
224
|
+
"При конфликте локального текста и шаблона приоритет у canonical files из node_modules.",
|
|
222
225
|
"Базовые правила шаблона:",
|
|
223
226
|
`- [Template agents.md](${packageRoot}/template/agents.md)`,
|
|
224
227
|
`- [Template tracker-command-template.md](${packageRoot}/template/tracker-command-template.md)`,
|
|
@@ -230,12 +233,22 @@ function buildAgentsLinkBlock(packageName) {
|
|
|
230
233
|
return `${lines.join("\n")}\n`;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
function buildAgentsBootstrapContent(fileName, block) {
|
|
237
|
+
return [
|
|
238
|
+
`# ${fileName}`,
|
|
239
|
+
"",
|
|
240
|
+
"Bootstrap file for agent runtime.",
|
|
241
|
+
"Before any task, read canonical rules from the npm-linked files in the block below.",
|
|
242
|
+
"",
|
|
243
|
+
block,
|
|
244
|
+
].join("\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function upsertAgentsLinksInFile(agentsPath, packageName) {
|
|
235
248
|
const block = buildAgentsLinkBlock(packageName);
|
|
236
249
|
|
|
237
250
|
if (!(await pathExists(agentsPath))) {
|
|
238
|
-
const content =
|
|
251
|
+
const content = buildAgentsBootstrapContent(path.basename(agentsPath), block);
|
|
239
252
|
await fs.writeFile(agentsPath, content, "utf8");
|
|
240
253
|
return { created: true, updated: false, path: agentsPath };
|
|
241
254
|
}
|
|
@@ -265,6 +278,31 @@ async function upsertAgentsLinks(targetDir, packageName) {
|
|
|
265
278
|
return { created: false, updated, path: agentsPath };
|
|
266
279
|
}
|
|
267
280
|
|
|
281
|
+
async function detectAgentsFilePath(targetDir) {
|
|
282
|
+
try {
|
|
283
|
+
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
284
|
+
const match = entries.find(
|
|
285
|
+
(entry) => entry.isFile() && entry.name.toLowerCase() === "agents.md",
|
|
286
|
+
);
|
|
287
|
+
if (match) {
|
|
288
|
+
return path.join(targetDir, match.name);
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
// target directory may not exist yet
|
|
292
|
+
}
|
|
293
|
+
return path.join(targetDir, DEFAULT_AGENTS_FILENAME);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function upsertAgentsLinks(targetDir, packageName) {
|
|
297
|
+
const agentsPath = await detectAgentsFilePath(targetDir);
|
|
298
|
+
const result = await upsertAgentsLinksInFile(agentsPath, packageName);
|
|
299
|
+
return {
|
|
300
|
+
created: result.created ? [result.path] : [],
|
|
301
|
+
updated: result.updated ? [result.path] : [],
|
|
302
|
+
unchanged: !result.created && !result.updated ? [result.path] : [],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
268
306
|
async function syncConfigFiles({ templateRoot, targetDir, values, force }) {
|
|
269
307
|
const sourceConfigRoot = path.join(templateRoot, ".meta-agents", "config");
|
|
270
308
|
const targetConfigRoot = path.join(targetDir, ".meta-agents", "config");
|
|
@@ -15,6 +15,7 @@ const TASK_KIND_MAP = {
|
|
|
15
15
|
release: "release",
|
|
16
16
|
hotfix: "hotfix",
|
|
17
17
|
};
|
|
18
|
+
const GOVERNANCE_CONTEXT_FILES = ["AGENTS.md", "agents.md"];
|
|
18
19
|
|
|
19
20
|
function git(args, allowFailure = false) {
|
|
20
21
|
try {
|
|
@@ -266,6 +267,16 @@ function readFileAtRef(ref, filePath) {
|
|
|
266
267
|
return git(["show", `${ref}:${filePath}`], true);
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
function readGovernanceContextAtRef(ref) {
|
|
271
|
+
for (const filePath of GOVERNANCE_CONTEXT_FILES) {
|
|
272
|
+
const content = readFileAtRef(ref, filePath);
|
|
273
|
+
if (content) {
|
|
274
|
+
return { filePath, content };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
269
280
|
function getContextWarnings(baseBranch) {
|
|
270
281
|
const warnings = [];
|
|
271
282
|
if (!baseBranch) {
|
|
@@ -280,19 +291,19 @@ function getContextWarnings(baseBranch) {
|
|
|
280
291
|
return warnings;
|
|
281
292
|
}
|
|
282
293
|
|
|
283
|
-
const currentAgents =
|
|
284
|
-
const baseAgents =
|
|
294
|
+
const currentAgents = readGovernanceContextAtRef("HEAD");
|
|
295
|
+
const baseAgents = readGovernanceContextAtRef(baseBranch);
|
|
285
296
|
|
|
286
297
|
if (!currentAgents || !baseAgents) {
|
|
287
298
|
warnings.push(
|
|
288
|
-
"Cannot compare governance context file 'agents.md' between current and base branch (missing in one of refs).",
|
|
299
|
+
"Cannot compare governance context file 'AGENTS.md/agents.md' between current and base branch (missing in one of refs).",
|
|
289
300
|
);
|
|
290
301
|
return warnings;
|
|
291
302
|
}
|
|
292
303
|
|
|
293
|
-
if (currentAgents !== baseAgents) {
|
|
304
|
+
if (currentAgents.content !== baseAgents.content) {
|
|
294
305
|
warnings.push(
|
|
295
|
-
`Governance context differs: '
|
|
306
|
+
`Governance context differs: '${currentAgents.filePath}' in current branch is not equal to '${baseBranch}:${baseAgents.filePath}'. Confirm switch before apply.`,
|
|
296
307
|
);
|
|
297
308
|
}
|
|
298
309
|
|
|
@@ -1,4 +1,549 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
const STATUS_LABELS = ["TODO", "IN_PROGRESS", "REVIEW", "READY", "BLOCKED", "DONE", "PUBLISH"];
|
|
5
|
+
const STATUS_COLORS = {
|
|
6
|
+
IN_PROGRESS: "FBCA04",
|
|
7
|
+
REVIEW: "5319E7",
|
|
8
|
+
READY: "0E8A16",
|
|
9
|
+
BLOCKED: "B60205",
|
|
10
|
+
DONE: "1D76DB",
|
|
11
|
+
PUBLISH: "0052CC",
|
|
12
|
+
};
|
|
13
|
+
STATUS_COLORS.TODO = "D4C5F9";
|
|
14
|
+
const FALLBACK_COLORS = {
|
|
15
|
+
type: "C2E0C6",
|
|
16
|
+
owner: "0E8A16",
|
|
17
|
+
tracker: "0052CC",
|
|
18
|
+
sprint: "F9D0C4",
|
|
19
|
+
default: "BFDADC",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function run(command, args, allowFailure = false) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync(command, args, {
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
}).trim();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (allowFailure) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
const stderr = String(error?.stderr || "").trim();
|
|
34
|
+
const stdout = String(error?.stdout || "").trim();
|
|
35
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${stderr || stdout || error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJson(command, args, allowFailure = false) {
|
|
40
|
+
const raw = run(command, args, allowFailure);
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`failed to parse JSON from '${command} ${args.join(" ")}': ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureGhReady() {
|
|
52
|
+
run("gh", ["--version"], false);
|
|
53
|
+
const auth = run("gh", ["auth", "status"], true);
|
|
54
|
+
if (!auth) {
|
|
55
|
+
throw new Error("gh is not authenticated. Run 'gh auth login' first.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toRepoArgs(repo) {
|
|
60
|
+
if (!repo) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return ["--repo", String(repo)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toNonEmptyString(value) {
|
|
67
|
+
if (value === null || value === undefined) {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
return String(value).trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pickFirstNonEmpty(...values) {
|
|
74
|
+
for (const value of values) {
|
|
75
|
+
const normalized = toNonEmptyString(value);
|
|
76
|
+
if (normalized) {
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseIssueNumber(value) {
|
|
84
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const source = toNonEmptyString(value);
|
|
89
|
+
if (!source) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const direct = source.match(/^#?(\d+)$/);
|
|
94
|
+
if (direct) {
|
|
95
|
+
return direct[1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const issueToken = source.match(/(?:^|[\/\s])issue-(\d+)(?:-|$|\s)/i);
|
|
99
|
+
if (issueToken) {
|
|
100
|
+
return issueToken[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const githubUrl = source.match(/\/issues\/(\d+)(?:$|[/?#])/i);
|
|
104
|
+
if (githubUrl) {
|
|
105
|
+
return githubUrl[1];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ref = source.match(/#(\d+)/);
|
|
109
|
+
if (ref) {
|
|
110
|
+
return ref[1];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeStatus(value, fallback = "TODO") {
|
|
117
|
+
const normalized = toNonEmptyString(value).toUpperCase() || fallback;
|
|
118
|
+
if (!STATUS_LABELS.includes(normalized)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`unsupported status '${normalized}'. Allowed: ${STATUS_LABELS.join(", ")}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeSprintLabel(value) {
|
|
127
|
+
const sprint = toNonEmptyString(value);
|
|
128
|
+
if (!sprint) {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
if (/^sprint:/i.test(sprint)) {
|
|
132
|
+
return sprint.replace(/^sprint:/i, "sprint:");
|
|
133
|
+
}
|
|
134
|
+
return `sprint:${sprint}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeOwnerLabel(value) {
|
|
138
|
+
const owner = toNonEmptyString(value);
|
|
139
|
+
if (!owner) {
|
|
140
|
+
return "owner:Engineering Agent";
|
|
141
|
+
}
|
|
142
|
+
if (/^owner:/i.test(owner)) {
|
|
143
|
+
return owner.replace(/^owner:/i, "owner:");
|
|
144
|
+
}
|
|
145
|
+
return `owner:${owner}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeTypeLabel(value) {
|
|
149
|
+
const kind = toNonEmptyString(value).toLowerCase() || "task";
|
|
150
|
+
if (kind.startsWith("type:")) {
|
|
151
|
+
return kind;
|
|
152
|
+
}
|
|
153
|
+
return `type:${kind}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseLabelsInput(value) {
|
|
157
|
+
if (!value) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.map((item) => toNonEmptyString(item)).filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return String(value)
|
|
166
|
+
.split(",")
|
|
167
|
+
.map((item) => item.trim())
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function labelColor(name) {
|
|
172
|
+
if (STATUS_COLORS[name]) {
|
|
173
|
+
return STATUS_COLORS[name];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lower = toNonEmptyString(name).toLowerCase();
|
|
177
|
+
if (lower.startsWith("type:")) {
|
|
178
|
+
return FALLBACK_COLORS.type;
|
|
179
|
+
}
|
|
180
|
+
if (lower.startsWith("owner:")) {
|
|
181
|
+
return FALLBACK_COLORS.owner;
|
|
182
|
+
}
|
|
183
|
+
if (lower.startsWith("tracker:")) {
|
|
184
|
+
return FALLBACK_COLORS.tracker;
|
|
185
|
+
}
|
|
186
|
+
if (lower.startsWith("sprint:")) {
|
|
187
|
+
return FALLBACK_COLORS.sprint;
|
|
188
|
+
}
|
|
189
|
+
return FALLBACK_COLORS.default;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mergeUnique(values) {
|
|
193
|
+
return Array.from(new Set(values.map((item) => toNonEmptyString(item)).filter(Boolean)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildCreateTaskLabels(payload) {
|
|
197
|
+
const status = normalizeStatus(payload.status || "TODO");
|
|
198
|
+
const labels = [
|
|
199
|
+
...parseLabelsInput(payload.labels),
|
|
200
|
+
status,
|
|
201
|
+
normalizeTypeLabel(payload.type),
|
|
202
|
+
normalizeOwnerLabel(payload.owner || payload.ownerRole),
|
|
203
|
+
"tracker:github",
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const sprintLabel = normalizeSprintLabel(payload.sprint);
|
|
207
|
+
if (sprintLabel) {
|
|
208
|
+
labels.push(sprintLabel);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return mergeUnique(labels);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderBulletedValue(value, fallbackText) {
|
|
215
|
+
const text = toNonEmptyString(value);
|
|
216
|
+
if (!text) {
|
|
217
|
+
return `- ${fallbackText}`;
|
|
218
|
+
}
|
|
219
|
+
if (text.includes("\n")) {
|
|
220
|
+
const lines = text
|
|
221
|
+
.split("\n")
|
|
222
|
+
.map((line) => line.trim())
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
if (lines.length === 0) {
|
|
225
|
+
return `- ${fallbackText}`;
|
|
226
|
+
}
|
|
227
|
+
return lines.map((line) => `- ${line}`).join("\n");
|
|
228
|
+
}
|
|
229
|
+
return `- ${text}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderCreateTaskBody(payload, labels) {
|
|
233
|
+
const description = renderBulletedValue(
|
|
234
|
+
payload.description,
|
|
235
|
+
"заполнить описание задачи",
|
|
236
|
+
);
|
|
237
|
+
const verifiability = renderBulletedValue(
|
|
238
|
+
payload.verifiability || payload.verification,
|
|
239
|
+
"strict: TBD; statistical: TBD; human: TBD",
|
|
240
|
+
);
|
|
241
|
+
const doneBlock = renderBulletedValue(
|
|
242
|
+
payload.done,
|
|
243
|
+
"[ ] Не начато",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const metadata = [];
|
|
247
|
+
metadata.push(`- tracker: github`);
|
|
248
|
+
metadata.push(`- labels: ${labels.join(", ")}`);
|
|
249
|
+
const sprintLabel = normalizeSprintLabel(payload.sprint);
|
|
250
|
+
if (sprintLabel) {
|
|
251
|
+
metadata.push(`- sprint: ${sprintLabel.replace(/^sprint:/i, "")}`);
|
|
252
|
+
}
|
|
253
|
+
const epicRef = parseIssueNumber(payload.epic || payload.linkToEpic || payload.parent);
|
|
254
|
+
if (epicRef) {
|
|
255
|
+
metadata.push(`- epic: #${epicRef}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [
|
|
259
|
+
"## PRD Step",
|
|
260
|
+
"",
|
|
261
|
+
"### Описание",
|
|
262
|
+
description,
|
|
263
|
+
"",
|
|
264
|
+
"### Проверяемость",
|
|
265
|
+
verifiability,
|
|
266
|
+
"",
|
|
267
|
+
"### Что сделано",
|
|
268
|
+
doneBlock,
|
|
269
|
+
"",
|
|
270
|
+
"## Metadata",
|
|
271
|
+
...metadata,
|
|
272
|
+
].join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ensureLabelExists(name, repo) {
|
|
276
|
+
const repoArgs = toRepoArgs(repo);
|
|
277
|
+
const exists = readJson(
|
|
278
|
+
"gh",
|
|
279
|
+
["label", "list", ...repoArgs, "--search", name, "--limit", "100", "--json", "name"],
|
|
280
|
+
true,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (Array.isArray(exists) && exists.some((item) => item?.name === name)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const color = labelColor(name);
|
|
288
|
+
const description = "Auto-created by meta:ops github adapter";
|
|
289
|
+
const created = run(
|
|
290
|
+
"gh",
|
|
291
|
+
["label", "create", name, "--color", color, "--description", description, ...repoArgs],
|
|
292
|
+
true,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (!created) {
|
|
296
|
+
const existsAfter = readJson(
|
|
297
|
+
"gh",
|
|
298
|
+
["label", "list", ...repoArgs, "--search", name, "--limit", "100", "--json", "name"],
|
|
299
|
+
true,
|
|
300
|
+
);
|
|
301
|
+
if (!(Array.isArray(existsAfter) && existsAfter.some((item) => item?.name === name))) {
|
|
302
|
+
throw new Error(`failed to create label '${name}'`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function ensureLabelsExist(labels, repo) {
|
|
308
|
+
for (const label of labels) {
|
|
309
|
+
ensureLabelExists(label, repo);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveTaskIssue(payload) {
|
|
314
|
+
const issue = parseIssueNumber(payload.task || payload.issue || payload.taskId || payload.id);
|
|
315
|
+
if (!issue) {
|
|
316
|
+
throw new Error("missing issue reference in payload (task/issue/taskId/id)");
|
|
317
|
+
}
|
|
318
|
+
return issue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getIssueLabels(issue, repo) {
|
|
322
|
+
const repoArgs = toRepoArgs(repo);
|
|
323
|
+
const issueData = readJson(
|
|
324
|
+
"gh",
|
|
325
|
+
["issue", "view", String(issue), "--json", "labels", ...repoArgs],
|
|
326
|
+
false,
|
|
327
|
+
);
|
|
328
|
+
const labels = issueData?.labels || [];
|
|
329
|
+
return labels.map((item) => toNonEmptyString(item?.name)).filter(Boolean);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function removeAndAddIssueLabels(issue, repo, labelsToRemove, labelsToAdd) {
|
|
333
|
+
const repoArgs = toRepoArgs(repo);
|
|
334
|
+
const args = ["issue", "edit", String(issue), ...repoArgs];
|
|
335
|
+
|
|
336
|
+
for (const label of labelsToRemove) {
|
|
337
|
+
args.push("--remove-label", label);
|
|
338
|
+
}
|
|
339
|
+
for (const label of labelsToAdd) {
|
|
340
|
+
args.push("--add-label", label);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
run("gh", args, false);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function commentIssue(issue, repo, body) {
|
|
347
|
+
const content = toNonEmptyString(body);
|
|
348
|
+
if (!content) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const repoArgs = toRepoArgs(repo);
|
|
352
|
+
run("gh", ["issue", "comment", String(issue), "--body", content, ...repoArgs], false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function gitCurrentBranch() {
|
|
356
|
+
return run("git", ["rev-parse", "--abbrev-ref", "HEAD"], true);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function gitAdd(files) {
|
|
360
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
361
|
+
run("git", ["add", ...files.map((item) => String(item))], false);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
run("git", ["add", "-A"], false);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function ensureStagedChanges() {
|
|
368
|
+
const staged = run("git", ["diff", "--cached", "--name-only"], true);
|
|
369
|
+
if (!staged) {
|
|
370
|
+
throw new Error("no staged changes to commit");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function buildCommitMessage(payload) {
|
|
375
|
+
const explicit = pickFirstNonEmpty(payload.message, payload.commitMessage);
|
|
376
|
+
if (explicit) {
|
|
377
|
+
return explicit;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const issue = parseIssueNumber(payload.task || payload.issue || payload.taskId || payload.id);
|
|
381
|
+
if (!issue) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
"missing commit message. Provide payload.message or payload.issue/task to build '#<issue> <summary>'",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const summary = pickFirstNonEmpty(payload.shortName, payload.title, payload.name, "update");
|
|
388
|
+
return `#${issue} ${summary}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function createTask(payload = {}) {
|
|
392
|
+
const title = pickFirstNonEmpty(payload.shortName, payload.title, payload.name);
|
|
393
|
+
if (!title) {
|
|
394
|
+
throw new Error("missing task title (shortName/title/name)");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const repo = pickFirstNonEmpty(payload.repo);
|
|
398
|
+
const labels = buildCreateTaskLabels(payload);
|
|
399
|
+
const body = renderCreateTaskBody(payload, labels);
|
|
400
|
+
|
|
401
|
+
if (payload.dryRun) {
|
|
402
|
+
console.log(
|
|
403
|
+
JSON.stringify(
|
|
404
|
+
{
|
|
405
|
+
command: "CREATE_TASK",
|
|
406
|
+
mode: "dry-run",
|
|
407
|
+
repo: repo || "current",
|
|
408
|
+
title,
|
|
409
|
+
labels,
|
|
410
|
+
},
|
|
411
|
+
null,
|
|
412
|
+
2,
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
ensureGhReady();
|
|
419
|
+
ensureLabelsExist(labels, repo);
|
|
420
|
+
|
|
421
|
+
const repoArgs = toRepoArgs(repo);
|
|
422
|
+
const args = ["issue", "create", "--title", title, "--body", body, ...repoArgs];
|
|
423
|
+
for (const label of labels) {
|
|
424
|
+
args.push("--label", label);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const issueUrl = run("gh", args, false);
|
|
428
|
+
const issue = parseIssueNumber(issueUrl);
|
|
429
|
+
const epic = parseIssueNumber(payload.epic || payload.linkToEpic || payload.parent);
|
|
430
|
+
if (issue && epic) {
|
|
431
|
+
commentIssue(issue, repo, `Linked to epic #${epic}.`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log(`github CREATE_TASK: ${issueUrl}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function setStatus(payload = {}) {
|
|
438
|
+
const issue = resolveTaskIssue(payload);
|
|
439
|
+
const repo = pickFirstNonEmpty(payload.repo);
|
|
440
|
+
const targetStatus = normalizeStatus(payload.status);
|
|
441
|
+
|
|
442
|
+
if (payload.dryRun) {
|
|
443
|
+
console.log(
|
|
444
|
+
JSON.stringify(
|
|
445
|
+
{
|
|
446
|
+
command: "SET_STATUS",
|
|
447
|
+
mode: "dry-run",
|
|
448
|
+
repo: repo || "current",
|
|
449
|
+
issue,
|
|
450
|
+
status: targetStatus,
|
|
451
|
+
},
|
|
452
|
+
null,
|
|
453
|
+
2,
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
ensureGhReady();
|
|
460
|
+
ensureLabelsExist([targetStatus], repo);
|
|
461
|
+
|
|
462
|
+
const currentLabels = getIssueLabels(issue, repo);
|
|
463
|
+
const removeStatuses = currentLabels.filter(
|
|
464
|
+
(label) => STATUS_LABELS.includes(label) && label !== targetStatus,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
removeAndAddIssueLabels(issue, repo, removeStatuses, [targetStatus]);
|
|
468
|
+
const reason = pickFirstNonEmpty(payload.reason);
|
|
469
|
+
if (reason) {
|
|
470
|
+
commentIssue(issue, repo, `Status -> ${targetStatus}. Reason: ${reason}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
console.log(`github SET_STATUS: #${issue} -> ${targetStatus}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function commitByName(payload = {}) {
|
|
477
|
+
const branch = gitCurrentBranch();
|
|
478
|
+
const expectedBranch = pickFirstNonEmpty(payload.branch);
|
|
479
|
+
if (expectedBranch && branch && branch !== expectedBranch) {
|
|
480
|
+
throw new Error(`branch mismatch: current '${branch}', expected '${expectedBranch}'`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const message = buildCommitMessage(payload);
|
|
484
|
+
const files = Array.isArray(payload.files) ? payload.files : [];
|
|
485
|
+
|
|
486
|
+
if (payload.dryRun) {
|
|
487
|
+
console.log(
|
|
488
|
+
JSON.stringify(
|
|
489
|
+
{
|
|
490
|
+
command: "COMMIT_BY_NAME",
|
|
491
|
+
mode: "dry-run",
|
|
492
|
+
branch: branch || "unknown",
|
|
493
|
+
message,
|
|
494
|
+
files: files.length > 0 ? files : ["-A"],
|
|
495
|
+
push: Boolean(payload.push),
|
|
496
|
+
},
|
|
497
|
+
null,
|
|
498
|
+
2,
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
gitAdd(files);
|
|
505
|
+
ensureStagedChanges();
|
|
506
|
+
run("git", ["commit", "-m", message], false);
|
|
507
|
+
if (payload.push) {
|
|
508
|
+
run("git", ["push"], false);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log(`github COMMIT_BY_NAME: ${message}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function assignSprint(payload = {}) {
|
|
515
|
+
const issue = resolveTaskIssue(payload);
|
|
516
|
+
const repo = pickFirstNonEmpty(payload.repo);
|
|
517
|
+
const sprintLabel = normalizeSprintLabel(payload.sprint);
|
|
518
|
+
if (!sprintLabel) {
|
|
519
|
+
throw new Error("missing sprint in payload");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (payload.dryRun) {
|
|
523
|
+
console.log(
|
|
524
|
+
JSON.stringify(
|
|
525
|
+
{
|
|
526
|
+
command: "ASSIGN_SPRINT",
|
|
527
|
+
mode: "dry-run",
|
|
528
|
+
repo: repo || "current",
|
|
529
|
+
issue,
|
|
530
|
+
sprint: sprintLabel,
|
|
531
|
+
},
|
|
532
|
+
null,
|
|
533
|
+
2,
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
ensureGhReady();
|
|
540
|
+
ensureLabelsExist([sprintLabel], repo);
|
|
541
|
+
|
|
542
|
+
const currentLabels = getIssueLabels(issue, repo);
|
|
543
|
+
const removeSprints = currentLabels.filter(
|
|
544
|
+
(label) => label.toLowerCase().startsWith("sprint:") && label !== sprintLabel,
|
|
545
|
+
);
|
|
546
|
+
removeAndAddIssueLabels(issue, repo, removeSprints, [sprintLabel]);
|
|
547
|
+
|
|
548
|
+
console.log(`github ASSIGN_SPRINT: #${issue} -> ${sprintLabel}`);
|
|
549
|
+
}
|
package/template/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Проект инициализирован шаблоном `meta-agents-template`.
|
|
4
4
|
|
|
5
5
|
## Что уже подключено
|
|
6
|
-
- `agents.md
|
|
6
|
+
- `AGENTS.md` (или существующий `agents.md`): локальный bootstrap-файл с ссылками на canonical правила в npm-пакете;
|
|
7
7
|
- `.meta-agents/config/project-context.yaml`: project name + выбранный tracker provider;
|
|
8
8
|
- `.meta-agents/config/trackers.yaml`: lock tracker provider и доступные providers;
|
|
9
9
|
- `meta:*` npm scripts, которые запускают canonical tooling из `node_modules/@k0t0vich/meta-agents-template/template/.meta-agents/scripts/*`.
|
|
@@ -64,7 +64,7 @@ npm run meta:status
|
|
|
64
64
|
Если передать `--tracker`, он обязан совпадать с зафиксированным provider проекта, иначе команда блокируется.
|
|
65
65
|
`meta:branch` валидирует ветку по Git Flow Lite (`main`, `develop`, `feature/*`, `release/*`, `hotfix/*`).
|
|
66
66
|
`meta:task-start` делает branch-routing preflight: сравнивает задачу с текущей веткой, показывает dirty/ahead блокеры и готовит маршрут (`stay_on_current_branch` или `create_new_branch`).
|
|
67
|
-
`meta:task-start` также делает context-protection check для `agents.md` между текущей и базовой веткой и требует явного подтверждения при diff.
|
|
67
|
+
`meta:task-start` также делает context-protection check для `AGENTS.md` (fallback: `agents.md`) между текущей и базовой веткой и требует явного подтверждения при diff.
|
|
68
68
|
Именование branch task ref: использовать GitHub issue ref в формате `issue-<number>`.
|
|
69
69
|
`meta:mr-review` формирует сводный pre-merge отчёт по MR/PR, `meta:mr-review-approve` фиксирует финальный PASS после подтверждения пользователя.
|
|
70
70
|
|
package/template/agents.md
CHANGED
|
@@ -172,7 +172,7 @@ Name: <agent role>
|
|
|
172
172
|
6. Согласовать с пользователем способ обработки (`commit/stash/discard/push`).
|
|
173
173
|
7. Переключиться на базовую ветку (`develop` или `main`) и обновить её.
|
|
174
174
|
8. Создать новую рабочую ветку по Git Flow Lite.
|
|
175
|
-
9. Перед переключением проверить консистентность `agents.md` между текущей и базовой веткой; при отличиях показать явное предупреждение и запросить подтверждение пользователя.
|
|
175
|
+
9. Перед переключением проверить консистентность `AGENTS.md` (fallback: `agents.md`) между текущей и базовой веткой; при отличиях показать явное предупреждение и запросить подтверждение пользователя.
|
|
176
176
|
|
|
177
177
|
Без этого preflight выполнение реализации запрещено.
|
|
178
178
|
|
|
@@ -201,7 +201,7 @@ Name: <agent role>
|
|
|
201
201
|
21. Для релиза обновлён публичный `CHANGELOG.md`.
|
|
202
202
|
22. Коммит следует формату `#issue-number <summary>` (issue ref в начале сообщения).
|
|
203
203
|
23. Для branch routing собран e2e evidence по двум сценариям: `same feature` и `different feature`.
|
|
204
|
-
24. Перед `--apply` подтверждена консистентность контекста `agents.md`
|
|
204
|
+
24. Перед `--apply` подтверждена консистентность контекста `AGENTS.md` (fallback: `agents.md`) или явно подтверждён осознанный switch при diff.
|
|
205
205
|
|
|
206
206
|
Если хотя бы один критерий не выполнен, задача не принимается.
|
|
207
207
|
|
|
@@ -67,7 +67,7 @@ result PASS
|
|
|
67
67
|
- branch naming policy: использовать только GitHub issue ref в формате `issue-<number>`;
|
|
68
68
|
- если задача атомарная и относится к текущей feature-ветке, допускается выполнение в текущей ветке после явного подтверждения пользователя;
|
|
69
69
|
- если задача не относится к текущей ветке, требуется: проверить dirty/ahead, согласовать действие с пользователем, перейти на `develop` (или `main` для hotfix), обновить и создать новую ветку;
|
|
70
|
-
- перед `--apply` обязателен контекстный check: сравнить `agents.md` текущей и базовой ветки, при diff запросить явное подтверждение пользователя;
|
|
70
|
+
- перед `--apply` обязателен контекстный check: сравнить `AGENTS.md` (fallback: `agents.md`) текущей и базовой ветки, при diff запросить явное подтверждение пользователя;
|
|
71
71
|
- сообщение коммита обязательно следует формату `#issue-number <summary>` (issue ref в начале).
|
|
72
72
|
|
|
73
73
|
Для GitHub трекера статус issue задаётся label-ом (одновременно только один статус из набора):
|
|
@@ -187,7 +187,7 @@ tracker __DEFAULT_TRACKER__,
|
|
|
187
187
|
task "#12",
|
|
188
188
|
task kind "atomic",
|
|
189
189
|
command "npm run meta:task-start -- --task #12 --slug api-redirect",
|
|
190
|
-
user dialogue "confirm branch context + dirty/ahead handling +
|
|
190
|
+
user dialogue "confirm branch context + dirty/ahead handling + AGENTS.md context warning + switch decision"
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
### SET_STATUS -> REVIEW (обязателен перед review gate)
|