@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +52 -10
  3. package/agents.md +67 -16
  4. package/package.json +1 -1
  5. package/template/.github/workflows/gitflow-lite-verify.yml +63 -0
  6. package/template/.meta-agents/config/project-context.yaml +7 -0
  7. package/template/.meta-agents/config/roles.yaml +5 -1
  8. package/template/.meta-agents/config/system.yaml +125 -3
  9. package/template/.meta-agents/config/trackers.yaml +1 -0
  10. package/template/.meta-agents/prompts/agile-manager.md +19 -1
  11. package/template/.meta-agents/prompts/clarifier.md +2 -0
  12. package/template/.meta-agents/prompts/mr-review-agent.md +26 -0
  13. package/template/.meta-agents/prompts/reviewer-judge.md +3 -1
  14. package/template/.meta-agents/prompts/status-agent.md +27 -0
  15. package/template/.meta-agents/scripts/run-mr-review-gate.mjs +488 -0
  16. package/template/.meta-agents/scripts/run-review-gate.mjs +54 -1
  17. package/template/.meta-agents/scripts/sync-status.mjs +620 -1
  18. package/template/.meta-agents/scripts/task-branch-router.mjs +530 -0
  19. package/template/.meta-agents/scripts/tracker/provider-lock.mjs +104 -0
  20. package/template/.meta-agents/scripts/tracker-gateway.mjs +199 -0
  21. package/template/.meta-agents/scripts/verify-branch-strategy.mjs +103 -0
  22. package/template/.meta-agents/scripts/verify-commit-link.mjs +27 -13
  23. package/template/.meta-agents/scripts/verify-governance.mjs +37 -12
  24. package/template/.meta-agents/templates/agent-work-contract.md +8 -1
  25. package/template/.meta-agents/templates/task-template.md +7 -1
  26. package/template/README.md +43 -6
  27. package/template/agents.md +67 -16
  28. package/template/package.json +6 -1
  29. package/template/tracker-command-template.md +122 -28
  30. package/tracker-command-template.md +109 -15
@@ -0,0 +1,530 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import process from "node:process";
3
+
4
+ const BRANCH_PATTERNS = [
5
+ { type: "main", regex: /^main$/ },
6
+ { type: "develop", regex: /^develop$/ },
7
+ { type: "feature", regex: /^feature\/.+/ },
8
+ { type: "release", regex: /^release\/.+/ },
9
+ { type: "hotfix", regex: /^hotfix\/.+/ },
10
+ { type: "feature", regex: /^codex\/feature\/.+/ },
11
+ { type: "release", regex: /^codex\/release\/.+/ },
12
+ { type: "hotfix", regex: /^codex\/hotfix\/.+/ },
13
+ ];
14
+
15
+ const TASK_KIND_MAP = {
16
+ atomic: "feature",
17
+ feature: "feature",
18
+ release: "release",
19
+ hotfix: "hotfix",
20
+ };
21
+
22
+ function git(args, allowFailure = false) {
23
+ try {
24
+ return execFileSync("git", args, {
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ cwd: process.cwd(),
28
+ }).trim();
29
+ } catch (error) {
30
+ if (allowFailure) {
31
+ return "";
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ function parseArgs(argv) {
38
+ const options = {
39
+ task: "",
40
+ slug: "",
41
+ kind: "atomic",
42
+ base: "",
43
+ apply: false,
44
+ allowDirty: false,
45
+ allowAhead: false,
46
+ json: false,
47
+ };
48
+
49
+ for (let i = 0; i < argv.length; i += 1) {
50
+ const arg = argv[i];
51
+ if (arg === "--task") {
52
+ options.task = (argv[i + 1] || "").trim();
53
+ i += 1;
54
+ continue;
55
+ }
56
+ if (arg === "--slug") {
57
+ options.slug = (argv[i + 1] || "").trim();
58
+ i += 1;
59
+ continue;
60
+ }
61
+ if (arg === "--kind") {
62
+ options.kind = (argv[i + 1] || "").trim().toLowerCase();
63
+ i += 1;
64
+ continue;
65
+ }
66
+ if (arg === "--base") {
67
+ options.base = (argv[i + 1] || "").trim();
68
+ i += 1;
69
+ continue;
70
+ }
71
+ if (arg === "--apply") {
72
+ options.apply = true;
73
+ continue;
74
+ }
75
+ if (arg === "--allow-dirty") {
76
+ options.allowDirty = true;
77
+ continue;
78
+ }
79
+ if (arg === "--allow-ahead") {
80
+ options.allowAhead = true;
81
+ continue;
82
+ }
83
+ if (arg === "--json") {
84
+ options.json = true;
85
+ continue;
86
+ }
87
+ }
88
+
89
+ return options;
90
+ }
91
+
92
+ function detectBranch() {
93
+ const direct = git(["rev-parse", "--abbrev-ref", "HEAD"], true);
94
+ if (direct && direct !== "HEAD") {
95
+ return direct;
96
+ }
97
+
98
+ const status = git(["status", "--short", "--branch"], true);
99
+ const header = status.split("\n")[0] || "";
100
+
101
+ const noCommitsMatch = header.match(/^##\s+No commits yet on\s+(.+)$/);
102
+ if (noCommitsMatch) {
103
+ return noCommitsMatch[1].trim();
104
+ }
105
+
106
+ const branchMatch = header.match(/^##\s+([^\.\s]+)(?:\.\.\.[^\s]+)?/);
107
+ if (branchMatch) {
108
+ return branchMatch[1].trim();
109
+ }
110
+
111
+ return "";
112
+ }
113
+
114
+ function classifyBranch(branch) {
115
+ const source = String(branch || "").trim();
116
+ for (const pattern of BRANCH_PATTERNS) {
117
+ if (pattern.regex.test(source)) {
118
+ return pattern.type;
119
+ }
120
+ }
121
+ return "unknown";
122
+ }
123
+
124
+ function parseBranchHeader(header) {
125
+ const result = {
126
+ upstream: "",
127
+ ahead: 0,
128
+ behind: 0,
129
+ };
130
+
131
+ const match = header.match(/^##\s+[^\.\s]+(?:\.\.\.([^\s]+))?(?:\s+\[(.+)\])?/);
132
+ if (!match) {
133
+ return result;
134
+ }
135
+
136
+ result.upstream = match[1] || "";
137
+ const details = match[2] || "";
138
+ const aheadMatch = details.match(/ahead\s+(\d+)/);
139
+ const behindMatch = details.match(/behind\s+(\d+)/);
140
+ result.ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
141
+ result.behind = behindMatch ? Number(behindMatch[1]) : 0;
142
+ return result;
143
+ }
144
+
145
+ function collectGitContext() {
146
+ const raw = git(["status", "--short", "--branch"], true);
147
+ const lines = raw ? raw.split("\n").filter(Boolean) : [];
148
+ const header = lines[0] || "";
149
+ const files = lines.slice(1);
150
+ const parsed = parseBranchHeader(header);
151
+
152
+ let modifiedOrStaged = 0;
153
+ let untracked = 0;
154
+ for (const line of files) {
155
+ const status = line.slice(0, 2).trim();
156
+ if (status === "??") {
157
+ untracked += 1;
158
+ } else {
159
+ modifiedOrStaged += 1;
160
+ }
161
+ }
162
+
163
+ const branch = detectBranch() || "unknown";
164
+ return {
165
+ branch,
166
+ branchType: classifyBranch(branch),
167
+ branchTaskRef: extractBranchTaskRef(branch),
168
+ upstream: parsed.upstream || "not configured",
169
+ ahead: parsed.ahead,
170
+ behind: parsed.behind,
171
+ modifiedOrStaged,
172
+ untracked,
173
+ dirty: modifiedOrStaged > 0 || untracked > 0,
174
+ };
175
+ }
176
+
177
+ function extractBranchTaskRef(branch) {
178
+ const source = String(branch || "").trim();
179
+ if (!source) {
180
+ return "";
181
+ }
182
+
183
+ const normalized = source.startsWith("codex/") ? source.slice("codex/".length) : source;
184
+ const match = normalized.match(/^(feature|release|hotfix)\/(.+)$/);
185
+ if (!match) {
186
+ return "";
187
+ }
188
+
189
+ const tail = match[2];
190
+ const taskMatch = tail.match(/^([A-Za-z][A-Za-z0-9_]*-\d+)/);
191
+ if (taskMatch) {
192
+ return taskMatch[1].toUpperCase();
193
+ }
194
+
195
+ const issueMatch = tail.match(/^(\d+)(?:-|$)/);
196
+ if (issueMatch) {
197
+ return issueMatch[1];
198
+ }
199
+
200
+ return "";
201
+ }
202
+
203
+ function normalizeTaskRef(task) {
204
+ const source = String(task || "").trim();
205
+ if (!source) {
206
+ return "";
207
+ }
208
+
209
+ const issue = source.match(/^#?(\d+)$/);
210
+ if (issue) {
211
+ return issue[1];
212
+ }
213
+
214
+ const fromBodyIssue = source.match(/#(\d+)/);
215
+ if (fromBodyIssue) {
216
+ return fromBodyIssue[1];
217
+ }
218
+
219
+ const taskRef = source.match(/([A-Za-z][A-Za-z0-9_]*-\d+)/);
220
+ if (taskRef) {
221
+ return taskRef[1].toUpperCase();
222
+ }
223
+
224
+ return source.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
225
+ }
226
+
227
+ function slugify(value) {
228
+ return String(value || "")
229
+ .trim()
230
+ .toLowerCase()
231
+ .replace(/[^a-z0-9_-]+/g, "-")
232
+ .replace(/-+/g, "-")
233
+ .replace(/^-|-$/g, "");
234
+ }
235
+
236
+ function normalizeKind(kind) {
237
+ const normalized = TASK_KIND_MAP[kind];
238
+ if (!normalized) {
239
+ throw new Error("unsupported --kind. Allowed: atomic, feature, release, hotfix");
240
+ }
241
+ return normalized;
242
+ }
243
+
244
+ function defaultBaseBranch(kind) {
245
+ if (kind === "hotfix") {
246
+ return "main";
247
+ }
248
+ return "develop";
249
+ }
250
+
251
+ function buildTargetBranch({ kind, taskRef, slug }) {
252
+ const prefix = `codex/${kind}`;
253
+ const taskPart = slugify(taskRef) || "task";
254
+ const slugPart = slugify(slug) || "work";
255
+ if (slugPart === taskPart) {
256
+ return `${prefix}/${taskPart}`;
257
+ }
258
+ return `${prefix}/${taskPart}-${slugPart}`;
259
+ }
260
+
261
+ function hasRef(ref) {
262
+ return Boolean(git(["show-ref", "--verify", ref], true));
263
+ }
264
+
265
+ function hasOrigin() {
266
+ return Boolean(git(["remote", "get-url", "origin"], true));
267
+ }
268
+
269
+ function readFileAtRef(ref, filePath) {
270
+ return git(["show", `${ref}:${filePath}`], true);
271
+ }
272
+
273
+ function getContextWarnings(baseBranch) {
274
+ const warnings = [];
275
+ if (!baseBranch) {
276
+ return warnings;
277
+ }
278
+
279
+ const baseExists = hasRef(`refs/heads/${baseBranch}`) || hasRef(`refs/remotes/origin/${baseBranch}`);
280
+ if (!baseExists) {
281
+ warnings.push(
282
+ `Cannot compare governance context: base branch '${baseBranch}' is not available locally/remotely.`,
283
+ );
284
+ return warnings;
285
+ }
286
+
287
+ const currentAgents = readFileAtRef("HEAD", "agents.md");
288
+ const baseAgents = readFileAtRef(baseBranch, "agents.md");
289
+
290
+ if (!currentAgents || !baseAgents) {
291
+ warnings.push(
292
+ "Cannot compare governance context file 'agents.md' between current and base branch (missing in one of refs).",
293
+ );
294
+ return warnings;
295
+ }
296
+
297
+ if (currentAgents !== baseAgents) {
298
+ warnings.push(
299
+ `Governance context differs: 'agents.md' in current branch is not equal to '${baseBranch}:agents.md'. Confirm switch before apply.`,
300
+ );
301
+ }
302
+
303
+ return warnings;
304
+ }
305
+
306
+ function executePlan(plan, gitContext) {
307
+ if (plan.decision === "stay_on_current_branch") {
308
+ return {
309
+ changedBranch: false,
310
+ branch: gitContext.branch,
311
+ message: "Task is related to current feature branch. Stay in place.",
312
+ };
313
+ }
314
+
315
+ if (plan.blockers.length > 0) {
316
+ throw new Error(`cannot apply route because blockers exist: ${plan.blockers.join("; ")}`);
317
+ }
318
+
319
+ if (hasRef(`refs/heads/${plan.targetBranch}`) || hasRef(`refs/remotes/origin/${plan.targetBranch}`)) {
320
+ throw new Error(`target branch '${plan.targetBranch}' already exists`);
321
+ }
322
+
323
+ const baseBranch = plan.requested.baseBranch;
324
+ const originExists = hasOrigin();
325
+ if (originExists) {
326
+ git(["fetch", "origin"], false);
327
+ }
328
+
329
+ const baseLocal = hasRef(`refs/heads/${baseBranch}`);
330
+ const baseRemote = hasRef(`refs/remotes/origin/${baseBranch}`);
331
+
332
+ if (!baseLocal && !baseRemote) {
333
+ throw new Error(
334
+ `base branch '${baseBranch}' not found locally or on origin. Create/sync it first.`,
335
+ );
336
+ }
337
+
338
+ if (baseLocal) {
339
+ git(["checkout", baseBranch], false);
340
+ } else {
341
+ git(["checkout", "-b", baseBranch, "--track", `origin/${baseBranch}`], false);
342
+ }
343
+
344
+ if (originExists && baseRemote) {
345
+ git(["pull", "--ff-only", "origin", baseBranch], false);
346
+ }
347
+
348
+ git(["checkout", "-b", plan.targetBranch], false);
349
+
350
+ return {
351
+ changedBranch: true,
352
+ branch: plan.targetBranch,
353
+ message: `Created and checked out '${plan.targetBranch}' from '${baseBranch}'.`,
354
+ };
355
+ }
356
+
357
+ function buildPlan(options, gitContext) {
358
+ if (!options.task) {
359
+ throw new Error("missing --task");
360
+ }
361
+
362
+ const kind = normalizeKind(options.kind);
363
+ const taskRef = normalizeTaskRef(options.task);
364
+ const sameFeature =
365
+ kind === "feature" &&
366
+ gitContext.branchType === "feature" &&
367
+ Boolean(taskRef) &&
368
+ taskRef === gitContext.branchTaskRef;
369
+
370
+ const baseBranch = options.base || defaultBaseBranch(kind);
371
+ const targetBranch = sameFeature
372
+ ? gitContext.branch
373
+ : buildTargetBranch({
374
+ kind,
375
+ taskRef,
376
+ slug: options.slug || options.task,
377
+ });
378
+
379
+ const blockers = [];
380
+ const requiresDialogue = [];
381
+
382
+ if (!sameFeature && gitContext.dirty && !options.allowDirty) {
383
+ blockers.push("working_tree_dirty");
384
+ requiresDialogue.push(
385
+ "Есть незакоммиченные изменения. Нужен выбор: commit/stash/discard перед переключением ветки.",
386
+ );
387
+ }
388
+
389
+ if (!sameFeature && gitContext.ahead > 0 && !options.allowAhead) {
390
+ blockers.push("current_branch_ahead");
391
+ requiresDialogue.push(
392
+ "В текущей ветке есть непушенные коммиты (ahead > 0). Нужна явная фиксация решения перед переключением.",
393
+ );
394
+ }
395
+
396
+ if (sameFeature) {
397
+ requiresDialogue.push(
398
+ "Задача атомарная и относится к текущей feature-ветке. Подтвердите, что продолжаем работу в этой ветке.",
399
+ );
400
+ } else {
401
+ requiresDialogue.push(
402
+ "Задача не относится к текущей ветке. Подтвердите переключение на базовую ветку, обновление и создание новой рабочей ветки.",
403
+ );
404
+ }
405
+
406
+ return {
407
+ requested: {
408
+ task: options.task,
409
+ taskRef,
410
+ kind,
411
+ baseBranch,
412
+ },
413
+ decision: sameFeature ? "stay_on_current_branch" : "create_new_branch",
414
+ current: gitContext,
415
+ targetBranch,
416
+ blockers,
417
+ contextWarnings: getContextWarnings(baseBranch),
418
+ requiresDialogue,
419
+ suggestedCommands: sameFeature
420
+ ? []
421
+ : [
422
+ "git fetch origin",
423
+ `git checkout ${baseBranch}`,
424
+ `git pull --ff-only origin ${baseBranch}`,
425
+ `git checkout -b ${targetBranch}`,
426
+ ],
427
+ };
428
+ }
429
+
430
+ function printPlan(plan, applyResult) {
431
+ console.log("# Task Branch Routing");
432
+ console.log("");
433
+ console.log("## Current Git Context");
434
+ console.log(`- branch: ${plan.current.branch}`);
435
+ console.log(`- branch type: ${plan.current.branchType}`);
436
+ console.log(`- branch task ref: ${plan.current.branchTaskRef || "not detected"}`);
437
+ console.log(`- upstream: ${plan.current.upstream}`);
438
+ console.log(`- ahead/behind: ${plan.current.ahead}/${plan.current.behind}`);
439
+ console.log(`- modified_or_staged: ${plan.current.modifiedOrStaged}`);
440
+ console.log(`- untracked: ${plan.current.untracked}`);
441
+
442
+ console.log("");
443
+ console.log("## Requested Task");
444
+ console.log(`- task: ${plan.requested.task}`);
445
+ console.log(`- normalized task ref: ${plan.requested.taskRef || "not detected"}`);
446
+ console.log(`- kind: ${plan.requested.kind}`);
447
+ console.log(`- base branch: ${plan.requested.baseBranch}`);
448
+
449
+ console.log("");
450
+ console.log("## Decision");
451
+ console.log(`- route: ${plan.decision}`);
452
+ console.log(`- target branch: ${plan.targetBranch}`);
453
+ if (plan.blockers.length === 0) {
454
+ console.log("- blockers: none");
455
+ } else {
456
+ console.log(`- blockers: ${plan.blockers.join(", ")}`);
457
+ }
458
+
459
+ console.log("");
460
+ console.log("## Context Warnings");
461
+ if (plan.contextWarnings.length === 0) {
462
+ console.log("- none");
463
+ } else {
464
+ for (const warning of plan.contextWarnings) {
465
+ console.log(`- ${warning}`);
466
+ }
467
+ }
468
+
469
+ console.log("");
470
+ console.log("## Required User Dialogue");
471
+ for (const line of plan.requiresDialogue) {
472
+ console.log(`- ${line}`);
473
+ }
474
+
475
+ console.log("");
476
+ console.log("## Suggested Commands");
477
+ if (plan.suggestedCommands.length === 0) {
478
+ console.log("- no branch switch required");
479
+ } else {
480
+ for (const cmd of plan.suggestedCommands) {
481
+ console.log(`- ${cmd}`);
482
+ }
483
+ }
484
+
485
+ if (applyResult) {
486
+ console.log("");
487
+ console.log("## Apply Result");
488
+ console.log(`- changed branch: ${applyResult.changedBranch ? "yes" : "no"}`);
489
+ console.log(`- active branch: ${applyResult.branch}`);
490
+ console.log(`- message: ${applyResult.message}`);
491
+ } else {
492
+ console.log("");
493
+ console.log("## Apply");
494
+ console.log("- preview mode only. Re-run with --apply after user confirmation.");
495
+ }
496
+ }
497
+
498
+ function main() {
499
+ const options = parseArgs(process.argv.slice(2));
500
+ const gitContext = collectGitContext();
501
+ const plan = buildPlan(options, gitContext);
502
+
503
+ let applyResult = null;
504
+ if (options.apply) {
505
+ applyResult = executePlan(plan, gitContext);
506
+ }
507
+
508
+ if (options.json) {
509
+ console.log(
510
+ JSON.stringify(
511
+ {
512
+ ...plan,
513
+ applyResult,
514
+ },
515
+ null,
516
+ 2,
517
+ ),
518
+ );
519
+ return;
520
+ }
521
+
522
+ printPlan(plan, applyResult);
523
+ }
524
+
525
+ try {
526
+ main();
527
+ } catch (error) {
528
+ console.error(`task-branch-router FAIL: ${error.message}`);
529
+ process.exit(1);
530
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const ALLOWED_TRACKERS = new Set(["github", "mcp", "local", "custom"]);
5
+
6
+ function normalizeTracker(value) {
7
+ if (!value) {
8
+ return "";
9
+ }
10
+ const normalized = String(value).trim().toLowerCase();
11
+ if (normalized === "mpc") {
12
+ return "mcp";
13
+ }
14
+ return normalized;
15
+ }
16
+
17
+ function isPlaceholder(value) {
18
+ return /^__[^_].*__$/.test(value);
19
+ }
20
+
21
+ function parseTrackersDefault(content) {
22
+ const match = content.match(/^\s*default:\s*"?([a-zA-Z0-9_-]+)"?\s*$/m);
23
+ return normalizeTracker(match?.[1] || "");
24
+ }
25
+
26
+ function parseProjectSelected(content) {
27
+ const match = content.match(/^\s*selected:\s*"?([a-zA-Z0-9_-]+)"?\s*$/m);
28
+ return normalizeTracker(match?.[1] || "");
29
+ }
30
+
31
+ function readTextOrEmpty(filePath) {
32
+ try {
33
+ return fs.readFileSync(filePath, "utf8");
34
+ } catch {
35
+ return "";
36
+ }
37
+ }
38
+
39
+ export function resolveLockedTrackerProvider(cwd = process.cwd()) {
40
+ const configDir = path.resolve(cwd, ".meta-agents", "config");
41
+ const trackersPath = path.join(configDir, "trackers.yaml");
42
+ const projectContextPath = path.join(configDir, "project-context.yaml");
43
+
44
+ const trackersYaml = readTextOrEmpty(trackersPath);
45
+ const projectContextYaml = readTextOrEmpty(projectContextPath);
46
+
47
+ const errors = [];
48
+ if (!trackersYaml) {
49
+ errors.push("missing .meta-agents/config/trackers.yaml");
50
+ }
51
+ if (!projectContextYaml) {
52
+ errors.push("missing .meta-agents/config/project-context.yaml");
53
+ }
54
+
55
+ const trackerDefault = parseTrackersDefault(trackersYaml);
56
+ const projectSelected = parseProjectSelected(projectContextYaml);
57
+
58
+ if (!trackerDefault) {
59
+ errors.push("trackers.yaml must define tracker_gateway.default");
60
+ }
61
+ if (!projectSelected) {
62
+ errors.push("project-context.yaml must define project_context.tracker_provider.selected");
63
+ }
64
+
65
+ if (trackerDefault && isPlaceholder(trackerDefault)) {
66
+ errors.push("trackers.yaml has unresolved placeholder in tracker_gateway.default");
67
+ }
68
+ if (projectSelected && isPlaceholder(projectSelected)) {
69
+ errors.push("project-context.yaml has unresolved placeholder in tracker_provider.selected");
70
+ }
71
+
72
+ if (trackerDefault && !ALLOWED_TRACKERS.has(trackerDefault)) {
73
+ errors.push(`unsupported tracker_gateway.default '${trackerDefault}'`);
74
+ }
75
+ if (projectSelected && !ALLOWED_TRACKERS.has(projectSelected)) {
76
+ errors.push(`unsupported tracker_provider.selected '${projectSelected}'`);
77
+ }
78
+
79
+ if (trackerDefault && projectSelected && trackerDefault !== projectSelected) {
80
+ errors.push(
81
+ `tracker mismatch: trackers.yaml default='${trackerDefault}' vs project-context selected='${projectSelected}'`,
82
+ );
83
+ }
84
+
85
+ if (errors.length > 0) {
86
+ throw new Error(`tracker provider lock FAIL: ${errors.join("; ")}`);
87
+ }
88
+
89
+ return trackerDefault || projectSelected;
90
+ }
91
+
92
+ export function resolveTrackerForCommand({ requestedTracker = "", cwd = process.cwd() } = {}) {
93
+ const lockedProvider = resolveLockedTrackerProvider(cwd);
94
+ const requested = normalizeTracker(requestedTracker);
95
+
96
+ if (requested && requested !== lockedProvider) {
97
+ throw new Error(
98
+ `tracker provider mismatch: requested='${requested}' but locked='${lockedProvider}'. ` +
99
+ "Update project-context.yaml and trackers.yaml together before switching tracker.",
100
+ );
101
+ }
102
+
103
+ return lockedProvider;
104
+ }