create-agentic-pdlc 3.0.0 → 3.1.1

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/bin/cli.js CHANGED
@@ -60,6 +60,7 @@ const i18n = {
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
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
+ upgrade_hint: t('💡 To add the full board + multi-agent automation later: npx create-agentic-pdlc --upgrade-to-agentic', '💡 Para adicionar o board completo + automação multi-agente mais tarde: npx create-agentic-pdlc --upgrade-to-agentic', '💡 Para agregar el tablero completo + automatización multi-agente más tarde: npx create-agentic-pdlc --upgrade-to-agentic'),
63
64
  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
65
  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
66
  update_all_ok: t('All agents configured!', 'Todos os agentes configurados!', '¡Todos los agentes configurados!'),
@@ -74,12 +75,36 @@ const i18n = {
74
75
  update_sentinel_ask: t(" Activate? CodeRabbit applies 'architecture-violation' label when violations are detected. (Y/n): ", " Ativar? CodeRabbit aplica a label 'architecture-violation' quando detecta violações. (S/n): ", " ¿Activar? CodeRabbit aplica la etiqueta 'architecture-violation' cuando detecta violaciones. (S/n): "),
75
76
  configuring_protection: t('[3/3] Configuring branch protection...', '[3/3] Configurando proteção de branch...', '[3/3] Configurando protección de rama...'),
76
77
  protection_ok: t('✅ Branch protection set — required checks: PDLC Stage Gate, QA Gate.', '✅ Proteção de branch configurada — checks obrigatórios: PDLC Stage Gate, QA Gate.', '✅ Protección de rama configurada — checks requeridos: PDLC Stage Gate, QA Gate.'),
77
- protection_warn: t('⚠️ Branch protection could not be set automatically.\n Set required checks manually: Settings → Branches → main → Required status checks.\n Required: "PDLC Stage Gate" and "QA Gate"', '⚠️ Proteção de branch não pôde ser configurada automaticamente.\n Configure manualmente: Settings → Branches → main → Required status checks.\n Obrigatórios: "PDLC Stage Gate" e "QA Gate"', '⚠️ No se pudo configurar la protección de rama automáticamente.\n Configúralo en: Settings → Branches → main → Required status checks.\n Requeridos: "PDLC Stage Gate" y "QA Gate"'),
78
+ protection_warn: t(
79
+ '⚠️ Branch protection requires admin access — could not be set automatically.\n\n What it does: prevents PRs from merging unless these CI checks pass:\n • "PDLC Stage Gate" — blocks merge if the linked issue lacks spec:approved\n • "QA Gate" — blocks merge if automated QA checks failed\n\n Without it: the workflow still runs, but PRs can be merged without approval.\n\n To enable: Settings → Branches → main → Required status checks\n Add: "PDLC Stage Gate" and "QA Gate"',
80
+ '⚠️ Proteção de branch requer acesso de admin — não pôde ser configurada automaticamente.\n\n O que faz: impede que PRs sejam mergeados sem que esses checks de CI passem:\n • "PDLC Stage Gate" — bloqueia merge se a issue não tiver spec:approved\n • "QA Gate" — bloqueia merge se os checks automáticos de QA falharem\n\n Sem ela: o workflow continua funcionando, mas PRs podem ser mergeados sem aprovação.\n\n Para ativar: Settings → Branches → main → Required status checks\n Adicionar: "PDLC Stage Gate" e "QA Gate"',
81
+ '⚠️ La protección de rama requiere acceso de administrador — no se pudo configurar automáticamente.\n\n Qué hace: impide que los PRs se fusionen sin que pasen estos checks de CI:\n • "PDLC Stage Gate" — bloquea el merge si la issue no tiene spec:approved\n • "QA Gate" — bloquea el merge si los checks automáticos de QA fallaron\n\n Sin ella: el flujo sigue funcionando, pero los PRs se pueden fusionar sin aprobación.\n\n Para activar: Settings → Branches → main → Required status checks\n Agregar: "PDLC Stage Gate" y "QA Gate"'
82
+ ),
78
83
  issue_templates_copied: t(
79
84
  '✅ Issue templates copied to .github/ISSUE_TEMPLATE/',
80
85
  '✅ Issue templates copiados para .github/ISSUE_TEMPLATE/',
81
86
  '✅ Issue templates copiados a .github/ISSUE_TEMPLATE/'
82
87
  ),
88
+ vars_project_id_ok: t(
89
+ '✅ vars.PROJECT_ID set as Actions Variable.',
90
+ '✅ vars.PROJECT_ID configurado como Variável do Actions.',
91
+ '✅ vars.PROJECT_ID configurado como Variable de Actions.'
92
+ ),
93
+ vars_project_id_warn: t(
94
+ '⚠️ Could not set vars.PROJECT_ID — token may lack variables:write scope.\n Set manually: repo Settings → Secrets and variables → Variables → PROJECT_ID = ',
95
+ '⚠️ Não foi possível configurar vars.PROJECT_ID — o token pode não ter permissão variables:write.\n Configure manualmente: repo Settings → Secrets and variables → Variables → PROJECT_ID = ',
96
+ '⚠️ No se pudo configurar vars.PROJECT_ID — el token puede no tener permiso variables:write.\n Configura manualmente: repo Settings → Secrets and variables → Variables → PROJECT_ID = '
97
+ ),
98
+ gh_not_installed: t(
99
+ '\n⚠️ GitHub CLI (gh) is not installed. Skipping label creation.',
100
+ '\n⚠️ GitHub CLI (gh) não está instalado. Pulando criação de labels.',
101
+ '\n⚠️ GitHub CLI (gh) no está instalado. Omitiendo creación de etiquetas.'
102
+ ),
103
+ repo_context_missing: t(
104
+ '\n⚠️ Repository owner or name missing in cli-context.json. Automatic label creation will be skipped.',
105
+ '\n⚠️ Dono ou nome do repositório ausente em cli-context.json. Criação automática de labels será pulada.',
106
+ '\n⚠️ Propietario o nombre del repositorio faltante en cli-context.json. Se omitirá la creación automática de etiquetas.'
107
+ ),
83
108
  };
84
109
 
85
110
  const cyan = '\x1b[36m';
@@ -89,19 +114,23 @@ const yellow = '\x1b[33m';
89
114
  const red = '\x1b[31m';
90
115
 
91
116
  const PDLC_LABELS = [
92
- { name: 'stage:exploration', color: '9b59b6', description: 'Issue is being evaluated' },
93
117
  { name: 'stage:brainstorming', color: 'e84393', description: 'Proposed approaches awaiting PM gate' },
94
118
  { name: 'stage:detailing', color: '3498db', description: 'Technical spec is being written' },
95
119
  { name: 'stage:development', color: 'e67e22', description: 'Agent is implementing the spec' },
96
- { name: 'stage:testing', color: '8e44ad', description: 'Agent is testing the implementation' },
97
120
  { name: 'spec:approved', color: '0e8a16', description: 'Spec approved — agent can implement' },
98
121
  { name: 'pr:in-review', color: 'e4e669', description: 'PR awaiting code review' },
99
122
  { name: 'pr:approved', color: '0e8a16', description: 'PR approved, ready for merge' },
100
123
  { name: 'architecture-violation', color: 'd93f0b', description: 'Invariant violation detected by CI' },
101
- { name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
102
- { name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
103
- { name: 'infra:qa-broken', color: 'F97316', description: 'QA Agent failed to run — manual review required' },
104
- { name: 'jules', color: '5319e7', description: 'Jules AI Agent' }
124
+ ];
125
+
126
+ const JULES_LABELS = [
127
+ { name: 'jules', color: '5319e7', description: 'Jules AI Agent' },
128
+ ];
129
+
130
+ const QA_LABELS = [
131
+ { name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
132
+ { name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
133
+ { name: 'infra:qa-broken',color: 'F97316', description: 'QA Agent failed to run — manual review required' },
105
134
  ];
106
135
 
107
136
  function buildBoardUrl(repoOwner, projectNumber, isOrg) {
@@ -282,6 +311,42 @@ function scaffoldLiteTemplates(sourceDir, targetDir) {
282
311
  }
283
312
  }
284
313
 
314
+ function createLabelsForRepo(labels, repo) {
315
+ for (const label of labels) {
316
+ try {
317
+ execFileSync('gh', ['label', 'create', label.name, '--color', label.color, '--description', label.description, '--repo', repo, '--force'], { stdio: 'ignore' });
318
+ } catch (err) {
319
+ if (err.code === 'ENOENT') {
320
+ console.warn(i18n.gh_not_installed);
321
+ break;
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ function setActionsVariable(repo, name, value, execFn = execFileSync) {
328
+ try {
329
+ execFn('gh', [
330
+ 'api', `repos/${repo}/actions/variables/${name}`,
331
+ '--method', 'PATCH',
332
+ '-f', `name=${name}`,
333
+ '-f', `value=${value}`
334
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
335
+ } catch (err) {
336
+ const msg = (err.stderr?.toString() || '') + (err.message || '');
337
+ if (msg.includes('404') || msg.includes('Not Found')) {
338
+ execFn('gh', [
339
+ 'api', `repos/${repo}/actions/variables`,
340
+ '--method', 'POST',
341
+ '-f', `name=${name}`,
342
+ '-f', `value=${value}`
343
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
344
+ } else {
345
+ throw err;
346
+ }
347
+ }
348
+ }
349
+
285
350
  function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName) {
286
351
  const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
287
352
  fs.mkdirSync(destTemplates, { recursive: true });
@@ -342,7 +407,6 @@ function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, o
342
407
  const paPath = path.join(destTemplates, '.github', 'workflows', 'project-automation.yml');
343
408
  if (fs.existsSync(paPath) && Object.keys(optionMap).length > 0) {
344
409
  let wfContent = fs.readFileSync(paPath, 'utf8');
345
- if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
346
410
  if (statusFieldId) wfContent = wfContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
347
411
  wfContent = wfContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
348
412
  wfContent = wfContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
@@ -536,21 +600,31 @@ async function runFullSetup() {
536
600
  }
537
601
  }
538
602
 
539
- // Auto-provision PROJECT_PAT for personal repos
603
+ // Auto-provision PROJECT_TOKEN for personal repos
540
604
  let patAutoSet = false;
541
605
  if (projectId && !isOrg) {
542
606
  try {
543
607
  const tokenOut = execFileSync('gh', ['auth', 'token'], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
544
608
  if (tokenOut) {
545
- execFileSync('gh', ['secret', 'set', 'PROJECT_PAT', '--body', tokenOut, '--repo', repo], { stdio: ['ignore', 'pipe', 'pipe'] });
609
+ execFileSync('gh', ['secret', 'set', 'PROJECT_TOKEN', '--body', tokenOut, '--repo', repo], { stdio: ['ignore', 'pipe', 'pipe'] });
546
610
  patAutoSet = true;
547
- console.log(`\n${green}✅ PROJECT_PAT secret set automatically (uses your gh OAuth token).${reset}`);
611
+ console.log(`\n${green}✅ PROJECT_TOKEN secret set automatically (uses your gh OAuth token).${reset}`);
548
612
  }
549
613
  } catch (err) {
550
- console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_PAT. Agent will guide manual setup.${reset}`);
614
+ console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_TOKEN. Agent will guide manual setup.${reset}`);
551
615
  }
552
616
  } else if (projectId && isOrg) {
553
- console.log(`\n${yellow}ℹ️ Org repo detected — PROJECT_PAT will require manual setup for security.${reset}`);
617
+ console.log(`\n${yellow}ℹ️ Org repo detected — PROJECT_TOKEN will require manual setup for security.${reset}`);
618
+ }
619
+
620
+ // Set PROJECT_ID as GitHub Actions Variable
621
+ if (projectId) {
622
+ try {
623
+ setActionsVariable(repo, 'PROJECT_ID', projectId);
624
+ console.log(`${green}${i18n.vars_project_id_ok}${reset}`);
625
+ } catch (_) {
626
+ console.log(`${yellow}${i18n.vars_project_id_warn}${projectId}${reset}`);
627
+ }
554
628
  }
555
629
 
556
630
  await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
@@ -710,6 +784,10 @@ async function runUpdate() {
710
784
  return;
711
785
  }
712
786
 
787
+ const repo = ctx.repoOwner && ctx.repoName ? `${ctx.repoOwner}/${ctx.repoName}` : null;
788
+ if (!repo) {
789
+ console.warn(`${yellow}${i18n.repo_context_missing}${reset}`);
790
+ }
713
791
  const paPath = path.join(targetDir, '.github', 'workflows', 'project-automation.yml');
714
792
  const atPath = path.join(targetDir, '.github', 'workflows', 'agent-trigger.yml');
715
793
  const results = [];
@@ -718,11 +796,14 @@ async function runUpdate() {
718
796
  console.log(`\n${cyan}${i18n.update_jules_header}${reset}`);
719
797
  const choice = (await askQuestion(i18n.update_jules_ask)).trim().toLowerCase();
720
798
  if (choice === 'a' || choice === '') {
799
+ if (repo) createLabelsForRepo(JULES_LABELS, repo);
721
800
  configureJules(atPath, '@google-labs-jules', 'jules');
722
801
  results.push(t('✅ Jules configured (@google-labs-jules)', '✅ Jules configurado (@google-labs-jules)', '✅ Jules configurado (@google-labs-jules)'));
723
802
  } else if (choice === 'b') {
724
803
  const handle = (await askQuestion(i18n.update_jules_ask_handle)).trim();
725
- configureJules(atPath, handle, handle.replace('@', '').toLowerCase());
804
+ const labelName = handle.replace('@', '').toLowerCase();
805
+ if (repo) createLabelsForRepo([{ name: labelName, color: '5319e7', description: `${handle} AI Agent` }], repo);
806
+ configureJules(atPath, handle, labelName);
726
807
  results.push(t(`✅ Agent configured (${handle})`, `✅ Agente configurado (${handle})`, `✅ Agente configurado (${handle})`));
727
808
  } else {
728
809
  results.push(t('⏭ Jules — skipped', '⏭ Jules — pulado', '⏭ Jules — omitido'));
@@ -733,6 +814,7 @@ async function runUpdate() {
733
814
  console.log(`\n${cyan}${i18n.update_qa_header}${reset}`);
734
815
  const answer = (await askQuestion(i18n.update_qa_ask)).trim().toLowerCase();
735
816
  if (!['n', 'no', 'não', 'nao'].includes(answer)) {
817
+ if (repo) createLabelsForRepo(QA_LABELS, repo);
736
818
  activateQaAgent(paPath);
737
819
  results.push(t(
738
820
  '✅ QA Agent configured — Variant B activated (uses GITHUB_TOKEN, no extra secrets needed)',
@@ -790,7 +872,7 @@ function resolveMode(args) {
790
872
  }
791
873
 
792
874
  // Export for testing
793
- if (typeof module !== 'undefined') module.exports = { resolveMode };
875
+ if (typeof module !== 'undefined') module.exports = { resolveMode, setActionsVariable, scaffoldLiteTemplates, scaffoldFullTemplates };
794
876
 
795
877
  // ─── runLiteSetup ─────────────────────────────────────────────────────────────
796
878
 
@@ -849,6 +931,7 @@ async function runLiteSetup() {
849
931
  });
850
932
 
851
933
  copyAdapterFiles(agent, sourceDir, targetDir);
934
+ console.log(`${cyan}${i18n.upgrade_hint}${reset}`);
852
935
 
853
936
  rl.close();
854
937
  }
@@ -988,20 +1071,30 @@ async function runUpgradeToAgentic() {
988
1071
  }
989
1072
  }
990
1073
 
991
- // Auto-provision PROJECT_PAT for personal repos
1074
+ // Auto-provision PROJECT_TOKEN for personal repos
992
1075
  let patAutoSet = false;
993
1076
  if (projectId && !isOrg) {
994
1077
  try {
995
1078
  const tokenOut = execFileSync('gh', ['auth', 'token'],
996
1079
  { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
997
1080
  if (tokenOut) {
998
- execFileSync('gh', ['secret', 'set', 'PROJECT_PAT', '--body', tokenOut, '--repo', repo],
1081
+ execFileSync('gh', ['secret', 'set', 'PROJECT_TOKEN', '--body', tokenOut, '--repo', repo],
999
1082
  { stdio: ['ignore', 'pipe', 'pipe'] });
1000
1083
  patAutoSet = true;
1001
- console.log(`\n${green}✅ PROJECT_PAT secret set automatically (uses your gh OAuth token).${reset}`);
1084
+ console.log(`\n${green}✅ PROJECT_TOKEN secret set automatically (uses your gh OAuth token).${reset}`);
1002
1085
  }
1003
1086
  } catch (_) {
1004
- console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_PAT. Agent will guide manual setup.${reset}`);
1087
+ console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_TOKEN. Agent will guide manual setup.${reset}`);
1088
+ }
1089
+ }
1090
+
1091
+ // Set PROJECT_ID as GitHub Actions Variable
1092
+ if (projectId) {
1093
+ try {
1094
+ setActionsVariable(repo, 'PROJECT_ID', projectId);
1095
+ console.log(`${green}${i18n.vars_project_id_ok}${reset}`);
1096
+ } catch (_) {
1097
+ console.log(`${yellow}${i18n.vars_project_id_warn}${projectId}${reset}`);
1005
1098
  }
1006
1099
  }
1007
1100
 
@@ -0,0 +1,105 @@
1
+ # Archive Board Card on Issue Close Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Archive the GitHub Project board card when an issue is closed, preventing stale cards from accumulating in active columns.
6
+
7
+ **Architecture:** Single new job appended to `project-automation.yml` template. Triggers on the already-present `issues: closed` event. Uses `addProjectV2ItemById` (idempotent) then `archiveProjectV2Item` — handles both issues closed without a PR and issues closed via PR merge safely.
8
+
9
+ **Tech Stack:** GitHub Actions, `actions/github-script@v8`, GitHub GraphQL API (`addProjectV2ItemById`, `archiveProjectV2Item`).
10
+
11
+ ---
12
+
13
+ ## File Map
14
+
15
+ | Action | Path | Responsibility |
16
+ |---|---|---|
17
+ | Modify | `templates/.github/workflows/project-automation.yml` | Add `move-card-on-issue-close` job at end of file |
18
+
19
+ No unit tests — YAML workflow template; correctness verified by grep and manual inspection.
20
+
21
+ ---
22
+
23
+ ### Task 1: Add `move-card-on-issue-close` job
24
+
25
+ **Files:**
26
+ - Modify: `templates/.github/workflows/project-automation.yml` (append after `cleanup-labels-on-close`)
27
+
28
+ - [ ] **Step 1: Verify the insertion point**
29
+
30
+ Run:
31
+ ```bash
32
+ tail -5 templates/.github/workflows/project-automation.yml
33
+ ```
34
+
35
+ Expected: last line is `console.log(\`Issue #\${issue_number} labels cleaned up\`);` followed by closing braces. The file ends after `cleanup-labels-on-close`.
36
+
37
+ - [ ] **Step 2: Append the new job**
38
+
39
+ At the end of `templates/.github/workflows/project-automation.yml`, add:
40
+
41
+ ```yaml
42
+
43
+ move-card-on-issue-close:
44
+ name: Closed issue → Archive from board
45
+ if: github.event_name == 'issues' && github.event.action == 'closed'
46
+ runs-on: ubuntu-latest
47
+ env:
48
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
49
+ steps:
50
+ - name: Archive board card
51
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
52
+ uses: actions/github-script@v8
53
+ with:
54
+ github-token: ${{ env.PROJECT_TOKEN }}
55
+ script: |
56
+ const nodeId = context.payload.issue.node_id;
57
+ let itemId;
58
+ try {
59
+ const added = await github.graphql(`
60
+ mutation($p: ID!, $c: ID!) {
61
+ addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
62
+ }`, { p: process.env.PROJECT_ID, c: nodeId });
63
+ itemId = added.addProjectV2ItemById.item.id;
64
+ } catch (e) {
65
+ console.log(`Could not add issue to project: ${e.message}`);
66
+ return;
67
+ }
68
+ await github.graphql(`
69
+ mutation($p: ID!, $i: ID!) {
70
+ archiveProjectV2Item(input: {projectId: $p, itemId: $i}) { item { id } }
71
+ }`, { p: process.env.PROJECT_ID, i: itemId });
72
+ console.log(`Issue #${context.payload.issue.number} archived from board`);
73
+ ```
74
+
75
+ - [ ] **Step 3: Verify job was added and file is syntactically correct**
76
+
77
+ Run:
78
+ ```bash
79
+ grep -n "move-card-on-issue-close\|archiveProjectV2Item" templates/.github/workflows/project-automation.yml
80
+ ```
81
+
82
+ Expected: two matches — the job name line and the mutation name.
83
+
84
+ Run:
85
+ ```bash
86
+ python3 -c "import yaml, sys; yaml.safe_load(open('templates/.github/workflows/project-automation.yml'))" 2>&1
87
+ ```
88
+
89
+ Expected: no output (valid YAML).
90
+
91
+ - [ ] **Step 4: Verify guard condition matches existing pattern**
92
+
93
+ Run:
94
+ ```bash
95
+ grep "PROJECT_TOKEN != ''" templates/.github/workflows/project-automation.yml
96
+ ```
97
+
98
+ Expected: multiple lines including the new job — confirms guard is consistent with other jobs.
99
+
100
+ - [ ] **Step 5: Commit**
101
+
102
+ ```bash
103
+ git add templates/.github/workflows/project-automation.yml
104
+ git commit -m "feat(templates): archive board card when issue is closed"
105
+ ```