@nfinitmonkeys/clan-engine 0.1.1 → 0.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/dist/inspect.js CHANGED
@@ -51,6 +51,70 @@ function brainRootFor(tier, alphaName) {
51
51
  return `.brains/${(alphaName ?? '').toLowerCase()}`;
52
52
  return '.brain';
53
53
  }
54
+ /** Vault presence at a root: a wiki/ dir or a .brain-meta.json. */
55
+ function vaultExists(repo, r) {
56
+ return existsSync(join(repo, r, 'wiki')) || existsSync(join(repo, r, '.brain-meta.json'));
57
+ }
58
+ /** The vault an EXISTING load-brain SessionStart hook already points at
59
+ * (repo-relative), when that directory still exists. The live hook is ground
60
+ * truth: it names the vault sessions ACTUALLY load, so setup must never
61
+ * repoint away from it (e.g. the jungle repo's debris-only .brain/ vs the
62
+ * real .brains/obsidian the hook hydrates). */
63
+ function hookedBrainRoot(repo) {
64
+ const raw = read(join(repo, '.claude', 'settings.json'));
65
+ if (raw === null)
66
+ return undefined;
67
+ let doc;
68
+ try {
69
+ doc = JSON.parse(raw);
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ const entries = Array.isArray(doc?.hooks?.SessionStart) ? doc.hooks.SessionStart : [];
75
+ for (const entry of entries) {
76
+ for (const h of entry?.hooks ?? []) {
77
+ const m = /(?:^|[\s"'=])((?:\$\{?CLAUDE_PROJECT_DIR\}?\/)?[^\s"']+)\/scripts\/load-brain\.(?:mjs|sh)/
78
+ .exec(h?.command ?? '');
79
+ if (!m)
80
+ continue;
81
+ const rel = m[1].replace(/^\$\{?CLAUDE_PROJECT_DIR\}?\//, '').replace(/^\.\//, '');
82
+ if (rel && !rel.startsWith('/') && !rel.includes('..') && existsSync(join(repo, rel)))
83
+ return rel;
84
+ }
85
+ }
86
+ return undefined;
87
+ }
88
+ /** An EXISTING vault at ANY known root. Precedence:
89
+ * 1. the vault a load-brain SessionStart hook already loads — wins outright;
90
+ * 2. vaults containing wiki/identity.md, in order .district/brain >
91
+ * tier-default root > .brain > .brains/<alpha> > other .brains/*
92
+ * (preferring 'obsidian') — a real identity beats debris (e.g. a
93
+ * sources-only .brain/ left by a memory bridge);
94
+ * 3. bare-existing vaults in that same order;
95
+ * 4. none → undefined (tier default applies downstream).
96
+ * Data safety: reconcile scaffolds at the detected root, never a tier-default
97
+ * duplicate — and never away from the vault the live hook hydrates. */
98
+ function detectBrainRoot(repo, tier, alphaName) {
99
+ const hooked = hookedBrainRoot(repo);
100
+ if (hooked)
101
+ return hooked;
102
+ const candidates = ['.district/brain', brainRootFor(tier, alphaName), '.brain'];
103
+ if (alphaName)
104
+ candidates.push(`.brains/${alphaName.toLowerCase()}`);
105
+ try {
106
+ const dirs = readdirSync(join(repo, '.brains')).filter(d => vaultExists(repo, `.brains/${d}`));
107
+ for (const d of [...dirs.filter(x => x === 'obsidian'), ...dirs.filter(x => x !== 'obsidian').sort()]) {
108
+ candidates.push(`.brains/${d}`);
109
+ }
110
+ }
111
+ catch { /* no .brains */ }
112
+ const uniq = [...new Set(candidates)].filter(c => c && !c.endsWith('/'));
113
+ const withIdentity = uniq.find(r => existsSync(join(repo, r, 'wiki', 'identity.md')));
114
+ if (withIdentity)
115
+ return withIdentity;
116
+ return uniq.find(r => vaultExists(repo, r));
117
+ }
54
118
  export function inspect(repo, opts = {}) {
55
119
  const notes = [];
56
120
  const { tier, source: tierSource } = detectTier(repo, opts.tierFlag);
@@ -69,15 +133,48 @@ export function inspect(repo, opts = {}) {
69
133
  notes.push('.jungle/config.yaml is unparseable');
70
134
  }
71
135
  }
72
- const connected = cfg?.jungle?.status === 'connected' || cfg?.jungle?.status === 'approved';
136
+ // 'registered' / 'onboarded' are older attach-era statuses pointing at a real
137
+ // Jungle (fleet: Funnel, Beacon) — flipping them to standalone would rewrite
138
+ // their packs with "the user is the approver" fiction. apiUrl is the
139
+ // connection intent (Funnel's apiKey is empty but its Jungle is real — the
140
+ // missing-key note below stays honest). Full creds with no status at all =
141
+ // legacy attach (old clan-ready treated apiUrl alone as connected).
142
+ const status = cfg?.jungle?.status;
143
+ const hasCreds = !!(cfg?.jungle?.apiUrl && cfg?.jungle?.alphaId && cfg?.jungle?.apiKey);
144
+ const connected = status === 'connected' || status === 'approved'
145
+ || ((status === 'registered' || status === 'onboarded') && !!cfg?.jungle?.apiUrl)
146
+ || (status === undefined && hasCreds);
73
147
  const mode = connected ? 'connected' : 'standalone';
74
- const identity = cfg?.project?.name && cfg?.project?.alphaName ? {
148
+ // ── .district/district.config.yaml (the district box's identity) ──
149
+ let dcfg = null;
150
+ const dcfgRaw = read(join(repo, '.district', 'district.config.yaml'));
151
+ if (dcfgRaw !== null) {
152
+ try {
153
+ dcfg = YAML.parse(dcfgRaw);
154
+ }
155
+ catch {
156
+ notes.push('.district/district.config.yaml is unparseable');
157
+ }
158
+ }
159
+ // ── existing vault (any known root) — wins over the tier default ──
160
+ const existingBrainRoot = detectBrainRoot(repo, tier, dcfg?.district?.alphaName ?? cfg?.project?.alphaName);
161
+ const identity = dcfg?.district?.name && dcfg?.district?.alphaName ? {
162
+ name: dcfg.district.name,
163
+ alphaName: dcfg.district.alphaName,
164
+ description: dcfg.district.description ?? '',
165
+ tier,
166
+ mode,
167
+ alphaId: dcfg.district.alphaId,
168
+ boxPort: String(dcfg.http?.port ?? '7787'),
169
+ brainRoot: existingBrainRoot,
170
+ } : cfg?.project?.name && cfg?.project?.alphaName ? {
75
171
  name: cfg.project.name,
76
172
  alphaName: cfg.project.alphaName,
77
173
  description: cfg.project.description ?? '',
78
174
  tier,
79
175
  mode,
80
176
  alphaId: cfg.jungle?.alphaId,
177
+ brainRoot: existingBrainRoot,
81
178
  jungle: connected && cfg.jungle?.apiUrl && cfg.jungle?.alphaId && cfg.jungle?.apiKey
82
179
  ? { apiUrl: cfg.jungle.apiUrl, alphaId: cfg.jungle.alphaId, apiKey: cfg.jungle.apiKey }
83
180
  : undefined,
@@ -104,8 +201,8 @@ export function inspect(repo, opts = {}) {
104
201
  notes.push('.mcp.json is unparseable');
105
202
  }
106
203
  }
107
- // ── brain (at the tier-aware root) ──
108
- const bRoot = brainRootFor(tier, cfg?.project?.alphaName);
204
+ // ── brain (existing vault at any root > the tier-aware default) ──
205
+ const bRoot = existingBrainRoot ?? brainRootFor(tier, dcfg?.district?.alphaName ?? cfg?.project?.alphaName);
109
206
  let hasBrain = false;
110
207
  let brainLayoutVersion;
111
208
  const metaRaw = read(join(repo, bRoot, '.brain-meta.json'));
@@ -166,6 +263,6 @@ export function inspect(repo, opts = {}) {
166
263
  repo, os, isGit, tier, tierSource, mode, identity,
167
264
  hasConfig, configError, hasMcp, mcpServers,
168
265
  inventory: scanInventory(repo, { home: opts.home }),
169
- hasBrain, brainLayoutVersion, state, notes,
266
+ hasBrain, brainLayoutVersion, brainRoot: existingBrainRoot, state, notes,
170
267
  };
171
268
  }
@@ -14,5 +14,7 @@ import type { InventoryItem } from './types.js';
14
14
  export declare function scanInventory(repo: string, opts?: {
15
15
  home?: string;
16
16
  }): InventoryItem[];
17
- /** Write the manifest the awareness skill references. Returns the repo-relative path. */
17
+ /** Write the manifest the awareness skill references. Returns the repo-relative
18
+ * path. PROJECT scope only — global (~/.claude) assets are the operator's
19
+ * private tooling and must not be persisted into a committable repo file. */
18
20
  export declare function writeInventoryManifest(repo: string, items: InventoryItem[]): string;
package/dist/inventory.js CHANGED
@@ -126,11 +126,13 @@ export function scanInventory(repo, opts = {}) {
126
126
  return true;
127
127
  });
128
128
  }
129
- /** Write the manifest the awareness skill references. Returns the repo-relative path. */
129
+ /** Write the manifest the awareness skill references. Returns the repo-relative
130
+ * path. PROJECT scope only — global (~/.claude) assets are the operator's
131
+ * private tooling and must not be persisted into a committable repo file. */
130
132
  export function writeInventoryManifest(repo, items) {
131
133
  const dir = join(repo, '.clan');
132
134
  mkdirSync(dir, { recursive: true });
133
- const rel = items.map(i => ({ kind: i.kind, name: i.name, scope: i.scope, description: i.description ?? null }));
135
+ const rel = items.filter(i => i.scope === 'project').map(i => ({ kind: i.kind, name: i.name, scope: i.scope, description: i.description ?? null }));
134
136
  writeFileSync(join(dir, 'inventory.json'), JSON.stringify({ generatedBy: 'clan-engine', items: rel }, null, 2) + '\n', 'utf-8');
135
137
  return '.clan/inventory.json';
136
138
  }
package/dist/mcp.d.ts CHANGED
@@ -17,6 +17,16 @@ export declare function jungleLocalServer(c: JungleCreds): {
17
17
  args: string[];
18
18
  env: Record<string, string>;
19
19
  };
20
+ /** District-tier creds: the jungle-local server speaks to the LOCAL district
21
+ * box (localhost:<boxPort>), not the cloud Jungle. Same server shape — only
22
+ * the URL differs. The self-row key is only capturable at district init (the
23
+ * box stores its hash); callers pass it via env, never argv. */
24
+ export declare function districtBoxCreds(boxPort: string, alphaId: string, apiKey: string): JungleCreds;
25
+ /** .mcp.json plan for a district — jungle-local → the local box. */
26
+ export declare function planDistrictMcpJson(repo: string, boxPort: string, creds: {
27
+ alphaId: string;
28
+ apiKey: string;
29
+ }, isGit: boolean): PlanAction[];
20
30
  /**
21
31
  * Produce the reconciliation action(s) for .mcp.json given the target creds.
22
32
  * Returns a 'merge' action (new file content) plus, when a fresh file is
package/dist/mcp.js CHANGED
@@ -17,6 +17,17 @@ export function jungleLocalServer(c) {
17
17
  env.JUNGLE_HUMAN_EMAIL = c.humanEmail;
18
18
  return { command: 'npx', args: ['-y', '@nfinitmonkeys/jungle-mcp'], env };
19
19
  }
20
+ /** District-tier creds: the jungle-local server speaks to the LOCAL district
21
+ * box (localhost:<boxPort>), not the cloud Jungle. Same server shape — only
22
+ * the URL differs. The self-row key is only capturable at district init (the
23
+ * box stores its hash); callers pass it via env, never argv. */
24
+ export function districtBoxCreds(boxPort, alphaId, apiKey) {
25
+ return { apiUrl: `http://localhost:${boxPort}`, alphaId, apiKey };
26
+ }
27
+ /** .mcp.json plan for a district — jungle-local → the local box. */
28
+ export function planDistrictMcpJson(repo, boxPort, creds, isGit) {
29
+ return planMcpJson(repo, districtBoxCreds(boxPort, creds.alphaId, creds.apiKey), isGit);
30
+ }
20
31
  /**
21
32
  * Produce the reconciliation action(s) for .mcp.json given the target creds.
22
33
  * Returns a 'merge' action (new file content) plus, when a fresh file is
package/dist/reconcile.js CHANGED
@@ -63,6 +63,11 @@ function reconcileClaudeMd(repo, i) {
63
63
  }
64
64
  export function reconcile(identity, ins, opts = {}) {
65
65
  const repo = ins.repo;
66
+ // Data safety: an EXISTING vault at any known root wins over the tier
67
+ // default — re-tiering (or the jungle repo's own .brain/) must never mint a
68
+ // duplicate vault. inspect detects it; every template downstream honors it.
69
+ if (ins.brainRoot && !identity.brainRoot)
70
+ identity = { ...identity, brainRoot: ins.brainRoot };
66
71
  const actions = [];
67
72
  // 1. CLAUDE.md declaration
68
73
  actions.push(reconcileClaudeMd(repo, identity));
@@ -83,19 +88,34 @@ export function reconcile(identity, ins, opts = {}) {
83
88
  // 4. managed .claude surface (awareness SKILL.md + tier command pack + hook script)
84
89
  for (const f of claudeSurface(identity))
85
90
  actions.push(reconcileFile(repo, f));
86
- // 5. inventory manifest (refresh when changed — idempotent otherwise)
87
- const inv = scanInventory(repo);
91
+ // 5. inventory manifest (refresh when changed — idempotent otherwise).
92
+ // PROJECT scope only: persisting the operator's global ~/.claude asset
93
+ // list would leak private tooling metadata into every (committable) repo
94
+ // and churn on every machine change. Global items stay in the in-memory
95
+ // InspectResult for the wizard.
96
+ const inv = scanInventory(repo).filter(i => i.scope === 'project');
88
97
  const invContent = JSON.stringify({ generatedBy: 'clan-engine', items: inv.map(i => ({ kind: i.kind, name: i.name, scope: i.scope, description: i.description ?? null })) }, null, 2) + '\n';
89
98
  const invExisting = read(repo, '.clan/inventory.json');
90
99
  if (invExisting === null)
91
- actions.push({ kind: 'create', path: '.clan/inventory.json', content: invContent, note: `inventory ${inv.length} existing asset(s) the Alpha can use` });
100
+ actions.push({ kind: 'create', path: '.clan/inventory.json', content: invContent, note: `inventory ${inv.length} existing project asset(s) the Alpha can use` });
92
101
  else if (invExisting !== invContent)
93
- actions.push({ kind: 'update', path: '.clan/inventory.json', content: invContent, note: `refresh inventory (${inv.length} asset(s))` });
102
+ actions.push({ kind: 'update', path: '.clan/inventory.json', content: invContent, note: `refresh inventory (${inv.length} project asset(s))` });
94
103
  else
95
104
  actions.push({ kind: 'skip', path: '.clan/inventory.json', note: 'inventory unchanged', reason: 'unchanged' });
96
105
  // 6. .mcp.json (connected mode)
97
106
  if (opts.jungle)
98
107
  actions.push(...planMcpJson(repo, opts.jungle, ins.isGit));
108
+ // 6b. journals + backups under .claude/.clan-engine/ can embed live-key
109
+ // artifacts (a backed-up .mcp.json on merge) — never committable. Ignore
110
+ // the dir in the same run that gitignores .mcp.json itself.
111
+ if (ins.isGit) {
112
+ const gi = read(repo, '.gitignore') ?? '';
113
+ if (!gi.split('\n').some(l => l.trim() === '.claude/.clan-engine/')) {
114
+ const priorAppend = actions.some(a => a.kind === 'append' && a.path === '.gitignore');
115
+ const prefix = priorAppend || gi.length === 0 || gi.endsWith('\n') ? '' : '\n';
116
+ actions.push({ kind: 'append', path: '.gitignore', content: prefix + '.claude/.clan-engine/\n', note: 'gitignore .claude/.clan-engine/ (journals + backups may hold backed-up keys)' });
117
+ }
118
+ }
99
119
  // 7. SessionStart brain hook (tier-aware command)
100
120
  actions.push(...planBrainHook(repo, identity));
101
121
  // 8. brain scaffold — create-only (never overwrite accumulated brain pages)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Secret / infra / PHI filter — the non-negotiable gate on anything that goes
3
+ * into a brain. A clan's brain_index is cross-clan-readable by council/master
4
+ * keys, and repo docs + session memory routinely hold ssh keys, prod creds,
5
+ * connection strings, and (for HIPAA clans) PHI. Every candidate body is scanned
6
+ * BEFORE it becomes a brain page; anything that trips is HELD BACK, never
7
+ * written. Reasons are LABELS ONLY — the matched secret text is never returned,
8
+ * so a scan result is safe to log.
9
+ *
10
+ * PHI scanning is ON BY DEFAULT (opt out with { hipaa: false }): a false-positive
11
+ * hold-back is cheap; a PHI leak is catastrophic.
12
+ */
13
+ export interface ScanResult {
14
+ flagged: boolean;
15
+ reasons: string[];
16
+ }
17
+ /**
18
+ * Scan text for secrets + infra identifiers (always) and PHI (unless
19
+ * hipaa:false is explicitly passed — PHI is scanned by default).
20
+ */
21
+ export declare function scanSensitive(text: string, opts?: {
22
+ hipaa?: boolean;
23
+ }): ScanResult;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Secret / infra / PHI filter — the non-negotiable gate on anything that goes
3
+ * into a brain. A clan's brain_index is cross-clan-readable by council/master
4
+ * keys, and repo docs + session memory routinely hold ssh keys, prod creds,
5
+ * connection strings, and (for HIPAA clans) PHI. Every candidate body is scanned
6
+ * BEFORE it becomes a brain page; anything that trips is HELD BACK, never
7
+ * written. Reasons are LABELS ONLY — the matched secret text is never returned,
8
+ * so a scan result is safe to log.
9
+ *
10
+ * PHI scanning is ON BY DEFAULT (opt out with { hipaa: false }): a false-positive
11
+ * hold-back is cheap; a PHI leak is catastrophic.
12
+ */
13
+ const SECRET_PATTERNS = [
14
+ ['private-key-block', /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP |ENCRYPTED )?PRIVATE KEY-----/],
15
+ ['ssh-public-key', /\bssh-(?:rsa|ed25519|dss)\s+AAAA[0-9A-Za-z+/]{20,}/],
16
+ ['aws-access-key-id', /\b(?:AKIA|ASIA|AGPA|AIDA)[0-9A-Z]{16}\b/],
17
+ ['aws-secret-assignment', /\b(?:aws.?)?secret.?access.?key\b\s*[:=]\s*\S{16,}/i],
18
+ ['gcp-api-key', /\bAIza[0-9A-Za-z_\-]{35,}/],
19
+ ['gcp-service-account', /\b[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com\b|"private_key_id"\s*:/],
20
+ ['azure-storage-key', /\bAccountKey\s*=\s*[A-Za-z0-9+/=]{20,}/i],
21
+ ['azure-sas', /\bSharedAccessSignature\b|[?&]sig=[A-Za-z0-9%]{20,}/],
22
+ ['stripe-key', /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b|\bwhsec_[A-Za-z0-9]{16,}\b/],
23
+ ['twilio-key', /\b(?:SK|AC)[0-9a-f]{32}\b/],
24
+ ['sendgrid-key', /\bSG\.[A-Za-z0-9_\-]{16,}\.[A-Za-z0-9_\-]{16,}\b/],
25
+ ['github-token', /\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b/],
26
+ ['slack-token', /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/],
27
+ ['openai-anthropic-cortex-key', /\bsk-(?:ant-|cortex-|proj-|live-)?[A-Za-z0-9_-]{16,}\b/],
28
+ ['jwt', /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/],
29
+ ['bearer-or-authorization', /\b(?:authorization|bearer)\b\s*:?\s*(?:bearer\s+)?[A-Za-z0-9._\-]{16,}/i],
30
+ ['uri-with-embedded-creds', /\b[a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+:[^\s@/]+@/i],
31
+ ['dotnet-connection-string', /\b(?:Password|Pwd)\s*=\s*[^;\s]{6,};?/i],
32
+ ['generic-secret-assignment', /\b(?:password|passwd|secret|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer|token)\b\s*[:=]\s*['"]?[A-Za-z0-9/_+=.\-]{10,}/i],
33
+ ['env-assignment-high-entropy', /^[A-Z][A-Z0-9_]{2,}\s*=\s*\S{16,}$/m],
34
+ ];
35
+ const INFRA_PATTERNS = [
36
+ ['aws-instance-id', /\bi-[0-9a-f]{8,17}\b/],
37
+ ['aws-sg-id', /\bsg-[0-9a-f]{8,17}\b/],
38
+ ['aws-subnet-or-vpc-id', /\b(?:subnet|vpc|eni|vol|ami)-[0-9a-f]{8,17}\b/],
39
+ ['aws-account-arn', /\b\d{12}\.dkr\.ecr\b|arn:aws:[^\s]*:\d{12}:/i],
40
+ ['rds-or-cloud-endpoint', /\b[a-z0-9-]+\.[a-z0-9]+\.[a-z0-9-]+\.(?:rds|elasticache|redshift)\.amazonaws\.com\b/i],
41
+ ['private-ipv4', /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3})\b/],
42
+ ['private-ipv6', /\b(?:fd[0-9a-f]{2}:[0-9a-f:]{2,}|fe80:[0-9a-f:]{2,})/i],
43
+ ['ssh-to-ip', /\bssh\b[^\n]*@\d{1,3}(?:\.\d{1,3}){3}\b/i],
44
+ ['key-file-ref', /\b[\w-]{3,}\.(?:pem|ppk)\b|(?:\/|~\/)?\.ssh\/[\w.-]+/],
45
+ ];
46
+ const PHI_PATTERNS = [
47
+ ['ssn', /\b\d{3}[-. ]\d{2}[-. ]\d{4}\b|\bssn\b[^\n]{0,20}\b\d{9}\b/i],
48
+ ['mrn', /\bMRN\b\s*[:#]?\s*\d{5,}/i],
49
+ ['dob', /\b(?:DOB|date of birth)\b\s*[:#]?\s*\d/i],
50
+ ['patient-identifier', /\bpatient\b[^.\n]{0,40}\b(?:name|ssn|mrn|dob|record\s*number)\b/i],
51
+ ];
52
+ /**
53
+ * Scan text for secrets + infra identifiers (always) and PHI (unless
54
+ * hipaa:false is explicitly passed — PHI is scanned by default).
55
+ */
56
+ export function scanSensitive(text, opts = {}) {
57
+ const reasons = [];
58
+ if (typeof text !== 'string' || text.length === 0)
59
+ return { flagged: false, reasons };
60
+ for (const [label, re] of SECRET_PATTERNS)
61
+ if (re.test(text))
62
+ reasons.push(label);
63
+ for (const [label, re] of INFRA_PATTERNS)
64
+ if (re.test(text))
65
+ reasons.push('infra:' + label);
66
+ if (opts.hipaa !== false)
67
+ for (const [label, re] of PHI_PATTERNS)
68
+ if (re.test(text))
69
+ reasons.push('phi:' + label);
70
+ return { flagged: reasons.length > 0, reasons };
71
+ }
@@ -36,7 +36,10 @@ export declare function findGeoTierMarker(content: string): {
36
36
  * (reconcile handles that) — we never rewrite a user's prose. */
37
37
  export declare function claudeMdIdentity(i: ClanIdentity): string;
38
38
  /** Where the brain lives, per tier (matches the fleet's clan-ready layout so
39
- * the engine never puts .brain/ where a district/jungle expects its vault). */
39
+ * the engine never puts .brain/ where a district/jungle expects its vault).
40
+ * An EXISTING vault (i.brainRoot, set from inspect) wins over the tier
41
+ * default — applying to a repo with a vault elsewhere must never mint a
42
+ * duplicate. */
40
43
  export declare function brainRoot(i: ClanIdentity): string;
41
44
  export declare function brainScaffold(i: ClanIdentity, today: string): GeneratedFile[];
42
45
  /** The command wired into .claude/settings.json SessionStart. Relative (hooks