create-agentic-pdlc 2.1.3 → 2.1.5

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.
@@ -42,7 +42,7 @@ If any of these files are missing, you are in **Setup Mode**. Do not proceed wit
42
42
  - c) **Yes, activate** — *Uncomment the `move-violation-to-board` job in `project-automation.yml`.* → Activate immediately.
43
43
  - **QA Agent:** Ask: *"Do you want to use a QA agent to verify PRs automatically before Code Review?"* Present the options:
44
44
  - a) **No (Variant A)** — *PRs go straight to Code Review. Standard and simpler.*
45
- - b) **Yes (Variant B), but I need help configuring it** — *PRs pass through a QA Agent before being reviewed. Requires a QA Agent (e.g., QAWolf).* → Guide the user through configuration.
45
+ - b) **Yes (Variant B), but I need help configuring it** — *PRs pass through a QA Agent before being reviewed. Requires a `GEMINI_API_KEY` secret (free tier available at ai.google.dev).* → Guide the user through configuration.
46
46
  - c) **Yes (Variant B), I already have it configured** — *PRs pass through a QA Agent before being reviewed.* → Activate Variant B immediately: change `STATUS_CODE_REVIEW_PR` to `STATUS_TESTING` in the `move-card-on-pr-open` job and uncomment the `move-card-on-qa-pass` job in `project-automation.yml`.
47
47
  - **Implementation Agent:** Ask: *"Do you use an autonomous implementation agent? (It implements the features you approve for development)"* Present the options:
48
48
  - a) **No** — *No autonomous implementation agent.*
@@ -70,6 +70,18 @@ If any of these files are missing, you are in **Setup Mode**. Do not proceed wit
70
70
 
71
71
  ---
72
72
 
73
+ ## UPDATE MODE
74
+
75
+ If the user says anything like "update the pipeline", "update the board", "update agentic-pdlc", or "configure the agents", run:
76
+
77
+ ```bash
78
+ npx create-agentic-pdlc --update
79
+ ```
80
+
81
+ This detects which optional agents (Jules, QA Agent, Sentinel) are already configured in the project and interactively configures the missing ones. It does **not** overwrite user-owned files (`AGENTS.md`, agent config files).
82
+
83
+ ---
84
+
73
85
  ## EXECUTION MODE
74
86
 
75
87
  If `AGENTS.md` and `docs/pdlc.md` are present, you are in **Execution Mode**.
package/bin/cli.js CHANGED
@@ -59,7 +59,19 @@ const i18n = {
59
59
  missing_claude: t('❌ Could not find instruction file at ', '❌ Não foi possível encontrar o arquivo de instrução em ', '❌ No se pudo encontrar el archivo de instrucción en '),
60
60
  cursor_rules_written: t('✅ Default cursor rules written to .cursorrules', '✅ Regras padrão do cursor salvas em .cursorrules', '✅ Reglas por defecto de cursor guardadas en .cursorrules'),
61
61
  setup_done: t('🎉 All set! Continue the setup with your agent:', '🎉 Aqui tá pronto! Continue o setup com o seu agente:', '🎉 ¡Listo! Continúa el setup con tu agente:'),
62
- setup_done_hint: t('>>> Tell it to read and execute the .agentic-setup.md file!', '>>> Diga a ele para ler e executar o arquivo .agentic-setup.md!', '>>> Dile que lea y ejecute el archivo .agentic-setup.md!')
62
+ setup_done_hint: t('>>> Tell it to read and execute the .agentic-setup.md file!', '>>> Diga a ele para ler e executar o arquivo .agentic-setup.md!', '>>> Dile que lea y ejecute el archivo .agentic-setup.md!'),
63
+ update_title: t('agentic-pdlc — Agent Configuration Status', 'agentic-pdlc — Status de Configuração dos Agentes', 'agentic-pdlc — Estado de Configuración de Agentes'),
64
+ update_no_context: t('❌ No .agentic-pdlc/cli-context.json found. Run npx create-agentic-pdlc first.', '❌ Arquivo .agentic-pdlc/cli-context.json não encontrado. Rode npx create-agentic-pdlc primeiro.', '❌ Archivo .agentic-pdlc/cli-context.json no encontrado. Ejecuta npx create-agentic-pdlc primero.'),
65
+ update_all_ok: t('All agents configured!', 'Todos os agentes configurados!', '¡Todos los agentes configurados!'),
66
+ update_ask_configure: t('Configure missing agents? (Y/n): ', 'Configurar agentes faltantes? (S/n): ', '¿Configurar agentes faltantes? (S/n): '),
67
+ update_skipped: t('Skipped.', 'Pulado.', 'Omitido.'),
68
+ update_jules_header: t('— Jules (autonomous implementation agent) —', '— Jules (agente de implementação autônomo) —', '— Jules (agente de implementación autónomo) —'),
69
+ update_jules_ask: t(' Which agent? (a) @google-labs-jules (b) Other (c) Skip: ', ' Qual agente? (a) @google-labs-jules (b) Outro (c) Pular: ', ' ¿Qué agente? (a) @google-labs-jules (b) Otro (c) Omitir: '),
70
+ update_jules_ask_handle: t(' Agent handle (e.g. @my-agent): ', ' Handle do agente (ex: @meu-agente): ', ' Handle del agente (ej: @mi-agente): '),
71
+ update_qa_header: t('— QA Agent (AC verification via Gemini — free tier) —', '— QA Agent (verificação de ACs via Gemini — free tier) —', '— QA Agent (verificación de ACs via Gemini — free tier) —'),
72
+ update_qa_ask: t(' Activate? Requires GEMINI_API_KEY secret. (Y/n): ', ' Ativar? Requer secret GEMINI_API_KEY. (S/n): ', ' ¿Activar? Requiere el secret GEMINI_API_KEY. (S/n): '),
73
+ update_sentinel_header: t('— Sentinel (architecture audit via Gemini Code Assist) —', '— Sentinel (auditoria de arquitetura via Gemini Code Assist) —', '— Sentinel (auditoría de arquitectura via Gemini Code Assist) —'),
74
+ update_sentinel_ask: t(' Activate? Requires Gemini Code Assist CI job. (Y/n): ', ' Ativar? Requer CI job do Gemini Code Assist. (S/n): ', ' ¿Activar? Requiere CI job de Gemini Code Assist. (S/n): '),
63
75
  };
64
76
 
65
77
  const cyan = '\x1b[36m';
@@ -159,6 +171,7 @@ async function runSetup() {
159
171
  { name: 'architecture-violation', color: 'd93f0b', description: 'Invariant violation detected by CI' },
160
172
  { name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
161
173
  { name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
174
+ { name: 'infra:qa-broken', color: 'F97316', description: 'QA Agent failed to run — manual review required' },
162
175
  { name: 'jules', color: '5319e7', description: 'Jules AI Agent' }
163
176
  ];
164
177
 
@@ -372,4 +385,189 @@ async function runSetup() {
372
385
  rl.close();
373
386
  }
374
387
 
375
- runSetup();
388
+ // ─── Update Mode helpers ──────────────────────────────────────────────────────
389
+
390
+ function detectAgentState(dir) {
391
+ const state = { jules: false, julesHandle: null, qaAgent: false, sentinel: false };
392
+
393
+ const atPath = path.join(dir, '.github', 'workflows', 'agent-trigger.yml');
394
+ if (fs.existsSync(atPath)) {
395
+ const content = fs.readFileSync(atPath, 'utf8');
396
+ if (!content.includes('{{AGENT_HANDLE}}') && !content.includes('{{IMPLEMENTATION_AGENT_LABEL}}')) {
397
+ const match = content.match(/(@[\w-]+)/);
398
+ if (match) { state.jules = true; state.julesHandle = match[1]; }
399
+ }
400
+ }
401
+
402
+ const paPath = path.join(dir, '.github', 'workflows', 'project-automation.yml');
403
+ if (fs.existsSync(paPath)) {
404
+ const content = fs.readFileSync(paPath, 'utf8');
405
+ state.qaAgent = /^ move-card-on-qa-pass:/m.test(content);
406
+ state.sentinel = /^ move-violation-to-board:/m.test(content);
407
+ }
408
+
409
+ return state;
410
+ }
411
+
412
+ function uncommentYamlJob(content, jobCommentedLine) {
413
+ if (!content.includes(jobCommentedLine)) return content;
414
+ const lines = content.split('\n');
415
+ const output = [];
416
+ let state = 'before';
417
+
418
+ for (const line of lines) {
419
+ if (state === 'before') {
420
+ if (line === jobCommentedLine) {
421
+ state = 'in-job';
422
+ output.push(line.replace(/^(\s{2})# ?/, '$1'));
423
+ } else if (/^\s{2}# (OPTIONAL:|When )/.test(line)) {
424
+ state = 'in-preamble';
425
+ } else {
426
+ output.push(line);
427
+ }
428
+ } else if (state === 'in-preamble') {
429
+ if (line === jobCommentedLine) {
430
+ state = 'in-job';
431
+ output.push(line.replace(/^(\s{2})# ?/, '$1'));
432
+ }
433
+ } else if (state === 'in-job') {
434
+ if (/^\s{2}#/.test(line)) {
435
+ output.push(line.replace(/^(\s{2})# ?/, '$1'));
436
+ } else {
437
+ state = 'after';
438
+ output.push(line);
439
+ }
440
+ } else {
441
+ output.push(line);
442
+ }
443
+ }
444
+
445
+ return output.join('\n');
446
+ }
447
+
448
+ function activateQaAgent(paPath) {
449
+ let content = fs.readFileSync(paPath, 'utf8');
450
+ content = uncommentYamlJob(content, ' # move-card-on-qa-pass:');
451
+
452
+ // Change STATUS_CODE_REVIEW_PR → STATUS_TESTING in move-card-on-pr-open only
453
+ const variantBIdx = content.indexOf('# 💡 VARIANT B');
454
+ if (variantBIdx !== -1) {
455
+ const before = content.slice(0, variantBIdx);
456
+ const after = content.slice(variantBIdx).replace('process.env.STATUS_CODE_REVIEW_PR', () => 'process.env.STATUS_TESTING');
457
+ content = before + after;
458
+ }
459
+
460
+ fs.writeFileSync(paPath, content, 'utf8');
461
+ }
462
+
463
+ function activateSentinel(paPath) {
464
+ let content = fs.readFileSync(paPath, 'utf8');
465
+ content = uncommentYamlJob(content, ' # move-violation-to-board:');
466
+ fs.writeFileSync(paPath, content, 'utf8');
467
+ }
468
+
469
+ function configureJules(atPath, handle, label) {
470
+ let content = fs.readFileSync(atPath, 'utf8');
471
+ const name = handle.replace('@', '');
472
+ content = content.replace(/\{\{IMPLEMENTATION_AGENT_NAME\}\}/g, () => name);
473
+ content = content.replace(/\{\{IMPLEMENTATION_AGENT_LABEL\}\}/g, () => label);
474
+ content = content.replace(/\{\{AGENT_HANDLE\}\}/g, () => handle);
475
+ fs.writeFileSync(atPath, content, 'utf8');
476
+ }
477
+
478
+ async function runUpdate() {
479
+ const contextPath = path.join(targetDir, '.agentic-pdlc', 'cli-context.json');
480
+ if (!fs.existsSync(contextPath)) {
481
+ console.error(`\n${red}${i18n.update_no_context}${reset}\n`);
482
+ rl.close();
483
+ process.exit(1);
484
+ }
485
+
486
+ const state = detectAgentState(targetDir);
487
+ const sep = '─'.repeat(55);
488
+
489
+ console.log(`\n${cyan}${sep}${reset}`);
490
+ console.log(`${cyan} ${i18n.update_title}${reset}`);
491
+ console.log(`${cyan}${sep}${reset}\n`);
492
+
493
+ const julesSuffix = state.julesHandle ? ` (${state.julesHandle})` : '';
494
+ console.log(` ${state.jules ? green + '✅' : red + '❌'} Jules${julesSuffix} — ${state.jules ? t('configured','configurado','configurado') : t('not configured','não configurado','no configurado')}${reset}`);
495
+ console.log(` ${state.qaAgent ? green + '✅' : red + '❌'} QA Agent — ${state.qaAgent ? t('active (Variant B)','ativo (Variant B)','activo (Variant B)') : t('not active (Variant A)','não ativo (Variant A)','no activo (Variant A)')}${reset}`);
496
+ console.log(` ${state.sentinel ? green + '✅' : red + '❌'} Sentinel — ${state.sentinel ? t('active','ativo','activo') : t('not configured','não configurado','no configurado')}${reset}`);
497
+
498
+ if (state.jules && state.qaAgent && state.sentinel) {
499
+ console.log(`\n${green}${i18n.update_all_ok}${reset}\n`);
500
+ rl.close();
501
+ return;
502
+ }
503
+
504
+ console.log(`\n${cyan}${sep}${reset}`);
505
+ const configureAnswer = await askQuestion(i18n.update_ask_configure);
506
+ const shouldConfigure = !['n', 'no', 'não', 'nao'].includes(configureAnswer.trim().toLowerCase());
507
+
508
+ if (!shouldConfigure) {
509
+ console.log(`\n${i18n.update_skipped}\n`);
510
+ rl.close();
511
+ return;
512
+ }
513
+
514
+ const paPath = path.join(targetDir, '.github', 'workflows', 'project-automation.yml');
515
+ const atPath = path.join(targetDir, '.github', 'workflows', 'agent-trigger.yml');
516
+ const results = [];
517
+
518
+ if (!state.jules && fs.existsSync(atPath)) {
519
+ console.log(`\n${cyan}${i18n.update_jules_header}${reset}`);
520
+ const choice = (await askQuestion(i18n.update_jules_ask)).trim().toLowerCase();
521
+ if (choice === 'a' || choice === '') {
522
+ configureJules(atPath, '@google-labs-jules', 'jules');
523
+ results.push(t('✅ Jules configured (@google-labs-jules)', '✅ Jules configurado (@google-labs-jules)', '✅ Jules configurado (@google-labs-jules)'));
524
+ } else if (choice === 'b') {
525
+ const handle = (await askQuestion(i18n.update_jules_ask_handle)).trim();
526
+ configureJules(atPath, handle, handle.replace('@', '').toLowerCase());
527
+ results.push(t(`✅ Agent configured (${handle})`, `✅ Agente configurado (${handle})`, `✅ Agente configurado (${handle})`));
528
+ } else {
529
+ results.push(t('⏭ Jules — skipped', '⏭ Jules — pulado', '⏭ Jules — omitido'));
530
+ }
531
+ }
532
+
533
+ if (!state.qaAgent && fs.existsSync(paPath)) {
534
+ console.log(`\n${cyan}${i18n.update_qa_header}${reset}`);
535
+ const answer = (await askQuestion(i18n.update_qa_ask)).trim().toLowerCase();
536
+ if (!['n', 'no', 'não', 'nao'].includes(answer)) {
537
+ activateQaAgent(paPath);
538
+ results.push(t(
539
+ '✅ QA Agent configured — Variant B activated\n Next: gh secret set GEMINI_API_KEY --body "<your-key>"',
540
+ '✅ QA Agent configurado — Variant B ativado\n Próximo: gh secret set GEMINI_API_KEY --body "<sua-chave>"',
541
+ '✅ QA Agent configurado — Variant B activado\n Siguiente: gh secret set GEMINI_API_KEY --body "<tu-clave>"'
542
+ ));
543
+ } else {
544
+ results.push(t('⏭ QA Agent — skipped', '⏭ QA Agent — pulado', '⏭ QA Agent — omitido'));
545
+ }
546
+ }
547
+
548
+ if (!state.sentinel && fs.existsSync(paPath)) {
549
+ console.log(`\n${cyan}${i18n.update_sentinel_header}${reset}`);
550
+ const answer = (await askQuestion(i18n.update_sentinel_ask)).trim().toLowerCase();
551
+ if (!['n', 'no', 'não', 'nao'].includes(answer)) {
552
+ activateSentinel(paPath);
553
+ results.push(t('✅ Sentinel configured', '✅ Sentinel configurado', '✅ Sentinel configurado'));
554
+ } else {
555
+ results.push(t('⏭ Sentinel — skipped', '⏭ Sentinel — pulado', '⏭ Sentinel — omitido'));
556
+ }
557
+ }
558
+
559
+ console.log(`\n${cyan}${sep}${reset}`);
560
+ for (const r of results) console.log(` ${r}`);
561
+ console.log(`${cyan}${sep}${reset}\n`);
562
+
563
+ rl.close();
564
+ }
565
+
566
+ // ─── Entry point ──────────────────────────────────────────────────────────────
567
+
568
+ const args = process.argv.slice(2);
569
+ if (args.includes('--update')) {
570
+ runUpdate().catch(err => { console.error(err.message); rl.close(); process.exit(1); });
571
+ } else {
572
+ runSetup().catch(err => { console.error(err.message); rl.close(); process.exit(1); });
573
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agentic-pdlc",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "Agentic PDLC Framework - Conversational setup for your AI coding assistants",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -2,7 +2,7 @@ name: PDLC Board Automation
2
2
 
3
3
  on:
4
4
  pull_request:
5
- types: [opened, reopened, closed]
5
+ types: [opened, reopened, closed, labeled]
6
6
  pull_request_review:
7
7
  types: [submitted]
8
8
  issues:
@@ -189,6 +189,52 @@ jobs:
189
189
  try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: 'pr:in-review' }); } catch {}
190
190
  await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:approved'] }).catch(() => {});
191
191
 
192
+ # OPTIONAL: Uncomment for Variant B (QA Agent enabled)
193
+ # When qa:approved is added to a PR, move linked issue to Code Review / PR
194
+ # move-card-on-qa-pass:
195
+ # name: qa:approved → Code Review / PR
196
+ # if: github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'qa:approved'
197
+ # runs-on: ubuntu-latest
198
+ # env:
199
+ # PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
200
+ # steps:
201
+ # - name: Move linked issue to Code Review / PR
202
+ # if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
203
+ # uses: actions/github-script@v7
204
+ # with:
205
+ # github-token: ${{ env.PROJECT_PAT }}
206
+ # script: |
207
+ # const prNumber = context.payload.pull_request.number;
208
+ # const { owner, repo } = context.repo;
209
+ # const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
210
+ # const body = pr.body ?? '';
211
+ # const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)].map(m => parseInt(m[1]));
212
+ # const moveItem = async (nodeId) => {
213
+ # const { addProjectV2ItemById: { item } } = await github.graphql(`
214
+ # mutation($p: ID!, $c: ID!) {
215
+ # addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
216
+ # }`, { p: process.env.PROJECT_ID, c: nodeId });
217
+ # await github.graphql(`
218
+ # mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
219
+ # updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
220
+ # projectV2Item { id }
221
+ # }
222
+ # }`, {
223
+ # p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
224
+ # v: { singleSelectOptionId: process.env.STATUS_CODE_REVIEW_PR }
225
+ # });
226
+ # };
227
+ # if (linkedIssues.length > 0) {
228
+ # for (const n of linkedIssues) {
229
+ # const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
230
+ # await moveItem(issue.node_id);
231
+ # console.log(`Issue #${n} → Code Review / PR`);
232
+ # }
233
+ # } else {
234
+ # await moveItem(pr.node_id);
235
+ # console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
236
+ # }
237
+
192
238
  # PR Merged → Production
193
239
  move-card-on-pr-merge:
194
240
  name: Merged PR → Production
@@ -6,22 +6,73 @@ on:
6
6
  permissions:
7
7
  pull-requests: write
8
8
  contents: read
9
+ issues: read
9
10
 
10
11
  jobs:
11
12
  qa:
12
- name: Run AI QA Agent
13
+ name: AC Coverage Verification (Gemini)
13
14
  runs-on: ubuntu-latest
14
15
  steps:
15
16
  - uses: actions/checkout@v4
16
- - name: Execute QA Tests
17
- run: |
18
- echo "Run your QA Agent here."
19
- echo "This could be QAWolf, a custom LLM script, or a secondary agent."
20
- echo "If tests pass: gh pr edit $PR_URL --add-label 'qa:pass'"
21
- echo "If tests fail: gh pr edit $PR_URL --add-label 'qa:fail'"
22
-
23
- # Example success:
24
- # gh pr edit ${{ github.event.pull_request.html_url }} --add-label "qa:pass"
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - name: Verify AC Coverage via Gemini
25
21
  env:
26
22
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27
- PR_URL: ${{ github.event.pull_request.html_url }}
23
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
24
+ run: |
25
+ set -e
26
+
27
+ PR_NUMBER="${{ github.event.pull_request.number }}"
28
+ BASE="${{ github.event.pull_request.base.sha }}"
29
+ HEAD="${{ github.event.pull_request.head.sha }}"
30
+
31
+ # Get PR diff (truncated to 8000 chars to stay within context limits)
32
+ DIFF=$(git diff "$BASE" "$HEAD" | head -c 8000)
33
+
34
+ # Extract linked issues from PR body
35
+ PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body // ""')
36
+ ISSUE_NUMS=$(echo "$PR_BODY" | grep -oiE '(Closes?|Fixes?|Resolves?)\s+#([0-9]+)' | grep -oE '[0-9]+' || true)
37
+
38
+ # Build acceptance criteria context
39
+ AC_CONTEXT=""
40
+ if [ -n "$ISSUE_NUMS" ]; then
41
+ for NUM in $ISSUE_NUMS; do
42
+ ISSUE_BODY=$(gh issue view "$NUM" --json body --jq '.body // ""' 2>/dev/null || echo "")
43
+ AC_CONTEXT="${AC_CONTEXT}\\n\\n--- Issue #${NUM} ---\\n${ISSUE_BODY}"
44
+ done
45
+ fi
46
+
47
+ if [ -z "$AC_CONTEXT" ]; then
48
+ AC_CONTEXT="No linked issue found. Evaluate if the PR description is self-contained."
49
+ fi
50
+
51
+ # Serialize prompt as JSON string and call Gemini API (30s timeout)
52
+ 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\nRespond with exactly one word: PASS or FAIL, then on the next line explain briefly why (max 3 sentences)." | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
53
+
54
+ RESPONSE=$(curl -s -X POST \
55
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}" \
56
+ -H "Content-Type: application/json" \
57
+ -d "{\"contents\":[{\"parts\":[{\"text\":${PROMPT_JSON}}]}]}" \
58
+ --max-time 30 || echo "API_ERROR")
59
+
60
+ if [ "$RESPONSE" = "API_ERROR" ]; then
61
+ gh pr edit "$PR_NUMBER" --add-label "infra:qa-broken"
62
+ gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** Could not reach Gemini API. Manual review required."
63
+ exit 0
64
+ fi
65
+
66
+ VERDICT=$(echo "$RESPONSE" | python3 -c 'import json,sys; d=json.load(sys.stdin); t=d.get("candidates",[{}])[0].get("content",{}).get("parts",[{}])[0].get("text","").strip(); print(t.split("\n")[0].upper() if t else "API_ERROR")')
67
+ EXPLANATION=$(echo "$RESPONSE" | python3 -c 'import json,sys; d=json.load(sys.stdin); t=d.get("candidates",[{}])[0].get("content",{}).get("parts",[{}])[0].get("text","").strip(); lines=t.split("\n",1); print(lines[1].strip() if len(lines)>1 else "")')
68
+
69
+ if echo "$VERDICT" | grep -q "^PASS"; then
70
+ gh pr edit "$PR_NUMBER" --add-label "qa:approved"
71
+ gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** AC coverage verified. ${EXPLANATION}"
72
+ elif echo "$VERDICT" | grep -q "^FAIL"; then
73
+ gh pr edit "$PR_NUMBER" --add-label "qa:needs-work"
74
+ gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** AC coverage insufficient. ${EXPLANATION}"
75
+ else
76
+ gh pr edit "$PR_NUMBER" --add-label "infra:qa-broken"
77
+ gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** Could not parse Gemini response. Manual review required."
78
+ fi
@@ -70,6 +70,7 @@ When detailing a solution in an issue body, you must **always** include both the
70
70
  - Never open a PR without passing the tests.
71
71
  - Never implement beyond the immediate scope of the issue.
72
72
  - Never create future-proofing abstractions for hypothetical features.
73
+ - Never add or remove `stage:*` or `qa:*` labels manually. These are owned by GitHub Actions automation and the PM only.
73
74
  {{EXTRA_DONT}}
74
75
 
75
76
  ## Project Standards
@@ -22,7 +22,7 @@ Adapt columns as needed. The functional baseline is:
22
22
  ## Workflow Variants (QA Agent)
23
23
 
24
24
  - **Variant A (Default):** PRs bypass the `Testing` column and land directly in `Code Review / PR`.
25
- - **Variant B (QA Agent Enabled):** PRs land in the `Testing` column first. An AI QA agent verifies the PR, adding `qa:pass` or `qa:fail`. Only after a `qa:pass` is the issue moved to `Code Review / PR`.
25
+ - **Variant B (QA Agent Enabled):** PRs land in the `Testing` column first. An AI QA agent verifies the PR, adding `qa:approved` or `qa:needs-work`. Only after a `qa:approved` is the issue moved to `Code Review / PR`.
26
26
 
27
27
  ## Board Identifiers (GitHub Projects)
28
28
 
@@ -77,6 +77,9 @@ REPO = {{REPO_OWNER}}/{{REPO_NAME}}
77
77
  | `spec:approved` | Issue | Green | Gate 2 — agent is cleared to implement |
78
78
  | `pr:in-review` | PR | Yellow | Awaiting code review |
79
79
  | `pr:approved` | PR | Green | Code review approved |
80
+ | `qa:approved` | PR | Green | QA Agent passed — AC coverage verified |
81
+ | `qa:needs-work` | PR | Red | QA Agent failed — PR needs changes |
82
+ | `infra:qa-broken` | PR | Orange | QA Agent error — manual review required |
80
83
 
81
84
  ## Approval Gates
82
85