start-vibing-stacks 2.5.1 → 2.7.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.
Files changed (82) hide show
  1. package/dist/detector.js +5 -2
  2. package/dist/index.js +16 -2
  3. package/dist/migrate.d.ts +27 -0
  4. package/dist/migrate.js +217 -0
  5. package/dist/scanner.js +91 -0
  6. package/dist/setup.js +10 -0
  7. package/package.json +1 -1
  8. package/stacks/_shared/agents/claude-md-compactor.md +1 -0
  9. package/stacks/_shared/agents/commit-manager.md +1 -0
  10. package/stacks/_shared/agents/documenter.md +1 -0
  11. package/stacks/_shared/agents/domain-updater.md +1 -0
  12. package/stacks/_shared/agents/research-web.md +1 -0
  13. package/stacks/_shared/agents/security-auditor.md +168 -0
  14. package/stacks/_shared/agents/tester.md +1 -0
  15. package/stacks/_shared/hooks/final-check.ts +205 -0
  16. package/stacks/_shared/hooks/stop-validator.ts +77 -1
  17. package/stacks/_shared/skills/accessibility-wcag22/SKILL.md +284 -0
  18. package/stacks/_shared/skills/ci-pipelines/SKILL.md +166 -0
  19. package/stacks/_shared/skills/codebase-knowledge/SKILL.md +5 -0
  20. package/stacks/_shared/skills/database-migrations/SKILL.md +256 -0
  21. package/stacks/_shared/skills/debugging-patterns/SKILL.md +5 -0
  22. package/stacks/_shared/skills/docker-patterns/SKILL.md +5 -0
  23. package/stacks/_shared/skills/docs-tracker/SKILL.md +5 -0
  24. package/stacks/_shared/skills/error-handling/SKILL.md +335 -0
  25. package/stacks/_shared/skills/final-check/SKILL.md +74 -37
  26. package/stacks/_shared/skills/git-workflow/SKILL.md +5 -0
  27. package/stacks/_shared/skills/hook-development/SKILL.md +5 -0
  28. package/stacks/_shared/skills/observability/SKILL.md +351 -0
  29. package/stacks/_shared/skills/performance-patterns/SKILL.md +5 -0
  30. package/stacks/_shared/skills/playwright-automation/SKILL.md +5 -0
  31. package/stacks/_shared/skills/quality-gate/SKILL.md +5 -0
  32. package/stacks/_shared/skills/research-cache/SKILL.md +5 -0
  33. package/stacks/_shared/skills/secrets-management/SKILL.md +245 -0
  34. package/stacks/_shared/skills/security-baseline/SKILL.md +202 -0
  35. package/stacks/_shared/skills/test-coverage/SKILL.md +5 -0
  36. package/stacks/_shared/skills/ui-ux-audit/SKILL.md +5 -0
  37. package/stacks/frontend/react/skills/preline-ui/SKILL.md +5 -0
  38. package/stacks/frontend/react/skills/react-patterns/SKILL.md +5 -0
  39. package/stacks/frontend/react/skills/react-standards/SKILL.md +5 -0
  40. package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +5 -0
  41. package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +5 -0
  42. package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +5 -0
  43. package/stacks/frontend/react/skills/zod-validation/SKILL.md +5 -0
  44. package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +5 -0
  45. package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +5 -0
  46. package/stacks/nodejs/skills/api-security-node/SKILL.md +275 -0
  47. package/stacks/nodejs/skills/bun-runtime/SKILL.md +5 -0
  48. package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +5 -0
  49. package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +5 -0
  50. package/stacks/nodejs/skills/trpc-api/SKILL.md +5 -0
  51. package/stacks/nodejs/skills/typescript-strict/SKILL.md +5 -0
  52. package/stacks/nodejs/stack.json +2 -1
  53. package/stacks/nodejs/workflows/ci.yml +90 -0
  54. package/stacks/nodejs/workflows/security.yml +45 -0
  55. package/stacks/php/skills/api-design/SKILL.md +5 -0
  56. package/stacks/php/skills/api-security/SKILL.md +5 -0
  57. package/stacks/php/skills/composer-workflow/SKILL.md +5 -0
  58. package/stacks/php/skills/external-api-patterns/SKILL.md +5 -0
  59. package/stacks/php/skills/inertia-react/SKILL.md +5 -0
  60. package/stacks/php/skills/laravel-inertia-i18n/SKILL.md +5 -0
  61. package/stacks/php/skills/laravel-octane/SKILL.md +5 -0
  62. package/stacks/php/skills/laravel-patterns/SKILL.md +5 -0
  63. package/stacks/php/skills/mariadb-octane/SKILL.md +5 -0
  64. package/stacks/php/skills/php-patterns/SKILL.md +5 -0
  65. package/stacks/php/skills/phpstan-analysis/SKILL.md +5 -0
  66. package/stacks/php/skills/phpunit-testing/SKILL.md +5 -0
  67. package/stacks/php/skills/security-scan-php/SKILL.md +5 -0
  68. package/stacks/php/workflows/ci.yml +106 -0
  69. package/stacks/php/workflows/security.yml +36 -0
  70. package/stacks/python/skills/api-security-python/SKILL.md +312 -0
  71. package/stacks/python/skills/async-patterns/SKILL.md +5 -0
  72. package/stacks/python/skills/django-patterns/SKILL.md +5 -0
  73. package/stacks/python/skills/fastapi-patterns/SKILL.md +5 -0
  74. package/stacks/python/skills/pydantic-validation/SKILL.md +5 -0
  75. package/stacks/python/skills/pytest-testing/SKILL.md +5 -0
  76. package/stacks/python/skills/python-patterns/SKILL.md +26 -5
  77. package/stacks/python/skills/python-performance/SKILL.md +5 -0
  78. package/stacks/python/skills/scripting-automation/SKILL.md +260 -0
  79. package/stacks/python/stack.json +70 -35
  80. package/stacks/python/workflows/ci.yml +76 -0
  81. package/stacks/python/workflows/security.yml +56 -0
  82. package/templates/CLAUDE-python.md +315 -0
package/dist/detector.js CHANGED
@@ -114,10 +114,8 @@ export function detectNodeFramework(projectDir) {
114
114
  return null;
115
115
  }
116
116
  export function detectPythonFramework(projectDir) {
117
- // Check manage.py (Django)
118
117
  if (existsSync(join(projectDir, 'manage.py')))
119
118
  return 'django';
120
- // Check for FastAPI in requirements or pyproject
121
119
  for (const reqFile of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
122
120
  const filePath = join(projectDir, reqFile);
123
121
  if (existsSync(filePath)) {
@@ -128,5 +126,10 @@ export function detectPythonFramework(projectDir) {
128
126
  return 'flask';
129
127
  }
130
128
  }
129
+ // If Python project detected but no web framework, suggest scripts
130
+ if (existsSync(join(projectDir, 'main.py')) &&
131
+ !existsSync(join(projectDir, 'app', 'main.py'))) {
132
+ return 'scripts';
133
+ }
131
134
  return null;
132
135
  }
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ const PKG_VERSION = JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf
24
24
  // CLI Arguments
25
25
  // =============================================================================
26
26
  const args = process.argv.slice(2);
27
+ const SUBCOMMAND = args[0] && !args[0].startsWith('-') ? args[0] : null;
27
28
  const FLAGS = {
28
29
  force: args.includes('--force'),
29
30
  noClaude: args.includes('--no-claude'),
@@ -31,6 +32,7 @@ const FLAGS = {
31
32
  noInstall: args.includes('--no-install'),
32
33
  help: args.includes('--help') || args.includes('-h'),
33
34
  version: args.includes('--version') || args.includes('-v'),
35
+ apply: args.includes('--apply'),
34
36
  };
35
37
  if (FLAGS.version) {
36
38
  console.log(PKG_VERSION);
@@ -40,10 +42,16 @@ if (FLAGS.help) {
40
42
  console.log(ui.LOGO);
41
43
  console.log(`
42
44
  ${chalk.bold('Usage:')}
43
- npx start-vibing-stacks [options]
45
+ npx start-vibing-stacks [command] [options]
46
+
47
+ ${chalk.bold('Commands:')}
48
+ (default) Setup or resume current project
49
+ migrate Compare installed vs bundled skill/agent versions
50
+ Add --apply to update outdated/missing items
44
51
 
45
52
  ${chalk.bold('Options:')}
46
- --force Overwrite existing configuration
53
+ --force Overwrite existing configuration (default command)
54
+ --apply Apply updates (migrate command)
47
55
  --no-claude Skip Claude Code installation
48
56
  --no-mcp Skip MCP server selection
49
57
  --no-install Skip dependency installation
@@ -57,6 +65,12 @@ if (FLAGS.help) {
57
65
  `);
58
66
  process.exit(0);
59
67
  }
68
+ // Subcommand: migrate
69
+ if (SUBCOMMAND === 'migrate') {
70
+ const { runMigrate } = await import('./migrate.js');
71
+ await runMigrate(process.cwd(), { apply: FLAGS.apply });
72
+ process.exit(0);
73
+ }
60
74
  const AVAILABLE_STACKS = [
61
75
  { id: 'php', name: 'PHP 8.3+', icon: '🐘', available: true },
62
76
  { id: 'nodejs', name: 'Node.js / TypeScript', icon: '📦', available: true },
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Start Vibing Stacks — Migrate
3
+ *
4
+ * Compares the SKILL.md / agent / hook versions installed in .claude/
5
+ * against the bundled stacks/<stack>/ and stacks/_shared/ versions.
6
+ *
7
+ * Reports outdated, missing, and modified items. Optionally upgrades.
8
+ *
9
+ * Skill version contract:
10
+ * YAML frontmatter at top of SKILL.md must include `version: X.Y.Z`.
11
+ * No frontmatter = treated as "v0" / pre-versioning.
12
+ */
13
+ export interface MigrateItem {
14
+ kind: 'skill' | 'agent' | 'hook';
15
+ name: string;
16
+ source: string;
17
+ target: string;
18
+ bundledVersion: string;
19
+ installedVersion: string | null;
20
+ status: 'missing' | 'outdated' | 'current' | 'ahead' | 'modified-no-version';
21
+ }
22
+ export interface MigrateOptions {
23
+ apply: boolean;
24
+ scope?: 'skills' | 'agents' | 'hooks' | 'all';
25
+ }
26
+ export declare function planMigration(projectDir: string, opts: MigrateOptions): MigrateItem[];
27
+ export declare function runMigrate(projectDir: string, opts: MigrateOptions): Promise<void>;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Start Vibing Stacks — Migrate
3
+ *
4
+ * Compares the SKILL.md / agent / hook versions installed in .claude/
5
+ * against the bundled stacks/<stack>/ and stacks/_shared/ versions.
6
+ *
7
+ * Reports outdated, missing, and modified items. Optionally upgrades.
8
+ *
9
+ * Skill version contract:
10
+ * YAML frontmatter at top of SKILL.md must include `version: X.Y.Z`.
11
+ * No frontmatter = treated as "v0" / pre-versioning.
12
+ */
13
+ import { existsSync, readFileSync, copyFileSync, mkdirSync, statSync, readdirSync } from 'fs';
14
+ import { join, relative, dirname, resolve } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import * as semver from 'semver';
17
+ import * as ui from './ui.js';
18
+ const __m_filename = fileURLToPath(import.meta.url);
19
+ const __m_dirname = dirname(__m_filename);
20
+ const CLI_ROOT = resolve(__m_dirname, '..');
21
+ const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---/;
22
+ const VERSION_RE = /^version:\s*["']?([0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?)["']?\s*$/m;
23
+ function parseVersion(file) {
24
+ if (!existsSync(file))
25
+ return null;
26
+ try {
27
+ const content = readFileSync(file, 'utf8');
28
+ const fm = FRONTMATTER_RE.exec(content);
29
+ if (!fm)
30
+ return null;
31
+ const v = VERSION_RE.exec(fm[1] ?? '');
32
+ return v?.[1] ?? null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function statusOf(bundled, installed) {
39
+ if (installed === null)
40
+ return 'modified-no-version';
41
+ const cmp = semver.compare(installed, bundled);
42
+ if (cmp < 0)
43
+ return 'outdated';
44
+ if (cmp > 0)
45
+ return 'ahead';
46
+ return 'current';
47
+ }
48
+ function listSubdirs(dir) {
49
+ if (!existsSync(dir))
50
+ return [];
51
+ try {
52
+ return readdirSync(dir).filter(n => {
53
+ try {
54
+ return statSync(join(dir, n)).isDirectory();
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ });
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ function listFiles(dir, suffix) {
66
+ if (!existsSync(dir))
67
+ return [];
68
+ try {
69
+ return readdirSync(dir).filter(n => n.endsWith(suffix));
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ function listSkillSources(stack, frontendSkillsDir) {
76
+ const out = [];
77
+ const sharedDir = join(CLI_ROOT, 'stacks', '_shared', 'skills');
78
+ const stackDir = join(CLI_ROOT, 'stacks', stack, 'skills');
79
+ const frontendDir = frontendSkillsDir
80
+ ? join(CLI_ROOT, 'stacks', 'frontend', frontendSkillsDir, 'skills')
81
+ : null;
82
+ for (const dir of [sharedDir, stackDir, frontendDir].filter(Boolean)) {
83
+ for (const entry of listSubdirs(dir)) {
84
+ const skillPath = join(dir, entry, 'SKILL.md');
85
+ if (existsSync(skillPath))
86
+ out.push({ source: skillPath, name: entry });
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ function listAgentSources() {
92
+ const dir = join(CLI_ROOT, 'stacks', '_shared', 'agents');
93
+ return listFiles(dir, '.md').map(n => ({ source: join(dir, n), name: n }));
94
+ }
95
+ function listHookSources() {
96
+ const dir = join(CLI_ROOT, 'stacks', '_shared', 'hooks');
97
+ return listFiles(dir, '.ts').map(n => ({ source: join(dir, n), name: n }));
98
+ }
99
+ function loadProjectConfig(projectDir) {
100
+ const path = join(projectDir, '.claude', 'config', 'active-project.json');
101
+ if (!existsSync(path))
102
+ return null;
103
+ try {
104
+ return JSON.parse(readFileSync(path, 'utf8'));
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ export function planMigration(projectDir, opts) {
111
+ const config = loadProjectConfig(projectDir);
112
+ if (!config) {
113
+ ui.error('No .claude/config/active-project.json found. Run `npx start-vibing-stacks` first.');
114
+ return [];
115
+ }
116
+ const items = [];
117
+ const scope = opts.scope ?? 'all';
118
+ if (scope === 'all' || scope === 'skills') {
119
+ for (const { source, name } of listSkillSources(config.stack, config.frontendSkillsDir)) {
120
+ const target = join(projectDir, '.claude', 'skills', name, 'SKILL.md');
121
+ const bundledVersion = parseVersion(source);
122
+ if (!bundledVersion)
123
+ continue;
124
+ const installedVersion = parseVersion(target);
125
+ const status = !existsSync(target)
126
+ ? 'missing'
127
+ : statusOf(bundledVersion, installedVersion);
128
+ items.push({ kind: 'skill', name, source, target, bundledVersion, installedVersion, status });
129
+ }
130
+ }
131
+ if (scope === 'all' || scope === 'agents') {
132
+ for (const { source, name } of listAgentSources()) {
133
+ const target = join(projectDir, '.claude', 'agents', name);
134
+ const bundledVersion = parseVersion(source);
135
+ if (!bundledVersion)
136
+ continue;
137
+ const installedVersion = parseVersion(target);
138
+ const status = !existsSync(target)
139
+ ? 'missing'
140
+ : statusOf(bundledVersion, installedVersion);
141
+ items.push({ kind: 'agent', name, source, target, bundledVersion, installedVersion, status });
142
+ }
143
+ }
144
+ if (scope === 'all' || scope === 'hooks') {
145
+ for (const { source, name } of listHookSources()) {
146
+ const target = join(projectDir, '.claude', 'hooks', name);
147
+ const bundledVersion = '0.0.0';
148
+ const installedVersion = existsSync(target) ? '0.0.0' : null;
149
+ const status = !existsSync(target) ? 'missing' : 'current';
150
+ items.push({ kind: 'hook', name, source, target, bundledVersion, installedVersion, status });
151
+ }
152
+ }
153
+ return items;
154
+ }
155
+ function applyOne(item) {
156
+ mkdirSync(dirname(item.target), { recursive: true });
157
+ copyFileSync(item.source, item.target);
158
+ }
159
+ export async function runMigrate(projectDir, opts) {
160
+ ui.header('🔄 Start Vibing — Migrate');
161
+ const items = planMigration(projectDir, opts);
162
+ if (items.length === 0) {
163
+ ui.info('Nothing to migrate.');
164
+ return;
165
+ }
166
+ const grouped = {
167
+ missing: [], outdated: [], current: [], ahead: [], 'modified-no-version': [],
168
+ };
169
+ for (const it of items)
170
+ grouped[it.status].push(it);
171
+ const summary = (label, list) => {
172
+ if (list.length === 0)
173
+ return;
174
+ console.log(`\n ${label} (${list.length}):`);
175
+ for (const it of list.slice(0, 30)) {
176
+ const v = it.installedVersion ?? '–';
177
+ console.log(` ${it.kind.padEnd(5)} ${it.name.padEnd(32)} installed=${v.padEnd(8)} bundled=${it.bundledVersion}`);
178
+ }
179
+ if (list.length > 30)
180
+ console.log(` ... and ${list.length - 30} more`);
181
+ };
182
+ summary('MISSING', grouped.missing);
183
+ summary('OUTDATED', grouped.outdated);
184
+ summary('AHEAD (installed newer than bundled — kept)', grouped.ahead);
185
+ summary('UNVERSIONED (manual review)', grouped['modified-no-version']);
186
+ summary('CURRENT', grouped.current);
187
+ const upgradable = [...grouped.missing, ...grouped.outdated];
188
+ if (upgradable.length === 0) {
189
+ console.log('');
190
+ ui.success('Everything up to date.');
191
+ return;
192
+ }
193
+ if (!opts.apply) {
194
+ console.log('');
195
+ ui.info(`Run with --apply to install/update ${upgradable.length} item(s).`);
196
+ return;
197
+ }
198
+ console.log('');
199
+ for (const it of upgradable) {
200
+ try {
201
+ applyOne(it);
202
+ ui.success(`updated ${it.kind} ${it.name} (${it.installedVersion ?? '–'} → ${it.bundledVersion})`);
203
+ }
204
+ catch (err) {
205
+ ui.warn(`failed ${it.kind} ${it.name}: ${err instanceof Error ? err.message : String(err)}`);
206
+ }
207
+ }
208
+ console.log('');
209
+ ui.success(`Migration complete: ${upgradable.length} item(s) updated.`);
210
+ if (grouped['modified-no-version'].length > 0) {
211
+ console.log('');
212
+ ui.warn(`${grouped['modified-no-version'].length} unversioned local item(s) skipped — review manually.`);
213
+ for (const it of grouped['modified-no-version'].slice(0, 10)) {
214
+ console.log(` ${relative(projectDir, it.target)}`);
215
+ }
216
+ }
217
+ }
package/dist/scanner.js CHANGED
@@ -322,6 +322,69 @@ function scanEslintConfig(projectDir) {
322
322
  ],
323
323
  };
324
324
  }
325
+ // ─── Python Scanners ───────────────────────────────────────────────────────
326
+ const PYTHON_PACKAGES = {
327
+ 'fastapi': { category: 'framework', name: 'FastAPI framework' },
328
+ 'django': { category: 'framework', name: 'Django framework' },
329
+ 'flask': { category: 'framework', name: 'Flask framework' },
330
+ 'uvicorn': { category: 'server', name: 'Uvicorn ASGI server' },
331
+ 'gunicorn': { category: 'server', name: 'Gunicorn WSGI server' },
332
+ 'sqlalchemy': { category: 'database', name: 'SQLAlchemy ORM' },
333
+ 'alembic': { category: 'database', name: 'Alembic migrations' },
334
+ 'asyncpg': { category: 'database', name: 'asyncpg (PostgreSQL async)' },
335
+ 'psycopg': { category: 'database', name: 'psycopg (PostgreSQL)' },
336
+ 'mariadb': { category: 'database', name: 'MariaDB connector' },
337
+ 'pymongo': { category: 'database', name: 'PyMongo (MongoDB)' },
338
+ 'beanie': { category: 'database', name: 'Beanie ODM (MongoDB)' },
339
+ 'motor': { category: 'database', name: 'Motor (MongoDB async)' },
340
+ 'pydantic': { category: 'validation', name: 'Pydantic v2 validation' },
341
+ 'pydantic-settings': { category: 'config', name: 'Pydantic Settings' },
342
+ 'httpx': { category: 'http', name: 'httpx HTTP client' },
343
+ 'tenacity': { category: 'reliability', name: 'tenacity retry logic' },
344
+ 'celery': { category: 'queue', name: 'Celery task queue' },
345
+ 'arq': { category: 'queue', name: 'ARQ async task queue' },
346
+ 'redis': { category: 'cache', name: 'Redis client' },
347
+ 'pytest': { category: 'testing', name: 'pytest testing' },
348
+ 'pytest-asyncio': { category: 'testing', name: 'pytest async support' },
349
+ 'mypy': { category: 'quality', name: 'mypy type checker' },
350
+ 'ruff': { category: 'quality', name: 'ruff linter + formatter' },
351
+ 'rich': { category: 'ui', name: 'rich CLI output' },
352
+ 'typer': { category: 'cli', name: 'Typer CLI framework' },
353
+ 'click': { category: 'cli', name: 'Click CLI framework' },
354
+ 'stripe': { category: 'billing', name: 'Stripe payments' },
355
+ 'google-ads': { category: 'ads', name: 'Google Ads API' },
356
+ 'facebook-business': { category: 'ads', name: 'Facebook/Meta Ads API' },
357
+ 'python-wordpress-xmlrpc': { category: 'cms', name: 'WordPress XML-RPC client' },
358
+ };
359
+ function scanPyprojectToml(projectDir) {
360
+ const content = readFileIfExists(join(projectDir, 'pyproject.toml')) ||
361
+ readFileIfExists(join(projectDir, 'requirements.txt'));
362
+ if (!content)
363
+ return null;
364
+ const patterns = [];
365
+ const lowerContent = content.toLowerCase();
366
+ for (const [pkg, meta] of Object.entries(PYTHON_PACKAGES)) {
367
+ if (lowerContent.includes(pkg.toLowerCase())) {
368
+ patterns.push({
369
+ category: meta.category,
370
+ name: meta.name,
371
+ confidence: 95,
372
+ detail: `Found: ${pkg}`,
373
+ });
374
+ }
375
+ }
376
+ const pythonVersionMatch = content.match(/requires-python\s*=\s*"([^"]+)"/i);
377
+ if (pythonVersionMatch) {
378
+ patterns.push({
379
+ category: 'runtime',
380
+ name: `Python version constraint: ${pythonVersionMatch[1]}`,
381
+ confidence: 100,
382
+ });
383
+ }
384
+ if (patterns.length === 0)
385
+ return null;
386
+ return { source: 'pyproject.toml', patterns };
387
+ }
325
388
  // ─── Shared Scanners ───────────────────────────────────────────────────────
326
389
  function scanProjectFiles(projectDir) {
327
390
  const patterns = [];
@@ -399,6 +462,33 @@ function scanProjectFiles(projectDir) {
399
462
  else if (existsSync(join(projectDir, 'yarn.lock'))) {
400
463
  patterns.push({ category: 'runtime', name: 'Yarn package manager in use', confidence: 100 });
401
464
  }
465
+ // ── Python: Project configs ──
466
+ if (existsSync(join(projectDir, 'pyproject.toml'))) {
467
+ patterns.push({ category: 'runtime', name: 'pyproject.toml present (modern Python)', confidence: 100 });
468
+ }
469
+ if (existsSync(join(projectDir, 'uv.lock'))) {
470
+ patterns.push({ category: 'runtime', name: 'uv package manager in use', confidence: 100 });
471
+ }
472
+ else if (existsSync(join(projectDir, 'Pipfile.lock'))) {
473
+ patterns.push({ category: 'runtime', name: 'Pipenv package manager in use', confidence: 100 });
474
+ }
475
+ if (existsSync(join(projectDir, 'mypy.ini')) || existsSync(join(projectDir, '.mypy.ini'))) {
476
+ patterns.push({ category: 'quality', name: 'mypy config present', confidence: 100 });
477
+ }
478
+ if (existsSync(join(projectDir, 'ruff.toml')) || existsSync(join(projectDir, '.ruff.toml'))) {
479
+ patterns.push({ category: 'quality', name: 'ruff config present', confidence: 100 });
480
+ }
481
+ if (existsSync(join(projectDir, 'alembic.ini'))) {
482
+ patterns.push({ category: 'database', name: 'Alembic migrations present', confidence: 100 });
483
+ }
484
+ if (existsSync(join(projectDir, 'pytest.ini')) || existsSync(join(projectDir, 'pyproject.toml'))) {
485
+ if (existsSync(join(projectDir, 'tests'))) {
486
+ patterns.push({ category: 'testing', name: 'pytest test directory present', confidence: 90 });
487
+ }
488
+ }
489
+ if (existsSync(join(projectDir, '.pre-commit-config.yaml'))) {
490
+ patterns.push({ category: 'quality', name: 'pre-commit hooks configured', confidence: 100 });
491
+ }
402
492
  // ── Deploy targets ──
403
493
  if (existsSync(join(projectDir, 'vercel.json'))) {
404
494
  patterns.push({ category: 'deploy', name: 'Vercel deployment config', confidence: 100 });
@@ -485,6 +575,7 @@ export function scanProjectStandards(projectDir) {
485
575
  scanPackageJson,
486
576
  scanTsConfig,
487
577
  scanEslintConfig,
578
+ scanPyprojectToml,
488
579
  scanProjectFiles,
489
580
  ];
490
581
  const results = [];
package/dist/setup.js CHANGED
@@ -169,6 +169,16 @@ export async function setupProject(projectDir, config, options = {}) {
169
169
  spinner.text = `Imported ${config.standardsReview.patterns.length} project standards`;
170
170
  }
171
171
  }
172
+ // 11d. Copy CI workflow templates (only when target dir is empty or --force)
173
+ const stackWorkflowsDir = join(PACKAGE_ROOT, 'stacks', config.stack, 'workflows');
174
+ if (existsSync(stackWorkflowsDir)) {
175
+ const ghWorkflowsDir = join(projectDir, '.github', 'workflows');
176
+ const targetIsEmpty = !existsSync(ghWorkflowsDir);
177
+ if (targetIsEmpty || options.force) {
178
+ copyDirRecursive(stackWorkflowsDir, ghWorkflowsDir, options.force);
179
+ spinner.text = 'Installed CI workflow templates';
180
+ }
181
+ }
172
182
  // 12. Copy commands
173
183
  const sharedCommandsDir = join(PACKAGE_ROOT, 'stacks', '_shared', 'commands');
174
184
  if (existsSync(sharedCommandsDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.5.1",
3
+ "version": "2.7.0",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: claude-md-compactor
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke when CLAUDE.md exceeds 40k chars OR auto memory MEMORY.md exceeds 200 lines. Compacts while preserving critical knowledge by offloading to topic files."
4
5
  model: sonnet
5
6
  tools: Read, Write, Edit, Bash, Grep, Glob
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: commit-manager
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke as FINAL AGENT when implementation is complete. Creates conventional commits, merges to main."
4
5
  model: haiku
5
6
  tools: Read, Write, Edit, Bash, Grep, Glob
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: documenter
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke AFTER any code implementation. Creates/updates domain documentation. PROACTIVELY runs after implementation."
4
5
  model: sonnet
5
6
  tools: Read, Write, Edit, Grep, Glob, Bash
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: domain-updater
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke BEFORE commit-manager at session end. Records problems, solutions, and learnings in domain docs."
4
5
  model: haiku
5
6
  tools: Read, Write, Edit, Bash, Grep, Glob
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: research-web
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke BEFORE implementing any new feature or technology. Triggers: new feature, new technology, 'search', 'find info'. Web research specialist."
4
5
  model: sonnet
5
6
  tools: WebSearch, WebFetch, Read, Write
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: security-auditor
3
+ version: 1.0.0
4
+ description: "AUTOMATICALLY invoke when code touches auth, sessions, user data, passwords, tokens, API routes, database queries, cookies, or env vars. VETO POWER — blocks insecure code. Runs AFTER tester, BEFORE quality-gate."
5
+ model: sonnet
6
+ tools: Read, Grep, Glob, Bash
7
+ skills: security-baseline, secrets-management
8
+ ---
9
+
10
+ # Security Auditor Agent
11
+
12
+ You audit code for security flaws. **You have VETO power** — when violations are found you block the workflow and require fixes.
13
+
14
+ ## When You Run
15
+
16
+ After implementation and tester, **before** quality-gate and commit-manager. Always run when modified files include:
17
+
18
+ - Auth, session, login, register, password, token, JWT
19
+ - Route Handlers, Server Actions, controllers, API endpoints
20
+ - Database queries, ORM models
21
+ - Cookie / header / CORS / CSP configuration
22
+ - File uploads
23
+ - Anything reading `process.env` / `os.environ` / `$_ENV` / `env()`
24
+
25
+ ## Step 1 — Read Stack Context
26
+
27
+ ```bash
28
+ cat .claude/config/active-project.json
29
+ ```
30
+
31
+ Branch logic on `stack`:
32
+ - `nodejs` → load `api-security-node` skill
33
+ - `python` → load `api-security-python` skill
34
+ - `php` → load `api-security` skill
35
+ - always → `security-baseline`, `secrets-management`
36
+
37
+ ## Step 2 — Identify Modified Files
38
+
39
+ ```bash
40
+ git diff --name-only --diff-filter=AM HEAD
41
+ git diff --name-only --cached --diff-filter=AM
42
+ ```
43
+
44
+ Read each modified source file.
45
+
46
+ ## Step 3 — Run the Audit Matrix
47
+
48
+ Apply each check below. **One violation = block.**
49
+
50
+ ### A. Authn / Session
51
+
52
+ | Check | Pattern that fails |
53
+ |---|---|
54
+ | User ID from session, not body | `req.body.userId`, `request.json()["user_id"]`, `$request->input('user_id')` used for ownership |
55
+ | Auth gate before logic | Route handler with no `auth()` / `Depends(current_user)` / `auth:sanctum` middleware |
56
+ | Algorithm pinned on JWT | `jwt.verify(token)` without `algorithms` array |
57
+ | Token in HttpOnly cookie | `localStorage.setItem('token', ...)` or token rendered into HTML |
58
+
59
+ ### B. Authz
60
+
61
+ | Check | Pattern that fails |
62
+ |---|---|
63
+ | Object-level scope | `Model.findById(id)` with no `where userId = session.user.id` |
64
+ | Role check on server | Role check only in client/UI |
65
+ | Mass assignment guard | `User.create(req.body)` without allowlist / Zod `.strict()` / Pydantic `extra="forbid"` / `$fillable` |
66
+
67
+ ### C. Input Validation
68
+
69
+ | Check | Pattern that fails |
70
+ |---|---|
71
+ | Schema at boundary | Route handler reads `req.body` / `request.json()` without Zod / Pydantic / FormRequest |
72
+ | Strict mode | Schema present but allows extra keys |
73
+ | Mongo operator injection | `User.findOne({ email: req.body.email })` without coercing email to string |
74
+ | SQL bindings | f-string / template-literal SQL: `f"... {x} ..."`, `\`SELECT ... ${x}\``, `"... $x ..."` |
75
+
76
+ ### D. Secrets / Env
77
+
78
+ Run secrets scan:
79
+
80
+ ```bash
81
+ # Quick high-signal grep
82
+ git diff --cached -U0 \
83
+ | grep -E '(api[_-]?key|secret|token|password|bearer|aws_|private_key)' \
84
+ | grep -vE '\.env\.example|TEMPLATE|placeholder|example|<your|YOUR_|XXXX'
85
+ ```
86
+
87
+ Also check:
88
+ - No `NEXT_PUBLIC_` containing `SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL`
89
+ - No hardcoded connection string with embedded password
90
+ - `.env` is in `.gitignore`
91
+ - `.env.example` exists if any env var is read
92
+
93
+ ### E. Cookies
94
+
95
+ | Check | Required |
96
+ |---|---|
97
+ | `httpOnly: true` | yes |
98
+ | `secure: true` in prod | yes |
99
+ | `sameSite` set | `lax` or `strict` |
100
+
101
+ ### F. CORS / Headers
102
+
103
+ - No `origin: '*'` or `allow_origins=["*"]` combined with `credentials: true` / `allow_credentials=True`
104
+ - Security headers configured (Helmet / middleware): HSTS, CSP, X-Content-Type-Options, Referrer-Policy
105
+
106
+ ### G. Rate Limiting
107
+
108
+ - Auth endpoints (`/login`, `/register`, `/password/reset`) have a rate limiter
109
+ - Webhook endpoints reject requests without verified signature
110
+
111
+ ### H. Logging
112
+
113
+ - No `console.log(req.body)` / `print(request.json())` / `Log::info($request->all())`
114
+ - No `console.log(token)` / logging of cookies, headers, or `Authorization`
115
+ - No PII (email, phone, full name) in logs without redaction
116
+
117
+ ### I. SSRF
118
+
119
+ - User-supplied URLs are validated against an allowlist OR private IP ranges are blocked
120
+
121
+ ### J. Webhook Verification
122
+
123
+ - Stripe / GitHub / GitHub App webhooks call `constructEvent` / signature verifier **before** parsing body
124
+
125
+ ## Step 4 — Report
126
+
127
+ If everything passes, output:
128
+
129
+ ```
130
+ ✅ Security audit passed.
131
+ Files audited: <n>
132
+ Checks: A-J all green.
133
+ ```
134
+
135
+ If violations are found, output:
136
+
137
+ ```
138
+ 🛑 SECURITY AUDIT BLOCKED
139
+
140
+ <n> violation(s) found:
141
+
142
+ 1. [HIGH] <file>:<line>
143
+ Issue: <one line>
144
+ Fix: <one line>
145
+ Reference: security-baseline §A01 (or whichever)
146
+
147
+ 2. [MEDIUM] ...
148
+ ```
149
+
150
+ Severity:
151
+ - **CRITICAL** — RCE, SQLi, missing auth, secret in commit. Block immediately.
152
+ - **HIGH** — Authz bypass, missing CSRF, unsafe cookie. Block.
153
+ - **MEDIUM** — Missing rate limit, weak headers. Block.
154
+ - **LOW** — Minor logging concern. Warn, allow.
155
+
156
+ ## Rules
157
+
158
+ 1. **VETO POWER** — `domain-updater` and `commit-manager` MUST NOT run while you have unresolved CRITICAL/HIGH/MEDIUM findings.
159
+ 2. **READ THE CODE** — never approve based on file names alone.
160
+ 3. **NO FALSE NEGATIVES > FALSE POSITIVES** — when in doubt, flag and explain.
161
+ 4. **CITE THE FIX** — every finding has a one-line fix and a skill reference.
162
+ 5. **RE-RUN AFTER FIXES** — never trust "I fixed it" without re-reading the file.
163
+
164
+ ## See Also
165
+
166
+ - Skill `security-baseline` — universal OWASP rules
167
+ - Skill `secrets-management` — env hygiene
168
+ - Stack-specific skill: `api-security-node` / `api-security-python` / PHP `api-security`
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: tester
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke AFTER implementing any function or utility. Creates tests using the stack-appropriate framework."
4
5
  model: sonnet
5
6
  tools: Read, Write, Edit, Bash, Grep, Glob