brainclaw 0.19.14 → 0.20.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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Agent capability profiles — describes what integration surfaces each
3
+ * agent supports so brainclaw can adapt its instruction file content,
4
+ * integration depth, and pressure level accordingly.
5
+ *
6
+ * Three profile tiers drive instruction file templates:
7
+ * A (full) — MCP + hooks + auto-approve → lightweight instructions
8
+ * B (standard) — MCP, no hooks → directive instructions with top traps
9
+ * C (limited) — no MCP → rich static content (plans, traps, decisions)
10
+ */
11
+ const PROFILES = {
12
+ 'claude-code': {
13
+ name: 'claude-code',
14
+ hasMcp: true,
15
+ hasHooks: true,
16
+ hasAutoApprove: true,
17
+ hasSkills: true,
18
+ hasRules: true,
19
+ instructionFile: 'CLAUDE.md',
20
+ sharedInstructionFile: true,
21
+ mcpConfigScope: 'both',
22
+ templateTier: 'A',
23
+ },
24
+ cursor: {
25
+ name: 'cursor',
26
+ hasMcp: true,
27
+ hasHooks: false,
28
+ hasAutoApprove: false,
29
+ hasSkills: false,
30
+ hasRules: true,
31
+ instructionFile: '.cursor/rules/brainclaw.md',
32
+ sharedInstructionFile: false,
33
+ mcpConfigScope: 'machine',
34
+ templateTier: 'B',
35
+ },
36
+ windsurf: {
37
+ name: 'windsurf',
38
+ hasMcp: true,
39
+ hasHooks: false,
40
+ hasAutoApprove: false,
41
+ hasSkills: false,
42
+ hasRules: true,
43
+ instructionFile: '.windsurfrules',
44
+ sharedInstructionFile: true,
45
+ mcpConfigScope: 'machine',
46
+ templateTier: 'B',
47
+ },
48
+ cline: {
49
+ name: 'cline',
50
+ hasMcp: true,
51
+ hasHooks: false,
52
+ hasAutoApprove: true,
53
+ hasSkills: false,
54
+ hasRules: true,
55
+ instructionFile: '.clinerules/brainclaw.md',
56
+ sharedInstructionFile: false,
57
+ mcpConfigScope: 'project',
58
+ templateTier: 'B',
59
+ },
60
+ roo: {
61
+ name: 'roo',
62
+ hasMcp: true,
63
+ hasHooks: false,
64
+ hasAutoApprove: true,
65
+ hasSkills: false,
66
+ hasRules: true,
67
+ instructionFile: '.roo/rules/brainclaw.md',
68
+ sharedInstructionFile: false,
69
+ mcpConfigScope: 'project',
70
+ templateTier: 'B',
71
+ },
72
+ continue: {
73
+ name: 'continue',
74
+ hasMcp: true,
75
+ hasHooks: false,
76
+ hasAutoApprove: false,
77
+ hasSkills: false,
78
+ hasRules: true,
79
+ instructionFile: '.continue/rules/brainclaw.md',
80
+ sharedInstructionFile: false,
81
+ mcpConfigScope: 'both',
82
+ templateTier: 'B',
83
+ },
84
+ opencode: {
85
+ name: 'opencode',
86
+ hasMcp: true,
87
+ hasHooks: false,
88
+ hasAutoApprove: false,
89
+ hasSkills: false,
90
+ hasRules: true,
91
+ instructionFile: 'AGENTS.md',
92
+ sharedInstructionFile: true,
93
+ mcpConfigScope: 'project',
94
+ templateTier: 'B',
95
+ },
96
+ codex: {
97
+ name: 'codex',
98
+ hasMcp: true,
99
+ hasHooks: false,
100
+ hasAutoApprove: false,
101
+ hasSkills: false,
102
+ hasRules: true,
103
+ instructionFile: 'AGENTS.md',
104
+ sharedInstructionFile: true,
105
+ mcpConfigScope: 'machine',
106
+ templateTier: 'B',
107
+ },
108
+ antigravity: {
109
+ name: 'antigravity',
110
+ hasMcp: true,
111
+ hasHooks: false,
112
+ hasAutoApprove: false,
113
+ hasSkills: false,
114
+ hasRules: true,
115
+ instructionFile: 'GEMINI.md',
116
+ sharedInstructionFile: true,
117
+ mcpConfigScope: 'machine',
118
+ templateTier: 'B',
119
+ },
120
+ 'github-copilot': {
121
+ name: 'github-copilot',
122
+ hasMcp: false,
123
+ hasHooks: false,
124
+ hasAutoApprove: false,
125
+ hasSkills: true,
126
+ hasRules: true,
127
+ instructionFile: '.github/copilot-instructions.md',
128
+ sharedInstructionFile: true,
129
+ mcpConfigScope: 'none',
130
+ templateTier: 'C',
131
+ },
132
+ };
133
+ /**
134
+ * Get the capability profile for a known agent.
135
+ * Returns undefined for unknown agent names.
136
+ */
137
+ export function getAgentCapabilityProfile(name) {
138
+ return PROFILES[name];
139
+ }
140
+ /**
141
+ * Get all known agent capability profiles.
142
+ */
143
+ export function getAllAgentCapabilityProfiles() {
144
+ return Object.values(PROFILES);
145
+ }
146
+ /**
147
+ * Get all agent names that match a given template tier.
148
+ */
149
+ export function getAgentsByTier(tier) {
150
+ return Object.values(PROFILES).filter((p) => p.templateTier === tier);
151
+ }
152
+ /**
153
+ * Check if an agent name is a known brainclaw-supported agent.
154
+ */
155
+ export function isKnownAgent(name) {
156
+ return name in PROFILES;
157
+ }
158
+ /**
159
+ * Summarize which integration surfaces are available for a given agent.
160
+ * Useful for setup UI to explain what brainclaw will configure.
161
+ */
162
+ export function describeAgentSurfaces(name) {
163
+ const profile = getAgentCapabilityProfile(name);
164
+ if (!profile)
165
+ return [];
166
+ const surfaces = [];
167
+ if (profile.hasMcp) {
168
+ surfaces.push(`MCP server (${profile.mcpConfigScope})`);
169
+ }
170
+ if (profile.hasRules) {
171
+ surfaces.push(`Instruction file (${profile.instructionFile})`);
172
+ }
173
+ if (profile.hasAutoApprove) {
174
+ surfaces.push('Auto-approve MCP tools');
175
+ }
176
+ if (profile.hasHooks) {
177
+ surfaces.push('Session hooks (pre-prompt + stop)');
178
+ }
179
+ if (profile.hasSkills) {
180
+ surfaces.push('Slash command / skill');
181
+ }
182
+ return surfaces;
183
+ }
184
+ //# sourceMappingURL=agent-capability.js.map
@@ -60,12 +60,30 @@ function readAgentsMarkdown(cwd) {
60
60
  const raw = fs.readFileSync(filepath, 'utf-8');
61
61
  const lines = raw.split(/\r?\n/);
62
62
  const title = lines.find((line) => line.trim().startsWith('#'))?.replace(/^#+\s*/, '').trim();
63
- const rules = lines
64
- .map((line) => line.trim())
65
- .filter((line) => /^([-*]|\d+\.)\s+/.test(line))
66
- .map((line) => line.replace(/^([-*]|\d+\.)\s+/, '').trim())
67
- .filter(Boolean)
68
- .slice(0, MAX_AGENT_RULES);
63
+ // Only extract rules from actionable sections, not from descriptive sections
64
+ // like "why this matters" which contain explanatory bullets, not instructions.
65
+ const SKIP_SECTIONS = /why this matters|what it provides|what brainclaw/i;
66
+ let currentSection = '';
67
+ let skipSection = false;
68
+ const rules = [];
69
+ for (const line of lines) {
70
+ const trimmed = line.trim();
71
+ if (trimmed.startsWith('#')) {
72
+ currentSection = trimmed.replace(/^#+\s*/, '');
73
+ skipSection = SKIP_SECTIONS.test(currentSection);
74
+ continue;
75
+ }
76
+ if (skipSection)
77
+ continue;
78
+ if (/^([-*]|\d+\.)\s+/.test(trimmed)) {
79
+ const text = trimmed.replace(/^([-*]|\d+\.)\s+/, '').trim();
80
+ if (text) {
81
+ rules.push(text);
82
+ if (rules.length >= MAX_AGENT_RULES)
83
+ break;
84
+ }
85
+ }
86
+ }
69
87
  return {
70
88
  present: true,
71
89
  title,
@@ -267,6 +267,12 @@ function buildBootstrapArtifacts(input) {
267
267
  const repoAnalysis = analyzeRepository(input.cwd);
268
268
  sourcesScanned.push('repo-analysis');
269
269
  seeds.push(...extractRepoAnalysisSeeds(repoAnalysis, input.target));
270
+ // Additional brownfield sources (step 12)
271
+ const additionalSeeds = extractAdditionalBrownfieldSeeds(input.cwd, input.target);
272
+ if (additionalSeeds.seeds.length > 0) {
273
+ sourcesScanned.push(...additionalSeeds.sources);
274
+ seeds.push(...additionalSeeds.seeds);
275
+ }
270
276
  const gitProbe = probeGit(input.cwd, input.target);
271
277
  if (gitProbe.available) {
272
278
  sourcesScanned.push('git');
@@ -703,6 +709,44 @@ function probeGit(cwd, target) {
703
709
  }));
704
710
  }
705
711
  }
712
+ // Step 13: Active branches
713
+ const branchResult = spawnSync('git', ['branch', '--no-merged', 'HEAD', '--format=%(refname:short)'], {
714
+ cwd,
715
+ encoding: 'utf-8',
716
+ timeout: 5000,
717
+ });
718
+ if (branchResult.status === 0) {
719
+ const branches = branchResult.stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean).slice(0, 5);
720
+ for (const branch of branches) {
721
+ hotspotSeeds.push(createSeed({
722
+ text: `Active branch: ${branch}`,
723
+ seedKind: 'hotspot',
724
+ sourceKind: 'git',
725
+ sourceRef: `branch:${branch}`,
726
+ confidence: 'low',
727
+ tags: ['bootstrap', 'git', 'branch'],
728
+ }));
729
+ }
730
+ }
731
+ // Step 13: Recent tags
732
+ const tagResult = spawnSync('git', ['tag', '--sort=-creatordate', '-l'], {
733
+ cwd,
734
+ encoding: 'utf-8',
735
+ timeout: 5000,
736
+ });
737
+ if (tagResult.status === 0) {
738
+ const tags = tagResult.stdout.split(/\r?\n/).map((t) => t.trim()).filter(Boolean).slice(0, 3);
739
+ if (tags.length > 0) {
740
+ hotspotSeeds.push(createSeed({
741
+ text: `Version tags: ${tags.join(', ')} (${tags.length} most recent)`,
742
+ seedKind: 'convention',
743
+ sourceKind: 'git',
744
+ sourceRef: 'tags',
745
+ confidence: 'medium',
746
+ tags: ['bootstrap', 'git', 'versioning'],
747
+ }));
748
+ }
749
+ }
706
750
  return {
707
751
  available: true,
708
752
  repoFingerprint,
@@ -1594,4 +1638,137 @@ function normalizeTarget(target) {
1594
1638
  const trimmed = target?.trim();
1595
1639
  return trimmed && trimmed.length > 0 ? trimmed : undefined;
1596
1640
  }
1641
+ // ─── Step 12: Additional brownfield sources ──────────────────────────────────
1642
+ const CI_WORKFLOW_DIRS = ['.github/workflows', '.gitlab'];
1643
+ const CI_FILES = ['.gitlab-ci.yml', 'Jenkinsfile', '.circleci/config.yml'];
1644
+ const CONTRIBUTING_FILES = ['CONTRIBUTING.md', 'CONTRIBUTING'];
1645
+ const CHANGELOG_FILES = ['CHANGELOG.md', 'CHANGELOG', 'HISTORY.md'];
1646
+ const DOCKER_FILES = ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'];
1647
+ const ENV_EXAMPLE_FILES = ['.env.example', '.env.sample', '.env.template'];
1648
+ const ADR_DIRS = ['doc/adr', 'docs/adr', 'doc/decisions', 'docs/decisions', 'adr'];
1649
+ function extractAdditionalBrownfieldSeeds(cwd, target) {
1650
+ const seeds = [];
1651
+ const sources = [];
1652
+ // CI/CD workflows
1653
+ for (const dir of CI_WORKFLOW_DIRS) {
1654
+ const fullPath = path.join(cwd, dir);
1655
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
1656
+ sources.push('ci_workflows');
1657
+ try {
1658
+ const files = fs.readdirSync(fullPath).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
1659
+ if (files.length > 0) {
1660
+ seeds.push(createSeed({
1661
+ text: `CI/CD: ${files.length} workflow(s) in ${dir}/`,
1662
+ seedKind: 'convention',
1663
+ sourceKind: 'ci_config',
1664
+ sourceRef: dir,
1665
+ confidence: 'medium',
1666
+ tags: ['bootstrap', 'ci'],
1667
+ relatedPaths: target ? [target] : undefined,
1668
+ }));
1669
+ }
1670
+ }
1671
+ catch { /* skip unreadable */ }
1672
+ break;
1673
+ }
1674
+ }
1675
+ for (const file of CI_FILES) {
1676
+ if (fs.existsSync(path.join(cwd, file))) {
1677
+ if (!sources.includes('ci_workflows'))
1678
+ sources.push('ci_config');
1679
+ seeds.push(createSeed({
1680
+ text: `CI/CD config: ${file}`,
1681
+ seedKind: 'convention',
1682
+ sourceKind: 'ci_config',
1683
+ sourceRef: file,
1684
+ confidence: 'medium',
1685
+ tags: ['bootstrap', 'ci'],
1686
+ }));
1687
+ break;
1688
+ }
1689
+ }
1690
+ // CONTRIBUTING.md
1691
+ const contributingPath = findFirstExisting(cwd, CONTRIBUTING_FILES);
1692
+ if (contributingPath) {
1693
+ sources.push('contributing');
1694
+ seeds.push(createSeed({
1695
+ text: `Contributing guide found: ${path.basename(contributingPath)}`,
1696
+ seedKind: 'convention',
1697
+ sourceKind: 'contributing',
1698
+ sourceRef: path.basename(contributingPath),
1699
+ confidence: 'medium',
1700
+ tags: ['bootstrap', 'contributing'],
1701
+ }));
1702
+ }
1703
+ // CHANGELOG
1704
+ const changelogPath = findFirstExisting(cwd, CHANGELOG_FILES);
1705
+ if (changelogPath) {
1706
+ sources.push('changelog');
1707
+ seeds.push(createSeed({
1708
+ text: `Changelog found: ${path.basename(changelogPath)}`,
1709
+ seedKind: 'convention',
1710
+ sourceKind: 'changelog',
1711
+ sourceRef: path.basename(changelogPath),
1712
+ confidence: 'low',
1713
+ tags: ['bootstrap', 'changelog'],
1714
+ }));
1715
+ }
1716
+ // Docker
1717
+ for (const file of DOCKER_FILES) {
1718
+ if (fs.existsSync(path.join(cwd, file))) {
1719
+ sources.push('docker');
1720
+ seeds.push(createSeed({
1721
+ text: `Docker config: ${file}`,
1722
+ seedKind: 'convention',
1723
+ sourceKind: 'docker',
1724
+ sourceRef: file,
1725
+ confidence: 'medium',
1726
+ tags: ['bootstrap', 'docker', 'infrastructure'],
1727
+ }));
1728
+ break;
1729
+ }
1730
+ }
1731
+ // .env.example
1732
+ const envPath = findFirstExisting(cwd, ENV_EXAMPLE_FILES);
1733
+ if (envPath) {
1734
+ sources.push('env_example');
1735
+ try {
1736
+ const content = fs.readFileSync(envPath, 'utf-8');
1737
+ const varCount = content.split(/\r?\n/).filter((l) => l.includes('=') && !l.startsWith('#')).length;
1738
+ seeds.push(createSeed({
1739
+ text: `Environment template: ${path.basename(envPath)} (${varCount} variables)`,
1740
+ seedKind: 'convention',
1741
+ sourceKind: 'env_example',
1742
+ sourceRef: path.basename(envPath),
1743
+ confidence: 'medium',
1744
+ tags: ['bootstrap', 'env', 'configuration'],
1745
+ }));
1746
+ }
1747
+ catch { /* skip unreadable */ }
1748
+ }
1749
+ // ADR (Architecture Decision Records)
1750
+ for (const dir of ADR_DIRS) {
1751
+ const fullPath = path.join(cwd, dir);
1752
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
1753
+ sources.push('adr');
1754
+ try {
1755
+ const files = fs.readdirSync(fullPath).filter((f) => f.endsWith('.md'));
1756
+ if (files.length > 0) {
1757
+ seeds.push(createSeed({
1758
+ text: `Architecture Decision Records: ${files.length} ADR(s) in ${dir}/`,
1759
+ seedKind: 'convention',
1760
+ sourceKind: 'adr',
1761
+ sourceRef: dir,
1762
+ confidence: 'high',
1763
+ tags: ['bootstrap', 'adr', 'architecture'],
1764
+ relatedPaths: target ? [target] : undefined,
1765
+ }));
1766
+ }
1767
+ }
1768
+ catch { /* skip unreadable */ }
1769
+ break;
1770
+ }
1771
+ }
1772
+ return { seeds, sources: [...new Set(sources)] };
1773
+ }
1597
1774
  //# sourceMappingURL=bootstrap.js.map
@@ -1,7 +1,8 @@
1
+ import path from 'node:path';
1
2
  import { loadConfig } from './config.js';
2
3
  import { resolveCrossProjectLinks, loadCrossProjectState } from './cross-project.js';
3
4
  import { buildContextDiff } from './context-diff.js';
4
- import { resolveStoreChain } from './store-resolution.js';
5
+ import { resolveContextStoreCwd, resolveStoreChain } from './store-resolution.js';
5
6
  import { findAgentIdentityByName, resolveCurrentAgentIdentity } from './agent-registry.js';
6
7
  import { hasReusableBootstrapProfile, runBootstrapProfile, selectDerivedSignals } from './bootstrap.js';
7
8
  import { buildAgentToolingContext } from './agent-context.js';
@@ -18,26 +19,28 @@ import { isTrapActive, listOperationalTraps } from './traps.js';
18
19
  import { buildEstimationReport } from '../commands/estimation-report.js';
19
20
  export const CONTEXT_SCHEMA_VERSION = '1.2';
20
21
  export function buildContext(options = {}) {
21
- const state = loadState(options.cwd);
22
- const config = loadConfig(options.cwd);
22
+ const requestedCwd = options.cwd ?? process.cwd();
23
+ const contextCwd = resolveContextStoreCwd(requestedCwd, options.target);
24
+ const state = loadState(contextCwd);
25
+ const config = loadConfig(contextCwd);
23
26
  // Resolve parent stores for multi-store merge (walk-up from cwd)
24
- const storeChain = resolveStoreChain(options.cwd ?? process.cwd());
27
+ const storeChain = resolveStoreChain(contextCwd);
25
28
  const profile = options.profile ?? config.profile ?? 'dev';
26
29
  const projectMode = config.project_mode ?? 'auto';
27
30
  const projectStrategy = config.projects?.strategy ?? 'manual';
28
31
  const currentHost = resolveCurrentHostId();
29
- const memoryVersion = getVisibleMemoryVersion({ cwd: options.cwd, hostId: options.host, allHosts: options.allHosts });
30
- const target = options.target?.trim() ?? '';
32
+ const memoryVersion = getVisibleMemoryVersion({ cwd: contextCwd, hostId: options.host, allHosts: options.allHosts });
33
+ const target = normalizeContextTarget(options.target, requestedCwd, contextCwd);
31
34
  const project = options.project?.trim() || inferProjectFromTarget(target, config);
32
35
  const agent = options.agent?.trim() || config.current_agent?.trim();
33
36
  const currentAgentIdentity = agent
34
- ? (options.agent?.trim() ? findAgentIdentityByName(agent, options.cwd) : resolveCurrentAgentIdentity(options.cwd))
37
+ ? (options.agent?.trim() ? findAgentIdentityByName(agent, contextCwd) : resolveCurrentAgentIdentity(contextCwd))
35
38
  : undefined;
36
39
  const profileMaxItems = { compact: 6, copilot: 5, quick: 3 };
37
40
  const maxItems = options.maxItems ?? profileMaxItems[profile] ?? 8;
38
41
  const maxChars = options.maxChars && options.maxChars > 0 ? options.maxChars : undefined;
39
42
  // Instructions will be resolved after parent-store merge below (line ~460)
40
- const rankingLookup = buildReputationRankingLookup(options.cwd);
43
+ const rankingLookup = buildReputationRankingLookup(contextCwd);
41
44
  const profileSections = {
42
45
  compact: ['plan', 'constraint'],
43
46
  copilot: ['constraint', 'trap'],
@@ -123,7 +126,7 @@ export function buildContext(options = {}) {
123
126
  },
124
127
  });
125
128
  }
126
- for (const trap of listOperationalTraps({ hostId: options.host, includeAllHosts: options.allHosts }, options.cwd).filter((entry) => isTrapActive(entry))) {
129
+ for (const trap of listOperationalTraps({ hostId: options.host, includeAllHosts: options.allHosts }, contextCwd).filter((entry) => isTrapActive(entry))) {
127
130
  items.push({
128
131
  id: trap.id,
129
132
  section: 'trap',
@@ -157,7 +160,7 @@ export function buildContext(options = {}) {
157
160
  const runtimeNotes = listRuntimeNotes({
158
161
  hostId: options.host,
159
162
  includeAllHosts: options.allHosts,
160
- }, options.cwd);
163
+ }, contextCwd);
161
164
  for (const note of runtimeNotes) {
162
165
  if (project && note.project && note.project !== project) {
163
166
  continue;
@@ -191,7 +194,7 @@ export function buildContext(options = {}) {
191
194
  });
192
195
  }
193
196
  if (options.includePending) {
194
- for (const p of listCandidates('pending', options.cwd)) {
197
+ for (const p of listCandidates('pending', contextCwd)) {
195
198
  const meta = [`${p.type}`, `stars:${p.star_count ?? 0}`, `uses:${p.usage_count ?? 0}`];
196
199
  if (p.author_id)
197
200
  meta.push(`author_id:${p.author_id}`);
@@ -294,7 +297,7 @@ export function buildContext(options = {}) {
294
297
  catch { /* non-fatal */ }
295
298
  }
296
299
  // Merge instructions from all stores in the chain
297
- const allInstructions = [...loadInstructions(options.cwd)];
300
+ const allInstructions = [...loadInstructions(contextCwd)];
298
301
  for (const parentStore of storeChain.slice(1)) {
299
302
  try {
300
303
  const parentInstrs = loadInstructions(parentStore.cwd);
@@ -330,34 +333,34 @@ export function buildContext(options = {}) {
330
333
  .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
331
334
  .slice(0, maxItems);
332
335
  const selected = maxChars ? applyCharBudget(ranked, maxChars) : ranked;
333
- const resumeSummary = buildCurrentAgentResumeSummary(options.cwd);
336
+ const resumeSummary = buildCurrentAgentResumeSummary(contextCwd);
334
337
  const scopedActivity = buildScopedActivity({
335
338
  target,
336
339
  project,
337
340
  state,
338
341
  runtimeNotes,
339
- pendingCandidates: listCandidates('pending', options.cwd),
342
+ pendingCandidates: listCandidates('pending', contextCwd),
340
343
  });
341
344
  const memoryDensity = classifyMemoryDensity(selected.length);
342
345
  const bootstrapEnabled = options.bootstrap !== false;
343
- let bootstrapAvailable = hasReusableBootstrapProfile(target, options.cwd);
346
+ let bootstrapAvailable = hasReusableBootstrapProfile(target, contextCwd);
344
347
  let derivedSignals;
345
348
  if (bootstrapEnabled && (options.refreshBootstrap || memoryDensity === 'low')) {
346
349
  const bootstrap = runBootstrapProfile({
347
350
  target,
348
351
  refresh: options.refreshBootstrap,
349
- cwd: options.cwd,
352
+ cwd: contextCwd,
350
353
  });
351
354
  bootstrapAvailable = bootstrap.profile.seed_count > 0;
352
355
  if (memoryDensity === 'low') {
353
- const signals = selectDerivedSignals(target, 5, options.cwd);
356
+ const signals = selectDerivedSignals(target, 5, contextCwd);
354
357
  if (signals.length > 0) {
355
358
  derivedSignals = signals;
356
359
  }
357
360
  }
358
361
  }
359
362
  else if (bootstrapEnabled && bootstrapAvailable && memoryDensity === 'low') {
360
- const signals = selectDerivedSignals(target, 5, options.cwd);
363
+ const signals = selectDerivedSignals(target, 5, contextCwd);
361
364
  if (signals.length > 0) {
362
365
  derivedSignals = signals;
363
366
  }
@@ -365,7 +368,7 @@ export function buildContext(options = {}) {
365
368
  const executionSensitive = isExecutionSensitiveTarget(target);
366
369
  const derivedUsesExecution = derivedSignals?.some((signal) => signal.source_kind === 'machine') ?? false;
367
370
  const derivedUsesTooling = derivedSignals?.some((signal) => signal.source_kind === 'skill' || signal.source_kind === 'mcp') ?? false;
368
- const rawAgentTooling = buildAgentToolingContext({ cwd: options.cwd });
371
+ const rawAgentTooling = buildAgentToolingContext({ cwd: contextCwd });
369
372
  const actionableAgentRules = rawAgentTooling.agents_rules.length > 0;
370
373
  const blockingTooling = rawAgentTooling.mcp_servers.some((server) => server.availability === 'missing_command');
371
374
  const shouldExposeExecution = memoryDensity === 'low' || executionSensitive || derivedUsesExecution;
@@ -375,7 +378,7 @@ export function buildContext(options = {}) {
375
378
  || actionableAgentRules
376
379
  || blockingTooling;
377
380
  const executionContext = shouldExposeExecution
378
- ? compactExecutionContext(buildExecutionContext({ cwd: options.cwd }))
381
+ ? compactExecutionContext(buildExecutionContext({ cwd: contextCwd }))
379
382
  : undefined;
380
383
  const agentTooling = shouldExposeAgentTooling
381
384
  ? summariseAgentTooling(rawAgentTooling)
@@ -385,7 +388,7 @@ export function buildContext(options = {}) {
385
388
  if (currentAgentIdentity || agent) {
386
389
  const agentName = agent;
387
390
  const agentId = currentAgentIdentity?.agent_id;
388
- const allClaims = [...listClaims(options.cwd), ...parentStoreClaims];
391
+ const allClaims = [...listClaims(contextCwd), ...parentStoreClaims];
389
392
  const activeClaims = allClaims.filter((c) => c.status === 'active' && (agentId ? c.agent_id === agentId : c.agent === agentName));
390
393
  const claimPlanIds = new Set(activeClaims.map((c) => c.plan_id).filter(Boolean));
391
394
  const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress' &&
@@ -399,7 +402,7 @@ export function buildContext(options = {}) {
399
402
  }
400
403
  // Cross-project items (subscriber links — read-only, always injected, bypass scoring)
401
404
  const crossProjectItems = [];
402
- for (const link of resolveCrossProjectLinks(options.cwd)) {
405
+ for (const link of resolveCrossProjectLinks(contextCwd)) {
403
406
  if (!link.available)
404
407
  continue;
405
408
  try {
@@ -448,7 +451,7 @@ export function buildContext(options = {}) {
448
451
  context_diff: options.sinceSession
449
452
  ? buildContextDiff({
450
453
  session: options.sinceSession,
451
- cwd: options.cwd,
454
+ cwd: contextCwd,
452
455
  includeItems: true,
453
456
  })
454
457
  : undefined,
@@ -460,7 +463,7 @@ export function buildContext(options = {}) {
460
463
  : undefined,
461
464
  estimation_calibration: (() => {
462
465
  try {
463
- const report = buildEstimationReport({ agent, cwd: options.cwd });
466
+ const report = buildEstimationReport({ agent, cwd: contextCwd });
464
467
  return report.summary.with_both >= 3 ? report.summary.calibration_hint : undefined;
465
468
  }
466
469
  catch {
@@ -1053,6 +1056,26 @@ function tokenise(input) {
1053
1056
  .map((x) => x.trim())
1054
1057
  .filter(Boolean);
1055
1058
  }
1059
+ function normalizeContextTarget(target, requestedCwd, contextCwd) {
1060
+ const trimmed = target?.trim() ?? '';
1061
+ if (!trimmed) {
1062
+ return '';
1063
+ }
1064
+ if (path.resolve(requestedCwd) === path.resolve(contextCwd)) {
1065
+ return trimmed;
1066
+ }
1067
+ if (!(path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('.'))) {
1068
+ return trimmed;
1069
+ }
1070
+ const absoluteTarget = path.isAbsolute(trimmed)
1071
+ ? path.resolve(trimmed)
1072
+ : path.resolve(requestedCwd, trimmed);
1073
+ const relativeToContext = path.relative(contextCwd, absoluteTarget);
1074
+ if (relativeToContext.startsWith('..') || path.isAbsolute(relativeToContext)) {
1075
+ return trimmed;
1076
+ }
1077
+ return relativeToContext.split(path.sep).join('/');
1078
+ }
1056
1079
  function matchesPath(pattern, target) {
1057
1080
  if (pattern === target)
1058
1081
  return true;