@nfinitmonkeys/clan-engine 0.2.0 → 0.4.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)
@@ -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
package/dist/templates.js CHANGED
@@ -85,13 +85,13 @@ This repo is a **${noun.toUpperCase()}** node in the Nfinit Monkeys geography
85
85
  ## Tool defaults
86
86
 
87
87
  - Default MCP prefix: \`mcp__jungle-local__*\` (this node's voice).
88
- - The Alpha's brain (persistent memory) lives under \`.brain/\` — hydrated at
89
- session start by the SessionStart hook.
88
+ - The Alpha's brain (persistent memory) lives under \`${brainRoot(i)}/\` — hydrated
89
+ at session start by the SessionStart hook.
90
90
 
91
91
  ## Scope
92
92
 
93
93
  Routine work here is node-local. Slash commands are tier-named:
94
- \`/${noun}:status\` · \`/${noun}:inbox\` · \`/${noun}:log\` · \`/${noun}:policies\` · \`/${noun}:request\`.
94
+ \`/${noun}:status\` · \`/${noun}:inbox\` · \`/${noun}:log\` · \`/${noun}:policies\` · \`/${noun}:request\` · \`/${noun}:up\`.
95
95
  `;
96
96
  }
97
97
  // ─── awareness skill (SKILL.md directory format — loadable) ─────────────────
@@ -100,14 +100,43 @@ function awarenessSkill(i) {
100
100
  const modeLine = i.mode === 'connected'
101
101
  ? `- **Mode:** connected${i.jungle?.apiUrl ? ` to ${i.jungle.apiUrl}` : ''}${i.alphaId ? `\n- **Alpha ID:** \`${i.alphaId}\`` : ''}`
102
102
  : `- **Mode:** standalone — no Jungle, no approval queue. The developer IS the Human Council.`;
103
- const governance = i.mode === 'connected'
104
- ? `## Governance (connected)
103
+ const workingAs = noun === 'clan'
104
+ ? `## Working *as* the Alpha
105
+
106
+ In this repo you act **as ${i.alphaName}**. But ${i.alphaName} is bigger than one
107
+ chat: it has a **brain** (\`npx -y @nfinitmonkeys/clan brain "…"\` — persistent
108
+ memory), an **inbox** (\`npx -y @nfinitmonkeys/clan talk "…"\`), and an
109
+ **autonomous loop** (\`npx -y @nfinitmonkeys/clan start\`). If the user says "I
110
+ want to talk to the Alpha," don't say "you already are" — point them to those.`
111
+ : noun === 'district'
112
+ ? `## Working *as* the Alpha
113
+
114
+ In this repo you act **as ${i.alphaName}**. The district box (localhost:${i.boxPort ?? '7787'})
115
+ is the node's substrate — speak through \`mcp__jungle-local__*\` tools, not the
116
+ clan CLI.`
117
+ : `## Working *as* the Alpha
118
+
119
+ In this repo you act **as ${i.alphaName}** — the org hub's Alpha. Speak through
120
+ \`mcp__jungle-local__*\` (this repo's declared identity).`;
121
+ const governance = noun === 'district'
122
+ ? `## Governance (district)
123
+
124
+ Be honest: **the district box has no policy engine yet**. Acts are ungated but
125
+ logged (see /district:log). The composing rule: *a parent sees a child only by
126
+ the child's consent; every child runs without its parent*.`
127
+ : noun === 'jungle'
128
+ ? `## Governance (jungle)
129
+
130
+ The invariant: Alphas **propose**, humans **approve** — every sensitive act
131
+ routes through the approval queue (BOSS).`
132
+ : i.mode === 'connected'
133
+ ? `## Governance (connected)
105
134
 
106
135
  Alphas **propose**, humans **approve**. Sensitive actions go through a gated
107
136
  approval flow (\`request_approval\` → Human Council reviews in BOSS → execute →
108
137
  logged). To change this node's permissions, tell the user to message the Alpha
109
138
  in natural language — it will propose the change for approval.`
110
- : `## Governance (standalone)
139
+ : `## Governance (standalone)
111
140
 
112
141
  There is no remote council. The developer at this terminal IS the council. When
113
142
  they want to change a policy: confirm it, edit \`.jungle/config.yaml\`, restart
@@ -118,27 +147,24 @@ If the user already has agents, skills, or slash commands (e.g. from other
118
147
  tools), PREFER them where they overlap instead of duplicating. The clan is a
119
148
  teammate to the existing setup, not a replacement — check \`.clan/inventory.json\`
120
149
  (written by the engine) for what's available, and route to it by name.`;
150
+ const runVia = noun === 'clan' ? ` run via \`@nfinitmonkeys/clan\`` : '';
121
151
  return {
122
152
  path: '.claude/skills/clan-awareness/SKILL.md',
123
153
  managed: true,
124
154
  content: `---
125
155
  name: clan-awareness
126
- description: This project is a ${noun} node (${i.alphaName} / ${i.name}) run via @nfinitmonkeys/clan — how to answer status, governance, and "talk to the Alpha" questions
156
+ description: This project is a ${noun} node (${i.alphaName} / ${i.name}) — how to answer status, governance, and "talk to the Alpha" questions
127
157
  ${markerLines()}
128
158
  ---
129
159
 
130
- # Clan Awareness — ${i.name}
131
-
132
- You are inside a **${noun}** node run via \`@nfinitmonkeys/clan\`, with its own
133
- Alpha (**${i.alphaName}**), policies, and accumulated memory (a "brain").
160
+ # Node Awareness — ${i.name}
134
161
 
135
- ## Working *as* the Alpha
162
+ This repo is a **${noun.toUpperCase()}** node in the Nfinit Monkeys geography
163
+ (Biome ⊃ Jungle ⊃ District ⊃ Clan)${runVia}, with its own Alpha
164
+ (**${i.alphaName}**), policies, and accumulated memory (a "brain"). Identity is
165
+ DECLARED here, never inferred.
136
166
 
137
- In this repo you act **as ${i.alphaName}**. But ${i.alphaName} is bigger than one
138
- chat: it has a **brain** (\`npx -y @nfinitmonkeys/clan brain "…"\` — persistent
139
- memory), an **inbox** (\`npx -y @nfinitmonkeys/clan talk "…"\`), and an
140
- **autonomous loop** (\`npx -y @nfinitmonkeys/clan start\`). If the user says "I
141
- want to talk to the Alpha," don't say "you already are" — point them to those.
167
+ ${workingAs}
142
168
 
143
169
  ## This node
144
170
 
@@ -149,10 +175,15 @@ ${modeLine}
149
175
 
150
176
  ## Slash commands (tier-named)
151
177
 
152
- \`/${noun}:status\` · \`/${noun}:inbox\` · \`/${noun}:log\` · \`/${noun}:policies\` · \`/${noun}:request\`
178
+ \`/${noun}:status\` · \`/${noun}:inbox\` · \`/${noun}:log\` · \`/${noun}:policies\` · \`/${noun}:request\` · \`/${noun}:up\` (setup/repair wizard)
153
179
 
154
180
  If the user types a different tier's command (e.g. \`/clan:status\` in a
155
- district), point them to the right one — don't silently alias.
181
+ district), point them to the right one — the correction teaches the tier; don't
182
+ silently alias.
183
+
184
+ The Alpha's brain (persistent memory) lives at \`${brainRoot(i)}\` — hydrated at
185
+ session start by the SessionStart hook. The anatomy is identical at every tier:
186
+ Alpha + brain + governance log + members + registry of children.
156
187
 
157
188
  ${usingExisting}
158
189
 
@@ -175,7 +206,127 @@ ${body}
175
206
  `,
176
207
  };
177
208
  }
209
+ /** Every pack asserts the declared identity up front — the correction teaches the tier. */
210
+ function header(i) {
211
+ return `Assert the declaration in the output header: **${i.alphaName} · ${i.name} · ${i.tier.toUpperCase()}**.`;
212
+ }
178
213
  function commandPack(i) {
214
+ if (i.tier === 'biome')
215
+ throw new Error('no pack template for tier biome yet — biome is unbuilt');
216
+ const pack = i.tier === 'district' ? districtPack(i) : i.tier === 'jungle' ? junglePack(i) : clanPack(i);
217
+ // /tier:up — the wizard door (design §4 Layer 2) renders at every built tier.
218
+ return [...pack, upCmd(i)];
219
+ }
220
+ /** Version-pinned engine invocation for generated wizard text. Unpinned
221
+ * `npx @nfinitmonkeys/clan-engine` would resolve to an OLDER published engine
222
+ * (no district/jungle packs, no existing-vault detection, no --json
223
+ * redaction) whose apply would overwrite the very pack that invoked it. */
224
+ const ENGINE_NPX = `npx -y @nfinitmonkeys/clan-engine@${ENGINE_VERSION.split('.').slice(0, 2).join('.')}`;
225
+ /** The /tier:up door — Claude Code IS the wizard; the engine does every write.
226
+ * Clan tier gets the full greenfield/convert/repair playbook; district/jungle
227
+ * get a short repair wizard (their identity is provisioned by node-up, never
228
+ * interviewed here). */
229
+ function upCmd(i) {
230
+ const t = i.tier;
231
+ if (t !== 'clan') {
232
+ return cmd(t, 'up', `Repair / update this ${t} node — engine inspect → plan → approval → apply → doctor`, `${header(i)}
233
+
234
+ Repair/update wizard for this already-provisioned ${t} node. **You improvise the
235
+ conversation; the engine does every write.** Never hand-author node files; never
236
+ print API keys or secrets — credentials flow via env, not argv (the engine's
237
+ --json output masks key values as \`[redacted]\`).
238
+
239
+ 1. \`${ENGINE_NPX} inspect --json\` — classify state (fresh / partial / current / legacy / drifted) and confirm tier=${t}.
240
+ 2. \`${ENGINE_NPX} plan --json\` — present the planned writes / merges / backups in plain language.
241
+ 3. Only on the user's explicit approval: \`${ENGINE_NPX} apply\`.
242
+ 4. \`${ENGINE_NPX} doctor\` — report honestly; a failing check is a finding, not noise.
243
+
244
+ Re-runs are repairs, not errors. Identity is DECLARED here — this wizard never
245
+ re-tiers or renames a node. Reverse any apply: \`${ENGINE_NPX} undo\`.
246
+ Day-to-day verbs are the tier-named commands: /${t}:status · /${t}:inbox · /${t}:log · /${t}:policies · /${t}:request.`);
247
+ }
248
+ return cmd(t, 'up', `The one door — conversational clan setup/repair wizard (the engine does every write)`, `You are the setup wizard for this repo's clan — the ONE door. Doctrine, non-negotiable:
249
+
250
+ - **You improvise the conversation; the engine does every write.** NEVER hand-author
251
+ clan files (the CLAUDE.md identity block, the .claude/ surface, .mcp.json, hooks,
252
+ brain pages).
253
+ - Every mutation = engine plan → show the user exactly what will be written, merged,
254
+ and left untouched → explicit user approval → engine apply. No approval, no apply.
255
+ - Never print API keys or secrets. Credentials flow via env vars, never argv
256
+ (the engine's --json output masks key values as \`[redacted]\`).
257
+ - Re-runs are repairs, not errors. This repo currently declares **${i.alphaName} of
258
+ ${i.name}** — if that's still right, a re-run just repairs/updates the surface.
259
+
260
+ ## 1 · Inspect
261
+
262
+ \`${ENGINE_NPX} inspect --json\`
263
+
264
+ Classify out loud: state (fresh / partial / current / legacy / drifted) ×
265
+ mode (connected / standalone) × tier. This decides everything downstream.
266
+
267
+ ## 2 · What they already have
268
+
269
+ The inspect output inventories existing agents, skills, commands, and hooks.
270
+ TELL the user what's already here — and that the clan will USE those rather than
271
+ duplicate them (they're recorded in \`.clan/inventory.json\` for the Alpha).
272
+
273
+ ## 3 · Identity — a first-class moment
274
+
275
+ Never silently derive the Alpha's name from the folder. Suggest a name with ONE
276
+ line of reasoning ("I'd suggest **Relay** — this project relays CRM events"),
277
+ then ask. Greenfield: interview first — "what is this clan for?" — the answer
278
+ becomes the description and, later, the brain's first pages.
279
+
280
+ ## 4 · Plan
281
+
282
+ \`${ENGINE_NPX} plan --json --alpha <Alpha> --name <Clan>\`
283
+
284
+ Present it in plain language: what will be CREATED, what will be MERGED (their
285
+ existing .mcp.json servers and settings.json hooks survive), what gets a BACKUP,
286
+ and what stays UNTOUCHED.
287
+
288
+ ## 5 · Apply — only on explicit OK
289
+
290
+ Only after the user explicitly approves the plan:
291
+
292
+ \`${ENGINE_NPX} apply --alpha <Alpha> --name <Clan>\`
293
+
294
+ Uniform backups + an undo journal are recorded automatically.
295
+
296
+ ## 6 · Doctor
297
+
298
+ \`${ENGINE_NPX} doctor\`
299
+
300
+ Report honestly — a failing check is a finding to fix or explain, never to hide.
301
+
302
+ ## 7 · Brain (offer, don't force)
303
+
304
+ Offer: \`npx -y @nfinitmonkeys/clan brain build\` (dry-run by default). Present
305
+ the result — "built N pages, M held back as sensitive" — and get consent BEFORE
306
+ re-running with \`--write\`.
307
+
308
+ ## 8 · Registration
309
+
310
+ Three honest options — ask which fits:
311
+
312
+ 1. **Standalone** — \`npx -y @nfinitmonkeys/clan wild --yes --alpha <Alpha> --name <Clan> --description "<one line>"\`
313
+ (the developer IS the council). ALWAYS pass \`--yes\` with the identity from
314
+ steps 3-4 — bare \`wild\` starts a TTY interview that dies silently (exit 0,
315
+ nothing written) in your non-TTY shell.
316
+ 2. **Attach** — provisioned credentials in env (\`ALPHA_ID\` + \`JUNGLE_API_KEY\`),
317
+ then \`npx -y @nfinitmonkeys/clan attach\`. Never paste credentials into argv or chat.
318
+ 3. **Not provisioned yet** — point them at their operator / Human Council.
319
+
320
+ After either flow, confirm \`.jungle/config.yaml\` exists — that file IS the
321
+ registration; without it the run failed regardless of exit code. Re-runs of any
322
+ of these are repairs, not errors.
323
+
324
+ ## 9 · Epilogue
325
+
326
+ Recap exactly what was written (from the apply output), where the backups are,
327
+ and how to reverse it: \`${ENGINE_NPX} undo\`.`);
328
+ }
329
+ function clanPack(i) {
179
330
  const t = i.tier;
180
331
  const files = [
181
332
  cmd(t, 'status', `Show this ${t}'s live status — identity, policies, inbox, telemetry`, `Run the clan status command and present it as a clean dashboard. If MCP is wired, you may also call \`mcp__jungle-local__jungle_whoami\` and \`mcp__jungle-local__jungle_get_self\`.
@@ -218,10 +369,83 @@ Probes come from \`.jungle/config.yaml\` under \`health:\`. Summarize each check
218
369
  }
219
370
  return files;
220
371
  }
372
+ /** District pack — the box's MCP tools (jungle-local → localhost:<boxPort>) are
373
+ * the substrate, never the clan CLI. All 5 verbs render unconditionally.
374
+ * Box-down recovery names the district CLI by its jungle-repo checkout path —
375
+ * the bare npm name is a stranger's squattable package. */
376
+ function districtPack(i) {
377
+ const t = i.tier;
378
+ const port = i.boxPort ?? '7787';
379
+ const db = '.district/db/district.db';
380
+ return [
381
+ cmd(t, 'status', `Show this district's live status — identity, member clans, inbox, work, discussion points`, `This repo is a **district** (${i.name} / Alpha ${i.alphaName}) — status comes from the district box's MCP tools (server \`jungle-local\` → localhost:${port}), NOT \`npx -y @nfinitmonkeys/clan\`.
382
+
383
+ ${header(i)}
384
+
385
+ Gather in parallel: \`jungle_whoami\`, \`jungle_get_children\` (member clans), \`jungle_check_inbox\` (unreadOnly), \`jungle_list_work\`, \`jungle_list_discussion_points\` — all via \`mcp__jungle-local__*\`.
386
+
387
+ Present one dashboard: identity header, member-clan roster with lastSeen, unread inbox, work by status, open discussion points.
388
+
389
+ If MCP calls fail with a connection error, the box is down — offer: \`DISTRICT_DATA_DIR=.district node <jungle-checkout>/packages/district/dist/cli.js start\` from this repo root (port ${port}; the district CLI lives in the jungle repo checkout — the bare npm name is a stranger's package; the env var is required — without it the CLI would create a fresh ./data dir and mint a DUPLICATE district identity).`),
390
+ cmd(t, 'inbox', `View recent messages in ${i.alphaName}'s district inbox`, `${header(i)}
391
+
392
+ Call \`mcp__jungle-local__jungle_check_inbox\` (unreadOnly first; if empty retry unreadOnly:false, limit 15). Show sender, subject, age. Threads via \`jungle_get_thread\`; mark read via \`jungle_mark_read\`; reply via \`jungle_send_message\` only after the user confirms content.`),
393
+ cmd(t, 'log', `Governance log — every recorded act on this district`, `${header(i)}
394
+
395
+ The box exposes no log tool yet — read the district's own sqlite directly (folder-rooted, ours):
396
+
397
+ \`\`\`bash
398
+ sqlite3 -header -column ${db} "SELECT ts, actor, action, target FROM governance_log ORDER BY ts DESC LIMIT 25;"
399
+ \`\`\`
400
+
401
+ Newest first, human timestamps. A short log is a young district, not an error.`),
402
+ cmd(t, 'policies', `This district's governance posture — honest about enforced vs. not yet built`, `${header(i)}
403
+
404
+ Be honest: **the district box has no policy engine yet** (Console P3 builds it). Show instead: (1) identity + posture from \`.district/district.config.yaml\` (never print apiKey lines); (2) the composing rule — *a parent sees a child only by the child's consent; every child runs without its parent*; (3) plainly: acts are ungated but logged (see /district:log).`),
405
+ cmd(t, 'request', `Raise a request or decision — discussion point, or escalate to the operating jungle`, `${header(i)}
406
+
407
+ Two real forms — ask which if unclear: (1) a decision for THIS district → \`mcp__jungle-local__jungle_create_discussion_point\` (title + context; lifecycle open→in_discussion→decided→resolved, +parked); (2) a request to the operating jungle → raise from the jungle repo as its Alpha, or record a DP here tagged for escalation. Never fabricate an approval flow that doesn't exist.`),
408
+ ];
409
+ }
410
+ /** Jungle pack — the org hub. Registry-wide status via this repo's declared
411
+ * identity; all 5 verbs render unconditionally. */
412
+ function junglePack(i) {
413
+ const t = i.tier;
414
+ return [
415
+ cmd(t, 'status', `Show this jungle's live status — identity, council, districts + clans, inbox`, `This repo is a **jungle** (${i.name} / Alpha ${i.alphaName}) — the org hub. Status via \`mcp__jungle-local__*\` (this repo's declared identity).
416
+
417
+ ${header(i)}
418
+
419
+ Gather in parallel: \`jungle_whoami\`, \`jungle_list_clans\` (the registry — group by geoTier: districts vs direct clans), \`jungle_check_inbox\` (unreadOnly), \`jungle_list_approvals\` (pending only).
420
+
421
+ Dashboard: identity header, council roster (permission-tier council members), districts with child counts, direct clans, unread inbox, pending approvals count.`),
422
+ cmd(t, 'inbox', `View recent messages in ${i.alphaName}'s inbox`, `${header(i)}
423
+
424
+ \`mcp__jungle-local__jungle_check_inbox\` (unreadOnly first, then all, limit 15). Sender, subject, age; threads via \`jungle_get_thread\`; reply via \`jungle_send_message\` only after the user confirms.`),
425
+ cmd(t, 'log', `Governance log for this jungle`, `${header(i)}
426
+
427
+ Use \`npx -y @nfinitmonkeys/clan status --json | jq '.self.governanceLog // [] | .[0:15]'\` (this repo is attached via .jungle/config.yaml). Timeline, newest first: who/what/when/why.`),
428
+ cmd(t, 'policies', `This jungle's policies + the governance invariant`, `${header(i)}
429
+
430
+ \`\`\`bash
431
+ npx -y @nfinitmonkeys/clan policies
432
+ \`\`\`
433
+
434
+ Explain the buckets, and the invariant: Alphas propose, humans approve — every sensitive act routes through the approval queue (BOSS).`),
435
+ cmd(t, 'request', `Propose a policy or governance change for this jungle`, `${header(i)}
436
+
437
+ Compose the change (actionType, bucket, rationale), confirm with the user, then send via \`npx -y @nfinitmonkeys/clan talk "..."\` or, for cross-clan governance acts, note that the human-council voice (\`mcp__jungle__*\`) is required and must be explicitly requested.`),
438
+ ];
439
+ }
221
440
  // ─── brain scaffold (canonical layout — <brainRoot>/wiki + .brain-meta.json) ─
222
441
  /** Where the brain lives, per tier (matches the fleet's clan-ready layout so
223
- * the engine never puts .brain/ where a district/jungle expects its vault). */
442
+ * the engine never puts .brain/ where a district/jungle expects its vault).
443
+ * An EXISTING vault (i.brainRoot, set from inspect) wins over the tier
444
+ * default — applying to a repo with a vault elsewhere must never mint a
445
+ * duplicate. */
224
446
  export function brainRoot(i) {
447
+ if (i.brainRoot)
448
+ return i.brainRoot;
225
449
  if (i.tier === 'district')
226
450
  return '.district/brain';
227
451
  if (i.tier === 'jungle')