create-agentic-pdlc 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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!'),
@@ -80,6 +81,16 @@ const i18n = {
80
81
  '✅ Issue templates copiados para .github/ISSUE_TEMPLATE/',
81
82
  '✅ Issue templates copiados a .github/ISSUE_TEMPLATE/'
82
83
  ),
84
+ vars_project_id_ok: t(
85
+ '✅ vars.PROJECT_ID set as Actions Variable.',
86
+ '✅ vars.PROJECT_ID configurado como Variável do Actions.',
87
+ '✅ vars.PROJECT_ID configurado como Variable de Actions.'
88
+ ),
89
+ vars_project_id_warn: t(
90
+ '⚠️ Could not set vars.PROJECT_ID — token may lack variables:write scope.\n Set manually: repo Settings → Secrets and variables → Variables → PROJECT_ID = ',
91
+ '⚠️ 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 = ',
92
+ '⚠️ 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 = '
93
+ ),
83
94
  };
84
95
 
85
96
  const cyan = '\x1b[36m';
@@ -282,6 +293,29 @@ function scaffoldLiteTemplates(sourceDir, targetDir) {
282
293
  }
283
294
  }
284
295
 
296
+ function setActionsVariable(repo, name, value, execFn = execFileSync) {
297
+ try {
298
+ execFn('gh', [
299
+ 'api', `repos/${repo}/actions/variables/${name}`,
300
+ '--method', 'PATCH',
301
+ '-f', `name=${name}`,
302
+ '-f', `value=${value}`
303
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
304
+ } catch (err) {
305
+ const msg = (err.stderr?.toString() || '') + (err.message || '');
306
+ if (msg.includes('404') || msg.includes('Not Found')) {
307
+ execFn('gh', [
308
+ 'api', `repos/${repo}/actions/variables`,
309
+ '--method', 'POST',
310
+ '-f', `name=${name}`,
311
+ '-f', `value=${value}`
312
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
313
+ } else {
314
+ throw err;
315
+ }
316
+ }
317
+ }
318
+
285
319
  function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName) {
286
320
  const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
287
321
  fs.mkdirSync(destTemplates, { recursive: true });
@@ -342,7 +376,6 @@ function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, o
342
376
  const paPath = path.join(destTemplates, '.github', 'workflows', 'project-automation.yml');
343
377
  if (fs.existsSync(paPath) && Object.keys(optionMap).length > 0) {
344
378
  let wfContent = fs.readFileSync(paPath, 'utf8');
345
- if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
346
379
  if (statusFieldId) wfContent = wfContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
347
380
  wfContent = wfContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
348
381
  wfContent = wfContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
@@ -536,21 +569,31 @@ async function runFullSetup() {
536
569
  }
537
570
  }
538
571
 
539
- // Auto-provision PROJECT_PAT for personal repos
572
+ // Auto-provision PROJECT_TOKEN for personal repos
540
573
  let patAutoSet = false;
541
574
  if (projectId && !isOrg) {
542
575
  try {
543
576
  const tokenOut = execFileSync('gh', ['auth', 'token'], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
544
577
  if (tokenOut) {
545
- execFileSync('gh', ['secret', 'set', 'PROJECT_PAT', '--body', tokenOut, '--repo', repo], { stdio: ['ignore', 'pipe', 'pipe'] });
578
+ execFileSync('gh', ['secret', 'set', 'PROJECT_TOKEN', '--body', tokenOut, '--repo', repo], { stdio: ['ignore', 'pipe', 'pipe'] });
546
579
  patAutoSet = true;
547
- console.log(`\n${green}✅ PROJECT_PAT secret set automatically (uses your gh OAuth token).${reset}`);
580
+ console.log(`\n${green}✅ PROJECT_TOKEN secret set automatically (uses your gh OAuth token).${reset}`);
548
581
  }
549
582
  } catch (err) {
550
- console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_PAT. Agent will guide manual setup.${reset}`);
583
+ console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_TOKEN. Agent will guide manual setup.${reset}`);
551
584
  }
552
585
  } else if (projectId && isOrg) {
553
- console.log(`\n${yellow}ℹ️ Org repo detected — PROJECT_PAT will require manual setup for security.${reset}`);
586
+ console.log(`\n${yellow}ℹ️ Org repo detected — PROJECT_TOKEN will require manual setup for security.${reset}`);
587
+ }
588
+
589
+ // Set PROJECT_ID as GitHub Actions Variable
590
+ if (projectId) {
591
+ try {
592
+ setActionsVariable(repo, 'PROJECT_ID', projectId);
593
+ console.log(`${green}${i18n.vars_project_id_ok}${reset}`);
594
+ } catch (_) {
595
+ console.log(`${yellow}${i18n.vars_project_id_warn}${projectId}${reset}`);
596
+ }
554
597
  }
555
598
 
556
599
  await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
@@ -790,7 +833,7 @@ function resolveMode(args) {
790
833
  }
791
834
 
792
835
  // Export for testing
793
- if (typeof module !== 'undefined') module.exports = { resolveMode };
836
+ if (typeof module !== 'undefined') module.exports = { resolveMode, setActionsVariable, scaffoldLiteTemplates, scaffoldFullTemplates };
794
837
 
795
838
  // ─── runLiteSetup ─────────────────────────────────────────────────────────────
796
839
 
@@ -849,6 +892,7 @@ async function runLiteSetup() {
849
892
  });
850
893
 
851
894
  copyAdapterFiles(agent, sourceDir, targetDir);
895
+ console.log(`${cyan}${i18n.upgrade_hint}${reset}`);
852
896
 
853
897
  rl.close();
854
898
  }
@@ -988,20 +1032,30 @@ async function runUpgradeToAgentic() {
988
1032
  }
989
1033
  }
990
1034
 
991
- // Auto-provision PROJECT_PAT for personal repos
1035
+ // Auto-provision PROJECT_TOKEN for personal repos
992
1036
  let patAutoSet = false;
993
1037
  if (projectId && !isOrg) {
994
1038
  try {
995
1039
  const tokenOut = execFileSync('gh', ['auth', 'token'],
996
1040
  { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
997
1041
  if (tokenOut) {
998
- execFileSync('gh', ['secret', 'set', 'PROJECT_PAT', '--body', tokenOut, '--repo', repo],
1042
+ execFileSync('gh', ['secret', 'set', 'PROJECT_TOKEN', '--body', tokenOut, '--repo', repo],
999
1043
  { stdio: ['ignore', 'pipe', 'pipe'] });
1000
1044
  patAutoSet = true;
1001
- console.log(`\n${green}✅ PROJECT_PAT secret set automatically (uses your gh OAuth token).${reset}`);
1045
+ console.log(`\n${green}✅ PROJECT_TOKEN secret set automatically (uses your gh OAuth token).${reset}`);
1002
1046
  }
1003
1047
  } catch (_) {
1004
- console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_PAT. Agent will guide manual setup.${reset}`);
1048
+ console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_TOKEN. Agent will guide manual setup.${reset}`);
1049
+ }
1050
+ }
1051
+
1052
+ // Set PROJECT_ID as GitHub Actions Variable
1053
+ if (projectId) {
1054
+ try {
1055
+ setActionsVariable(repo, 'PROJECT_ID', projectId);
1056
+ console.log(`${green}${i18n.vars_project_id_ok}${reset}`);
1057
+ } catch (_) {
1058
+ console.log(`${yellow}${i18n.vars_project_id_warn}${projectId}${reset}`);
1005
1059
  }
1006
1060
  }
1007
1061
 
@@ -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
+ ```
@@ -0,0 +1,336 @@
1
+ # PROJECT_ID as Actions Variable 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:** Replace hardcoded `PROJECT_ID: "PVT_xxx"` in installed YAML files with `${{ vars.PROJECT_ID }}`, and have the installer set the value via the GitHub Actions Variables REST API.
6
+
7
+ **Architecture:** Three workflow templates swap their `PROJECT_ID` env value and guard condition. `bin/cli.js` gains a `setActionsVariable(repo, name, value, execFn)` helper (dependency-injected `execFn` for testability) called after board creation in both the `runFullSetup` and `runUpgradeToAgentic` flows.
8
+
9
+ **Tech Stack:** Node.js 22, `node:test` + `node:assert/strict`, `gh` CLI (`execFileSync`), GitHub Actions Variables REST API (`PATCH`/`POST /repos/{owner}/{repo}/actions/variables`).
10
+
11
+ ---
12
+
13
+ ## File Map
14
+
15
+ | Action | Path | Responsibility |
16
+ |---|---|---|
17
+ | Modify | `templates/.github/workflows/project-automation.yml` | Remove `{{PROJECT_ID}}` placeholder, source from `vars` |
18
+ | Modify | `templates/.github/workflows/add-to-board.yml` | Same |
19
+ | Modify | `templates/.github/workflows/agent-trigger.yml` | Same |
20
+ | Modify | `bin/cli.js` | Add helper, wire call sites, export helper |
21
+ | Modify | `tests/cli.test.js` | Tests for `setActionsVariable` logic |
22
+
23
+ ---
24
+
25
+ ### Task 1: Update workflow templates
26
+
27
+ **Files:**
28
+ - Modify: `templates/.github/workflows/project-automation.yml`
29
+ - Modify: `templates/.github/workflows/add-to-board.yml`
30
+ - Modify: `templates/.github/workflows/agent-trigger.yml`
31
+
32
+ No tests for template content — correctness is verified by the installer integration.
33
+
34
+ - [ ] **Step 1: Replace `PROJECT_ID` env value in all three templates**
35
+
36
+ In each of the three files, find every line matching:
37
+ ```yaml
38
+ PROJECT_ID: "{{PROJECT_ID}}"
39
+ ```
40
+ Replace with:
41
+ ```yaml
42
+ PROJECT_ID: ${{ vars.PROJECT_ID }}
43
+ ```
44
+
45
+ `project-automation.yml` line 12, `add-to-board.yml` line 8, `agent-trigger.yml` line 21.
46
+
47
+ - [ ] **Step 2: Replace guard conditions in all three templates**
48
+
49
+ In each of the three files, find every line matching:
50
+ ```yaml
51
+ env.PROJECT_ID != '{{PROJECT_ID}}'
52
+ ```
53
+ Replace with:
54
+ ```yaml
55
+ env.PROJECT_ID != ''
56
+ ```
57
+
58
+ Do a full-file search in each — there are multiple occurrences per file (every job that conditionally runs).
59
+
60
+ - [ ] **Step 3: Verify no `{{PROJECT_ID}}` remains in workflow files**
61
+
62
+ Run:
63
+ ```bash
64
+ grep -rn "{{PROJECT_ID}}" templates/.github/workflows/
65
+ ```
66
+
67
+ Expected: no output. If any lines appear, fix them before proceeding.
68
+
69
+ - [ ] **Step 4: Commit**
70
+
71
+ ```bash
72
+ git add templates/.github/workflows/project-automation.yml \
73
+ templates/.github/workflows/add-to-board.yml \
74
+ templates/.github/workflows/agent-trigger.yml
75
+ git commit -m "feat(templates): source PROJECT_ID from vars instead of hardcoded placeholder"
76
+ ```
77
+
78
+ ---
79
+
80
+ ### Task 2: Remove `{{PROJECT_ID}}` YAML substitution from `scaffoldFullTemplates`
81
+
82
+ **Files:**
83
+ - Modify: `bin/cli.js:345`
84
+
85
+ - [ ] **Step 1: Delete the single PROJECT_ID substitution line**
86
+
87
+ In `bin/cli.js`, find and remove exactly this line (currently line 345):
88
+ ```javascript
89
+ if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
90
+ ```
91
+
92
+ The block around it (lines 342–356) substitutes `STATUS_FIELD_ID` and all `ID_*` column options into `project-automation.yml`. Keep all those lines. Only the `PROJECT_ID` line is removed.
93
+
94
+ - [ ] **Step 2: Verify other substitutions are intact**
95
+
96
+ Run:
97
+ ```bash
98
+ grep -n "wfContent.replace" bin/cli.js
99
+ ```
100
+
101
+ Expected output must include lines for `STATUS_FIELD_ID`, `ID_IDEA`, `ID_BRAINSTORMING`, `ID_DETAILING`, `ID_APPROVAL`, `ID_DEVELOPMENT`, `ID_TESTING`, `ID_CODE_REVIEW_PR`, `ID_PRODUCTION`. Must NOT include `PROJECT_ID`.
102
+
103
+ - [ ] **Step 3: Commit**
104
+
105
+ ```bash
106
+ git add bin/cli.js
107
+ git commit -m "fix(cli): remove PROJECT_ID hardcoding from scaffoldFullTemplates"
108
+ ```
109
+
110
+ ---
111
+
112
+ ### Task 3: Add `setActionsVariable` helper (TDD)
113
+
114
+ **Files:**
115
+ - Modify: `bin/cli.js` (add function before `scaffoldFullTemplates`, ~line 283)
116
+ - Modify: `tests/cli.test.js` (add describe block)
117
+ - Modify: `bin/cli.js:793` (add to `module.exports`)
118
+
119
+ - [ ] **Step 1: Write failing tests**
120
+
121
+ Add to `tests/cli.test.js`:
122
+
123
+ ```javascript
124
+ const { describe, it, mock } = require('node:test');
125
+
126
+ // ... existing imports and tests above ...
127
+
128
+ describe('setActionsVariable', () => {
129
+ it('calls PATCH first', () => {
130
+ const calls = [];
131
+ const execFn = (cmd, args) => { calls.push(args); };
132
+ const { setActionsVariable } = require('../bin/cli.js');
133
+ setActionsVariable('owner/repo', 'PROJECT_ID', 'PVT_abc', execFn);
134
+ assert.equal(calls.length, 1);
135
+ assert.ok(calls[0].includes('--method'));
136
+ assert.ok(calls[0].includes('PATCH'));
137
+ assert.ok(calls[0].some(a => a.includes('PROJECT_ID')));
138
+ });
139
+
140
+ it('falls back to POST on 404', () => {
141
+ const calls = [];
142
+ let callCount = 0;
143
+ const execFn = (cmd, args) => {
144
+ calls.push([...args]);
145
+ callCount++;
146
+ if (callCount === 1) {
147
+ const err = new Error('Not Found');
148
+ err.stderr = Buffer.from('Not Found');
149
+ throw err;
150
+ }
151
+ };
152
+ const { setActionsVariable } = require('../bin/cli.js');
153
+ setActionsVariable('owner/repo', 'PROJECT_ID', 'PVT_abc', execFn);
154
+ assert.equal(calls.length, 2);
155
+ assert.ok(calls[0].includes('PATCH'));
156
+ assert.ok(calls[1].includes('POST'));
157
+ });
158
+
159
+ it('throws on 403', () => {
160
+ const execFn = () => {
161
+ const err = new Error('Forbidden');
162
+ err.stderr = Buffer.from('Forbidden');
163
+ throw err;
164
+ };
165
+ const { setActionsVariable } = require('../bin/cli.js');
166
+ assert.throws(
167
+ () => setActionsVariable('owner/repo', 'PROJECT_ID', 'PVT_abc', execFn),
168
+ /Forbidden/
169
+ );
170
+ });
171
+ });
172
+ ```
173
+
174
+ - [ ] **Step 2: Run tests to verify they fail**
175
+
176
+ ```bash
177
+ npm test
178
+ ```
179
+
180
+ Expected: 3 new test failures — `setActionsVariable` is not exported yet.
181
+
182
+ - [ ] **Step 3: Add `setActionsVariable` to `bin/cli.js`**
183
+
184
+ Insert this function before `scaffoldFullTemplates` (~line 283):
185
+
186
+ ```javascript
187
+ function setActionsVariable(repo, name, value, execFn = execFileSync) {
188
+ try {
189
+ execFn('gh', [
190
+ 'api', `repos/${repo}/actions/variables/${name}`,
191
+ '--method', 'PATCH',
192
+ '-f', `name=${name}`,
193
+ '-f', `value=${value}`
194
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
195
+ } catch (err) {
196
+ const msg = (err.stderr?.toString() || '') + (err.message || '');
197
+ if (msg.includes('404') || msg.includes('Not Found')) {
198
+ execFn('gh', [
199
+ 'api', `repos/${repo}/actions/variables`,
200
+ '--method', 'POST',
201
+ '-f', `name=${name}`,
202
+ '-f', `value=${value}`
203
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
204
+ } else {
205
+ throw err;
206
+ }
207
+ }
208
+ }
209
+ ```
210
+
211
+ - [ ] **Step 4: Export the function**
212
+
213
+ In `bin/cli.js` at line 793, update:
214
+ ```javascript
215
+ if (typeof module !== 'undefined') module.exports = { resolveMode };
216
+ ```
217
+ to:
218
+ ```javascript
219
+ if (typeof module !== 'undefined') module.exports = { resolveMode, setActionsVariable };
220
+ ```
221
+
222
+ - [ ] **Step 5: Run tests to verify they pass**
223
+
224
+ ```bash
225
+ npm test
226
+ ```
227
+
228
+ Expected: all tests pass including the 3 new `setActionsVariable` tests.
229
+
230
+ - [ ] **Step 6: Commit**
231
+
232
+ ```bash
233
+ git add bin/cli.js tests/cli.test.js
234
+ git commit -m "feat(cli): add setActionsVariable helper with PATCH→POST fallback"
235
+ ```
236
+
237
+ ---
238
+
239
+ ### Task 4: Wire `setActionsVariable` into the create flow (`runFullSetup`)
240
+
241
+ **Files:**
242
+ - Modify: `bin/cli.js` (~line 554)
243
+
244
+ - [ ] **Step 1: Add the call site after the PROJECT_PAT block**
245
+
246
+ In `bin/cli.js`, locate the end of the `PROJECT_PAT` block in `runFullSetup` (the `} else if (projectId && isOrg)` line at ~554). Add after it:
247
+
248
+ ```javascript
249
+ // Set PROJECT_ID as GitHub Actions Variable
250
+ if (projectId) {
251
+ try {
252
+ setActionsVariable(repo, 'PROJECT_ID', projectId);
253
+ console.log(`${green}✅ vars.PROJECT_ID set as Actions Variable.${reset}`);
254
+ } catch (_) {
255
+ console.log(`${yellow}⚠️ Could not set vars.PROJECT_ID — token may lack variables:write scope.\n Set manually: repo Settings → Secrets and variables → Variables → PROJECT_ID = ${projectId}${reset}`);
256
+ }
257
+ }
258
+ ```
259
+
260
+ Insert this block between line 554 (`} else if (projectId && isOrg) { ... }`) and line 556 (`await setBranchProtection(...)`).
261
+
262
+ - [ ] **Step 2: Verify placement**
263
+
264
+ ```bash
265
+ grep -n "vars.PROJECT_ID\|setBranchProtection\|isOrg\)" bin/cli.js | head -20
266
+ ```
267
+
268
+ The `vars.PROJECT_ID` console log should appear between the `isOrg` block and `setBranchProtection`.
269
+
270
+ - [ ] **Step 3: Commit**
271
+
272
+ ```bash
273
+ git add bin/cli.js
274
+ git commit -m "feat(cli): set vars.PROJECT_ID as Actions Variable in create flow"
275
+ ```
276
+
277
+ ---
278
+
279
+ ### Task 5: Wire `setActionsVariable` into the upgrade flow (`runUpgradeToAgentic`)
280
+
281
+ **Files:**
282
+ - Modify: `bin/cli.js` (~line 1006)
283
+
284
+ - [ ] **Step 1: Add the call site after the PROJECT_PAT block in the upgrade flow**
285
+
286
+ In `bin/cli.js`, locate the end of the `PROJECT_PAT` block in `runUpgradeToAgentic` (the closing `}` of `if (projectId && !isOrg)` at ~line 1006). Add after it:
287
+
288
+ ```javascript
289
+ // Set PROJECT_ID as GitHub Actions Variable
290
+ if (projectId) {
291
+ try {
292
+ setActionsVariable(repo, 'PROJECT_ID', projectId);
293
+ console.log(`${green}✅ vars.PROJECT_ID set as Actions Variable.${reset}`);
294
+ } catch (_) {
295
+ console.log(`${yellow}⚠️ Could not set vars.PROJECT_ID — token may lack variables:write scope.\n Set manually: repo Settings → Secrets and variables → Variables → PROJECT_ID = ${projectId}${reset}`);
296
+ }
297
+ }
298
+ ```
299
+
300
+ Insert between line ~1006 (`}` closing the `PROJECT_PAT` block) and line ~1008 (`console.log scaffolding`).
301
+
302
+ - [ ] **Step 2: Verify no `{{PROJECT_ID}}` placeholder survives install**
303
+
304
+ ```bash
305
+ grep -rn "{{PROJECT_ID}}" templates/
306
+ ```
307
+
308
+ Expected: only `templates/full/docs/pdlc.md` (the documentation file). No workflow YAML files.
309
+
310
+ - [ ] **Step 3: Run full test suite**
311
+
312
+ ```bash
313
+ npm test
314
+ ```
315
+
316
+ Expected: all tests pass.
317
+
318
+ - [ ] **Step 4: Commit**
319
+
320
+ ```bash
321
+ git add bin/cli.js
322
+ git commit -m "feat(cli): set vars.PROJECT_ID as Actions Variable in upgrade flow"
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Self-Review Checklist
328
+
329
+ - Acceptance Criteria 1 (new install sets `vars.PROJECT_ID`): Task 4 ✓
330
+ - Acceptance Criteria 2 (`resolve-ids` works without YAML modification): Task 1 ✓
331
+ - Acceptance Criteria 3 (`--update` sets variable): Task 5 ✓
332
+ - Acceptance Criteria 4 (403 warning, non-fatal): Tasks 3, 4, 5 ✓
333
+ - Edge case — PATCH first, POST on 404: Task 3 ✓
334
+ - Edge case — `vars.PROJECT_ID` unset resolves to `''`, guard works: Task 1 ✓
335
+ - No `{{PROJECT_ID}}` in any YAML after install: Tasks 1 + 2 ✓
336
+ - All other ID substitutions preserved: Task 2 Step 2 ✓