create-agentic-pdlc 2.3.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/.agentic-pdlc/metrics/raw/2026-W22.jsonl +114 -0
- package/.claude/settings.json +18 -0
- package/.coderabbit.yaml +35 -0
- package/.github/ISSUE_TEMPLATE/bug.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature.md +54 -0
- package/.github/ISSUE_TEMPLATE/task.md +33 -0
- package/.github/workflows/add-to-board.yml +1 -1
- package/.github/workflows/agent-trigger.yml +4 -4
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/npm-publish.yml +2 -2
- package/.github/workflows/pdlc-health-check.yml +1 -1
- package/.github/workflows/pdlc-stage-gate.yml +2 -2
- package/.github/workflows/project-automation.yml +25 -40
- package/AGENTS.md +50 -8
- package/CLAUDE.md +3 -1
- package/README.md +33 -32
- package/SETUP.md +2 -1
- package/adapters/claude-code/skill.md +39 -14
- package/adapters/hooks/pdlc-stage-gate.sh +3 -8
- package/bin/cli.js +555 -194
- package/docs/pdlc.md +5 -5
- package/docs/superpowers/plans/2026-05-28-jules-label-pat-split.md +240 -0
- package/docs/superpowers/plans/2026-05-29-agentic-pulse-rework-taxonomy.md +474 -0
- package/docs/superpowers/plans/2026-05-29-qa-gate-enforcement.md +354 -0
- 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-05-29-agentic-pulse-rework-taxonomy-design.md +122 -0
- package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
- package/package.json +2 -2
- package/templates/.github/ISSUE_TEMPLATE/bug.md +53 -0
- package/templates/.github/ISSUE_TEMPLATE/feature.md +54 -0
- package/templates/.github/ISSUE_TEMPLATE/task.md +33 -0
- package/templates/.github/workflows/add-to-board.yml +4 -4
- package/templates/.github/workflows/agent-trigger.yml +22 -13
- package/{.agentic-pdlc/templates → templates}/.github/workflows/agentic-metrics.yml +150 -27
- package/templates/.github/workflows/ci.yml +1 -1
- package/templates/.github/workflows/pdlc-health-check.yml +1 -1
- package/templates/.github/workflows/pdlc-stage-gate.yml +2 -2
- package/templates/.github/workflows/project-automation.yml +71 -32
- package/templates/.github/workflows/qa-agent.yml +32 -18
- package/templates/.github/workflows/qa-gate.yml +51 -0
- package/templates/full/AGENTS.md +143 -0
- package/templates/full/CLAUDE.md +30 -0
- package/templates/{docs → full/docs}/pdlc.md +4 -4
- package/templates/lite/AGENTS.md +121 -0
- package/templates/lite/CLAUDE.md +44 -0
- package/tests/cli.test.js +32 -0
- package/.agentic-pdlc/templates/.github/CODEOWNERS +0 -5
- package/.agentic-pdlc/templates/.github/copilot-instructions.md +0 -12
- package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +0 -38
- package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +0 -146
- package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +0 -16
- package/.agentic-pdlc/templates/.github/workflows/ci.yml +0 -54
- package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +0 -121
- package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +0 -51
- package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +0 -274
- package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +0 -21
- package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +0 -128
- package/.agentic-pdlc/templates/AGENTS.md +0 -104
- package/.agentic-pdlc/templates/docs/pdlc.md +0 -123
- package/.github/workflows/agentic-metrics.yml +0 -422
- package/.github/workflows/qa-agent.yml +0 -128
- package/templates/AGENTS.md +0 -115
|
@@ -9,15 +9,20 @@ permissions:
|
|
|
9
9
|
contents: write
|
|
10
10
|
issues: write
|
|
11
11
|
|
|
12
|
+
env:
|
|
13
|
+
AGENTIC_PULSE_REVIEWERS: |
|
|
14
|
+
code_reviewer=gemini-code-assist[bot]
|
|
15
|
+
qa_agent=github-actions[bot]
|
|
16
|
+
|
|
12
17
|
jobs:
|
|
13
18
|
generate-pulse:
|
|
14
19
|
name: Generate Weekly Agentic Pulse
|
|
15
20
|
runs-on: ubuntu-latest
|
|
16
21
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v5.0.1
|
|
18
23
|
|
|
19
24
|
- name: Collect Stage Residence Time
|
|
20
|
-
uses: actions/github-script@
|
|
25
|
+
uses: actions/github-script@v8
|
|
21
26
|
with:
|
|
22
27
|
script: |
|
|
23
28
|
const fs = require('fs');
|
|
@@ -104,13 +109,26 @@ jobs:
|
|
|
104
109
|
git push
|
|
105
110
|
|
|
106
111
|
- name: Collect PR and Issue Insights
|
|
107
|
-
uses: actions/github-script@
|
|
112
|
+
uses: actions/github-script@v8
|
|
108
113
|
with:
|
|
109
114
|
script: |
|
|
110
115
|
const fs = require('fs');
|
|
111
116
|
const { owner, repo } = context.repo;
|
|
112
117
|
const weekKey = process.env.WEEK_KEY;
|
|
113
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
|
+
|
|
114
132
|
// ── Helper ──────────────────────────────────────────────────────
|
|
115
133
|
function daysSince(isoStr) {
|
|
116
134
|
return (Date.now() - new Date(isoStr).getTime()) / 864e5;
|
|
@@ -138,6 +156,22 @@ jobs:
|
|
|
138
156
|
// ── Signal collection ───────────────────────────────────────────
|
|
139
157
|
const signals = [];
|
|
140
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
|
+
|
|
141
175
|
// 1. Orphan issues: open >14 days with no linked PR
|
|
142
176
|
const closeRe = /(?:closes?|fixes?|resolves?)\s+#(\d+)/gi;
|
|
143
177
|
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
|
|
@@ -223,29 +257,68 @@ jobs:
|
|
|
223
257
|
}
|
|
224
258
|
}
|
|
225
259
|
|
|
226
|
-
// 3. Rework rate
|
|
260
|
+
// 3. Rework rate with actor taxonomy (if AGENTIC_PULSE_REVIEWERS configured)
|
|
227
261
|
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
228
262
|
const weekMerged = recentPRs.filter(pr => pr.merged_at && new Date(pr.merged_at) > weekAgo);
|
|
229
263
|
|
|
230
264
|
if (weekMerged.length > 0) {
|
|
231
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
|
+
|
|
232
270
|
for (const pr of weekMerged.slice(0, 10)) {
|
|
233
271
|
try {
|
|
234
272
|
const commits = await github.rest.pulls.listCommits({
|
|
235
273
|
owner, repo, pull_number: pr.number, per_page: 100
|
|
236
274
|
});
|
|
237
|
-
const times = commits.data
|
|
275
|
+
const times = commits.data
|
|
276
|
+
.map(c => new Date(c.commit.committer.date).getTime())
|
|
277
|
+
.sort((a, b) => a - b);
|
|
278
|
+
|
|
238
279
|
let sessions = 1;
|
|
239
280
|
for (let i = 1; i < times.length; i++) {
|
|
240
281
|
if (times[i] - times[i-1] > 10 * 60 * 1000) sessions++;
|
|
241
282
|
}
|
|
242
|
-
|
|
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
|
+
|
|
243
315
|
} catch (e) { /* skip if commits not accessible */ }
|
|
244
316
|
}
|
|
317
|
+
|
|
245
318
|
const total = Math.min(weekMerged.length, 10);
|
|
246
319
|
const pct = Math.round(firstShots / total * 100);
|
|
320
|
+
const reworkCount = total - firstShots;
|
|
247
321
|
|
|
248
|
-
// Detect if repo uses an agent label (jules, sweep, codex, etc.)
|
|
249
322
|
const agentLabels = new Set(['jules', 'sweep', 'codex', 'copilot']);
|
|
250
323
|
const usesAgent = weekMerged.some(pr =>
|
|
251
324
|
(pr.labels || []).some(l => agentLabels.has(l.name.toLowerCase()))
|
|
@@ -253,27 +326,78 @@ jobs:
|
|
|
253
326
|
const subject = usesAgent ? 'Agent first-shot rate' : 'PRs sem rework';
|
|
254
327
|
const verb = usesAgent ? 'acertaram de primeira' : 'foram mergeados sem rework';
|
|
255
328
|
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
|
|
264
345
|
signals.push({
|
|
265
|
-
level
|
|
266
|
-
emoji
|
|
267
|
-
title:
|
|
268
|
-
body:
|
|
346
|
+
level,
|
|
347
|
+
emoji,
|
|
348
|
+
title: `**Rework: ${100 - pct}%** — ${reworkCount} de ${total} PRs tiveram commits extras`,
|
|
349
|
+
body: lines.join('\n')
|
|
269
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
|
+
|
|
270
380
|
} else {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
}
|
|
277
401
|
}
|
|
278
402
|
}
|
|
279
403
|
|
|
@@ -292,7 +416,6 @@ jobs:
|
|
|
292
416
|
|
|
293
417
|
// ── Stage Residence Time section (conditional) ──────────────────
|
|
294
418
|
let stageSection = '';
|
|
295
|
-
const jsonlPath = `.agentic-pdlc/metrics/raw/${weekKey}.jsonl`;
|
|
296
419
|
if (fs.existsSync(jsonlPath)) {
|
|
297
420
|
const records = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
|
|
298
421
|
if (records.length > 0) {
|
|
@@ -373,7 +496,7 @@ jobs:
|
|
|
373
496
|
console.log(`Built pulse issue for ${weekKey} — ${signals.length} signals, ${reds} red, ${yellows} yellow`);
|
|
374
497
|
|
|
375
498
|
- name: Create Weekly Pulse Issue
|
|
376
|
-
uses: actions/github-script@
|
|
499
|
+
uses: actions/github-script@v8
|
|
377
500
|
with:
|
|
378
501
|
script: |
|
|
379
502
|
const { owner, repo } = context.repo;
|
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
name: Run tests and linters
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v5.0.1
|
|
15
15
|
|
|
16
16
|
- name: Setup environment
|
|
17
17
|
run: echo "Replace this with your language/toolchain setup (e.g., actions/setup-node)"
|
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
steps:
|
|
28
28
|
- name: Validate Board Configuration
|
|
29
29
|
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
30
|
-
uses: actions/github-script@
|
|
30
|
+
uses: actions/github-script@v8
|
|
31
31
|
with:
|
|
32
32
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
33
33
|
script: |
|
|
@@ -37,12 +37,12 @@ jobs:
|
|
|
37
37
|
|
|
38
38
|
for NUM in $ISSUE_NUMS; do
|
|
39
39
|
LABELS=$(gh issue view "$NUM" --repo "$REPO" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
|
|
40
|
-
if echo "$LABELS" | grep -qw "stage:approval" || echo "$LABELS" | grep -qw "spec:approved" || echo "$LABELS" | grep -qw "stage:development"; then
|
|
40
|
+
if echo "$LABELS" | grep -qw "stage:approval" || echo "$LABELS" | grep -qw "spec:approved" || echo "$LABELS" | grep -qw "stage:development" || echo "$LABELS" | grep -qw "stage:testing" || echo "$LABELS" | grep -qw "human-approved"; then
|
|
41
41
|
echo "✅ Issue #$NUM approved"
|
|
42
42
|
else
|
|
43
43
|
STAGE=$(echo "$LABELS" | tr ' ' '\n' | grep "^stage:" | head -1 || echo "none")
|
|
44
44
|
echo "❌ Issue #$NUM missing approval (current: $STAGE)"
|
|
45
|
-
echo " Required: stage:approval OR spec:approved OR stage:development label on the issue."
|
|
45
|
+
echo " Required: stage:approval OR spec:approved OR stage:development OR stage:testing OR human-approved label on the issue."
|
|
46
46
|
echo " Emergency bypass: add 'hotfix' label to this PR."
|
|
47
47
|
exit 1
|
|
48
48
|
fi
|
|
@@ -27,13 +27,13 @@ jobs:
|
|
|
27
27
|
if: github.event_name == 'issues' && github.event.action == 'labeled'
|
|
28
28
|
runs-on: ubuntu-latest
|
|
29
29
|
env:
|
|
30
|
-
|
|
30
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
31
31
|
steps:
|
|
32
32
|
- name: Detect Label and Move Issue
|
|
33
|
-
if: ${{ env.
|
|
34
|
-
uses: actions/github-script@
|
|
33
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
34
|
+
uses: actions/github-script@v8
|
|
35
35
|
with:
|
|
36
|
-
github-token: ${{ env.
|
|
36
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
37
37
|
script: |
|
|
38
38
|
const labelName = context.payload.label.name;
|
|
39
39
|
let targetStatusId = null;
|
|
@@ -51,9 +51,6 @@ jobs:
|
|
|
51
51
|
} else if (labelName === 'stage:development') {
|
|
52
52
|
targetStatusId = process.env.STATUS_DEVELOPMENT;
|
|
53
53
|
stageName = 'Development';
|
|
54
|
-
} else if (labelName === 'stage:testing') {
|
|
55
|
-
targetStatusId = process.env.STATUS_TESTING;
|
|
56
|
-
stageName = 'Testing';
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
if (!targetStatusId) {
|
|
@@ -91,13 +88,13 @@ jobs:
|
|
|
91
88
|
contains(github.event.issue.labels.*.name, 'stage:detailing')
|
|
92
89
|
runs-on: ubuntu-latest
|
|
93
90
|
env:
|
|
94
|
-
|
|
91
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
95
92
|
steps:
|
|
96
93
|
- name: Check spec markers and swap labels
|
|
97
|
-
if: ${{ env.
|
|
98
|
-
uses: actions/github-script@
|
|
94
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
95
|
+
uses: actions/github-script@v8
|
|
99
96
|
with:
|
|
100
|
-
github-token: ${{ env.
|
|
97
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
101
98
|
script: |
|
|
102
99
|
const body = context.payload.issue.body ?? '';
|
|
103
100
|
if (!body.includes('## Acceptance Criteria') || !body.includes('## Files to modify')) return;
|
|
@@ -109,6 +106,42 @@ jobs:
|
|
|
109
106
|
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'stage:detailing' }).catch(() => {});
|
|
110
107
|
console.log(`Issue #${issue_number} auto-moved to stage:approval`);
|
|
111
108
|
|
|
109
|
+
# human-approved on issue → qa:approved on linked open PRs
|
|
110
|
+
handle-human-approved:
|
|
111
|
+
name: human-approved → qa:approved on linked PRs
|
|
112
|
+
if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'human-approved'
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
permissions:
|
|
115
|
+
issues: write
|
|
116
|
+
pull-requests: write
|
|
117
|
+
steps:
|
|
118
|
+
- name: Add qa:approved to linked open PRs
|
|
119
|
+
uses: actions/github-script@v8
|
|
120
|
+
with:
|
|
121
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
122
|
+
script: |
|
|
123
|
+
const { owner, repo } = context.repo;
|
|
124
|
+
const issueNumber = context.payload.issue.number;
|
|
125
|
+
const pattern = new RegExp(`(?:Closes?|Fixes?|Resolves?)\\s+#${issueNumber}\\b`, 'i');
|
|
126
|
+
|
|
127
|
+
const prs = await github.paginate(github.rest.pulls.list, {
|
|
128
|
+
owner, repo, state: 'open', per_page: 100
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const linked = prs.filter(pr => pattern.test(pr.body ?? ''));
|
|
132
|
+
|
|
133
|
+
if (linked.length === 0) {
|
|
134
|
+
console.log(`No open PRs linking issue #${issueNumber}. Exiting.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const pr of linked) {
|
|
139
|
+
await github.rest.issues.addLabels({
|
|
140
|
+
owner, repo, issue_number: pr.number, labels: ['qa:approved']
|
|
141
|
+
}).catch(() => {});
|
|
142
|
+
console.log(`PR #${pr.number} → qa:approved`);
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
# OPTIONAL: Uncomment to enable architecture-violation → Idea
|
|
113
146
|
# move-violation-to-board:
|
|
114
147
|
# name: architecture-violation → 💡 Idea
|
|
@@ -116,10 +149,10 @@ jobs:
|
|
|
116
149
|
# runs-on: ubuntu-latest
|
|
117
150
|
# steps:
|
|
118
151
|
# - name: Move issue to Idea
|
|
119
|
-
# if: ${{ env.
|
|
120
|
-
# uses: actions/github-script@
|
|
152
|
+
# if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
153
|
+
# uses: actions/github-script@v8
|
|
121
154
|
# with:
|
|
122
|
-
# github-token: ${{ env.
|
|
155
|
+
# github-token: ${{ env.PROJECT_TOKEN }}
|
|
123
156
|
# script: |
|
|
124
157
|
# const { issue: { number, node_id } } = context.payload;
|
|
125
158
|
# const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
@@ -143,13 +176,13 @@ jobs:
|
|
|
143
176
|
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
|
144
177
|
runs-on: ubuntu-latest
|
|
145
178
|
env:
|
|
146
|
-
|
|
179
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
147
180
|
steps:
|
|
148
181
|
- name: Move linked issue to Testing
|
|
149
|
-
if: ${{ env.
|
|
150
|
-
uses: actions/github-script@
|
|
182
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
183
|
+
uses: actions/github-script@v8
|
|
151
184
|
with:
|
|
152
|
-
github-token: ${{ env.
|
|
185
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
153
186
|
script: |
|
|
154
187
|
const prNumber = context.payload.pull_request.number;
|
|
155
188
|
const { owner, repo } = context.repo;
|
|
@@ -197,13 +230,13 @@ jobs:
|
|
|
197
230
|
if: github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'qa:approved'
|
|
198
231
|
runs-on: ubuntu-latest
|
|
199
232
|
env:
|
|
200
|
-
|
|
233
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
201
234
|
steps:
|
|
202
235
|
- name: Move linked issue to Code Review / PR
|
|
203
|
-
if: ${{ env.
|
|
204
|
-
uses: actions/github-script@
|
|
236
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
237
|
+
uses: actions/github-script@v8
|
|
205
238
|
with:
|
|
206
|
-
github-token: ${{ env.
|
|
239
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
207
240
|
script: |
|
|
208
241
|
const prNumber = context.payload.pull_request.number;
|
|
209
242
|
const { owner, repo } = context.repo;
|
|
@@ -229,6 +262,9 @@ jobs:
|
|
|
229
262
|
for (const n of linkedIssues) {
|
|
230
263
|
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
231
264
|
await moveItem(issue.node_id);
|
|
265
|
+
if (issue.labels?.some(l => l.name === 'stage:testing')) {
|
|
266
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:testing' }).catch(() => {});
|
|
267
|
+
}
|
|
232
268
|
console.log(`Issue #${n} → Code Review / PR`);
|
|
233
269
|
}
|
|
234
270
|
} else {
|
|
@@ -242,13 +278,13 @@ jobs:
|
|
|
242
278
|
if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved'
|
|
243
279
|
runs-on: ubuntu-latest
|
|
244
280
|
env:
|
|
245
|
-
|
|
281
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
246
282
|
steps:
|
|
247
283
|
- name: Swap PR labels
|
|
248
|
-
if: ${{ env.
|
|
249
|
-
uses: actions/github-script@
|
|
284
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
285
|
+
uses: actions/github-script@v8
|
|
250
286
|
with:
|
|
251
|
-
github-token: ${{ env.
|
|
287
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
252
288
|
script: |
|
|
253
289
|
const prNumber = context.payload.pull_request.number;
|
|
254
290
|
const { owner, repo } = context.repo;
|
|
@@ -261,13 +297,13 @@ jobs:
|
|
|
261
297
|
if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true
|
|
262
298
|
runs-on: ubuntu-latest
|
|
263
299
|
env:
|
|
264
|
-
|
|
300
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
265
301
|
steps:
|
|
266
302
|
- name: Move issue to Production
|
|
267
|
-
if: ${{ env.
|
|
268
|
-
uses: actions/github-script@
|
|
303
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
304
|
+
uses: actions/github-script@v8
|
|
269
305
|
with:
|
|
270
|
-
github-token: ${{ env.
|
|
306
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
271
307
|
script: |
|
|
272
308
|
const prNumber = context.payload.pull_request.number;
|
|
273
309
|
const { owner, repo } = context.repo;
|
|
@@ -293,6 +329,9 @@ jobs:
|
|
|
293
329
|
for (const n of linkedIssues) {
|
|
294
330
|
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
295
331
|
await moveItem(issue.node_id);
|
|
332
|
+
if (issue.labels?.some(l => l.name === 'stage:approval')) {
|
|
333
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:approval' }).catch(() => {});
|
|
334
|
+
}
|
|
296
335
|
console.log(`Issue #${n} → Production`);
|
|
297
336
|
}
|
|
298
337
|
} else {
|
|
@@ -305,7 +344,7 @@ jobs:
|
|
|
305
344
|
runs-on: ubuntu-latest
|
|
306
345
|
steps:
|
|
307
346
|
- name: Remove transient labels
|
|
308
|
-
uses: actions/github-script@
|
|
347
|
+
uses: actions/github-script@v8
|
|
309
348
|
with:
|
|
310
349
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
311
350
|
script: |
|
|
@@ -314,7 +353,7 @@ jobs:
|
|
|
314
353
|
const toRemove = [
|
|
315
354
|
'stage:brainstorming', 'stage:detailing',
|
|
316
355
|
'stage:approval', 'stage:development', 'stage:testing',
|
|
317
|
-
'
|
|
356
|
+
'qa:needs-work', 'pr:in-review', 'jules'
|
|
318
357
|
];
|
|
319
358
|
for (const label of toRemove) {
|
|
320
359
|
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }).catch(() => {});
|
|
@@ -14,9 +14,9 @@ jobs:
|
|
|
14
14
|
name: AC Coverage Verification (GitHub Models)
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
env:
|
|
17
|
-
|
|
17
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@v5.0.1
|
|
20
20
|
with:
|
|
21
21
|
fetch-depth: 0
|
|
22
22
|
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
32
32
|
|
|
33
33
|
# Get PR diff (truncated to 8000 chars to stay within context limits)
|
|
34
|
-
DIFF=$(git diff "$BASE" "$HEAD" | head -c
|
|
34
|
+
DIFF=$(git diff "$BASE" "$HEAD" | head -c 8000)
|
|
35
35
|
|
|
36
36
|
# Extract linked issues from PR body
|
|
37
37
|
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body // ""')
|
|
@@ -51,31 +51,45 @@ jobs:
|
|
|
51
51
|
fi
|
|
52
52
|
|
|
53
53
|
# Serialize prompt as JSON string and call GitHub Models API (30s timeout)
|
|
54
|
-
PROMPT_JSON=$(printf '%s' "You are
|
|
54
|
+
PROMPT_JSON=$(printf '%s' "You are an adversarial product tester. Your mission is to find what the Acceptance Criteria do NOT cover — undefined edge cases, ambiguous states, missing user scenarios. Do NOT review code quality, file structure, or technical consistency.\n\nACCEPTANCE CRITERIA:\n${AC_CONTEXT}\n\nPR DIFF:\n${DIFF}\n\nRespond in exactly 3 lines (do NOT wrap your response in markdown code blocks or any other formatting):\nLine 1: PASS or FAIL (PASS if the PR diff fully satisfies the stated Acceptance Criteria, FAIL if it falls short of covering them)\nLine 2: Gaps: [one-line summary of AC gaps found, or \"none\"]\nLine 3: Not covered: [AC refs where diff falls short, or \"all covered\"]" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
|
|
55
55
|
|
|
56
|
-
RESPONSE
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
RESPONSE="API_ERROR"
|
|
57
|
+
for attempt in 1 2 3; do
|
|
58
|
+
RESULT=$(curl -s -X POST \
|
|
59
|
+
"https://models.github.ai/inference/chat/completions" \
|
|
60
|
+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
|
61
|
+
-H "Content-Type: application/json" \
|
|
62
|
+
-d "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}]}" \
|
|
63
|
+
-w "\n__HTTP_STATUS__:%{http_code}" \
|
|
64
|
+
--max-time 45 2>/dev/null)
|
|
65
|
+
HTTP_STATUS=$(echo "$RESULT" | grep -o '__HTTP_STATUS__:[0-9]*' | cut -d: -f2)
|
|
66
|
+
BODY=$(echo "$RESULT" | sed 's/__HTTP_STATUS__:[0-9]*$//')
|
|
67
|
+
echo "Attempt $attempt: HTTP $HTTP_STATUS"
|
|
68
|
+
if [ "$HTTP_STATUS" = "200" ]; then RESPONSE="$BODY"; break; fi
|
|
69
|
+
[ $attempt -lt 3 ] && sleep 20
|
|
70
|
+
done
|
|
62
71
|
|
|
63
72
|
if [ "$RESPONSE" = "API_ERROR" ]; then
|
|
64
|
-
GH_TOKEN="$
|
|
73
|
+
GH_TOKEN="$PROJECT_TOKEN" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=infra:qa-broken'
|
|
65
74
|
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** Could not reach GitHub Models API. Manual review required."
|
|
66
|
-
exit
|
|
75
|
+
exit 1
|
|
67
76
|
fi
|
|
68
77
|
|
|
69
|
-
VERDICT=$(echo "$RESPONSE" | python3 -c 'import json,sys,re; d=json.load(sys.stdin); t=d.get("choices",[{}])[0].get("message",{}).get("content","").strip();
|
|
78
|
+
VERDICT=$(echo "$RESPONSE" | python3 -c 'import json,sys,re; d=json.load(sys.stdin); t=d.get("choices",[{}])[0].get("message",{}).get("content","").strip(); lines=[l for l in t.split("\n") if not l.strip().startswith("```")]; first=lines[0].upper() if lines else ""; print("FAIL" if re.search(r"\bFAIL\b",first) else "PASS" if re.search(r"\bPASS\b",first) else "API_ERROR")')
|
|
70
79
|
EXPLANATION=$(echo "$RESPONSE" | python3 -c 'import json,sys; d=json.load(sys.stdin); t=d.get("choices",[{}])[0].get("message",{}).get("content","").strip(); lines=t.split("\n",1); print(lines[1].strip() if len(lines)>1 else "")')
|
|
71
80
|
|
|
72
81
|
if echo "$VERDICT" | grep -q "^PASS"; then
|
|
73
|
-
GH_TOKEN="$
|
|
74
|
-
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:**
|
|
82
|
+
GH_TOKEN="$PROJECT_TOKEN" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=qa:approved'
|
|
83
|
+
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** PASS
|
|
84
|
+
|
|
85
|
+
${EXPLANATION}"
|
|
75
86
|
elif echo "$VERDICT" | grep -q "^FAIL"; then
|
|
76
|
-
GH_TOKEN="$
|
|
77
|
-
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:**
|
|
87
|
+
GH_TOKEN="$PROJECT_TOKEN" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=qa:needs-work'
|
|
88
|
+
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** FAIL
|
|
89
|
+
|
|
90
|
+
${EXPLANATION}"
|
|
78
91
|
else
|
|
79
|
-
GH_TOKEN="$
|
|
92
|
+
GH_TOKEN="$PROJECT_TOKEN" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=infra:qa-broken'
|
|
80
93
|
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** Could not parse GitHub Models response. Manual review required."
|
|
94
|
+
exit 1
|
|
81
95
|
fi
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: QA Gate
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
pull-requests: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
qa-gate:
|
|
12
|
+
name: QA Gate
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Check QA status label
|
|
16
|
+
env:
|
|
17
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
18
|
+
run: |
|
|
19
|
+
set -e
|
|
20
|
+
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
21
|
+
REPO="${{ github.repository }}"
|
|
22
|
+
|
|
23
|
+
PR_LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
|
|
24
|
+
|
|
25
|
+
if echo "$PR_LABELS" | grep -qx "hotfix"; then
|
|
26
|
+
echo "✅ QA Gate: hotfix label — bypassed."
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if echo "$PR_LABELS" | grep -qx "human-approved"; then
|
|
31
|
+
echo "✅ QA Gate: human-approved label — manual QA sign-off, bypassed."
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if echo "$PR_LABELS" | grep -qx "qa:approved"; then
|
|
36
|
+
echo "✅ QA Gate: qa:approved — merge allowed."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if echo "$PR_LABELS" | grep -qx "infra:qa-broken"; then
|
|
41
|
+
echo "❌ QA Gate: infra:qa-broken — GitHub Models API unreachable. Manual QA review required before merge."
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if echo "$PR_LABELS" | grep -qx "qa:needs-work"; then
|
|
46
|
+
echo "❌ QA Gate: qa:needs-work — acceptance criteria not fully met. Fix required before merge."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo "❌ QA Gate: no QA label found — AC Coverage Verification has not completed. Wait for the check to finish."
|
|
51
|
+
exit 1
|