analyzthis_design 2.1.2 → 2.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.
@@ -29,18 +29,46 @@ function buildSynthesisPrompt(plan, results, contextPack) {
29
29
  lines.push(r.output.slice(0, 1200));
30
30
  }
31
31
 
32
+ // Detect disagreements between personas for explicit arbitration.
33
+ var disagreements = [];
34
+ for (var a = 0; a < results.length; a++) {
35
+ for (var b = a + 1; b < results.length; b++) {
36
+ var va = results[a].structured_output && results[a].structured_output.verdict;
37
+ var vb = results[b].structured_output && results[b].structured_output.verdict;
38
+ if (va && vb && va !== vb) {
39
+ disagreements.push(results[a].persona + ' says ' + va + ' but ' + results[b].persona + ' says ' + vb);
40
+ }
41
+ }
42
+ }
43
+
32
44
  lines.push('',
45
+ 'You MUST do the following:',
46
+ '',
47
+ '1. RESOLVE CONFLICTS: If personas disagree on any point, pick a winner. Do not present both sides and leave it open. State which persona is correct and why, with a forward path.',
48
+ '2. CHALLENGE THE PREMISE: Before accepting the task framing, ask: "Are we solving the right problem?" If the premise is questionable, flag it explicitly.',
49
+ '3. Give a definitive answer — not a summary of opinions.',
50
+ '',
33
51
  'Produce:',
52
+ '- Premise check: Is the task framing valid? If not, what is the real problem?',
53
+ '- Resolved conflicts: For each disagreement, who is right and why.',
34
54
  '- Verdict: SHIP | REVISE | BLOCK',
35
55
  '- Composite score out of 5 if applicable',
36
- '- Top 3 actionable changes',
56
+ '- Top 3 actionable changes (each with a specific forward path, not just "fix it")',
37
57
  '- Brief reasoning',
38
58
  '',
39
59
  'Use the same format as the design-critic synthesis.'
40
60
  );
41
61
 
62
+ if (disagreements.length) {
63
+ lines.splice(lines.indexOf('You MUST do the following:'), 0,
64
+ 'DETECTED DISAGREEMENTS (must resolve each):', '');
65
+ for (var d = 0; d < disagreements.length; d++) {
66
+ lines.splice(lines.indexOf('DETECTED DISAGREEMENTS'), 0, ' - ' + disagreements[d]);
67
+ }
68
+ }
69
+
42
70
  return {
43
- system: 'You synthesize multi-persona design critique outputs. Be concise and cite the most important conflicts.',
71
+ system: 'You synthesize multi-persona design critique outputs. You are decisive not a summarizer. Resolve conflicts explicitly, challenge premises, and give forward paths. If personas contradict, pick a winner with reasoning.',
44
72
  user: lines.join('\n'),
45
73
  };
46
74
  }
@@ -138,6 +166,9 @@ async function synthesize(opts) {
138
166
  markdown: syn.markdown || text,
139
167
  model_used: { provider: model.provider, model: model.model, cost: model.cost },
140
168
  contradictions: contradictions,
169
+ premise_challenge: extractPremiseCheck(text),
170
+ conflict_resolutions: extractConflictResolutions(text),
171
+ raw_synthesis: text,
141
172
  tokens: { input: inTok, output: outTok },
142
173
  cost: chunkModels.estimateCost(model, inTok, outTok),
143
174
  };
@@ -169,8 +200,34 @@ function fallbackSynthesis(results) {
169
200
  };
170
201
  }
171
202
 
203
+ function extractPremiseCheck(text) {
204
+ if (!text) return null;
205
+ var m = text.match(/premise[\s:]*check[\s:]*([^\n]+)/i);
206
+ if (m) return m[1].trim();
207
+ m = text.match(/are we solving[\s\S]{0,200}/i);
208
+ if (m) return m[0].trim();
209
+ m = text.match(/premise[\s:]*([valid|questionable|invalid]+)/i);
210
+ if (m) return m[0].trim();
211
+ return null;
212
+ }
213
+
214
+ function extractConflictResolutions(text) {
215
+ if (!text) return [];
216
+ var resolutions = [];
217
+ var blocks = text.split(/\n(?=\d+\.|resolve|conflict|disagree)/i);
218
+ for (var i = 0; i < blocks.length; i++) {
219
+ var b = blocks[i].trim();
220
+ if (/^(resolve|conflict|disagree)/i.test(b) || /^\d+\.\s*(resolve|conflict)/i.test(b)) {
221
+ resolutions.push(b.slice(0, 300));
222
+ }
223
+ }
224
+ return resolutions;
225
+ }
226
+
172
227
  module.exports = {
173
228
  synthesize: synthesize,
174
229
  buildSynthesisPrompt: buildSynthesisPrompt,
175
230
  hasContradictions: hasContradictions,
231
+ extractPremiseCheck: extractPremiseCheck,
232
+ extractConflictResolutions: extractConflictResolutions,
176
233
  };
@@ -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) {
@@ -227,6 +227,17 @@ function install({ silent = false, force = false, target = 'cursor', showBanner
227
227
  if (showBanner && !silent && (installed.length > 0 || skipped.length > 0)) {
228
228
  printWelcomeBanner(tId, log);
229
229
  }
230
+
231
+ // Auto-configure MCP server for supported hosts.
232
+ if (tId === 'cursor' || tId === 'claude' || tId === 'windsurf') {
233
+ try {
234
+ const mcpServer = require('./mcp-server');
235
+ mcpServer.writeMcpConfig(tId);
236
+ log(' MCP server configured. Restart ' + t.label + ' to activate.\n');
237
+ } catch (e) {
238
+ // MCP config is best-effort — don't fail install if it doesn't work.
239
+ }
240
+ }
230
241
  }
231
242
  }
232
243
 
@@ -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 };