create-agentic-pdlc 2.2.1 → 2.4.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 (59) hide show
  1. package/.agentic-pdlc/SETUP_PROMPT.md +3 -4
  2. package/.agentic-pdlc/metrics/raw/2026-W18.jsonl +2 -0
  3. package/.agentic-pdlc/metrics/raw/2026-W21.jsonl +68 -0
  4. package/.agentic-pdlc/metrics/raw/2026-W22.jsonl +114 -0
  5. package/.agentic-setup-prompt.md +3 -4
  6. package/.agentic-setup.md +3 -4
  7. package/.github/ISSUE_TEMPLATE/bug.md +53 -0
  8. package/.github/ISSUE_TEMPLATE/feature.md +54 -0
  9. package/.github/ISSUE_TEMPLATE/task.md +33 -0
  10. package/.github/workflows/add-to-board.yml +1 -1
  11. package/.github/workflows/agent-trigger.yml +4 -4
  12. package/.github/workflows/agentic-metrics.yml +171 -38
  13. package/.github/workflows/ci.yml +1 -1
  14. package/.github/workflows/npm-publish.yml +2 -2
  15. package/.github/workflows/pdlc-health-check.yml +1 -3
  16. package/.github/workflows/pdlc-stage-gate.yml +2 -2
  17. package/.github/workflows/project-automation.yml +79 -16
  18. package/.github/workflows/qa-agent.yml +26 -15
  19. package/.github/workflows/qa-gate.yml +51 -0
  20. package/AGENTS.md +50 -8
  21. package/CLAUDE.md +53 -3
  22. package/SETUP.md +4 -1
  23. package/adapters/claude-code/skill.md +44 -20
  24. package/adapters/hooks/pdlc-stage-gate.sh +2 -7
  25. package/bin/cli.js +41 -9
  26. package/docs/flow.md +8 -21
  27. package/docs/pdlc.md +24 -16
  28. package/docs/superpowers/plans/2026-05-28-jules-label-pat-split.md +240 -0
  29. package/docs/superpowers/plans/2026-05-29-agentic-pulse-rework-taxonomy.md +474 -0
  30. package/docs/superpowers/plans/2026-05-29-qa-gate-enforcement.md +354 -0
  31. package/docs/superpowers/specs/2026-05-29-agentic-pulse-rework-taxonomy-design.md +122 -0
  32. package/package.json +1 -1
  33. package/templates/.github/ISSUE_TEMPLATE/bug.md +53 -0
  34. package/templates/.github/ISSUE_TEMPLATE/feature.md +54 -0
  35. package/templates/.github/ISSUE_TEMPLATE/task.md +33 -0
  36. package/templates/.github/workflows/add-to-board.yml +4 -4
  37. package/templates/.github/workflows/agent-trigger.yml +24 -15
  38. package/{.agentic-pdlc/templates → templates}/.github/workflows/agentic-metrics.yml +166 -36
  39. package/templates/.github/workflows/ci.yml +15 -1
  40. package/templates/.github/workflows/pdlc-health-check.yml +1 -3
  41. package/templates/.github/workflows/pdlc-stage-gate.yml +2 -2
  42. package/templates/.github/workflows/project-automation.yml +93 -36
  43. package/templates/.github/workflows/qa-agent.yml +33 -17
  44. package/templates/.github/workflows/qa-gate.yml +51 -0
  45. package/templates/AGENTS.md +74 -23
  46. package/templates/docs/pdlc.md +24 -16
  47. package/.agentic-pdlc/templates/.github/CODEOWNERS +0 -5
  48. package/.agentic-pdlc/templates/.github/copilot-instructions.md +0 -12
  49. package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +0 -38
  50. package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +0 -146
  51. package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +0 -16
  52. package/.agentic-pdlc/templates/.github/workflows/ci.yml +0 -40
  53. package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +0 -123
  54. package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +0 -51
  55. package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +0 -278
  56. package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +0 -21
  57. package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +0 -128
  58. package/.agentic-pdlc/templates/AGENTS.md +0 -81
  59. package/.agentic-pdlc/templates/docs/pdlc.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');
@@ -25,7 +30,7 @@ jobs:
25
30
  const { owner, repo } = context.repo;
26
31
  const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
27
32
  const STAGE_LABELS = new Set([
28
- 'stage:exploration', 'stage:brainstorming', 'stage:detailing',
33
+ 'stage:brainstorming', 'stage:detailing',
29
34
  'stage:approval', 'stage:development', 'stage:testing'
30
35
  ]);
31
36
 
@@ -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,12 +416,10 @@ 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) {
299
422
  const STAGES = [
300
- ['stage:exploration', 'Exploration'],
301
423
  ['stage:brainstorming', 'Brainstorming'],
302
424
  ['stage:detailing', 'Detailing'],
303
425
  ['stage:approval', 'Approval'],
@@ -311,11 +433,14 @@ jobs:
311
433
  }
312
434
  const avg = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
313
435
 
314
- let maxStage = null, maxDays = 0;
436
+ let maxStage = null, maxDays = -1;
315
437
  const rows = [];
316
438
  for (const [stage, label] of STAGES) {
317
439
  const days = byStage[stage];
318
- if (!days) continue;
440
+ if (!days) {
441
+ rows.push(`| **${label}** | — | — |`);
442
+ continue;
443
+ }
319
444
  const a = round1(avg(days));
320
445
  rows.push(`| **${label}** | ${a}d | ${days.length} |`);
321
446
  if (a > maxDays) { maxStage = label; maxDays = a; }
@@ -374,7 +499,7 @@ jobs:
374
499
  console.log(`Built pulse issue for ${weekKey} — ${signals.length} signals, ${reds} red, ${yellows} yellow`);
375
500
 
376
501
  - name: Create Weekly Pulse Issue
377
- uses: actions/github-script@v7
502
+ uses: actions/github-script@v8
378
503
  with:
379
504
  script: |
380
505
  const { owner, repo } = context.repo;
@@ -394,19 +519,27 @@ jobs:
394
519
  console.log(`Created label ${LABEL}`);
395
520
  }
396
521
 
397
- // Close previous pulse issues
522
+ // Close previous pulse issues; upsert if same week already exists
398
523
  const prev = await github.rest.issues.listForRepo({
399
524
  owner, repo, labels: LABEL, state: 'open', per_page: 20
400
525
  });
526
+ let existingIssue = null;
401
527
  for (const issue of prev.data) {
402
- if (issue.title !== title) {
528
+ if (issue.title === title) {
529
+ existingIssue = issue;
530
+ console.log(`Found existing pulse for ${title}: #${issue.number} — will update body`);
531
+ } else {
403
532
  await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed' });
404
533
  console.log(`Closed previous pulse: #${issue.number}`);
405
534
  }
406
535
  }
407
536
 
408
- // Create new pulse issue
409
- const created = await github.rest.issues.create({
410
- owner, repo, title, body, labels: [LABEL]
411
- });
412
- console.log(`✅ Created pulse issue: #${created.data.number} — ${title}`);
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
+ }
@@ -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)"
@@ -14,10 +14,10 @@ jobs:
14
14
 
15
15
  steps:
16
16
  - name: Checkout Code
17
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v5.0.1
18
18
 
19
19
  - name: Setup Node.js
20
- uses: actions/setup-node@v4
20
+ uses: actions/setup-node@v6.4.0
21
21
  with:
22
22
  node-version: '24'
23
23
  registry-url: 'https://registry.npmjs.org'
@@ -8,7 +8,6 @@ on:
8
8
  env:
9
9
  PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
10
10
  STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
11
- STATUS_EXPLORATION: "96ac537d"
12
11
  STATUS_BRAINSTORMING: "8eb07c5b"
13
12
  STATUS_DETAILING: "9f6ce70e"
14
13
  STATUS_APPROVAL: "31bf4610"
@@ -28,14 +27,13 @@ jobs:
28
27
  steps:
29
28
  - name: Validate Board Configuration
30
29
  if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
31
- uses: actions/github-script@v7
30
+ uses: actions/github-script@v8
32
31
  with:
33
32
  github-token: ${{ env.PROJECT_TOKEN }}
34
33
  script: |
35
34
  const projectId = process.env.PROJECT_ID;
36
35
  const statusFieldId = process.env.STATUS_FIELD_ID;
37
36
  const envVars = {
38
- 'STATUS_EXPLORATION': process.env.STATUS_EXPLORATION,
39
37
  'STATUS_BRAINSTORMING': process.env.STATUS_BRAINSTORMING,
40
38
  'STATUS_DETAILING': process.env.STATUS_DETAILING,
41
39
  'STATUS_APPROVAL': process.env.STATUS_APPROVAL,
@@ -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
@@ -6,13 +6,12 @@ on:
6
6
  pull_request_review:
7
7
  types: [submitted]
8
8
  issues:
9
- types: [labeled]
9
+ types: [labeled, closed]
10
10
 
11
11
  env:
12
12
  PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
13
13
  STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
14
14
  STATUS_IDEA: "bb6e5a20"
15
- STATUS_EXPLORATION: "96ac537d"
16
15
  STATUS_BRAINSTORMING: "8eb07c5b"
17
16
  STATUS_DETAILING: "9f6ce70e"
18
17
  STATUS_APPROVAL: "31bf4610"
@@ -32,7 +31,7 @@ jobs:
32
31
  steps:
33
32
  - name: Detect Label and Move Issue
34
33
  if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
35
- uses: actions/github-script@v7
34
+ uses: actions/github-script@v8
36
35
  with:
37
36
  github-token: ${{ env.PROJECT_TOKEN }}
38
37
  script: |
@@ -40,10 +39,7 @@ jobs:
40
39
  let targetStatusId = null;
41
40
  let stageName = null;
42
41
 
43
- if (labelName === 'stage:exploration') {
44
- targetStatusId = process.env.STATUS_EXPLORATION;
45
- stageName = 'Exploration';
46
- } else if (labelName === 'stage:brainstorming') {
42
+ if (labelName === 'stage:brainstorming') {
47
43
  targetStatusId = process.env.STATUS_BRAINSTORMING;
48
44
  stageName = 'Brainstorming';
49
45
  } else if (labelName === 'stage:detailing') {
@@ -55,9 +51,6 @@ jobs:
55
51
  } else if (labelName === 'stage:development') {
56
52
  targetStatusId = process.env.STATUS_DEVELOPMENT;
57
53
  stageName = 'Development';
58
- } else if (labelName === 'stage:testing') {
59
- targetStatusId = process.env.STATUS_TESTING;
60
- stageName = 'Testing';
61
54
  }
62
55
 
63
56
  if (!targetStatusId) {
@@ -87,6 +80,42 @@ jobs:
87
80
  await moveItem(node_id, targetStatusId);
88
81
  console.log(`Issue #${number} moved to ${stageName}`);
89
82
 
83
+ # human-approved on issue → qa:approved on linked open PRs
84
+ handle-human-approved:
85
+ name: human-approved → qa:approved on linked PRs
86
+ if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'human-approved'
87
+ runs-on: ubuntu-latest
88
+ permissions:
89
+ issues: write
90
+ pull-requests: write
91
+ steps:
92
+ - name: Add qa:approved to linked open PRs
93
+ uses: actions/github-script@v8
94
+ with:
95
+ github-token: ${{ secrets.GITHUB_TOKEN }}
96
+ script: |
97
+ const { owner, repo } = context.repo;
98
+ const issueNumber = context.payload.issue.number;
99
+ const pattern = new RegExp(`(?:Closes?|Fixes?|Resolves?)\\s+#${issueNumber}\\b`, 'i');
100
+
101
+ const prs = await github.paginate(github.rest.pulls.list, {
102
+ owner, repo, state: 'open', per_page: 100
103
+ });
104
+
105
+ const linked = prs.filter(pr => pattern.test(pr.body ?? ''));
106
+
107
+ if (linked.length === 0) {
108
+ console.log(`No open PRs linking issue #${issueNumber}. Exiting.`);
109
+ return;
110
+ }
111
+
112
+ for (const pr of linked) {
113
+ await github.rest.issues.addLabels({
114
+ owner, repo, issue_number: pr.number, labels: ['qa:approved']
115
+ }).catch(() => {});
116
+ console.log(`PR #${pr.number} → qa:approved`);
117
+ }
118
+
90
119
  # OPTIONAL: Uncomment to enable architecture-violation → Idea
91
120
  # move-violation-to-board:
92
121
  # name: architecture-violation → 💡 Idea
@@ -95,7 +124,7 @@ jobs:
95
124
  # steps:
96
125
  # - name: Move issue to Idea
97
126
  # if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
98
- # uses: actions/github-script@v7
127
+ # uses: actions/github-script@v8
99
128
  # with:
100
129
  # github-token: ${{ env.PROJECT_TOKEN }}
101
130
  # script: |
@@ -125,7 +154,7 @@ jobs:
125
154
  steps:
126
155
  - name: Move linked issue to Testing
127
156
  if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
128
- uses: actions/github-script@v7
157
+ uses: actions/github-script@v8
129
158
  with:
130
159
  github-token: ${{ env.PROJECT_TOKEN }}
131
160
  script: |
@@ -156,11 +185,17 @@ jobs:
156
185
  });
157
186
  };
158
187
 
188
+ const stageLabelsToRemove = ['stage:development', 'stage:brainstorming', 'stage:detailing', 'jules'];
189
+
159
190
  if (linkedIssues.length > 0) {
160
191
  for (const n of linkedIssues) {
161
192
  const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
162
193
  await moveItem(issue.node_id);
163
- console.log(`Issue #${n} Testing`);
194
+ for (const label of stageLabelsToRemove) {
195
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: label }).catch(() => {});
196
+ }
197
+ await github.rest.issues.addLabels({ owner, repo, issue_number: n, labels: ['stage:testing'] }).catch(() => {});
198
+ console.log(`Issue #${n} → Testing (labels updated)`);
164
199
  }
165
200
  } else {
166
201
  await moveItem(pr.node_id);
@@ -179,7 +214,7 @@ jobs:
179
214
  steps:
180
215
  - name: Move linked issue to Code Review / PR
181
216
  if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
182
- uses: actions/github-script@v7
217
+ uses: actions/github-script@v8
183
218
  with:
184
219
  github-token: ${{ env.PROJECT_TOKEN }}
185
220
  script: |
@@ -213,6 +248,9 @@ jobs:
213
248
  for (const n of linkedIssues) {
214
249
  const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
215
250
  await moveItem(issue.node_id);
251
+ if (issue.labels?.some(l => l.name === 'stage:testing')) {
252
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:testing' }).catch(() => {});
253
+ }
216
254
  console.log(`Issue #${n} → Code Review / PR`);
217
255
  }
218
256
  } else {
@@ -230,7 +268,7 @@ jobs:
230
268
  steps:
231
269
  - name: Swap PR labels
232
270
  if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
233
- uses: actions/github-script@v7
271
+ uses: actions/github-script@v8
234
272
  with:
235
273
  github-token: ${{ env.PROJECT_TOKEN }}
236
274
  script: |
@@ -249,7 +287,7 @@ jobs:
249
287
  steps:
250
288
  - name: Move issue to Production
251
289
  if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
252
- uses: actions/github-script@v7
290
+ uses: actions/github-script@v8
253
291
  with:
254
292
  github-token: ${{ env.PROJECT_TOKEN }}
255
293
  script: |
@@ -277,8 +315,33 @@ jobs:
277
315
  for (const n of linkedIssues) {
278
316
  const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
279
317
  await moveItem(issue.node_id);
318
+ if (issue.labels?.some(l => l.name === 'stage:approval')) {
319
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:approval' }).catch(() => {});
320
+ }
280
321
  console.log(`Issue #${n} → Production`);
281
322
  }
282
323
  } else {
283
324
  await moveItem(pr.node_id);
284
325
  }
326
+
327
+ cleanup-labels-on-close:
328
+ name: Issue closed → strip stage/agent labels
329
+ if: github.event_name == 'issues' && github.event.action == 'closed'
330
+ runs-on: ubuntu-latest
331
+ steps:
332
+ - name: Remove transient labels
333
+ uses: actions/github-script@v8
334
+ with:
335
+ github-token: ${{ secrets.GITHUB_TOKEN }}
336
+ script: |
337
+ const { owner, repo } = context.repo;
338
+ const issue_number = context.payload.issue.number;
339
+ const toRemove = [
340
+ 'stage:brainstorming', 'stage:detailing',
341
+ 'stage:approval', 'stage:development', 'stage:testing',
342
+ 'qa:needs-work', 'pr:in-review', 'jules'
343
+ ];
344
+ for (const label of toRemove) {
345
+ await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }).catch(() => {});
346
+ }
347
+ console.log(`Issue #${issue_number} labels cleaned up`);