analyzthis_design 2.1.2 → 2.2.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/README.md CHANGED
@@ -4,6 +4,12 @@ A set of AI design personas and a task-first evaluation framework that plugs int
4
4
 
5
5
  Install once. Run structured UX critiques, multi-phase ideation, and task-grounded screen reviews — directly inside your AI chat. **No external LLM API keys required** for CLI orchestrator runs: **`/devi`** voices each persona from your host IDE (Cursor, Claude, etc.).
6
6
 
7
+ ## v2.2 — project-scoped knowledge bank
8
+
9
+ Knowledge sources are now scoped per project by default. `collect`, `connect`, `sync`, `disconnect`, and `status` operate on the project derived from your current working directory, and the built `knowledge-bank` skill is written into that project's local skills directory (`<project>/.claude/skills/knowledge-bank/SKILL.md`, `<project>/.cursor/skills/...`, etc.). Invoking a skill from one project never reads another project's vaults.
10
+
11
+ Pass `--global` to opt into the legacy merged behavior (read `config.sources` and write into `~/.claude/skills/...`). Use `--global` only when you deliberately want cross-project blending.
12
+
7
13
  ## v2.0 — chunked execution by default
8
14
 
9
15
  `npx analyzthis_design run --task "..."` now uses a **frontier planner + cheap chunk models**:
@@ -15,7 +21,7 @@ Install once. Run structured UX critiques, multi-phase ideation, and task-ground
15
21
 
16
22
  Use `npx analyzthis_design run-unchunked` for the legacy single-pass orchestrator.
17
23
 
18
- **npm:** [analyzthis_design](https://www.npmjs.com/package/analyzthis_design) · **Current version:** 2.1.2 · **Step-by-step guide:** [HOW-TO-USE.md](./HOW-TO-USE.md)
24
+ **npm:** [analyzthis_design](https://www.npmjs.com/package/analyzthis_design) · **Current version:** 2.2.0 · **Step-by-step guide:** [HOW-TO-USE.md](./HOW-TO-USE.md)
19
25
 
20
26
  ---
21
27
 
@@ -536,12 +542,14 @@ npx analyzthis_design feedback status
536
542
 
537
543
  **What does NOT get sent:** project paths, repo names, emails, API keys, full source trees.
538
544
 
539
- **Maintainer setup (Supabase):**
545
+ **Maintainer setup (vendor-neutral HTTP endpoint):**
540
546
 
541
- 1. Create a Supabase project
542
- 2. Run `supabase/migrations/001_persona_feedback.sql` in the SQL editor
543
- 3. Copy `supabase/feedback-config.example.json` into `~/.analyzthis_design/config.json` under `"feedback"` (or set env vars `ANALYZTHIS_FEEDBACK_URL` + `ANALYZTHIS_FEEDBACK_ANON_KEY`)
544
- 4. View submissions in Supabase Table Editor `persona_feedback`
547
+ The `feedback submit` client is a plain HTTPS POST with an `apikey` header — it works with any REST endpoint that accepts anonymous inserts, not only Supabase. A reference schema (with row-level security for insert-only anon access) lives in [`supabase/migrations/001_persona_feedback.sql`](./supabase/migrations/001_persona_feedback.sql) in the repo. That folder is **not** shipped in the npm package, so installing `analyzthis_design` does not pull a Supabase-branded folder into `node_modules`.
548
+
549
+ 1. Stand up any HTTP endpoint that accepts anonymous JSON inserts (Supabase with RLS is one option; a small Cloudflare Worker or a self-hosted Postgres + thin API work too).
550
+ 2. If you use the reference schema, run `supabase/migrations/001_persona_feedback.sql` in your SQL editor.
551
+ 3. Copy the endpoint URL and anon key into `~/.analyzthis_design/config.json` under `"feedback"` (or set env vars `ANALYZTHIS_FEEDBACK_URL` + `ANALYZTHIS_FEEDBACK_ANON_KEY`).
552
+ 4. Read submissions from your endpoint's dashboard.
545
553
 
546
554
  Users can also file GitHub issues via **Persona feedback** template if they prefer not to use CLI submit.
547
555
 
@@ -869,6 +877,22 @@ skills/
869
877
 
870
878
  ---
871
879
 
880
+ ## Marketing site
881
+
882
+ The public lab site lives in `website/` and is deployed on Vercel as **analyzthis-lab** (site only — not the npm package). Copy now names two doors: lab products for people who already use Cursor or Claude, and custom teams that arrive in a firm’s existing tools. The Vercel project root is `website/` with no build step — do not set Output Directory to `public`.
883
+
884
+ - Live: [https://analyzthis-lab.vercel.app](https://analyzthis-lab.vercel.app)
885
+ - Design team deep-dive: [https://analyzthis-lab.vercel.app/design](https://analyzthis-lab.vercel.app/design) (`website/design.html`)
886
+ - Form test (does not write to Airtable): add `?form=mock` to the URL
887
+ - Leads go to Airtable through `website/api/lead.js`. Each step is saved; unfinished fills are marked `partial`.
888
+ - Booking uses Google Calendar: [https://calendar.app.google/KtKJ7hCAx1duA8m58](https://calendar.app.google/KtKJ7hCAx1duA8m58)
889
+ - Form views are counted first-party (no cookies, no third-party pixel). They are only stored if you set the optional `AIRTABLE_EVENTS_TABLE` env var to a separate Airtable table; without it the count is discarded and the Leads table is never touched.
890
+ - There is exactly one privacy disclosure, in the `.privacy-note` directly above the inquiry form. If you change what the site collects, update that sentence — do not add a second notice elsewhere on the page.
891
+ - Header layout: the real `logo.svg` sits left, nav links centre, one orange CTA right. No webfont is loaded, so the wordmark must stay an SVG.
892
+ - The hero is tuned so the primary CTA and trust ticks stay above the fold at 1440×800 — check that before changing hero padding or the `h1` clamp.
893
+
894
+ ---
895
+
872
896
  ## License
873
897
 
874
898
  MIT — [Rishikesh Joshi](https://github.com/rishikeshjoshi)
package/dist/README.md CHANGED
@@ -4,6 +4,12 @@ A set of AI design personas and a task-first evaluation framework that plugs int
4
4
 
5
5
  Install once. Run structured UX critiques, multi-phase ideation, and task-grounded screen reviews — directly inside your AI chat. **No external LLM API keys required** for CLI orchestrator runs: **`/devi`** voices each persona from your host IDE (Cursor, Claude, etc.).
6
6
 
7
+ ## v2.2 — project-scoped knowledge bank
8
+
9
+ Knowledge sources are now scoped per project by default. `collect`, `connect`, `sync`, `disconnect`, and `status` operate on the project derived from your current working directory, and the built `knowledge-bank` skill is written into that project's local skills directory (`<project>/.claude/skills/knowledge-bank/SKILL.md`, `<project>/.cursor/skills/...`, etc.). Invoking a skill from one project never reads another project's vaults.
10
+
11
+ Pass `--global` to opt into the legacy merged behavior (read `config.sources` and write into `~/.claude/skills/...`). Use `--global` only when you deliberately want cross-project blending.
12
+
7
13
  ## v2.0 — chunked execution by default
8
14
 
9
15
  `npx analyzthis_design run --task "..."` now uses a **frontier planner + cheap chunk models**:
@@ -15,7 +21,7 @@ Install once. Run structured UX critiques, multi-phase ideation, and task-ground
15
21
 
16
22
  Use `npx analyzthis_design run-unchunked` for the legacy single-pass orchestrator.
17
23
 
18
- **npm:** [analyzthis_design](https://www.npmjs.com/package/analyzthis_design) · **Current version:** 2.1.2 · **Step-by-step guide:** [HOW-TO-USE.md](./HOW-TO-USE.md)
24
+ **npm:** [analyzthis_design](https://www.npmjs.com/package/analyzthis_design) · **Current version:** 2.2.0 · **Step-by-step guide:** [HOW-TO-USE.md](./HOW-TO-USE.md)
19
25
 
20
26
  ---
21
27
 
@@ -536,12 +542,14 @@ npx analyzthis_design feedback status
536
542
 
537
543
  **What does NOT get sent:** project paths, repo names, emails, API keys, full source trees.
538
544
 
539
- **Maintainer setup (Supabase):**
545
+ **Maintainer setup (vendor-neutral HTTP endpoint):**
540
546
 
541
- 1. Create a Supabase project
542
- 2. Run `supabase/migrations/001_persona_feedback.sql` in the SQL editor
543
- 3. Copy `supabase/feedback-config.example.json` into `~/.analyzthis_design/config.json` under `"feedback"` (or set env vars `ANALYZTHIS_FEEDBACK_URL` + `ANALYZTHIS_FEEDBACK_ANON_KEY`)
544
- 4. View submissions in Supabase Table Editor `persona_feedback`
547
+ The `feedback submit` client is a plain HTTPS POST with an `apikey` header — it works with any REST endpoint that accepts anonymous inserts, not only Supabase. A reference schema (with row-level security for insert-only anon access) lives in [`supabase/migrations/001_persona_feedback.sql`](./supabase/migrations/001_persona_feedback.sql) in the repo. That folder is **not** shipped in the npm package, so installing `analyzthis_design` does not pull a Supabase-branded folder into `node_modules`.
548
+
549
+ 1. Stand up any HTTP endpoint that accepts anonymous JSON inserts (Supabase with RLS is one option; a small Cloudflare Worker or a self-hosted Postgres + thin API work too).
550
+ 2. If you use the reference schema, run `supabase/migrations/001_persona_feedback.sql` in your SQL editor.
551
+ 3. Copy the endpoint URL and anon key into `~/.analyzthis_design/config.json` under `"feedback"` (or set env vars `ANALYZTHIS_FEEDBACK_URL` + `ANALYZTHIS_FEEDBACK_ANON_KEY`).
552
+ 4. Read submissions from your endpoint's dashboard.
545
553
 
546
554
  Users can also file GitHub issues via **Persona feedback** template if they prefer not to use CLI submit.
547
555
 
@@ -869,6 +877,22 @@ skills/
869
877
 
870
878
  ---
871
879
 
880
+ ## Marketing site
881
+
882
+ The public lab site lives in `website/` and is deployed on Vercel as **analyzthis-lab** (site only — not the npm package). Copy now names two doors: lab products for people who already use Cursor or Claude, and custom teams that arrive in a firm’s existing tools. The Vercel project root is `website/` with no build step — do not set Output Directory to `public`.
883
+
884
+ - Live: [https://analyzthis-lab.vercel.app](https://analyzthis-lab.vercel.app)
885
+ - Design team deep-dive: [https://analyzthis-lab.vercel.app/design](https://analyzthis-lab.vercel.app/design) (`website/design.html`)
886
+ - Form test (does not write to Airtable): add `?form=mock` to the URL
887
+ - Leads go to Airtable through `website/api/lead.js`. Each step is saved; unfinished fills are marked `partial`.
888
+ - Booking uses Google Calendar: [https://calendar.app.google/KtKJ7hCAx1duA8m58](https://calendar.app.google/KtKJ7hCAx1duA8m58)
889
+ - Form views are counted first-party (no cookies, no third-party pixel). They are only stored if you set the optional `AIRTABLE_EVENTS_TABLE` env var to a separate Airtable table; without it the count is discarded and the Leads table is never touched.
890
+ - There is exactly one privacy disclosure, in the `.privacy-note` directly above the inquiry form. If you change what the site collects, update that sentence — do not add a second notice elsewhere on the page.
891
+ - Header layout: the real `logo.svg` sits left, nav links centre, one orange CTA right. No webfont is loaded, so the wordmark must stay an SVG.
892
+ - The hero is tuned so the primary CTA and trust ticks stay above the fold at 1440×800 — check that before changing hero padding or the `h1` clamp.
893
+
894
+ ---
895
+
872
896
  ## License
873
897
 
874
898
  MIT — [Rishikesh Joshi](https://github.com/rishikeshjoshi)
package/dist/bin/cli.js CHANGED
@@ -33,14 +33,20 @@ Usage:
33
33
  welcome Print getting-started banner for your AI tool (--target)
34
34
 
35
35
  ── Knowledge bank commands ──────────────────────────────────
36
- connect Register a vault or knowledge folder as a source
37
- sync Read all sources and build the knowledge-bank skill
38
- disconnect Remove a source from the registry
39
- status Show connected sources and last sync time
36
+ connect Register a vault or knowledge folder as a source (scoped to cwd project)
37
+ sync Read this project's sources and build the knowledge-bank skill
38
+ disconnect Remove a source from this project's registry
39
+ status Show this project's sources and last sync time
40
40
  collect Kavi: scan codebase → Obsidian vault → enrich → sync knowledge bank
41
41
  (--vault path, --dry-run, --no-enrich, --no-web, --no-discover,
42
42
  --web-limit N, --target cursor|claude|codex|grok|windsurf|agents|all)
43
43
 
44
+ Scope flags (connect/sync/disconnect/status):
45
+ --project <id> Explicit project id (overrides cwd-derived id)
46
+ --global Use the legacy merged pool (config.sources → ~/.claude/skills/...)
47
+ instead of per-project scoping. Only use when you want
48
+ multiple projects' notes blended together.
49
+
44
50
  ── Design spec commands ─────────────────────────────────────
45
51
  spec show Print design_spec from session state
46
52
  spec validate Validate a DesignSpec JSON file (--file) or session
@@ -61,7 +67,7 @@ Usage:
61
67
  feedback list List feedback entries for this project (--all for every project)
62
68
  feedback export Export rejected + correction JSONL pairs for training
63
69
  (--persona <id>, --all, --output <path>, --include-positive)
64
- feedback submit Opt-in: send anonymized corrections to community store (Supabase)
70
+ feedback submit Opt-in: send anonymized corrections to a community store (HTTP endpoint)
65
71
  (--yes skip prompt, --dry-run preview, --all unsent across projects)
66
72
  feedback status Show submit consent, endpoint config, unsent count
67
73
  feedback revoke Revoke opt-in consent for community submit
@@ -175,9 +181,11 @@ Usage:
175
181
  npx analyzthis_design connect --vault ~/Documents/MyVault
176
182
  npx analyzthis_design connect --vault ~/docs --tags design,brand,product
177
183
  npx analyzthis_design connect --vault ~/vault --include Design,Research
178
- npx analyzthis_design sync # sync to Cursor
179
- npx analyzthis_design sync --target all # sync to all tools
180
- npx analyzthis_design status # show sources
184
+ npx analyzthis_design sync # sync to Cursor (project-scoped)
185
+ npx analyzthis_design sync --target all # sync to all tools (project-scoped)
186
+ npx analyzthis_design sync --global # legacy: blend all projects into ~/.claude/skills/
187
+ npx analyzthis_design status # show this project's sources
188
+ npx analyzthis_design status --global # show the legacy merged pool
181
189
  npx analyzthis_design disconnect --vault ~/Documents/MyVault
182
190
 
183
191
  npx analyzthis_design session init # start a new session for this repo
@@ -255,6 +263,7 @@ const vaultVal = getFlag('vault');
255
263
  const tagsVal = getFlag('tags');
256
264
  const includeVal = getFlag('include');
257
265
  const projectVal = getFlag('project');
266
+ const globalFlag = flags.includes('--global');
258
267
  const allFlag = flags.includes('--all');
259
268
  const urlVal = getFlag('url');
260
269
  const queryVal = getFlag('query');
@@ -352,8 +361,10 @@ switch (cmd) {
352
361
  try {
353
362
  const tags = tagsVal ? tagsVal.split(',').map(t => t.trim()) : [];
354
363
  const include = includeVal ? includeVal.split(',').map(t => t.trim()) : [];
355
- const abs = connect({ vaultPath: vaultVal, tags, include });
364
+ const abs = connect({ vaultPath: vaultVal, tags, include, project: projectVal, global: globalFlag });
365
+ const scopeLabel = globalFlag ? 'global (merged across projects)' : (projectVal || `project derived from ${path.resolve(process.cwd())}`);
356
366
  console.log(`\n✅ Connected: ${abs}`);
367
+ console.log(` Scope: ${scopeLabel}`);
357
368
  if (tags.length) console.log(` Tags filter: ${tags.join(', ')}`);
358
369
  if (include.length) console.log(` Folder filter: ${include.join(', ')}`);
359
370
  console.log(`\n Run "npx analyzthis_design sync" to build the knowledge bank.\n`);
@@ -369,7 +380,7 @@ switch (cmd) {
369
380
  console.error('\n ✗ --vault is required. Example: npx analyzthis_design disconnect --vault ~/Documents/MyVault\n');
370
381
  process.exit(1);
371
382
  }
372
- disconnect(vaultVal);
383
+ disconnect(vaultVal, { project: projectVal, global: globalFlag });
373
384
  console.log(`\n🗑 Disconnected: ${path.resolve(vaultVal)}\n`);
374
385
  break;
375
386
  }
@@ -381,9 +392,10 @@ switch (cmd) {
381
392
  console.error(`\n ✗ Unknown target "${targetVal}". Choose: ${ALL_TARGET_IDS.join(', ')}, all\n`);
382
393
  process.exit(1);
383
394
  }
384
- console.log('\n⏳ Syncing knowledge bank...\n');
395
+ const scopeLabel = globalFlag ? 'global (merged across projects)' : (projectVal || `project derived from ${path.resolve(process.cwd())}`);
396
+ console.log(`\n⏳ Syncing knowledge bank (${scopeLabel})...\n`);
385
397
  try {
386
- const result = sync({ targets });
398
+ const result = sync({ targets, project: projectVal, global: globalFlag });
387
399
  if (result.message) {
388
400
  console.log(` ⚠ ${result.message}\n`);
389
401
  } else {
@@ -399,18 +411,22 @@ switch (cmd) {
399
411
  }
400
412
 
401
413
  case 'status': {
402
- const cfg = status();
403
- if (!cfg.sources || cfg.sources.length === 0) {
404
- console.log('\n No knowledge sources connected.\n Run: npx analyzthis_design connect --vault /path/to/vault\n');
414
+ const view = status({ project: projectVal, global: globalFlag });
415
+ if (!view.sources || view.sources.length === 0) {
416
+ const scopeHint = view.scope === 'global'
417
+ ? 'Run: npx analyzthis_design connect --vault /path/to/vault --global'
418
+ : 'Run: npx analyzthis_design collect (from inside the project) or npx analyzthis_design connect --vault /path/to/vault';
419
+ console.log(`\n No knowledge sources connected for this ${view.scope} scope.\n ${scopeHint}\n`);
405
420
  } else {
406
- console.log(`\n📚 Knowledge bank sources (${cfg.sources.length}):\n`);
407
- for (const s of cfg.sources) {
421
+ const scopeLabel = view.scope === 'global' ? 'global (merged across projects)' : `project: ${view.projectId}`;
422
+ console.log(`\n📚 Knowledge bank sources (${view.sources.length}) — ${scopeLabel}:\n`);
423
+ for (const s of view.sources) {
408
424
  console.log(` • ${s.path}`);
409
- if (s.tags.length) console.log(` Tags: ${s.tags.join(', ')}`);
410
- if (s.include.length) console.log(` Folders: ${s.include.join(', ')}`);
425
+ if (s.tags && s.tags.length) console.log(` Tags: ${s.tags.join(', ')}`);
426
+ if (s.include && s.include.length) console.log(` Folders: ${s.include.join(', ')}`);
411
427
  console.log(` Added: ${s.addedAt}`);
412
428
  }
413
- if (cfg.lastSync) console.log(`\n Last sync: ${cfg.lastSync}`);
429
+ if (view.lastSync) console.log(`\n Last sync: ${view.lastSync}`);
414
430
  console.log('');
415
431
  }
416
432
  break;
@@ -767,22 +767,26 @@ async function collect(opts = {}) {
767
767
  }
768
768
  }
769
769
 
770
- // Auto-connect discovered sources + Kavi vault, then sync knowledge bank
771
- console.log(`\n⏳ Wiring knowledge bank...`);
770
+ // Auto-connect discovered sources + Kavi vault, then sync knowledge bank.
771
+ // Scoped to the current project so this project's vault never bleeds into
772
+ // another project's knowledge bank (the v2.2 project-scoping fix).
773
+ console.log(`\n⏳ Wiring knowledge bank (project: ${projectId})...`);
772
774
  const connectResult = discoverSources
773
775
  ? connectDiscoveredSources({
774
776
  discoveries: sourceDiscoveries,
775
777
  kaviVaultPath: vaultPath,
776
778
  config,
777
779
  autoConnect: config.collect?.auto_connect_discovered !== false,
780
+ project: projectId,
781
+ cwd,
778
782
  })
779
783
  : { connected: [], skipped: [] };
780
784
  if (connectResult.connected.length > 0) {
781
785
  console.log(` Auto-connected ${connectResult.connected.length} source(s):`);
782
786
  for (const p of connectResult.connected) console.log(` • ${p}`);
783
787
  }
784
- knowledge.connect({ vaultPath, tags: [], include: [] });
785
- const syncResult = knowledge.sync({ targets });
788
+ knowledge.connect({ vaultPath, tags: [], include: [], project: projectId, cwd });
789
+ const syncResult = knowledge.sync({ targets, project: projectId, cwd });
786
790
  cache.invalidatePrefix('kb:');
787
791
 
788
792
  if (syncResult.message) {
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const crypto = require('crypto');
7
8
 
8
9
  const CONFIG_DIR = path.join(os.homedir(), '.analyzthis_design');
9
10
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
@@ -32,9 +33,14 @@ const CATEGORIES = {
32
33
  // ─── Config helpers ──────────────────────────────────────────────────────────
33
34
 
34
35
  function loadConfig() {
35
- if (!fs.existsSync(CONFIG_FILE)) return { sources: [] };
36
- try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); }
37
- catch { return { sources: [] }; }
36
+ if (!fs.existsSync(CONFIG_FILE)) return { sources: [], projects: {} };
37
+ try {
38
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
39
+ if (!cfg.sources) cfg.sources = [];
40
+ if (!cfg.projects) cfg.projects = {};
41
+ return cfg;
42
+ }
43
+ catch { return { sources: [], projects: {} }; }
38
44
  }
39
45
 
40
46
  function saveConfig(config) {
@@ -42,6 +48,79 @@ function saveConfig(config) {
42
48
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
43
49
  }
44
50
 
51
+ // ─── Project scoping ─────────────────────────────────────────────────────────
52
+ // Knowledge sources are scoped per project by default. Each project's sources
53
+ // live under config.projects[projectId].sources and the master knowledge-bank
54
+ // is written into that project's local skills directory (e.g.
55
+ // <projectRoot>/.claude/skills/knowledge-bank/SKILL.md), so invoking a skill
56
+ // from one project never reads another project's vaults.
57
+ //
58
+ // Pass { global: true } (or --global on the CLI) to opt into the legacy merged
59
+ // behavior: read config.sources and write into ~/.claude/skills/... This is the
60
+ // only path that ever blends multiple projects' notes together.
61
+
62
+ function resolveProjectScope({ project, global, cwd } = {}) {
63
+ // Explicit --global wins: read the legacy merged pool, write to global skills dirs.
64
+ if (global) return { scope: 'global', projectId: null, projectRoot: null };
65
+
66
+ // --project <id> (or a caller-provided project id): use that id, rooted at cwd.
67
+ let projectId = project;
68
+ let projectRoot = path.resolve(cwd || process.cwd());
69
+
70
+ // Default: auto-derive a project id from cwd, the same way session.js does.
71
+ if (!projectId) {
72
+ const abs = path.resolve(cwd || process.cwd());
73
+ const slug = path.basename(abs).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'project';
74
+ const hash = crypto.createHash('sha1').update(abs).digest('hex').slice(0, 8);
75
+ projectId = `${slug}-${hash}`;
76
+ }
77
+
78
+ return { scope: 'project', projectId, projectRoot };
79
+ }
80
+
81
+ // Return the sources array for a resolved scope, creating the project entry
82
+ // on first use. For global scope, returns the legacy top-level config.sources.
83
+ function scopedSources(config, scopeInfo, { create = false } = {}) {
84
+ if (scopeInfo.scope === 'global') return config.sources;
85
+ if (!config.projects[scopeInfo.projectId] && create) {
86
+ config.projects[scopeInfo.projectId] = { sources: [], addedAt: new Date().toISOString() };
87
+ }
88
+ const proj = config.projects[scopeInfo.projectId];
89
+ return proj ? proj.sources : null;
90
+ }
91
+
92
+ // Per-tool skill roots, expressed relative to a project root, mirroring the
93
+ // global TARGETS layout in platforms.js. Used when writing project-local
94
+ // knowledge-bank files so each project's skills load only inside that project.
95
+ const PROJECT_REL_TARGETS = [
96
+ { id: 'cursor', rel: ['.cursor', 'skills'], layout: 'dir' },
97
+ { id: 'claude', rel: ['.claude', 'skills'], layout: 'dir' },
98
+ { id: 'claude-cmds', rel: ['.claude', 'commands'], layout: 'flat' },
99
+ { id: 'codex', rel: ['.codex', 'skills'], layout: 'dir' },
100
+ { id: 'grok', rel: ['.grok', 'skills'], layout: 'dir' },
101
+ { id: 'windsurf', rel: ['.codeium', 'windsurf', 'skills'], layout: 'dir' },
102
+ { id: 'agents', rel: ['.agents', 'skills'], layout: 'dir' },
103
+ ];
104
+
105
+ function projectTargets(projectRoot, requestedTargets) {
106
+ // requestedTargets is the resolved list from resolveTargets (e.g. ['cursor'],
107
+ // ['claude'], or ALL_TARGET_IDS). claude-cmds is always paired with claude.
108
+ const want = new Set(requestedTargets);
109
+ const out = [];
110
+ for (const t of PROJECT_REL_TARGETS) {
111
+ if (t.id === 'claude-cmds') {
112
+ if (want.has('claude')) out.push(t);
113
+ } else if (want.has(t.id)) {
114
+ out.push(t);
115
+ }
116
+ }
117
+ return out.map((t) => ({
118
+ id: t.id,
119
+ root: path.join(projectRoot, ...t.rel),
120
+ layout: t.layout,
121
+ }));
122
+ }
123
+
45
124
  // ─── Vault reading ───────────────────────────────────────────────────────────
46
125
 
47
126
  // Recursively collect all .md files under a directory
@@ -103,29 +182,54 @@ function categorize(title, tags, body) {
103
182
 
104
183
  /**
105
184
  * Register a vault or folder as a knowledge source.
185
+ *
186
+ * By default the source is scoped to the project derived from cwd, so it only
187
+ * feeds critiques run from that project. Pass { global: true } to register it
188
+ * in the legacy merged pool (config.sources) instead.
189
+ *
106
190
  * Options:
107
191
  * include — array of sub-folder prefixes to include, e.g. ['Design', 'Brand']
108
192
  * tags — array of tags to filter by, e.g. ['ux', 'design', 'brand']
193
+ * project — explicit project id (overrides cwd-derived id)
194
+ * global — when true, register in the legacy merged pool instead
195
+ * cwd — working directory used to derive the project id (default: process.cwd())
109
196
  */
110
- function connect({ vaultPath, include = [], tags = [] }) {
197
+ function connect({ vaultPath, include = [], tags = [], project, global = false, cwd } = {}) {
111
198
  const abs = path.resolve(vaultPath);
112
199
  if (!fs.existsSync(abs)) throw new Error(`Path does not exist: ${abs}`);
113
200
 
114
201
  const config = loadConfig();
115
- // Replace any existing entry with the same path
116
- config.sources = config.sources.filter(s => s.path !== abs);
117
- config.sources.push({ path: abs, include, tags, addedAt: new Date().toISOString() });
202
+ const scopeInfo = resolveProjectScope({ project, global, cwd });
203
+ const sources = scopedSources(config, scopeInfo, { create: true });
204
+ // Replace any existing entry with the same path within this scope
205
+ const filtered = sources.filter(s => s.path !== abs);
206
+ filtered.push({ path: abs, include, tags, addedAt: new Date().toISOString() });
207
+ if (scopeInfo.scope === 'global') {
208
+ config.sources = filtered;
209
+ } else {
210
+ config.projects[scopeInfo.projectId].sources = filtered;
211
+ }
118
212
  saveConfig(config);
119
213
  return abs;
120
214
  }
121
215
 
122
216
  /**
123
217
  * Remove a vault/folder from the knowledge sources list.
218
+ * Accepts the same { project, global, cwd } options as connect() to target
219
+ * the right scope.
124
220
  */
125
- function disconnect(vaultPath) {
221
+ function disconnect(vaultPath, opts = {}) {
126
222
  const abs = path.resolve(vaultPath);
127
223
  const config = loadConfig();
128
- config.sources = config.sources.filter(s => s.path !== abs);
224
+ const scopeInfo = resolveProjectScope(opts);
225
+ const sources = scopedSources(config, scopeInfo);
226
+ if (!sources) return; // nothing in this scope yet
227
+ const filtered = sources.filter(s => s.path !== abs);
228
+ if (scopeInfo.scope === 'global') {
229
+ config.sources = filtered;
230
+ } else if (config.projects[scopeInfo.projectId]) {
231
+ config.projects[scopeInfo.projectId].sources = filtered;
232
+ }
129
233
  saveConfig(config);
130
234
  }
131
235
 
@@ -133,19 +237,38 @@ function disconnect(vaultPath) {
133
237
  * Read all connected sources, filter notes, build knowledge-bank.md,
134
238
  * and copy it to all requested target AI tool directories.
135
239
  *
136
- * targets array of tool names: 'cursor', 'claude', 'codex'
240
+ * By default the bank is scoped to the project derived from cwd: it reads
241
+ * only that project's sources and writes into that project's local skills
242
+ * directory (<projectRoot>/.claude/skills/knowledge-bank/SKILL.md, etc.), so
243
+ * invoking a skill from one project never pulls in another project's vaults.
244
+ *
245
+ * Pass { global: true } (or --global) to opt into the legacy merged behavior:
246
+ * read config.sources and write into ~/.claude/skills/... so multiple
247
+ * projects' notes blend together. Use this only when you deliberately want
248
+ * cross-project blending.
249
+ *
250
+ * targets — array of tool names: 'cursor', 'claude', 'codex', 'grok',
251
+ * 'windsurf', 'agents', or ['all']
252
+ * project — explicit project id (overrides cwd-derived id)
253
+ * global — when true, use the legacy merged pool
254
+ * cwd — working directory used to derive the project id
137
255
  */
138
- function sync({ targets = ['cursor'] } = {}) {
256
+ function sync({ targets = ['cursor'], project, global = false, cwd } = {}) {
139
257
  const config = loadConfig();
140
-
141
- if (!config.sources || config.sources.length === 0) {
142
- return { synced: 0, message: 'No sources connected. Run: npx analyzthis_design connect --vault /path/to/vault' };
258
+ const scopeInfo = resolveProjectScope({ project, global, cwd });
259
+ const sources = scopedSources(config, scopeInfo) || [];
260
+
261
+ if (!sources || sources.length === 0) {
262
+ const hint = scopeInfo.scope === 'global'
263
+ ? 'No sources connected. Run: npx analyzthis_design connect --vault /path/to/vault --global'
264
+ : `No sources connected for project "${scopeInfo.projectId}". Run: npx analyzthis_design collect (from inside the project) or npx analyzthis_design connect --vault /path/to/vault`;
265
+ return { synced: 0, message: hint };
143
266
  }
144
267
 
145
268
  const sections = { prd: [], brand: [], product: [], design: [], research: [], tech: [], web: [], other: [] };
146
269
  let totalFiles = 0;
147
270
 
148
- for (const source of config.sources) {
271
+ for (const source of sources) {
149
272
  const files = readMarkdownFiles(source.path);
150
273
 
151
274
  for (const filePath of files) {
@@ -184,7 +307,7 @@ function sync({ targets = ['cursor'] } = {}) {
184
307
  } catch { /* no session / research available — fine */ }
185
308
 
186
309
  // Build the knowledge-bank markdown
187
- const sourceList = config.sources.map(s => s.path).join(', ');
310
+ const sourceList = sources.map(s => s.path).join(', ');
188
311
  const date = new Date().toISOString().split('T')[0];
189
312
 
190
313
  // PRD/stories listed first — ux-story-gate reads this section in Phase 0
@@ -199,6 +322,10 @@ function sync({ targets = ['cursor'] } = {}) {
199
322
  { key: 'other', heading: '## Additional Context' },
200
323
  ];
201
324
 
325
+ const scopeLabel = scopeInfo.scope === 'global'
326
+ ? 'global (merged across projects)'
327
+ : `project: ${scopeInfo.projectId}`;
328
+
202
329
  let md = `---
203
330
  name: knowledge-bank
204
331
  description: Personal knowledge bank — takes precedence over all built-in persona defaults.
@@ -208,6 +335,7 @@ disable-model-invocation: true
208
335
  # Knowledge Bank
209
336
 
210
337
  > Last synced: ${date}
338
+ > Scope: ${scopeLabel}
211
339
  > Sources: ${sourceList}
212
340
  > Files loaded: ${totalFiles}
213
341
 
@@ -232,53 +360,63 @@ disable-model-invocation: true
232
360
  md += `_No matching files found. Check your --tags or --include filters, or remove filters to include all notes._\n`;
233
361
  }
234
362
 
235
- // Write to the package's own skills/knowledge-bank/SKILL.md
363
+ // Write to the package's own skills/knowledge-bank/SKILL.md (source of truth)
236
364
  fs.mkdirSync(path.dirname(KNOWLEDGE_SKILL), { recursive: true });
237
365
  fs.writeFileSync(KNOWLEDGE_SKILL, md);
238
366
 
239
367
  // Build per-persona knowledge slices (priority + fallback)
240
368
  try {
241
369
  const session = require('./session');
242
- const projectId = session.getProjectId();
370
+ const projectId = scopeInfo.scope === 'project' ? scopeInfo.projectId : session.getProjectId();
243
371
  writePersonaSlices(sections, sectionDefs, projectId);
244
372
  } catch {
245
373
  // sessions unavailable — skip slices
246
374
  }
247
375
 
248
- // Copy knowledge-bank into every requested platform (dir and/or flat layout)
376
+ // Copy knowledge-bank into every requested platform.
377
+ // Project-scoped → write into <projectRoot>/.{tool}/skills/... (project-local,
378
+ // loaded only when the skill is invoked from inside that project).
379
+ // Global-scoped → write into ~/.{tool}/skills/... (legacy merged behavior).
249
380
  const { TARGETS, resolveTargets } = require('./platforms');
250
381
  const copiedTo = [];
251
382
  const resolved = targets.includes('all')
252
383
  ? resolveTargets('all')
253
384
  : targets.filter((t) => TARGETS[t]);
254
385
 
255
- for (const targetId of resolved) {
256
- const t = TARGETS[targetId];
257
- const destinations = [{ root: t.root, layout: t.layout }];
258
- if (t.also) destinations.push({ root: t.also.root, layout: t.also.layout });
259
-
260
- for (const dest of destinations) {
261
- fs.mkdirSync(dest.root, { recursive: true });
262
- if (dest.layout === 'dir') {
263
- const skillDir = path.join(dest.root, 'knowledge-bank');
264
- fs.mkdirSync(skillDir, { recursive: true });
265
- fs.copyFileSync(KNOWLEDGE_SKILL, path.join(skillDir, 'SKILL.md'));
266
- } else {
267
- fs.copyFileSync(KNOWLEDGE_SKILL, path.join(dest.root, 'knowledge-bank.md'));
268
- }
269
- copiedTo.push(`${targetId} ${dest.root}`);
386
+ const destinations = scopeInfo.scope === 'project'
387
+ ? projectTargets(scopeInfo.projectRoot, resolved)
388
+ : resolved.map((id) => {
389
+ const t = TARGETS[id];
390
+ const list = [{ root: t.root, layout: t.layout, id }];
391
+ if (t.also) list.push({ root: t.also.root, layout: t.also.layout, id: `${id}-cmds` });
392
+ return list;
393
+ }).flat();
394
+
395
+ for (const dest of destinations) {
396
+ fs.mkdirSync(dest.root, { recursive: true });
397
+ if (dest.layout === 'dir') {
398
+ const skillDir = path.join(dest.root, 'knowledge-bank');
399
+ fs.mkdirSync(skillDir, { recursive: true });
400
+ fs.copyFileSync(KNOWLEDGE_SKILL, path.join(skillDir, 'SKILL.md'));
401
+ } else {
402
+ fs.copyFileSync(KNOWLEDGE_SKILL, path.join(dest.root, 'knowledge-bank.md'));
270
403
  }
404
+ copiedTo.push(`${dest.id} → ${dest.root}`);
271
405
  }
272
406
 
273
- // Persist lastSync timestamp
274
- config.lastSync = new Date().toISOString();
407
+ // Persist lastSync timestamp on the scoped config entry
408
+ if (scopeInfo.scope === 'global') {
409
+ config.lastSync = new Date().toISOString();
410
+ } else if (config.projects[scopeInfo.projectId]) {
411
+ config.projects[scopeInfo.projectId].lastSync = new Date().toISOString();
412
+ }
275
413
  saveConfig(config);
276
414
 
277
415
  // Knowledge bank content just changed for every project — drop any cached
278
416
  // knowledge-bank slices so the next run re-reads the fresh sync.
279
417
  try { require('./cache').invalidatePrefix('kb:'); } catch { /* cache module unavailable — fine */ }
280
418
 
281
- return { synced: totalFiles, copiedTo };
419
+ return { synced: totalFiles, copiedTo, scope: scopeLabel };
282
420
  }
283
421
 
284
422
  function loadManifest(id) {
@@ -376,10 +514,24 @@ function getPersonaSliceForPrompt(state, personaId) {
376
514
  }
377
515
 
378
516
  /**
379
- * Return current config (sources list, lastSync).
517
+ * Return the config view for the resolved scope.
518
+ * Accepts the same { project, global, cwd } options as connect/sync.
519
+ * Returns { scope, projectId, sources, lastSync } so the CLI can print
520
+ * a scope-aware status without leaking other projects' sources.
380
521
  */
381
- function status() {
382
- return loadConfig();
522
+ function status(opts = {}) {
523
+ const config = loadConfig();
524
+ const scopeInfo = resolveProjectScope(opts);
525
+ const sources = scopedSources(config, scopeInfo) || [];
526
+ const lastSync = scopeInfo.scope === 'global'
527
+ ? config.lastSync
528
+ : (config.projects[scopeInfo.projectId] && config.projects[scopeInfo.projectId].lastSync);
529
+ return {
530
+ scope: scopeInfo.scope,
531
+ projectId: scopeInfo.projectId,
532
+ sources,
533
+ lastSync,
534
+ };
383
535
  }
384
536
 
385
- module.exports = { connect, disconnect, sync, status, buildPersonaSlice, writePersonaSlices, readPersonaSlice, getPersonaSliceForPrompt, KNOWLEDGE_SLICE_ROOT };
537
+ module.exports = { connect, disconnect, sync, status, buildPersonaSlice, writePersonaSlices, readPersonaSlice, getPersonaSliceForPrompt, KNOWLEDGE_SLICE_ROOT, resolveProjectScope };
@@ -449,9 +449,12 @@ function writeSourceManifestNotes({ discoveries, vaultPath, dryRun }) {
449
449
  }
450
450
 
451
451
  /**
452
- * Auto-connect discovered sources (high/medium confidence) into config.sources.
452
+ * Auto-connect discovered sources (high/medium confidence) into the project's
453
+ * scoped sources (config.projects[projectId].sources). Pass { project, cwd }
454
+ * so the scope matches the collect() call site; falls back to cwd-derived
455
+ * scoping inside knowledge.connect when omitted.
453
456
  */
454
- function connectDiscoveredSources({ discoveries, kaviVaultPath, config, autoConnect = true }) {
457
+ function connectDiscoveredSources({ discoveries, kaviVaultPath, config, autoConnect = true, project, cwd }) {
455
458
  const connected = [];
456
459
  const skipped = [];
457
460
  if (!autoConnect) return { connected, skipped };
@@ -469,7 +472,7 @@ function connectDiscoveredSources({ discoveries, kaviVaultPath, config, autoConn
469
472
  continue;
470
473
  }
471
474
  try {
472
- knowledge.connect({ vaultPath: d.path, tags: [], include: [] });
475
+ knowledge.connect({ vaultPath: d.path, tags: [], include: [], project, cwd });
473
476
  connected.push(d.path);
474
477
  } catch (err) {
475
478
  skipped.push({ path: d.path, reason: err.message });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "analyzthis_design",
3
- "version": "2.1.2",
4
- "description": "8 AI design personas — v2.0 chunked execution with frontier planner + free/cheap chunk models, adversarial deliberation loops, opt-in community feedback, Kavi knowledge collection, DesignSpec producer path, wireframe skills, UX critique, Agent Skills for Cursor, Claude, Codex, Grok, Windsurf. Plain source — no obfuscation, no auto-install.",
3
+ "version": "2.2.0",
4
+ "description": "8 AI design personas — v2.2 project-scoped knowledge bank (no cross-project vault entanglement), v2.0 chunked execution with frontier planner + free/cheap chunk models, adversarial deliberation loops, opt-in community feedback, Kavi knowledge collection, DesignSpec producer path, wireframe skills, UX critique, Agent Skills for Cursor, Claude, Codex, Grok, Windsurf. Plain source — no obfuscation, no auto-install.",
5
5
  "keywords": [
6
6
  "cursor",
7
7
  "cursor-skill",
@@ -33,7 +33,6 @@
33
33
  "dist/",
34
34
  "skills/",
35
35
  "agents/",
36
- "supabase/",
37
36
  ".github/",
38
37
  "scripts/validate-csvs.js",
39
38
  "README.md",
@@ -1,15 +0,0 @@
1
- {
2
- "deliberation": {
3
- "satisfaction_threshold": 0.4,
4
- "max_rounds": 3,
5
- "parallel_pairs": [["noor", "anuj"], ["meera", "priya"]],
6
- "objection_token_cap": 600,
7
- "prior_output_chars_first": 800,
8
- "prior_output_chars_rebuttal": 400,
9
- "prior_output_chars_produce": 1200,
10
- "context_pack_chars_objection": 1500,
11
- "context_pack_chars_produce": 3000,
12
- "escalate_to_raj_after_round": 2,
13
- "default_mode": "adversarial"
14
- }
15
- }
@@ -1,7 +0,0 @@
1
- {
2
- "feedback": {
3
- "submit_enabled": true,
4
- "submit_url": "https://YOUR_PROJECT.supabase.co/rest/v1/persona_feedback",
5
- "anon_key": "YOUR_SUPABASE_ANON_KEY"
6
- }
7
- }
@@ -1,54 +0,0 @@
1
- -- Persona feedback — anonymous community corrections for analyzthis_design
2
- -- Run in Supabase SQL editor or via supabase db push
3
-
4
- create table if not exists public.persona_feedback (
5
- id uuid primary key default gen_random_uuid(),
6
- created_at timestamptz not null default now(),
7
-
8
- install_id text not null,
9
- package_version text,
10
- feedback_id text,
11
-
12
- persona text not null,
13
- satisfied boolean not null default false,
14
- rating smallint check (rating is null or (rating >= 1 and rating <= 5)),
15
- tags text[] not null default '{}',
16
-
17
- user_comment text,
18
- assistant_rejected text,
19
- assistant_preferred text,
20
- task_summary text,
21
- problem_type text,
22
- mode text,
23
- recorded_at timestamptz
24
- );
25
-
26
- create index if not exists persona_feedback_persona_idx on public.persona_feedback (persona);
27
- create index if not exists persona_feedback_created_at_idx on public.persona_feedback (created_at desc);
28
- create index if not exists persona_feedback_tags_idx on public.persona_feedback using gin (tags);
29
-
30
- alter table public.persona_feedback enable row level security;
31
-
32
- -- Anonymous clients may INSERT only (no read/update/delete for anon)
33
- drop policy if exists "anon_insert_persona_feedback" on public.persona_feedback;
34
- create policy "anon_insert_persona_feedback"
35
- on public.persona_feedback
36
- for insert
37
- to anon
38
- with check (
39
- char_length(coalesce(user_comment, '')) <= 2000
40
- and char_length(coalesce(assistant_rejected, '')) <= 4000
41
- and char_length(coalesce(assistant_preferred, '')) <= 4000
42
- and persona ~ '^[a-z][a-z0-9_-]{0,31}$'
43
- );
44
-
45
- -- Service role (dashboard) can read everything — use Supabase dashboard or service key
46
- drop policy if exists "service_read_persona_feedback" on public.persona_feedback;
47
- create policy "service_read_persona_feedback"
48
- on public.persona_feedback
49
- for select
50
- to service_role
51
- using (true);
52
-
53
- comment on table public.persona_feedback is
54
- 'Anonymous opt-in persona correction data from analyzthis_design CLI (feedback submit).';
@@ -1,15 +0,0 @@
1
- {
2
- "deliberation": {
3
- "satisfaction_threshold": 0.4,
4
- "max_rounds": 3,
5
- "parallel_pairs": [["noor", "anuj"], ["meera", "priya"]],
6
- "objection_token_cap": 600,
7
- "prior_output_chars_first": 800,
8
- "prior_output_chars_rebuttal": 400,
9
- "prior_output_chars_produce": 1200,
10
- "context_pack_chars_objection": 1500,
11
- "context_pack_chars_produce": 3000,
12
- "escalate_to_raj_after_round": 2,
13
- "default_mode": "adversarial"
14
- }
15
- }
@@ -1,7 +0,0 @@
1
- {
2
- "feedback": {
3
- "submit_enabled": true,
4
- "submit_url": "https://YOUR_PROJECT.supabase.co/rest/v1/persona_feedback",
5
- "anon_key": "YOUR_SUPABASE_ANON_KEY"
6
- }
7
- }
@@ -1,54 +0,0 @@
1
- -- Persona feedback — anonymous community corrections for analyzthis_design
2
- -- Run in Supabase SQL editor or via supabase db push
3
-
4
- create table if not exists public.persona_feedback (
5
- id uuid primary key default gen_random_uuid(),
6
- created_at timestamptz not null default now(),
7
-
8
- install_id text not null,
9
- package_version text,
10
- feedback_id text,
11
-
12
- persona text not null,
13
- satisfied boolean not null default false,
14
- rating smallint check (rating is null or (rating >= 1 and rating <= 5)),
15
- tags text[] not null default '{}',
16
-
17
- user_comment text,
18
- assistant_rejected text,
19
- assistant_preferred text,
20
- task_summary text,
21
- problem_type text,
22
- mode text,
23
- recorded_at timestamptz
24
- );
25
-
26
- create index if not exists persona_feedback_persona_idx on public.persona_feedback (persona);
27
- create index if not exists persona_feedback_created_at_idx on public.persona_feedback (created_at desc);
28
- create index if not exists persona_feedback_tags_idx on public.persona_feedback using gin (tags);
29
-
30
- alter table public.persona_feedback enable row level security;
31
-
32
- -- Anonymous clients may INSERT only (no read/update/delete for anon)
33
- drop policy if exists "anon_insert_persona_feedback" on public.persona_feedback;
34
- create policy "anon_insert_persona_feedback"
35
- on public.persona_feedback
36
- for insert
37
- to anon
38
- with check (
39
- char_length(coalesce(user_comment, '')) <= 2000
40
- and char_length(coalesce(assistant_rejected, '')) <= 4000
41
- and char_length(coalesce(assistant_preferred, '')) <= 4000
42
- and persona ~ '^[a-z][a-z0-9_-]{0,31}$'
43
- );
44
-
45
- -- Service role (dashboard) can read everything — use Supabase dashboard or service key
46
- drop policy if exists "service_read_persona_feedback" on public.persona_feedback;
47
- create policy "service_read_persona_feedback"
48
- on public.persona_feedback
49
- for select
50
- to service_role
51
- using (true);
52
-
53
- comment on table public.persona_feedback is
54
- 'Anonymous opt-in persona correction data from analyzthis_design CLI (feedback submit).';