create-agentic-pdlc 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
- package/.claude/settings.json +18 -0
- package/.coderabbit.yaml +35 -0
- package/.github/workflows/project-automation.yml +13 -67
- package/CLAUDE.md +1 -1
- package/README.md +33 -32
- package/adapters/claude-code/skill.md +7 -3
- package/bin/cli.js +549 -209
- package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
- package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
- package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
- package/package.json +2 -2
- package/templates/full/CLAUDE.md +30 -0
- package/templates/lite/AGENTS.md +121 -0
- package/templates/lite/CLAUDE.md +44 -0
- package/tests/cli.test.js +32 -0
- package/.github/workflows/agentic-metrics.yml +0 -545
- package/.github/workflows/qa-agent.yml +0 -139
- package/.github/workflows/qa-gate.yml +0 -51
- /package/templates/{AGENTS.md → full/AGENTS.md} +0 -0
- /package/templates/{docs → full/docs}/pdlc.md +0 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
# Two-Tier Installer 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:** Refactor `bin/cli.js` so `npx create-agentic-pdlc` installs a minimal lite profile by default and `--agentic` opts into the full board machine.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Extract `runSetup()` into three separate functions (`runLiteSetup`, `runFullSetup`, `runUpgradeToAgentic`) sharing helper functions for auth, hook install, branch protection, and template copying. Entry point routes by CLI arg.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js 18+, built-in `node:test` + `node:assert` for unit tests, `child_process.execFileSync`, `fs`, `path`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Map
|
|
14
|
+
|
|
15
|
+
| File | Change |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `bin/cli.js` | Main refactor — extract helpers, add three profile functions, update entry point |
|
|
18
|
+
| `tests/cli.test.js` | New — unit tests for pure helpers |
|
|
19
|
+
| `package.json` | Update test script to run `node:test` |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Task 1: Create branch + add test infrastructure
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
- Modify: `package.json`
|
|
27
|
+
- Create: `tests/cli.test.js`
|
|
28
|
+
|
|
29
|
+
- [ ] **Step 1: Update package.json test script**
|
|
30
|
+
|
|
31
|
+
Open `package.json` and replace:
|
|
32
|
+
```json
|
|
33
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
34
|
+
```
|
|
35
|
+
with:
|
|
36
|
+
```json
|
|
37
|
+
"test": "node --test tests/cli.test.js"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 2: Create `tests/cli.test.js` with routing tests**
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const { describe, it } = require('node:test');
|
|
44
|
+
const assert = require('node:assert/strict');
|
|
45
|
+
|
|
46
|
+
// We import helpers from cli.js once they are extracted.
|
|
47
|
+
// For now, define the expected pure functions here as contracts.
|
|
48
|
+
|
|
49
|
+
function resolveMode(args) {
|
|
50
|
+
if (args.includes('--update')) return 'update';
|
|
51
|
+
if (args.includes('--upgrade-to-agentic')) return 'upgrade';
|
|
52
|
+
if (args.includes('--agentic')) return 'full';
|
|
53
|
+
return 'lite';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('resolveMode', () => {
|
|
57
|
+
it('returns lite when no flags', () => {
|
|
58
|
+
assert.equal(resolveMode([]), 'lite');
|
|
59
|
+
});
|
|
60
|
+
it('returns full for --agentic', () => {
|
|
61
|
+
assert.equal(resolveMode(['--agentic']), 'full');
|
|
62
|
+
});
|
|
63
|
+
it('returns update for --update', () => {
|
|
64
|
+
assert.equal(resolveMode(['--update']), 'update');
|
|
65
|
+
});
|
|
66
|
+
it('returns upgrade for --upgrade-to-agentic', () => {
|
|
67
|
+
assert.equal(resolveMode(['--upgrade-to-agentic']), 'upgrade');
|
|
68
|
+
});
|
|
69
|
+
it('--update takes precedence over --agentic', () => {
|
|
70
|
+
assert.equal(resolveMode(['--update', '--agentic']), 'update');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('buildFullClaudeContent', () => {
|
|
75
|
+
it('concatenates lite and full with a newline separator', () => {
|
|
76
|
+
const lite = '# Lite\ncontent';
|
|
77
|
+
const full = '## Extra\nmore';
|
|
78
|
+
const result = lite + '\n' + full;
|
|
79
|
+
assert.ok(result.startsWith('# Lite'));
|
|
80
|
+
assert.ok(result.includes('## Extra'));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- [ ] **Step 3: Run tests — expect PASS (pure functions defined inline)**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expected output: `✔ resolveMode` and `✔ buildFullClaudeContent` tests all pass.
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 4: Commit**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git add package.json tests/cli.test.js
|
|
97
|
+
git commit -m "test: add test infra and routing contract tests"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Task 2: Extract `resolveMode` + update entry point in cli.js
|
|
103
|
+
|
|
104
|
+
**Files:**
|
|
105
|
+
- Modify: `bin/cli.js:700-708`
|
|
106
|
+
|
|
107
|
+
- [ ] **Step 1: Read the current entry point block** (lines 700–708 of `bin/cli.js`)
|
|
108
|
+
|
|
109
|
+
Current code at the bottom of `bin/cli.js`:
|
|
110
|
+
```js
|
|
111
|
+
const args = process.argv.slice(2);
|
|
112
|
+
if (args.includes('--update')) {
|
|
113
|
+
runUpdate().catch(err => { console.error(err.message); rl.close(); process.exit(1); });
|
|
114
|
+
} else {
|
|
115
|
+
runSetup().catch(err => { console.error(err.message); rl.close(); process.exit(1); });
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- [ ] **Step 2: Replace entry point with routed version**
|
|
120
|
+
|
|
121
|
+
Replace the block above with:
|
|
122
|
+
```js
|
|
123
|
+
function resolveMode(args) {
|
|
124
|
+
if (args.includes('--update')) return 'update';
|
|
125
|
+
if (args.includes('--upgrade-to-agentic')) return 'upgrade';
|
|
126
|
+
if (args.includes('--agentic')) return 'full';
|
|
127
|
+
return 'lite';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const args = process.argv.slice(2);
|
|
131
|
+
const mode = resolveMode(args);
|
|
132
|
+
|
|
133
|
+
const handler =
|
|
134
|
+
mode === 'update' ? runUpdate :
|
|
135
|
+
mode === 'upgrade' ? runUpgradeToAgentic :
|
|
136
|
+
mode === 'full' ? runFullSetup :
|
|
137
|
+
runLiteSetup;
|
|
138
|
+
|
|
139
|
+
handler().catch(err => { console.error(err.message); rl.close(); process.exit(1); });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Note: `runUpgradeToAgentic`, `runFullSetup`, and `runLiteSetup` will be added in later tasks. The file will not run cleanly until Task 6.
|
|
143
|
+
|
|
144
|
+
- [ ] **Step 3: Commit**
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git add bin/cli.js
|
|
148
|
+
git commit -m "refactor(cli): add resolveMode and routed entry point"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Task 3: Extract shared helper functions
|
|
154
|
+
|
|
155
|
+
**Files:**
|
|
156
|
+
- Modify: `bin/cli.js` — add helpers after `printSetupDone()`, before `runSetup()`
|
|
157
|
+
|
|
158
|
+
These helpers extract repeated logic from the current `runSetup()`. Add them as named functions between `printSetupDone()` (line ~119) and the start of `runSetup()` (line ~121).
|
|
159
|
+
|
|
160
|
+
- [ ] **Step 1: Add `checkGhAuth()` helper**
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
async function checkGhAuth() {
|
|
164
|
+
console.log(`${yellow}${i18n.checking_gh}${reset}`);
|
|
165
|
+
try {
|
|
166
|
+
execSync('gh auth status', { stdio: 'ignore' });
|
|
167
|
+
console.log(`${green}${i18n.gh_ok}${reset}\n`);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`${red}${i18n.gh_error}${reset}`);
|
|
170
|
+
console.error(`${i18n.gh_install}`);
|
|
171
|
+
rl.close();
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- [ ] **Step 2: Add `checkAndRefreshProjectScope()` helper**
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
function getScopes() {
|
|
181
|
+
try {
|
|
182
|
+
const out = execFileSync('gh', ['api', 'user', '-i'], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
183
|
+
const line = out.split('\n').find(l => l.toLowerCase().startsWith('x-oauth-scopes:'));
|
|
184
|
+
return line ? line.split(':').slice(1).join(':').split(',').map(s => s.trim()) : [];
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function checkAndRefreshProjectScope() {
|
|
191
|
+
const scopesBefore = getScopes();
|
|
192
|
+
if (scopesBefore.length === 0 || scopesBefore.includes('project')) return;
|
|
193
|
+
|
|
194
|
+
console.log(`${yellow}⚠️ Token missing 'project' scope — required for GitHub Projects board.${reset}`);
|
|
195
|
+
console.log(`${yellow} Refreshing token now (browser may open)...${reset}\n`);
|
|
196
|
+
try {
|
|
197
|
+
execSync('gh auth refresh -h github.com -s project', { stdio: 'inherit' });
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.log(`${red}❌ Token refresh failed. Run manually: gh auth refresh -h github.com -s project${reset}`);
|
|
200
|
+
rl.close();
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const scopesAfter = getScopes();
|
|
204
|
+
if (scopesAfter.length > 0 && !scopesAfter.includes('project')) {
|
|
205
|
+
console.log(`\n${red}❌ 'project' scope still missing after refresh.${reset}`);
|
|
206
|
+
console.log(`${yellow} Active scopes: ${scopesAfter.join(', ')}${reset}`);
|
|
207
|
+
console.log(`${yellow} Try manually: gh auth refresh -h github.com -s project${reset}`);
|
|
208
|
+
rl.close();
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
if (scopesAfter.length > 0) {
|
|
212
|
+
console.log(`\n${green}✅ Token refreshed. Active scopes: ${scopesAfter.join(', ')}${reset}\n`);
|
|
213
|
+
} else {
|
|
214
|
+
console.log(`\n${green}✅ Token refreshed with 'project' scope.${reset}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Note: `getScopes()` and its `try/catch` block already exist inside `runSetup()`. This step moves them to module scope and wraps them in `checkAndRefreshProjectScope()`.
|
|
220
|
+
|
|
221
|
+
- [ ] **Step 3: Add `installHook(sourceDir, targetDir)` helper**
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
function installHook(sourceDir, targetDir) {
|
|
225
|
+
const hookSrc = path.join(sourceDir, 'adapters', 'hooks', 'pdlc-stage-gate.sh');
|
|
226
|
+
const hookDir = path.join(targetDir, '.agentic-pdlc', 'hooks');
|
|
227
|
+
const hookDest = path.join(hookDir, 'pdlc-stage-gate.sh');
|
|
228
|
+
if (!fs.existsSync(hookSrc)) return;
|
|
229
|
+
|
|
230
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
231
|
+
fs.copyFileSync(hookSrc, hookDest);
|
|
232
|
+
fs.chmodSync(hookDest, '755');
|
|
233
|
+
|
|
234
|
+
const settingsDir = path.join(targetDir, '.claude');
|
|
235
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
236
|
+
if (!fs.existsSync(settingsPath)) {
|
|
237
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
238
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
239
|
+
hooks: {
|
|
240
|
+
PreToolUse: [{
|
|
241
|
+
matcher: 'Bash',
|
|
242
|
+
hooks: [{ type: 'command', command: 'bash .agentic-pdlc/hooks/pdlc-stage-gate.sh' }]
|
|
243
|
+
}]
|
|
244
|
+
}
|
|
245
|
+
}, null, 2) + '\n');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
- [ ] **Step 4: Add `setBranchProtection(repo, requiredChecks)` helper**
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
async function setBranchProtection(repo, requiredChecks) {
|
|
254
|
+
console.log(`\n${cyan}${i18n.configuring_protection}${reset}`);
|
|
255
|
+
try {
|
|
256
|
+
const defaultBranch = execFileSync(
|
|
257
|
+
'gh', ['api', `repos/${repo}`, '--jq', '.default_branch'],
|
|
258
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }
|
|
259
|
+
).trim() || 'main';
|
|
260
|
+
|
|
261
|
+
const protectionPayload = JSON.stringify({
|
|
262
|
+
required_status_checks: { strict: false, contexts: requiredChecks },
|
|
263
|
+
enforce_admins: false,
|
|
264
|
+
required_pull_request_reviews: null,
|
|
265
|
+
restrictions: null
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
execFileSync(
|
|
269
|
+
'gh',
|
|
270
|
+
['api', `repos/${repo}/branches/${defaultBranch}/protection`, '--method', 'PUT', '--input', '-'],
|
|
271
|
+
{ input: protectionPayload, stdio: ['pipe', 'ignore', 'pipe'] }
|
|
272
|
+
);
|
|
273
|
+
console.log(` ${green}${i18n.protection_ok}${reset}`);
|
|
274
|
+
} catch (_) {
|
|
275
|
+
console.log(` ${yellow}${i18n.protection_warn}${reset}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- [ ] **Step 5: Add `copyAdapterFiles(agent, sourceDir, targetDir)` helper**
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
function copyAdapterFiles(agent, sourceDir, targetDir) {
|
|
284
|
+
const claudeSetupSrc = path.join(sourceDir, 'adapters', 'claude-code', 'skill.md');
|
|
285
|
+
const cursorSetupSrc = path.join(sourceDir, 'adapters', 'cursor', 'rules.md');
|
|
286
|
+
|
|
287
|
+
if (agent === 'cursor') {
|
|
288
|
+
if (fs.existsSync(cursorSetupSrc)) {
|
|
289
|
+
fs.copyFileSync(cursorSetupSrc, path.join(targetDir, '.cursorrules'));
|
|
290
|
+
console.log(`${i18n.cursor_rules_written}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (fs.existsSync(claudeSetupSrc)) {
|
|
294
|
+
fs.copyFileSync(claudeSetupSrc, path.join(targetDir, '.agentic-setup.md'));
|
|
295
|
+
console.log(`${i18n.setup_written}`);
|
|
296
|
+
printSetupDone();
|
|
297
|
+
} else {
|
|
298
|
+
console.error(`${i18n.missing_claude}${claudeSetupSrc}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- [ ] **Step 6: Add `scaffoldLiteTemplates(sourceDir, targetDir)` helper**
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
function scaffoldLiteTemplates(sourceDir, targetDir) {
|
|
307
|
+
const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
308
|
+
fs.mkdirSync(destTemplates, { recursive: true });
|
|
309
|
+
|
|
310
|
+
// CLAUDE.md — lite version
|
|
311
|
+
const liteClaudeSrc = path.join(sourceDir, 'templates', 'lite', 'CLAUDE.md');
|
|
312
|
+
if (fs.existsSync(liteClaudeSrc)) {
|
|
313
|
+
fs.copyFileSync(liteClaudeSrc, path.join(destTemplates, 'CLAUDE.md'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// AGENTS.md — lite version
|
|
317
|
+
const liteAgentsSrc = path.join(sourceDir, 'templates', 'lite', 'AGENTS.md');
|
|
318
|
+
if (fs.existsSync(liteAgentsSrc)) {
|
|
319
|
+
fs.copyFileSync(liteAgentsSrc, path.join(destTemplates, 'AGENTS.md'));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Issue templates — shared between lite and full
|
|
323
|
+
const issueTemplateSrc = path.join(sourceDir, 'templates', '.github', 'ISSUE_TEMPLATE');
|
|
324
|
+
const issueTemplateDest = path.join(destTemplates, '.github', 'ISSUE_TEMPLATE');
|
|
325
|
+
if (fs.existsSync(issueTemplateSrc)) {
|
|
326
|
+
copyDirSync(issueTemplateSrc, issueTemplateDest);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- [ ] **Step 7: Add `scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName)` helper**
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName) {
|
|
335
|
+
const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
336
|
+
fs.mkdirSync(destTemplates, { recursive: true });
|
|
337
|
+
|
|
338
|
+
// CLAUDE.md — concatenate lite + full addon
|
|
339
|
+
const liteClaudeSrc = path.join(sourceDir, 'templates', 'lite', 'CLAUDE.md');
|
|
340
|
+
const fullClaudeSrc = path.join(sourceDir, 'templates', 'full', 'CLAUDE.md');
|
|
341
|
+
if (fs.existsSync(liteClaudeSrc) && fs.existsSync(fullClaudeSrc)) {
|
|
342
|
+
const combined = fs.readFileSync(liteClaudeSrc, 'utf8') + '\n' + fs.readFileSync(fullClaudeSrc, 'utf8');
|
|
343
|
+
fs.writeFileSync(path.join(destTemplates, 'CLAUDE.md'), combined);
|
|
344
|
+
} else if (fs.existsSync(liteClaudeSrc)) {
|
|
345
|
+
fs.copyFileSync(liteClaudeSrc, path.join(destTemplates, 'CLAUDE.md'));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// AGENTS.md — full version
|
|
349
|
+
const fullAgentsSrc = path.join(sourceDir, 'templates', 'full', 'AGENTS.md');
|
|
350
|
+
if (fs.existsSync(fullAgentsSrc)) {
|
|
351
|
+
fs.copyFileSync(fullAgentsSrc, path.join(destTemplates, 'AGENTS.md'));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// All of templates/.github/ (issue templates + workflows)
|
|
355
|
+
const githubSrc = path.join(sourceDir, 'templates', '.github');
|
|
356
|
+
const githubDest = path.join(destTemplates, '.github');
|
|
357
|
+
if (fs.existsSync(githubSrc)) {
|
|
358
|
+
copyDirSync(githubSrc, githubDest);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// docs/pdlc.md — substitute board IDs
|
|
362
|
+
const pdlcSrc = path.join(sourceDir, 'templates', 'full', 'docs', 'pdlc.md');
|
|
363
|
+
const pdlcDest = path.join(destTemplates, 'docs', 'pdlc.md');
|
|
364
|
+
if (fs.existsSync(pdlcSrc)) {
|
|
365
|
+
fs.mkdirSync(path.join(destTemplates, 'docs'), { recursive: true });
|
|
366
|
+
let pdlcContent = fs.readFileSync(pdlcSrc, 'utf8');
|
|
367
|
+
if (projectId) pdlcContent = pdlcContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
368
|
+
if (statusFieldId) pdlcContent = pdlcContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
369
|
+
pdlcContent = pdlcContent.replace(/\{\{REPO_OWNER\}\}/g, () => repoOwner);
|
|
370
|
+
pdlcContent = pdlcContent.replace(/\{\{REPO_NAME\}\}/g, () => repoName);
|
|
371
|
+
if (Object.keys(optionMap).length > 0) {
|
|
372
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
|
|
373
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
|
|
374
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_BRAINSTORMING\}\}/g, () => optionMap['🧠 Brainstorming'] || 'MISSING_ID');
|
|
375
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_DETAIL\}\}/g, () => optionMap['📐 Detail Solution'] || 'MISSING_ID');
|
|
376
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_APPROVAL\}\}/g, () => optionMap['✅ Approval'] || 'MISSING_ID');
|
|
377
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_DEVELOPMENT\}\}/g, () => optionMap['⚙️ Development'] || 'MISSING_ID');
|
|
378
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_TESTING\}\}/g, () => optionMap['🧪 Testing'] || 'MISSING_ID');
|
|
379
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_CODE_REVIEW_PR\}\}/g, () => optionMap['👁 Code Review / PR'] || 'MISSING_ID');
|
|
380
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_READY_FOR_PRODUCTION\}\}/g,() => optionMap['🚀 Ready for Production']|| 'MISSING_ID');
|
|
381
|
+
}
|
|
382
|
+
fs.writeFileSync(pdlcDest, pdlcContent);
|
|
383
|
+
if (projectId && statusFieldId && Object.keys(optionMap).length > 0) {
|
|
384
|
+
console.log(`${i18n.pdlc_prefilled}`);
|
|
385
|
+
} else {
|
|
386
|
+
console.log(`${yellow}⚠️ pdlc.md copied — Project IDs not filled (board creation failed). Re-run after fixing token.${reset}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// project-automation.yml — substitute IDs
|
|
391
|
+
const paPath = path.join(destTemplates, '.github', 'workflows', 'project-automation.yml');
|
|
392
|
+
if (fs.existsSync(paPath) && Object.keys(optionMap).length > 0) {
|
|
393
|
+
let wfContent = fs.readFileSync(paPath, 'utf8');
|
|
394
|
+
if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
395
|
+
if (statusFieldId) wfContent = wfContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
396
|
+
wfContent = wfContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
|
|
397
|
+
wfContent = wfContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
|
|
398
|
+
wfContent = wfContent.replace(/\{\{ID_BRAINSTORMING\}\}/g, () => optionMap['🧠 Brainstorming'] || 'MISSING_ID');
|
|
399
|
+
wfContent = wfContent.replace(/\{\{ID_DETAILING\}\}/g, () => optionMap['📐 Detail Solution'] || 'MISSING_ID');
|
|
400
|
+
wfContent = wfContent.replace(/\{\{ID_APPROVAL\}\}/g, () => optionMap['✅ Approval'] || 'MISSING_ID');
|
|
401
|
+
wfContent = wfContent.replace(/\{\{ID_DEVELOPMENT\}\}/g, () => optionMap['⚙️ Development'] || 'MISSING_ID');
|
|
402
|
+
wfContent = wfContent.replace(/\{\{ID_TESTING\}\}/g, () => optionMap['🧪 Testing'] || 'MISSING_ID');
|
|
403
|
+
wfContent = wfContent.replace(/\{\{ID_CODE_REVIEW_PR\}\}/g, () => optionMap['👁 Code Review / PR'] || 'MISSING_ID');
|
|
404
|
+
wfContent = wfContent.replace(/\{\{ID_PRODUCTION\}\}/g, () => optionMap['🚀 Ready for Production']|| 'MISSING_ID');
|
|
405
|
+
fs.writeFileSync(paPath, wfContent);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.log(`${i18n.templates_copied}`);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
- [ ] **Step 8: Add `writeCliContext(targetDir, profile, data)` helper**
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
function writeCliContext(targetDir, profile, data) {
|
|
416
|
+
try {
|
|
417
|
+
const contextPath = path.join(targetDir, '.agentic-pdlc', 'cli-context.json');
|
|
418
|
+
fs.mkdirSync(path.join(targetDir, '.agentic-pdlc'), { recursive: true });
|
|
419
|
+
fs.writeFileSync(contextPath, JSON.stringify({ profile, ...data }, null, 2));
|
|
420
|
+
} catch (_) {
|
|
421
|
+
// Non-fatal — agent will ask for the values instead
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
- [ ] **Step 9: Remove now-duplicated helpers from `runSetup()` body**
|
|
427
|
+
|
|
428
|
+
In `runSetup()`, remove or comment out:
|
|
429
|
+
- The `getScopes()` inner function (moved to module scope)
|
|
430
|
+
- The `scopesBefore` / `scopesAfter` token refresh block (now in `checkAndRefreshProjectScope()`)
|
|
431
|
+
- The hook install block (lines ~457–478) — now in `installHook()`
|
|
432
|
+
- The branch protection block (lines ~362–377) — now in `setBranchProtection()`
|
|
433
|
+
- The adapter copy block (lines ~481–517) — now in `copyAdapterFiles()`
|
|
434
|
+
|
|
435
|
+
Do NOT refactor `runSetup()` further yet — that happens in Task 5.
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 10: Commit**
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
git add bin/cli.js
|
|
441
|
+
git commit -m "refactor(cli): extract shared helper functions"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Task 4: Implement `runLiteSetup()`
|
|
447
|
+
|
|
448
|
+
**Files:**
|
|
449
|
+
- Modify: `bin/cli.js` — add `runLiteSetup()` after `runSetup()`
|
|
450
|
+
|
|
451
|
+
- [ ] **Step 1: Add `runLiteSetup()` to `bin/cli.js`**
|
|
452
|
+
|
|
453
|
+
Insert the following function after the closing brace of `runSetup()` (before `runUpdate()`):
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
async function runLiteSetup() {
|
|
457
|
+
await checkGhAuth();
|
|
458
|
+
|
|
459
|
+
const agentAnswer = await askQuestion(i18n.ask_agent);
|
|
460
|
+
const agent = agentAnswer.trim().toLowerCase();
|
|
461
|
+
if (!['claude', 'cursor', 'copilot'].includes(agent)) {
|
|
462
|
+
console.log(t(
|
|
463
|
+
`ℹ️ Generating Universal Setup for '${agent}' (Compatible with any Markdown-reading agent).`,
|
|
464
|
+
`ℹ️ Gerando Setup Universal para '${agent}' (Compatível com qualquer agente que leia Markdown).`,
|
|
465
|
+
`ℹ️ Generando Setup Universal para '${agent}' (Compatible con cualquier agente que lea Markdown).`
|
|
466
|
+
));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let repoOwner, repoName, repo;
|
|
470
|
+
while (true) {
|
|
471
|
+
let repoUrl = (await askQuestion(i18n.ask_repo)).trim();
|
|
472
|
+
if (repoUrl.endsWith('/')) repoUrl = repoUrl.slice(0, -1);
|
|
473
|
+
if (repoUrl.endsWith('.git')) repoUrl = repoUrl.slice(0, -4);
|
|
474
|
+
const repoParts = repoUrl.split('/');
|
|
475
|
+
if (repoParts.length >= 2) {
|
|
476
|
+
repoOwner = repoParts[repoParts.length - 2];
|
|
477
|
+
repoName = repoParts[repoParts.length - 1];
|
|
478
|
+
repo = `${repoOwner}/${repoName}`;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
console.log(`${red}${i18n.invalid_repo}${reset}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
console.log(`\n${yellow}${i18n.starting_setup}${reset}`);
|
|
485
|
+
|
|
486
|
+
installHook(sourceDir, targetDir);
|
|
487
|
+
|
|
488
|
+
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
489
|
+
scaffoldLiteTemplates(sourceDir, targetDir);
|
|
490
|
+
console.log(`${i18n.templates_copied}`);
|
|
491
|
+
|
|
492
|
+
await setBranchProtection(repo, ['PDLC Stage Gate']);
|
|
493
|
+
|
|
494
|
+
writeCliContext(targetDir, 'lite', { repoOwner, repoName, projectNumber: null, isOrg: false, boardUrl: null, patAutoSet: false });
|
|
495
|
+
|
|
496
|
+
copyAdapterFiles(agent, sourceDir, targetDir);
|
|
497
|
+
|
|
498
|
+
rl.close();
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
- [ ] **Step 2: Verify file runs without syntax errors**
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
node --check bin/cli.js
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Expected: no output (no errors).
|
|
509
|
+
|
|
510
|
+
- [ ] **Step 3: Commit**
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
git add bin/cli.js
|
|
514
|
+
git commit -m "feat(cli): implement runLiteSetup — lite profile"
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Task 5: Refactor `runSetup()` into `runFullSetup()`
|
|
520
|
+
|
|
521
|
+
**Files:**
|
|
522
|
+
- Modify: `bin/cli.js` — rename and update `runSetup()`
|
|
523
|
+
|
|
524
|
+
The existing `runSetup()` already works correctly. This task renames it to `runFullSetup()`, replaces inline duplicated code with helper calls, and updates template copying to use `scaffoldFullTemplates()`.
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 1: Rename `runSetup` → `runFullSetup`**
|
|
527
|
+
|
|
528
|
+
Find and replace (single occurrence — the function declaration):
|
|
529
|
+
```js
|
|
530
|
+
async function runSetup() {
|
|
531
|
+
```
|
|
532
|
+
→
|
|
533
|
+
```js
|
|
534
|
+
async function runFullSetup() {
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
- [ ] **Step 2: Replace inline hook install block with helper call**
|
|
538
|
+
|
|
539
|
+
Find the block (approximately lines 457–478 in original):
|
|
540
|
+
```js
|
|
541
|
+
// Install PDLC stage gate hook (all agents)
|
|
542
|
+
const hookSrc = path.join(sourceDir, 'adapters', 'hooks', 'pdlc-stage-gate.sh');
|
|
543
|
+
const hookDir = path.join(targetDir, '.agentic-pdlc', 'hooks');
|
|
544
|
+
const hookDest = path.join(hookDir, 'pdlc-stage-gate.sh');
|
|
545
|
+
if (fs.existsSync(hookSrc)) {
|
|
546
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
547
|
+
fs.copyFileSync(hookSrc, hookDest);
|
|
548
|
+
fs.chmodSync(hookDest, '755');
|
|
549
|
+
}
|
|
550
|
+
const claudeSettingsDir = path.join(targetDir, '.claude');
|
|
551
|
+
const claudeSettingsPath = path.join(claudeSettingsDir, 'settings.json');
|
|
552
|
+
if (!fs.existsSync(claudeSettingsPath)) {
|
|
553
|
+
fs.mkdirSync(claudeSettingsDir, { recursive: true });
|
|
554
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify({
|
|
555
|
+
hooks: {
|
|
556
|
+
PreToolUse: [{
|
|
557
|
+
matcher: 'Bash',
|
|
558
|
+
hooks: [{ type: 'command', command: 'bash .agentic-pdlc/hooks/pdlc-stage-gate.sh' }]
|
|
559
|
+
}]
|
|
560
|
+
}
|
|
561
|
+
}, null, 2) + '\n');
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Replace with:
|
|
566
|
+
```js
|
|
567
|
+
installHook(sourceDir, targetDir);
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
- [ ] **Step 3: Replace inline branch protection block with helper call**
|
|
571
|
+
|
|
572
|
+
Find the block:
|
|
573
|
+
```js
|
|
574
|
+
// Branch protection — require PDLC Stage Gate + QA Gate on default branch
|
|
575
|
+
console.log(`\n${cyan}${i18n.configuring_protection}${reset}`);
|
|
576
|
+
try {
|
|
577
|
+
const defaultBranch = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.default_branch'],
|
|
578
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim() || 'main';
|
|
579
|
+
const protectionPayload = JSON.stringify({
|
|
580
|
+
required_status_checks: { strict: false, contexts: ['PDLC Stage Gate', 'QA Gate'] },
|
|
581
|
+
enforce_admins: false,
|
|
582
|
+
required_pull_request_reviews: null,
|
|
583
|
+
restrictions: null
|
|
584
|
+
});
|
|
585
|
+
execFileSync('gh', ['api', `repos/${repo}/branches/${defaultBranch}/protection`, '--method', 'PUT', '--input', '-'],
|
|
586
|
+
{ input: protectionPayload, stdio: ['pipe', 'ignore', 'pipe'] });
|
|
587
|
+
console.log(` ${green}${i18n.protection_ok}${reset}`);
|
|
588
|
+
} catch (_) {
|
|
589
|
+
console.log(` ${yellow}${i18n.protection_warn}${reset}`);
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Replace with:
|
|
594
|
+
```js
|
|
595
|
+
await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
- [ ] **Step 4: Replace inline template copying with `scaffoldFullTemplates()` call**
|
|
599
|
+
|
|
600
|
+
Find the block that starts with:
|
|
601
|
+
```js
|
|
602
|
+
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
603
|
+
|
|
604
|
+
// We copy the templates folder so the agent has the real text logic to replace and rename
|
|
605
|
+
const sourceTemplates = path.join(sourceDir, 'templates');
|
|
606
|
+
const targetTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
607
|
+
```
|
|
608
|
+
and ends just before the `// Write CLI context` block.
|
|
609
|
+
|
|
610
|
+
Replace the entire scaffolding block with:
|
|
611
|
+
```js
|
|
612
|
+
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
613
|
+
scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
- [ ] **Step 5: Replace inline adapter copy block with helper call**
|
|
617
|
+
|
|
618
|
+
Find the block that starts with:
|
|
619
|
+
```js
|
|
620
|
+
// Handle the specific setup instructions target
|
|
621
|
+
const claudeSetupSrc = path.join(sourceDir, 'adapters', 'claude-code', 'skill.md');
|
|
622
|
+
const cursorSetupSrc = path.join(sourceDir, 'adapters', 'cursor', 'rules.md');
|
|
623
|
+
|
|
624
|
+
if (agent === 'claude') {
|
|
625
|
+
```
|
|
626
|
+
and continues to the end of `runFullSetup()` (the `rl.close()` call just before).
|
|
627
|
+
|
|
628
|
+
Replace with:
|
|
629
|
+
```js
|
|
630
|
+
copyAdapterFiles(agent, sourceDir, targetDir);
|
|
631
|
+
|
|
632
|
+
rl.close();
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
- [ ] **Step 6: Update `writeCliContext` call at the end of the existing block**
|
|
636
|
+
|
|
637
|
+
Find:
|
|
638
|
+
```js
|
|
639
|
+
fs.writeFileSync(cliContextPath, JSON.stringify({
|
|
640
|
+
projectName,
|
|
641
|
+
repoOwner,
|
|
642
|
+
repoName,
|
|
643
|
+
projectNumber,
|
|
644
|
+
isOrg,
|
|
645
|
+
boardUrl,
|
|
646
|
+
patAutoSet
|
|
647
|
+
}, null, 2));
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Replace with:
|
|
651
|
+
```js
|
|
652
|
+
writeCliContext(targetDir, 'full', {
|
|
653
|
+
projectName,
|
|
654
|
+
repoOwner,
|
|
655
|
+
repoName,
|
|
656
|
+
projectNumber,
|
|
657
|
+
isOrg,
|
|
658
|
+
boardUrl,
|
|
659
|
+
patAutoSet
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Also remove the surrounding `try/catch` block around the old `fs.writeFileSync` (the `writeCliContext` helper handles that internally).
|
|
664
|
+
|
|
665
|
+
- [ ] **Step 7: Remove the duplicate `getScopes()` inner function and inline token-refresh block**
|
|
666
|
+
|
|
667
|
+
Inside `runFullSetup()`, remove the inner `getScopes()` definition and the `scopesBefore`/`scopesAfter` block. Replace them with a single call:
|
|
668
|
+
|
|
669
|
+
```js
|
|
670
|
+
await checkAndRefreshProjectScope();
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Place this call immediately after `await checkGhAuth();` and before the `agentAnswer` prompt.
|
|
674
|
+
|
|
675
|
+
- [ ] **Step 8: Syntax check**
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
node --check bin/cli.js
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Expected: no output.
|
|
682
|
+
|
|
683
|
+
- [ ] **Step 9: Commit**
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
git add bin/cli.js
|
|
687
|
+
git commit -m "refactor(cli): runSetup → runFullSetup, replace inline blocks with helpers"
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Task 6: Implement `runUpgradeToAgentic()`
|
|
693
|
+
|
|
694
|
+
**Files:**
|
|
695
|
+
- Modify: `bin/cli.js` — add `runUpgradeToAgentic()` after `runLiteSetup()`
|
|
696
|
+
|
|
697
|
+
- [ ] **Step 1: Add `runUpgradeToAgentic()` to `bin/cli.js`**
|
|
698
|
+
|
|
699
|
+
Insert after `runLiteSetup()` and before `runUpdate()`:
|
|
700
|
+
|
|
701
|
+
```js
|
|
702
|
+
async function runUpgradeToAgentic() {
|
|
703
|
+
const contextPath = path.join(targetDir, '.agentic-pdlc', 'cli-context.json');
|
|
704
|
+
if (!fs.existsSync(contextPath)) {
|
|
705
|
+
console.error(`\n${red}${i18n.update_no_context}${reset}\n`);
|
|
706
|
+
rl.close();
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
|
|
711
|
+
if (ctx.profile === 'full') {
|
|
712
|
+
console.log(`\n${green}✅ Already running full profile. Nothing to upgrade.${reset}\n`);
|
|
713
|
+
rl.close();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
await checkGhAuth();
|
|
718
|
+
await checkAndRefreshProjectScope();
|
|
719
|
+
|
|
720
|
+
const { repoOwner, repoName } = ctx;
|
|
721
|
+
const repo = `${repoOwner}/${repoName}`;
|
|
722
|
+
|
|
723
|
+
const askProjectName = t(
|
|
724
|
+
`What is the project name for the board? (default: ${repoName.toUpperCase()}): `,
|
|
725
|
+
`Qual o nome do projeto em que o board será configurado? (padrão: ${repoName.toUpperCase()}): `,
|
|
726
|
+
`¿Cuál es el nombre del proyecto en el que se configurará el board? (por defecto: ${repoName.toUpperCase()}): `
|
|
727
|
+
);
|
|
728
|
+
const projectNameAnswer = await askQuestion(askProjectName);
|
|
729
|
+
const projectName = projectNameAnswer.trim() ? projectNameAnswer.trim().toUpperCase() : repoName.toUpperCase();
|
|
730
|
+
const boardName = `BOARD - ${projectName}`;
|
|
731
|
+
|
|
732
|
+
let isOrg = ctx.isOrg || false;
|
|
733
|
+
try {
|
|
734
|
+
const ownerType = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.owner.type'],
|
|
735
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
736
|
+
isOrg = ownerType === 'Organization';
|
|
737
|
+
} catch (_) {}
|
|
738
|
+
|
|
739
|
+
console.log(`\n${yellow}${i18n.starting_setup}${reset}`);
|
|
740
|
+
|
|
741
|
+
// Labels
|
|
742
|
+
const labels = [
|
|
743
|
+
{ name: 'stage:exploration', color: '9b59b6', description: 'Issue is being evaluated' },
|
|
744
|
+
{ name: 'stage:brainstorming', color: 'e84393', description: 'Proposed approaches awaiting PM gate' },
|
|
745
|
+
{ name: 'stage:detailing', color: '3498db', description: 'Technical spec is being written' },
|
|
746
|
+
{ name: 'stage:development', color: 'e67e22', description: 'Agent is implementing the spec' },
|
|
747
|
+
{ name: 'stage:testing', color: '8e44ad', description: 'Agent is testing the implementation' },
|
|
748
|
+
{ name: 'spec:approved', color: '0e8a16', description: 'Spec approved — agent can implement' },
|
|
749
|
+
{ name: 'pr:in-review', color: 'e4e669', description: 'PR awaiting code review' },
|
|
750
|
+
{ name: 'pr:approved', color: '0e8a16', description: 'PR approved, ready for merge' },
|
|
751
|
+
{ name: 'architecture-violation', color: 'd93f0b', description: 'Invariant violation detected by CI' },
|
|
752
|
+
{ name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
|
|
753
|
+
{ name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
|
|
754
|
+
{ name: 'infra:qa-broken', color: 'F97316', description: 'QA Agent failed to run — manual review required' },
|
|
755
|
+
{ name: 'jules', color: '5319e7', description: 'Jules AI Agent' }
|
|
756
|
+
];
|
|
757
|
+
|
|
758
|
+
console.log(`\n${cyan}${i18n.creating_labels}${reset}`);
|
|
759
|
+
for (const label of labels) {
|
|
760
|
+
try {
|
|
761
|
+
execFileSync('gh', ['label', 'create', label.name, '--color', label.color, '--description', label.description, '--repo', repo, '--force'], { stdio: 'ignore' });
|
|
762
|
+
console.log(` ${i18n.label_ok}${label.name}`);
|
|
763
|
+
} catch (err) {
|
|
764
|
+
console.log(` ${i18n.label_warn}${label.name}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Board
|
|
769
|
+
console.log(`\n${cyan}${i18n.creating_project}${reset}`);
|
|
770
|
+
let ownerId, projectId, projectNumber;
|
|
771
|
+
try {
|
|
772
|
+
if (isOrg) {
|
|
773
|
+
ownerId = execFileSync('gh', ['api', 'graphql', '-f', 'query=query($login: String!) { organization(login: $login) { id } }', '-f', `login=${repoOwner}`, '--jq', '.data.organization.id'],
|
|
774
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
775
|
+
} else {
|
|
776
|
+
ownerId = execFileSync('gh', ['api', 'graphql', '-f', 'query={ viewer { id } }', '--jq', '.data.viewer.id'],
|
|
777
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const raw = execFileSync('gh', ['api', 'graphql', '-f',
|
|
781
|
+
'query=mutation($owner: ID!, $title: String!) { createProjectV2(input: {ownerId: $owner, title: $title}) { projectV2 { id number } } }',
|
|
782
|
+
'-f', `owner=${ownerId}`, '-f', `title=${boardName}`],
|
|
783
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
784
|
+
const resp = raw ? JSON.parse(raw) : null;
|
|
785
|
+
if (resp?.errors) throw new Error(resp.errors.map(e => e.message).join('; '));
|
|
786
|
+
const pData = resp?.data?.createProjectV2?.projectV2;
|
|
787
|
+
projectId = pData?.id;
|
|
788
|
+
projectNumber = pData?.number;
|
|
789
|
+
console.log(` ${i18n.project_ok}${projectId})`);
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const repoNodeId = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.node_id'],
|
|
793
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
794
|
+
execFileSync('gh', ['api', 'graphql', '-f',
|
|
795
|
+
'query=mutation($projectId: ID!, $repositoryId: ID!) { linkProjectV2ToRepository(input: {projectId: $projectId, repositoryId: $repositoryId}) { repository { name } } }',
|
|
796
|
+
'-f', `projectId=${projectId}`, '-f', `repositoryId=${repoNodeId}`],
|
|
797
|
+
{ stdio: 'ignore' });
|
|
798
|
+
console.log(` ${i18n.link_project_ok}`);
|
|
799
|
+
} catch (_) {
|
|
800
|
+
console.log(` ${i18n.link_project_warn}`);
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
console.log(` ${i18n.project_err}${err.message}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
let statusFieldId;
|
|
807
|
+
let optionMap = {};
|
|
808
|
+
|
|
809
|
+
if (projectId) {
|
|
810
|
+
console.log(` ${cyan}${i18n.config_columns}${reset}`);
|
|
811
|
+
try {
|
|
812
|
+
statusFieldId = execFileSync('gh', ['api', 'graphql', '-f',
|
|
813
|
+
'query=query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name } } } } } }',
|
|
814
|
+
'-f', `projectId=${projectId}`, '--jq', '.data.node.fields.nodes[] | select(.name == "Status") | .id'
|
|
815
|
+
]).toString().trim();
|
|
816
|
+
|
|
817
|
+
if (statusFieldId) {
|
|
818
|
+
const columns = [
|
|
819
|
+
{ name: '💡 Idea - No move to Exploration directly', description: 'Just tell your agent to work on issue #XX', color: 'GRAY' },
|
|
820
|
+
{ name: '🔍 Exploration', description: 'AI is analyzing code and context', color: 'PURPLE' },
|
|
821
|
+
{ name: '🧠 Brainstorming', description: 'AI proposed approaches and trade-offs', color: 'PINK' },
|
|
822
|
+
{ name: '📐 Detail Solution', description: 'AI is writing the technical spec', color: 'BLUE' },
|
|
823
|
+
{ name: '✅ Approval', description: 'Spec ready, awaiting `spec:approved` label', color: 'GREEN' },
|
|
824
|
+
{ name: '⚙️ Development', description: 'AI implementing the spec', color: 'ORANGE' },
|
|
825
|
+
{ name: '🧪 Testing', description: 'QA testing and CI pipeline checks', color: 'RED' },
|
|
826
|
+
{ name: '👁 Code Review / PR',description: 'PR opened, awaiting your review', color: 'YELLOW' },
|
|
827
|
+
{ name: '🚀 Ready for Production', description: 'Merged and ready for production', color: 'GREEN' }
|
|
828
|
+
];
|
|
829
|
+
|
|
830
|
+
const queryPayload = JSON.stringify({
|
|
831
|
+
query: `mutation($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]) {
|
|
832
|
+
updateProjectV2Field(input: { fieldId: $fieldId, singleSelectOptions: $options }) {
|
|
833
|
+
projectV2Field { ... on ProjectV2SingleSelectField { options { id name } } }
|
|
834
|
+
}
|
|
835
|
+
}`,
|
|
836
|
+
variables: { fieldId: statusFieldId, options: columns }
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
const updateOutput = execFileSync('gh', ['api', 'graphql', '--input', '-'],
|
|
840
|
+
{ input: queryPayload }).toString().trim();
|
|
841
|
+
const jsonResponse = updateOutput ? JSON.parse(updateOutput) : null;
|
|
842
|
+
const returnedOptions = jsonResponse?.data?.updateProjectV2Field?.projectV2Field?.options || [];
|
|
843
|
+
for (const opt of returnedOptions) optionMap[opt.name] = opt.id;
|
|
844
|
+
console.log(` ${i18n.columns_ok}`);
|
|
845
|
+
}
|
|
846
|
+
} catch (_) {
|
|
847
|
+
console.log(` ${i18n.columns_warn}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Auto-provision PROJECT_PAT for personal repos
|
|
852
|
+
let patAutoSet = false;
|
|
853
|
+
if (projectId && !isOrg) {
|
|
854
|
+
try {
|
|
855
|
+
const tokenOut = execFileSync('gh', ['auth', 'token'],
|
|
856
|
+
{ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
857
|
+
if (tokenOut) {
|
|
858
|
+
execFileSync('gh', ['secret', 'set', 'PROJECT_PAT', '--body', tokenOut, '--repo', repo],
|
|
859
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] });
|
|
860
|
+
patAutoSet = true;
|
|
861
|
+
console.log(`\n${green}✅ PROJECT_PAT secret set automatically (uses your gh OAuth token).${reset}`);
|
|
862
|
+
}
|
|
863
|
+
} catch (_) {
|
|
864
|
+
console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_PAT. Agent will guide manual setup.${reset}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
869
|
+
scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName);
|
|
870
|
+
|
|
871
|
+
await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
|
|
872
|
+
|
|
873
|
+
const boardUrl = projectNumber ? buildBoardUrl(repoOwner, projectNumber, isOrg) : null;
|
|
874
|
+
writeCliContext(targetDir, 'full', {
|
|
875
|
+
projectName,
|
|
876
|
+
repoOwner,
|
|
877
|
+
repoName,
|
|
878
|
+
projectNumber,
|
|
879
|
+
isOrg,
|
|
880
|
+
boardUrl,
|
|
881
|
+
patAutoSet
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const line1 = t('🎉 Upgrade complete! Board:', '🎉 Upgrade concluído! Board:', '🎉 ¡Actualización completada! Board:');
|
|
885
|
+
console.log(`\n${green}${line1} ${boardUrl || '(board creation failed)'}${reset}\n`);
|
|
886
|
+
|
|
887
|
+
rl.close();
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
- [ ] **Step 2: Syntax check**
|
|
892
|
+
|
|
893
|
+
```bash
|
|
894
|
+
node --check bin/cli.js
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Expected: no output.
|
|
898
|
+
|
|
899
|
+
- [ ] **Step 3: Commit**
|
|
900
|
+
|
|
901
|
+
```bash
|
|
902
|
+
git add bin/cli.js
|
|
903
|
+
git commit -m "feat(cli): implement runUpgradeToAgentic — upgrade lite to full"
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Task 7: Backwards compat for `runUpdate()` with missing `profile` field
|
|
909
|
+
|
|
910
|
+
**Files:**
|
|
911
|
+
- Modify: `bin/cli.js` — `runUpdate()` function
|
|
912
|
+
|
|
913
|
+
`runUpdate()` reads `cli-context.json`. Existing installs lack the `profile` field. The function must treat missing `profile` as `'full'` to avoid breakage.
|
|
914
|
+
|
|
915
|
+
- [ ] **Step 1: Locate the context read in `runUpdate()`**
|
|
916
|
+
|
|
917
|
+
Find (approximately line 615 of original):
|
|
918
|
+
```js
|
|
919
|
+
async function runUpdate() {
|
|
920
|
+
const contextPath = path.join(targetDir, '.agentic-pdlc', 'cli-context.json');
|
|
921
|
+
if (!fs.existsSync(contextPath)) {
|
|
922
|
+
console.error(`\n${red}${i18n.update_no_context}${reset}\n`);
|
|
923
|
+
rl.close();
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const state = detectAgentState(targetDir);
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
- [ ] **Step 2: Add backwards-compat profile check**
|
|
931
|
+
|
|
932
|
+
Insert after the `fs.existsSync` guard and before `const state = detectAgentState(targetDir);`:
|
|
933
|
+
|
|
934
|
+
```js
|
|
935
|
+
const ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
|
|
936
|
+
if ((ctx.profile || 'full') === 'lite') {
|
|
937
|
+
console.log(`\n${yellow}⚠️ Lite install detected. Run --upgrade-to-agentic to add the full board machine first.${reset}\n`);
|
|
938
|
+
rl.close();
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
This guards against accidentally running `--update` (which configures Jules/QA/Sentinel) on a lite install that has no board.
|
|
944
|
+
|
|
945
|
+
- [ ] **Step 3: Syntax check**
|
|
946
|
+
|
|
947
|
+
```bash
|
|
948
|
+
node --check bin/cli.js
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
Expected: no output.
|
|
952
|
+
|
|
953
|
+
- [ ] **Step 4: Commit**
|
|
954
|
+
|
|
955
|
+
```bash
|
|
956
|
+
git add bin/cli.js
|
|
957
|
+
git commit -m "fix(cli): guard runUpdate against lite installs, treat missing profile as full"
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## Task 8: Run all tests + final integration check
|
|
963
|
+
|
|
964
|
+
- [ ] **Step 1: Run unit tests**
|
|
965
|
+
|
|
966
|
+
```bash
|
|
967
|
+
npm test
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
Expected: all `resolveMode` and `buildFullClaudeContent` tests pass.
|
|
971
|
+
|
|
972
|
+
- [ ] **Step 2: Dry-run syntax check on final cli.js**
|
|
973
|
+
|
|
974
|
+
```bash
|
|
975
|
+
node --check bin/cli.js
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
Expected: no output.
|
|
979
|
+
|
|
980
|
+
- [ ] **Step 3: Smoke-test lite mode (no real repo needed)**
|
|
981
|
+
|
|
982
|
+
The `--help` flag doesn't exist, but you can verify the routing by reading the entry point:
|
|
983
|
+
|
|
984
|
+
```bash
|
|
985
|
+
node -e "
|
|
986
|
+
const { execSync } = require('child_process');
|
|
987
|
+
// Patch: override process.argv to simulate no-flag invocation
|
|
988
|
+
// Then verify resolveMode is in scope and returns 'lite'
|
|
989
|
+
const src = require('fs').readFileSync('bin/cli.js', 'utf8');
|
|
990
|
+
const match = src.match(/function resolveMode\(args\)/);
|
|
991
|
+
console.log(match ? '✅ resolveMode found' : '❌ resolveMode missing');
|
|
992
|
+
const entryLite = src.includes(\"mode === 'lite'\") || src.includes('runLiteSetup');
|
|
993
|
+
const entryFull = src.includes(\"mode === 'full'\") || src.includes('runFullSetup');
|
|
994
|
+
const entryUpgrade = src.includes('runUpgradeToAgentic');
|
|
995
|
+
console.log(entryLite ? '✅ runLiteSetup wired' : '❌ runLiteSetup missing');
|
|
996
|
+
console.log(entryFull ? '✅ runFullSetup wired' : '❌ runFullSetup missing');
|
|
997
|
+
console.log(entryUpgrade ? '✅ runUpgradeToAgentic wired' : '❌ runUpgradeToAgentic missing');
|
|
998
|
+
"
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Expected:
|
|
1002
|
+
```
|
|
1003
|
+
✅ resolveMode found
|
|
1004
|
+
✅ runLiteSetup wired
|
|
1005
|
+
✅ runFullSetup wired
|
|
1006
|
+
✅ runUpgradeToAgentic wired
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
- [ ] **Step 4: Verify template paths exist**
|
|
1010
|
+
|
|
1011
|
+
```bash
|
|
1012
|
+
node -e "
|
|
1013
|
+
const fs = require('fs');
|
|
1014
|
+
const checks = [
|
|
1015
|
+
'templates/lite/CLAUDE.md',
|
|
1016
|
+
'templates/lite/AGENTS.md',
|
|
1017
|
+
'templates/full/CLAUDE.md',
|
|
1018
|
+
'templates/full/AGENTS.md',
|
|
1019
|
+
'templates/full/docs/pdlc.md',
|
|
1020
|
+
'templates/.github/ISSUE_TEMPLATE',
|
|
1021
|
+
'templates/.github/workflows/project-automation.yml',
|
|
1022
|
+
'adapters/hooks/pdlc-stage-gate.sh',
|
|
1023
|
+
];
|
|
1024
|
+
for (const p of checks) {
|
|
1025
|
+
console.log(fs.existsSync(p) ? '✅ ' + p : '❌ MISSING: ' + p);
|
|
1026
|
+
}
|
|
1027
|
+
"
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
Expected: all `✅`.
|
|
1031
|
+
|
|
1032
|
+
- [ ] **Step 5: Final commit**
|
|
1033
|
+
|
|
1034
|
+
```bash
|
|
1035
|
+
git add bin/cli.js tests/cli.test.js package.json
|
|
1036
|
+
git commit -m "test: final integration checks pass"
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
## Self-Review
|
|
1042
|
+
|
|
1043
|
+
### Spec coverage
|
|
1044
|
+
|
|
1045
|
+
| AC | Task |
|
|
1046
|
+
|---|---|
|
|
1047
|
+
| `npx create-agentic-pdlc` installs only lite artifacts | Task 4 — `runLiteSetup()` |
|
|
1048
|
+
| `--agentic` installs lite + board workflows | Task 5 — `runFullSetup()` |
|
|
1049
|
+
| `--update` on lite preserves board IDs | Task 7 — guard + no-op on lite |
|
|
1050
|
+
| `--upgrade-to-agentic` adds board without touching lite config | Task 6 — `runUpgradeToAgentic()` |
|
|
1051
|
+
| Legacy install (no `profile` field) treated as `full` | Task 7 — `(ctx.profile \|\| 'full')` |
|
|
1052
|
+
|
|
1053
|
+
### Known gaps
|
|
1054
|
+
|
|
1055
|
+
- `scaffoldLiteTemplates` does not copy a `pdlc-stage-gate.yml` CI workflow to the target `.github/workflows/` — lite uses only branch protection, no CI workflow. This is intentional per the spec ("No workflows beyond branch protection and CI"). If a minimal `pdlc-stage-gate.yml` is needed for the required status check to appear in GitHub, add a step in Task 4 to also copy `templates/.github/workflows/pdlc-stage-gate.yml` to the lite scaffolding.
|
|
1056
|
+
- `runUpgradeToAgentic()` duplicates the board creation logic from `runFullSetup()`. This is acceptable at this scope (Option B). If duplication becomes a burden, extract a `createBoard(repo, boardName, isOrg)` helper — that's a future cleanup, not in scope here.
|