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.
- package/HOW-TO-USE.md +19 -0
- package/README.md +64 -6
- package/agents/cards/raj.md +3 -3
- package/dist/HOW-TO-USE.md +19 -0
- package/dist/README.md +64 -6
- package/dist/agents/cards/raj.md +3 -3
- package/dist/bin/cli.js +56 -20
- package/dist/lib/chunk-planner.js +29 -6
- package/dist/lib/chunk-synthesis.js +59 -2
- package/dist/lib/collect.js +8 -4
- package/dist/lib/install.js +11 -0
- package/dist/lib/knowledge.js +196 -44
- package/dist/lib/mcp-server.js +745 -0
- package/dist/lib/orchestrator/run.js +6 -0
- package/dist/lib/source-discovery.js +6 -3
- package/dist/lib/system-prompt.js +112 -0
- package/dist/skills/knowledge-bank/SKILL.md +116768 -24
- package/dist/skills/raj/SKILL.md +4 -2
- package/package.json +2 -3
- package/skills/knowledge-bank/SKILL.md +116768 -24
- package/skills/raj/SKILL.md +4 -2
- package/dist/supabase/deliberation-config.example.json +0 -15
- package/dist/supabase/feedback-config.example.json +0 -7
- package/dist/supabase/migrations/001_persona_feedback.sql +0 -54
- package/supabase/deliberation-config.example.json +0 -15
- package/supabase/feedback-config.example.json +0 -7
- package/supabase/migrations/001_persona_feedback.sql +0 -54
|
@@ -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.
|
|
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
|
};
|
package/dist/lib/collect.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/lib/install.js
CHANGED
|
@@ -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
|
|
package/dist/lib/knowledge.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const fs
|
|
5
|
-
const path
|
|
6
|
-
const 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 {
|
|
37
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|