@nfinitmonkeys/clan-engine 0.2.0 → 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/README.md +62 -10
- package/dist/apply.js +7 -1
- package/dist/brain-build.d.ts +23 -7
- package/dist/brain-build.js +48 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +29 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/inspect.js +102 -5
- package/dist/inventory.d.ts +3 -1
- package/dist/inventory.js +4 -2
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.js +11 -0
- package/dist/reconcile.js +24 -4
- package/dist/templates.d.ts +4 -1
- package/dist/templates.js +244 -20
- package/dist/types.d.ts +24 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,18 +9,27 @@ tooling all render from here, so their output can never drift apart again.
|
|
|
9
9
|
`.mcp.json` instead of clobbering, backs up every file it touches, and journals
|
|
10
10
|
every run so it can be undone exactly.
|
|
11
11
|
|
|
12
|
+
## Tiers
|
|
13
|
+
|
|
14
|
+
As of 0.3.0 the engine is authoritative for **clan, district, and jungle**
|
|
15
|
+
tiers — one template source renders the right `/clan:*`, `/district:*`, or
|
|
16
|
+
`/jungle:*` pack from the repo's declared `<!-- geo-tier: X -->`. **Biome is
|
|
17
|
+
unbuilt**: asking for a biome pack throws (`no pack template for tier biome
|
|
18
|
+
yet`) rather than emitting something half-real.
|
|
19
|
+
|
|
12
20
|
## Verbs
|
|
13
21
|
|
|
14
22
|
```
|
|
15
|
-
clan-engine inspect
|
|
16
|
-
clan-engine plan
|
|
17
|
-
clan-engine apply
|
|
18
|
-
clan-engine
|
|
19
|
-
clan-engine
|
|
23
|
+
clan-engine inspect [--repo .] [--json] # map a repo's state (never mutates)
|
|
24
|
+
clan-engine plan [--repo .] --alpha X --name Y [--tier clan] [--json]
|
|
25
|
+
clan-engine apply [--repo .] --alpha X --name Y # write it (backups + journal)
|
|
26
|
+
clan-engine brain-build [--repo .] [--write] [--memory] [--json] # mine the brain
|
|
27
|
+
clan-engine undo [--repo .] [--journal <path>] # reverse the last apply
|
|
28
|
+
clan-engine doctor [--repo .] [--json] # validate the whole loop
|
|
20
29
|
```
|
|
21
30
|
|
|
22
31
|
`plan` is a dry run; `apply` writes. Both are idempotent — a second `apply`
|
|
23
|
-
is all skips.
|
|
32
|
+
is all skips. `--json` output masks live key values as `[redacted]` everywhere.
|
|
24
33
|
|
|
25
34
|
## What it authors
|
|
26
35
|
|
|
@@ -28,7 +37,13 @@ is all skips.
|
|
|
28
37
|
never rewritten; only the `<!-- geo-tier: X -->` marker is ensured).
|
|
29
38
|
- **`.claude/skills/clan-awareness/SKILL.md`** — the awareness skill in the
|
|
30
39
|
directory format Claude Code actually loads (a flat file never loads).
|
|
31
|
-
- **`.claude/commands/<tier>/*.md`** — tier-named slash-command pack
|
|
40
|
+
- **`.claude/commands/<tier>/*.md`** — tier-named slash-command pack, including
|
|
41
|
+
**`/<tier>:up`** — the wizard door. Clan tier gets the full conversational
|
|
42
|
+
setup/convert/repair playbook (Claude Code improvises the conversation; the
|
|
43
|
+
engine does every write); district/jungle get a short repair wizard (their
|
|
44
|
+
identity is provisioned by node-up, never interviewed). Generated wizard text
|
|
45
|
+
**version-pins** the engine (`npx -y @nfinitmonkeys/clan-engine@0.3`) so an
|
|
46
|
+
older published engine can never overwrite the pack that invoked it.
|
|
32
47
|
- **`.mcp.json`** — a correctly-named `jungle-local` server **merged** alongside
|
|
33
48
|
any servers you already have, and gitignored (it holds a live key). Never the
|
|
34
49
|
bare name `jungle` (it collides with the global council server).
|
|
@@ -39,6 +54,39 @@ is all skips.
|
|
|
39
54
|
- **`.clan/inventory.json`** — the user's EXISTING agents/skills/commands/hooks,
|
|
40
55
|
so the Alpha works *with* them instead of duplicating.
|
|
41
56
|
|
|
57
|
+
## brain-build
|
|
58
|
+
|
|
59
|
+
Mines README, `docs/` (recursive), CLAUDE.md, package manifests, code structure,
|
|
60
|
+
the inventory, and (`--memory`) Claude Code session memory into brain pages.
|
|
61
|
+
Every source passes `scanSensitive` first — secrets, infra identifiers, and PHI
|
|
62
|
+
are **held back and reported, never written**. Dry-run by default; `--write` to
|
|
63
|
+
save; create-only (your accumulated pages are never touched).
|
|
64
|
+
|
|
65
|
+
**LLM curation is API-only.** The engine itself has no LLM and the CLI has no
|
|
66
|
+
`--curate` flag. Callers pass a hook:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const res = await brainBuild(repo, identity, { curate: async ({ pages, identity, sourceNotes }) => curatedPages });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The caller brings the LLM (this is what `clan brain build --curate` does with
|
|
73
|
+
the clan's configured provider); the engine re-gates **every** curated page
|
|
74
|
+
through `scanSensitive` + create-only, and falls back to the mechanical pages
|
|
75
|
+
with a note if the curator throws. Sanitization is not optional at any layer.
|
|
76
|
+
|
|
77
|
+
## Existing-vault detection
|
|
78
|
+
|
|
79
|
+
`inspect` finds the repo's REAL vault so setup and brain-build never scaffold a
|
|
80
|
+
tier-default duplicate nothing loads. Precedence:
|
|
81
|
+
|
|
82
|
+
1. the vault a live `load-brain` SessionStart hook already hydrates — wins
|
|
83
|
+
outright (the hook is ground truth for what sessions actually load);
|
|
84
|
+
2. vaults containing `wiki/identity.md`, in order `.district/brain` >
|
|
85
|
+
tier-default root > `.brain` > `.brains/<alpha>` > other `.brains/*`
|
|
86
|
+
(preferring `obsidian`) — a real identity beats debris;
|
|
87
|
+
3. bare-existing vaults in the same order;
|
|
88
|
+
4. none → the tier default applies.
|
|
89
|
+
|
|
42
90
|
## Reconciliation
|
|
43
91
|
|
|
44
92
|
Every generated file carries a `clan-engine@<v>` + `layout: <n>` marker. That's
|
|
@@ -46,16 +94,20 @@ how the engine tells its own output (safe to update) from a file you wrote at
|
|
|
46
94
|
the same path (backed up, then replaced). Brain pages are create-only — your
|
|
47
95
|
accumulated memory is never clobbered.
|
|
48
96
|
|
|
97
|
+
`ENGINE_VERSION` (in `src/types.ts`) always equals the package version — the
|
|
98
|
+
test suite fails on drift. `LAYOUT_VERSION` bumps only when generated output
|
|
99
|
+
changes shape (that's what triggers migrations); `ENGINE_VERSION` bumps freely.
|
|
100
|
+
|
|
49
101
|
## Programmatic API
|
|
50
102
|
|
|
51
103
|
```js
|
|
52
|
-
import { inspect, reconcile, apply, undo, doctor, setup } from '@nfinitmonkeys/clan-engine';
|
|
104
|
+
import { inspect, reconcile, apply, undo, doctor, setup, brainBuild } from '@nfinitmonkeys/clan-engine';
|
|
53
105
|
|
|
54
106
|
const { plan, result } = setup(repo, identity, { at, today }); // inspect → reconcile → apply
|
|
55
107
|
```
|
|
56
108
|
|
|
57
|
-
The conversational setup wizard drives these verbs — the LLM
|
|
58
|
-
conversation, the engine does every write.
|
|
109
|
+
The conversational setup wizard (`/clan:up`) drives these verbs — the LLM
|
|
110
|
+
improvises the conversation, the engine does every write.
|
|
59
111
|
|
|
60
112
|
## License
|
|
61
113
|
|
package/dist/apply.js
CHANGED
|
@@ -74,9 +74,15 @@ export function apply(plan, opts) {
|
|
|
74
74
|
}
|
|
75
75
|
let journalPath;
|
|
76
76
|
if (!opts.noJournal && ops.length > 0) {
|
|
77
|
+
// NEVER journal a live key. The journal lives under .claude/ (which many
|
|
78
|
+
// repos commit); identity here is undo metadata, not a credential store —
|
|
79
|
+
// undo only reads ops. The key itself stays in .jungle/config.yaml.
|
|
80
|
+
const journalIdentity = plan.identity.jungle
|
|
81
|
+
? { ...plan.identity, jungle: { ...plan.identity.jungle, apiKey: '[redacted]' } }
|
|
82
|
+
: plan.identity;
|
|
77
83
|
const journal = {
|
|
78
84
|
engineVersion: ENGINE_VERSION, layoutVersion: LAYOUT_VERSION,
|
|
79
|
-
at: runId, repo, identity:
|
|
85
|
+
at: runId, repo, identity: journalIdentity, ops,
|
|
80
86
|
};
|
|
81
87
|
const jRel = join(runDir, `journal-${runId}.json`);
|
|
82
88
|
const jAbs = join(repo, jRel);
|
package/dist/brain-build.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* HELD BACK, never written, and reported for the operator to review. Pages are
|
|
10
10
|
* create-only — accumulated brain content is never clobbered.
|
|
11
11
|
*
|
|
12
|
-
* Mechanical (no LLM required) — the right no-key base layer. LLM curation
|
|
13
|
-
*
|
|
12
|
+
* Mechanical (no LLM required) — the right no-key base layer. LLM curation is
|
|
13
|
+
* an opt-in HOOK (opts.curate): the caller brings the LLM, the engine stays
|
|
14
|
+
* zero-LLM and re-gates everything the curator returns.
|
|
14
15
|
*/
|
|
15
|
-
import type { ClanIdentity, GeneratedFile } from './types.js';
|
|
16
|
+
import type { ClanIdentity, GeneratedFile, BrainCurator } from './types.js';
|
|
16
17
|
export interface HeldBack {
|
|
17
18
|
from: string;
|
|
18
19
|
reasons: string[];
|
|
@@ -21,13 +22,28 @@ export interface BrainBuildResult {
|
|
|
21
22
|
pages: GeneratedFile[];
|
|
22
23
|
heldBack: HeldBack[];
|
|
23
24
|
scanned: number;
|
|
25
|
+
/** Operator-facing report lines (e.g. curation fallback) — never secrets. */
|
|
26
|
+
notes: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface BrainBuildOpts {
|
|
29
|
+
home?: string;
|
|
30
|
+
hipaa?: boolean;
|
|
31
|
+
includeMemory?: boolean;
|
|
32
|
+
/** LLM curation hook — API-only (the engine CLI has no LLM to drive one).
|
|
33
|
+
* Curated output replaces the candidates but every returned page re-passes
|
|
34
|
+
* scanSensitive + create-only; a throwing curator falls back to the
|
|
35
|
+
* mechanical pages with a note in the report. */
|
|
36
|
+
curate?: BrainCurator;
|
|
24
37
|
}
|
|
25
38
|
/**
|
|
26
39
|
* Mine existing files into brain pages. Every candidate is scanned; flagged
|
|
27
40
|
* ones are held back. Returns create-candidate pages + the held-back report.
|
|
41
|
+
* Without opts.curate the call stays synchronous (existing callers unchanged);
|
|
42
|
+
* with a curator it returns a Promise of the curated (re-gated) result.
|
|
28
43
|
*/
|
|
29
|
-
export declare function brainBuild(repo: string, identity: ClanIdentity, opts?: {
|
|
30
|
-
|
|
31
|
-
hipaa?: boolean;
|
|
32
|
-
includeMemory?: boolean;
|
|
44
|
+
export declare function brainBuild(repo: string, identity: ClanIdentity, opts?: BrainBuildOpts & {
|
|
45
|
+
curate?: undefined;
|
|
33
46
|
}): BrainBuildResult;
|
|
47
|
+
export declare function brainBuild(repo: string, identity: ClanIdentity, opts: BrainBuildOpts & {
|
|
48
|
+
curate: BrainCurator;
|
|
49
|
+
}): Promise<BrainBuildResult>;
|
package/dist/brain-build.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* HELD BACK, never written, and reported for the operator to review. Pages are
|
|
10
10
|
* create-only — accumulated brain content is never clobbered.
|
|
11
11
|
*
|
|
12
|
-
* Mechanical (no LLM required) — the right no-key base layer. LLM curation
|
|
13
|
-
*
|
|
12
|
+
* Mechanical (no LLM required) — the right no-key base layer. LLM curation is
|
|
13
|
+
* an opt-in HOOK (opts.curate): the caller brings the LLM, the engine stays
|
|
14
|
+
* zero-LLM and re-gates everything the curator returns.
|
|
14
15
|
*/
|
|
15
16
|
import { readFileSync, existsSync, readdirSync, lstatSync } from 'fs';
|
|
16
17
|
import { join } from 'path';
|
|
@@ -188,10 +189,6 @@ function mineMemory(repo, home) {
|
|
|
188
189
|
}
|
|
189
190
|
return out;
|
|
190
191
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Mine existing files into brain pages. Every candidate is scanned; flagged
|
|
193
|
-
* ones are held back. Returns create-candidate pages + the held-back report.
|
|
194
|
-
*/
|
|
195
192
|
export function brainBuild(repo, identity, opts = {}) {
|
|
196
193
|
const home = opts.home ?? homedir();
|
|
197
194
|
const candidates = [
|
|
@@ -206,6 +203,7 @@ export function brainBuild(repo, identity, opts = {}) {
|
|
|
206
203
|
const root = brainRoot(identity);
|
|
207
204
|
const pages = [];
|
|
208
205
|
const heldBack = [];
|
|
206
|
+
const sourceNotes = [];
|
|
209
207
|
const seen = new Set();
|
|
210
208
|
for (const c of candidates) {
|
|
211
209
|
// Scan the body AND the derived title AND the source path — a secret/PHI in
|
|
@@ -232,6 +230,49 @@ export function brainBuild(repo, identity, opts = {}) {
|
|
|
232
230
|
managed: true,
|
|
233
231
|
content: `---\nname: ${JSON.stringify(title)}\ndescription: ${JSON.stringify('mined from ' + c.from)}\ntype: ${JSON.stringify(c.type)}\nsource: ${JSON.stringify(c.from)}\n${markerLines()}\n---\n\n# ${title}\n\n${c.body}\n`,
|
|
234
232
|
});
|
|
233
|
+
sourceNotes.push(`${path} ← ${c.from}`);
|
|
234
|
+
}
|
|
235
|
+
const mech = { pages, heldBack, scanned: candidates.length, notes: [] };
|
|
236
|
+
if (!opts.curate)
|
|
237
|
+
return mech;
|
|
238
|
+
return runCurate(repo, identity, opts, mech, sourceNotes);
|
|
239
|
+
}
|
|
240
|
+
/** Apply the caller's curation hook, then re-gate its output. The curated set
|
|
241
|
+
* REPLACES the mechanical pages, but the invariants survive curation:
|
|
242
|
+
* scanSensitive on every body+path, create-only, and vault-root confinement
|
|
243
|
+
* (an LLM cannot aim a write at .claude/, .mcp.json, or outside the brain). */
|
|
244
|
+
async function runCurate(repo, identity, opts, mech, sourceNotes) {
|
|
245
|
+
let curated;
|
|
246
|
+
try {
|
|
247
|
+
curated = await opts.curate({ pages: mech.pages, identity, sourceNotes });
|
|
248
|
+
if (!Array.isArray(curated))
|
|
249
|
+
throw new Error('curator returned non-array');
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return { ...mech, notes: [...mech.notes, `curation failed: ${e.message}; mechanical pages used`] };
|
|
253
|
+
}
|
|
254
|
+
const root = brainRoot(identity);
|
|
255
|
+
const pages = [];
|
|
256
|
+
const heldBack = [...mech.heldBack];
|
|
257
|
+
const notes = [...mech.notes];
|
|
258
|
+
let outside = 0;
|
|
259
|
+
for (const p of curated) {
|
|
260
|
+
if (typeof p?.path !== 'string' || typeof p?.content !== 'string')
|
|
261
|
+
continue;
|
|
262
|
+
if (p.path.includes('..') || !p.path.startsWith(root + '/')) {
|
|
263
|
+
outside++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const scan = scanSensitive(`${p.content}\n${p.path}`, { hipaa: opts.hipaa });
|
|
267
|
+
if (scan.flagged) {
|
|
268
|
+
heldBack.push({ from: `curated:${p.path}`, reasons: scan.reasons });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (existsSync(join(repo, p.path)))
|
|
272
|
+
continue;
|
|
273
|
+
pages.push({ path: p.path, content: p.content, managed: true });
|
|
235
274
|
}
|
|
236
|
-
|
|
275
|
+
if (outside)
|
|
276
|
+
notes.push(`curation dropped ${outside} page(s) targeting paths outside ${root}/`);
|
|
277
|
+
return { pages, heldBack, scanned: mech.scanned, notes };
|
|
237
278
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* clan-engine inspect [--repo .] [--json]
|
|
8
8
|
* clan-engine plan [--repo .] [--tier clan] [--alpha X] [--name Y] [--json]
|
|
9
|
-
* clan-engine apply [--repo .] [--alpha X] [--name Y]
|
|
9
|
+
* clan-engine apply [--repo .] [--alpha X] [--name Y]
|
|
10
10
|
* clan-engine undo [--repo .] [--journal <path>]
|
|
11
11
|
* clan-engine doctor [--repo .] [--json]
|
|
12
12
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* clan-engine inspect [--repo .] [--json]
|
|
8
8
|
* clan-engine plan [--repo .] [--tier clan] [--alpha X] [--name Y] [--json]
|
|
9
|
-
* clan-engine apply [--repo .] [--alpha X] [--name Y]
|
|
9
|
+
* clan-engine apply [--repo .] [--alpha X] [--name Y]
|
|
10
10
|
* clan-engine undo [--repo .] [--journal <path>]
|
|
11
11
|
* clan-engine doctor [--repo .] [--json]
|
|
12
12
|
*/
|
|
@@ -63,9 +63,25 @@ function resolveIdentity(ins, flags) {
|
|
|
63
63
|
description: flags['description'] ?? base?.description ?? '',
|
|
64
64
|
alphaId: flags['alpha-id'] ?? base?.alphaId ?? jungle?.alphaId,
|
|
65
65
|
mode: jungle ? 'connected' : 'standalone',
|
|
66
|
+
boxPort: flags['box-port'] ?? base?.boxPort,
|
|
67
|
+
// Existing-vault detection must survive into every verb — without this,
|
|
68
|
+
// brain-build would target the tier-default vault on multi-root repos
|
|
69
|
+
// (reconcile has its own safety net; brainBuild does not).
|
|
70
|
+
brainRoot: base?.brainRoot ?? ins.brainRoot,
|
|
66
71
|
jungle,
|
|
67
72
|
};
|
|
68
73
|
}
|
|
74
|
+
/** JSON output with secret VALUES masked. The /tier:up wizard pipes --json
|
|
75
|
+
* straight into an LLM conversation, so a live jungle apiKey must never
|
|
76
|
+
* appear — neither in identity.jungle nor embedded in a planned .mcp.json
|
|
77
|
+
* merge content. */
|
|
78
|
+
function redactedJson(value, secrets) {
|
|
79
|
+
let out = JSON.stringify(value, null, 2);
|
|
80
|
+
for (const s of secrets)
|
|
81
|
+
if (s)
|
|
82
|
+
out = out.split(s).join('[redacted]');
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
69
85
|
function printPlan(plan) {
|
|
70
86
|
const glyph = { create: '+', update: '~', 'backup-replace': '!', merge: '~', append: '»', remove: '-', skip: '·' };
|
|
71
87
|
const acting = plan.actions.filter(a => a.kind !== 'skip');
|
|
@@ -84,7 +100,7 @@ async function main() {
|
|
|
84
100
|
case 'inspect': {
|
|
85
101
|
const ins = inspect(repo, { tierFlag: flags['tier'] });
|
|
86
102
|
if (json) {
|
|
87
|
-
console.log(
|
|
103
|
+
console.log(redactedJson(ins, [ins.identity?.jungle?.apiKey]));
|
|
88
104
|
break;
|
|
89
105
|
}
|
|
90
106
|
console.log(`\nclan-engine inspect — ${repo}`);
|
|
@@ -106,7 +122,7 @@ async function main() {
|
|
|
106
122
|
const plan = reconcile(identity, ins, { jungle: identity.jungle, today: today() });
|
|
107
123
|
if (cmd === 'plan') {
|
|
108
124
|
if (json)
|
|
109
|
-
console.log(
|
|
125
|
+
console.log(redactedJson(plan, [identity.jungle?.apiKey]));
|
|
110
126
|
else {
|
|
111
127
|
printPlan(plan);
|
|
112
128
|
console.log(`\n(dry-run — run \`clan-engine apply\` to write)\n`);
|
|
@@ -118,7 +134,7 @@ async function main() {
|
|
|
118
134
|
printPlan(plan);
|
|
119
135
|
const res = apply(plan, { at: stamp() });
|
|
120
136
|
if (json)
|
|
121
|
-
console.log(
|
|
137
|
+
console.log(redactedJson(res, [identity.jungle?.apiKey]));
|
|
122
138
|
else {
|
|
123
139
|
console.log(`\n✓ applied ${res.applied.length} action(s).`);
|
|
124
140
|
if (res.backupDir)
|
|
@@ -130,9 +146,12 @@ async function main() {
|
|
|
130
146
|
break;
|
|
131
147
|
}
|
|
132
148
|
case 'brain-build': {
|
|
149
|
+
// LLM curation is API-only: brainBuild({ curate }) from a caller that has
|
|
150
|
+
// an LLM (e.g. @nfinitmonkeys/clan `brain build --curate`). This CLI has
|
|
151
|
+
// no LLM to drive one, so it deliberately grows no --curate flag.
|
|
133
152
|
const ins = inspect(repo, { tierFlag: flags['tier'] });
|
|
134
153
|
const identity = resolveIdentity(ins, flags);
|
|
135
|
-
const { pages, heldBack, scanned } = brainBuild(repo, identity, {
|
|
154
|
+
const { pages, heldBack, scanned, notes } = brainBuild(repo, identity, {
|
|
136
155
|
includeMemory: flags['memory'] === 'true',
|
|
137
156
|
// PHI scanning is ON by default; --no-phi opts out.
|
|
138
157
|
hipaa: flags['no-phi'] === 'true' ? false : undefined,
|
|
@@ -146,7 +165,7 @@ async function main() {
|
|
|
146
165
|
// session memory), so writing is opt-in via --write.
|
|
147
166
|
const doWrite = flags['write'] === 'true' || flags['apply'] === 'true';
|
|
148
167
|
if (json) {
|
|
149
|
-
console.log(JSON.stringify({ scanned, built: toCreate.length, heldBack, wrote: doWrite }, null, 2));
|
|
168
|
+
console.log(JSON.stringify({ scanned, built: toCreate.length, heldBack, notes, wrote: doWrite }, null, 2));
|
|
150
169
|
}
|
|
151
170
|
else {
|
|
152
171
|
console.log(`\nclan-engine brain-build — scanned ${scanned} source(s)`);
|
|
@@ -160,6 +179,8 @@ async function main() {
|
|
|
160
179
|
for (const h of heldBack)
|
|
161
180
|
console.log(` · ${h.from} — ${h.reasons.join(', ')}`);
|
|
162
181
|
}
|
|
182
|
+
for (const n of notes)
|
|
183
|
+
console.log(` ⚠ ${n}`);
|
|
163
184
|
}
|
|
164
185
|
if (doWrite && toCreate.length) {
|
|
165
186
|
const res = apply(plan, { at: stamp() });
|
|
@@ -211,7 +232,8 @@ async function main() {
|
|
|
211
232
|
apply write it (uniform backups + a journal for undo)
|
|
212
233
|
brain-build mine existing files (README/docs/CLAUDE.md/code/inventory,
|
|
213
234
|
+--memory) into brain pages; sensitive content held back.
|
|
214
|
-
dry-run by default; --write to save
|
|
235
|
+
dry-run by default; --write to save. LLM curation is API-only
|
|
236
|
+
(brainBuild({curate}) — this CLI has no LLM, so no --curate flag)
|
|
215
237
|
undo reverse the last apply (--journal <path> to pick one)
|
|
216
238
|
doctor validate the whole loop (config, mcp name, brain, hook, creds)
|
|
217
239
|
`);
|
package/dist/index.d.ts
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
* from here, so their output can never drift again. The conversational wizard
|
|
7
7
|
* drives these same verbs; the LLM improvises the conversation, never the writes.
|
|
8
8
|
*/
|
|
9
|
-
export { ENGINE_VERSION, LAYOUT_VERSION, GENERATED_BY, type Tier, type Mode, type ClanIdentity, type GeneratedFile, type PlanAction, type Plan, type InspectResult, type InventoryItem, type NodeState, type McpServerInfo, type Journal, type JournalOp, type ApplyResult, type DoctorCheck, } from './types.js';
|
|
9
|
+
export { ENGINE_VERSION, LAYOUT_VERSION, GENERATED_BY, type Tier, type Mode, type ClanIdentity, type GeneratedFile, type PlanAction, type Plan, type InspectResult, type InventoryItem, type NodeState, type McpServerInfo, type Journal, type JournalOp, type ApplyResult, type DoctorCheck, type BrainCurateInput, type BrainCurator, } from './types.js';
|
|
10
10
|
export { inspect } from './inspect.js';
|
|
11
11
|
export { reconcile, type ReconcileOptions } from './reconcile.js';
|
|
12
12
|
export { apply, type ApplyOptions } from './apply.js';
|
|
13
13
|
export { undo, listJournals, type UndoResult } from './undo.js';
|
|
14
14
|
export { doctor } from './doctor.js';
|
|
15
15
|
export { scanInventory, writeInventoryManifest } from './inventory.js';
|
|
16
|
-
export { brainBuild, type BrainBuildResult, type HeldBack } from './brain-build.js';
|
|
16
|
+
export { brainBuild, type BrainBuildResult, type BrainBuildOpts, type HeldBack } from './brain-build.js';
|
|
17
17
|
export { scanSensitive, type ScanResult } from './sanitize.js';
|
|
18
|
-
export { planMcpJson, jungleLocalServer, type JungleCreds } from './mcp.js';
|
|
18
|
+
export { planMcpJson, planDistrictMcpJson, jungleLocalServer, districtBoxCreds, type JungleCreds } from './mcp.js';
|
|
19
19
|
export { planBrainHook } from './hooks.js';
|
|
20
20
|
export { claudeSurface, brainScaffold, claudeMdIdentity, loadBrainScript, geoTierMarker, hasManagedMarker, findGeoTierMarker, markerLines, hookCommand, brainRoot, } from './templates.js';
|
|
21
21
|
import type { ClanIdentity, Plan, ApplyResult } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { doctor } from './doctor.js';
|
|
|
15
15
|
export { scanInventory, writeInventoryManifest } from './inventory.js';
|
|
16
16
|
export { brainBuild } from './brain-build.js';
|
|
17
17
|
export { scanSensitive } from './sanitize.js';
|
|
18
|
-
export { planMcpJson, jungleLocalServer } from './mcp.js';
|
|
18
|
+
export { planMcpJson, planDistrictMcpJson, jungleLocalServer, districtBoxCreds } from './mcp.js';
|
|
19
19
|
export { planBrainHook } from './hooks.js';
|
|
20
20
|
export { claudeSurface, brainScaffold, claudeMdIdentity, loadBrainScript, geoTierMarker, hasManagedMarker, findGeoTierMarker, markerLines, hookCommand, brainRoot, } from './templates.js';
|
|
21
21
|
/**
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/inventory.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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)
|
package/dist/templates.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
104
|
-
? `##
|
|
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
|
-
|
|
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})
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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')
|
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* file's marker so a future engine can tell "my old output" from "my current
|
|
7
7
|
* output" and run migrations. Bump LAYOUT_VERSION when the template SHAPE
|
|
8
8
|
* changes (a path moves, a format changes); bump ENGINE_VERSION freely. */
|
|
9
|
-
export declare const ENGINE_VERSION = "0.
|
|
9
|
+
export declare const ENGINE_VERSION = "0.3.0";
|
|
10
10
|
export declare const LAYOUT_VERSION = 1;
|
|
11
11
|
/** The marker key written into generated-file frontmatter / headers. */
|
|
12
12
|
export declare const GENERATED_BY = "clan-engine";
|
|
@@ -22,6 +22,12 @@ export interface ClanIdentity {
|
|
|
22
22
|
description: string;
|
|
23
23
|
tier: Tier;
|
|
24
24
|
mode: Mode;
|
|
25
|
+
/** district box HTTP port (from .district/district.config.yaml http.port). */
|
|
26
|
+
boxPort?: string;
|
|
27
|
+
/** EXISTING vault root (repo-relative) when one was detected at a non-default
|
|
28
|
+
* location — overrides the tier default so applying never mints a duplicate
|
|
29
|
+
* vault (e.g. the jungle repo's own .brain/ despite tier jungle). */
|
|
30
|
+
brainRoot?: string;
|
|
25
31
|
/** connected-mode credentials (drive .mcp.json). */
|
|
26
32
|
jungle?: {
|
|
27
33
|
apiUrl: string;
|
|
@@ -40,6 +46,19 @@ export interface GeneratedFile {
|
|
|
40
46
|
/** POSIX chmod to apply after write (e.g. 0o755 for scripts). */
|
|
41
47
|
chmod?: number;
|
|
42
48
|
}
|
|
49
|
+
/** Input handed to a brain-build curation hook. The engine stays zero-LLM —
|
|
50
|
+
* the hook is supplied by a caller that has one (e.g. @nfinitmonkeys/clan). */
|
|
51
|
+
export interface BrainCurateInput {
|
|
52
|
+
/** The mechanical candidate pages (already sanitized + create-only filtered). */
|
|
53
|
+
pages: GeneratedFile[];
|
|
54
|
+
identity: ClanIdentity;
|
|
55
|
+
/** Provenance, one line per candidate: "<page path> ← <source file>". */
|
|
56
|
+
sourceNotes: string[];
|
|
57
|
+
}
|
|
58
|
+
/** Returns the curated page set (replaces/augments the candidates). The engine
|
|
59
|
+
* re-gates EVERY curated body through scanSensitive + create-only regardless —
|
|
60
|
+
* an LLM echoing a secret from context is held back like a mined one. */
|
|
61
|
+
export type BrainCurator = (input: BrainCurateInput) => Promise<GeneratedFile[]>;
|
|
43
62
|
export type ActionKind = 'create' | 'update' | 'backup-replace' | 'merge' | 'append' | 'remove' | 'skip';
|
|
44
63
|
export interface PlanAction {
|
|
45
64
|
kind: ActionKind;
|
|
@@ -89,6 +108,10 @@ export interface InspectResult {
|
|
|
89
108
|
/** brain vault present + its layout version (from .brain-meta.json). */
|
|
90
109
|
hasBrain: boolean;
|
|
91
110
|
brainLayoutVersion?: number;
|
|
111
|
+
/** where an EXISTING vault lives (repo-relative), checked across every known
|
|
112
|
+
* root (.district/brain > .brain > .brains/*) — reconcile scaffolds THERE,
|
|
113
|
+
* never a tier-default duplicate. Unset when no vault exists yet. */
|
|
114
|
+
brainRoot?: string;
|
|
92
115
|
/** the reconciled state classification. */
|
|
93
116
|
state: NodeState;
|
|
94
117
|
/** actionable notes for the wizard / caller. */
|
package/dist/types.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* file's marker so a future engine can tell "my old output" from "my current
|
|
7
7
|
* output" and run migrations. Bump LAYOUT_VERSION when the template SHAPE
|
|
8
8
|
* changes (a path moves, a format changes); bump ENGINE_VERSION freely. */
|
|
9
|
-
export const ENGINE_VERSION = '0.
|
|
9
|
+
export const ENGINE_VERSION = '0.3.0';
|
|
10
10
|
export const LAYOUT_VERSION = 1;
|
|
11
11
|
/** The marker key written into generated-file frontmatter / headers. */
|
|
12
12
|
export const GENERATED_BY = 'clan-engine';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nfinitmonkeys/clan-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Deterministic setup engine for Nfinit Monkeys clans \u2014 the single template + reconciliation authority behind the published CLIs and the internal fleet tooling. Idempotent, dry-run-by-default, non-destructive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|