@jbarket/loomai 0.9.2 → 0.11.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.
@@ -313,7 +313,7 @@ function canonical(v) {
313
313
  return JSON.stringify(v);
314
314
  }
315
315
  // ─── Store ───────────────────────────────────────────────────────────────────
316
- const PEOPLE_MIGRATION = 'people_cards';
316
+ const PEOPLE_MIGRATIONS = ['people_cards', 'person_revisions_actor'];
317
317
  function hasTable(db, table) {
318
318
  return db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
319
319
  }
@@ -339,19 +339,23 @@ function parseRow(raw) {
339
339
  export class PeopleStore {
340
340
  db;
341
341
  now;
342
+ actor;
342
343
  /** False on a read-only store whose tables do not exist yet. */
343
344
  migrated;
344
345
  constructor(config) {
345
346
  this.now = config.now ?? Date.now;
347
+ this.actor = config.actor ?? null;
346
348
  this.db = new BetterSqlite3(config.dbPath, config.readonly ? { readonly: true } : {});
347
349
  this.db.pragma(`busy_timeout = ${config.busyTimeoutMs ?? 5000}`);
348
350
  if (!config.readonly) {
349
351
  this.db.pragma('journal_mode = WAL');
350
352
  // Only this feature's migration: the people tables do not depend on the
351
353
  // memories schema, and a raw store must not half-run the others.
352
- const m = MIGRATIONS.find((x) => x.id === PEOPLE_MIGRATION);
353
- if (m.pending(this.db))
354
- m.run(this.db);
354
+ for (const id of PEOPLE_MIGRATIONS) {
355
+ const m = MIGRATIONS.find((x) => x.id === id);
356
+ if (m.pending(this.db))
357
+ m.run(this.db);
358
+ }
355
359
  }
356
360
  this.migrated = hasTable(this.db, 'people') && hasTable(this.db, 'person_revisions');
357
361
  }
@@ -368,8 +372,8 @@ export class PeopleStore {
368
372
  const snap = { ...prev, card: JSON.parse(prev.card_json) };
369
373
  delete snap.card_json;
370
374
  const id = Number(this.db
371
- .prepare('INSERT INTO person_revisions (slug, snapshot_json, op, replaced_at) VALUES (?, ?, ?, ?)')
372
- .run(prev.slug, JSON.stringify(snap), op, at).lastInsertRowid);
375
+ .prepare('INSERT INTO person_revisions (slug, snapshot_json, op, replaced_at, actor) VALUES (?, ?, ?, ?, ?)')
376
+ .run(prev.slug, JSON.stringify(snap), op, at, this.actor).lastInsertRowid);
373
377
  this.db
374
378
  .prepare(`DELETE FROM person_revisions WHERE slug = ? AND id NOT IN
375
379
  (SELECT id FROM person_revisions WHERE slug = ? ORDER BY id DESC LIMIT ?)`)
@@ -681,10 +685,41 @@ export class PeopleStore {
681
685
  fail(`slug: ${slugErr}`);
682
686
  if (!this.migrated)
683
687
  return [];
688
+ const hasActor = this.db.pragma('table_info(person_revisions)').some((c) => c.name === 'actor');
684
689
  return this.db
685
- .prepare('SELECT id, op, replaced_at FROM person_revisions WHERE slug = ? ORDER BY id DESC')
690
+ .prepare(`SELECT id, op, replaced_at${hasActor ? ', actor' : ''} FROM person_revisions WHERE slug = ? ORDER BY id DESC`)
686
691
  .all(slug);
687
692
  }
693
+ /**
694
+ * Put a card in place as is, keeping the row's timestamps and archive state
695
+ * (a migration, not an edit). Recorded as a 'split' revision when a row is
696
+ * replaced.
697
+ */
698
+ replaceCard(slug, card, keep) {
699
+ validateCard(card, null);
700
+ this.mutate(() => {
701
+ const existing = this.readRow(slug);
702
+ if (existing)
703
+ this.snapshot(existing, 'split', this.nowIso());
704
+ this.store({ slug, name: card.name, card_json: JSON.stringify(card), ...keep });
705
+ });
706
+ }
707
+ /** The raw stored rows (timestamps and archive state included), for a migration. */
708
+ rows() {
709
+ if (!this.migrated)
710
+ return [];
711
+ return this.db.prepare('SELECT slug, name, card_json, archived_at, archived_by, archive_reason, updated, synthesized_at FROM people ORDER BY slug').all()
712
+ .map((r) => ({ slug: r.slug, card: JSON.parse(r.card_json), updated: r.updated, synthesized_at: r.synthesized_at, archived_at: r.archived_at, archived_by: r.archived_by, archive_reason: r.archive_reason }));
713
+ }
714
+ /** This database's `user` memories about a person, newer than `since` (all of them when null). */
715
+ memoriesSince(slug, since) {
716
+ if (!hasTable(this.db, 'memories'))
717
+ return 0;
718
+ const nowIso = new Date(this.now()).toISOString();
719
+ return this.db.prepare(`SELECT COUNT(*) AS n FROM memories
720
+ WHERE category = 'user' AND archived = 0 AND json_extract(metadata, '$.person') = ?
721
+ AND (expires_at IS NULL OR expires_at > ?) AND (? IS NULL OR created > ?)`).get(slug, nowIso, since, since).n;
722
+ }
688
723
  /** A revision's snapshot (the whole previous row), for tests and inspection. */
689
724
  revisionSnapshot(id) {
690
725
  const rev = this.db.prepare('SELECT snapshot_json FROM person_revisions WHERE id = ?').get(id);
package/dist/cli/args.js CHANGED
@@ -5,9 +5,11 @@
5
5
  */
6
6
  import { resolve } from 'node:path';
7
7
  import { resolveDefaultContextPath } from '../config.js';
8
+ import { contextFromHome } from '../household.js';
8
9
  export function resolveEnv(flags, processEnv) {
9
10
  const contextDir = flags.contextDir ??
10
11
  processEnv.LOOM_CONTEXT_DIR ??
12
+ contextFromHome(processEnv) ??
11
13
  resolveDefaultContextPath(processEnv.HOME);
12
14
  return {
13
15
  contextDir: resolve(contextDir),
@@ -0,0 +1,3 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function runHousehold(argv: string[], io: IOStreams): Promise<number>;
3
+ export declare function runToken(argv: string[], io: IOStreams): Promise<number>;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * `loom household` and `loom token` (docs/specs/household.md). Local only:
3
+ * neither is a remote verb, so they never run through /cli. They read and
4
+ * write $LOOM_HOME (default ~/.config/loom) directly.
5
+ */
6
+ import { addAgent, agentDir, moveKnowledgeToCommons, createToken, householdHome, HouseholdError, initHousehold, loadHousehold, loadTokens, parseRole, revokeToken, } from '../household.js';
7
+ import { splitPeopleIntoCommons } from '../backends/people-household.js';
8
+ const HOUSEHOLD_USAGE = `Usage: loom household <command>
9
+
10
+ show The household at $LOOM_HOME: agents, the default, the owner
11
+ init <agent> [--owner <name>]
12
+ Make $LOOM_HOME/<agent> (a context dir) the first agent of a new
13
+ household: it moves to agents/<agent>, and a symlink stays behind
14
+ add <agent> Add an agent whose directory already exists under agents/
15
+ commons Move the default agent's knowledge.db to commons/knowledge.db and
16
+ split every agent's person cards (handles and facts to the commons
17
+ directory, the rest stays as its relationships). Once, with
18
+ every loom daemon on this household stopped
19
+
20
+ $LOOM_HOME defaults to ~/.config/loom.
21
+ `;
22
+ const TOKEN_USAGE = `Usage: loom token <command>
23
+
24
+ create (--agent <name> | --owner | --peer <name>) --label <text>
25
+ Mint a token. It's printed once; only its sha256 is kept
26
+ list Every token: label, role, created, last used, revoked
27
+ revoke <label> Revoke the live token with that label (takes effect on the next request)
28
+
29
+ Roles: agent:<name> is MCP and the CLI as that agent; owner sees everything and
30
+ the page; peer:<name> reaches commons knowledge only.
31
+ `;
32
+ function flag(args, name) {
33
+ const i = args.indexOf(name);
34
+ return i === -1 ? undefined : args[i + 1];
35
+ }
36
+ function need(io) {
37
+ const home = householdHome(io.env);
38
+ const h = loadHousehold(home);
39
+ if (!h)
40
+ io.stderr(`No household at ${home} (no household.json). Make one with: loom household init <agent>\n`);
41
+ return h;
42
+ }
43
+ export async function runHousehold(argv, io) {
44
+ const [sub, ...rest] = argv;
45
+ try {
46
+ if (sub === 'show') {
47
+ const h = need(io);
48
+ if (!h)
49
+ return 1;
50
+ io.stdout(`home ${h.home}\nagents ${h.agents.join(', ')}\ndefault ${h.default}\n${h.owner ? `owner ${h.owner}\n` : ''}`);
51
+ return 0;
52
+ }
53
+ if (sub === 'init') {
54
+ const agent = rest[0];
55
+ if (!agent || agent.startsWith('-')) {
56
+ io.stderr(HOUSEHOLD_USAGE);
57
+ return 2;
58
+ }
59
+ const h = initHousehold(householdHome(io.env), agent, { owner: flag(rest, '--owner') });
60
+ const cards = splitPeopleIntoCommons(h, agent, agentDir(h, agent));
61
+ io.stdout(`Household at ${h.home}: ${agent} moved to agents/${agent}; ${h.home}/${agent} is now a symlink to it. ${cards} person card${cards === 1 ? '' : 's'} split into the commons directory.\n`);
62
+ return 0;
63
+ }
64
+ if (sub === 'commons') {
65
+ const h = need(io);
66
+ if (!h)
67
+ return 1;
68
+ const moved = moveKnowledgeToCommons(h, h.default);
69
+ io.stdout(moved ? `Knowledge moved to ${moved}; every agent shares it now.\n` : `agents/${h.default} has no knowledge.db to move.\n`);
70
+ // Every agent's cards, so the daemon's "haven't been split" refusal can
71
+ // always be cleared here.
72
+ for (const a of h.agents) {
73
+ const cards = splitPeopleIntoCommons(h, a, agentDir(h, a));
74
+ io.stdout(`${a}: ${cards} person card${cards === 1 ? '' : 's'} split (handles and facts in the commons directory, the rest as ${a}'s relationships).\n`);
75
+ }
76
+ return 0;
77
+ }
78
+ if (sub === 'add') {
79
+ const h = need(io);
80
+ if (!h)
81
+ return 1;
82
+ const agent = rest[0];
83
+ if (!agent) {
84
+ io.stderr(HOUSEHOLD_USAGE);
85
+ return 2;
86
+ }
87
+ const next = addAgent(h, agent);
88
+ io.stdout(`Agents: ${next.agents.join(', ')}\n`);
89
+ return 0;
90
+ }
91
+ io.stderr(HOUSEHOLD_USAGE);
92
+ return sub === undefined || sub === '--help' || sub === '-h' ? 0 : 2;
93
+ }
94
+ catch (err) {
95
+ if (err instanceof HouseholdError) {
96
+ io.stderr(`${err.message}\n`);
97
+ return 1;
98
+ }
99
+ throw err;
100
+ }
101
+ }
102
+ export async function runToken(argv, io) {
103
+ const [sub, ...rest] = argv;
104
+ try {
105
+ if (sub === 'create') {
106
+ const h = need(io);
107
+ if (!h)
108
+ return 1;
109
+ const label = flag(rest, '--label');
110
+ const picked = [flag(rest, '--agent') && `agent:${flag(rest, '--agent')}`, rest.includes('--owner') && 'owner', flag(rest, '--peer') && `peer:${flag(rest, '--peer')}`].filter(Boolean);
111
+ const role = picked.length === 1 ? parseRole(picked[0]) : null;
112
+ if (!label || !role) {
113
+ io.stderr(TOKEN_USAGE);
114
+ return 2;
115
+ }
116
+ const { token, record } = createToken(h, { role, label });
117
+ // The token goes to stdout alone, so it can be piped straight into a file.
118
+ io.stderr(`Created ${record.role} token "${record.label}". It won't be shown again.\n`);
119
+ io.stdout(`${token}\n`);
120
+ return 0;
121
+ }
122
+ if (sub === 'list') {
123
+ const h = need(io);
124
+ if (!h)
125
+ return 1;
126
+ const tokens = loadTokens(h.home);
127
+ if (!tokens.length) {
128
+ io.stdout('No tokens yet.\n');
129
+ return 0;
130
+ }
131
+ for (const t of tokens) {
132
+ io.stdout(`${t.revoked ? 'revoked' : 'live '} ${t.role.padEnd(16)} ${t.label} (created ${t.created.slice(0, 10)}${t.last_used ? `, last used ${t.last_used.slice(0, 16).replace('T', ' ')}` : ', never used'}${t.revoked ? `, revoked ${t.revoked.slice(0, 10)}` : ''})\n`);
133
+ }
134
+ return 0;
135
+ }
136
+ if (sub === 'revoke') {
137
+ const h = need(io);
138
+ if (!h)
139
+ return 1;
140
+ const label = rest.join(' ').trim();
141
+ if (!label) {
142
+ io.stderr(TOKEN_USAGE);
143
+ return 2;
144
+ }
145
+ const t = revokeToken(h, label);
146
+ io.stdout(`Revoked ${t.role} token "${t.label}".\n`);
147
+ return 0;
148
+ }
149
+ io.stderr(TOKEN_USAGE);
150
+ return sub === undefined || sub === '--help' || sub === '-h' ? 0 : 2;
151
+ }
152
+ catch (err) {
153
+ if (err instanceof HouseholdError) {
154
+ io.stderr(`${err.message}\n`);
155
+ return 1;
156
+ }
157
+ throw err;
158
+ }
159
+ }
package/dist/cli/index.js CHANGED
@@ -32,6 +32,8 @@ Commands:
32
32
  doctor Probe the loom environment (read-only)
33
33
  migrate Apply pending schema migrations to memories.db
34
34
  pieces List installed pieces (loom-<name> on PATH)
35
+ household The household: agents and the default (docs/specs/household.md)
36
+ token Mint, list and revoke the household's tokens
35
37
 
36
38
  Pieces: \`loom <name> ...\` runs an installed extension, loom-<name> on PATH
37
39
  (e.g. loom tasks list). Core commands always win. See docs/extensions.md.
@@ -198,6 +200,14 @@ async function dispatch(sub, rest, io) {
198
200
  const { runPiecesCommand } = await import('./pieces.js');
199
201
  return runPiecesCommand(rest, io);
200
202
  }
203
+ case 'household': {
204
+ const { runHousehold } = await import('./household.js');
205
+ return runHousehold(rest, io);
206
+ }
207
+ case 'token': {
208
+ const { runToken } = await import('./household.js');
209
+ return runToken(rest, io);
210
+ }
201
211
  default:
202
212
  io.stderr(`Subcommand not implemented yet: ${sub}\n`);
203
213
  return 2;
package/dist/cli/serve.js CHANGED
@@ -17,6 +17,7 @@ import { parseArgs } from 'node:util';
17
17
  import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
18
18
  import { createLoomServer } from '../server.js';
19
19
  import { createKnowledgeOnlyServer } from '../transport/knowledge-server.js';
20
+ import { householdOf } from '../household.js';
20
21
  import { startHttpServer } from '../transport/http-server.js';
21
22
  import { resolveContextDir } from '../config.js';
22
23
  const USAGE = `Usage: loom serve [options]
@@ -140,9 +141,15 @@ export async function run(argv, io) {
140
141
  const host = values.host ?? process.env.LOOM_HTTP_HOST ?? '127.0.0.1';
141
142
  const port = Number(values.port ?? process.env.LOOM_HTTP_PORT ?? 8787);
142
143
  const token = process.env.LOOM_BEARER_TOKEN || undefined;
143
- const handle = await startHttpServer({ contextDir, host, port, token, gapsDir });
144
+ // A context dir at <home>/agents/<name> next to a household.json serves
145
+ // the whole household (docs/specs/household.md); anything else is a
146
+ // household of one, exactly as before.
147
+ const found = householdOf(contextDir);
148
+ const handle = await startHttpServer({ contextDir, host, port, token, gapsDir, household: found?.household });
144
149
  io.stderr(`loom: HTTP MCP daemon on http://${handle.host}:${handle.port} ` +
145
- `(${token ? 'bearer-gated' : 'open — network is the boundary'})\n`);
150
+ (found
151
+ ? `(household of ${found.household.agents.join(', ')}; tokens in ${found.household.home}/tokens.json)\n`
152
+ : `(${token ? 'bearer-gated' : 'open — network is the boundary'})\n`));
146
153
  // Hold open until the process is signalled.
147
154
  await new Promise(() => { });
148
155
  return 0;
@@ -3,5 +3,5 @@
3
3
  * (src/cli/index.ts) and the CLI-vs-MCP routing gate (src/index.ts) so
4
4
  * the set never drifts between entry points.
5
5
  */
6
- export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate', 'pieces'];
6
+ export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate', 'pieces', 'household', 'token'];
7
7
  export type Subcommand = typeof SUBCOMMANDS[number];
@@ -7,5 +7,5 @@ export const SUBCOMMANDS = [
7
7
  'wake', 'recall', 'remember', 'forget', 'update',
8
8
  'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve',
9
9
  'inject', 'harness',
10
- 'install', 'doctor', 'migrate', 'pieces',
10
+ 'install', 'doctor', 'migrate', 'pieces', 'household', 'token',
11
11
  ];
package/dist/config.d.ts CHANGED
@@ -24,6 +24,11 @@ export declare function assertContextBootable(contextDir: string, defaultPath?:
24
24
  */
25
25
  export declare function resolveRepoRoot(): string;
26
26
  export declare function resolveSqliteDbPath(contextDir: string): string;
27
+ /**
28
+ * Knowledge is facts about the world. In a household it's the commons, shared
29
+ * by every agent (docs/specs/household.md); otherwise it lives in the
30
+ * agent's own context dir, as it always has.
31
+ */
27
32
  export declare function resolveKnowledgeDbPath(contextDir: string): string;
28
33
  /**
29
34
  * Resolve the acting identity's name for actor attribution (knowledge
package/dist/config.js CHANGED
@@ -18,6 +18,7 @@ import { resolve, dirname, basename } from 'node:path';
18
18
  import { homedir } from 'node:os';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { existsSync, readFileSync, writeFileSync, statSync, realpathSync } from 'node:fs';
21
+ import { commonsKnowledgePath, contextFromHome, householdOf } from './household.js';
21
22
  /**
22
23
  * The single canonical helper for the default context path. Every call
23
24
  * site that needs `~/.config/loom/default` must go through this —
@@ -34,6 +35,10 @@ export function resolveContextDir() {
34
35
  if (argIdx !== -1 && process.argv[argIdx + 1]) {
35
36
  return resolve(process.argv[argIdx + 1]);
36
37
  }
38
+ // LOOM_HOME with a household.json: the agent LOOM_AGENT names, or its default.
39
+ const fromHome = contextFromHome();
40
+ if (fromHome)
41
+ return fromHome;
37
42
  return resolveDefaultContextPath();
38
43
  }
39
44
  /**
@@ -67,8 +72,18 @@ export function resolveRepoRoot() {
67
72
  export function resolveSqliteDbPath(contextDir) {
68
73
  return process.env.LOOM_SQLITE_DB_PATH ?? resolve(contextDir, 'memories.db');
69
74
  }
75
+ /**
76
+ * Knowledge is facts about the world. In a household it's the commons, shared
77
+ * by every agent (docs/specs/household.md); otherwise it lives in the
78
+ * agent's own context dir, as it always has.
79
+ */
70
80
  export function resolveKnowledgeDbPath(contextDir) {
71
- return process.env.LOOM_KNOWLEDGE_DB_PATH ?? resolve(contextDir, 'knowledge.db');
81
+ if (process.env.LOOM_KNOWLEDGE_DB_PATH)
82
+ return process.env.LOOM_KNOWLEDGE_DB_PATH;
83
+ const hh = householdOf(contextDir);
84
+ if (hh)
85
+ return commonsKnowledgePath(hh.household);
86
+ return resolve(contextDir, 'knowledge.db');
72
87
  }
73
88
  /**
74
89
  * Resolve the acting identity's name for actor attribution (knowledge
@@ -0,0 +1,117 @@
1
+ export declare const AGENT_NAME: RegExp;
2
+ export interface Household {
3
+ /** $LOOM_HOME: where household.json lives. */
4
+ home: string;
5
+ agents: string[];
6
+ /** The agent the owner's token acts as when it names none. */
7
+ default: string;
8
+ owner?: string;
9
+ }
10
+ export type Role = {
11
+ kind: 'agent';
12
+ agent: string;
13
+ } | {
14
+ kind: 'owner';
15
+ } | {
16
+ kind: 'peer';
17
+ name: string;
18
+ };
19
+ export interface TokenRecord {
20
+ label: string;
21
+ /** `agent:<name>`, `owner` or `peer:<name>`. */
22
+ role: string;
23
+ /** sha256 of the token, hex. The token itself is never stored. */
24
+ sha256: string;
25
+ created: string;
26
+ last_used?: string;
27
+ revoked?: string;
28
+ }
29
+ export declare class HouseholdError extends Error {
30
+ }
31
+ /** $LOOM_HOME, else ~/.config/loom. */
32
+ export declare function householdHome(env?: NodeJS.ProcessEnv): string;
33
+ export declare function agentDir(h: Household, agent: string): string;
34
+ /** The household at `home`, or null when there's none. Throws on a malformed file. */
35
+ export declare function loadHousehold(home: string): Household | null;
36
+ /**
37
+ * The household a context dir belongs to: one whose `agents/<name>` is this
38
+ * dir (after resolving symlinks, so an old `~/.config/loom/art` link still
39
+ * finds it). Null for a plain context dir.
40
+ */
41
+ export declare function householdOf(contextDir: string): {
42
+ household: Household;
43
+ agent: string;
44
+ } | null;
45
+ export declare function parseRole(s: string): Role | null;
46
+ export declare function roleString(r: Role): string;
47
+ export declare function loadTokens(home: string): TokenRecord[];
48
+ /** Mint a token. Returns it once; only its sha256 is kept. */
49
+ export declare function createToken(h: Household, input: {
50
+ role: Role;
51
+ label: string;
52
+ }, now?: Date): {
53
+ token: string;
54
+ record: TokenRecord;
55
+ };
56
+ export declare function revokeToken(h: Household, label: string, now?: Date): TokenRecord;
57
+ /**
58
+ * Resolves presented tokens to roles. Reads tokens.json when it changes (so
59
+ * a revoke takes effect on the next request), compares digests in constant
60
+ * time, and records when a token was last used, at most once a minute.
61
+ */
62
+ export declare class TokenResolver {
63
+ private readonly h;
64
+ private cache;
65
+ private lastTouch;
66
+ constructor(h: Household);
67
+ private current;
68
+ /**
69
+ * The role for a presented token, or null. Revoked tokens and roles naming a
70
+ * missing agent resolve to null. Throws HouseholdError when tokens.json is
71
+ * unreadable or untrusted: the caller fails closed.
72
+ */
73
+ resolve(presented: string): Role | null;
74
+ /** Whether a live owner token has this sha256 (the page's sessions check this, so revoking ends them). */
75
+ ownerDigestLive(digestHex: string): boolean;
76
+ private touch;
77
+ }
78
+ /**
79
+ * Turn a plain context dir into the first agent of a new household: move
80
+ * `<home>/<name>` to `<home>/agents/<name>`, leave a symlink at the old path
81
+ * so anything that still names it keeps working, and write household.json.
82
+ */
83
+ export declare function initHousehold(home: string, agent: string, opts?: {
84
+ owner?: string;
85
+ }): Household;
86
+ /**
87
+ * An agent's directory must be a real directory, not a symlink: `agents/mark
88
+ * -> art` would make two agents share one stack.
89
+ */
90
+ export declare function assertRealAgentDir(h: Household, agent: string): void;
91
+ /** Add an agent to a household (its directory must already exist under agents/). */
92
+ export declare function addAgent(h: Household, agent: string): Household;
93
+ /**
94
+ * The context dir a household names when nothing else does: with LOOM_HOME
95
+ * set and a household.json there, `agents/<LOOM_AGENT or the default>`.
96
+ * Undefined otherwise. Only an explicit LOOM_HOME is consulted here, so a
97
+ * stray household.json never changes where an unconfigured process looks.
98
+ * (A daemon is different: one whose context dir already is
99
+ * `<home>/agents/<name>` serves that household; see householdOf.)
100
+ */
101
+ export declare function contextFromHome(env?: NodeJS.ProcessEnv): string | undefined;
102
+ export declare function commonsDir(h: Household): string;
103
+ /** The household's shared knowledge store. */
104
+ export declare function commonsKnowledgePath(h: Household): string;
105
+ /**
106
+ * Why this household can't serve yet, or null. An agent's own knowledge.db
107
+ * beside the commons would quietly fork the knowledge base, so both "only the
108
+ * agent's" (not moved yet) and "both" (needs a merge) are refusals.
109
+ */
110
+ export declare function commonsProblem(h: Household): string | null;
111
+ /**
112
+ * Move one agent's knowledge.db to the commons: checkpoint its WAL into the
113
+ * main file, then rename it (and any sidecars). Refuses when the commons
114
+ * already has a knowledge store. Run with every daemon on this household
115
+ * stopped.
116
+ */
117
+ export declare function moveKnowledgeToCommons(h: Household, agent: string): string | null;