@k0t0vich/meta-agents-template 0.1.2 → 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 +41 -0
- package/README.md +54 -10
- package/agents.md +69 -13
- package/package.json +2 -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 +127 -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/publishing-agent.md +2 -1
- 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 -5
- package/template/agents.md +68 -15
- package/template/package.json +6 -1
- package/template/tracker-command-template.md +123 -28
- package/tracker-command-template.md +142 -14
|
@@ -1 +1,620 @@
|
|
|
1
|
-
|
|
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 STATUS_LABELS = ["IN_PROGRESS", "REVIEW", "READY", "BLOCKED", "TODO", "DONE", "PUBLISH"];
|
|
8
|
+
const EXCLUDED_TASK_FILES = new Set(["backlog.md", "task-status-log.md"]);
|
|
9
|
+
const BRANCH_PATTERNS = [
|
|
10
|
+
{ type: "main", regex: /^main$/ },
|
|
11
|
+
{ type: "develop", regex: /^develop$/ },
|
|
12
|
+
{ type: "feature", regex: /^feature\/.+/ },
|
|
13
|
+
{ type: "release", regex: /^release\/.+/ },
|
|
14
|
+
{ type: "hotfix", regex: /^hotfix\/.+/ },
|
|
15
|
+
{ type: "feature", regex: /^codex\/feature\/.+/ },
|
|
16
|
+
{ type: "release", regex: /^codex\/release\/.+/ },
|
|
17
|
+
{ type: "hotfix", regex: /^codex\/hotfix\/.+/ },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const options = {
|
|
22
|
+
tracker: "",
|
|
23
|
+
includeLocalCache: false,
|
|
24
|
+
json: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (arg === "--tracker") {
|
|
30
|
+
options.tracker = (argv[i + 1] || "").trim().toLowerCase();
|
|
31
|
+
i += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === "--include-local-cache") {
|
|
35
|
+
options.includeLocalCache = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--json") {
|
|
39
|
+
options.json = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return options;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function run(command, args, allowFailure = false) {
|
|
48
|
+
try {
|
|
49
|
+
return execFileSync(command, args, {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
cwd: process.cwd(),
|
|
52
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
+
}).trim();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (allowFailure) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseBranchHeader(header) {
|
|
63
|
+
// Example: "## main...origin/main [ahead 1]"
|
|
64
|
+
const result = {
|
|
65
|
+
branch: "unknown",
|
|
66
|
+
upstream: "",
|
|
67
|
+
ahead: 0,
|
|
68
|
+
behind: 0,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const noCommitsMatch = header.match(/^##\s+No commits yet on\s+(.+)$/);
|
|
72
|
+
if (noCommitsMatch) {
|
|
73
|
+
result.branch = noCommitsMatch[1].trim();
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const match = header.match(/^##\s+([^\.\s]+)(?:\.\.\.([^\s]+))?(?:\s+\[(.+)\])?/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result.branch = match[1] || "unknown";
|
|
83
|
+
result.upstream = match[2] || "";
|
|
84
|
+
|
|
85
|
+
const aheadMatch = (match[3] || "").match(/ahead\s+(\d+)/);
|
|
86
|
+
const behindMatch = (match[3] || "").match(/behind\s+(\d+)/);
|
|
87
|
+
result.ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
|
|
88
|
+
result.behind = behindMatch ? Number(behindMatch[1]) : 0;
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectGitSummary() {
|
|
94
|
+
const raw = run("git", ["status", "--short", "--branch"], true);
|
|
95
|
+
if (!raw) {
|
|
96
|
+
return {
|
|
97
|
+
branch: "unknown",
|
|
98
|
+
branchType: "unknown",
|
|
99
|
+
branchTaskRef: "",
|
|
100
|
+
upstream: "",
|
|
101
|
+
ahead: 0,
|
|
102
|
+
behind: 0,
|
|
103
|
+
modifiedOrStaged: 0,
|
|
104
|
+
untracked: 0,
|
|
105
|
+
changedFiles: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
110
|
+
const header = lines[0] || "";
|
|
111
|
+
const files = lines.slice(1);
|
|
112
|
+
const parsed = parseBranchHeader(header);
|
|
113
|
+
|
|
114
|
+
let modifiedOrStaged = 0;
|
|
115
|
+
let untracked = 0;
|
|
116
|
+
const changedFiles = [];
|
|
117
|
+
|
|
118
|
+
for (const line of files) {
|
|
119
|
+
const status = line.slice(0, 2).trim();
|
|
120
|
+
const file = line.slice(3).trim();
|
|
121
|
+
|
|
122
|
+
if (status === "??") {
|
|
123
|
+
untracked += 1;
|
|
124
|
+
} else {
|
|
125
|
+
modifiedOrStaged += 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (file) {
|
|
129
|
+
changedFiles.push(file);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...parsed,
|
|
135
|
+
branchType: classifyBranch(parsed.branch),
|
|
136
|
+
branchTaskRef: extractBranchTaskRef(parsed.branch),
|
|
137
|
+
modifiedOrStaged,
|
|
138
|
+
untracked,
|
|
139
|
+
changedFiles,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readTrackersConfig() {
|
|
144
|
+
const file = path.resolve(process.cwd(), ".meta-agents", "config", "trackers.yaml");
|
|
145
|
+
const content = await fs.readFile(file, "utf8");
|
|
146
|
+
|
|
147
|
+
const providers = [];
|
|
148
|
+
const lines = content.split("\n");
|
|
149
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
150
|
+
const line = lines[i];
|
|
151
|
+
const match = line.match(/^\s{4}([a-zA-Z0-9_-]+):\s*$/);
|
|
152
|
+
if (!match) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const name = match[1];
|
|
157
|
+
if (name === "providers") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const next = lines[i + 1] || "";
|
|
162
|
+
if (/^\s{6}mode:\s*/.test(next)) {
|
|
163
|
+
providers.push(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return providers;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function parseLocalTaskStatus(filePath) {
|
|
171
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
172
|
+
const id = path.basename(filePath, ".md");
|
|
173
|
+
const statusMatch = content.match(/^\-\s+Status:\s*(.+)$/m);
|
|
174
|
+
const shortNameMatch = content.match(/^\-\s+Short Name:\s*(.+)$/m);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
id,
|
|
178
|
+
shortName: (shortNameMatch?.[1] || "").trim(),
|
|
179
|
+
status: (statusMatch?.[1] || "UNKNOWN").trim(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function listLocalTaskFiles() {
|
|
184
|
+
const tasksDir = path.resolve(process.cwd(), "tasks");
|
|
185
|
+
try {
|
|
186
|
+
const entries = await fs.readdir(tasksDir, { withFileTypes: true });
|
|
187
|
+
return entries
|
|
188
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && !EXCLUDED_TASK_FILES.has(entry.name) && !entry.name.startsWith("sprint-"))
|
|
189
|
+
.map((entry) => path.join(tasksDir, entry.name));
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function parseLocalSprint() {
|
|
196
|
+
const tasksDir = path.resolve(process.cwd(), "tasks");
|
|
197
|
+
let entries;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
entries = await fs.readdir(tasksDir, { withFileTypes: true });
|
|
201
|
+
} catch {
|
|
202
|
+
return { id: "", status: "", source: "none" };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sprintFiles = entries
|
|
206
|
+
.filter((entry) => entry.isFile() && /^sprint-.*\.md$/i.test(entry.name))
|
|
207
|
+
.map((entry) => path.join(tasksDir, entry.name));
|
|
208
|
+
|
|
209
|
+
if (sprintFiles.length === 0) {
|
|
210
|
+
return { id: "", status: "", source: "none" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parsed = [];
|
|
214
|
+
for (const file of sprintFiles) {
|
|
215
|
+
const content = await fs.readFile(file, "utf8");
|
|
216
|
+
const idMatch = content.match(/^\-\s+Sprint ID:\s*(.+)$/m);
|
|
217
|
+
const statusMatch = content.match(/^\-\s+Status:\s*(.+)$/m);
|
|
218
|
+
parsed.push({
|
|
219
|
+
file,
|
|
220
|
+
id: (idMatch?.[1] || path.basename(file, ".md")).trim(),
|
|
221
|
+
status: (statusMatch?.[1] || "UNKNOWN").trim(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const active = parsed.find((item) => /IN_PROGRESS|ACTIVE/i.test(item.status));
|
|
226
|
+
return active || parsed[0];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function parseLocalStatusLogCurrentTask() {
|
|
230
|
+
const file = path.resolve(process.cwd(), "tasks", "task-status-log.md");
|
|
231
|
+
let content = "";
|
|
232
|
+
try {
|
|
233
|
+
content = await fs.readFile(file, "utf8");
|
|
234
|
+
} catch {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const lines = content.split("\n").map((line) => line.trim());
|
|
239
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
240
|
+
const line = lines[i];
|
|
241
|
+
const match = line.match(/^\-\s*\d{4}-\d{2}-\d{2}\s*\|\s*([A-Z][A-Z0-9_]*-\d+)\s*\|\s*([A-Z_]+)\s*\|/);
|
|
242
|
+
if (!match) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const taskId = match[1];
|
|
247
|
+
const status = match[2];
|
|
248
|
+
if (!["DONE", "PUBLISH", "BLOCKED"].includes(status)) {
|
|
249
|
+
return `${taskId} (${status})`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function detectTaskFromBranch(branch) {
|
|
257
|
+
const taskRef = extractBranchTaskRef(branch);
|
|
258
|
+
if (!taskRef) {
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
if (/^\d+$/.test(taskRef)) {
|
|
262
|
+
return `#${taskRef}`;
|
|
263
|
+
}
|
|
264
|
+
return taskRef;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function classifyBranch(branch) {
|
|
268
|
+
const source = String(branch || "").trim();
|
|
269
|
+
for (const pattern of BRANCH_PATTERNS) {
|
|
270
|
+
if (pattern.regex.test(source)) {
|
|
271
|
+
return pattern.type;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return "unknown";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function extractBranchTaskRef(branch) {
|
|
278
|
+
const source = String(branch || "").trim();
|
|
279
|
+
if (!source) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const normalized = source.startsWith("codex/") ? source.slice("codex/".length) : source;
|
|
284
|
+
const match = normalized.match(/^(feature|release|hotfix)\/(.+)$/);
|
|
285
|
+
if (!match) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const tail = match[2];
|
|
290
|
+
const uppercaseToken = tail.match(/^([A-Z][A-Z0-9_]*-\d+)/);
|
|
291
|
+
if (uppercaseToken) {
|
|
292
|
+
return uppercaseToken[1].toUpperCase();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const numericToken = tail.match(/^(\d+)(?:-|$)/);
|
|
296
|
+
if (numericToken) {
|
|
297
|
+
return numericToken[1];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function extractTaskRefFromCurrentTask(currentTask) {
|
|
304
|
+
const source = String(currentTask || "").trim();
|
|
305
|
+
if (!source) {
|
|
306
|
+
return "";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const issueMatch = source.match(/#(\d+)/);
|
|
310
|
+
if (issueMatch) {
|
|
311
|
+
return issueMatch[1];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const taskMatch = source.match(/\b([A-Z][A-Z0-9_]*-\d+)\b/);
|
|
315
|
+
if (taskMatch) {
|
|
316
|
+
return taskMatch[1].toUpperCase();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function computeBranchTaskAlignment({ branchType, branchTaskRef, currentTask }) {
|
|
323
|
+
const currentTaskRef = extractTaskRefFromCurrentTask(currentTask);
|
|
324
|
+
if (!currentTaskRef || !branchTaskRef) {
|
|
325
|
+
return {
|
|
326
|
+
status: "unknown",
|
|
327
|
+
currentTaskRef,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (currentTaskRef === branchTaskRef) {
|
|
332
|
+
return {
|
|
333
|
+
status: "match",
|
|
334
|
+
currentTaskRef,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (branchType === "feature") {
|
|
339
|
+
return {
|
|
340
|
+
status: "mismatch",
|
|
341
|
+
currentTaskRef,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
status: "unknown",
|
|
347
|
+
currentTaskRef,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function normalizeStatusRank(status) {
|
|
352
|
+
const idx = STATUS_LABELS.indexOf(status);
|
|
353
|
+
return idx === -1 ? 999 : idx;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function findSprintLabel(labels) {
|
|
357
|
+
for (const label of labels) {
|
|
358
|
+
const name = label.name || "";
|
|
359
|
+
if (/^sprint[:\-]/i.test(name) || /^SPRINT-\d+$/i.test(name)) {
|
|
360
|
+
return name;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return "";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseIssueStatus(labels) {
|
|
367
|
+
for (const label of labels) {
|
|
368
|
+
const name = (label.name || "").toUpperCase();
|
|
369
|
+
if (STATUS_LABELS.includes(name)) {
|
|
370
|
+
return name;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function pickCurrentLocalTask(localTasks) {
|
|
377
|
+
const priority = ["IN_PROGRESS", "REVIEW", "READY", "TODO", "BLOCKED"];
|
|
378
|
+
for (const target of priority) {
|
|
379
|
+
const found = localTasks.find((task) => String(task.status || "").toUpperCase() === target);
|
|
380
|
+
if (found) {
|
|
381
|
+
const short = found.shortName ? ` ${found.shortName}` : "";
|
|
382
|
+
return `${found.id}${short} (${target})`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function readJson(command, args) {
|
|
389
|
+
const raw = run(command, args, false);
|
|
390
|
+
return JSON.parse(raw || "null");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function collectGithubWorkflow() {
|
|
394
|
+
try {
|
|
395
|
+
run("gh", ["--version"], false);
|
|
396
|
+
} catch {
|
|
397
|
+
return { available: false, reason: "gh CLI not installed" };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const auth = run("gh", ["auth", "status"], true);
|
|
401
|
+
if (!auth) {
|
|
402
|
+
return { available: false, reason: "gh auth status unavailable" };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const repo = readJson("gh", ["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
|
407
|
+
const issues = readJson("gh", ["issue", "list", "--state", "open", "--limit", "100", "--json", "number,title,labels"]);
|
|
408
|
+
|
|
409
|
+
const normalizedIssues = (issues || []).map((issue) => {
|
|
410
|
+
const labels = issue.labels || [];
|
|
411
|
+
const status = parseIssueStatus(labels) || "UNLABELED";
|
|
412
|
+
const sprint = findSprintLabel(labels);
|
|
413
|
+
return {
|
|
414
|
+
number: issue.number,
|
|
415
|
+
title: issue.title || "",
|
|
416
|
+
status,
|
|
417
|
+
sprint,
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
normalizedIssues.sort((a, b) => normalizeStatusRank(a.status) - normalizeStatusRank(b.status));
|
|
422
|
+
|
|
423
|
+
const currentIssue = normalizedIssues.find((issue) => ["IN_PROGRESS", "REVIEW", "READY", "BLOCKED", "TODO"].includes(issue.status)) || null;
|
|
424
|
+
|
|
425
|
+
const sprintCounts = new Map();
|
|
426
|
+
for (const issue of normalizedIssues) {
|
|
427
|
+
if (!issue.sprint) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
sprintCounts.set(issue.sprint, (sprintCounts.get(issue.sprint) || 0) + 1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let currentSprint = "";
|
|
434
|
+
let maxCount = 0;
|
|
435
|
+
for (const [sprint, count] of sprintCounts.entries()) {
|
|
436
|
+
if (count > maxCount) {
|
|
437
|
+
maxCount = count;
|
|
438
|
+
currentSprint = sprint;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
available: true,
|
|
444
|
+
repo: repo?.nameWithOwner || "",
|
|
445
|
+
defaultBranch: repo?.defaultBranchRef?.name || "",
|
|
446
|
+
currentIssue,
|
|
447
|
+
currentSprint,
|
|
448
|
+
openIssues: normalizedIssues.length,
|
|
449
|
+
};
|
|
450
|
+
} catch (error) {
|
|
451
|
+
return {
|
|
452
|
+
available: false,
|
|
453
|
+
reason: `github query failed: ${error.message}`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function main() {
|
|
459
|
+
const options = parseArgs(process.argv.slice(2));
|
|
460
|
+
const trackerProvider = resolveTrackerForCommand({
|
|
461
|
+
requestedTracker: options.tracker,
|
|
462
|
+
cwd: process.cwd(),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const git = collectGitSummary();
|
|
466
|
+
const providers = await readTrackersConfig();
|
|
467
|
+
const localTaskFiles = await listLocalTaskFiles();
|
|
468
|
+
const localTasks = [];
|
|
469
|
+
|
|
470
|
+
for (const file of localTaskFiles) {
|
|
471
|
+
localTasks.push(await parseLocalTaskStatus(file));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const localSprint = await parseLocalSprint();
|
|
475
|
+
const localCurrentTask = await parseLocalStatusLogCurrentTask();
|
|
476
|
+
const branchTask = detectTaskFromBranch(git.branch);
|
|
477
|
+
|
|
478
|
+
let currentTask = "";
|
|
479
|
+
let currentSprint = "";
|
|
480
|
+
let workflowSource = "";
|
|
481
|
+
const notes = [];
|
|
482
|
+
|
|
483
|
+
if (trackerProvider === "github") {
|
|
484
|
+
const gh = collectGithubWorkflow();
|
|
485
|
+
if (gh.available) {
|
|
486
|
+
workflowSource = `github:${gh.repo || "unknown"}`;
|
|
487
|
+
currentTask = gh.currentIssue ? `#${gh.currentIssue.number} ${gh.currentIssue.title} (${gh.currentIssue.status})` : "";
|
|
488
|
+
currentSprint = gh.currentSprint || (gh.currentIssue?.sprint || "");
|
|
489
|
+
notes.push(`GitHub issues open: ${gh.openIssues}`);
|
|
490
|
+
} else {
|
|
491
|
+
workflowSource = "github (fallback)";
|
|
492
|
+
notes.push(`GitHub tracker snapshot unavailable: ${gh.reason}`);
|
|
493
|
+
if (options.includeLocalCache) {
|
|
494
|
+
currentTask = localCurrentTask || branchTask;
|
|
495
|
+
currentSprint = localSprint.id || "";
|
|
496
|
+
notes.push("Used local tasks cache because --include-local-cache is enabled.");
|
|
497
|
+
} else {
|
|
498
|
+
currentTask = branchTask;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (localTasks.length > 0) {
|
|
503
|
+
notes.push(
|
|
504
|
+
`Detected ${localTasks.length} local task file(s) in tasks/ (treated as cache, non-authoritative for github tracker).`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
} else if (trackerProvider === "local") {
|
|
508
|
+
workflowSource = "local tasks";
|
|
509
|
+
currentTask = localCurrentTask || pickCurrentLocalTask(localTasks) || branchTask;
|
|
510
|
+
currentSprint = localSprint.id || "";
|
|
511
|
+
} else {
|
|
512
|
+
workflowSource = trackerProvider;
|
|
513
|
+
currentTask = branchTask;
|
|
514
|
+
if (localTasks.length > 0) {
|
|
515
|
+
notes.push(`Detected ${localTasks.length} local task file(s) in tasks/ (cache).`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const branchTaskAlignment = computeBranchTaskAlignment({
|
|
520
|
+
branchType: git.branchType,
|
|
521
|
+
branchTaskRef: git.branchTaskRef,
|
|
522
|
+
currentTask,
|
|
523
|
+
});
|
|
524
|
+
const currentTaskRef = branchTaskAlignment.currentTaskRef;
|
|
525
|
+
|
|
526
|
+
if (git.branchType === "main") {
|
|
527
|
+
notes.push("Current branch is main; task-level implementation should run on feature/release/hotfix branches.");
|
|
528
|
+
}
|
|
529
|
+
if (git.branchType === "develop" && currentTaskRef) {
|
|
530
|
+
notes.push("Current task detected on develop; create a working branch before implementation changes.");
|
|
531
|
+
}
|
|
532
|
+
if (branchTaskAlignment.status === "match" && git.branchType === "feature") {
|
|
533
|
+
notes.push("Current task matches feature branch; atomic subtasks can stay in this branch.");
|
|
534
|
+
}
|
|
535
|
+
if (branchTaskAlignment.status === "mismatch") {
|
|
536
|
+
notes.push(
|
|
537
|
+
`Current task (${currentTaskRef}) is different from branch context (${git.branchTaskRef}); switch to develop and create a dedicated branch.`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const report = {
|
|
542
|
+
tracker: {
|
|
543
|
+
lockedProvider: trackerProvider,
|
|
544
|
+
availableProviders: providers,
|
|
545
|
+
workflowSource,
|
|
546
|
+
},
|
|
547
|
+
process: {
|
|
548
|
+
currentSprint: currentSprint || "not detected",
|
|
549
|
+
currentTask: currentTask || "not detected",
|
|
550
|
+
currentTaskRef: currentTaskRef || "not detected",
|
|
551
|
+
branchTaskAlignment: branchTaskAlignment.status,
|
|
552
|
+
},
|
|
553
|
+
git: {
|
|
554
|
+
branch: git.branch,
|
|
555
|
+
branchType: git.branchType,
|
|
556
|
+
branchTaskRef: git.branchTaskRef || "not detected",
|
|
557
|
+
upstream: git.upstream || "not configured",
|
|
558
|
+
ahead: git.ahead,
|
|
559
|
+
behind: git.behind,
|
|
560
|
+
modifiedOrStaged: git.modifiedOrStaged,
|
|
561
|
+
untracked: git.untracked,
|
|
562
|
+
changedFiles: git.changedFiles,
|
|
563
|
+
},
|
|
564
|
+
summary: notes,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
if (options.json) {
|
|
568
|
+
console.log(JSON.stringify(report, null, 2));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
console.log("# Status Snapshot");
|
|
573
|
+
console.log("");
|
|
574
|
+
console.log("## Trackers");
|
|
575
|
+
console.log(`- locked provider: ${report.tracker.lockedProvider}`);
|
|
576
|
+
console.log(`- available providers: ${report.tracker.availableProviders.join(", ") || "unknown"}`);
|
|
577
|
+
console.log(`- workflow source: ${report.tracker.workflowSource}`);
|
|
578
|
+
|
|
579
|
+
console.log("");
|
|
580
|
+
console.log("## Process");
|
|
581
|
+
console.log(`- current sprint: ${report.process.currentSprint}`);
|
|
582
|
+
console.log(`- current task: ${report.process.currentTask}`);
|
|
583
|
+
console.log(`- current task ref: ${report.process.currentTaskRef}`);
|
|
584
|
+
console.log(`- branch/task alignment: ${report.process.branchTaskAlignment}`);
|
|
585
|
+
|
|
586
|
+
console.log("");
|
|
587
|
+
console.log("## Git");
|
|
588
|
+
console.log(`- branch: ${report.git.branch}`);
|
|
589
|
+
console.log(`- branch type: ${report.git.branchType}`);
|
|
590
|
+
console.log(`- branch task ref: ${report.git.branchTaskRef}`);
|
|
591
|
+
console.log(`- upstream: ${report.git.upstream}`);
|
|
592
|
+
console.log(`- ahead/behind: ${report.git.ahead}/${report.git.behind}`);
|
|
593
|
+
console.log(`- modified_or_staged: ${report.git.modifiedOrStaged}`);
|
|
594
|
+
console.log(`- untracked: ${report.git.untracked}`);
|
|
595
|
+
|
|
596
|
+
if (report.git.changedFiles.length > 0) {
|
|
597
|
+
console.log("- changed files:");
|
|
598
|
+
for (const file of report.git.changedFiles.slice(0, 20)) {
|
|
599
|
+
console.log(` - ${file}`);
|
|
600
|
+
}
|
|
601
|
+
if (report.git.changedFiles.length > 20) {
|
|
602
|
+
console.log(` - ... +${report.git.changedFiles.length - 20} more`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
console.log("");
|
|
607
|
+
console.log("## Summary");
|
|
608
|
+
if (report.summary.length === 0) {
|
|
609
|
+
console.log("- no additional notes");
|
|
610
|
+
} else {
|
|
611
|
+
for (const item of report.summary) {
|
|
612
|
+
console.log(`- ${item}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
main().catch((error) => {
|
|
618
|
+
console.error(`sync-status FAIL: ${error.message}`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
});
|