create-agentic-pdlc 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
- package/.claude/settings.json +18 -0
- package/.coderabbit.yaml +35 -0
- package/.github/workflows/project-automation.yml +13 -67
- package/CLAUDE.md +1 -1
- package/README.md +33 -32
- package/adapters/claude-code/skill.md +7 -3
- package/bin/cli.js +549 -209
- package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
- package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
- package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
- package/package.json +2 -2
- package/templates/full/CLAUDE.md +30 -0
- package/templates/lite/AGENTS.md +121 -0
- package/templates/lite/CLAUDE.md +44 -0
- package/tests/cli.test.js +32 -0
- package/.github/workflows/agentic-metrics.yml +0 -545
- package/.github/workflows/qa-agent.yml +0 -139
- package/.github/workflows/qa-gate.yml +0 -51
- /package/templates/{AGENTS.md → full/AGENTS.md} +0 -0
- /package/templates/{docs → full/docs}/pdlc.md +0 -0
|
@@ -1,545 +0,0 @@
|
|
|
1
|
-
name: Agentic Metrics — Weekly Pulse
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
schedule:
|
|
5
|
-
- cron: '0 8 * * 0' # Sunday 8am UTC
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: write
|
|
10
|
-
issues: write
|
|
11
|
-
|
|
12
|
-
env:
|
|
13
|
-
AGENTIC_PULSE_REVIEWERS: |
|
|
14
|
-
code_reviewer=gemini-code-assist[bot]
|
|
15
|
-
qa_agent=github-actions[bot]
|
|
16
|
-
|
|
17
|
-
jobs:
|
|
18
|
-
generate-pulse:
|
|
19
|
-
name: Generate Weekly Agentic Pulse
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v5.0.1
|
|
23
|
-
|
|
24
|
-
- name: Collect Stage Residence Time
|
|
25
|
-
uses: actions/github-script@v8
|
|
26
|
-
with:
|
|
27
|
-
script: |
|
|
28
|
-
const fs = require('fs');
|
|
29
|
-
|
|
30
|
-
const { owner, repo } = context.repo;
|
|
31
|
-
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
32
|
-
const STAGE_LABELS = new Set([
|
|
33
|
-
'stage:brainstorming', 'stage:detailing',
|
|
34
|
-
'stage:approval', 'stage:development', 'stage:testing'
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const QUERY = `
|
|
38
|
-
query($owner: String!, $repo: String!, $since: DateTime!, $cursor: String) {
|
|
39
|
-
repository(owner: $owner, name: $repo) {
|
|
40
|
-
issues(first: 50, after: $cursor, filterBy: { since: $since }) {
|
|
41
|
-
pageInfo { hasNextPage endCursor }
|
|
42
|
-
nodes {
|
|
43
|
-
number
|
|
44
|
-
timelineItems(first: 100, itemTypes: [LABELED_EVENT, UNLABELED_EVENT]) {
|
|
45
|
-
nodes {
|
|
46
|
-
... on LabeledEvent { __typename createdAt label { name } }
|
|
47
|
-
... on UnlabeledEvent { __typename createdAt label { name } }
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
`;
|
|
55
|
-
|
|
56
|
-
const records = [];
|
|
57
|
-
let cursor = null;
|
|
58
|
-
do {
|
|
59
|
-
const result = await github.graphql(QUERY, { owner, repo, since, cursor });
|
|
60
|
-
const { nodes, pageInfo } = result.repository.issues;
|
|
61
|
-
|
|
62
|
-
for (const issue of nodes) {
|
|
63
|
-
const events = issue.timelineItems.nodes
|
|
64
|
-
.filter(e => e.label && STAGE_LABELS.has(e.label.name))
|
|
65
|
-
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
|
66
|
-
|
|
67
|
-
const labeledAt = {};
|
|
68
|
-
for (const ev of events) {
|
|
69
|
-
const stage = ev.label.name;
|
|
70
|
-
if (ev.__typename === 'LabeledEvent') {
|
|
71
|
-
labeledAt[stage] = new Date(ev.createdAt);
|
|
72
|
-
} else if (ev.__typename === 'UnlabeledEvent' && labeledAt[stage]) {
|
|
73
|
-
const durationDays = Math.round((new Date(ev.createdAt) - labeledAt[stage]) / 864e5 * 10) / 10;
|
|
74
|
-
records.push({ issueNumber: issue.number, stage, durationDays });
|
|
75
|
-
delete labeledAt[stage];
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
|
|
81
|
-
} while (cursor);
|
|
82
|
-
|
|
83
|
-
function isoWeek(d) {
|
|
84
|
-
const dt = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
85
|
-
dt.setUTCDate(dt.getUTCDate() + 4 - (dt.getUTCDay() || 7));
|
|
86
|
-
const y = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
|
|
87
|
-
return { year: dt.getUTCFullYear(), week: Math.ceil((((dt - y) / 86400000) + 1) / 7) };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const { year, week } = isoWeek(new Date());
|
|
91
|
-
const weekKey = `${year}-W${String(week).padStart(2, '0')}`;
|
|
92
|
-
|
|
93
|
-
fs.mkdirSync('.agentic-pdlc/metrics/raw', { recursive: true });
|
|
94
|
-
fs.writeFileSync(
|
|
95
|
-
`.agentic-pdlc/metrics/raw/${weekKey}.jsonl`,
|
|
96
|
-
records.map(r => JSON.stringify(r)).join('\n')
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
core.exportVariable('WEEK_KEY', weekKey);
|
|
100
|
-
console.log(`Collected ${records.length} stage transitions → week ${weekKey}`);
|
|
101
|
-
|
|
102
|
-
- name: Commit JSONL raw data
|
|
103
|
-
run: |
|
|
104
|
-
git config user.name "github-actions[bot]"
|
|
105
|
-
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
106
|
-
git add .agentic-pdlc/metrics/raw/
|
|
107
|
-
git diff --staged --quiet && echo "No new stage data" && exit 0
|
|
108
|
-
git commit -m "metrics: raw stage data ${WEEK_KEY} [skip ci]"
|
|
109
|
-
git push
|
|
110
|
-
|
|
111
|
-
- name: Collect PR and Issue Insights
|
|
112
|
-
uses: actions/github-script@v8
|
|
113
|
-
with:
|
|
114
|
-
script: |
|
|
115
|
-
const fs = require('fs');
|
|
116
|
-
const { owner, repo } = context.repo;
|
|
117
|
-
const weekKey = process.env.WEEK_KEY;
|
|
118
|
-
|
|
119
|
-
// ── Preload stage:detailing times for stage correlation ──────────
|
|
120
|
-
const detailingByIssue = {};
|
|
121
|
-
const jsonlPath = `.agentic-pdlc/metrics/raw/${weekKey}.jsonl`;
|
|
122
|
-
if (fs.existsSync(jsonlPath)) {
|
|
123
|
-
const rawLines = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
124
|
-
for (const line of rawLines) {
|
|
125
|
-
const r = JSON.parse(line);
|
|
126
|
-
if (r.stage === 'stage:detailing') {
|
|
127
|
-
detailingByIssue[r.issueNumber] = round1((detailingByIssue[r.issueNumber] || 0) + r.durationDays);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ── Helper ──────────────────────────────────────────────────────
|
|
133
|
-
function daysSince(isoStr) {
|
|
134
|
-
return (Date.now() - new Date(isoStr).getTime()) / 864e5;
|
|
135
|
-
}
|
|
136
|
-
function round1(n) { return Math.round(n * 10) / 10; }
|
|
137
|
-
|
|
138
|
-
// Week date range for issue title (ISO week, Mon–Sun)
|
|
139
|
-
function weekDateRange(key) {
|
|
140
|
-
const [yr, wStr] = key.split('-W');
|
|
141
|
-
const week = parseInt(wStr, 10);
|
|
142
|
-
// Jan 4 is always in week 1
|
|
143
|
-
const jan4 = new Date(Date.UTC(parseInt(yr, 10), 0, 4));
|
|
144
|
-
const monday = new Date(jan4);
|
|
145
|
-
monday.setUTCDate(jan4.getUTCDate() - ((jan4.getUTCDay() + 6) % 7) + (week - 1) * 7);
|
|
146
|
-
const sunday = new Date(monday);
|
|
147
|
-
sunday.setUTCDate(monday.getUTCDate() + 6);
|
|
148
|
-
const months = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
|
|
149
|
-
const pad = n => String(n).padStart(2, '0');
|
|
150
|
-
const startMonth = months[monday.getUTCMonth()];
|
|
151
|
-
const endMonth = months[sunday.getUTCMonth()];
|
|
152
|
-
const monthLabel = startMonth === endMonth ? startMonth : startMonth + '/' + endMonth;
|
|
153
|
-
return pad(monday.getUTCDate()) + '-' + pad(sunday.getUTCDate()) + '/' + monthLabel;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── Signal collection ───────────────────────────────────────────
|
|
157
|
-
const signals = [];
|
|
158
|
-
|
|
159
|
-
// ── Review actor map (from AGENTIC_PULSE_REVIEWERS env var) ─────
|
|
160
|
-
const actorMap = {}; // login → role
|
|
161
|
-
const reviewersEnv = (process.env.AGENTIC_PULSE_REVIEWERS || '').trim();
|
|
162
|
-
if (reviewersEnv) {
|
|
163
|
-
for (const line of reviewersEnv.split('\n')) {
|
|
164
|
-
const eq = line.indexOf('=');
|
|
165
|
-
if (eq < 0) continue;
|
|
166
|
-
const role = line.slice(0, eq).trim();
|
|
167
|
-
const logins = line.slice(eq + 1).trim();
|
|
168
|
-
for (const login of logins.split(',').map(l => l.trim()).filter(Boolean)) {
|
|
169
|
-
actorMap[login] = role;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const taxonomyEnabled = Object.keys(actorMap).length > 0;
|
|
174
|
-
|
|
175
|
-
// 1. Orphan issues: open >14 days with no linked PR
|
|
176
|
-
const closeRe = /(?:closes?|fixes?|resolves?)\s+#(\d+)/gi;
|
|
177
|
-
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
|
|
178
|
-
owner, repo, state: 'open', per_page: 100
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const recentPRs = await github.paginate(github.rest.pulls.list, {
|
|
182
|
-
owner, repo, state: 'closed', per_page: 50,
|
|
183
|
-
sort: 'updated', direction: 'desc'
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const linkedIssueNums = new Set();
|
|
187
|
-
for (const pr of recentPRs) {
|
|
188
|
-
const body = pr.body || '';
|
|
189
|
-
let m;
|
|
190
|
-
while ((m = closeRe.exec(body)) !== null) linkedIssueNums.add(parseInt(m[1]));
|
|
191
|
-
closeRe.lastIndex = 0;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const orphans = openIssues.filter(i =>
|
|
195
|
-
!i.pull_request &&
|
|
196
|
-
daysSince(i.created_at) > 14 &&
|
|
197
|
-
!linkedIssueNums.has(i.number)
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
if (orphans.length > 0) {
|
|
201
|
-
const names = orphans.slice(0, 5).map(i => `#${i.number} "${i.title}"`).join(', ');
|
|
202
|
-
const extra = orphans.length > 5 ? ` e mais ${orphans.length - 5}` : '';
|
|
203
|
-
signals.push({
|
|
204
|
-
level: 'red',
|
|
205
|
-
emoji: '🔴',
|
|
206
|
-
title: `**${orphans.length} issue${orphans.length > 1 ? 's abertas' : ' aberta'} há 14+ dias sem PR linkado**`,
|
|
207
|
-
body: `${names}${extra}\n→ Atribua, planeje ou feche se não for mais relevante.`
|
|
208
|
-
});
|
|
209
|
-
} else {
|
|
210
|
-
signals.push({
|
|
211
|
-
level: 'green',
|
|
212
|
-
emoji: '🟢',
|
|
213
|
-
title: '**Nenhuma issue esquecida**',
|
|
214
|
-
body: 'Todas as issues abertas têm menos de 14 dias ou têm PR linkado.'
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// 2. PR merge time trend: last 10 merged PRs split 5+5
|
|
219
|
-
const mergedPRs = recentPRs
|
|
220
|
-
.filter(pr => pr.merged_at)
|
|
221
|
-
.slice(0, 10);
|
|
222
|
-
|
|
223
|
-
if (mergedPRs.length >= 4) {
|
|
224
|
-
const half = Math.floor(mergedPRs.length / 2);
|
|
225
|
-
const recent = mergedPRs.slice(0, half);
|
|
226
|
-
const older = mergedPRs.slice(half);
|
|
227
|
-
const avgDays = prs => {
|
|
228
|
-
const total = prs.reduce((s, pr) =>
|
|
229
|
-
s + (new Date(pr.merged_at) - new Date(pr.created_at)) / 864e5, 0);
|
|
230
|
-
return round1(total / prs.length);
|
|
231
|
-
};
|
|
232
|
-
const recentAvg = avgDays(recent);
|
|
233
|
-
const olderAvg = avgDays(older);
|
|
234
|
-
const ratio = recentAvg / olderAvg;
|
|
235
|
-
|
|
236
|
-
if (ratio >= 1.5) {
|
|
237
|
-
signals.push({
|
|
238
|
-
level: 'yellow',
|
|
239
|
-
emoji: '🟡',
|
|
240
|
-
title: `**Tempo de merge aumentou ${round1(ratio)}x esta semana**`,
|
|
241
|
-
body: `Últimos ${half} PRs: **${recentAvg} dias** · ${half} anteriores: **${olderAvg} dias**\n→ Algo desacelerou o review. Verifique PRs pendentes de aprovação.`
|
|
242
|
-
});
|
|
243
|
-
} else if (ratio <= 0.67) {
|
|
244
|
-
signals.push({
|
|
245
|
-
level: 'green',
|
|
246
|
-
emoji: '🟢',
|
|
247
|
-
title: `**Tempo de merge melhorou ${round1(1/ratio)}x esta semana**`,
|
|
248
|
-
body: `Últimos ${half} PRs: **${recentAvg} dias** · ${half} anteriores: **${olderAvg} dias** ✅`
|
|
249
|
-
});
|
|
250
|
-
} else {
|
|
251
|
-
signals.push({
|
|
252
|
-
level: 'neutral',
|
|
253
|
-
emoji: '🔵',
|
|
254
|
-
title: `**Tempo de merge estável: ${recentAvg} dias** (semana passada: ${olderAvg}d)`,
|
|
255
|
-
body: ''
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 3. Rework rate with actor taxonomy (if AGENTIC_PULSE_REVIEWERS configured)
|
|
261
|
-
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
262
|
-
const weekMerged = recentPRs.filter(pr => pr.merged_at && new Date(pr.merged_at) > weekAgo);
|
|
263
|
-
|
|
264
|
-
if (weekMerged.length > 0) {
|
|
265
|
-
let firstShots = 0;
|
|
266
|
-
const reworkByRole = {};
|
|
267
|
-
const reworkDetails = []; // { pr_number, issue_number } for stage correlation
|
|
268
|
-
const issueRe = /(?:closes?|fixes?|resolves?)\s+#(\d+)/i;
|
|
269
|
-
|
|
270
|
-
for (const pr of weekMerged.slice(0, 10)) {
|
|
271
|
-
try {
|
|
272
|
-
const commits = await github.rest.pulls.listCommits({
|
|
273
|
-
owner, repo, pull_number: pr.number, per_page: 100
|
|
274
|
-
});
|
|
275
|
-
const times = commits.data
|
|
276
|
-
.map(c => new Date(c.commit.committer.date).getTime())
|
|
277
|
-
.sort((a, b) => a - b);
|
|
278
|
-
|
|
279
|
-
let sessions = 1;
|
|
280
|
-
for (let i = 1; i < times.length; i++) {
|
|
281
|
-
if (times[i] - times[i-1] > 10 * 60 * 1000) sessions++;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (sessions === 1) { firstShots++; continue; }
|
|
285
|
-
|
|
286
|
-
if (!taxonomyEnabled) continue;
|
|
287
|
-
|
|
288
|
-
let reviewTriggered = false;
|
|
289
|
-
try {
|
|
290
|
-
const reviews = await github.rest.pulls.listReviews({
|
|
291
|
-
owner, repo, pull_number: pr.number, per_page: 100
|
|
292
|
-
});
|
|
293
|
-
const attributedRoles = new Set();
|
|
294
|
-
for (const review of reviews.data) {
|
|
295
|
-
if (!review.user) continue;
|
|
296
|
-
const role = actorMap[review.user.login];
|
|
297
|
-
if (!role || review.state === 'APPROVED' || attributedRoles.has(role)) continue;
|
|
298
|
-
const reviewTime = new Date(review.submitted_at).getTime();
|
|
299
|
-
if (times.some(t => t > reviewTime)) {
|
|
300
|
-
reworkByRole[role] = (reworkByRole[role] || 0) + 1;
|
|
301
|
-
reviewTriggered = true;
|
|
302
|
-
attributedRoles.add(role);
|
|
303
|
-
if (!reworkDetails.some(d => d.pr_number === pr.number)) {
|
|
304
|
-
const m = issueRe.exec(pr.body || '');
|
|
305
|
-
if (m) reworkDetails.push({ pr_number: pr.number, issue_number: parseInt(m[1]) });
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} catch(e) { /* reviews not accessible — skip taxonomy for this PR */ }
|
|
310
|
-
|
|
311
|
-
if (!reviewTriggered) {
|
|
312
|
-
reworkByRole.self_correction = (reworkByRole.self_correction || 0) + 1;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
} catch (e) { /* skip if commits not accessible */ }
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const total = Math.min(weekMerged.length, 10);
|
|
319
|
-
const pct = Math.round(firstShots / total * 100);
|
|
320
|
-
const reworkCount = total - firstShots;
|
|
321
|
-
|
|
322
|
-
const agentLabels = new Set(['jules', 'sweep', 'codex', 'copilot']);
|
|
323
|
-
const usesAgent = weekMerged.some(pr =>
|
|
324
|
-
(pr.labels || []).some(l => agentLabels.has(l.name.toLowerCase()))
|
|
325
|
-
);
|
|
326
|
-
const subject = usesAgent ? 'Agent first-shot rate' : 'PRs sem rework';
|
|
327
|
-
const verb = usesAgent ? 'acertaram de primeira' : 'foram mergeados sem rework';
|
|
328
|
-
|
|
329
|
-
if (taxonomyEnabled && reworkCount > 0) {
|
|
330
|
-
const lines = [];
|
|
331
|
-
for (const [role, count] of Object.entries(reworkByRole).sort((a, b) => b[1] - a[1])) {
|
|
332
|
-
const s = count > 1 ? 's' : '';
|
|
333
|
-
if (role === 'code_reviewer') lines.push(` ↳ Code reviewer: **${count} PR${s}** → revisar DoD em stage:development`);
|
|
334
|
-
else if (role === 'qa_agent') lines.push(` ↳ QA Agent: **${count} PR${s}** → spec com lacunas funcionais em stage:detailing`);
|
|
335
|
-
else if (role === 'self_correction') lines.push(` ↳ Self-correction: **${count} PR${s}** (causa não determinada automaticamente)`);
|
|
336
|
-
else lines.push(` ↳ ${role}: **${count} PR${s}**`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const reviewerRework = reworkByRole.code_reviewer || 0;
|
|
340
|
-
const level = reviewerRework >= Math.ceil(reworkCount * 0.8) ? 'red'
|
|
341
|
-
: (reviewerRework >= Math.ceil(reworkCount * 0.5) || (reworkByRole.qa_agent || 0) > 0) ? 'yellow'
|
|
342
|
-
: 'neutral';
|
|
343
|
-
const emoji = level === 'red' ? '🔴' : level === 'yellow' ? '🟡' : '🔵';
|
|
344
|
-
|
|
345
|
-
signals.push({
|
|
346
|
-
level,
|
|
347
|
-
emoji,
|
|
348
|
-
title: `**Rework: ${100 - pct}%** — ${reworkCount} de ${total} PRs tiveram commits extras`,
|
|
349
|
-
body: lines.join('\n')
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
// ── Stage correlation ────────────────────────────────────────
|
|
353
|
-
if (reworkDetails.length > 0 && Object.keys(detailingByIssue).length > 0) {
|
|
354
|
-
const reworkIssueNums = new Set(reworkDetails.map(d => d.issue_number));
|
|
355
|
-
|
|
356
|
-
const reworkGroup = reworkDetails
|
|
357
|
-
.map(d => detailingByIssue[d.issue_number])
|
|
358
|
-
.filter(t => t !== undefined);
|
|
359
|
-
|
|
360
|
-
const cleanGroup = weekMerged.slice(0, 10)
|
|
361
|
-
.map(pr => { const m = issueRe.exec(pr.body || ''); return m ? parseInt(m[1]) : null; })
|
|
362
|
-
.filter(n => n !== null && !reworkIssueNums.has(n))
|
|
363
|
-
.map(n => detailingByIssue[n])
|
|
364
|
-
.filter(t => t !== undefined);
|
|
365
|
-
|
|
366
|
-
if (reworkGroup.length >= 3 && cleanGroup.length >= 3) {
|
|
367
|
-
const avgRework = round1(reworkGroup.reduce((a, b) => a + b, 0) / reworkGroup.length);
|
|
368
|
-
const avgClean = round1(cleanGroup.reduce((a, b) => a + b, 0) / cleanGroup.length);
|
|
369
|
-
if (avgRework < avgClean * 0.75) {
|
|
370
|
-
signals.push({
|
|
371
|
-
level: 'neutral',
|
|
372
|
-
emoji: '💡',
|
|
373
|
-
title: `**Stage correlation:** PRs com reviewer rework tiveram Detailing médio de ${avgRework}d vs ${avgClean}d (N=${reworkGroup.length} vs ${cleanGroup.length})`,
|
|
374
|
-
body: '→ Specs rápidas correlacionam com mais rework de review'
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
} else {
|
|
381
|
-
// Taxonomy disabled or no rework — existing signal unchanged
|
|
382
|
-
if (pct >= 80) {
|
|
383
|
-
signals.push({
|
|
384
|
-
level: 'green', emoji: '🟢',
|
|
385
|
-
title: `**${subject}: ${pct}%**`,
|
|
386
|
-
body: `${firstShots} de ${total} PRs ${verb} esta semana. ✅`
|
|
387
|
-
});
|
|
388
|
-
} else if (pct < 50) {
|
|
389
|
-
signals.push({
|
|
390
|
-
level: 'yellow', emoji: '🟡',
|
|
391
|
-
title: `**${subject}: ${pct}% — rework alto**`,
|
|
392
|
-
body: `Apenas ${firstShots} de ${total} PRs sem commits extras.\n→ Specs incompletas ou mudanças de requisito durante implementação.`
|
|
393
|
-
});
|
|
394
|
-
} else {
|
|
395
|
-
signals.push({
|
|
396
|
-
level: 'neutral', emoji: '🔵',
|
|
397
|
-
title: `**${subject}: ${pct}%**`,
|
|
398
|
-
body: `${firstShots} de ${total} PRs sem rework commits.`
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 4. Unlinked PRs: merged without Closes/Fixes #N
|
|
405
|
-
const unlinkRe = /(?:closes?|fixes?|resolves?)\s+#\d+/i;
|
|
406
|
-
const unlinked = weekMerged.filter(pr => !unlinkRe.test(pr.body || ''));
|
|
407
|
-
if (unlinked.length > 0) {
|
|
408
|
-
const nums = unlinked.map(pr => `#${pr.number}`).join(', ');
|
|
409
|
-
signals.push({
|
|
410
|
-
level: 'yellow',
|
|
411
|
-
emoji: '🔵',
|
|
412
|
-
title: `**${unlinked.length} PR${unlinked.length > 1 ? 's mergeados' : ' mergeado'} sem link de issue**`,
|
|
413
|
-
body: `${nums} não ${unlinked.length > 1 ? 'têm' : 'tem'} \`Closes #N\` no body. Difícil rastrear o que resolveram.\n→ Edite o body do PR ou feche as issues relacionadas manualmente.`
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ── Stage Residence Time section (conditional) ──────────────────
|
|
418
|
-
let stageSection = '';
|
|
419
|
-
if (fs.existsSync(jsonlPath)) {
|
|
420
|
-
const records = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
|
|
421
|
-
if (records.length > 0) {
|
|
422
|
-
const STAGES = [
|
|
423
|
-
['stage:brainstorming', 'Brainstorming'],
|
|
424
|
-
['stage:detailing', 'Detailing'],
|
|
425
|
-
['stage:approval', 'Approval'],
|
|
426
|
-
['stage:development', 'Development'],
|
|
427
|
-
['stage:testing', 'Testing'],
|
|
428
|
-
];
|
|
429
|
-
const byStage = {};
|
|
430
|
-
for (const r of records) {
|
|
431
|
-
if (!byStage[r.stage]) byStage[r.stage] = [];
|
|
432
|
-
byStage[r.stage].push(r.durationDays);
|
|
433
|
-
}
|
|
434
|
-
const avg = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
435
|
-
|
|
436
|
-
let maxStage = null, maxDays = -1;
|
|
437
|
-
const rows = [];
|
|
438
|
-
for (const [stage, label] of STAGES) {
|
|
439
|
-
const days = byStage[stage];
|
|
440
|
-
if (!days) {
|
|
441
|
-
rows.push(`| **${label}** | — | — |`);
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
const a = round1(avg(days));
|
|
445
|
-
rows.push(`| **${label}** | ${a}d | ${days.length} |`);
|
|
446
|
-
if (a > maxDays) { maxStage = label; maxDays = a; }
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (rows.length > 0) {
|
|
450
|
-
stageSection = `\n---\n\n### 🔄 Stage Residence Time *(board ativo)*\n\n| Stage | Média | Issues |\n|---|---|---|\n${rows.join('\n')}\n\n> \`${maxStage}\` é o maior gargalo (${maxDays}d avg). Considere quebrar specs ou aumentar cadência de revisão nessa fase.\n`;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// ── Narrative ───────────────────────────────────────────────────
|
|
456
|
-
const reds = signals.filter(s => s.level === 'red').length;
|
|
457
|
-
const yellows = signals.filter(s => s.level === 'yellow').length;
|
|
458
|
-
|
|
459
|
-
let narrative;
|
|
460
|
-
if (reds >= 2) narrative = '⚠️ Fluxo sob pressão esta semana. Múltiplos bloqueios identificados. Priorize limpar issues orphans e revisar PRs pendentes antes de iniciar novas features.';
|
|
461
|
-
else if (reds === 1 && yellows >= 1) narrative = 'Semana mista. Um ponto crítico e sinais de desaceleração. Verifique os itens marcados em 🔴 antes de avançar.';
|
|
462
|
-
else if (reds === 0 && yellows === 0) narrative = '🟢 Fluxo saudável. Nenhum bloqueio identificado esta semana. Bom momento para avançar em novas features.';
|
|
463
|
-
else narrative = 'Fluxo normal. Alguns ajustes de processo podem melhorar rastreabilidade e velocidade.';
|
|
464
|
-
|
|
465
|
-
// ── Build issue body ────────────────────────────────────────────
|
|
466
|
-
const signalLines = signals.map(s =>
|
|
467
|
-
`${s.emoji} ${s.title}${s.body ? '\n' + s.body : ''}`
|
|
468
|
-
).join('\n\n');
|
|
469
|
-
|
|
470
|
-
const nextWeekNum = (() => {
|
|
471
|
-
const d = new Date(); d.setDate(d.getDate() + 7);
|
|
472
|
-
const dt = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
473
|
-
dt.setUTCDate(dt.getUTCDate() + 4 - (dt.getUTCDay() || 7));
|
|
474
|
-
const y = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
|
|
475
|
-
const w = Math.ceil((((dt - y) / 86400000) + 1) / 7);
|
|
476
|
-
return `${dt.getUTCFullYear()}-W${String(w).padStart(2, '0')}`;
|
|
477
|
-
})();
|
|
478
|
-
|
|
479
|
-
const body = [
|
|
480
|
-
'> Auto-generated by [agentic-pdlc](https://github.com/rafaeltcosta86/agentic-pdlc). Appears every Sunday.',
|
|
481
|
-
'',
|
|
482
|
-
narrative,
|
|
483
|
-
'',
|
|
484
|
-
'---',
|
|
485
|
-
'',
|
|
486
|
-
'### Sinais desta semana',
|
|
487
|
-
'',
|
|
488
|
-
signalLines,
|
|
489
|
-
stageSection,
|
|
490
|
-
'---',
|
|
491
|
-
'*Próximo pulse: ' + nextWeekNum + '*',
|
|
492
|
-
'',
|
|
493
|
-
'💬 Este pulse foi útil? [Conta pra gente](https://github.com/rafaeltcosta86/agentic-pdlc/issues/new?template=pulse-feedback.md&title=' + encodeURIComponent('[Pulse] ' + weekKey) + ') — 1 clique, sem formulário.'
|
|
494
|
-
].join('\n');
|
|
495
|
-
|
|
496
|
-
const dateRange = weekDateRange(weekKey);
|
|
497
|
-
core.exportVariable('PULSE_TITLE', '📊 Agentic Pulse — ' + weekKey + ' (' + dateRange + ')');
|
|
498
|
-
core.exportVariable('PULSE_BODY', body);
|
|
499
|
-
console.log(`Built pulse issue for ${weekKey} — ${signals.length} signals, ${reds} red, ${yellows} yellow`);
|
|
500
|
-
|
|
501
|
-
- name: Create Weekly Pulse Issue
|
|
502
|
-
uses: actions/github-script@v8
|
|
503
|
-
with:
|
|
504
|
-
script: |
|
|
505
|
-
const { owner, repo } = context.repo;
|
|
506
|
-
const title = process.env.PULSE_TITLE;
|
|
507
|
-
const body = process.env.PULSE_BODY;
|
|
508
|
-
const LABEL = 'metrics:weekly';
|
|
509
|
-
|
|
510
|
-
// Ensure label exists
|
|
511
|
-
try {
|
|
512
|
-
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
|
|
513
|
-
} catch (e) {
|
|
514
|
-
await github.rest.issues.createLabel({
|
|
515
|
-
owner, repo, name: LABEL,
|
|
516
|
-
color: '0075ca',
|
|
517
|
-
description: 'Auto-generated weekly metrics pulse'
|
|
518
|
-
});
|
|
519
|
-
console.log(`Created label ${LABEL}`);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Close previous pulse issues; upsert if same week already exists
|
|
523
|
-
const prev = await github.rest.issues.listForRepo({
|
|
524
|
-
owner, repo, labels: LABEL, state: 'open', per_page: 20
|
|
525
|
-
});
|
|
526
|
-
let existingIssue = null;
|
|
527
|
-
for (const issue of prev.data) {
|
|
528
|
-
if (issue.title === title) {
|
|
529
|
-
existingIssue = issue;
|
|
530
|
-
console.log(`Found existing pulse for ${title}: #${issue.number} — will update body`);
|
|
531
|
-
} else {
|
|
532
|
-
await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed' });
|
|
533
|
-
console.log(`Closed previous pulse: #${issue.number}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (existingIssue) {
|
|
538
|
-
await github.rest.issues.update({ owner, repo, issue_number: existingIssue.number, body });
|
|
539
|
-
console.log(`✅ Updated pulse issue: #${existingIssue.number} — ${title}`);
|
|
540
|
-
} else {
|
|
541
|
-
const created = await github.rest.issues.create({
|
|
542
|
-
owner, repo, title, body, labels: [LABEL]
|
|
543
|
-
});
|
|
544
|
-
console.log(`✅ Created pulse issue: #${created.data.number} — ${title}`);
|
|
545
|
-
}
|