aped-method 1.0.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aped-method",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold the APED pipeline (Analyze, PRD, Epics, Dev, Review) into any Claude Code project",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  import { createInterface } from 'node:readline';
2
+ import { existsSync, readFileSync, writeFileSync as writeFS } from 'node:fs';
2
3
  import { stdin, stdout } from 'node:process';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import { scaffold } from './scaffold.js';
4
7
 
8
+ // ── CLI version (read from package.json) ──
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
11
+
5
12
  const DEFAULTS = {
6
13
  apedDir: '.aped',
7
14
  outputDir: 'docs/aped',
@@ -10,8 +17,13 @@ const DEFAULTS = {
10
17
  projectName: '',
11
18
  communicationLang: 'english',
12
19
  documentLang: 'english',
20
+ ticketSystem: 'none',
21
+ gitProvider: 'github',
13
22
  };
14
23
 
24
+ const TICKET_OPTIONS = ['none', 'jira', 'linear', 'github-issues', 'gitlab-issues'];
25
+ const GIT_OPTIONS = ['github', 'gitlab', 'bitbucket'];
26
+
15
27
  // ── ANSI helpers ──
16
28
  const ESC = '\x1b';
17
29
  const a = {
@@ -27,24 +39,16 @@ const a = {
27
39
  magenta: `${ESC}[35m`,
28
40
  cyan: `${ESC}[36m`,
29
41
  white: `${ESC}[37m`,
30
- // 256-color greens for richer palette
31
- lime: `${ESC}[38;5;118m`, // bright lime
32
- emerald: `${ESC}[38;5;42m`, // emerald
33
- mint: `${ESC}[38;5;48m`, // mint
34
- forest: `${ESC}[38;5;34m`, // forest
35
- spring: `${ESC}[38;5;82m`, // spring green
42
+ lime: `${ESC}[38;5;118m`,
43
+ emerald: `${ESC}[38;5;42m`,
44
+ mint: `${ESC}[38;5;48m`,
45
+ forest: `${ESC}[38;5;34m`,
46
+ spring: `${ESC}[38;5;82m`,
36
47
  };
37
48
 
38
49
  const bold = (s) => `${a.bold}${s}${a.reset}`;
39
50
  const dim = (s) => `${a.dim}${s}${a.reset}`;
40
- const green = (s) => `${a.green}${s}${a.reset}`;
41
- const lime = (s) => `${a.lime}${s}${a.reset}`;
42
- const emerald = (s) => `${a.emerald}${s}${a.reset}`;
43
- const mint = (s) => `${a.mint}${s}${a.reset}`;
44
51
  const yellow = (s) => `${a.yellow}${s}${a.reset}`;
45
- const magenta = (s) => `${a.magenta}${s}${a.reset}`;
46
- const red = (s) => `${a.red}${s}${a.reset}`;
47
- const cyan = (s) => `${a.cyan}${s}${a.reset}`;
48
52
 
49
53
  // ── ASCII Art Logo ──
50
54
  const LOGO = `
@@ -69,7 +73,7 @@ function createSpinner(text) {
69
73
  let interval;
70
74
  return {
71
75
  start() {
72
- process.stdout.write('\x1b[?25l'); // hide cursor
76
+ process.stdout.write('\x1b[?25l');
73
77
  interval = setInterval(() => {
74
78
  const frame = SPINNER_FRAMES[i % SPINNER_FRAMES.length];
75
79
  process.stdout.write(`\r ${a.emerald}${frame}${a.reset} ${text}`);
@@ -79,7 +83,7 @@ function createSpinner(text) {
79
83
  stop(finalText) {
80
84
  clearInterval(interval);
81
85
  process.stdout.write(`\r ${a.lime}${a.bold}✓${a.reset} ${finalText}\x1b[K\n`);
82
- process.stdout.write('\x1b[?25h'); // show cursor
86
+ process.stdout.write('\x1b[?25h');
83
87
  },
84
88
  fail(finalText) {
85
89
  clearInterval(interval);
@@ -93,7 +97,6 @@ function sleep(ms) {
93
97
  return new Promise((resolve) => setTimeout(resolve, ms));
94
98
  }
95
99
 
96
- // ── Section display ──
97
100
  function sectionHeader(title) {
98
101
  console.log('');
99
102
  console.log(` ${a.emerald}${a.bold}┌─${a.reset} ${a.bold}${title}${a.reset}`);
@@ -104,12 +107,55 @@ function sectionEnd() {
104
107
  console.log(` ${a.emerald}${a.bold}└──────────────────────────────────${a.reset}`);
105
108
  }
106
109
 
110
+ // ── Semver compare (1=a>b, -1=a<b, 0=equal) ──
111
+ function semverCompare(va, vb) {
112
+ const pa = va.split('.').map(Number);
113
+ const pb = vb.split('.').map(Number);
114
+ for (let i = 0; i < 3; i++) {
115
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
116
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
117
+ }
118
+ return 0;
119
+ }
120
+
121
+ // ── Detect existing install & read config ──
122
+ function detectExisting(apedDir) {
123
+ const cwd = process.cwd();
124
+ const configPath = join(cwd, apedDir, 'config.yaml');
125
+ if (!existsSync(configPath)) return null;
126
+
127
+ // Parse existing config.yaml (simple key: value)
128
+ const existing = {};
129
+ try {
130
+ const content = readFileSync(configPath, 'utf-8');
131
+ for (const line of content.split('\n')) {
132
+ const match = line.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
133
+ if (match) existing[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
134
+ }
135
+ } catch { /* ignore */ }
136
+
137
+ return {
138
+ projectName: existing.project_name || '',
139
+ authorName: existing.user_name || '',
140
+ communicationLang: existing.communication_language || DEFAULTS.communicationLang,
141
+ documentLang: existing.document_output_language || DEFAULTS.documentLang,
142
+ apedDir: existing.aped_path || apedDir,
143
+ outputDir: existing.output_path || DEFAULTS.outputDir,
144
+ ticketSystem: existing.ticket_system || DEFAULTS.ticketSystem,
145
+ gitProvider: existing.git_provider || DEFAULTS.gitProvider,
146
+ installedVersion: existing.aped_version || '0.0.0',
147
+ };
148
+ }
149
+
107
150
  // ── Args ──
108
151
  function parseArgs(argv) {
109
152
  const args = {};
110
153
  for (let i = 2; i < argv.length; i++) {
111
154
  const arg = argv[i];
112
155
  if (arg === '--yes' || arg === '-y') { args.yes = true; continue; }
156
+ if (arg === '--update' || arg === '-u') { args.mode = 'update'; continue; }
157
+ if (arg === '--fresh' || arg === '--force') { args.mode = 'fresh'; continue; }
158
+ if (arg === '--version' || arg === '-v') { args.version = true; continue; }
113
159
  const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
114
160
  if (match) {
115
161
  const key = match[1].replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
@@ -147,33 +193,65 @@ export async function run() {
147
193
 
148
194
  let detectedProject = '';
149
195
  try {
150
- const { readFileSync } = await import('node:fs');
151
196
  const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
152
197
  detectedProject = pkg.name || '';
153
198
  } catch {
154
199
  detectedProject = process.cwd().split('/').pop();
155
200
  }
156
201
 
202
+ // ── Version flag ──
203
+ if (args.version) {
204
+ console.log(`aped-method v${CLI_VERSION}`);
205
+ return;
206
+ }
207
+
157
208
  // ── Banner ──
158
209
  console.log(LOGO);
210
+ console.log(`${a.dim} v${CLI_VERSION}${a.reset}`);
211
+ console.log('');
159
212
  console.log(PIPELINE);
160
213
  console.log('');
161
214
 
215
+ // ── Detect existing installation ──
216
+ const apedDir = args.aped || args.apedDir || DEFAULTS.apedDir;
217
+ const existing = detectExisting(apedDir);
218
+
219
+ // ── Version upgrade detection ──
220
+ if (existing && existing.installedVersion !== '0.0.0') {
221
+ if (existing.installedVersion === CLI_VERSION) {
222
+ console.log(` ${a.lime}${a.bold}✓${a.reset} ${dim(`Installed: v${existing.installedVersion} — up to date`)}`);
223
+ } else if (semverCompare(CLI_VERSION, existing.installedVersion) > 0) {
224
+ console.log(` ${a.yellow}${a.bold}↑${a.reset} ${bold(`Upgrade available: v${existing.installedVersion} → v${CLI_VERSION}`)}`);
225
+ console.log(` ${dim(' Run with --update to upgrade engine files')}`);
226
+ } else {
227
+ console.log(` ${a.yellow}${a.bold}!${a.reset} ${dim(`Installed v${existing.installedVersion} is newer than CLI v${CLI_VERSION}`)}`);
228
+ }
229
+ console.log('');
230
+ }
231
+
162
232
  if (args.yes) {
233
+ // Non-interactive mode
234
+ let mode = args.mode || (existing ? 'update' : 'install');
235
+
236
+ const defaults = existing || DEFAULTS;
163
237
  const config = {
164
- projectName: args.project || args.projectName || detectedProject || 'my-project',
165
- authorName: args.author || args.authorName || '',
166
- communicationLang: args.lang || args.communicationLang || DEFAULTS.communicationLang,
167
- documentLang: args.docLang || args.documentLang || DEFAULTS.documentLang,
168
- apedDir: args.aped || args.apedDir || DEFAULTS.apedDir,
169
- outputDir: args.output || args.outputDir || DEFAULTS.outputDir,
238
+ projectName: args.project || args.projectName || defaults.projectName || detectedProject || 'my-project',
239
+ authorName: args.author || args.authorName || defaults.authorName || '',
240
+ communicationLang: args.lang || args.communicationLang || defaults.communicationLang,
241
+ documentLang: args.docLang || args.documentLang || defaults.documentLang,
242
+ apedDir: args.aped || args.apedDir || defaults.apedDir || DEFAULTS.apedDir,
243
+ outputDir: args.output || args.outputDir || defaults.outputDir || DEFAULTS.outputDir,
170
244
  commandsDir: args.commands || args.commandsDir || DEFAULTS.commandsDir,
245
+ ticketSystem: args.tickets || args.ticketSystem || defaults.ticketSystem || DEFAULTS.ticketSystem,
246
+ gitProvider: args.git || args.gitProvider || defaults.gitProvider || DEFAULTS.gitProvider,
247
+ cliVersion: CLI_VERSION,
171
248
  };
172
249
 
173
- await runScaffold(config);
250
+ await runScaffold(config, mode);
174
251
  return;
175
252
  }
176
253
 
254
+ // ── Interactive mode ──
177
255
  const rl = createInterface({ input: stdin, output: stdout });
178
256
 
179
257
  const prompt = stdinLines
@@ -186,20 +264,58 @@ export async function run() {
186
264
  : (question, def) => ask(rl, question, def);
187
265
 
188
266
  try {
267
+ let mode = 'install';
268
+
269
+ // ── Existing install detected ──
270
+ if (existing) {
271
+ const vInfo = existing.installedVersion !== '0.0.0'
272
+ ? ` | v${existing.installedVersion}`
273
+ : '';
274
+ console.log(` ${a.yellow}${a.bold}⚠${a.reset} ${bold('Existing APED installation detected')}`);
275
+ console.log(` ${dim(` Project: ${existing.projectName}${vInfo}`)}`);
276
+ console.log('');
277
+ console.log(` ${a.emerald}│${a.reset} ${bold('1')} ${dim('Update engine')} ${dim('— upgrade skills, scripts, hooks. Preserve config & artifacts')}`);
278
+ console.log(` ${a.emerald}│${a.reset} ${bold('2')} ${dim('Fresh install')} ${dim('— delete everything and start from zero')}`);
279
+ console.log(` ${a.emerald}│${a.reset} ${bold('3')} ${dim('Cancel')}`);
280
+ console.log(` ${a.emerald}│${a.reset}`);
281
+
282
+ const choice = await prompt(`${bold('Choice')}`, '1');
283
+
284
+ if (choice === '3' || choice.toLowerCase() === 'n') {
285
+ console.log(`\n ${yellow('Cancelled.')}\n`);
286
+ return;
287
+ }
288
+
289
+ mode = choice === '2' ? 'fresh' : 'update';
290
+ }
291
+
292
+ // ── Config prompts ──
293
+ // For update: pre-fill with existing values
294
+ const defaults = (mode === 'update' && existing) ? existing : DEFAULTS;
295
+
189
296
  sectionHeader('Configuration');
190
297
 
191
298
  const config = {};
192
- config.projectName = await prompt(`${bold('Project name')}`, detectedProject);
193
- config.authorName = await prompt(`${bold('Author')}`, DEFAULTS.authorName);
194
- config.communicationLang = await prompt(`${bold('Communication language')}`, DEFAULTS.communicationLang);
195
- config.documentLang = await prompt(`${bold('Document language')}`, DEFAULTS.documentLang);
196
- config.apedDir = await prompt(`${bold('APED dir')} ${dim('(engine)')}`, DEFAULTS.apedDir);
197
- config.outputDir = await prompt(`${bold('Output dir')} ${dim('(artifacts)')}`, DEFAULTS.outputDir);
299
+ if (mode === 'update') {
300
+ // In update mode, show current config and only ask what to change
301
+ console.log(` ${a.emerald}│${a.reset} ${dim('Current config loaded. Press Enter to keep, or type new value.')}`);
302
+ console.log(` ${a.emerald}│${a.reset}`);
303
+ }
304
+
305
+ config.projectName = await prompt(`${bold('Project name')}`, defaults.projectName || detectedProject);
306
+ config.authorName = await prompt(`${bold('Author')}`, defaults.authorName);
307
+ config.communicationLang = await prompt(`${bold('Communication language')}`, defaults.communicationLang);
308
+ config.documentLang = await prompt(`${bold('Document language')}`, defaults.documentLang);
309
+ config.apedDir = await prompt(`${bold('APED dir')} ${dim('(engine)')}`, defaults.apedDir || DEFAULTS.apedDir);
310
+ config.outputDir = await prompt(`${bold('Output dir')} ${dim('(artifacts)')}`, defaults.outputDir || DEFAULTS.outputDir);
198
311
  config.commandsDir = await prompt(`${bold('Commands dir')}`, DEFAULTS.commandsDir);
312
+ config.ticketSystem = await prompt(`${bold('Ticket system')} ${dim(`(${TICKET_OPTIONS.join('/')})`)}`, defaults.ticketSystem || DEFAULTS.ticketSystem);
313
+ config.gitProvider = await prompt(`${bold('Git provider')} ${dim(`(${GIT_OPTIONS.join('/')})`)}`, defaults.gitProvider || DEFAULTS.gitProvider);
314
+ config.cliVersion = CLI_VERSION;
199
315
 
200
316
  sectionEnd();
201
317
  console.log('');
202
- printConfig(config);
318
+ printConfig(config, mode);
203
319
  console.log('');
204
320
 
205
321
  const confirm = await prompt(`${bold('Proceed?')}`, 'Y');
@@ -208,50 +324,78 @@ export async function run() {
208
324
  return;
209
325
  }
210
326
 
211
- await runScaffold(config);
327
+ await runScaffold(config, mode);
212
328
  } finally {
213
329
  rl.close();
214
330
  }
215
331
  }
216
332
 
217
- async function runScaffold(config) {
218
- // ── Phase 1: Validating config ──
333
+ async function runScaffold(config, mode) {
334
+ const modeLabel = mode === 'update' ? 'Updating' : mode === 'fresh' ? 'Fresh installing' : 'Installing';
335
+ const modeTag = mode === 'update'
336
+ ? `${a.yellow}${a.bold}UPDATE${a.reset}`
337
+ : mode === 'fresh'
338
+ ? `${a.red}${a.bold}FRESH${a.reset}`
339
+ : `${a.lime}${a.bold}INSTALL${a.reset}`;
340
+
341
+ console.log(` ${modeTag} ${dim(modeLabel + ' APED Method...')}`);
342
+ console.log('');
343
+
344
+ // ── Phase 1: Clean if fresh ──
345
+ if (mode === 'fresh') {
346
+ const s0 = createSpinner('Removing existing installation...');
347
+ s0.start();
348
+ const { rmSync } = await import('node:fs');
349
+ const cwd = process.cwd();
350
+ try { rmSync(join(cwd, config.apedDir), { recursive: true, force: true }); } catch { /* ok */ }
351
+ try { rmSync(join(cwd, config.outputDir), { recursive: true, force: true }); } catch { /* ok */ }
352
+ // Don't delete commands dir — may have non-aped commands
353
+ // Delete only aped-* command files
354
+ const { readdirSync } = await import('node:fs');
355
+ try {
356
+ const cmdDir = join(cwd, config.commandsDir);
357
+ for (const f of readdirSync(cmdDir)) {
358
+ if (f.startsWith('aped-')) {
359
+ rmSync(join(cmdDir, f), { force: true });
360
+ }
361
+ }
362
+ } catch { /* ok */ }
363
+ await sleep(300);
364
+ s0.stop('Previous installation removed');
365
+ }
366
+
367
+ // ── Phase 2: Validate ──
219
368
  const s1 = createSpinner('Validating configuration...');
220
369
  s1.start();
221
370
  await sleep(400);
222
371
  s1.stop('Configuration validated');
223
372
 
224
- // ── Phase 2: Creating directory structure ──
225
- const s2 = createSpinner('Creating directory structure...');
226
- s2.start();
227
- await sleep(300);
228
- s2.stop('Directory structure ready');
229
-
230
- // ── Phase 3: Scaffolding ──
231
- sectionHeader('Scaffolding Pipeline');
373
+ // ── Phase 3: Scaffold ──
374
+ sectionHeader(`Scaffolding Pipeline ${dim(`(${mode})`)}`);
232
375
  console.log(` ${a.emerald}│${a.reset}`);
233
376
 
234
- const count = await scaffoldWithCheckpoints(config);
377
+ const { created, updated, skipped } = await scaffoldWithCheckpoints(config, mode);
235
378
 
236
379
  sectionEnd();
237
380
 
238
- // ── Phase 4: Setting up hooks ──
381
+ // ── Phase 4: Hooks ──
239
382
  const s3 = createSpinner('Installing guardrail hook...');
240
383
  s3.start();
241
384
  await sleep(350);
242
385
  s3.stop('Guardrail hook installed');
243
386
 
244
- // ── Phase 5: Final verification ──
387
+ // ── Phase 5: Verify ──
388
+ const total = created + updated;
245
389
  const s4 = createSpinner('Verifying installation...');
246
390
  s4.start();
247
391
  await sleep(300);
248
- s4.stop(`Installation verified — ${bold(String(count))} files`);
392
+ s4.stop(`Verified — ${bold(String(total))} files`);
249
393
 
250
394
  // ── Done ──
251
- printDone(count);
395
+ printDone(created, updated, skipped, mode);
252
396
  }
253
397
 
254
- async function scaffoldWithCheckpoints(config) {
398
+ async function scaffoldWithCheckpoints(config, mode) {
255
399
  const { getTemplates } = await import('./templates/index.js');
256
400
  const { mkdirSync, writeFileSync, chmodSync, existsSync } = await import('node:fs');
257
401
  const { join, dirname } = await import('node:path');
@@ -280,7 +424,14 @@ async function scaffoldWithCheckpoints(config) {
280
424
  else groups.config.items.push(tpl);
281
425
  }
282
426
 
283
- let count = 0;
427
+ // Paths to preserve during update (user data, not engine)
428
+ const preserveOnUpdate = new Set([
429
+ join(config.outputDir, 'state.yaml'),
430
+ ]);
431
+
432
+ let created = 0;
433
+ let updated = 0;
434
+ let skipped = 0;
284
435
 
285
436
  for (const [, group] of Object.entries(groups)) {
286
437
  if (group.items.length === 0) continue;
@@ -289,31 +440,108 @@ async function scaffoldWithCheckpoints(config) {
289
440
  sp.start();
290
441
  await sleep(200);
291
442
 
292
- let groupCount = 0;
443
+ let groupCreated = 0;
444
+ let groupUpdated = 0;
445
+ let groupSkipped = 0;
446
+
293
447
  for (const tpl of group.items) {
294
448
  const fullPath = join(cwd, tpl.path);
449
+ const fileExists = existsSync(fullPath);
450
+
295
451
  mkdirSync(dirname(fullPath), { recursive: true });
296
- if (!existsSync(fullPath)) {
452
+
453
+ if (!fileExists) {
454
+ // New file — always create
297
455
  writeFileSync(fullPath, tpl.content, 'utf-8');
298
456
  if (tpl.executable) chmodSync(fullPath, 0o755);
299
- groupCount++;
300
- count++;
457
+ groupCreated++;
458
+ created++;
459
+ } else if (mode === 'update') {
460
+ // File exists + update mode
461
+ if (preserveOnUpdate.has(tpl.path)) {
462
+ // Preserve user artifacts
463
+ groupSkipped++;
464
+ skipped++;
465
+ } else if (tpl.path.endsWith('settings.local.json')) {
466
+ // Merge settings instead of overwrite
467
+ mergeSettings(fullPath, tpl.content);
468
+ groupUpdated++;
469
+ updated++;
470
+ } else {
471
+ // Engine file — overwrite with new version
472
+ writeFileSync(fullPath, tpl.content, 'utf-8');
473
+ if (tpl.executable) chmodSync(fullPath, 0o755);
474
+ groupUpdated++;
475
+ updated++;
476
+ }
477
+ } else if (mode === 'fresh') {
478
+ // Fresh mode — overwrite everything
479
+ writeFileSync(fullPath, tpl.content, 'utf-8');
480
+ if (tpl.executable) chmodSync(fullPath, 0o755);
481
+ groupCreated++;
482
+ created++;
483
+ } else {
484
+ // Install mode — skip existing
485
+ groupSkipped++;
486
+ skipped++;
301
487
  }
302
488
  }
303
489
 
304
- sp.stop(`${group.icon} ${group.label} ${dim(`(${groupCount} files)`)}`);
490
+ const parts = [];
491
+ if (groupCreated > 0) parts.push(`${a.lime}+${groupCreated}${a.reset}`);
492
+ if (groupUpdated > 0) parts.push(`${a.yellow}↑${groupUpdated}${a.reset}`);
493
+ if (groupSkipped > 0) parts.push(`${a.dim}=${groupSkipped}${a.reset}`);
494
+ const stats = parts.length > 0 ? parts.join(' ') : `${a.dim}0${a.reset}`;
495
+
496
+ sp.stop(`${group.icon} ${group.label} ${dim('(')}${stats}${dim(')')}`);
305
497
  }
306
498
 
307
- return count;
499
+ return { created, updated, skipped };
500
+ }
501
+
502
+ function mergeSettings(filePath, newContent) {
503
+ try {
504
+ const existing = JSON.parse(readFileSync(filePath, 'utf-8'));
505
+ const incoming = JSON.parse(newContent);
506
+
507
+ // Merge hooks: add guardrail if not already present
508
+ if (incoming.hooks) {
509
+ if (!existing.hooks) existing.hooks = {};
510
+ for (const [event, handlers] of Object.entries(incoming.hooks)) {
511
+ if (!existing.hooks[event]) {
512
+ existing.hooks[event] = handlers;
513
+ } else {
514
+ // Check if guardrail hook already exists
515
+ for (const handler of handlers) {
516
+ const hookCmds = handler.hooks || [];
517
+ for (const hook of hookCmds) {
518
+ const alreadyExists = existing.hooks[event].some((h) =>
519
+ (h.hooks || []).some((hk) => hk.command === hook.command)
520
+ );
521
+ if (!alreadyExists) {
522
+ existing.hooks[event].push(handler);
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+ }
529
+
530
+ writeFS(filePath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
531
+ } catch {
532
+ // If can't parse existing, overwrite
533
+ writeFS(filePath, newContent, 'utf-8');
534
+ }
308
535
  }
309
536
 
310
- function printConfig(config) {
537
+ function printConfig(config, mode) {
311
538
  const box = (label, value, extra) => {
312
539
  const e = extra ? ` ${dim(extra)}` : '';
313
540
  console.log(` ${a.emerald}│${a.reset} ${dim(label.padEnd(16))}${bold(value)}${e}`);
314
541
  };
315
542
 
316
- console.log(` ${a.emerald}${a.bold}┌─${a.reset} ${bold('Summary')}`);
543
+ const modeTag = mode === 'update' ? ` ${a.yellow}${a.bold}UPDATE${a.reset}` : mode === 'fresh' ? ` ${a.red}${a.bold}FRESH${a.reset}` : '';
544
+ console.log(` ${a.emerald}${a.bold}┌─${a.reset} ${bold('Summary')}${modeTag}`);
317
545
  console.log(` ${a.emerald}│${a.reset}`);
318
546
  box('Project', config.projectName);
319
547
  box('Author', config.authorName || dim('(not set)'));
@@ -322,15 +550,33 @@ function printConfig(config) {
322
550
  box('APED', config.apedDir + '/', 'engine');
323
551
  box('Output', config.outputDir + '/', 'artifacts');
324
552
  box('Commands', config.commandsDir + '/');
553
+ box('Tickets', config.ticketSystem, config.ticketSystem === 'none' ? '' : 'integrated');
554
+ box('Git', config.gitProvider);
555
+
556
+ if (mode === 'update') {
557
+ console.log(` ${a.emerald}│${a.reset}`);
558
+ console.log(` ${a.emerald}│${a.reset} ${a.yellow}${a.bold}↑${a.reset} ${dim('Engine files will be overwritten')}`);
559
+ console.log(` ${a.emerald}│${a.reset} ${a.lime}${a.bold}=${a.reset} ${dim('Config, state & artifacts preserved')}`);
560
+ } else if (mode === 'fresh') {
561
+ console.log(` ${a.emerald}│${a.reset}`);
562
+ console.log(` ${a.emerald}│${a.reset} ${a.red}${a.bold}!${a.reset} ${dim('All existing files will be deleted and recreated')}`);
563
+ }
564
+
325
565
  console.log(` ${a.emerald}│${a.reset}`);
326
566
  console.log(` ${a.emerald}${a.bold}└──────────────────────────────────${a.reset}`);
327
567
  }
328
568
 
329
- function printDone(count) {
569
+ function printDone(created, updated, skipped, mode) {
570
+ const total = created + updated;
330
571
  console.log('');
331
572
  console.log(` ${a.emerald}${a.bold}╔══════════════════════════════════════╗${a.reset}`);
332
- console.log(` ${a.emerald}${a.bold}║${a.reset} ${a.lime}${a.bold}✓${a.reset} ${bold(`${count} files scaffolded`)} ${a.emerald}${a.bold}║${a.reset}`);
333
- console.log(` ${a.emerald}${a.bold}║${a.reset} ${dim('Pipeline ready to use')} ${a.emerald}${a.bold}║${a.reset}`);
573
+ if (mode === 'update') {
574
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${a.lime}${a.bold}✓${a.reset} ${bold('Update complete')} ${a.emerald}${a.bold}║${a.reset}`);
575
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${dim(` +${created} created ↑${updated} updated =${skipped} kept`)} ${a.emerald}${a.bold}║${a.reset}`);
576
+ } else {
577
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${a.lime}${a.bold}✓${a.reset} ${bold(`${total} files scaffolded`)} ${a.emerald}${a.bold}║${a.reset}`);
578
+ console.log(` ${a.emerald}${a.bold}║${a.reset} ${dim('Pipeline ready to use')} ${a.emerald}${a.bold}║${a.reset}`);
579
+ }
334
580
  console.log(` ${a.emerald}${a.bold}╚══════════════════════════════════════╝${a.reset}`);
335
581
  console.log('');
336
582
 
@@ -341,6 +587,8 @@ function printDone(count) {
341
587
  console.log(` ${a.yellow}${a.bold}/aped-e${a.reset} ${dim('Epics — requirements decomposition')}`);
342
588
  console.log(` ${a.lime}${a.bold}/aped-d${a.reset} ${dim('Dev — TDD story implementation')}`);
343
589
  console.log(` ${a.red}${a.bold}/aped-r${a.reset} ${dim('Review — adversarial code review')}`);
590
+ console.log('');
591
+ console.log(` ${a.spring}${a.bold}/aped-quick${a.reset} ${dim('Quick fix/feature — bypass full pipeline')}`);
344
592
  console.log(` ${a.spring}${a.bold}/aped-all${a.reset} ${dim('Full pipeline A→P→E→D→R')}`);
345
593
  console.log('');
346
594
  console.log(` ${dim('Guardrail hook active — pipeline coherence enforced')}`);
@@ -49,6 +49,16 @@ description: 'Adversarial code review for completed story. Use when user says "r
49
49
  ---
50
50
 
51
51
  Read and follow the SKILL.md at $PROJECT_ROOT/${a}/aped-r/SKILL.md
52
+ `,
53
+ },
54
+ {
55
+ path: `${c.commandsDir}/aped-quick.md`,
56
+ content: `---
57
+ name: aped-quick
58
+ description: 'Quick fix/feature bypassing full pipeline. Use when user says "quick fix", "quick feature", "aped quick", or "hotfix"'
59
+ ---
60
+
61
+ Read and follow the SKILL.md at $PROJECT_ROOT/${a}/aped-quick/SKILL.md
52
62
  `,
53
63
  },
54
64
  {
@@ -1,6 +1,9 @@
1
1
  export function configFiles(c) {
2
2
  const a = c.apedDir;
3
3
  const o = c.outputDir;
4
+ const ts = c.ticketSystem || 'none';
5
+ const gp = c.gitProvider || 'github';
6
+ const ver = c.cliVersion || '0.0.0';
4
7
  return [
5
8
  {
6
9
  path: `${a}/config.yaml`,
@@ -11,10 +14,15 @@ communication_language: ${c.communicationLang}
11
14
  document_output_language: ${c.documentLang}
12
15
  aped_path: ${a}
13
16
  output_path: ${o}
17
+ aped_version: ${ver}
18
+
19
+ # Integrations
20
+ ticket_system: ${ts}
21
+ git_provider: ${gp}
14
22
  `,
15
23
  },
16
24
  {
17
- path: `${a}/state.yaml`,
25
+ path: `${o}/state.yaml`,
18
26
  content: `# APED Pipeline State
19
27
  pipeline:
20
28
  current_phase: "none"
@@ -224,6 +232,35 @@ sprint:
224
232
  ### Completion Notes
225
233
 
226
234
  ### File List
235
+ `,
236
+ },
237
+ {
238
+ path: `${a}/templates/quick-spec.md`,
239
+ content: `# Quick Spec: {{title}}
240
+
241
+ **Date:** {{date}}
242
+ **Author:** {{user_name}}
243
+ **Type:** {{fix|feature|refactor}}
244
+
245
+ ## What
246
+
247
+ <!-- 1-2 sentences: what needs to change -->
248
+
249
+ ## Why
250
+
251
+ <!-- 1 sentence: why this change matters now -->
252
+
253
+ ## Acceptance Criteria
254
+
255
+ - [ ] {{criterion}}
256
+
257
+ ## Files to Change
258
+
259
+ - {{file_path}} — {{what to change}}
260
+
261
+ ## Test Plan
262
+
263
+ - {{test description}}
227
264
  `,
228
265
  },
229
266
  ];
@@ -13,7 +13,7 @@ export function guardrail(c) {
13
13
  set -euo pipefail
14
14
 
15
15
  PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
16
- STATE_FILE="$PROJECT_ROOT/${a}/state.yaml"
16
+ STATE_FILE="$PROJECT_ROOT/${o}/state.yaml"
17
17
  CONFIG_FILE="$PROJECT_ROOT/${a}/config.yaml"
18
18
  OUTPUT_DIR="$PROJECT_ROOT/${o}"
19
19
 
@@ -56,6 +56,9 @@ WANTS_CODE=false
56
56
  [[ "$PROMPT_LOWER" =~ (aped-d|/aped-d|dev|implement|code|build|create.*component|create.*service) ]] && WANTS_DEV=true
57
57
  [[ "$PROMPT_LOWER" =~ (aped-r|/aped-r|review|audit) ]] && WANTS_REVIEW=true
58
58
  [[ "$PROMPT_LOWER" =~ (aped-all|/aped-all|full.pipeline|start.from.scratch) ]] && WANTS_ALL=true
59
+ WANTS_QUICK=false
60
+
61
+ [[ "$PROMPT_LOWER" =~ (aped-quick|/aped-quick|quick.fix|quick.feature|hotfix) ]] && WANTS_QUICK=true
59
62
  [[ "$PROMPT_LOWER" =~ (code|implement|write.*function|create.*file|add.*feature|fix.*bug|refactor) ]] && WANTS_CODE=true
60
63
 
61
64
  # ── Check artifact existence ──
@@ -84,6 +87,11 @@ phase_index() {
84
87
 
85
88
  CURRENT_IDX=$(phase_index "$CURRENT_PHASE")
86
89
 
90
+ # Rule 0: Quick mode bypasses pipeline checks
91
+ if [[ "$WANTS_QUICK" == "true" ]]; then
92
+ exit 0
93
+ fi
94
+
87
95
  # Rule 1: Trying to code without epics/stories
88
96
  if [[ "$WANTS_CODE" == "true" || "$WANTS_DEV" == "true" ]] && [[ "$CURRENT_PHASE" != "dev" && "$CURRENT_PHASE" != "review" ]]; then
89
97
  if [[ "$HAS_EPICS" == "false" ]]; then
@@ -1,6 +1,6 @@
1
1
  export function skills(c) {
2
2
  const a = c.apedDir; // .aped (engine: skills, config, templates)
3
- const o = c.outputDir; // docs/aped (output: generated artifacts)
3
+ const o = c.outputDir; // docs/aped (output: generated artifacts + state)
4
4
  return [
5
5
  // ── aped-a ──────────────────────────────────────────────
6
6
  {
@@ -14,8 +14,8 @@ description: 'Analyze project idea through parallel market, domain, and technica
14
14
 
15
15
  ## Setup
16
16
 
17
- 1. Read \`${a}/config.yaml\` — extract \`user_name\`, \`communication_language\`
18
- 2. Read \`${a}/state.yaml\` — check \`pipeline.phases.analyze\`
17
+ 1. Read \`${a}/config.yaml\` — extract \`user_name\`, \`communication_language\`, \`ticket_system\`, \`git_provider\`
18
+ 2. Read \`${o}/state.yaml\` — check \`pipeline.phases.analyze\`
19
19
  - If status is \`done\`: ask user — redo analysis or skip to next phase?
20
20
  - If user skips: invoke Skill tool with \`skill: "aped-p"\` and stop
21
21
 
@@ -80,7 +80,7 @@ If validation fails: fix missing sections and re-validate.
80
80
 
81
81
  ## State Update
82
82
 
83
- Update \`${a}/state.yaml\`:
83
+ Update \`${o}/state.yaml\`:
84
84
  \`\`\`yaml
85
85
  pipeline:
86
86
  current_phase: "analyze"
@@ -108,7 +108,7 @@ description: 'Generate PRD autonomously from product brief. Use when user says "
108
108
  ## Setup
109
109
 
110
110
  1. Read \`${a}/config.yaml\` — extract \`user_name\`, \`communication_language\`, \`document_output_language\`
111
- 2. Read \`${a}/state.yaml\` — check pipeline state
111
+ 2. Read \`${o}/state.yaml\` — check pipeline state
112
112
  - If \`pipeline.phases.prd.status\` is \`done\`: ask user — redo PRD or skip?
113
113
  - If user skips: invoke Skill tool with \`skill: "aped-e"\` and stop
114
114
 
@@ -162,7 +162,7 @@ bash ${a}/aped-p/scripts/validate-prd.sh ${o}/prd.md
162
162
  ## Output & State
163
163
 
164
164
  1. Write PRD to \`${o}/prd.md\`
165
- 2. Update \`${a}/state.yaml\`:
165
+ 2. Update \`${o}/state.yaml\`:
166
166
  \`\`\`yaml
167
167
  pipeline:
168
168
  current_phase: "prd"
@@ -189,8 +189,8 @@ description: 'Create epics and stories from PRD with full FR coverage. Use when
189
189
 
190
190
  ## Setup
191
191
 
192
- 1. Read \`${a}/config.yaml\` — extract config
193
- 2. Read \`${a}/state.yaml\` — check pipeline state
192
+ 1. Read \`${a}/config.yaml\` — extract config including \`ticket_system\`
193
+ 2. Read \`${o}/state.yaml\` — check pipeline state
194
194
  - If \`pipeline.phases.epics.status\` is \`done\`: ask user — redo or skip?
195
195
  - If user skips: invoke Skill tool with \`skill: "aped-d"\` and stop
196
196
 
@@ -225,6 +225,16 @@ Story files: \`${o}/stories/{story-key}.md\`
225
225
  - ACs in **Given/When/Then** format
226
226
  - Tasks as checkboxes: \`- [ ] task [AC: AC#]\`
227
227
 
228
+ ## Ticket System Integration
229
+
230
+ Read \`ticket_system\` from config. If not \`none\`:
231
+ - Add ticket reference in each story header: \`**Ticket:** {{ticket_id}}\`
232
+ - If \`jira\`: format as \`PROJ-###\` placeholder
233
+ - If \`linear\`: format as \`TEAM-###\` placeholder
234
+ - If \`github-issues\`: format as \`#issue_number\` placeholder
235
+ - If \`gitlab-issues\`: format as \`#issue_number\` placeholder
236
+ - Note: actual ticket creation is manual — these are reference placeholders
237
+
228
238
  ## FR Coverage Map
229
239
 
230
240
  Every FR from PRD mapped to exactly one epic. No orphans, no phantoms.
@@ -243,7 +253,7 @@ mkdir -p ${o}/stories
243
253
 
244
254
  1. Write epics to \`${o}/epics.md\`
245
255
  2. Create story files in \`${o}/stories/\` using \`${a}/templates/story.md\`
246
- 3. Update \`${a}/state.yaml\` with sprint section and pipeline phase
256
+ 3. Update \`${o}/state.yaml\` with sprint section and pipeline phase
247
257
 
248
258
  ## Chain
249
259
 
@@ -262,8 +272,8 @@ description: 'Dev sprint - implement next story with TDD red-green-refactor. Use
262
272
 
263
273
  ## Setup
264
274
 
265
- 1. Read \`${a}/config.yaml\` — extract config
266
- 2. Read \`${a}/state.yaml\` — find next story
275
+ 1. Read \`${a}/config.yaml\` — extract config including \`ticket_system\`, \`git_provider\`
276
+ 2. Read \`${o}/state.yaml\` — find next story
267
277
 
268
278
  ## Story Selection
269
279
 
@@ -278,7 +288,7 @@ If story has \`[AI-Review]\` items: address them BEFORE regular tasks.
278
288
 
279
289
  ## State Update (start)
280
290
 
281
- Update \`${a}/state.yaml\`: story — \`in-progress\`, epic — \`in-progress\` if first story.
291
+ Update \`${o}/state.yaml\`: story — \`in-progress\`, epic — \`in-progress\` if first story.
282
292
 
283
293
  ## Context Gathering
284
294
 
@@ -308,10 +318,16 @@ Mark \`[x]\` ONLY when: tests exist, pass 100%, implementation matches, ACs sati
308
318
 
309
319
  **STOP and ask user if:** new dependency, 3 consecutive failures, missing config, ambiguity.
310
320
 
321
+ ## Git Commit Convention
322
+
323
+ Read \`git_provider\` and \`ticket_system\` from config:
324
+ - Commit message format: \`type(scope): description\`
325
+ - If ticket system configured, append ticket ref: \`type(scope): description [TICKET-ID]\`
326
+
311
327
  ## Completion
312
328
 
313
329
  1. Update story: mark tasks \`[x]\`, fill Dev Agent Record
314
- 2. Update \`${a}/state.yaml\`: story — \`review\`
330
+ 2. Update \`${o}/state.yaml\`: story — \`review\`
315
331
  3. Chain to \`/aped-r\`
316
332
  `,
317
333
  },
@@ -327,8 +343,8 @@ description: 'Adversarial code review for completed stories. Use when user says
327
343
 
328
344
  ## Setup
329
345
 
330
- 1. Read \`${a}/config.yaml\` — extract config
331
- 2. Read \`${a}/state.yaml\` — find first story with status \`review\`
346
+ 1. Read \`${a}/config.yaml\` — extract config including \`git_provider\`
347
+ 2. Read \`${o}/state.yaml\` — find first story with status \`review\`
332
348
  - If none: report "No stories pending review" and stop
333
349
 
334
350
  ## Load Story
@@ -367,7 +383,78 @@ Severity: CRITICAL > HIGH > MEDIUM > LOW. Format: \`[Severity] Description [file
367
383
 
368
384
  ## State Update
369
385
 
370
- Update \`${a}/state.yaml\`. If more stories — chain to \`/aped-d\`. If all done — report completion.
386
+ Update \`${o}/state.yaml\`. If more stories — chain to \`/aped-d\`. If all done — report completion.
387
+ `,
388
+ },
389
+ // ── aped-quick ────────────────────────────────────────────
390
+ {
391
+ path: `${a}/aped-quick/SKILL.md`,
392
+ content: `---
393
+ name: aped-quick
394
+ description: 'Quick feature/fix implementation bypassing full pipeline. Use when user says "quick fix", "quick feature", "aped quick", or invokes /aped-quick.'
395
+ ---
396
+
397
+ # APED Quick — Fast Track for Small Changes
398
+
399
+ Use this for isolated fixes, small features, or refactors that don't warrant the full A→P→E→D→R pipeline.
400
+
401
+ ## Setup
402
+
403
+ 1. Read \`${a}/config.yaml\` — extract config
404
+ 2. Read \`${o}/state.yaml\` — note current phase for context
405
+
406
+ ## Scope Check
407
+
408
+ This mode is for changes that:
409
+ - Touch **5 files or fewer**
410
+ - Can be completed in **1 session**
411
+ - Don't introduce **new architectural patterns**
412
+ - Don't require **new dependencies**
413
+
414
+ If any of these are violated, recommend the full pipeline instead.
415
+
416
+ ## Quick Spec (2 minutes)
417
+
418
+ Ask the user:
419
+ 1. **What?** — What needs to change (1-2 sentences)
420
+ 2. **Why?** — Why now, what breaks without it
421
+ 3. **Type?** — fix | feature | refactor
422
+
423
+ Generate a quick spec using \`${a}/templates/quick-spec.md\`:
424
+ - Fill: title, type, what, why, acceptance criteria, files to change, test plan
425
+ - Write to \`${o}/quick-specs/{date}-{slug}.md\`
426
+
427
+ ## Implementation (TDD)
428
+
429
+ Same TDD cycle as aped-d but compressed:
430
+
431
+ 1. **RED** — Write test for the expected behavior
432
+ 2. **GREEN** — Minimal implementation to pass
433
+ 3. **REFACTOR** — Clean up while green
434
+
435
+ Run tests: \`bash ${a}/aped-d/scripts/run-tests.sh\`
436
+
437
+ ## Self-Review (30 seconds)
438
+
439
+ Quick checklist — no full adversarial review:
440
+ - [ ] Tests pass
441
+ - [ ] No security issues introduced
442
+ - [ ] No regressions in existing tests
443
+ - [ ] AC from quick spec satisfied
444
+
445
+ ## Git Commit
446
+
447
+ Read \`ticket_system\` and \`git_provider\` from config.
448
+ - Format: \`type(scope): description\`
449
+ - Append ticket ref if configured
450
+ - If \`git_provider\` is \`github\`: suggest PR creation with \`gh pr create\`
451
+ - If \`git_provider\` is \`gitlab\`: suggest MR creation with \`glab mr create\`
452
+
453
+ ## Output
454
+
455
+ 1. Write quick spec to \`${o}/quick-specs/\` (create dir if needed)
456
+ 2. No state.yaml update — quick specs don't affect pipeline phase
457
+ 3. Report: files changed, tests added, quick spec path
371
458
  `,
372
459
  },
373
460
  // ── aped-all ─────────────────────────────────────────────
@@ -382,7 +469,7 @@ description: 'Run full APED pipeline from Analyze through Review. Use when user
382
469
 
383
470
  ## Resume Logic
384
471
 
385
- 1. Read \`${a}/state.yaml\`
472
+ 1. Read \`${o}/state.yaml\`
386
473
  2. Determine resume point:
387
474
 
388
475
  | State | Action |
@@ -397,11 +484,11 @@ description: 'Run full APED pipeline from Analyze through Review. Use when user
397
484
  ## Execution
398
485
 
399
486
  Use the Skill tool to invoke each phase: aped-a, aped-p, aped-e, aped-d, aped-r.
400
- Each phase updates \`${a}/state.yaml\` and chains automatically.
487
+ Each phase updates \`${o}/state.yaml\` and chains automatically.
401
488
 
402
489
  ## Interruption Handling
403
490
 
404
- State persists in \`${a}/state.yaml\`. Next \`/aped-all\` resumes from last incomplete phase.
491
+ State persists in \`${o}/state.yaml\`. Next \`/aped-all\` resumes from last incomplete phase.
405
492
 
406
493
  ## Completion Report
407
494