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.
Files changed (64) hide show
  1. package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
  2. package/.agentic-pdlc/metrics/raw/2026-W22.jsonl +114 -0
  3. package/.claude/settings.json +18 -0
  4. package/.coderabbit.yaml +35 -0
  5. package/.github/ISSUE_TEMPLATE/bug.md +53 -0
  6. package/.github/ISSUE_TEMPLATE/feature.md +54 -0
  7. package/.github/ISSUE_TEMPLATE/task.md +33 -0
  8. package/.github/workflows/add-to-board.yml +1 -1
  9. package/.github/workflows/agent-trigger.yml +4 -4
  10. package/.github/workflows/ci.yml +1 -1
  11. package/.github/workflows/npm-publish.yml +2 -2
  12. package/.github/workflows/pdlc-health-check.yml +1 -1
  13. package/.github/workflows/pdlc-stage-gate.yml +2 -2
  14. package/.github/workflows/project-automation.yml +25 -40
  15. package/AGENTS.md +50 -8
  16. package/CLAUDE.md +3 -1
  17. package/README.md +33 -32
  18. package/SETUP.md +2 -1
  19. package/adapters/claude-code/skill.md +39 -14
  20. package/adapters/hooks/pdlc-stage-gate.sh +3 -8
  21. package/bin/cli.js +555 -194
  22. package/docs/pdlc.md +5 -5
  23. package/docs/superpowers/plans/2026-05-28-jules-label-pat-split.md +240 -0
  24. package/docs/superpowers/plans/2026-05-29-agentic-pulse-rework-taxonomy.md +474 -0
  25. package/docs/superpowers/plans/2026-05-29-qa-gate-enforcement.md +354 -0
  26. package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
  27. package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
  28. package/docs/superpowers/specs/2026-05-29-agentic-pulse-rework-taxonomy-design.md +122 -0
  29. package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
  30. package/package.json +2 -2
  31. package/templates/.github/ISSUE_TEMPLATE/bug.md +53 -0
  32. package/templates/.github/ISSUE_TEMPLATE/feature.md +54 -0
  33. package/templates/.github/ISSUE_TEMPLATE/task.md +33 -0
  34. package/templates/.github/workflows/add-to-board.yml +4 -4
  35. package/templates/.github/workflows/agent-trigger.yml +22 -13
  36. package/{.agentic-pdlc/templates → templates}/.github/workflows/agentic-metrics.yml +150 -27
  37. package/templates/.github/workflows/ci.yml +1 -1
  38. package/templates/.github/workflows/pdlc-health-check.yml +1 -1
  39. package/templates/.github/workflows/pdlc-stage-gate.yml +2 -2
  40. package/templates/.github/workflows/project-automation.yml +71 -32
  41. package/templates/.github/workflows/qa-agent.yml +32 -18
  42. package/templates/.github/workflows/qa-gate.yml +51 -0
  43. package/templates/full/AGENTS.md +143 -0
  44. package/templates/full/CLAUDE.md +30 -0
  45. package/templates/{docs → full/docs}/pdlc.md +4 -4
  46. package/templates/lite/AGENTS.md +121 -0
  47. package/templates/lite/CLAUDE.md +44 -0
  48. package/tests/cli.test.js +32 -0
  49. package/.agentic-pdlc/templates/.github/CODEOWNERS +0 -5
  50. package/.agentic-pdlc/templates/.github/copilot-instructions.md +0 -12
  51. package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +0 -38
  52. package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +0 -146
  53. package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +0 -16
  54. package/.agentic-pdlc/templates/.github/workflows/ci.yml +0 -54
  55. package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +0 -121
  56. package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +0 -51
  57. package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +0 -274
  58. package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +0 -21
  59. package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +0 -128
  60. package/.agentic-pdlc/templates/AGENTS.md +0 -104
  61. package/.agentic-pdlc/templates/docs/pdlc.md +0 -123
  62. package/.github/workflows/agentic-metrics.yml +0 -422
  63. package/.github/workflows/qa-agent.yml +0 -128
  64. 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@v4
22
+ - uses: actions/checkout@v5.0.1
18
23
 
19
24
  - name: Collect Stage Residence Time
20
- uses: actions/github-script@v7
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@v7
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: commits per PR single push session = first-shot
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.map(c => new Date(c.commit.committer.date).getTime()).sort();
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
- if (sessions === 1) firstShots++;
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 (pct >= 80) {
257
- signals.push({
258
- level: 'green',
259
- emoji: '🟢',
260
- title: `**${subject}: ${pct}%**`,
261
- body: `${firstShots} de ${total} PRs ${verb} esta semana. ✅`
262
- });
263
- } else if (pct < 50) {
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: 'yellow',
266
- emoji: '🟡',
267
- title: `**${subject}: ${pct}%rework alto**`,
268
- body: `Apenas ${firstShots} de ${total} PRs sem commits extras.\n→ Specs incompletas ou mudanças de requisito durante implementação.`
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
- signals.push({
272
- level: 'neutral',
273
- emoji: '🔵',
274
- title: `**${subject}: ${pct}%**`,
275
- body: `${firstShots} de ${total} PRs sem rework commits.`
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@v7
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@v4
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@v7
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
30
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
31
31
  steps:
32
32
  - name: Detect Label and Move Issue
33
- if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
34
- uses: actions/github-script@v7
33
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
34
+ uses: actions/github-script@v8
35
35
  with:
36
- github-token: ${{ env.PROJECT_PAT }}
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
91
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
95
92
  steps:
96
93
  - name: Check spec markers and swap labels
97
- if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
98
- uses: actions/github-script@v7
94
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
95
+ uses: actions/github-script@v8
99
96
  with:
100
- github-token: ${{ env.PROJECT_PAT }}
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.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
120
- # uses: actions/github-script@v7
152
+ # if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
153
+ # uses: actions/github-script@v8
121
154
  # with:
122
- # github-token: ${{ env.PROJECT_PAT }}
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
179
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
147
180
  steps:
148
181
  - name: Move linked issue to Testing
149
- if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
150
- uses: actions/github-script@v7
182
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
183
+ uses: actions/github-script@v8
151
184
  with:
152
- github-token: ${{ env.PROJECT_PAT }}
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
233
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
201
234
  steps:
202
235
  - name: Move linked issue to Code Review / PR
203
- if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
204
- uses: actions/github-script@v7
236
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
237
+ uses: actions/github-script@v8
205
238
  with:
206
- github-token: ${{ env.PROJECT_PAT }}
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
281
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
246
282
  steps:
247
283
  - name: Swap PR labels
248
- if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
249
- uses: actions/github-script@v7
284
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
285
+ uses: actions/github-script@v8
250
286
  with:
251
- github-token: ${{ env.PROJECT_PAT }}
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
300
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
265
301
  steps:
266
302
  - name: Move issue to Production
267
- if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
268
- uses: actions/github-script@v7
303
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
304
+ uses: actions/github-script@v8
269
305
  with:
270
- github-token: ${{ env.PROJECT_PAT }}
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@v7
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
- 'agent:working', 'qa:needs-work', 'pr:in-review', 'jules'
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
- PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
17
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
18
18
  steps:
19
- - uses: actions/checkout@v4
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 64000)
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 a senior QA engineer. Review whether this PR diff satisfies the Acceptance Criteria below.\n\nACCEPTANCE CRITERIA:\n${AC_CONTEXT}\n\nPR DIFF:\n${DIFF}\n\nFirst line of your response must be exactly one word: PASS or FAIL. Second line: brief explanation (max 3 sentences)." | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
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=$(curl -sf -X POST \
57
- "https://models.github.ai/inference/chat/completions" \
58
- -H "Authorization: Bearer ${GITHUB_TOKEN}" \
59
- -H "Content-Type: application/json" \
60
- -d "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}]}" \
61
- --max-time 30 || echo "API_ERROR")
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="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=infra:qa-broken'
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 0
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(); first=t.split("\n")[0].upper() if t else ""; print("FAIL" if re.search(r"\bFAIL\b",first) else "PASS" if re.search(r"\bPASS\b",first) else "API_ERROR")')
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="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=qa:approved'
74
- gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** AC coverage verified. ${EXPLANATION}"
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="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=qa:needs-work'
77
- gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** AC coverage insufficient. ${EXPLANATION}"
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="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=infra:qa-broken'
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